This commit is contained in:
@@ -335,6 +335,7 @@ discord-bot --help
|
|||||||
| `/edit` | `path` | Modal editor for `~/` and `/tmp` (≤20 000 chars, 5 fields). Save writes via VFS |
|
| `/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`) |
|
| `/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` |
|
| `/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 **10 MiB**. 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) |
|
| `/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. |
|
| `/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. |
|
| `/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.
|
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`)
|
### Settings editor (`/settings`)
|
||||||
|
|
||||||
@@ -435,7 +436,7 @@ A file named `*.disabled` or a folder with `"enabled": false` is skipped.
|
|||||||
### Command names
|
### Command names
|
||||||
|
|
||||||
- `a-z`, `0-9`, hyphen; 1–32 characters; must start with a letter or digit.
|
- `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`.
|
- **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**.
|
- At most **40** plugins. Each source file is capped at **64 KiB**.
|
||||||
|
|
||||||
### JSON plugin SDK
|
### JSON plugin SDK
|
||||||
|
|||||||
+983
-52
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_BINS_AT = 0
|
||||||
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
|
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
|
||||||
var BARE_OS_DISCORD_UPLOAD_MS = 60000
|
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 = [
|
var BARE_OS_DISCORD_SH_BUILTINS = [
|
||||||
'alias',
|
'alias',
|
||||||
'unalias',
|
'unalias',
|
||||||
@@ -773,8 +775,11 @@ function discordCmdReplyPayload(result) {
|
|||||||
payload.components = discordUniqueComponents(result.components)
|
payload.components = discordUniqueComponents(result.components)
|
||||||
if (!payload.components.length) delete payload.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 (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
|
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
|
||||||
delete payload.fetchReply
|
delete payload.fetchReply
|
||||||
delete payload.ephemeral
|
delete payload.ephemeral
|
||||||
@@ -2829,7 +2834,10 @@ async function discordCmdHandleBare(ctx, sub) {
|
|||||||
discordField('/svc', 'Services — list, start, stop, restart, logs'),
|
discordField('/svc', 'Services — list, start, stop, restart, logs'),
|
||||||
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
|
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
|
||||||
discordField('/net', 'Swarm — peers, summary'),
|
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('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'),
|
||||||
discordField('/r /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
|
discordField('/r /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
|
||||||
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins'),
|
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)
|
var BARE_OS_DISCORD_HDMS_SESSIONS = Object.create(null)
|
||||||
|
|
||||||
function discordHdmsKey(interaction) {
|
function discordHdmsKey(interaction) {
|
||||||
@@ -6505,9 +6625,11 @@ var BARE_OS_DISCORD_PLUGIN_RESERVED = {
|
|||||||
ping: 1,
|
ping: 1,
|
||||||
plugins: 1,
|
plugins: 1,
|
||||||
upload: 1,
|
upload: 1,
|
||||||
|
'open-file': 1,
|
||||||
hdms: 1,
|
hdms: 1,
|
||||||
holesail: 1,
|
holesail: 1,
|
||||||
agent: 1
|
agent: 1,
|
||||||
|
status: 1
|
||||||
}
|
}
|
||||||
var BARE_OS_DISCORD_PLUGINS = []
|
var BARE_OS_DISCORD_PLUGINS = []
|
||||||
var BARE_OS_DISCORD_PLUGINS_READY = false
|
var BARE_OS_DISCORD_PLUGINS_READY = false
|
||||||
@@ -7075,6 +7197,9 @@ function discordBuildSlashCommands(dj, ctx) {
|
|||||||
const upload = new B2()
|
const upload = new B2()
|
||||||
.setName('upload')
|
.setName('upload')
|
||||||
.setDescription('Upload an attachment; wget saves it in the /r cwd or path')
|
.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()
|
const hdms = new B2()
|
||||||
.setName('hdms')
|
.setName('hdms')
|
||||||
.setDescription('Hyperdrive management (list, create, add, invite, pair)')
|
.setDescription('Hyperdrive management (list, create, add, invite, pair)')
|
||||||
@@ -7291,6 +7416,7 @@ function discordBuildSlashCommands(dj, ctx) {
|
|||||||
discordCmdOpt(files, 'path', 'Directory to open', false, true)
|
discordCmdOpt(files, 'path', 'Directory to open', false, true)
|
||||||
discordCmdAtt(upload, 'file', 'File to upload (Discord CDN, then wget into the VFS)', 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(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, {
|
discordCmdOpt(status, 'activity', 'playing, watching, listening, competing, custom, streaming', false, false, {
|
||||||
choices: [
|
choices: [
|
||||||
['Playing', 'playing'],
|
['Playing', 'playing'],
|
||||||
@@ -7313,7 +7439,7 @@ function discordBuildSlashCommands(dj, ctx) {
|
|||||||
['Invisible', 'invisible']
|
['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) {
|
function (c) {
|
||||||
return discordStampUserInstallCommand(c.toJSON(), ctx)
|
return discordStampUserInstallCommand(c.toJSON(), ctx)
|
||||||
}
|
}
|
||||||
@@ -9158,7 +9284,7 @@ async function discordSendResult(ctx, interaction, result) {
|
|||||||
if (result && result.settings) {
|
if (result && result.settings) {
|
||||||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
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 rec = discordFmGet(interaction, ctx)
|
||||||
const want = String(result.files || '').trim()
|
const want = String(result.files || '').trim()
|
||||||
if (want) {
|
if (want) {
|
||||||
@@ -9380,7 +9506,9 @@ async function discordSendResult(ctx, interaction, result) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await discordIdleWatch(interaction, payload, sent)
|
const persist =
|
||||||
|
(result && result.keep) || (payload.files && payload.files.length)
|
||||||
|
if (!persist) await discordIdleWatch(interaction, payload, sent)
|
||||||
} catch {
|
} catch {
|
||||||
/* tracking must not break the bot */
|
/* tracking must not break the bot */
|
||||||
}
|
}
|
||||||
@@ -10450,6 +10578,8 @@ async function discordDispatchInteraction(ctx, interaction) {
|
|||||||
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
|
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
|
||||||
} else if (name === 'upload') {
|
} else if (name === 'upload') {
|
||||||
result = await discordCmdHandleUpload(ctx, interaction, opt('path'))
|
result = await discordCmdHandleUpload(ctx, interaction, opt('path'))
|
||||||
|
} else if (name === 'open-file') {
|
||||||
|
result = await discordCmdHandleOpenFile(ctx, interaction, opt('path'))
|
||||||
} else if (name === 'hdms') {
|
} else if (name === 'hdms') {
|
||||||
result = await discordCmdHandleHdms(ctx, interaction, sub, opt)
|
result = await discordCmdHandleHdms(ctx, interaction, sub, opt)
|
||||||
} else if (name === 'holesail') {
|
} else if (name === 'holesail') {
|
||||||
@@ -10545,6 +10675,8 @@ var bareOsDiscordCommands = {
|
|||||||
parseHolesailList: discordParseHolesailList,
|
parseHolesailList: discordParseHolesailList,
|
||||||
parseHolesailShow: discordParseHolesailShow,
|
parseHolesailShow: discordParseHolesailShow,
|
||||||
pathOk: discordCmdPathOk,
|
pathOk: discordCmdPathOk,
|
||||||
|
openFileMax: BARE_OS_DISCORD_OPEN_FILE_MAX,
|
||||||
|
handleOpenFile: discordCmdHandleOpenFile,
|
||||||
result: discordResult,
|
result: discordResult,
|
||||||
packEmbed: discordPackEmbed,
|
packEmbed: discordPackEmbed,
|
||||||
textPages: discordTextPages,
|
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('settings') >= 0)
|
||||||
t.ok(names.indexOf('plugins') >= 0)
|
t.ok(names.indexOf('plugins') >= 0)
|
||||||
t.ok(names.indexOf('upload') >= 0)
|
t.ok(names.indexOf('upload') >= 0)
|
||||||
|
t.ok(names.indexOf('open-file') >= 0)
|
||||||
t.ok(names.indexOf('hdms') >= 0)
|
t.ok(names.indexOf('hdms') >= 0)
|
||||||
t.ok(names.indexOf('holesail') >= 0)
|
t.ok(names.indexOf('holesail') >= 0)
|
||||||
t.ok(names.indexOf('agent') >= 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 uploadCmd = body.find((c) => c.name === 'upload')
|
||||||
const fileOpt = uploadCmd && (uploadCmd.options || []).find((o) => o.name === 'file')
|
const fileOpt = uploadCmd && (uploadCmd.options || []).find((o) => o.name === 'file')
|
||||||
t.ok(fileOpt && fileOpt.type === 11 && fileOpt.required, '/upload file attachment is required')
|
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)
|
t.ok(body.length >= 8)
|
||||||
|
|
||||||
const replies = []
|
const replies = []
|
||||||
@@ -360,6 +364,7 @@ test('discord command catalog builds Bare OS slash commands', async (t) => {
|
|||||||
await cmds.dispatchInteraction({ env: {}, vfs: {}, console: {} }, interaction)
|
await cmds.dispatchInteraction({ env: {}, vfs: {}, console: {} }, interaction)
|
||||||
const helpBlob = JSON.stringify(replies[0] || {})
|
const helpBlob = JSON.stringify(replies[0] || {})
|
||||||
t.ok(/\/bare/.test(helpBlob), 'help mentions /bare')
|
t.ok(/\/bare/.test(helpBlob), 'help mentions /bare')
|
||||||
|
t.ok(/open-file/.test(helpBlob), 'help mentions /open-file')
|
||||||
assertUniqueCustomIds(t, replies[0], 'help')
|
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')
|
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) => {
|
test('Discord /hdms manages extra Hyperdrives', async (t) => {
|
||||||
const req = createRequire(fileURLToPath(import.meta.url))
|
const req = createRequire(fileURLToPath(import.meta.url))
|
||||||
const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs')
|
const cmds = req('./lib/services/bare-os-discord-commands-guest.cjs')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Unreleased
|
## 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`).
|
- **`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).
|
- **`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`**.
|
- **`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": [
|
"synopsis": [
|
||||||
"discord-bot [--env PATH] [--token TOKEN] [--guild ID] [--check] [--debug] [--message-content] [--login-timeout MS] [--no-user-install]"
|
"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": [
|
"options": [
|
||||||
{
|
{
|
||||||
"flag": "--env PATH",
|
"flag": "--env PATH",
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ function discordUsage(argv0) {
|
|||||||
'host DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env).\n' +
|
'host DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env).\n' +
|
||||||
'Ctrl+C stops the foreground bot. Initd unit bare-os-discord appears in\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' +
|
'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' +
|
'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' +
|
'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' +
|
'/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
Reference in New Issue
Block a user