open files for discord bot
Release rolling / release (push) Successful in 9m36s

This commit is contained in:
2026-08-19 18:28:52 -04:00
parent be5c11cec0
commit 03ba640b57
10 changed files with 2253 additions and 117 deletions
+3 -2
View File
@@ -335,6 +335,7 @@ discord-bot --help
| `/edit` | `path` | Modal editor for `~/` and `/tmp` (≤20000 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` |
| `/open-file` | `path` **(required)** | Read a VFS file and post it as a Discord **attachment** (uploaded to the Discord CDN). The reply is **not** idle-deleted, so the CDN URL stays. Same read path rules as `/fs` (`~/`, `/tmp`, `/home`, `/proc`, `/etc`, `/var/log`, `/run`, `/share`, `/mnt`; no `..`; token files blocked). Relative names resolve against the `/r` cwd. Max **10MiB**. Directories and empty files are refused. |
| `/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. **Direct Messages:** a whitelisted user can send a normal DM to the bot (no slash) and it runs the same agent turn. Text `help` / `reset` / `stop` / `ping` are shortcuts. Empty whitelist or an unknown user gets Access denied; guild channel text is **not** an agent chat. **Message channel:** the first allowed DM opens `~/.discord/channel.json`. The agent can then **reach out first** with `discord_send_message` (live `ctx.bareOsDiscordSendDm`, else `~/.discord/outbox.json` drained every 1.5s). Inbound copies: `~/.discord/inbox.json`. `emit_host_notification` also fans out to the open DM. |
@@ -352,7 +353,7 @@ discord-bot --help
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.
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. **`/open-file` attachments are kept** — that reply is not armed for idle delete so the file remains on the Discord CDN.
### Settings editor (`/settings`)
@@ -435,7 +436,7 @@ A file named `*.disabled` or a folder with `"enabled": false` is skipped.
### Command names
- `a-z`, `0-9`, hyphen; 132 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`.
- **Reserved** (cannot override): `bare`, `sys`, `svc`, `fs`, `net`, `man`, `say`, `run`, `journal`, `edit`, `create`, `files`, `browse`, `settings`, `panel`, `ping`, `plugins`, `upload`, `open-file`, `hdms`, `holesail`, `agent`, `status`.
- At most **40** plugins. Each source file is capped at **64 KiB**.
### JSON plugin SDK
+968 -37
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -59,6 +59,8 @@ var BARE_OS_DISCORD_SH_BINS = null
var BARE_OS_DISCORD_SH_BINS_AT = 0
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
var BARE_OS_DISCORD_UPLOAD_MS = 60000
/** Discord's default bot attachment cap (10 MiB). */
var BARE_OS_DISCORD_OPEN_FILE_MAX = 10 * 1024 * 1024
var BARE_OS_DISCORD_SH_BUILTINS = [
'alias',
'unalias',
@@ -773,8 +775,11 @@ function discordCmdReplyPayload(result) {
payload.components = discordUniqueComponents(result.components)
if (!payload.components.length) delete payload.components
}
if (result && result.attachments && result.attachments.length) {
payload.files = result.attachments
}
if (result && result.text) payload.content = discordCmdClip(result.text)
if (!payload.content && !payload.embeds) payload.content = '(no output)'
if (!payload.content && !payload.embeds && !payload.files) payload.content = '(no output)'
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
delete payload.fetchReply
delete payload.ephemeral
@@ -2829,7 +2834,10 @@ async function discordCmdHandleBare(ctx, sub) {
discordField('/svc', 'Services — list, start, stop, restart, logs'),
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
discordField('/net', 'Swarm — peers, summary'),
discordField('/files /edit /create /upload', 'Browse, edit, or wget an attachment into `~/` `/tmp`'),
discordField(
'/files /edit /create /upload /open-file',
'Browse, edit, wget in, or attach a VFS file out (CDN, kept)'
),
discordField('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'),
discordField('/r /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins'),
@@ -3931,6 +3939,118 @@ async function discordCmdHandleUpload(ctx, interaction, destRaw) {
)
}
function discordOpenFileByteLength(buf) {
if (buf == null) return 0
if (typeof buf.byteLength === 'number') return buf.byteLength
if (typeof buf.length === 'number') return buf.length
if (typeof buf === 'string') {
if (typeof Buffer !== 'undefined' && typeof Buffer.byteLength === 'function') {
return Buffer.byteLength(buf)
}
return buf.length
}
return 0
}
function discordOpenFileAsAttachment(ctx, buf, name) {
let data = buf
if (typeof data === 'string') {
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') data = ctx.b4a.from(data)
else if (typeof Buffer !== 'undefined') data = Buffer.from(data)
else {
const u8 = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) u8[i] = data.charCodeAt(i) & 0xff
data = u8
}
}
return { attachment: data, name: name }
}
function discordOpenFileResolvePath(ctx, interaction, rawPath) {
const raw = String(rawPath || '').trim()
if (!raw) return ''
if (raw.charAt(0) === '/' || raw.charAt(0) === '~' || raw === '.') return raw
const rec = discordShGet(interaction, ctx)
return discordJoinPath(rec.cwd || '~', raw)
}
async function discordCmdHandleOpenFile(ctx, interaction, rawPath) {
const given = String(rawPath || '').trim()
if (!given) {
return {
text: 'Give `/open-file` a VFS path (e.g. `~/notes.txt`). The bot attaches it to this channel (Discord CDN; not auto-deleted).',
ephemeral: true
}
}
const resolved = discordOpenFileResolvePath(ctx, interaction, given)
const p = discordCmdPathOk(resolved)
if (!p) {
return {
text: 'path not allowed (no `..`; stay under /proc /etc /var/log /run /home ~ /share /tmp /mnt)',
ephemeral: true
}
}
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') {
return { text: 'vfs.readFile is unavailable in this session', ephemeral: true }
}
const st = await discordFmStat(ctx, p)
if (discordIsDirStat(st)) {
return { text: '`' + p + '` is a directory. Pick a file.', ephemeral: true }
}
if (!st && !(await discordFileExists(ctx, p))) {
return { text: 'No such file: `' + p + '`', ephemeral: true }
}
if (st && st.size != null && Number(st.size) > BARE_OS_DISCORD_OPEN_FILE_MAX) {
return {
text:
'`' +
p +
'` is ' +
discordPrettyBytes(st.size) +
' (max ' +
discordPrettyBytes(BARE_OS_DISCORD_OPEN_FILE_MAX) +
' for Discord attachments).',
ephemeral: true
}
}
let buf
try {
buf = await ctx.vfs.readFile(p)
} catch (err) {
return {
text: 'Could not read `' + p + '`: ' + ((err && err.message) || err),
ephemeral: true
}
}
if (buf == null) {
return { text: '`' + p + '` is empty or missing.', ephemeral: true }
}
const bytes = discordOpenFileByteLength(buf)
if (bytes > BARE_OS_DISCORD_OPEN_FILE_MAX) {
return {
text:
'`' +
p +
'` is ' +
discordPrettyBytes(bytes) +
' (max ' +
discordPrettyBytes(BARE_OS_DISCORD_OPEN_FILE_MAX) +
' for Discord attachments).',
ephemeral: true
}
}
if (bytes === 0) {
return { text: '`' + p + '` is empty (0 bytes). Nothing to attach.', ephemeral: true }
}
const name = discordUploadSafeName(discordBaseName(p) || 'file.bin')
const size = st && st.size != null ? Number(st.size) : bytes
return {
text: '`' + p + '` · ' + discordPrettyBytes(size),
attachments: [discordOpenFileAsAttachment(ctx, buf, name)],
keep: true
}
}
var BARE_OS_DISCORD_HDMS_SESSIONS = Object.create(null)
function discordHdmsKey(interaction) {
@@ -6505,9 +6625,11 @@ var BARE_OS_DISCORD_PLUGIN_RESERVED = {
ping: 1,
plugins: 1,
upload: 1,
'open-file': 1,
hdms: 1,
holesail: 1,
agent: 1
agent: 1,
status: 1
}
var BARE_OS_DISCORD_PLUGINS = []
var BARE_OS_DISCORD_PLUGINS_READY = false
@@ -7075,6 +7197,9 @@ function discordBuildSlashCommands(dj, ctx) {
const upload = new B2()
.setName('upload')
.setDescription('Upload an attachment; wget saves it in the /r cwd or path')
const openFile = new B2()
.setName('open-file')
.setDescription('Send a VFS file as a Discord attachment (kept, not auto-deleted)')
const hdms = new B2()
.setName('hdms')
.setDescription('Hyperdrive management (list, create, add, invite, pair)')
@@ -7291,6 +7416,7 @@ function discordBuildSlashCommands(dj, ctx) {
discordCmdOpt(files, 'path', 'Directory to open', false, true)
discordCmdAtt(upload, 'file', 'File to upload (Discord CDN, then wget into the VFS)', true)
discordCmdOpt(upload, 'path', 'VFS dest (file or dir; default: /r cwd + name)', false, true)
discordCmdOpt(openFile, 'path', 'VFS file to attach (uploaded to the Discord CDN)', true, true)
discordCmdOpt(status, 'activity', 'playing, watching, listening, competing, custom, streaming', false, false, {
choices: [
['Playing', 'playing'],
@@ -7313,7 +7439,7 @@ function discordBuildSlashCommands(dj, ctx) {
['Invisible', 'invisible']
]
})
const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping, status, upload, hdms, holesail, agent].map(
const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping, status, upload, openFile, hdms, holesail, agent].map(
function (c) {
return discordStampUserInstallCommand(c.toJSON(), ctx)
}
@@ -9158,7 +9284,7 @@ async function discordSendResult(ctx, interaction, result) {
if (result && result.settings) {
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
}
if (result && result.files !== undefined) {
if (result && result.files !== undefined && !result.attachments && !Array.isArray(result.files)) {
const rec = discordFmGet(interaction, ctx)
const want = String(result.files || '').trim()
if (want) {
@@ -9380,7 +9506,9 @@ async function discordSendResult(ctx, interaction, result) {
return
}
try {
await discordIdleWatch(interaction, payload, sent)
const persist =
(result && result.keep) || (payload.files && payload.files.length)
if (!persist) await discordIdleWatch(interaction, payload, sent)
} catch {
/* tracking must not break the bot */
}
@@ -10450,6 +10578,8 @@ async function discordDispatchInteraction(ctx, interaction) {
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
} else if (name === 'upload') {
result = await discordCmdHandleUpload(ctx, interaction, opt('path'))
} else if (name === 'open-file') {
result = await discordCmdHandleOpenFile(ctx, interaction, opt('path'))
} else if (name === 'hdms') {
result = await discordCmdHandleHdms(ctx, interaction, sub, opt)
} else if (name === 'holesail') {
@@ -10545,6 +10675,8 @@ var bareOsDiscordCommands = {
parseHolesailList: discordParseHolesailList,
parseHolesailShow: discordParseHolesailShow,
pathOk: discordCmdPathOk,
openFileMax: BARE_OS_DISCORD_OPEN_FILE_MAX,
handleOpenFile: discordCmdHandleOpenFile,
result: discordResult,
packEmbed: discordPackEmbed,
textPages: discordTextPages,
@@ -329,6 +329,7 @@ test('discord command catalog builds Bare OS slash commands', async (t) => {
t.ok(names.indexOf('settings') >= 0)
t.ok(names.indexOf('plugins') >= 0)
t.ok(names.indexOf('upload') >= 0)
t.ok(names.indexOf('open-file') >= 0)
t.ok(names.indexOf('hdms') >= 0)
t.ok(names.indexOf('holesail') >= 0)
t.ok(names.indexOf('agent') >= 0)
@@ -343,6 +344,9 @@ test('discord command catalog builds Bare OS slash commands', async (t) => {
const uploadCmd = body.find((c) => c.name === 'upload')
const fileOpt = uploadCmd && (uploadCmd.options || []).find((o) => o.name === 'file')
t.ok(fileOpt && fileOpt.type === 11 && fileOpt.required, '/upload file attachment is required')
const openFileCmd = body.find((c) => c.name === 'open-file')
const openPath = openFileCmd && (openFileCmd.options || []).find((o) => o.name === 'path')
t.ok(openPath && openPath.type === 3 && openPath.required, '/open-file path is required')
t.ok(body.length >= 8)
const replies = []
@@ -360,6 +364,7 @@ test('discord command catalog builds Bare OS slash commands', async (t) => {
await cmds.dispatchInteraction({ env: {}, vfs: {}, console: {} }, interaction)
const helpBlob = JSON.stringify(replies[0] || {})
t.ok(/\/bare/.test(helpBlob), 'help mentions /bare')
t.ok(/open-file/.test(helpBlob), 'help mentions /open-file')
assertUniqueCustomIds(t, replies[0], 'help')
})
@@ -1272,6 +1277,142 @@ test('Discord /upload wget-saves an attachment into the /run cwd', async (t) =>
t.is(lines.length, 3, 'denied upload does not run wget')
})
test('Discord /open-file attaches a VFS file and does not idle-delete it', async (t) => {
const req = createRequire(fileURLToPath(import.meta.url))
const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs')
t.ok(cmds.openFileMax >= 10 * 1024 * 1024, 'Discord default attachment cap')
const packed = cmds.replyPayload({
text: 'note',
attachments: [{ attachment: Buffer.from('a'), name: 'a.bin' }],
keep: true
})
t.is(packed.content, 'note')
t.is(packed.files[0].name, 'a.bin')
t.absent(packed.keep, 'keep is not sent to Discord')
const env = { USER: 'alice', HOME: '/home/alice', PWD: '/home/alice' }
const files = {
'/home/alice/hello.txt': 'hello from vfs',
'/home/alice/rel.bin': 'rel-body',
'/tmp/photo.png': Buffer.from([0x89, 0x50, 0x4e, 0x47, 0, 1, 2]).toString('binary'),
'/tmp/empty.txt': '',
'/tmp/as-string.txt': 'string-body'
}
let readCount = 0
const ctx = {
env,
vfs: {
env,
stat: async (p) => {
if (p === '/tmp' || p === '/home/alice') return { isDirectory: true, type: 'directory' }
if (p === '/tmp/empty.txt') return { isFile: true, type: 'file', size: 0 }
if (p === '/tmp/huge.bin') return { isFile: true, type: 'file', size: cmds.openFileMax + 1 }
if (Object.prototype.hasOwnProperty.call(files, p)) {
return { isFile: true, type: 'file', size: Buffer.from(files[p], 'binary').length }
}
const e = new Error('ENOENT')
e.code = 'ENOENT'
throw e
},
readFile: async (p) => {
readCount++
if (p === '/tmp/as-string.txt') return files[p]
if (!Object.prototype.hasOwnProperty.call(files, p)) {
const e = new Error('ENOENT')
e.code = 'ENOENT'
throw e
}
return Buffer.from(files[p], 'binary')
}
},
b4a: {
toString: (b) => Buffer.from(b).toString('utf8'),
from: (s) => Buffer.from(String(s))
},
console: { log() {}, error() {} }
}
function ix(path, replies) {
return {
isChatInputCommand: () => true,
isAutocomplete: () => false,
isButton: () => false,
isStringSelectMenu: () => false,
isModalSubmit: () => false,
commandName: 'open-file',
user: { id: 'of1' },
options: {
getSubcommand: () => '',
getString: (k) => (k === 'path' ? path : '')
},
reply: async (p) => {
replies.push(p)
return { id: 'msg-open-' + replies.length }
}
}
}
const missing = []
await cmds.dispatchInteraction(ctx, ix('', missing))
t.ok(/Give `\/open-file`/.test(JSON.stringify(missing[0] || {})), 'requires a path')
t.ok(missing[0] && missing[0].flags, 'missing path is ephemeral')
t.absent(missing[0] && missing[0].files)
const liveBefore = Object.keys(cmds.idleLive()).length
const views = []
await cmds.dispatchInteraction(ctx, ix('/home/alice/hello.txt', views))
t.ok(views[0] && Array.isArray(views[0].files) && views[0].files.length === 1, 'posts an attachment')
t.is(views[0].files[0].name, 'hello.txt')
t.is(Buffer.from(views[0].files[0].attachment).toString('utf8'), 'hello from vfs')
t.ok(/hello\.txt/.test(String(views[0].content || '')))
t.absent(views[0].embeds, 'no expire-able embed')
t.absent(views[0].components, 'no expire-able components')
t.is(Object.keys(cmds.idleLive()).length, liveBefore, 'does not arm idle delete')
const rel = []
await cmds.dispatchInteraction(ctx, ix('rel.bin', rel))
t.is(rel[0] && rel[0].files && rel[0].files[0] && rel[0].files[0].name, 'rel.bin')
t.is(Buffer.from(rel[0].files[0].attachment).toString('utf8'), 'rel-body')
const bin = []
await cmds.dispatchInteraction(ctx, ix('/tmp/photo.png', bin))
t.is(bin[0].files[0].name, 'photo.png')
t.ok(Buffer.from(bin[0].files[0].attachment)[0] === 0x89, 'binary bytes preserved')
const asString = []
await cmds.dispatchInteraction(ctx, ix('/tmp/as-string.txt', asString))
t.ok(typeof asString[0].files[0].attachment !== 'string', 'string reads become attachment bytes')
t.is(Buffer.from(asString[0].files[0].attachment).toString('utf8'), 'string-body')
const denied = []
const readsBeforeDeny = readCount
await cmds.dispatchInteraction(ctx, ix('~/.discord/.env', denied))
t.ok(/not allowed/.test(JSON.stringify(denied[0] || {})), 'blocks token files')
t.absent(denied[0] && denied[0].files)
t.is(readCount, readsBeforeDeny, 'denied path does not read the file')
const dir = []
await cmds.dispatchInteraction(ctx, ix('/tmp', dir))
t.ok(/directory/.test(JSON.stringify(dir[0] || {})), 'refuses directories')
t.absent(dir[0] && dir[0].files)
const missingFile = []
await cmds.dispatchInteraction(ctx, ix('/tmp/nope.txt', missingFile))
t.ok(/No such file/.test(JSON.stringify(missingFile[0] || {})))
const empty = []
await cmds.dispatchInteraction(ctx, ix('/tmp/empty.txt', empty))
t.ok(/empty/.test(JSON.stringify(empty[0] || {})))
t.absent(empty[0] && empty[0].files)
const huge = []
const readsBeforeHuge = readCount
await cmds.dispatchInteraction(ctx, ix('/tmp/huge.bin', huge))
t.ok(/max/.test(JSON.stringify(huge[0] || {})), 'rejects oversize before upload')
t.absent(huge[0] && huge[0].files)
t.is(readCount, readsBeforeHuge, 'oversize does not read the file')
})
test('Discord /hdms manages extra Hyperdrives', async (t) => {
const req = createRequire(fileURLToPath(import.meta.url))
const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs')
+1 -1
View File
@@ -3,7 +3,7 @@
## Unreleased
- **`agent`**: Production ReAct harness. Default **QVAC** (local); optional OpenAI-compatible **REST** (`agent --setup` stores only the chosen backend). Full guest admin by default (**denylist**, not allowlist). Production flags **`--plan`**, **`--auto`**, **`--new`**, **`--compact`**, **`--max-turns`**, **`--model`**, **`--system`**. Inspect (no model): **`skills`**, **`todos`**, **`plan`**, **`undo`**, **`hooks`**, **`history`**, **`rewind`**, **`export`**, **`remember`**, **`recap`**. Grok-port tools include **`grep`** (`output_mode`), **`apply_patch`**, **`find_symbol`**, **`copy_path`**, **`diff_files`**, **`read_many`**, **`fuzzy_find`**, **`wait_for`**, **`undo_last_edit`**, **`git_log`/`show`/`blame`**, **`create_skill`**, session rewind/export, walk-up project skills (`.grok/skills`, `.agents/skills`, …). Discord **`/agent`** is the same session (shared `~/.agent`).
- **`discord-bot`**: Full **`/agent`** surface (ask + inspect + session + models + settings). User-installable profile app; **`DISCORD_ID_WHITELIST`** always enforced (loaded from `~/.discord/.env` / `~/.env`). **`/holesail`** and **`/hdms`** interactive managers. See [developer-guide ch.21](../../developer-guide/21-discord-bots.md).
- **`discord-bot`**: Full **`/agent`** surface (ask + inspect + session + models + settings). User-installable profile app; **`DISCORD_ID_WHITELIST`** always enforced (loaded from `~/.discord/.env` / `~/.env`). **`/holesail`** and **`/hdms`** interactive managers. **`/open-file`** posts a VFS file as a Discord attachment (CDN upload; not idle-deleted). See [developer-guide ch.21](../../developer-guide/21-discord-bots.md).
- **`summon`**: Text web browser (`ctx.tui`, cell buffer). HTML/CSS engine plus page JS via **`ctx.bare.bareVm`** (`bare-vm` / `bare-realm`) or a Function sandbox. **`--no-js`** / **`J`**. HTTP via **`ctx.httpFetch`**. See [summon.md](../../docs/reference/summon.md).
- **`irc`**: Room browser (`/rooms`, Ctrl+L), featured Libera channels, `LIST` filter, self-JOIN switches buffer. Full slash table (`/query`/`/msg` print usage when args are missing). Random `osXXXXX` nick when USER looks like a container id. TLS prefers **`ctx.bareOsTlsConnect`** and falls back to **`ctx.bare.bareTls`**.
@@ -5,7 +5,7 @@
"synopsis": [
"discord-bot [--env PATH] [--token TOKEN] [--guild ID] [--check] [--debug] [--message-content] [--login-timeout MS] [--no-user-install]"
],
"description": "Bare OS Discord bot via ctx.bare.discordJS. Commands run as the unlocked session user (not guest). /r requires cmd and is a non-interactive shell (cwd, history, autocomplete). /upload attaches a file and wget-saves it into the /r cwd or a given path. /hdms manages extra Hyperdrives (list, create, add, invite, pair, health) via ctx.runHdms. /holesail manages persisted Holesail tunnels and the bare-holesail unit (list, add, edit, start/stop, enable/disable, service, url) via ctx.bareOsRunHolesailCli; show never prints seed material. /agent is the full guest /bin/agent harness when ~/.agent/config.json is ready (QVAC + host bridge, or REST + API key + base URL): ask (required prompt; new/plan/auto/compact/max_turns/model), status, config, models, skills, todos, plan, hooks, history, recap, undo, rewind, compact, export, remember, reset, stop. History, skills, todos, and MEMORY.md are shared with the TTY; API keys are never shown. Whitelisted users can Direct Message the bot to talk to the agent without a slash command (same ~/.agent session; help / reset / stop as text). The first allowed DM opens a persistent message channel (~/.discord/channel.json). The agent can then reach out anytime with discord_send_message (live send via ctx.bareOsDiscordSendDm, or queue ~/.discord/outbox.json until the bot drains it). Inbound copies land in ~/.discord/inbox.json. Empty DISCORD_ID_WHITELIST denies every DM, inbound and outbound. /status sets the bot presence shown in the member list (playing, watching, listening, competing, custom, streaming plus online/idle/dnd/invisible); persisted as DISCORD_STATUS / DISCORD_ACTIVITY_TYPE / DISCORD_ACTIVITY_NAME in ~/.discord/.env and also under /settings Discord. /plugins loads JSON or JS from ~/.discord/plugins (see developer-guide ch.21). Also /settings, /files, /panel, /edit, /create, /bare, /sys, /svc, /fs, /net, /man, /say, /journal, /ping. Embeds paginate when large and expire after 2 minutes idle. Token from --token, DISCORD_TOKEN, or ~/.discord/.env. User-installable (Add App to your Discord profile) is on by default so slash commands work in DMs and any server; DISCORD_ID_WHITELIST is always enforced on those surfaces (empty list denies user-install / DM use). DirectMessages intent is requested so DM chat works without the privileged Message Content Intent. The initd unit bare-os-discord is listed by systemctl only when ~/.discord/.env exists.",
"description": "Bare OS Discord bot via ctx.bare.discordJS. Commands run as the unlocked session user (not guest). /r requires cmd and is a non-interactive shell (cwd, history, autocomplete). /upload attaches a file and wget-saves it into the /r cwd or a given path. /open-file path reads a VFS file and posts it as a Discord attachment (uploaded to the Discord CDN; the message is not auto-deleted after idle). /hdms manages extra Hyperdrives (list, create, add, invite, pair, health) via ctx.runHdms. /holesail manages persisted Holesail tunnels and the bare-holesail unit (list, add, edit, start/stop, enable/disable, service, url) via ctx.bareOsRunHolesailCli; show never prints seed material. /agent is the full guest /bin/agent harness when ~/.agent/config.json is ready (QVAC + host bridge, or REST + API key + base URL): ask (required prompt; new/plan/auto/compact/max_turns/model), status, config, models, skills, todos, plan, hooks, history, recap, undo, rewind, compact, export, remember, reset, stop. History, skills, todos, and MEMORY.md are shared with the TTY; API keys are never shown. Whitelisted users can Direct Message the bot to talk to the agent without a slash command (same ~/.agent session; help / reset / stop as text). The first allowed DM opens a persistent message channel (~/.discord/channel.json). The agent can then reach out anytime with discord_send_message (live send via ctx.bareOsDiscordSendDm, or queue ~/.discord/outbox.json until the bot drains it). Inbound copies land in ~/.discord/inbox.json. Empty DISCORD_ID_WHITELIST denies every DM, inbound and outbound. /status sets the bot presence shown in the member list (playing, watching, listening, competing, custom, streaming plus online/idle/dnd/invisible); persisted as DISCORD_STATUS / DISCORD_ACTIVITY_TYPE / DISCORD_ACTIVITY_NAME in ~/.discord/.env and also under /settings Discord. /plugins loads JSON or JS from ~/.discord/plugins (see developer-guide ch.21). Also /settings, /files, /open-file, /panel, /edit, /create, /bare, /sys, /svc, /fs, /net, /man, /say, /journal, /ping. Embeds paginate when large and expire after 2 minutes idle; /open-file attachments stay. Token from --token, DISCORD_TOKEN, or ~/.discord/.env. User-installable (Add App to your Discord profile) is on by default so slash commands work in DMs and any server; DISCORD_ID_WHITELIST is always enforced on those surfaces (empty list denies user-install / DM use). DirectMessages intent is requested so DM chat works without the privileged Message Content Intent. The initd unit bare-os-discord is listed by systemctl only when ~/.discord/.env exists.",
"options": [
{
"flag": "--env PATH",
@@ -166,7 +166,7 @@ function discordUsage(argv0) {
'host DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env).\n' +
'Ctrl+C stops the foreground bot. Initd unit bare-os-discord appears in\n' +
'systemctl only when ~/.discord/.env exists with DISCORD_TOKEN=.\n' +
'Slash commands: /panel /files /edit /create /upload /hdms /holesail /agent /status /bare /sys /svc /fs /net /man /say /r /journal /ping.\n' +
'Slash commands: /panel /files /edit /create /upload /open-file /hdms /holesail /agent /status /bare /sys /svc /fs /net /man /say /r /journal /ping.\n' +
'Commands run as the unlocked session user. Options use Discord autocomplete.\n' +
'Whitelisted users can DM the bot to talk to /bin/agent (same ~/.agent session).\n' +
'/status sets playing / watching / listening / competing in the member list.\n' +
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long