This commit is contained in:
@@ -311,31 +311,157 @@ function bareOsHexEncode(u8) {
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared GNU-style checksum FILE / stdin loop used by *sum commands.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} argv
|
||||
* @param {{
|
||||
* cmd: string,
|
||||
* help?: string,
|
||||
* digest: (u8: Uint8Array) => string | Promise<string>
|
||||
* }} opts
|
||||
*/
|
||||
async function bareChecksumRun(ctx, argv, opts) {
|
||||
const cmd = String(opts.cmd || 'checksum')
|
||||
const help =
|
||||
opts.help ||
|
||||
'usage: ' +
|
||||
cmd +
|
||||
' [FILE]...\nWith no FILE, or when FILE is -, read standard input.'
|
||||
const digest = opts.digest
|
||||
const paths = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-h' || a === '--help') {
|
||||
ctx.console.log(help)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-') && a !== '-') {
|
||||
ctx.console.error(cmd + ': unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
paths.push(a)
|
||||
}
|
||||
const b4 = ctx.b4a
|
||||
async function one(name, buf) {
|
||||
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
|
||||
try {
|
||||
const hex = await digest(u8)
|
||||
ctx.console.log(hex + ' ' + name)
|
||||
} catch (e) {
|
||||
ctx.console.error(cmd + ': ' + (e.message || e))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
|
||||
await one('-', b4.from(bareStdin(ctx)))
|
||||
return
|
||||
}
|
||||
for (const p of paths) {
|
||||
if (p === '-') {
|
||||
await one('-', b4.from(bareStdin(ctx)))
|
||||
continue
|
||||
}
|
||||
const b = await ctx.vfs.readFile(p)
|
||||
if (!b) {
|
||||
ctx.console.error(cmd + ': ' + p + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
continue
|
||||
}
|
||||
await one(p, b)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebCrypto hex digest; throws the same missing-subtle message as the old *sum files.
|
||||
* @param {string} algo
|
||||
* @param {string} missingMsg
|
||||
* @returns {(u8: Uint8Array) => Promise<string>}
|
||||
*/
|
||||
function bareSubtleHexDigest(algo, missingMsg) {
|
||||
return async function (u8) {
|
||||
const subtle = globalThis.crypto?.subtle
|
||||
if (!subtle || typeof subtle.digest !== 'function') {
|
||||
throw new Error(missingMsg)
|
||||
}
|
||||
const hash = await subtle.digest(algo, u8)
|
||||
return bareOsHexEncode(new Uint8Array(hash))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner/group name → uid/gid used by chown / chgrp.
|
||||
* @param {string} spec
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {number | null}
|
||||
*/
|
||||
function parseGroupSpec(spec, env) {
|
||||
const s = String(spec).trim()
|
||||
if (!s) return null
|
||||
if (/^\d+$/.test(s)) {
|
||||
const n = Number.parseInt(s, 10)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||
if (s === 'root') return 0
|
||||
if (s === 'guest' || s === 'nobody') return 65534
|
||||
if (g && s === g) {
|
||||
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||
return Number.isFinite(gid) ? gid : 65534
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* User name / numeric uid used by chown.
|
||||
* @param {string} spec
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {{ uid: number | null, gid: number | null }}
|
||||
*/
|
||||
function parseIdSpec(spec, env) {
|
||||
const s = String(spec).trim()
|
||||
if (!s) return { uid: null, gid: null }
|
||||
if (/^\d+$/.test(s)) {
|
||||
const n = Number.parseInt(s, 10)
|
||||
return { uid: Number.isFinite(n) ? n : null, gid: null }
|
||||
}
|
||||
const u = (env.USER || env.LOGNAME || '').trim()
|
||||
if (s === 'root') return { uid: 0, gid: null }
|
||||
if (s === 'guest' || s === 'nobody') return { uid: 65534, gid: null }
|
||||
if (u && s === u) {
|
||||
const uid = Number.parseInt(String(env.UID ?? '65534'), 10)
|
||||
return { uid: Number.isFinite(uid) ? uid : 65534, gid: null }
|
||||
}
|
||||
return { uid: null, gid: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared QVAC chat catalog + REST /models helpers (preamble for agent and discord-bot).
|
||||
*/
|
||||
|
||||
/** @type {{ id: string, family: string, label: string, tools: boolean, ramGb: number, profile?: string }[]} */
|
||||
/** @type {{ id: string, family: string, label: string, tools: boolean, ramGb: number, ctxSize: number, profile?: string }[]} */
|
||||
var BARE_AGENT_QVAC_CHAT_MODELS = [
|
||||
{ id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite' },
|
||||
{ id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended' },
|
||||
{ id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong' },
|
||||
{ id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16 },
|
||||
{ id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24 },
|
||||
{ id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny' },
|
||||
{ id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6 },
|
||||
{ id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3 },
|
||||
{ id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24 },
|
||||
{ id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8 },
|
||||
{ id: 'GEMMA4_2B_MULTIMODAL_Q6_K', family: 'gemma', label: 'Gemma 4 2B multimodal Q6', tools: true, ramGb: 10 },
|
||||
{ id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16 },
|
||||
{ id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48 },
|
||||
{ id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10 },
|
||||
{ id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10 },
|
||||
{ id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16 },
|
||||
{ id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 8B multimodal Q4', tools: true, ramGb: 24 },
|
||||
{ id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28 },
|
||||
{ id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48 }
|
||||
{ id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite', ctxSize: 32768 },
|
||||
{ id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended', ctxSize: 32768 },
|
||||
{ id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong', ctxSize: 32768 },
|
||||
{ id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16, ctxSize: 32768 },
|
||||
{ id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24, ctxSize: 32768 },
|
||||
{ id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny', ctxSize: 131072 },
|
||||
{ id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6, ctxSize: 131072 },
|
||||
{ id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3, ctxSize: 8192 },
|
||||
{ id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24, ctxSize: 131072 },
|
||||
{ id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8, ctxSize: 131072 },
|
||||
{ id: 'GEMMA4_2B_MULTIMODAL_Q6_K', family: 'gemma', label: 'Gemma 4 2B multimodal Q6', tools: true, ramGb: 10, ctxSize: 131072 },
|
||||
{ id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16, ctxSize: 131072 },
|
||||
{ id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48, ctxSize: 262144 },
|
||||
{ id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10, ctxSize: 131072 },
|
||||
{ id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10, ctxSize: 262144 },
|
||||
{ id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16, ctxSize: 262144 },
|
||||
{ id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 0.8B multimodal Q4', tools: true, ramGb: 8, ctxSize: 262144 },
|
||||
{ id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28, ctxSize: 262144 },
|
||||
{ id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48, ctxSize: 262144 }
|
||||
]
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
@@ -437,6 +563,72 @@ function bareAgentParseOpenAiModels(json) {
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep an explicit QVAC model id (Discord / `--model` / picker).
|
||||
* Profile defaults only apply when no model is set.
|
||||
* @param {Record<string, unknown>} config
|
||||
* @param {{ chatModel?: string }} [profile]
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
function bareAgentHonorExplicitQvacModel(config, profile) {
|
||||
const out = config && typeof config === 'object' ? config : {}
|
||||
const explicit = String(out.qvac_model || out.model || '').trim()
|
||||
if (explicit) {
|
||||
out.qvac_model = explicit
|
||||
out.model = explicit
|
||||
if (typeof bareAgentQvacSyncSettingsForModel === 'function') {
|
||||
bareAgentQvacSyncSettingsForModel(out, explicit)
|
||||
} else {
|
||||
const card =
|
||||
typeof bareAgentQvacFindChatModel === 'function'
|
||||
? bareAgentQvacFindChatModel(explicit)
|
||||
: null
|
||||
if (card && card.profile) out.qvac_profile = card.profile
|
||||
}
|
||||
return out
|
||||
}
|
||||
const fallback = String((profile && profile.chatModel) || '').trim()
|
||||
if (fallback) {
|
||||
out.qvac_model = fallback
|
||||
out.model = fallback
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a live model switch for the current backend.
|
||||
* @param {Record<string, unknown>} config
|
||||
* @param {string} modelId
|
||||
* @returns {Record<string, unknown>}
|
||||
*/
|
||||
function bareAgentApplyLiveModel(config, modelId) {
|
||||
const out = { ...(config && typeof config === 'object' ? config : {}) }
|
||||
const id = String(modelId || '').trim()
|
||||
if (!id) return out
|
||||
let backend = 'qvac'
|
||||
if (typeof bareAgentResolveBackend === 'function') {
|
||||
backend = bareAgentResolveBackend(out)
|
||||
} else {
|
||||
const b = String(out.backend || '').trim().toLowerCase()
|
||||
const p = String(out.provider || '').trim().toLowerCase()
|
||||
if (b === 'rest' || b === 'openai' || b === 'http') backend = 'rest'
|
||||
else if (b === 'qvac') backend = 'qvac'
|
||||
else if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') backend = 'rest'
|
||||
else if (out.rest_api_key && String(out.rest_api_key).trim()) backend = 'rest'
|
||||
}
|
||||
if (backend === 'qvac') {
|
||||
out.backend = 'qvac'
|
||||
out.provider = 'qvac'
|
||||
out.qvac_model = id
|
||||
out.model = id
|
||||
out.qvac_ctx_size = 0
|
||||
return bareAgentHonorExplicitQvacModel(out, null)
|
||||
}
|
||||
out.backend = 'rest'
|
||||
out.model = id
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [provider]
|
||||
*/
|
||||
@@ -3344,17 +3536,16 @@ async function discordCmdHandleBare(ctx, sub) {
|
||||
title: 'Commands',
|
||||
desc: 'Running as **' + user + '**. Open **Menu** for destinations, or use a slash command.',
|
||||
fields: [
|
||||
discordField('/bare', 'Session — status, whoami, hostname, date, uptime, motd, uname'),
|
||||
discordField('/sys', 'Host — disk, memory, processes, env, doctor, features, limits'),
|
||||
discordField('/svc', 'Services — list, start, stop, restart, logs'),
|
||||
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
|
||||
discordField('/net', 'Swarm — peers, summary'),
|
||||
discordField('/panel', 'Control panel (also Menu on every reply)'),
|
||||
discordField('/bare', 'Session — ping, about, help, status, whoami, hostname, date, uptime, motd, uname, presence'),
|
||||
discordField('/sys', 'Host — disk, memory, processes, env, doctor, features, limits, peers, swarm'),
|
||||
discordField('/svc', 'Services — list, start, stop, restart, logs, journal'),
|
||||
discordField(
|
||||
'/files /edit /create /upload /open-file',
|
||||
'Browse, edit, wget in, or attach a VFS file out (CDN, kept)'
|
||||
'/files',
|
||||
'VFS — browse, ls, cat, stat, head, edit, create, upload, open-file (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('/r /say /man', 'Full shell (cwd + autocomplete), speak, man pages'),
|
||||
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins'),
|
||||
discordField('/hdms', 'Hyperdrives — list, create, add, invite, pair, /mnt'),
|
||||
discordField('/holesail', 'Tunnels — list, add, edit, start/stop, enable, service'),
|
||||
@@ -3362,8 +3553,7 @@ async function discordCmdHandleBare(ctx, sub) {
|
||||
discordField(
|
||||
'DM this bot',
|
||||
'Whitelisted users: send a normal message in a Direct Message to talk to the agent (same ~/.agent history). help / reset / stop work as text.'
|
||||
),
|
||||
discordField('/status', 'Bot presence — playing / watching / listening / competing')
|
||||
)
|
||||
]
|
||||
})
|
||||
)
|
||||
@@ -4493,7 +4683,7 @@ 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).',
|
||||
text: 'Give `/files open-file` a VFS path (e.g. `~/notes.txt`). The bot attaches it to this channel (Discord CDN; not auto-deleted).',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
@@ -7675,27 +7865,16 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
const finish = function () {
|
||||
const B2 = dj.SlashCommandBuilder
|
||||
const bare = new B2().setName('bare').setDescription('Bare OS session (logged-in user)')
|
||||
const sys = new B2().setName('sys').setDescription('Bare OS system snapshots')
|
||||
const svc = new B2().setName('svc').setDescription('systemctl units')
|
||||
const fsCmd = new B2().setName('fs').setDescription('Read-only VFS')
|
||||
const net = new B2().setName('net').setDescription('Swarm / network')
|
||||
const sys = new B2().setName('sys').setDescription('Bare OS system snapshots and swarm')
|
||||
const svc = new B2().setName('svc').setDescription('systemctl units and journal')
|
||||
const man = new B2().setName('man').setDescription('Look up a man page')
|
||||
const say = new B2().setName('say').setDescription('Speak as Bare OS (or open a compose form)')
|
||||
const run = new B2()
|
||||
.setName('r')
|
||||
.setDescription('Run a Bare OS shell command (cwd, pipes, cd, history)')
|
||||
const journal = new B2()
|
||||
.setName('journal')
|
||||
.setDescription('Tail a unit or system log')
|
||||
const edit = new B2()
|
||||
.setName('edit')
|
||||
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
|
||||
const create = new B2()
|
||||
.setName('create')
|
||||
.setDescription('Create a new text file (pick a path, then enter contents)')
|
||||
const files = new B2()
|
||||
.setName('files')
|
||||
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
|
||||
.setDescription('VFS: browse, read, edit, create, upload, attach')
|
||||
const settings = new B2()
|
||||
.setName('settings')
|
||||
.setDescription('Live session settings (theme, shell, discord, agent, aliases)')
|
||||
@@ -7705,16 +7884,6 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
const panel = new B2()
|
||||
.setName('panel')
|
||||
.setDescription('Interactive Bare OS control panel')
|
||||
const ping = new B2().setName('ping').setDescription('Reply pong')
|
||||
const status = new B2()
|
||||
.setName('status')
|
||||
.setDescription('Bot presence in the member list (playing / watching / …)')
|
||||
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)')
|
||||
@@ -7735,7 +7904,31 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
['date', 'Clock'],
|
||||
['uptime', '/proc/uptime'],
|
||||
['motd', '/etc/motd'],
|
||||
['uname', 'uname -a style']
|
||||
['uname', 'uname -a style'],
|
||||
['presence', 'Bot presence in the member list', function (s) {
|
||||
discordCmdOpt(s, 'activity', 'playing, watching, listening, competing, custom, streaming', false, false, {
|
||||
choices: [
|
||||
['Playing', 'playing'],
|
||||
['Watching', 'watching'],
|
||||
['Listening', 'listening'],
|
||||
['Competing', 'competing'],
|
||||
['Custom', 'custom'],
|
||||
['Streaming', 'streaming']
|
||||
]
|
||||
})
|
||||
discordCmdOpt(s, 'name', 'Activity text shown in the member list', false, false, {
|
||||
min: 1,
|
||||
max: 128
|
||||
})
|
||||
discordCmdOpt(s, 'state', 'online, idle, dnd, or invisible', false, false, {
|
||||
choices: [
|
||||
['Online', 'online'],
|
||||
['Idle', 'idle'],
|
||||
['Do not disturb', 'dnd'],
|
||||
['Invisible', 'invisible']
|
||||
]
|
||||
})
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(sys, [
|
||||
['df', 'Disk / host resources'],
|
||||
@@ -7744,7 +7937,10 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
['env', 'Redacted environment'],
|
||||
['doctor', 'Security / debug posture'],
|
||||
['features', '/proc/bare_os_features'],
|
||||
['rlimits', 'Resource limits']
|
||||
['rlimits', 'Resource limits'],
|
||||
['peers', 'Swarm peers'],
|
||||
['swarm', 'Swarm snapshot'],
|
||||
['summary', 'net_summary']
|
||||
]) &&
|
||||
discordCmdAddSubs(svc, [
|
||||
['list', 'systemctl list'],
|
||||
@@ -7762,9 +7958,15 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
}],
|
||||
['logs', 'Unit logs', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['journal', 'Tail a unit or system log', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', false, true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(fsCmd, [
|
||||
discordCmdAddSubs(files, [
|
||||
['browse', 'Interactive file manager', function (s) {
|
||||
discordCmdOpt(s, 'path', 'Directory to open', false, true)
|
||||
}],
|
||||
['ls', 'List a directory', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', false, true)
|
||||
}],
|
||||
@@ -7777,13 +7979,21 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
['head', 'First lines of a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
discordCmdOpt(s, 'lines', 'Line count', false, false)
|
||||
}],
|
||||
['edit', 'Edit a text file in a Discord modal', function (s) {
|
||||
discordCmdOpt(s, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
|
||||
}],
|
||||
['create', 'Create a new text file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
|
||||
}],
|
||||
['upload', 'Upload an attachment; wget saves it in the /r cwd or path', function (s) {
|
||||
discordCmdAtt(s, 'file', 'File to upload (Discord CDN, then wget into the VFS)', true)
|
||||
discordCmdOpt(s, 'path', 'VFS dest (file or dir; default: /r cwd + name)', false, true)
|
||||
}],
|
||||
['open-file', 'Send a VFS file as a Discord attachment (kept)', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS file to attach (uploaded to the Discord CDN)', true, true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(net, [
|
||||
['peers', 'Swarm peers'],
|
||||
['swarm', 'Swarm snapshot'],
|
||||
['summary', 'net_summary']
|
||||
]) &&
|
||||
discordCmdAddSubs(plugins, [
|
||||
['list', 'Loaded plugins'],
|
||||
['reload', 'Rescan ~/.discord/plugins'],
|
||||
@@ -7920,41 +8130,13 @@ function discordBuildSlashCommands(dj, ctx) {
|
||||
['stop', 'Abort the in-flight turn']
|
||||
])
|
||||
if (!ok) {
|
||||
return [ping.toJSON()]
|
||||
const ping = new B2().setName('ping').setDescription('Reply pong')
|
||||
return [discordStampUserInstallCommand(ping.toJSON(), ctx)]
|
||||
}
|
||||
discordCmdOpt(man, 'page', 'Command name (e.g. uname)', true, true)
|
||||
discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false)
|
||||
discordCmdOpt(run, 'cmd', 'Shell line (cd, pipes, &&, redirects, $VAR)', true, true)
|
||||
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
|
||||
discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
|
||||
discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
|
||||
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'],
|
||||
['Watching', 'watching'],
|
||||
['Listening', 'listening'],
|
||||
['Competing', 'competing'],
|
||||
['Custom', 'custom'],
|
||||
['Streaming', 'streaming']
|
||||
]
|
||||
})
|
||||
discordCmdOpt(status, 'name', 'Activity text shown in the member list', false, false, {
|
||||
min: 1,
|
||||
max: 128
|
||||
})
|
||||
discordCmdOpt(status, 'state', 'online, idle, dnd, or invisible', false, false, {
|
||||
choices: [
|
||||
['Online', 'online'],
|
||||
['Idle', 'idle'],
|
||||
['Do not disturb', 'dnd'],
|
||||
['Invisible', 'invisible']
|
||||
]
|
||||
})
|
||||
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(
|
||||
const stock = [bare, sys, svc, files, man, say, run, settings, plugins, panel, hdms, holesail, agent].map(
|
||||
function (c) {
|
||||
return discordStampUserInstallCommand(c.toJSON(), ctx)
|
||||
}
|
||||
@@ -8012,15 +8194,40 @@ function discordShowModal(interaction, spec) {
|
||||
return discordShowForm(interaction, spec)
|
||||
}
|
||||
|
||||
function discordCmdHandleEdit(opt) {
|
||||
const path = opt('path')
|
||||
if (!path) return { modal: 'edit:path' }
|
||||
return { edit: path }
|
||||
}
|
||||
|
||||
function discordCmdHandleCreate(opt) {
|
||||
const path = opt('path')
|
||||
if (!path) return { createPick: true }
|
||||
return { create: path }
|
||||
}
|
||||
|
||||
async function discordRouteCommand(ctx, name, sub, opt) {
|
||||
if (name === 'panel') return discordPanel(ctx)
|
||||
if (name === 'ping' || (name === 'bare' && (sub === 'ping' || !sub))) {
|
||||
return discordCmdHandleBare(ctx, 'ping')
|
||||
}
|
||||
if (name === 'bare') return discordCmdHandleBare(ctx, sub)
|
||||
if (name === 'sys') return discordCmdHandleSys(ctx, sub)
|
||||
if (name === 'svc') return discordCmdHandleSvc(ctx, sub, opt('unit'))
|
||||
if (name === 'fs') return discordCmdHandleFs(ctx, sub, opt('path'), opt('lines'))
|
||||
if (name === 'bare') {
|
||||
if (sub === 'presence') return discordCmdHandlePresence(ctx, opt)
|
||||
return discordCmdHandleBare(ctx, sub)
|
||||
}
|
||||
if (name === 'sys') {
|
||||
if (sub === 'peers' || sub === 'swarm' || sub === 'summary') {
|
||||
return discordCmdHandleNet(ctx, sub)
|
||||
}
|
||||
return discordCmdHandleSys(ctx, sub)
|
||||
}
|
||||
if (name === 'svc') {
|
||||
if (sub === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
|
||||
return discordCmdHandleSvc(ctx, sub, opt('unit'))
|
||||
}
|
||||
if (name === 'fs' || (name === 'files' && (sub === 'ls' || sub === 'cat' || sub === 'stat' || sub === 'head'))) {
|
||||
return discordCmdHandleFs(ctx, sub, opt('path'), opt('lines'))
|
||||
}
|
||||
if (name === 'net') return discordCmdHandleNet(ctx, sub)
|
||||
if (name === 'man') return discordCmdHandleMan(ctx, opt('page'))
|
||||
if (name === 'say') {
|
||||
@@ -8035,16 +8242,8 @@ async function discordRouteCommand(ctx, name, sub, opt) {
|
||||
}
|
||||
if (name === 'r' || name === 'run') return { run: opt('cmd') || '' }
|
||||
if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
|
||||
if (name === 'edit') {
|
||||
const path = opt('path')
|
||||
if (!path) return { modal: 'edit:path' }
|
||||
return { edit: path }
|
||||
}
|
||||
if (name === 'create') {
|
||||
const path = opt('path')
|
||||
if (!path) return { createPick: true }
|
||||
return { create: path }
|
||||
}
|
||||
if (name === 'edit' || (name === 'files' && sub === 'edit')) return discordCmdHandleEdit(opt)
|
||||
if (name === 'create' || (name === 'files' && sub === 'create')) return discordCmdHandleCreate(opt)
|
||||
if (name === 'files' || name === 'browse') {
|
||||
return { files: opt('path') || '' }
|
||||
}
|
||||
@@ -8110,7 +8309,7 @@ async function discordEditSaveFromModal(ctx, interaction) {
|
||||
const rec = discordEditGet(interaction)
|
||||
if (!rec || !rec.path) {
|
||||
return {
|
||||
text: 'Edit session expired (15 minutes). Run `/edit` again.',
|
||||
text: 'Edit session expired (15 minutes). Run `/files edit` again.',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
@@ -8134,7 +8333,7 @@ async function discordEditSaveFromModal(ctx, interaction) {
|
||||
const body = parts.join('')
|
||||
if (rec.created && (await discordFileExists(ctx, p))) {
|
||||
return {
|
||||
text: '`' + p + '` already exists. Use `/edit` to change it.',
|
||||
text: '`' + p + '` already exists. Use `/files edit` to change it.',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
@@ -10080,12 +10279,24 @@ async function discordDispatchAutocomplete(ctx, interaction) {
|
||||
else if (fname === 'cmd') choices = await discordSuggestRun(ctx, interaction, q)
|
||||
else if (fname === 'path') {
|
||||
const cmd = String(interaction.commandName || '')
|
||||
choices =
|
||||
cmd === 'edit' || cmd === 'create' || cmd === 'upload'
|
||||
? discordSuggestEditPaths(ctx, q)
|
||||
: cmd === 'hdms'
|
||||
? await discordSuggestHdmsLabels(ctx, q)
|
||||
: discordSuggestPaths(ctx, q)
|
||||
let sub = ''
|
||||
try {
|
||||
if (interaction.options && typeof interaction.options.getSubcommand === 'function') {
|
||||
sub = String(interaction.options.getSubcommand(false) || '')
|
||||
}
|
||||
} catch {
|
||||
sub = ''
|
||||
}
|
||||
const writePath =
|
||||
cmd === 'edit' ||
|
||||
cmd === 'create' ||
|
||||
cmd === 'upload' ||
|
||||
(cmd === 'files' && (sub === 'edit' || sub === 'create' || sub === 'upload'))
|
||||
choices = writePath
|
||||
? discordSuggestEditPaths(ctx, q)
|
||||
: cmd === 'hdms'
|
||||
? await discordSuggestHdmsLabels(ctx, q)
|
||||
: discordSuggestPaths(ctx, q)
|
||||
} else if (fname === 'label' && String(interaction.commandName || '') === 'hdms') {
|
||||
choices = await discordSuggestHdmsLabels(ctx, q)
|
||||
} else if (fname === 'id' && String(interaction.commandName || '') === 'holesail') {
|
||||
@@ -10924,7 +11135,7 @@ function discordDmHelpResult() {
|
||||
discordField('reset / !reset / !new', 'Clear chat history (keeps config)'),
|
||||
discordField('stop / !stop', 'Abort the in-flight turn'),
|
||||
discordField('ping', 'Pong'),
|
||||
discordField('/agent /status', 'Slash commands still work in this DM'),
|
||||
discordField('/agent /bare presence', 'Slash commands still work in this DM'),
|
||||
discordField(
|
||||
'Agent reach-out',
|
||||
'The agent can DM you first on this channel (`discord_send_message`). Same whitelist.'
|
||||
@@ -11091,9 +11302,12 @@ async function discordDispatchInteraction(ctx, interaction) {
|
||||
await discordPluginsEnsure(ctx)
|
||||
if (name === 'plugins') {
|
||||
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
|
||||
} else if (name === 'upload') {
|
||||
} else if (name === 'upload' || (name === 'files' && sub === 'upload')) {
|
||||
result = await discordCmdHandleUpload(ctx, interaction, opt('path'))
|
||||
} else if (name === 'open-file') {
|
||||
} else if (
|
||||
name === 'open-file' ||
|
||||
(name === 'files' && (sub === 'open-file' || sub === 'attach'))
|
||||
) {
|
||||
result = await discordCmdHandleOpenFile(ctx, interaction, opt('path'))
|
||||
} else if (name === 'hdms') {
|
||||
result = await discordCmdHandleHdms(ctx, interaction, sub, opt)
|
||||
@@ -11214,6 +11428,7 @@ var bareOsDiscordCommands = {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = bareOsDiscordCommands
|
||||
}
|
||||
|
||||
/**
|
||||
* Stock Discord ping-pong bot via ctx.bare.discordJS (vendored bare-discord-js).
|
||||
* Token: --token, DISCORD_TOKEN, --env PATH, DISCORD_ENV_FILE, ~/.discord.env.
|
||||
@@ -11382,10 +11597,10 @@ 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 /open-file /hdms /holesail /agent /status /bare /sys /svc /fs /net /man /say /r /journal /ping.\n' +
|
||||
'Slash commands: /panel /bare /sys /svc /files /man /say /r /settings /plugins /hdms /holesail /agent.\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' +
|
||||
'/bare presence sets playing / watching / listening / competing in the member list.\n' +
|
||||
'Channel "ping" still replies pong when Message Content Intent is enabled.\n' +
|
||||
'User-installable (profile app) is on by default so operators can use slash\n' +
|
||||
'commands in DMs and any server. DISCORD_ID_WHITELIST is always enforced on\n' +
|
||||
|
||||
Reference in New Issue
Block a user