719 lines
42 KiB
Markdown
719 lines
42 KiB
Markdown
# Chapter 21 — Building Discord bots on Bare OS
|
||
|
||
Guest scripts have no `import` or `require`. Discord bots on Bare OS therefore do **not** `import('discord.js')`. The booter vendors **[bare-discord-js](../packages/bare-os-booter/vendor/bare-discord-js/README.md)** (official **discord.js** 14 on Bare) and attaches it as **`ctx.bare.discordJS`**. You construct a `Client` from that object, the same way you would on Node, then keep the process alive until logout, Ctrl+C, or `client.destroy()`.
|
||
|
||
This chapter is the complete how-to: Developer Portal setup, secrets, intents, a drop-in guest script, the stock **`/bin/discord-bot`** slash surface, the **`bare-os-discord`** initd unit, access control, packing notes, tests, and failure modes.
|
||
|
||
Canonical env inventory: [Environment and POSIX appendix](../docs/reference/environment-and-posix-appendix.md) (`DISCORD_*`, `BARE_OS_DISCORD*`). Command reference: **`man discord-bot`**. Minimal example: [`examples/discord-ping-pong/`](../examples/discord-ping-pong/).
|
||
|
||
> **Important**
|
||
> Use the **Bot token** from Developer Portal → Bot → Reset Token. The OAuth2 **client secret** is a different string and will fail gateway auth (`4004` / `TokenInvalid`). Never put a token in a commit, a chat log, or `console.log`.
|
||
|
||
---
|
||
|
||
## On this page
|
||
|
||
- [Who this is for](#who-this-is-for)
|
||
- [How Discord fits the two runtimes](#how-discord-fits-the-two-runtimes)
|
||
- [What you can run today](#what-you-can-run-today)
|
||
- [Developer Portal checklist](#developer-portal-checklist)
|
||
- [Secrets, `.env`, and token load order](#secrets-env-and-token-load-order)
|
||
- [Access control (`DISCORD_ID_WHITELIST`)](#access-control-discord_id_whitelist)
|
||
- [Gateway intents](#gateway-intents)
|
||
- [Minimal guest bot](#minimal-guest-bot)
|
||
- [Slash commands](#slash-commands)
|
||
- [Stock `/bin/discord-bot`](#stock-bindiscord-bot)
|
||
- [Settings editor (`/settings`)](#settings-editor-settings)
|
||
- [Run as an initd unit](#run-as-an-initd-unit)
|
||
- [Plugin system (`~/.discord/plugins`)](#plugin-system-discordplugins)
|
||
- [Extending the stock catalog](#extending-the-stock-catalog)
|
||
- [Shipping your own `/bin` bot](#shipping-your-own-bin-bot)
|
||
- [Host, Pear, and packed standalone](#host-pear-and-packed-standalone)
|
||
- [Testing](#testing)
|
||
- [Troubleshooting](#troubleshooting)
|
||
- [Security](#security)
|
||
- [File map](#file-map)
|
||
- [See also](#see-also)
|
||
|
||
---
|
||
|
||
## Who this is for
|
||
|
||
- You want a Discord bot that talks to **this guest session** (`ctx.vfs`, `ctx.execLine`, `systemctl`).
|
||
- You want a **custom** bot (your own slash commands) as a file on the personal drive.
|
||
- You are changing the stock catalog, the initd unit, or the packed WebSocket path.
|
||
|
||
Read [Chapter 1 — Two runtimes](01-two-runtimes-host-vs-image.md) and [Chapter 5 — Modules](05-modules-and-imports.md) first if `import` in a guest script is still surprising.
|
||
|
||
---
|
||
|
||
## How Discord fits the two runtimes
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph portal [Discord]
|
||
API[REST and gateway]
|
||
end
|
||
subgraph host [Booter host]
|
||
Load["loadBareDiscordJs"]
|
||
WS["WHATWG bare-ws wrapper"]
|
||
Inject["applyDiscordHostEnvToShellEnv"]
|
||
end
|
||
subgraph guest [In-image]
|
||
Ctx["ctx.bare.discordJS"]
|
||
Bin["/bin/discord-bot"]
|
||
Script["~/my-bot.js"]
|
||
Unit["bare-os-discord"]
|
||
end
|
||
Load --> Ctx
|
||
WS --> Load
|
||
Inject --> Bin
|
||
Inject --> Unit
|
||
Ctx --> Bin
|
||
Ctx --> Script
|
||
Ctx --> Unit
|
||
Bin --> API
|
||
Script --> API
|
||
Unit --> API
|
||
```
|
||
|
||
In prose: the **host** (Pear, Node, or a packed `bare-os-booter`) loads vendored **discord.js** and copies token-related env into the session. **In-image** code only sees **`ctx.bare.discordJS`**. Drive-resident JS is an **`AsyncFunction`** body, so `import` / `require` are syntax errors. Shared helpers are either inlined or concatenated at coreutils build time ([Chapter 5](05-modules-and-imports.md)).
|
||
|
||
`ctx.bare.discordJS` is **not** a `ctx.bare` manifest row. It is attached by [`bare-discord-js-loader.js`](../packages/bare-os-booter/lib/bare-discord-js-loader.js) after the host `ctx.bare` merge. Disable the load with **`BARE_OS_DISCORD=0`**. Disable all of `ctx.bare` with **`BARE_OS_BARE_MODULES=0`**.
|
||
|
||
Useful exports on that object (same names as discord.js 14):
|
||
|
||
| Key | Use |
|
||
| --- | --- |
|
||
| `Client` | Gateway client |
|
||
| `GatewayIntentBits` | Intent bitmask |
|
||
| `Events` | `ClientReady`, `InteractionCreate`, `MessageCreate`, `ShardError`, … |
|
||
| `REST` / `Routes` | Slash-command registration and REST probes |
|
||
| `SlashCommandBuilder` | Command JSON for `Routes.applicationCommands` |
|
||
|
||
If `ctx.bare.discordJS` is missing, check `ctx.env.BARE_OS_DISCORD_LOAD_ERROR` (set when the vendor load failed) and that you are not on a host that skipped Discord.
|
||
|
||
---
|
||
|
||
## What you can run today
|
||
|
||
| Path | When to use | How it starts |
|
||
| --- | --- | --- |
|
||
| **`init-discord`** | First-time setup: `.env` + hello plugin | Interactive prompts or `--yes --token …` |
|
||
| **Stock `/bin/discord-bot`** | Full Bare OS slash surface (`/bare`, `/sys`, `/fs`, …) | Foreground in the guest shell |
|
||
| **`bare-os-discord` unit** | Same catalog, background after identity unlock | `systemctl start` when `~/.discord/.env` exists |
|
||
| **Guest script** (`~/my-bot.js`) | Your own commands; copy the ping-pong example | `./my-bot.js` or `my-bot.js` in `$PWD` |
|
||
| **New `/bin` name** | Ship a first-party utility | Coreutils `src/` + optional preamble ([Chapter 16](16-how-to-add-bin-utility.md)) |
|
||
|
||
The stock binary and the initd unit share one guest-safe catalog: [`bare-os-discord-commands-guest.cjs`](../packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs). The coreutils build **prepends** that file to [`src/discord-bot.js`](../packages/bare-os-coreutils/src/discord-bot.js). The unit **statically imports** the `.cjs` so **bare-pack** rewrites the binding into `app.bundle` (`createRequire` cannot resolve siblings under `bare:/app.bundle/`).
|
||
|
||
---
|
||
|
||
## Developer Portal checklist
|
||
|
||
1. [Discord Developer Portal](https://discord.com/developers/applications) → New Application.
|
||
2. **Bot** → Reset Token → copy once into `~/.discord/.env` (guest) or a host `.env` you will not commit.
|
||
3. **Privileged Gateway Intents** — leave **Message Content Intent** **off** unless you need channel text (`ping` → `pong`). The stock client requests **Guilds only** by default. Requesting Message Content while the portal toggle is off closes the gateway with **4014**.
|
||
4. **Installation** — enable **User Install** *and* **Guild Install**. User Install scope is **`applications.commands`** only (profile app). Guild Install scopes are **`bot`** + **`applications.commands`**. The stock bot PATCHes this on login; you can also flip it in the portal. Pick guild permissions you actually need (Send Messages, Use Slash Commands, Embed Links, Attach Files).
|
||
5. Copy the **guild (server) id** and your **user id** (Discord Settings → Advanced → Developer Mode, then right-click → Copy ID). Guild id still makes slash registration instant on the home server. User id **must** go on **`DISCORD_ID_WHITELIST`** — user-install / DM commands deny everyone when the list is empty.
|
||
6. After the bot is online, open the logged **add to your Discord profile** URL (`https://discord.com/oauth2/authorize?client_id=…`) and choose **Add to My Apps**. Slash commands then work in DMs and any server from your user profile. Only whitelist members can actually run them.
|
||
|
||
> **Tip**
|
||
> `discord-bot --check --env ~/.discord/.env` verifies that `ctx.bare.discordJS` exists and a token can be resolved. It does **not** connect to the gateway.
|
||
|
||
---
|
||
|
||
## Secrets, `.env`, and token load order
|
||
|
||
Preferred setup:
|
||
|
||
```
|
||
init-discord
|
||
```
|
||
|
||
That asks for **token**, **guild id**, and **whitelist**, creates `~/.discord/` and `~/.discord/plugins/`, writes `~/.discord/.env`, and installs `/hello` from [`examples/discord-plugins/hello.json`](../examples/discord-plugins/hello.json). Flags (`--token`, `--guild`, `--whitelist`, `--yes`) work for scripts. The full token is never printed.
|
||
|
||
Or write the file yourself:
|
||
|
||
```bash
|
||
# ~/.discord/.env (VFS path on the personal drive)
|
||
DISCORD_TOKEN=your-bot-token
|
||
DISCORD_GUILD_ID=123456789012345678
|
||
DISCORD_ID_WHITELIST=123456789012345678
|
||
```
|
||
|
||
`export KEY=value`, quotes, `#` comments, and a UTF-8 BOM are accepted. The token is normalized (trim, strip BOM / zero-width spaces). A trailing newline in the file is fine; a token pasted with invisible characters is not — reset the token if login returns **4004**.
|
||
|
||
### Foreground `/bin/discord-bot` (first match wins)
|
||
|
||
1. `--token TOKEN`
|
||
2. `ctx.env.DISCORD_TOKEN` (already in the session, including a **host** copy)
|
||
3. `--env` / `--env-file PATH` (guest VFS)
|
||
4. `DISCORD_ENV_FILE` / `BARE_OS_DISCORD_ENV_FILE` (guest VFS)
|
||
5. `~/.discord/.env`, `~/.discord.env`, `~/discord.env`, `./.env`, `~/.env`
|
||
|
||
`DISCORD_GUILD_ID` and `DISCORD_ID_WHITELIST` are also read from those files (session env wins if already set). `--guild` / `--guild-id` override the guild for slash registration.
|
||
|
||
### Host injection
|
||
|
||
When the **booter process** has `DISCORD_TOKEN`, `DISCORD_ID_WHITELIST`, `DISCORD_GUILD_ID`, or `DISCORD_ENV_FILE` pointing at a **host filesystem** path, [`applyDiscordHostEnvToShellEnv`](../packages/bare-os-booter/lib/bare-discord-js-loader.js) copies them into the guest session **before** `/boot/init.js`. The guest then does not need host paths. Existing session values are not overwritten.
|
||
|
||
### Initd unit
|
||
|
||
The **`bare-os-discord`** unit requires `DISCORD_TOKEN=` (or `BOT_TOKEN=`) in **`~/.discord/.env`** — that file is the gate for whether `systemctl` lists the unit at all. After the token is found it also copies **`DISCORD_ID_WHITELIST`**, **`DISCORD_GUILD_ID`**, and **`DISCORD_USER_INSTALL`** from that file, then from `~/.discord.env`, `~/discord.env`, **`~/.env`**, and `./.env` (first non-empty value wins). Values are written onto both `ctx.env` and `ctx.vfs.env` so user-install checks see them. No `--env` flag.
|
||
|
||
Do not pass `--token` on a recorded shell line. Prefer the `.env` file (mode `600` if your VFS exposes modes).
|
||
|
||
---
|
||
|
||
## Access control (`DISCORD_ID_WHITELIST`)
|
||
|
||
Comma-separated Discord **user** snowflakes. Spaces around commas are ignored. A pasted mention (`<@123>` / `<@!123>`) is accepted.
|
||
|
||
| Value | Effect |
|
||
| --- | --- |
|
||
| Unset or empty | Guild-installed commands in a server keep the old “anyone may start” rule. **User-install, Bot DM, and private-channel** interactions are **always denied**. |
|
||
| One or more ids | Only those users may use slash commands, autocomplete, buttons, selects, modals, and channel `ping` — in every install context (guild, profile app, DM) |
|
||
| User not on a non-empty list | Denied. Replies are **ephemeral** and do **not** update the original message. Channel `ping` gets a short deny message |
|
||
|
||
Enforced in `discordDispatchInteraction` for **every** interaction type (slash, autocomplete, button, select, modal) and in the message/`/ping` fallbacks. User-install / DM / private-channel traffic is identified via `authorizingIntegrationOwners` and `interaction.context` and **cannot** skip the list. A whitelist member cannot drive another operator’s HUD — the clicker must match the user who posted the components. Startup logs `DISCORD_ID_WHITELIST active (N user id(s))` without printing the ids. User-install with an empty list logs that those surfaces are denied.
|
||
|
||
Set **`DISCORD_USER_INSTALL=0`** (or **`--no-user-install`**) for a guild-only bot. Default is on: commands are registered **globally** with `integration_types` `[guild, user]` and `contexts` `[guild, bot_dm, private_channel]`.
|
||
|
||
The stock `/sys env` handler redacts keys matching `TOKEN|SECRET|PASSWORD|…`. It still prints `DISCORD_ID_WHITELIST` (ids are not a bot token). Combine the allowlist with the `/r` and `/fs` allowlists below — a Discord user on the list can still reach a lot of the guest.
|
||
|
||
---
|
||
|
||
## Gateway intents
|
||
|
||
```js
|
||
const intents = [dj.GatewayIntentBits.Guilds]
|
||
if (wantsMessageContent) intents.push(dj.GatewayIntentBits.MessageContent)
|
||
```
|
||
|
||
| Need | Intent | How to enable |
|
||
| --- | --- | --- |
|
||
| Slash commands (`/ping`, `/bare`, …) | **Guilds** only | Default |
|
||
| Channel message `ping` → `pong` | **Message Content** (privileged) | Portal toggle **and** `--message-content` or `DISCORD_MESSAGE_CONTENT=1` |
|
||
|
||
Requesting Message Content without the portal toggle used to **hang** `client.login()` forever: discord.js swallowed **4014** on `ShardError` / `ShardDisconnect` and never rejected the login promise. Stock `/bin/discord-bot` now fails closed (default login timeout **45000** ms, override `--login-timeout` / `DISCORD_LOGIN_TIMEOUT_MS`). Do not copy an older “wait forever on `login()`” snippet.
|
||
|
||
Recommend identify properties so the gateway sees a stable client:
|
||
|
||
```js
|
||
ws: {
|
||
identifyProperties: {
|
||
os: (typeof process !== 'undefined' && process.platform) || 'darwin',
|
||
browser: 'bare-os',
|
||
device: 'bare-os'
|
||
}
|
||
}
|
||
```
|
||
|
||
Fatal close codes the stock bot treats as login failure: **4014** (disallowed intents), **4013** (invalid intents), **4004** (authentication failed).
|
||
|
||
---
|
||
|
||
## Minimal guest bot
|
||
|
||
Save as `~/discord-ping.js` on the **personal** drive ([Chapter 4](04-user-scripts-and-path.md)). No `import`. Run: `discord-ping.js --env ~/.discord/.env`.
|
||
|
||
```js
|
||
async function run(ctx, argv) {
|
||
const dj = ctx.bare && ctx.bare.discordJS
|
||
if (!dj || typeof dj.Client !== 'function') {
|
||
ctx.console.error('ctx.bare.discordJS is unavailable')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
const token = String((ctx.env && ctx.env.DISCORD_TOKEN) || '').trim()
|
||
if (!token) {
|
||
ctx.console.error('set DISCORD_TOKEN or --env ~/.discord/.env')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
const client = new dj.Client({
|
||
intents: [dj.GatewayIntentBits.Guilds]
|
||
})
|
||
|
||
client.once(dj.Events.ClientReady, function (ready) {
|
||
ctx.console.log('Logged in as ' + ready.user.tag)
|
||
})
|
||
|
||
client.on(dj.Events.InteractionCreate, async function (interaction) {
|
||
if (!interaction.isChatInputCommand || !interaction.isChatInputCommand()) {
|
||
return
|
||
}
|
||
if (interaction.commandName !== 'ping') return
|
||
await interaction.reply({ content: 'pong' })
|
||
})
|
||
|
||
await client.login(token)
|
||
|
||
await new Promise(function (resolve) {
|
||
function shutdown() {
|
||
Promise.resolve(client.destroy()).catch(function () {}).then(resolve)
|
||
}
|
||
if (typeof ctx.registerKernelShutdownHook === 'function') {
|
||
ctx.registerKernelShutdownHook(shutdown)
|
||
}
|
||
if (dj.Events.Invalidated) client.once(dj.Events.Invalidated, shutdown)
|
||
})
|
||
}
|
||
```
|
||
|
||
For a fuller guest script (`.env` parser, `--check`, REST `/gateway/bot` probe, login timeout, Ctrl+C), copy [`examples/discord-ping-pong/index.js`](../examples/discord-ping-pong/index.js). To register `/ping` immediately, pass `--guild <id>` or set `DISCORD_GUILD_ID`.
|
||
|
||
Foreground stock bot: **Ctrl+C** sets exit **130** and destroys the client (`SIGINT`, `SIGTERM`, and `bare-os:host-sigint`).
|
||
|
||
---
|
||
|
||
## Slash commands
|
||
|
||
Register **after** `Events.ClientReady` with the REST client:
|
||
|
||
```js
|
||
const rest = new dj.REST().setToken(token)
|
||
const body = [new dj.SlashCommandBuilder().setName('ping').setDescription('Replies with pong.').toJSON()]
|
||
if (guildId) {
|
||
await rest.put(dj.Routes.applicationGuildCommands(appId, guildId), { body: body })
|
||
} else {
|
||
await rest.put(dj.Routes.applicationCommands(appId), { body: body })
|
||
}
|
||
```
|
||
|
||
- **Guild** registration is visible in seconds. Use it while iterating.
|
||
- **Global** registration can take up to about an hour.
|
||
- Discord rejects replies longer than **2000** characters of **message content**. Embeds have a separate budget: description **4096**, field value **1024**, **25** fields, and **6000** characters across the whole embed. The stock catalog packs every embed to those caps (line-aware clip, never mid-fence), and paginates long file / journal / man / run / `systemctl` output with **← Prev / Next →** when it does not fit on one card. Short output is shown in full. Token-shaped strings are redacted.
|
||
- Prefer **`ephemeral: true`** for errors, denies, and anything that should not stay in the channel.
|
||
- `interaction.reply` can be used once. After that, `followUp` or `editReply` (if you deferred).
|
||
|
||
Probe REST before `login()` if you want a fast token check:
|
||
|
||
```js
|
||
const gw = await new dj.REST({ timeout: 15000 }).setToken(token).get(dj.Routes.gatewayBot())
|
||
```
|
||
|
||
HTTP **401** here means a bad token, not a gateway hang.
|
||
|
||
---
|
||
|
||
## Stock `/bin/discord-bot`
|
||
|
||
```
|
||
discord-bot --check --env ~/.discord/.env
|
||
discord-bot --env ~/.discord/.env --guild 123456789012345678
|
||
discord-bot --help
|
||
```
|
||
|
||
| Flag | Meaning |
|
||
| --- | --- |
|
||
| `--env PATH` | Guest VFS `.env` (`--env-file` is the same) |
|
||
| `--token TOKEN` | Bot token (prefer a file) |
|
||
| `--guild ID` | Instant guild slash registration |
|
||
| `--check` | Resolve token + `discordJS`; do not login |
|
||
| `--debug` | Print discord.js debug (`DISCORD_DEBUG=1`) |
|
||
| `--message-content` | Request Message Content Intent |
|
||
| `--login-timeout MS` | Gateway ready deadline (default 45000) |
|
||
|
||
### Slash map
|
||
|
||
| Command | Subcommands / args | Notes |
|
||
| --- | --- | --- |
|
||
| `/panel` | — | Control panel. Global destinations live behind a single **Menu** button on other replies (expand / Hide menu) |
|
||
| `/bare` | `ping` `about` `help` `status` `whoami` `hostname` `date` `uptime` `motd` `uname` | **Logged-in** session (not guest). Embed + nav buttons |
|
||
| `/sys` | `df` `mem` `ps` `env` `doctor` `features` `rlimits` | Parsed `/proc` JSON → embed fields (RAM, units, limits) |
|
||
| `/svc` | `list` `status` `start` `stop` `restart` `logs` | Unit **autocomplete**; select menu + start/stop buttons |
|
||
| `/fs` | `ls` `cat` `stat` `head` | Read-only VFS; see path rules |
|
||
| `/net` | `peers` / `swarm` / summary | Swarm + `net_summary.json` |
|
||
| `/man` | `page` | Runs `man <page>` |
|
||
| `/edit` | `path` | Modal editor for `~/` and `/tmp` (≤20 000 chars, 5 fields). Save writes via VFS |
|
||
| `/create` | `path` | Create a **new** file: path autocomplete, select menu, or custom path, then a contents modal. Refuses to overwrite (offers `/edit`) |
|
||
| `/upload` | `file` **(required)**, optional `path` | Attach a file (Discord CDN). The bot then runs guest **`wget -O dest URL`** into the **`/r` cwd** (or `path` if given: a directory gets `path/filename`, a file path is `-O`). Writable trees only (`~/`, `/tmp`). 60s timeout. If `BARE_OS_HTTP_ALLOWLIST` is set, include `cdn.discordapp.com` and `media.discordapp.net` |
|
||
| `/hdms` | `list` `help` `health` `hints` `show` `create` `add` `remove` `invite` `pair` | Full Hyperdrive manager (`ctx.runHdms` / `/bin/hdms`). Default **list** is an interactive HUD: pick a drive, show, browse `/mnt/<label>`, invite (pair / RW), remove (confirm). **create** / **add** / **pair** open modals when args are omitted. **invite** prints the Autopass z32. **pair** defers (can wait up to the CLI timeout). **health** / **hints** read `/proc/bare_os/hdms_*.json` (no secrets). Requires **unlocked** identity. Writer secrets in `show` are redacted. `/mnt/…` is now allowed for `/fs` and `/files` (VFS still enforces RO mounts) |
|
||
| `/holesail` | `list` `help` `status` `path` `logs` `show` `url` `add` `edit` `remove` `start` `stop` `restart` `enable` `disable` `service` | Full Holesail manager (`ctx.bareOsRunHolesailCli` / `/bin/holesail`). Default **list** is an interactive HUD: pick a tunnel, show, start/stop, enable/disable, edit (modal), remove (confirm). **Add server** / **Add client** open modals. No selection exposes **start/stop/restart** for the **`bare-holesail`** unit (`systemctl`). **show** / **url** print the shareable **`hs://`** key; **seed** material is never shown (64-hex redacted). **service** `start` `stop` `restart` `status` the initd unit. Live tunnel ops defer. |
|
||
| `/agent` | `list` `help` `status` `config` `models` `ask` `skills` `todos` `plan` `hooks` `history` `recap` `undo` `rewind` `compact` `export` `remember` `reset` `stop` | Full guest `/bin/agent` harness from Discord when **configured**. Ready means: `~/.agent/config.json` exists **and** (QVAC model + `ctx.bareOsQvacAvailable`, or REST + `rest_api_key` + `rest_base_url`). Default **list** is an HUD: Ask (modal), Stop, Reset, Models, Recap, Compact, plus an **Inspect / session** menu (skills, todos, plan, history, hooks, undo, rewind, export, remember). **ask** `prompt` (required) runs one turn via `execLine` with `BARE_OS_AGENT_DISCORD=1` (Discord-flavored Markdown, shared `~/.agent/history.json`, up to 13 minutes, live progress from `progress.txt`). Optional ask flags: `new`, `plan`, `auto`, `compact`, `max_turns`, `model`. Inspect subs call the same CLI as TTY (`agent skills`, `agent remember …`, …). Unconfigured HUD explains `agent --setup`. API keys never appear in Discord. Whitelist still applies. |
|
||
| `/files` | `path` | File manager: browse, paginate, open, new file/folder, rename, copy, delete (writable trees only) |
|
||
| `/settings` | — | Live session settings editor. Categories: appearance (theme, color depth, `LS_COLORS`, TUI), shell flags (`errexit`, `pipefail`, completion, …), session (`HOSTNAME`, `TZ`, `EDITOR`), Discord (guild / whitelist / user-install / debug — **never** the token), agent (`~/.agent/config.json` non-secret keys), IRC nick/autojoin, aliases. Changes persist to `~/.barerc`, `~/.discord/.env`, or app JSON and apply live when the guest already honors the knob |
|
||
| `/r` | `cmd` **(required)** | Full non-interactive guest shell as the unlocked user (short name, was `/run`). Discord always prompts for `cmd` (autocomplete: commands, `cd` dirs, flags, history). Persistent **cwd** per Discord user. Pipes, `&&`, redirects, `$VAR` work. `exit` does not stop the bot. 20s exec timeout. **All command output** (`ctx.console`, `bareOsBinWrite`, `process.stdout`/`stderr`, `writeScreen`) is captured into the Discord reply and is not printed on the booter TTY. The shell HUD stays available from **`/panel` → Shell** and from buttons after a command |
|
||
| `/plugins` | `list` `reload` `info` `disable` `enable` | Personal-drive plugins under `~/.discord/plugins` |
|
||
| `/journal` | optional `unit` | `journalctl` or `/var/log/bare-os/…` |
|
||
| `/ping` | — | `pong · Bare OS is online` |
|
||
|
||
`/r` is the Discord shell: each user keeps a working directory, command history, and last exit. Autocomplete completes builtins, `/bin`, aliases, flags, and paths relative to that cwd. Interactive TUIs (`edit`, `btop`, `irc`, …) need a real TTY and are labeled as such. Output paginates when it exceeds embed limits. Utilities such as **`echo`** and **`cat`** write via `process.stdout` (`bareOsEmitRaw`); `/r` hooks that stream for the duration of the command so the booter prompt does not echo guest output. **ANSI color and other terminal escapes are stripped** from every Discord payload (`NO_COLOR=1` / `TERM=dumb` while capturing; `ls` will not emit `[01;32m…`).
|
||
|
||
`/fs` allows `.`, `~`, `~/…`, `/proc`, `/etc`, `/var/log`, `/run`, `/home`, `/usr/share`, `/share`, `/tmp`, `/mnt`. It rejects `..`, NUL, `~/.discord/.env`, and `~/.discord.env`. Reads are capped (~12 KiB). `/files` and `/edit` may write under `/mnt/<label>/…` when the mount is writable (VFS still enforces read-only HDMS drives).
|
||
|
||
Channel text `ping` still replies `pong` only when Message Content Intent is on.
|
||
|
||
Posted embeds expire after **2 minutes of inactivity**. A click or select from the operator who posted the message resets the timer. Denied clicks (off-whitelist or another user) do not. When the timer fires the bot **deletes** the message (or strips its components and marks the embed expired if delete is denied) and drops the matching in-memory edit / files / settings session.
|
||
|
||
### Settings editor (`/settings`)
|
||
|
||
Bare OS has two layers of configuration: **host/boot** flags (read once when the booter starts) and **live session** knobs (read from `ctx.env` / `vfs.env` on the next command, or applied immediately via a ctx hook). `/settings` only exposes the second set, plus a few persist-now / apply-next-start values that live in files the guest already owns.
|
||
|
||
| Surface | What you can change | When it takes effect | Where it is stored |
|
||
| --- | --- | --- | --- |
|
||
| Appearance | `theme`, `BARE_OS_COLOR_DEPTH`, `BARE_OS_LS_COLORS_LOCKED`, `BARE_OS_TUI_NO_ALTSCREEN`, `NO_COLOR`, `BARE_OS_DIRCOLORS` | Theme / color / dircolors apply immediately via `ctx.bareOsApplyTheme`. TUI altscreen is next TUI | `theme …` and `export …` lines in `~/.barerc` |
|
||
| Shell | `BARE_OS_COMPACT_MENU`, `BARE_OS_SHELL_ERREXIT`, `NOUNSET`, `NOGLOB`, `PIPEFAIL`, `PIPESTATUS`, `POSIX_MODE`, `GROUPING`, `DOUBLE_BRACKET`, `CMDSUBST`, `STREAMING`, `BRACE_EXPANSION`, `PARAM_EXPANSION`, `UNTIL`, `LOOP_CONTROL`, `READ_BUILTIN` | Next matching command / Tab / pipeline (the shell reads these from env) | `export` in `~/.barerc` |
|
||
| Session | `HOSTNAME` (needs host `BARE_OS_HOSTNAME_SET=1` for `hostname --set`), `TZ`, `LANG`, `EDITOR`, `PAGER` | Hostname now when mutation is allowed; others next consumer | `export` in `~/.barerc` |
|
||
| Discord | `DISCORD_GUILD_ID`, `DISCORD_ID_WHITELIST`, `DISCORD_DEBUG`, `DISCORD_MESSAGE_CONTENT`, `DISCORD_LOGIN_TIMEOUT_MS` | Whitelist is next slash command. Guild / debug / intents / timeout need a bot restart | Non-secret keys only in `~/.discord/.env` (existing `DISCORD_TOKEN` is left untouched) |
|
||
| Agent | `backend`, `qvac_profile`, `qvac_device`, `qvac_ctx_size`, `qvac_gpu_layers`, `qvac_main_gpu`, `model` / `qvac_model`, `owner_name`, `agent_label`, `rest_base_url`, `temperature`, `max_tokens`, `max_iterations`, `stream`, `access_policy`, `context_compaction`, `compaction_keep_recent`, `autonomous_mode_enabled`, `todo_nudge_enabled`, `tool_parallelism`, `request_timeout_ms`, `allow_delete`, `show_reasoning`, `reasoning_mode`, host-bridge flags, `emergency_stop_mutations` | Next `agent` run | `~/.agent/config.json` — **never** `rest_api_key` / confirm tokens |
|
||
| IRC | `nick`, `autojoin`, `shareChannels` | Next `irc` | `~/.irc/config.json` — **never** `~/.irc/secrets.json` |
|
||
| Aliases | add `name=command`, remove an alias | Immediate (`ctx.shellAliases`) | `alias` / `unalias` lines in `~/.barerc` (`barerc` only understands `export`, `alias`, `unalias`, `theme`) |
|
||
|
||
**Not in the editor** (boot-only, host-only, or secret):
|
||
|
||
- Tokens and keys: `DISCORD_TOKEN`, `rest_api_key`, SASL password, IPC RPC token
|
||
- Boot / host gates: `BARE_OS_DISCORD`, `BARE_OS_TUI`, `BARE_OS_FISH`, `BARE_OS_BARE_MODULES`, boot profile / skip / policy, store paths, swarm / HTTP allowlists
|
||
- Identity: `USER` / `HOME` after `login` (those come from unlock, not a preference)
|
||
|
||
Use **Reload barerc** after hand-editing `~/.barerc` so aliases and exports re-apply. `~/.barerc` is not a general shell script.
|
||
|
||
---
|
||
|
||
## Run as an initd unit
|
||
|
||
The unit **`bare-os-discord`** is registered only when **`~/.discord/.env`** exists with a token. If that file is missing, `systemctl list` does **not** show the unit. After creating or editing the file:
|
||
|
||
```
|
||
systemctl daemon-reload
|
||
systemctl start bare-os-discord
|
||
systemctl status bare-os-discord
|
||
```
|
||
|
||
`daemon-reload` calls `syncBareOsDiscordInitd`. Identity unlock starts the unit when the file is present (`maybeStartBareOsDiscordAfterIdentity`). Logout unregisters it so the next session does not inherit a stale client.
|
||
|
||
Disable even when the file exists: **`BARE_OS_DISCORD_INITD=0`** (or `BARE_OS_DISCORD=0`).
|
||
|
||
Log: **`/var/log/bare-os/discord.log`**. Example unit comments: [`kernel/etc/bare-os/units/bare-os-discord.unit.example`](../kernel/etc/bare-os/units/bare-os-discord.unit.example) (keep the seeder kernel copy identical — `verify-kernel-seeder-parity`).
|
||
|
||
The unit uses **Guilds** only (no Message Content). Slash dispatch is the same catalog as `/bin/discord-bot`.
|
||
|
||
---
|
||
|
||
## Plugin system (`~/.discord/plugins`)
|
||
|
||
Drop files on the **personal drive**. No image rebuild. Plugins run as the **unlocked session user** (same trust as `/r` and `~/.barerc`). Gate the bot with **`DISCORD_ID_WHITELIST`**.
|
||
|
||
```
|
||
~/.discord/plugins/
|
||
hello.json # JSON plugin (easiest)
|
||
echo.js # JS plugin
|
||
weather/
|
||
plugin.json # optional manifest
|
||
index.js # handler
|
||
disabled.txt # one plugin name per line
|
||
```
|
||
|
||
Copy the samples from [`examples/discord-plugins/`](../examples/discord-plugins/). Then:
|
||
|
||
```
|
||
/plugins reload
|
||
```
|
||
|
||
New **slash names** are published immediately when the running bot has a registrar (stock `/bin/discord-bot` and `bare-os-discord` do). Otherwise restart the bot / unit. Guild registration is instant; global can take up to an hour.
|
||
|
||
### `/plugins`
|
||
|
||
| Sub | What it does |
|
||
| --- | --- |
|
||
| `list` | Loaded, disabled, and failed plugins |
|
||
| `reload` | Rescan the directory and re-register slash commands |
|
||
| `info name:` | Manifest, kind, error |
|
||
| `disable name:` | Append to `disabled.txt` |
|
||
| `enable name:` | Remove from `disabled.txt` |
|
||
|
||
A file named `*.disabled` or a folder with `"enabled": false` is skipped.
|
||
|
||
### Command names
|
||
|
||
- `a-z`, `0-9`, hyphen; 1–32 characters; must start with a letter or digit.
|
||
- **Reserved** (cannot override): `bare`, `sys`, `svc`, `fs`, `net`, `man`, `say`, `run`, `journal`, `edit`, `create`, `files`, `browse`, `settings`, `panel`, `ping`, `plugins`.
|
||
- At most **40** plugins. Each source file is capped at **64 KiB**.
|
||
|
||
### JSON plugin SDK
|
||
|
||
A single `~/.discord/plugins/<name>.json` (or `…/<name>/plugin.json`):
|
||
|
||
```json
|
||
{
|
||
"name": "hello",
|
||
"description": "Greet someone from Bare OS",
|
||
"version": "1.0.0",
|
||
"options": [
|
||
{ "name": "who", "description": "Name to greet", "required": false, "autocomplete": false }
|
||
],
|
||
"run": "echo Hello, ${who:-world} — from $USER on $HOSTNAME",
|
||
"title": "hello",
|
||
"ephemeral": false
|
||
}
|
||
```
|
||
|
||
| Field | Meaning |
|
||
| --- | --- |
|
||
| `name` | Slash command (`/hello`) |
|
||
| `description` | Shown in Discord |
|
||
| `options[]` | String options (`name`, `description`, `required`, `autocomplete`) |
|
||
| `subcommands[]` | Optional. Each may have its own `options`, `run`, `file`, `embed` |
|
||
| `run` / `shell` | Guest shell line via `ctx.execLine` (20s timeout). Expands `${opt}` / `${opt:-default}` and `$ENV` |
|
||
| `file` | Read a VFS path and page it (same pager as `/fs cat`) |
|
||
| `embed` | `{ title, desc, fields, file }` static card |
|
||
| `title` | Embed title for `run` / `file` |
|
||
| `ephemeral` | Reply only visible to the caller |
|
||
| `enabled` | `false` skips the plugin |
|
||
|
||
`${who}` is the slash option; `$USER` / `$HOME` / `$HOSTNAME` come from session env. Secrets are still redacted in output.
|
||
|
||
### JS plugin SDK
|
||
|
||
`~/.discord/plugins/<name>.js` or `…/<name>/index.js`. **No `import` / `require`.** The loader evaluates the file and looks for:
|
||
|
||
1. `async function register(bot) { ... }` (preferred), or
|
||
2. `const plugin = { name, description, run, ... }`.
|
||
|
||
```js
|
||
function register(bot) {
|
||
bot.command({
|
||
name: 'echo',
|
||
description: 'Echo text back as an embed',
|
||
options: [{ name: 'text', description: 'What to say', required: true }],
|
||
async run(ev) {
|
||
return { title: 'echo', desc: ev.opt('text'), footer: ev.user + ' · plugin' }
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
`run` may return:
|
||
|
||
| Return | Result |
|
||
| --- | --- |
|
||
| `string` | Plain message content (redacted) |
|
||
| `{ title, desc, fields, footer, color }` | One embed |
|
||
| `{ embeds, components, ephemeral }` | Full catalog result |
|
||
| `{ more: { title, body, fence } }` | Paged long text |
|
||
| `{ text, ephemeral }` | Short reply |
|
||
|
||
#### `bot` (also passed as `sdk`)
|
||
|
||
| Method | Role |
|
||
| --- | --- |
|
||
| `bot.command(spec)` | Register this slash command (call once, or once per command) |
|
||
| `bot.embed(opts)` / `bot.field(name, value, inline)` | Same cards as stock embeds (auto-fit Discord limits) |
|
||
| `bot.result(embed, extra)` | Attach components / nav |
|
||
| `bot.fence(text)` / `bot.redact(text)` | Code block + token redaction |
|
||
| `bot.more({ title, body, fence })` | Long-text pager |
|
||
| `bot.id('ok')` | Custom id `plug:<name>:ok` (never collides with stock buttons) |
|
||
| `bot.sh(line)` | `execLine` + captured stdout/stderr (20s) |
|
||
| `bot.read(path)` / `bot.ls(path)` | VFS read / readdir |
|
||
| `bot.write(path, text)` | Write only under `~/`, `/tmp`, `/home/$USER` (token files blocked) |
|
||
| `bot.user()` / `bot.home()` / `bot.env()` | Session identity; `env()` omits `TOKEN`/`SECRET`/… |
|
||
|
||
#### `ev` (the `run` argument)
|
||
|
||
| Field | Role |
|
||
| --- | --- |
|
||
| `ev.opt('text')` | Slash string option |
|
||
| `ev.sub` | Subcommand name |
|
||
| `ev.user` / `ev.userId` | Session user / Discord id |
|
||
| `ev.interaction` | Raw discord.js interaction (advanced) |
|
||
| `ev.ctx` | Full guest `ctx` (advanced — same power as `/r`) |
|
||
| `ev.bot` | The `bot` SDK |
|
||
| `ev.sh` / `ev.read` / `ev.write` / `ev.ls` | Same helpers |
|
||
|
||
Optional hooks on the same spec:
|
||
|
||
```js
|
||
bot.command({
|
||
name: 'pick',
|
||
description: 'Pick a color',
|
||
options: [{ name: 'color', description: 'Color', autocomplete: true }],
|
||
async run(ev) {
|
||
return { title: ev.opt('color') }
|
||
},
|
||
async onAutocomplete(ev) {
|
||
return ['red', 'green', 'blue']
|
||
},
|
||
async onComponent(ev) {
|
||
// ev.id is the suffix after plug:<name>:
|
||
return { text: 'clicked ' + ev.id }
|
||
},
|
||
async onModal(ev) {
|
||
return { text: ev.value }
|
||
}
|
||
})
|
||
```
|
||
|
||
Buttons you add must use `bot.id('save')` so the catalog routes them back to `onComponent`.
|
||
|
||
### Folder plugin
|
||
|
||
```
|
||
~/.discord/plugins/weather/
|
||
plugin.json # name, description, options
|
||
index.js # register(bot) — can ignore name if plugin.json set it
|
||
```
|
||
|
||
JS wins for `run` when both exist; JSON still supplies the slash metadata.
|
||
|
||
### Reloading
|
||
|
||
`/plugins reload` re-reads the directory and, on stock `/bin/discord-bot` / `bare-os-discord`, PUTs slash commands again. Dispatch uses the new handlers immediately.
|
||
|
||
---
|
||
|
||
## Extending the stock catalog
|
||
|
||
Edit [`packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs`](../packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs). That file must stay **guest-safe**: no `import` / `export`. Use `var` / `function`. `module.exports` is the host/pack surface (static `import` from initd). The `/bin` preamble ignores `module.exports` when `module` is absent.
|
||
|
||
1. Add a `SlashCommandBuilder` (and subcommands) in `discordBuildSlashCommands`.
|
||
2. Handle the name in `discordDispatchInteraction`.
|
||
3. Keep replies under ~1900 characters. Use `discordCmdRedact` / `discordCmdFence` for command output.
|
||
4. Do not read `~/.discord/.env` in `/fs`.
|
||
5. Rebuild the image binary:
|
||
|
||
```bash
|
||
npm run build -w bare-os-coreutils
|
||
```
|
||
|
||
That prepends the catalog onto `/bin/discord-bot` and mirrors **`packages/bare-os-seeder/kernel/`**.
|
||
|
||
6. Add a Brittle case in [`test.bare-discord-initd.js`](../packages/bare-os-booter/test.bare-discord-initd.js) (`buildSlashCommands` + `dispatchInteraction` with a fake interaction).
|
||
|
||
Do not add `require('discord.js')` to guest sources. The catalog receives a `dj` object (`SlashCommandBuilder` only) at register time and `ctx` at dispatch time.
|
||
|
||
---
|
||
|
||
## Shipping your own `/bin` bot
|
||
|
||
Follow [Chapter 16](16-how-to-add-bin-utility.md):
|
||
|
||
1. `packages/bare-os-coreutils/src/<name>.js` with `async function run(ctx, argv)` and **no** `import`.
|
||
2. Optional `preamble` in `build.mjs` if you need shared helpers (the stock bot prepends `bare-os-discord-commands-guest.cjs` from the booter tree).
|
||
3. `man/pages/<name>.json`.
|
||
4. Register the name in `lib/commands.mjs`.
|
||
5. `npm run build -w bare-os-coreutils`.
|
||
|
||
For one-off bots, a personal-drive `*.js` is enough. A `/bin` name is for something every guest should have.
|
||
|
||
---
|
||
|
||
## Host, Pear, and packed standalone
|
||
|
||
Maintainers changing login, packing, or CI should treat these as one path:
|
||
|
||
| Piece | Role |
|
||
| --- | --- |
|
||
| [`vendor/bare-discord-js`](../packages/bare-os-booter/vendor/bare-discord-js/) | Official discord.js 14 + Bare remaps |
|
||
| [`bare-os-discord-ws-bootstrap.mjs`](../packages/bare-os-booter/lib/bare-os-discord-ws-bootstrap.mjs) | Installs the WHATWG **`bare-ws`** wrapper **before** packed `import discord.js` |
|
||
| [`whatwg-ws.cjs`](../packages/bare-os-booter/vendor/bare-discord-js/src/adapters/whatwg-ws.cjs) | `send` / `onmessage` (npm `ws` / raw `bare-ws.Socket` is a Duplex and will not IDENTIFY correctly) |
|
||
| [`bare-os-standalone-pack-imports.mjs`](../packages/bare-os-booter/lib/bare-os-standalone-pack-imports.mjs) | Pack graph: bootstrap → commands-guest → host modules → packed discord.js |
|
||
| `build/stubs/zlib-sync.cjs` | Must export **`null`**. A truthy `{}` stub makes `@discordjs/ws` take the zlib-stream path and hang before READY |
|
||
| `process.versions.bun = 'bare-os'` | Set by the wrapper so `@discordjs/ws` selects `globalThis.WebSocket` |
|
||
|
||
On macOS, **do not overwrite a running `bare-os-booter` in place**. That invalidates the ad-hoc code signature and the next exec is **SIGKILL**. Write a new inode, then codesign.
|
||
|
||
Bare-only smoke (needs a real token in the environment the script already documents — do not print it):
|
||
|
||
```bash
|
||
npm run test:discord-login -w bare-os-booter
|
||
```
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
| Test | What it covers |
|
||
| --- | --- |
|
||
| `packages/bare-os-booter/test.bare-discord-env.js` | Token normalize, `.env` parse, host inject, `--check`, guest-safe sources, default intents, 4014 fail-closed |
|
||
| `packages/bare-os-booter/test.bare-discord-initd.js` | Unit hidden without `~/.discord/.env`, catalog, whitelist, `/files`, `/settings`, `/r` shell, plugins (JSON + JS), embed limits |
|
||
| Guest `--check` | `ctx.bare.discordJS` + token present, no gateway |
|
||
|
||
Fake an interaction without Discord:
|
||
|
||
```js
|
||
const replies = []
|
||
await cmds.dispatchInteraction(
|
||
{ env: { DISCORD_ID_WHITELIST: '111' }, vfs: {}, console: {} },
|
||
{
|
||
isChatInputCommand: () => true,
|
||
commandName: 'ping',
|
||
user: { id: '999' },
|
||
reply: async (p) => {
|
||
replies.push(p)
|
||
}
|
||
}
|
||
)
|
||
```
|
||
|
||
Do not assert on live tokens in CI. Redact `DISCORD_TOKEN` in any fixture `.env`.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Likely cause | What to do |
|
||
| --- | --- | --- |
|
||
| `ctx.bare.discordJS is unavailable` | Load skipped or failed | Unset `BARE_OS_DISCORD=0`. Read `BARE_OS_DISCORD_LOAD_ERROR`. Restart the **booter**, not only `/bin` |
|
||
| `missing DISCORD_TOKEN` | No session token and no readable `.env` | Write `~/.discord/.env` or pass `--env`. Confirm VFS path (`~` is the personal home) |
|
||
| REST `/gateway/bot` **401** | Wrong secret | Bot token, not OAuth2 client secret. Reset Token |
|
||
| Login timeout, last line `Identifying` | Packed `ws` / zlib stub / old binary | Confirm WS bootstrap runs before discord.js; zlib-sync stub is `null`; you are running the rebuilt booter |
|
||
| Gateway **4014** | Message Content requested, portal off | Drop `--message-content` or enable the intent |
|
||
| Gateway **4004** | Bad token (BOM, extra quotes, old token) | Normalize / reset |
|
||
| Slash commands missing | Global register delay, or bot not in guild | Set `DISCORD_GUILD_ID` / `--guild`. Re-invite with `applications.commands` |
|
||
| `Access denied` | Allowlist | Add your user id to `DISCORD_ID_WHITELIST`, restart the bot |
|
||
| Unit missing from `systemctl` | No `~/.discord/.env` or `BARE_OS_DISCORD_INITD=0` | Create the file, `systemctl daemon-reload` |
|
||
| Hang after “logging in” with no REST line | Old binary still requesting privileged intents | Rebuild `/bin/discord-bot`; default must be Guilds-only |
|
||
| macOS `SIGKILL` after replacing booter | Code signature invalidated | Atomic replace + codesign a new inode |
|
||
| Channel `ping` ignored | No Message Content | Expected on Guilds-only. Use `/ping` |
|
||
| `reply failed: process.emitWarning is not a function` | Bare `process` lacks Node's `emitWarning`; older bots passed `ephemeral:` | Rebuild `/bin/discord-bot` / booter. Replies use `flags` (64) instead of `ephemeral`. Restart the bot. |
|
||
| `Supplying "fetchReply" … is deprecated` | Older catalog passed `fetchReply: true` on `reply` / `followUp` | Rebuild. Replies no longer send that option; the message is fetched afterwards for the idle timer. |
|
||
|
||
`--debug` / `DISCORD_DEBUG=1` prints discord.js debug during login. Token JSON is redacted in the stock `ws send` hook (`"token":"***"`).
|
||
|
||
---
|
||
|
||
## Security
|
||
|
||
- The stock bot is a **remote shell surface** for whoever can invoke slash commands. Put **`DISCORD_ID_WHITELIST`** in `.env` before inviting the bot to a public guild.
|
||
- `/svc` can start and stop units. `/r` is a full guest shell. Treat whitelist members as operators.
|
||
- Tokens in `ctx.env` are redacted by `/sys env` and `discordCmdRedact`. Do not `console.log` `ctx.env`.
|
||
- Guest scripts are **not** a sandbox ([Chapter 9](09-security-and-trust.md)). Stock `/r` is a full `ctx.execLine` shell as the unlocked user. Gate it with **`DISCORD_ID_WHITELIST`**. Tokens are redacted in output; `exit` / `login` / `logout` are intercepted so they cannot tear down the bot.
|
||
- Plugins under `~/.discord/plugins` are **your** code with the same power as `/r` (`execLine`, VFS). Do not install plugin JS from untrusted people. The SDK `write` path still blocks token files.
|
||
- Rotate a token if it appeared in a screenshot, issue, or chat.
|
||
|
||
---
|
||
|
||
## File map
|
||
|
||
| Path | Role |
|
||
| --- | --- |
|
||
| [`packages/bare-os-coreutils/src/init-discord.js`](../packages/bare-os-coreutils/src/init-discord.js) | Setup wizard (`~/.discord/.env` + hello plugin) |
|
||
| [`packages/bare-os-coreutils/src/discord-bot.js`](../packages/bare-os-coreutils/src/discord-bot.js) | Foreground guest bot (`run`) |
|
||
| [`packages/bare-os-coreutils/man/pages/discord-bot.json`](../packages/bare-os-coreutils/man/pages/discord-bot.json) | `man discord-bot` |
|
||
| [`packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs`](../packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs) | Slash catalog (guest-safe CJS) |
|
||
| [`packages/bare-os-booter/lib/bare-os-discord-initd.js`](../packages/bare-os-booter/lib/bare-os-discord-initd.js) | `bare-os-discord` unit |
|
||
| [`packages/bare-os-booter/lib/bare-discord-js-loader.js`](../packages/bare-os-booter/lib/bare-discord-js-loader.js) | Load + host `.env` inject |
|
||
| [`packages/bare-os-booter/lib/bare-os-discord-ws-bootstrap.mjs`](../packages/bare-os-booter/lib/bare-os-discord-ws-bootstrap.mjs) | Packed gateway WebSocket |
|
||
| [`examples/discord-ping-pong/index.js`](../examples/discord-ping-pong/index.js) | Copy-paste guest example |
|
||
| [`examples/discord-plugins/`](../examples/discord-plugins/) | Sample JSON + JS plugins for `~/.discord/plugins` |
|
||
| `~/.discord/plugins/` | User plugins (personal drive) |
|
||
| [`kernel/etc/bare-os/units/bare-os-discord.unit.example`](../kernel/etc/bare-os/units/bare-os-discord.unit.example) | Unit comments |
|
||
|
||
---
|
||
|
||
## See also
|
||
|
||
- [Chapter 1 — Two runtimes](01-two-runtimes-host-vs-image.md)
|
||
- [Chapter 4 — User scripts and PATH](04-user-scripts-and-path.md)
|
||
- [Chapter 5 — Modules and `import`](05-modules-and-imports.md)
|
||
- [Chapter 7 — Apps beyond the shell](07-apps-beyond-the-shell.md)
|
||
- [Chapter 12 — `ctx.bare`](12-bare-modules-and-pear-ecosystem.md)
|
||
- [Chapter 16 — How to add a `/bin` utility](16-how-to-add-bin-utility.md)
|
||
- [Environment appendix — `DISCORD_*`](../docs/reference/environment-and-posix-appendix.md)
|
||
- [Reference — `/bin/discord-bot`](../docs/reference/README.md)
|
||
- **`man discord-bot`** · **`man devguide-21-discord-bots`** (after a coreutils build)
|