/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** Session env map (`vfs.env`, then `ctx.env`). Never throws. */ function bareOsEnv(ctx) { const v = ctx && ctx.vfs && ctx.vfs.env if (v && typeof v === 'object') return v const e = ctx && ctx.env if (e && typeof e === 'object') return e return {} } /** * Strict POSIX-ish decimal integer (no octal, no exponent, no empty). * @param {unknown} s * @returns {number} */ function bareOsParseDecInt(s) { const t = String(s == null ? '' : s).trim() if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN const n = Number.parseInt(t, 10) return Number.isSafeInteger(n) ? n : NaN } /** @param {unknown} s */ function bareOsParseNonNegInt(s) { const n = bareOsParseDecInt(s) return n >= 0 ? n : NaN } /** * @param {Record} ctx * @param {string} name * @param {number} fallback * @param {number} [min] * @param {number} [max] */ function bareOsEnvInt(ctx, name, fallback, min, max) { const raw = bareOsEnv(ctx)[name] if (raw == null || raw === '') return fallback const n = Number.parseInt(String(raw), 10) if (!Number.isFinite(n)) return fallback let v = n if (min != null && v < min) v = min if (max != null && v > max) v = max return v } /** * @param {Record} ctx * @param {string} msg * @param {number} [code] */ function bareOsFail(ctx, msg, code) { if (msg) ctx.console.error(msg) ctx.exitCode = code == null ? 1 : code } /** @param {unknown} e */ function bareOsIsNotFoundErr(e) { const code = e && typeof e === 'object' ? e.code : '' if (code === 'ENOENT') return true const msg = String((e && e.message) || e || '') return /ENOENT|No such file|not found/i.test(msg) } /** * @param {Record} ctx * @param {unknown} buf * @returns {Uint8Array} */ function bareOsToU8(ctx, buf) { if (!buf) return new Uint8Array(0) if (buf instanceof Uint8Array) return buf if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf) return new Uint8Array(buf) } /** @param {string} dir @param {string} name */ function bareOsJoinPath(dir, name) { const d = String(dir || '').replace(/\/+$/, '') const n = String(name || '').replace(/^\/+/, '') if (!d || d === '/') return '/' + n return d + '/' + n } /** @param {string} p */ function bareOsBaseName(p) { const t = String(p || '').replace(/\/+$/, '') if (!t || t === '/') return t === '/' ? '/' : '' const i = t.lastIndexOf('/') return i < 0 ? t : t.slice(i + 1) || t } /** @param {string} p */ function bareOsParentDir(p) { const t = String(p || '').replace(/\/+$/, '') || '/' if (t === '/') return '/' const i = t.lastIndexOf('/') return i <= 0 ? '/' : t.slice(0, i) || '/' } /** @param {string} p */ function bareOsNormPath(p) { return String(p || '').replace(/\/+$/, '') || '/' } /** * @param {Record} ctx * @param {string} p */ function bareOsResolvePath(ctx, p) { if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') { try { return String(ctx.vfs.resolveLogical(p) || p) } catch { /* fall through */ } } return String(p || '') } /** * True when dest is src or lives under src (self-copy / self-move). * @param {Record} ctx * @param {string} src * @param {string} dest */ function bareOsDestInsideSrc(ctx, src, dest) { const s = bareOsNormPath(bareOsResolvePath(ctx, src)) const d = bareOsNormPath(bareOsResolvePath(ctx, dest)) if (s === d) return true if (s === '/') return d !== '/' return d === s || d.startsWith(s + '/') } const BARE_OS_B64_ALPH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' /** @param {Uint8Array} u8 */ function bareOsB64Encode(u8) { let out = '' let i = 0 for (; i + 2 < u8.length; i += 3) { const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2] out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + BARE_OS_B64_ALPH[n & 63] } const rest = u8.length - i if (rest === 1) { const n = u8[i] << 16 out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '==' } else if (rest === 2) { const n = (u8[i] << 16) | (u8[i + 1] << 8) out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + '=' } return out } /** * RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk. * @param {string} s * @returns {Uint8Array} */ function bareOsB64Decode(s) { const t = String(s).replace(/\s+/g, '') if (!t) return new Uint8Array(0) if (t.length % 4 === 1) throw new Error('invalid base64 length') let pad = 0 if (t.endsWith('==')) pad = 2 else if (t.endsWith('=')) pad = 1 const body = pad ? t.slice(0, t.length - pad) : t const bytes = [] let buf = 0 let bits = 0 for (let i = 0; i < body.length; i++) { const c = body[i] let v = BARE_OS_B64_ALPH.indexOf(c) if (v < 0) { if (c === '-') v = 62 else if (c === '_') v = 63 else throw new Error('invalid base64 character') } buf = (buf << 6) | v bits += 6 if (bits >= 8) { bits -= 8 bytes.push((buf >> bits) & 255) } } if (pad) { const want = Math.floor((body.length * 6) / 8) if (bytes.length > want) bytes.length = want } return new Uint8Array(bytes) } /** * @param {string} s * @returns {Uint8Array} */ function bareOsHexDecode(s) { const t = String(s).replace(/\s+/g, '') if (t.length % 2 !== 0) throw new Error('odd hex length') const out = new Uint8Array(t.length / 2) for (let i = 0; i < out.length; i++) { const pair = t.slice(i * 2, i * 2 + 2) if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex') out[i] = Number.parseInt(pair, 16) } return out } /** @param {Uint8Array} u8 */ function bareOsHexEncode(u8) { let s = '' for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0') return s } /** * 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 }[]} */ 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 } ] /** @type {Record} */ var BARE_AGENT_REST_MODEL_FALLBACKS = { groq: [ 'llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'openai/gpt-oss-120b', 'openai/gpt-oss-20b', 'qwen/qwen3-32b', 'moonshotai/kimi-k2-instruct', 'meta-llama/llama-4-scout-17b-16e-instruct', 'meta-llama/llama-4-maverick-17b-128e-instruct', 'groq/compound' ], xai: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212', 'grok-2-vision-1212'], openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3'], custom: [] } /** * @param {string} [profileId] */ function bareAgentQvacModelForProfile(profileId) { const id = String(profileId || '').trim().toLowerCase() for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) { if (BARE_AGENT_QVAC_CHAT_MODELS[i].profile === id) return BARE_AGENT_QVAC_CHAT_MODELS[i] } return BARE_AGENT_QVAC_CHAT_MODELS[1] || BARE_AGENT_QVAC_CHAT_MODELS[0] } /** * @param {string} [modelId] */ function bareAgentQvacFindChatModel(modelId) { const id = String(modelId || '').trim() for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) { if (BARE_AGENT_QVAC_CHAT_MODELS[i].id === id) return BARE_AGENT_QVAC_CHAT_MODELS[i] } return null } /** * @param {{ id: string, label?: string, family?: string }[]} models * @param {{ family?: string, query?: string }} [opts] */ function bareAgentFilterModelList(models, opts) { const o = opts && typeof opts === 'object' ? opts : {} const fam = String(o.family || 'all').trim().toLowerCase() const q = String(o.query || '').trim().toLowerCase() const rows = Array.isArray(models) ? models : [] /** @type {{ id: string, label?: string, family?: string }[]} */ const out = [] for (let i = 0; i < rows.length; i++) { const m = rows[i] if (!m || !m.id) continue if (fam && fam !== 'all' && String(m.family || '').toLowerCase() !== fam) continue if (q) { const blob = (String(m.id) + ' ' + String(m.label || '')).toLowerCase() if (blob.indexOf(q) === -1) continue } out.push(m) } return out } /** * @param {unknown} json * @returns {{ id: string, label: string, family: string, owned_by: string }[]} */ function bareAgentParseOpenAiModels(json) { const raw = json && typeof json === 'object' && Array.isArray(/** @type {{ data?: unknown }} */ (json).data) ? /** @type {{ data: unknown[] }} */ (json).data : Array.isArray(json) ? json : [] const skip = /embed|whisper|tts|dall-e|davinci|babbage|audio|moderation|realtime|image|sora/i /** @type {{ id: string, label: string, family: string, owned_by: string }[]} */ const out = [] const seen = Object.create(null) for (let i = 0; i < raw.length; i++) { const row = raw[i] && typeof raw[i] === 'object' ? /** @type {Record} */ (raw[i]) : null if (!row) continue const id = String(row.id || row.name || '').trim() if (!id || seen[id] || skip.test(id)) continue seen[id] = 1 const owned = String(row.owned_by || row.ownedBy || '') out.push({ id: id, label: id, family: owned || 'api', owned_by: owned }) } out.sort(function (a, b) { return a.id.localeCompare(b.id) }) return out } /** * @param {string} [provider] */ function bareAgentRestModelsFallback(provider) { const key = String(provider || 'groq').trim().toLowerCase() const ids = BARE_AGENT_REST_MODEL_FALLBACKS[key] || BARE_AGENT_REST_MODEL_FALLBACKS.groq return (ids || []).map(function (id) { return { id: id, label: id, family: key, owned_by: key } }) } /** * @param {(url: string, init?: object) => Promise<{ ok?: boolean, status?: number, json?: () => Promise, text?: () => Promise }>} fetchFn * @param {{ baseUrl?: string, apiKey?: string }} opts */ async function bareAgentFetchRestModels(fetchFn, opts) { const o = opts && typeof opts === 'object' ? opts : {} const base = String(o.baseUrl || '').trim().replace(/\/+$/, '') if (!base) throw new Error('rest_base_url required') if (typeof fetchFn !== 'function') throw new Error('fetch unavailable') const url = base + '/models' /** @type {Record} */ const headers = { Accept: 'application/json' } const key = String(o.apiKey || '').trim() if (key) headers.Authorization = 'Bearer ' + key const res = await fetchFn(url, { method: 'GET', headers: headers }) if (!res || res.ok === false) { const st = res && res.status != null ? String(res.status) : 'fetch_failed' throw new Error('models_http_' + st) } let json if (typeof res.json === 'function') json = await res.json() else if (typeof res.text === 'function') json = JSON.parse(await res.text()) else throw new Error('models_unreadable') const list = bareAgentParseOpenAiModels(json) if (!list.length) throw new Error('models_empty') return list } /** * Bare OS Discord slash-command catalog (guest-safe: no import/export). * Used by /bin/discord-bot (prepended) and the bare-os-discord initd unit. * Host/pack must static-import this .cjs (createRequire fails under app.bundle). */ var BARE_OS_DISCORD_REPLY_MAX = 1900 var BARE_OS_DISCORD_FS_MAX = 12 * 1024 /** Discord API hard caps (message content / embed parts / combined embed). */ var BARE_OS_DISCORD_LIMIT = { content: 2000, title: 256, desc: 4096, fieldName: 256, fieldValue: 1024, footer: 2048, author: 256, fields: 25, embedTotal: 6000, pageChars: 3000 } var BARE_OS_DISCORD_MORE_SESSIONS = Object.create(null) /** discord.js MessageFlags.Ephemeral (1 << 6). Avoid the deprecated `ephemeral` option. */ var BARE_OS_DISCORD_FLAG_EPHEMERAL = 64 var BARE_OS_DISCORD_WHITELIST_DENY = 'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.' var BARE_OS_DISCORD_OWNER_DENY = 'This control belongs to another operator.' /** ApplicationIntegrationType: GUILD_INSTALL / USER_INSTALL */ var BARE_OS_DISCORD_INTEGRATION_GUILD = 0 var BARE_OS_DISCORD_INTEGRATION_USER = 1 /** InteractionContextType: GUILD / BOT_DM / PRIVATE_CHANNEL */ var BARE_OS_DISCORD_CONTEXT_GUILD = 0 var BARE_OS_DISCORD_CONTEXT_BOT_DM = 1 var BARE_OS_DISCORD_CONTEXT_PRIVATE = 2 /** Discord ActivityType (Playing / Streaming / Listening / Watching / Custom / Competing). */ var BARE_OS_DISCORD_ACTIVITY_TYPE = { playing: 0, streaming: 1, listening: 2, watching: 3, custom: 4, competing: 5 } var BARE_OS_DISCORD_PRESENCE_STATUS = { online: 1, idle: 1, dnd: 1, invisible: 1 } /** View Channel + Send Messages + Embed Links + Attach Files + Read History + Use App Commands */ var BARE_OS_DISCORD_GUILD_INSTALL_PERMISSIONS = '2147597312' var BARE_OS_DISCORD_COLOR = 0x5865f2 var BARE_OS_DISCORD_COLOR_OK = 0x57f287 var BARE_OS_DISCORD_COLOR_WARN = 0xfee75c var BARE_OS_DISCORD_COLOR_ERR = 0xed4245 var BARE_OS_DISCORD_SH_TTL_MS = 30 * 60 * 1000 var BARE_OS_DISCORD_SH_SESSIONS = Object.create(null) 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', 'barerc', 'cd', 'export', 'unset', 'set', 'true', 'false', 'echo', 'pwd', 'type', 'command', 'umask', 'readonly', 'test', '[', 'jobs', 'wait', ':', 'help' ] var BARE_OS_DISCORD_SH_TTY = { edit: 1, nano: 1, vim: 1, vi: 1, btop: 1, baretop: 1, irc: 1, chat: 1, dhttop: 1, swarmmap: 1, summon: 1, 'holepunch-view': 1, routeview: 1 } var BARE_OS_DISCORD_SH_FLAGS = { ls: ['-a', '-l', '-la', '-lh', '-h', '-1', '-F'], grep: ['-i', '-v', '-n', '-r', '-E', '-F'], find: ['-name', '-type', '-size', '-path'], rm: ['-r', '-f', '-rf', '-v'], cp: ['-r', '-v', '-n'], mv: ['-v', '-n'], cat: ['-n', '-A'], head: ['-n'], tail: ['-n', '-f'], chmod: ['-R'], chown: ['-R'], df: ['-h', '-T'], du: ['-h', '-s', '-a'], ps: ['-e', '-f'], mkdir: ['-p'], tar: ['-xvf', '-cvf', '-tzf'] } var BARE_OS_DISCORD_RUN_ALLOW = { uname: 1, whoami: 1, hostname: 1, date: 1, uptime: 1, id: 1, pwd: 1, arch: 1, nproc: 1, help: 1, motd: 1, true: 1, false: 1, df: 1, ps: 1, procstat: 1, 'uname -a': 1 } var BARE_OS_DISCORD_KNOWN_UNITS = [ 'bare-os-discord', 'bare-os-www', 'bare-os-chat', 'bare-holesail', 'kernel-logger' ] var BARE_OS_DISCORD_EDIT_CHUNK = 4000 var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5 var BARE_OS_DISCORD_IDLE_MS = 2 * 60 * 1000 var BARE_OS_DISCORD_EDIT_TTL_MS = BARE_OS_DISCORD_IDLE_MS var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null) var BARE_OS_DISCORD_FM_PAGE = 20 var BARE_OS_DISCORD_FM_TTL_MS = BARE_OS_DISCORD_IDLE_MS var BARE_OS_DISCORD_FM_SESSIONS = Object.create(null) var BARE_OS_DISCORD_LIVE = Object.create(null) var BARE_OS_DISCORD_IDLE_SEQ = 0 var BARE_OS_DISCORD_EDIT_SUGGEST = [ '~/notes.txt', '~/.barerc', '~/TODO.md', '/tmp/scratch.txt' ] var BARE_OS_DISCORD_FS_SUGGEST = [ '~', '/etc/os-release', '/etc/motd', '/proc/uptime', '/proc/meminfo', '/proc/bare_os/features', '/proc/bare_os/security_posture.json', '/proc/bare_os/process_table.json', '/proc/bare_os/net_summary.json', '/var/log/bare-os/discord.log', '/var/log/bare-os/kernel-console.log', '/mnt', '/proc/bare_os/hdms_health.json', '/proc/bare_os/hdms_hints.json', '/share/man/man.json', '/tmp' ] /** * bare-process has no process.emitWarning. discord.js 14 calls it whenever * reply options include the deprecated `ephemeral` key (even when false). */ function discordInstallProcessEmitWarning(proc) { const p = proc || (typeof globalThis.process !== 'undefined' ? globalThis.process : null) if (!p || typeof p.emitWarning === 'function') return p p.emitWarning = function emitWarning(warning, type, code) { let name = 'Warning' let id = '' let msg = '' if (warning && typeof warning === 'object' && warning.type && !(warning instanceof Error)) { name = String(warning.type || 'Warning') id = String(warning.code || '') msg = String(warning.message || warning) } else if (type && typeof type === 'object') { name = String(type.type || 'Warning') id = String(type.code || '') msg = warning instanceof Error ? warning.message : String(warning) } else { name = typeof type === 'string' ? type : 'Warning' id = typeof code === 'string' ? code : '' msg = warning instanceof Error ? warning.message : String(warning) } const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg try { if (typeof p.emit === 'function') { const err = warning instanceof Error ? warning : new Error(msg) err.name = name if (id) err.code = id p.emit('warning', err) } } catch { /* ignore */ } try { if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(line) } } catch { /* ignore */ } } return p } discordInstallProcessEmitWarning() function discordStripAnsi(text) { let s = String(text == null ? '' : text) s = s.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '') s = s.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '') s = s.replace(/\u009b[0-?]*[ -/]*[@-~]/g, '') s = s.replace(/\u001b[@-Z\\-_]/g, '') s = s.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u009b]/g, '') return s } function discordCmdClip(text, max) { const s = discordStripAnsi(text) const n = max || BARE_OS_DISCORD_REPLY_MAX if (s.length <= n) return s return discordClipSmart(s, n, '\n…(truncated)').text } function discordClipSmart(text, max, suffix) { const s = String(text == null ? '' : text) const tail = suffix == null ? '\n…' : suffix if (s.length <= max) return { text: s, truncated: false, hidden: 0 } const room = Math.max(8, max - tail.length) let cut = s.lastIndexOf('\n', room) if (cut < room * 0.55) { const sp = s.lastIndexOf(' ', room) if (sp > room * 0.55) cut = sp } if (cut < Math.floor(room * 0.4)) cut = room return { text: s.slice(0, cut) + tail, truncated: true, hidden: s.length - cut } } function discordSafeDesc(desc, max) { const s = String(desc == null ? '' : desc) if (max <= 0) return '' if (s.length <= max) return s const open = s.indexOf('```') if (open >= 0) { const nl = s.indexOf('\n', open) const close = s.lastIndexOf('```') if (nl > open && close > nl) { const head = s.slice(0, nl + 1) const after = s.slice(close + 3) const hint = '\n…' const budget = max - head.length - 4 - after.length - hint.length if (budget >= 16) { const body = s.slice(nl + 1, close).replace(/\n+$/, '') const fit = discordClipSmart(body, budget, hint) return head + fit.text.replace(/```/g, '`ˋ`') + '\n```' + after } } } return discordClipSmart(s, max, '\n…').text } function discordCmdRedact(text) { return discordStripAnsi(text) .replace(/[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{20,}/g, '[token]') .replace(/(DISCORD_TOKEN|BOT_TOKEN|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*[=:]\s*\S+/gi, '$1=[redacted]') } function discordCmdFence(text, lang, max) { const tag = lang ? String(lang) : '' const wrap = 8 + tag.length const cap = Math.max(32, (max || BARE_OS_DISCORD_LIMIT.desc) - wrap) const fit = discordClipSmart(discordCmdRedact(text).replace(/```/g, '`ˋ`'), cap, '\n…') return '```' + tag + '\n' + fit.text + '\n```' } function discordTextPages(text, pageChars) { const s = String(text == null ? '' : text) const size = Math.max(200, pageChars || BARE_OS_DISCORD_LIMIT.pageChars) if (!s) return [''] if (s.length <= size) return [s] const pages = [] let i = 0 while (i < s.length) { if (s.length - i <= size) { pages.push(s.slice(i)) break } let cut = s.lastIndexOf('\n', i + size) if (cut <= i + Math.floor(size * 0.4)) cut = i + size pages.push(s.slice(i, cut)) i = cut while (s.charAt(i) === '\n') i++ } return pages } function discordMarkdownCloseFences(text) { const s = String(text == null ? '' : text) const n = (s.match(/```/g) || []).length if (n % 2 === 1) return s.replace(/\s*$/, '') + '\n```' return s } function discordMarkdownPages(text, pageChars) { const s = String(text == null ? '' : text) const size = Math.max(200, pageChars || BARE_OS_DISCORD_LIMIT.pageChars) if (!s) return [''] const raw = discordTextPages(s, size) const pages = [] let open = false for (let i = 0; i < raw.length; i++) { let chunk = raw[i] const marks = chunk.match(/```/g) const count = marks ? marks.length : 0 if (open) chunk = '```\n' + chunk const nowOpen = (open && count % 2 === 0) || (!open && count % 2 === 1) if (nowOpen) chunk = discordMarkdownCloseFences(chunk) open = nowOpen pages.push(chunk) } return pages.length ? pages : [''] } function discordEmbedSize(e) { if (!e) return 0 let n = 0 if (e.title) n += String(e.title).length if (e.description) n += String(e.description).length if (e.footer && e.footer.text) n += String(e.footer.text).length if (e.author && e.author.name) n += String(e.author.name).length const fs = e.fields || [] for (let i = 0; i < fs.length; i++) { n += String(fs[i].name || '').length + String(fs[i].value || '').length } return n } function discordPackEmbed(opts) { const o = opts || {} const L = BARE_OS_DISCORD_LIMIT const title = o.title ? discordStripAnsi(String(o.title)).slice(0, L.title) : '' const footer = discordStripAnsi(String(o.footer || 'Bare OS · expires after 2m idle')).slice(0, L.footer) const authorName = o.author && o.author.name ? discordStripAnsi(String(o.author.name)).slice(0, L.author) : '' const chrome = title.length + footer.length + authorName.length const preferDesc = o.prefer === 'desc' const rawFields = [] if (o.fields && o.fields.length) { for (let i = 0; i < o.fields.length && rawFields.length < L.fields; i++) { const f = o.fields[i] if (!f) continue let v = String(f.value == null ? '' : f.value).trim() if (!v || v === '—') continue v = discordCmdRedact(v) if (v.length > L.fieldValue) v = discordClipSmart(v, L.fieldValue, '\n…').text rawFields.push({ name: String(f.name || '·').slice(0, L.fieldName), value: v, inline: Boolean(f.inline) }) } } let fieldBytes = 0 for (let i = 0; i < rawFields.length; i++) { fieldBytes += rawFields[i].name.length + rawFields[i].value.length } let descRoom if (preferDesc) { descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - Math.min(fieldBytes, 900))) } else { descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - fieldBytes)) } let desc = o.desc ? discordCmdRedact(String(o.desc)) : '' if (desc) desc = discordSafeDesc(desc, descRoom) let fields = rawFields.slice() let used = chrome + desc.length + fieldBytes if (used > L.embedTotal && fields.length) { const keep = [] let acc = chrome + desc.length for (let i = 0; i < fields.length; i++) { const add = fields[i].name.length + fields[i].value.length if (acc + add > L.embedTotal - 48) break keep.push(fields[i]) acc += add } const omitted = fields.length - keep.length if (omitted > 0) { const note = '_' + omitted + ' more not shown_' if (keep.length && acc + note.length + 8 <= L.embedTotal) { keep.push({ name: '…', value: note, inline: false }) } else if (desc.length + 2 + note.length <= L.desc) { desc = (desc ? desc + '\n' : '') + note } } fields = keep used = chrome + desc.length for (let i = 0; i < fields.length; i++) used += fields[i].name.length + fields[i].value.length } if (used > L.embedTotal && desc) { desc = discordSafeDesc(desc, Math.max(0, desc.length - (used - L.embedTotal))) } const embed = { color: o.color == null ? BARE_OS_DISCORD_COLOR : o.color, timestamp: new Date().toISOString(), footer: { text: footer } } if (title) embed.title = title if (desc) embed.description = desc if (fields.length) embed.fields = fields if (o.author) embed.author = { name: authorName || 'Bare OS' } return { embed: embed, truncated: fields.length < rawFields.length || !!(o.desc && desc.length < String(o.desc).length), size: discordEmbedSize(embed) } } function discordTryJson(raw) { if (raw && typeof raw === 'object') return raw const s = String(raw || '').trim() if (!s || (s.charAt(0) !== '{' && s.charAt(0) !== '[')) return null try { return JSON.parse(s) } catch { return null } } function discordPrettyBytes(n) { const x = Number(n) if (!Number.isFinite(x) || x < 0) return String(n) if (x < 1024) return String(Math.round(x)) + ' B' const units = ['KiB', 'MiB', 'GiB', 'TiB'] let v = x / 1024 let i = 0 while (v >= 1024 && i < units.length - 1) { v /= 1024 i++ } return (v >= 10 ? v.toFixed(0) : v.toFixed(1)) + ' ' + units[i] } function discordIsByteishKey(k) { return /byte|bytes|mem|rss|size|freemem|totalmem|avail|queued|f_bsize|f_frsize/i.test( String(k || '') ) } function discordPrettyValue(key, val) { if (val == null || val === '') return '—' if (typeof val === 'boolean') return val ? 'yes' : 'no' if (typeof val === 'number') { if (discordIsByteishKey(key) && val >= 1024) return discordPrettyBytes(val) if (key === 'atMs' || /AtMs$/.test(key)) { if (val > 1e12) return new Date(val).toISOString() return String(val) } if (/Ms$/.test(key) && val >= 1000) { const s = Math.round(val / 1000) if (s < 60) return s + 's' return Math.floor(s / 60) + 'm ' + (s % 60) + 's' } return String(val) } if (typeof val === 'string') { const t = discordCmdRedact(val) if (/^[0-9a-f]{24,}$/i.test(t)) return '`' + t.slice(0, 16) + (t.length > 16 ? '…' : '') + '`' return t.length > 180 ? t.slice(0, 177) + '…' : t } if (Array.isArray(val)) { if (!val.length) return '(none)' if (val.every(function (x) { return x == null || typeof x !== 'object' })) { const shown = val.slice(0, 10).map(function (x) { return String(x) }) return shown.join(', ') + (val.length > 10 ? ' +' + (val.length - 10) : '') } return String(val.length) + ' items' } if (typeof val === 'object') return Object.keys(val).length + ' keys' return String(val) } function discordSkipPrettyKey(k) { return /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|API_KEY/i.test(k) || k === 'note' || k === 'schema' || k === 'schemaVersion' } function discordValueEmpty(val) { if (val == null) return true if (val === '') return true if (val === '—') return true if (Array.isArray(val) && !val.length) return true if (typeof val === 'object' && !Array.isArray(val) && !Object.keys(val).length) return true return false } function discordHumanLabel(key) { const known = { schemaVersion: 'Schema', topicHex: 'Topic', peerCount: 'Peers', seedHandshakeError: 'Handshake', seedRole: 'Role', replicationQueueDepth: 'Queue depth', replicationQueue: 'Replication', stagingSlot: 'Staging', snapshotHints: 'Snapshot', peerFirewallStats: 'Firewall', peerFirewallAcceptedTotal: 'Accepted', peerFirewallRejectedTotal: 'Rejected', peerFirewallInboundTotal: 'Inbound', peerFirewallOutboundTotal: 'Outbound', peerFirewallE2e: 'Firewall e2e', manifestPathCount: 'Manifests', localRamBlockCount: 'RAM blocks', hypercoreLengthHint: 'Core length', queueDepthEstimate: 'Queue estimate', snapshotWorkflowNote: 'Note', activeSlot: 'Active slot', pendingSlot: 'Pending', previousSlot: 'Previous', canarySlot: 'Canary', drainDeadlineMs: 'Drain', atMs: 'Updated' } if (known[key]) return known[key] const s = String(key || '') .replace(/[_-]+/g, ' ') .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/\s+/g, ' ') .trim() if (!s) return String(key || '') return s.charAt(0).toUpperCase() + s.slice(1) } function discordCollectFields(obj, prefix, out, depth) { if (!obj || typeof obj !== 'object' || Array.isArray(obj) || depth > 2) return const keys = Object.keys(obj) for (let i = 0; i < keys.length && out.length < 18; i++) { const k = keys[i] if (discordSkipPrettyKey(k)) continue const val = obj[k] if (discordValueEmpty(val)) continue const label = prefix ? prefix + ' · ' + discordHumanLabel(k) : discordHumanLabel(k) if (val && typeof val === 'object' && !Array.isArray(val) && depth < 2) { const lines = [] const sub = Object.keys(val) for (let j = 0; j < sub.length && lines.length < 8; j++) { if (discordSkipPrettyKey(sub[j])) continue const sv = val[sub[j]] if (discordValueEmpty(sv)) continue if (sv && typeof sv === 'object') continue const pretty = discordPrettyValue(sub[j], sv) if (pretty === '—' || pretty === '(none)') continue lines.push('**' + discordHumanLabel(sub[j]) + '** ' + pretty) } if (lines.length) out.push(discordField(discordHumanLabel(k), lines.join('\n'), false)) } else { const pretty = discordPrettyValue(k, val) if (pretty === '—' || pretty === '(none)') continue out.push(discordField(label, pretty, true)) } } } function discordPrettyUptime(raw) { const s = String(raw || '').trim() const sec = Number(s.split(/\s+/)[0]) if (!Number.isFinite(sec) || sec < 0) return s || '—' const d = Math.floor(sec / 86400) const h = Math.floor((sec % 86400) / 3600) const m = Math.floor((sec % 3600) / 60) const parts = [] if (d) parts.push(d + 'd') if (h || d) parts.push(h + 'h') parts.push(m + 'm') return parts.join(' ') + ' (' + Math.round(sec) + 's)' } function discordParseMeminfo(text) { const out = {} const lines = String(text || '').split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const m = /^([A-Za-z0-9_()]+):\s+(\d+)/.exec(lines[i]) if (!m) continue out[m[1]] = Number(m[2]) * 1024 } return out } function discordParseSystemctlList(text) { const rows = [] const lines = String(text || '').split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const line = lines[i].trim() if (!line || /^UNIT\b/.test(line) || line.indexOf('LOAD') === 0) continue const parts = line.split(/\s+/) if (parts.length < 4) continue rows.push({ unit: parts[0].replace(/\.service$/, ''), load: parts[1] || '', preset: parts[2] || '', active: parts[3] || '', sub: parts[4] || '', desc: parts.slice(5).join(' ') }) } return rows } function discordPrettySnapshot(title, raw, extras) { const parsed = discordTryJson(raw) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { const text = raw == null ? '' : String(raw) if (!text) { return discordResult(discordEmbed({ title: title, desc: 'unavailable' }), extras) } return { more: { title: title, body: text, fence: true, components: extras && extras.components, editPath: extras && extras.editPath, created: extras && extras.created, color: extras && extras.color } } } const fields = [] discordCollectFields(parsed, '', fields, 0) const note = typeof parsed.note === 'string' ? parsed.note.slice(0, 280) : '' return discordResult( discordEmbed({ title: title, desc: note, fields: fields.slice(0, 18), color: extras && extras.color }), extras ) } function discordHex12(ctx, u8) { if (!u8 || typeof u8.length !== 'number') return '' if (ctx && ctx.b4a && typeof ctx.b4a.toString === 'function') { try { return String(ctx.b4a.toString(u8, 'hex') || '').slice(0, 12) } catch { /* fall through */ } } let hex = '' const n = Math.min(u8.length, 6) for (let i = 0; i < n; i++) { const h = (u8[i] & 0xff).toString(16) hex += h.length < 2 ? '0' + h : h } return hex.slice(0, 12) } /** Prefer live VFS env (identity mutates this). Keep ctx.env in sync. */ function discordSyncSessionIdentity(ctx) { if (!ctx) return const vfsEnv = ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null const env = vfsEnv || (ctx.env && typeof ctx.env === 'object' ? ctx.env : null) if (!env) return if (ctx.env && vfsEnv && ctx.env !== vfsEnv) { const keys = [ 'USER', 'LOGNAME', 'HOME', 'PWD', 'UID', 'GID', 'GROUP', 'SHELL', 'BARE_OS_IDENTITY', 'BARE_OS_PUBLIC_KEY' ] for (let i = 0; i < keys.length; i++) { const k = keys[i] if (vfsEnv[k] != null && String(vfsEnv[k]) !== '') ctx.env[k] = vfsEnv[k] } } const ident = ctx.identity if (!ident || ident.state !== 'unlocked') return let name = String(env.USER || env.LOGNAME || '').trim() if (!name || name === 'guest') { name = discordHex12(ctx, ident.publicKey) if (!name) return env.USER = name env.LOGNAME = name env.GROUP = name if (ctx.env && ctx.env !== env) { ctx.env.USER = name ctx.env.LOGNAME = name } } if (!env.HOME || env.HOME === '/home/guest') env.HOME = '/home/' + name if (!env.PWD || env.PWD === '/home/guest') env.PWD = env.HOME env.BARE_OS_IDENTITY = 'unlocked' } function discordCmdEnv(ctx) { discordSyncSessionIdentity(ctx) if (ctx && ctx.vfs && ctx.vfs.env) return ctx.vfs.env if (ctx && ctx.env) return ctx.env return {} } function discordSessionUser(ctx) { const e = discordCmdEnv(ctx) const u = String(e.USER || e.LOGNAME || e.USERNAME || '').trim() return u || 'guest' } function discordUniqueComponents(rows) { const seen = Object.create(null) const out = [] if (!Array.isArray(rows)) return out for (let i = 0; i < rows.length && out.length < 5; i++) { const row = rows[i] if (!row || !Array.isArray(row.components)) continue const comps = [] for (let j = 0; j < row.components.length; j++) { const c = row.components[j] if (!c) continue const id = c.custom_id != null ? String(c.custom_id) : '' if (id) { if (seen[id]) continue seen[id] = 1 } comps.push(c) } if (!comps.length) continue out.push({ type: row.type || 1, components: comps }) } return out } function discordCmdReplyPayload(result) { const payload = {} if (result && result.embeds && result.embeds.length) payload.embeds = result.embeds if (result && result.components && result.components.length) { 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.files) payload.content = '(no output)' if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL delete payload.fetchReply delete payload.ephemeral return payload } function discordEmbed(opts) { return discordPackEmbed(opts).embed } function discordField(name, value, inline) { let v = value == null || value === '' ? '—' : String(value) v = discordCmdRedact(v) if (v.length > BARE_OS_DISCORD_LIMIT.fieldValue) { v = discordClipSmart(v, BARE_OS_DISCORD_LIMIT.fieldValue, '\n…').text } if (!v) v = '—' return { name: String(name).slice(0, BARE_OS_DISCORD_LIMIT.fieldName), value: v, inline: Boolean(inline) } } function discordMoreKey(interaction) { return discordInteractionUserId(interaction) || 'anon' } function discordMoreGc() { const now = Date.now() for (const k in BARE_OS_DISCORD_MORE_SESSIONS) { const r = BARE_OS_DISCORD_MORE_SESSIONS[k] if (!r || now - r.atMs > BARE_OS_DISCORD_IDLE_MS) delete BARE_OS_DISCORD_MORE_SESSIONS[k] } } function discordMorePut(interaction, rec) { discordMoreGc() rec.atMs = Date.now() BARE_OS_DISCORD_MORE_SESSIONS[discordMoreKey(interaction)] = rec } function discordMoreGet(interaction) { discordMoreGc() return BARE_OS_DISCORD_MORE_SESSIONS[discordMoreKey(interaction)] || null } function discordMoreResult(interaction) { const rec = discordMoreGet(interaction) if (!rec) return { text: 'This view expired after 2 minutes idle.', ephemeral: true } const n = rec.pages.length if (rec.page < 0) rec.page = n - 1 if (rec.page >= n) rec.page = 0 rec.atMs = Date.now() const chunk = rec.pages[rec.page] || '' const inner = rec.fence === false ? chunk : discordCmdFence(chunk, rec.lang, 3600) const navline = n > 1 ? '\nPage **' + (rec.page + 1) + '/' + n + '** · ' + rec.total + ' characters' : '' const btns = n > 1 ? discordButtons([ { id: 'more:prev', label: '← Prev' }, { id: 'more:next', label: 'Next →', style: 1 } ]) : null const extra = (rec.extra || []).concat(btns ? [btns] : []) const out = discordResult( discordEmbed({ title: rec.title, desc: (rec.lead || '') + inner + navline, color: rec.color, footer: rec.footer, prefer: 'desc' }), { components: extra } ) return out } function discordLongTextResult(interaction, opts) { const o = opts || {} const body = String(o.body == null ? '' : o.body) const pages = o.fence === false ? discordMarkdownPages(body, o.pageChars || BARE_OS_DISCORD_LIMIT.pageChars) : discordTextPages(body, o.pageChars || BARE_OS_DISCORD_LIMIT.pageChars) const extraComps = o.components || [] if (pages.length > 1) { let lead = o.lead ? String(o.lead) : '' if (lead && !/\n$/.test(lead)) lead += '\n' discordMorePut(interaction, { title: o.title, pages: pages, page: 0, fence: o.fence !== false, lang: o.lang || '', lead: lead, color: o.color, footer: o.footer, extra: extraComps, total: body.length }) const view = discordMoreResult(interaction) if (o.editPath) view.editPath = o.editPath if (o.created) view.created = true return view } const inner = o.fence === false ? body : discordCmdFence(body, o.lang, 3800) let lead = o.lead ? String(o.lead) : '' if (lead && inner && !/\n$/.test(lead)) lead += '\n' const out = discordResult( discordEmbed({ title: o.title, desc: lead + inner, color: o.color, footer: o.footer, prefer: 'desc' }), { components: extraComps, editPath: o.editPath, created: o.created } ) return out } function discordButtons(items) { const row = { type: 1, components: [] } for (let i = 0; i < items.length && row.components.length < 5; i++) { const it = items[i] if (!it || !it.id) continue row.components.push({ type: 2, style: it.style || 2, label: String(it.label || it.id).slice(0, 80), custom_id: String(it.id).slice(0, 100) }) } return row.components.length ? row : null } function discordSelect(id, placeholder, options) { const opts = [] for (let i = 0; i < options.length && opts.length < 25; i++) { const o = options[i] if (!o) continue const value = String(o.value || o.label || '').slice(0, 100) if (!value) continue const item = { label: String(o.label || value).slice(0, 100), value: value } if (o.description) item.description = String(o.description).slice(0, 100) opts.push(item) } if (!opts.length) return null return { type: 1, components: [ { type: 3, custom_id: String(id).slice(0, 100), placeholder: String(placeholder || 'Choose…').slice(0, 150), min_values: 1, max_values: 1, options: opts } ] } } var BARE_OS_DISCORD_NAV_IDS = { 'nav:menu': 1, 'nav:panel': 1, 'bare:status': 1, 'bare:whoami': 1, 'bare:help': 1, 'sys:doctor': 1, 'svc:list': 1, 'sys:ps': 1, 'net:summary': 1, 'say:compose': 1, 'hdms:home': 1, 'holesail:home': 1, 'agent:home': 1, 'run:compose': 1, 'edit:new': 1, 'create:new': 1, 'fm:home': 1, 'set:home': 1 } function discordCompCustomId(c) { if (!c) return '' if (c.custom_id != null) return String(c.custom_id) if (c.customId != null) return String(c.customId) if (c.data && c.data.custom_id != null) return String(c.data.custom_id) return '' } function discordNormalizeRows(rows) { const out = [] if (!Array.isArray(rows)) return out for (let i = 0; i < rows.length; i++) { const row = rows[i] if (!row) continue const raw = typeof row.toJSON === 'function' ? row.toJSON() : { type: row.type || 1, components: row.components || (row.data && row.data.components) || [] } const comps = [] const list = raw.components || [] for (let j = 0; j < list.length; j++) { const c = list[j] if (!c) continue const json = typeof c.toJSON === 'function' ? c.toJSON() : c if (json) comps.push(json) } if (comps.length) out.push({ type: raw.type || 1, components: comps }) } return out } function discordNormalizeEmbeds(embeds) { if (!Array.isArray(embeds) || !embeds.length) return null const out = [] for (let i = 0; i < embeds.length; i++) { const e = embeds[i] if (!e) continue out.push(typeof e.toJSON === 'function' ? e.toJSON() : e) } return out.length ? out : null } function discordNavIsExpanded(rows) { const list = discordNormalizeRows(rows) for (let i = 0; i < list.length; i++) { const comps = list[i].components || [] for (let j = 0; j < comps.length; j++) { if (discordCompCustomId(comps[j]) === 'nav:panel') return true } } return false } function discordRowIsNavChrome(comps) { if (!comps || !comps.length) return false for (let i = 0; i < comps.length; i++) { const id = discordCompCustomId(comps[i]) if (!id || !BARE_OS_DISCORD_NAV_IDS[id]) return false } return true } function discordStripNavIds(rows) { const out = [] const list = discordNormalizeRows(rows) for (let i = 0; i < list.length; i++) { const src = list[i].components || [] if (discordRowIsNavChrome(src)) continue const comps = [] for (let j = 0; j < src.length; j++) { if (discordCompCustomId(src[j]) === 'nav:menu') continue comps.push(src[j]) } if (comps.length) out.push({ type: list[i].type || 1, components: comps }) } return out } function discordMergeMenu(extraRows, menuRows) { const extra = (extraRows || []).filter(Boolean) const menu = menuRows && menuRows[0] const btn = menu && menu.components && menu.components[0] if (!btn) return extra if (!extra.length) return menuRows const last = extra[extra.length - 1] const comps = last.components || [] if (last.type === 1 && comps.length && comps.length < 5 && comps[0].type === 2) { const copy = extra.slice() copy[copy.length - 1] = { type: 1, components: comps.concat([btn]) } return copy } return extra.concat(menuRows) } function discordNavRows(open) { if (!open) { const row = discordButtons([{ id: 'nav:menu', label: 'Menu', style: 2 }]) return row ? [row] : [] } const rows = [] const a = discordButtons([ { id: 'nav:menu', label: 'Hide menu', style: 2 }, { id: 'nav:panel', label: 'Panel', style: 1 }, { id: 'bare:status', label: 'Status', style: 1 }, { id: 'bare:whoami', label: 'Whoami' }, { id: 'bare:help', label: 'Help' } ]) const b = discordButtons([ { id: 'sys:doctor', label: 'Doctor' }, { id: 'svc:list', label: 'Services' }, { id: 'holesail:home', label: 'Holesail', style: 1 }, { id: 'net:summary', label: 'Network' }, { id: 'hdms:home', label: 'HDMS', style: 1 } ]) const c = discordButtons([ { id: 'run:compose', label: 'Shell' }, { id: 'edit:new', label: 'Edit file…', style: 1 }, { id: 'create:new', label: 'Create file…', style: 3 }, { id: 'fm:home', label: 'Files', style: 1 }, { id: 'set:home', label: 'Settings' } ]) const d = discordButtons([{ id: 'agent:home', label: 'Agent', style: 1 }]) if (a) rows.push(a) if (b) rows.push(b) if (c) rows.push(c) if (d) rows.push(d) return rows } function discordApplyNav(rows, open) { const base = discordStripNavIds(rows) const nav = discordNavRows(open) if (!open) return discordMergeMenu(base, nav) const room = 5 - nav.length const keep = room > 0 ? base.slice(0, room) : [] return keep.concat(nav) } function discordResult(embed, extra) { const out = { embeds: [embed] } const extraRows = extra && extra.components ? extra.components : [] const rows = extra && extra.nav === false ? discordUniqueComponents(extraRows) : discordUniqueComponents(discordApplyNav(extraRows, false)) if (rows.length) out.components = rows if (extra && extra.ephemeral) out.ephemeral = true if (extra && extra.text) out.text = extra.text if (extra && extra.editPath) out.editPath = extra.editPath if (extra && extra.created) out.created = true return out } async function discordCmdReadText(ctx, logicalPath) { if (!logicalPath || typeof ctx.vfs?.readFile !== 'function') return '' try { const buf = await ctx.vfs.readFile(logicalPath) if (!buf) return '' if (typeof ctx.b4a?.toString === 'function') return ctx.b4a.toString(buf) return String(buf) } catch { return '' } } async function discordCmdReadJson(ctx, logicalPath) { const t = await discordCmdReadText(ctx, logicalPath) if (!t) return null try { return JSON.parse(t) } catch { return null } } function discordCmdPathOk(raw) { const p = String(raw || '').trim() || '.' if (!p || p.indexOf('\0') >= 0) return null if (p.indexOf('..') >= 0) return null if (p === '~/.discord/.env' || p === '~/.discord.env') return null const allow = p === '.' || p === '~' || p.charAt(0) === '~' || p.indexOf('/proc') === 0 || p.indexOf('/etc') === 0 || p.indexOf('/var/log') === 0 || p.indexOf('/run') === 0 || p.indexOf('/home') === 0 || p.indexOf('/usr/share') === 0 || p.indexOf('/share') === 0 || p.indexOf('/tmp') === 0 || p === '/mnt' || p.indexOf('/mnt/') === 0 return allow ? p : null } function discordCmdPathWriteOk(ctx, raw) { const p = discordCmdPathOk(raw) if (!p) return null if (p === '~/.discord/.env' || p === '~/.discord.env') return null if (p.indexOf('~/.discord') === 0) return null const user = discordSessionUser(ctx) const home = '/home/' + user const allow = p === '~' || p.indexOf('~/') === 0 || p.indexOf('/tmp/') === 0 || p === '/tmp' || p === home || p.indexOf(home + '/') === 0 || (p.indexOf('/mnt/') === 0 && p.length > 5) return allow ? p : null } async function discordFileExists(ctx, p) { if (!ctx || !ctx.vfs) return false const stfn = ctx.vfs.lstat || ctx.vfs.stat if (typeof stfn === 'function') { try { const st = await stfn.call(ctx.vfs, p) return Boolean(st) } catch { return false } } if (typeof ctx.vfs.readFile !== 'function') return false try { await ctx.vfs.readFile(p) return true } catch { return false } } function discordLooksBinary(text) { const s = String(text || '') if (s.indexOf('\0') >= 0) return true let bad = 0 const n = Math.min(s.length, 800) for (let i = 0; i < n; i++) { const c = s.charCodeAt(i) if (c < 9 || (c > 13 && c < 32)) bad++ } return bad > 8 } function discordTextToBuf(ctx, text) { const s = String(text == null ? '' : text) if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(s) if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf8') const out = new Uint8Array(s.length) for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff return out } function discordEditGc() { const now = Date.now() for (const k in BARE_OS_DISCORD_EDIT_SESSIONS) { const rec = BARE_OS_DISCORD_EDIT_SESSIONS[k] if (!rec || now - rec.atMs > BARE_OS_DISCORD_EDIT_TTL_MS) { delete BARE_OS_DISCORD_EDIT_SESSIONS[k] } } } function discordEditKey(interaction) { return discordInteractionUserId(interaction) || 'anon' } function discordEditPut(interaction, rec) { discordEditGc() BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] = { path: rec.path, origLen: rec.origLen || 0, truncated: !!rec.truncated, created: !!rec.created, atMs: Date.now() } } function discordEditGet(interaction) { discordEditGc() const rec = BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] return rec || null } function discordEditClear(interaction) { delete BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] } function discordEditChunks(text) { const s = String(text == null ? '' : text) const cap = BARE_OS_DISCORD_EDIT_CHUNK * BARE_OS_DISCORD_EDIT_MAX_CHUNKS const truncated = s.length > cap const body = truncated ? s.slice(0, cap) : s const chunks = [] if (!body.length) chunks.push('') else { for (let i = 0; i < body.length; i += BARE_OS_DISCORD_EDIT_CHUNK) { chunks.push(body.slice(i, i + BARE_OS_DISCORD_EDIT_CHUNK)) } } return { chunks: chunks.slice(0, BARE_OS_DISCORD_EDIT_MAX_CHUNKS), truncated: truncated, origLen: s.length } } function discordEditModalPayload(path, chunks) { const base = String(path || 'file').split('/').pop() || String(path) const n = Math.max(1, chunks.length) const components = [] for (let i = 0; i < n; i++) { const label = n === 1 ? 'Contents (save closes the form)' : 'Part ' + (i + 1) + ' / ' + n const field = { type: 4, custom_id: 'c' + i, label: label.slice(0, 45), style: 2, required: false, max_length: BARE_OS_DISCORD_EDIT_CHUNK } const v = String(chunks[i] || '').slice(0, BARE_OS_DISCORD_EDIT_CHUNK) if (v) field.value = v components.push({ type: 1, components: [field] }) } return { custom_id: 'edit:save', title: ('Edit ' + base).slice(0, 45), components: components } } var BARE_OS_DISCORD_CAPTURE_STACK = [] var BARE_OS_DISCORD_STDIO_HOOK = null function discordCaptureChunkToText(chunk, enc) { if (chunk == null) return '' if (typeof chunk === 'string') return chunk const encoding = typeof enc === 'string' && enc ? enc : 'utf8' if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(chunk)) { try { return chunk.toString(encoding) } catch { return chunk.toString('utf8') } } if (chunk instanceof Uint8Array || (chunk && typeof chunk.length === 'number' && chunk.buffer)) { try { if (typeof TextDecoder === 'function') { return new TextDecoder(encoding === 'utf8' ? 'utf-8' : encoding).decode(chunk) } } catch { /* fall through */ } let s = '' for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i] & 255) return s } return String(chunk) } function discordCaptureAppend(text) { const n = BARE_OS_DISCORD_CAPTURE_STACK.length if (!n) return false const top = BARE_OS_DISCORD_CAPTURE_STACK[n - 1] top.buf += text return true } function discordCaptureConsoleLine() { const parts = [] for (let i = 0; i < arguments.length; i++) { const a = arguments[i] parts.push(typeof a === 'string' ? a : String(a)) } discordCaptureAppend(parts.join(' ') + '\n') } function discordCaptureRaw(chunk, enc) { discordCaptureAppend(discordCaptureChunkToText(chunk, enc)) } function discordCaptureHookStdio() { if (BARE_OS_DISCORD_STDIO_HOOK) return const proc = typeof globalThis.process !== 'undefined' ? globalThis.process : null if (!proc) return const so = proc.stdout const se = proc.stderr function wrapWrite() { return function (chunk, enc, cb) { discordCaptureRaw(chunk, typeof enc === 'string' ? enc : undefined) const done = typeof enc === 'function' ? enc : typeof cb === 'function' ? cb : null if (done) { try { done() } catch { /* ignore */ } } return true } } BARE_OS_DISCORD_STDIO_HOOK = { stdout: so, stderr: se, outWrite: so && so.write, errWrite: se && se.write, outIsTTY: so ? so.isTTY : undefined, errIsTTY: se ? se.isTTY : undefined } if (so && typeof so.write === 'function') so.write = wrapWrite() if (se && typeof se.write === 'function') se.write = wrapWrite() try { if (so) so.isTTY = false } catch { /* ignore */ } try { if (se) se.isTTY = false } catch { /* ignore */ } } function discordCaptureUnhookStdio() { if (BARE_OS_DISCORD_CAPTURE_STACK.length) return const h = BARE_OS_DISCORD_STDIO_HOOK if (!h) return if (h.stdout && h.outWrite) h.stdout.write = h.outWrite if (h.stderr && h.errWrite) h.stderr.write = h.errWrite try { if (h.stdout && h.outIsTTY !== undefined) h.stdout.isTTY = h.outIsTTY } catch { /* ignore */ } try { if (h.stderr && h.errIsTTY !== undefined) h.stderr.isTTY = h.errIsTTY } catch { /* ignore */ } BARE_OS_DISCORD_STDIO_HOOK = null } var BARE_OS_DISCORD_COLOR_ENV = ['NO_COLOR', 'CLICOLOR', 'CLICOLOR_FORCE', 'FORCE_COLOR', 'TERM'] function discordCaptureColorOff(env, saved) { if (!env || typeof env !== 'object') return for (let i = 0; i < BARE_OS_DISCORD_COLOR_ENV.length; i++) { const k = BARE_OS_DISCORD_COLOR_ENV[i] saved[k] = Object.prototype.hasOwnProperty.call(env, k) ? env[k] : undefined } env.NO_COLOR = '1' env.CLICOLOR = '0' env.CLICOLOR_FORCE = '0' env.FORCE_COLOR = '0' env.TERM = 'dumb' } function discordCaptureColorRestore(env, saved) { if (!env || typeof env !== 'object' || !saved) return for (let i = 0; i < BARE_OS_DISCORD_COLOR_ENV.length; i++) { const k = BARE_OS_DISCORD_COLOR_ENV[i] if (saved[k] === undefined) delete env[k] else env[k] = saved[k] } } function discordCaptureFinish(buf) { return discordStripAnsi(String(buf || '')) .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\n$/, '') } async function discordCmdCapture(ctx, fn) { const rec = { buf: '' } BARE_OS_DISCORD_CAPTURE_STACK.push(rec) if (!ctx.console || typeof ctx.console !== 'object') ctx.console = {} const cons = ctx.console const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : null const vfsEnv = ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null const saved = { log: cons.log, error: cons.error, info: cons.info, warn: cons.warn, debug: cons.debug, binWrite: ctx.bareOsBinWrite, writeScreen: ctx.writeScreen, captured: ctx.bareOsStdoutCaptured, env: {}, vfsEnv: {} } discordCaptureColorOff(env, saved.env) discordCaptureColorOff(vfsEnv, saved.vfsEnv) ctx.bareOsStdoutCaptured = true cons.log = discordCaptureConsoleLine cons.error = discordCaptureConsoleLine cons.info = discordCaptureConsoleLine cons.warn = discordCaptureConsoleLine cons.debug = discordCaptureConsoleLine ctx.bareOsBinWrite = function (chunk) { discordCaptureRaw(chunk) } ctx.writeScreen = function (chunk) { discordCaptureRaw(chunk) } discordCaptureHookStdio() try { await fn() } finally { cons.log = saved.log cons.error = saved.error cons.info = saved.info cons.warn = saved.warn cons.debug = saved.debug if (saved.binWrite === undefined) delete ctx.bareOsBinWrite else ctx.bareOsBinWrite = saved.binWrite if (saved.writeScreen === undefined) delete ctx.writeScreen else ctx.writeScreen = saved.writeScreen if (saved.captured === undefined) delete ctx.bareOsStdoutCaptured else ctx.bareOsStdoutCaptured = saved.captured discordCaptureColorRestore(env, saved.env) discordCaptureColorRestore(vfsEnv, saved.vfsEnv) BARE_OS_DISCORD_CAPTURE_STACK.pop() discordCaptureUnhookStdio() } return discordCaptureFinish(rec.buf) } async function discordCmdOsRelease(ctx) { const t = await discordCmdReadText(ctx, '/etc/os-release') const out = {} const lines = String(t).split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const m = /^([A-Z0-9_]+)=(.*)$/.exec(lines[i].trim()) if (!m) continue out[m[1]] = m[2].replace(/^"|"$/g, '') } return out } function discordParseIdWhitelist(raw) { const ids = Object.create(null) const s = String(raw == null ? '' : raw).trim() if (!s) return ids const parts = s.split(',') for (let i = 0; i < parts.length; i++) { let id = String(parts[i] || '').trim() if (!id) continue if (id.charAt(0) === '<' && id.charAt(id.length - 1) === '>') { id = id.slice(1, -1) if (id.charAt(0) === '@') id = id.slice(1) if (id.charAt(0) === '!') id = id.slice(1) id = id.trim() } if (id) ids[id] = 1 } return ids } function discordEnvLookup(ctx, keys) { const maps = [] if (ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object') { maps.push(ctx.vfs.env) } if (ctx && ctx.env && typeof ctx.env === 'object' && ctx.env !== maps[0]) { maps.push(ctx.env) } for (let i = 0; i < maps.length; i++) { const e = maps[i] for (let j = 0; j < keys.length; j++) { const v = e[keys[j]] if (v != null && String(v).trim()) return String(v) } } return '' } function discordWhitelistRaw(ctx) { return discordEnvLookup(ctx, [ 'DISCORD_ID_WHITELIST', 'BARE_OS_DISCORD_ID_WHITELIST' ]) } function discordWhitelistCount(ctx) { const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx)) let n = 0 for (const k in ids) { if (Object.prototype.hasOwnProperty.call(ids, k)) n++ } return n } /** * User-installable profile app (Discord "Add App" → User Install). * Default on. Set DISCORD_USER_INSTALL=0 to keep a guild-only bot. */ function discordUserInstallEnabled(ctx) { const raw = String( discordEnvLookup(ctx, ['DISCORD_USER_INSTALL', 'BARE_OS_DISCORD_USER_INSTALL']) || '1' ) .trim() .toLowerCase() return raw !== '0' && raw !== 'false' && raw !== 'no' && raw !== 'off' } /** * True when this interaction was authorized by a user-install (profile app) * or used in Bot DM / private channel. Those surfaces are world-reachable * once the app is user-installable — whitelist is mandatory. */ function discordIsUserInstallInteraction(interaction) { if (!interaction) return false const owners = interaction.authorizingIntegrationOwners || interaction.authorizing_integration_owners if (owners && typeof owners === 'object') { if ( owners[BARE_OS_DISCORD_INTEGRATION_USER] != null || owners[String(BARE_OS_DISCORD_INTEGRATION_USER)] != null ) { return true } } const c = interaction.context if ( c === BARE_OS_DISCORD_CONTEXT_BOT_DM || c === BARE_OS_DISCORD_CONTEXT_PRIVATE || c === String(BARE_OS_DISCORD_CONTEXT_BOT_DM) || c === String(BARE_OS_DISCORD_CONTEXT_PRIVATE) ) { return true } return false } function discordStampUserInstallCommand(json, ctx) { if (!json || typeof json !== 'object') return json if (!discordUserInstallEnabled(ctx)) return json json.integration_types = [ BARE_OS_DISCORD_INTEGRATION_GUILD, BARE_OS_DISCORD_INTEGRATION_USER ] json.contexts = [ BARE_OS_DISCORD_CONTEXT_GUILD, BARE_OS_DISCORD_CONTEXT_BOT_DM, BARE_OS_DISCORD_CONTEXT_PRIVATE ] return json } function discordStampUserInstallCommands(body, ctx) { const list = Array.isArray(body) ? body : [] for (let i = 0; i < list.length; i++) { discordStampUserInstallCommand(list[i], ctx) } return list } function discordUserInstallAuthorizeUrl(appId) { const id = String(appId || '').trim() if (!id) return '' return 'https://discord.com/oauth2/authorize?client_id=' + encodeURIComponent(id) } function discordUserInstallAppConfig() { const cfg = {} cfg[String(BARE_OS_DISCORD_INTEGRATION_GUILD)] = { oauth2_install_params: { scopes: ['applications.commands', 'bot'], permissions: BARE_OS_DISCORD_GUILD_INSTALL_PERMISSIONS } } cfg[String(BARE_OS_DISCORD_INTEGRATION_USER)] = { oauth2_install_params: { scopes: ['applications.commands'] } } return { integration_types_config: cfg } } async function discordEnableUserInstallApp(rest, Routes) { if (!rest || typeof rest.patch !== 'function') return false if (!Routes || typeof Routes.currentApplication !== 'function') return false await rest.patch(Routes.currentApplication(), { body: discordUserInstallAppConfig() }) return true } /** * User-install commands must be global. Also PUT guild commands when * DISCORD_GUILD_ID is set so the home server updates immediately. */ async function discordPutSlashCommands(rest, Routes, appId, body, opts) { opts = opts || {} const guildId = String(opts.guildId || '').trim() const userInstall = opts.userInstall !== false const out = { global: false, guild: false } if (!rest || typeof rest.put !== 'function' || !Routes || !appId) return out if (userInstall || !guildId) { if (typeof Routes.applicationCommands === 'function') { await rest.put(Routes.applicationCommands(appId), { body: body }) out.global = true } } if (guildId && typeof Routes.applicationGuildCommands === 'function') { await rest.put(Routes.applicationGuildCommands(appId, guildId), { body: body }) out.guild = true } return out } function discordIsDirectMessage(message) { if (!message) return false if (message.guildId || message.guild) return false const ch = message.channel if (ch && typeof ch.isDMBased === 'function') { try { if (ch.isDMBased()) return true } catch { /* ignore */ } } const t = ch && ch.type if (t === 1 || t === 3 || t === 'DM' || t === 'GROUP_DM') return true if (message.channelId && !message.guildId && !message.guild) return true return false } function discordDmGateInteraction(message) { const uid = message && message.author && message.author.id ? String(message.author.id) : '' const owners = {} owners[String(BARE_OS_DISCORD_INTEGRATION_USER)] = uid return { context: BARE_OS_DISCORD_CONTEXT_BOT_DM, authorizingIntegrationOwners: owners, user: message && message.author } } function discordPresenceFromEnv(ctx) { const statusRaw = String( discordEnvLookup(ctx, ['DISCORD_STATUS', 'BARE_OS_DISCORD_STATUS']) || 'online' ) .trim() .toLowerCase() const typeRaw = String( discordEnvLookup(ctx, [ 'DISCORD_ACTIVITY_TYPE', 'BARE_OS_DISCORD_ACTIVITY_TYPE' ]) || 'playing' ) .trim() .toLowerCase() let name = discordEnvLookup(ctx, [ 'DISCORD_ACTIVITY_NAME', 'BARE_OS_DISCORD_ACTIVITY_NAME' ]) const url = discordEnvLookup(ctx, [ 'DISCORD_ACTIVITY_URL', 'BARE_OS_DISCORD_ACTIVITY_URL' ]) const status = BARE_OS_DISCORD_PRESENCE_STATUS[statusRaw] ? statusRaw : 'online' const type = BARE_OS_DISCORD_ACTIVITY_TYPE[typeRaw] == null ? 0 : BARE_OS_DISCORD_ACTIVITY_TYPE[typeRaw] const typeName = BARE_OS_DISCORD_ACTIVITY_TYPE[typeRaw] == null ? 'playing' : typeRaw const off = /^(off|none|-)$/i.test(name) if (!name) name = 'Bare OS' return { status: status, type: type, typeName: typeName, name: off ? '' : name, url: url } } function discordApplyPresence(ctx, client) { const c = client || (ctx && ctx.bareOsDiscordClient) || (ctx && ctx.discordClient) || null if (!c || !c.user) return false const p = discordPresenceFromEnv(ctx) const activities = [] if (p.name) { const act = { name: p.name, type: p.type } if (p.type === 1 && p.url) act.url = p.url activities.push(act) } const body = { status: p.status, activities: activities } try { if (typeof c.user.setPresence === 'function') c.user.setPresence(body) else if (typeof c.user.setActivity === 'function') { if (activities[0]) c.user.setActivity(activities[0].name, { type: activities[0].type }) else c.user.setActivity(null) } else return false return true } catch { return false } } async function discordCmdHandlePresence(ctx, opt) { const typeIn = String((opt && opt('activity')) || (opt && opt('type')) || '') .trim() .toLowerCase() const nameIn = String((opt && opt('name')) || '').trim() const stateIn = String((opt && opt('state')) || (opt && opt('status')) || '') .trim() .toLowerCase() const env = discordCmdEnv(ctx) let changed = false if (typeIn) { if (BARE_OS_DISCORD_ACTIVITY_TYPE[typeIn] == null) { return { text: 'Unknown activity. Use playing, watching, listening, competing, custom, or streaming.', ephemeral: true } } env.DISCORD_ACTIVITY_TYPE = typeIn if (ctx.env && ctx.env !== env) ctx.env.DISCORD_ACTIVITY_TYPE = typeIn try { await discordPersistDiscordEnvKey(ctx, 'DISCORD_ACTIVITY_TYPE', typeIn) } catch { /* session env still applied */ } changed = true } if (nameIn) { env.DISCORD_ACTIVITY_NAME = nameIn if (ctx.env && ctx.env !== env) ctx.env.DISCORD_ACTIVITY_NAME = nameIn try { await discordPersistDiscordEnvKey(ctx, 'DISCORD_ACTIVITY_NAME', nameIn) } catch { /* session env still applied */ } changed = true } if (stateIn) { if (!BARE_OS_DISCORD_PRESENCE_STATUS[stateIn]) { return { text: 'Unknown status. Use online, idle, dnd, or invisible.', ephemeral: true } } env.DISCORD_STATUS = stateIn if (ctx.env && ctx.env !== env) ctx.env.DISCORD_STATUS = stateIn try { await discordPersistDiscordEnvKey(ctx, 'DISCORD_STATUS', stateIn) } catch { /* session env still applied */ } changed = true } if (changed) discordApplyPresence(ctx) const p = discordPresenceFromEnv(ctx) const desc = p.name ? '**' + p.typeName + '** ' + p.name + '\nStatus: **' + p.status + '**' : 'No activity\nStatus: **' + p.status + '**' return discordResult( discordEmbed({ title: changed ? 'Presence updated' : 'Presence', desc: desc, color: BARE_OS_DISCORD_COLOR_OK, footer: 'Shown in the Discord member list · persisted in ~/.discord/.env' }) ) } function discordUserAllowed(ctx, userId, interaction) { const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx)) let n = 0 for (const k in ids) { if (Object.prototype.hasOwnProperty.call(ids, k)) n++ } const id = String(userId == null ? '' : userId).trim() const userInstallIx = discordIsUserInstallInteraction(interaction) // Profile-install / DM / private-channel: never open to the world. if (n === 0) return !userInstallIx return Boolean(id && ids[id]) } var BARE_OS_DISCORD_CHANNEL_PATH = '~/.discord/channel.json' var BARE_OS_DISCORD_OUTBOX_PATH = '~/.discord/outbox.json' var BARE_OS_DISCORD_INBOX_PATH = '~/.discord/inbox.json' var BARE_OS_DISCORD_CHANNEL_TIMER = null var BARE_OS_DISCORD_CHANNEL_SEQ = 0 function discordChannelDmGate(userId) { const uid = String(userId || '').trim() const owners = {} owners[String(BARE_OS_DISCORD_INTEGRATION_USER)] = uid return { context: BARE_OS_DISCORD_CONTEXT_BOT_DM, authorizingIntegrationOwners: owners, user: { id: uid } } } function discordChannelSnowflake(raw) { const id = String(raw == null ? '' : raw).trim() return /^\d{3,32}$/.test(id) ? id : '' } function discordChannelSoleWhitelistId(ctx) { const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx)) const keys = [] for (const k in ids) { if (Object.prototype.hasOwnProperty.call(ids, k)) keys.push(k) } return keys.length === 1 ? keys[0] : '' } async function discordChannelReadList(ctx, path) { const t = await discordCmdReadText(ctx, path) const j = discordTryJson(t) return Array.isArray(j) ? j : [] } async function discordChannelWriteList(ctx, path, list) { if (!ctx || !ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { throw new Error('writeFile unavailable') } await discordEnsureDir(ctx, '~/.discord') const body = JSON.stringify(Array.isArray(list) ? list : [], null, 2) + '\n' await ctx.vfs.writeFile(path, discordTextToBuf(ctx, body)) } async function discordChannelLoad(ctx) { const rec = await discordReadJsonFile(ctx, BARE_OS_DISCORD_CHANNEL_PATH) const userId = discordChannelSnowflake(rec.userId || rec.user_id) return { userId: userId, open: rec.open !== false && Boolean(userId), openedAt: rec.openedAt || '', lastInboundAt: rec.lastInboundAt || '', lastOutboundAt: rec.lastOutboundAt || '', lastInboundPreview: rec.lastInboundPreview || '', lastOutboundPreview: rec.lastOutboundPreview || '' } } async function discordChannelSave(ctx, rec) { await discordEnsureDir(ctx, '~/.discord') await discordWriteJsonFile(ctx, BARE_OS_DISCORD_CHANNEL_PATH, rec || {}) } function discordChannelPreview(text) { return String(text || '') .replace(/\s+/g, ' ') .slice(0, 120) } async function discordChannelRememberInbound(ctx, userId, text) { const uid = discordChannelSnowflake(userId) if (!uid) return null if (!discordUserAllowed(ctx, uid, discordChannelDmGate(uid))) return null const now = new Date().toISOString() const rec = await discordChannelLoad(ctx) rec.userId = uid rec.open = true if (!rec.openedAt) rec.openedAt = now rec.lastInboundAt = now rec.lastInboundPreview = discordChannelPreview(text) await discordChannelSave(ctx, rec) try { const inbox = await discordChannelReadList(ctx, BARE_OS_DISCORD_INBOX_PATH) inbox.push({ id: 'in-' + String(Date.now()) + '-' + String(++BARE_OS_DISCORD_CHANNEL_SEQ), userId: uid, text: String(text || '').slice(0, 4000), createdAt: now, direction: 'in' }) await discordChannelWriteList(ctx, BARE_OS_DISCORD_INBOX_PATH, inbox.slice(-50)) } catch { /* inbox is optional */ } return rec } function discordChannelResolveUserId(ctx, rec, explicit) { const raw = String(explicit == null ? '' : explicit).trim() if (raw) { const want = discordChannelSnowflake(raw) if (!want) return { error: 'invalid_user_id' } return { userId: want } } if (rec && rec.userId) return { userId: rec.userId } const sole = discordChannelSoleWhitelistId(ctx) return sole ? { userId: sole } : { error: 'no_channel_partner' } } async function discordClientSendDm(client, userId, payload) { if (!client) throw new Error('discord client unavailable') if (typeof client.bareOsSendDm === 'function') { return client.bareOsSendDm(userId, payload) } const users = client.users if (users && typeof users.fetch === 'function') { const user = await users.fetch(userId) if (user && typeof user.send === 'function') return user.send(payload) } if (users && typeof users.createDM === 'function') { const ch = await users.createDM(userId) if (ch && typeof ch.send === 'function') return ch.send(payload) } if (client.user && typeof client.user.createDM === 'function') { const ch = await client.user.createDM(userId) if (ch && typeof ch.send === 'function') return ch.send(payload) } throw new Error('discord client cannot send DMs') } function discordChannelPayload(text) { const body = discordCmdRedact(String(text == null ? '' : text).trim()) if (!body) return null if (body.length <= 1900) return { content: body } return { embeds: [ discordEmbed({ desc: body, color: BARE_OS_DISCORD_COLOR_OK, prefer: 'desc' }) ] } } async function discordChannelEnqueue(ctx, item) { const list = await discordChannelReadList(ctx, BARE_OS_DISCORD_OUTBOX_PATH) list.push(item) await discordChannelWriteList(ctx, BARE_OS_DISCORD_OUTBOX_PATH, list.slice(-100)) return item } async function discordChannelSend(ctx, opts) { opts = opts || {} const text = String(opts.text == null ? opts.message || '' : opts.text).trim() if (!text) return { ok: false, error: 'empty_message' } if (text.length > 4000) return { ok: false, error: 'message_too_long', max: 4000 } const rec = await discordChannelLoad(ctx) const resolved = discordChannelResolveUserId(ctx, rec, opts.userId || opts.user_id) const userId = resolved && resolved.userId if (!userId) { return { ok: false, error: (resolved && resolved.error) || 'no_channel_partner', hint: 'A whitelisted user must DM the bot first, or pass user_id.' } } if (!discordUserAllowed(ctx, userId, discordChannelDmGate(userId))) { return { ok: false, error: 'whitelist_denied', userId: userId } } const payload = discordChannelPayload(text) if (!payload) return { ok: false, error: 'empty_message' } const client = (ctx && ctx.bareOsDiscordClient) || (ctx && ctx.discordClient) || null const queued = { id: 'out-' + String(Date.now()) + '-' + String(++BARE_OS_DISCORD_CHANNEL_SEQ), userId: userId, text: text.slice(0, 4000), createdAt: new Date().toISOString(), source: String(opts.source || 'agent').slice(0, 40) } if (!client) { await discordChannelEnqueue(ctx, queued) return { ok: true, queued: true, id: queued.id, userId: userId } } try { await discordClientSendDm(client, userId, payload) } catch (err) { await discordChannelEnqueue(ctx, queued) return { ok: true, queued: true, id: queued.id, userId: userId, error: String((err && err.message) || err) } } rec.userId = userId rec.open = true rec.lastOutboundAt = queued.createdAt rec.lastOutboundPreview = discordChannelPreview(text) if (!rec.openedAt) rec.openedAt = queued.createdAt await discordChannelSave(ctx, rec) return { ok: true, queued: false, id: queued.id, userId: userId } } async function discordChannelStatus(ctx) { const rec = await discordChannelLoad(ctx) const outbox = await discordChannelReadList(ctx, BARE_OS_DISCORD_OUTBOX_PATH) const inbox = await discordChannelReadList(ctx, BARE_OS_DISCORD_INBOX_PATH) return { ok: true, open: rec.open, userId: rec.userId || '', openedAt: rec.openedAt || '', lastInboundAt: rec.lastInboundAt || '', lastOutboundAt: rec.lastOutboundAt || '', lastInboundPreview: rec.lastInboundPreview || '', lastOutboundPreview: rec.lastOutboundPreview || '', pendingOutbox: outbox.length, inboxCount: inbox.length, liveClient: Boolean( (ctx && ctx.bareOsDiscordClient) || (ctx && ctx.discordClient) ) } } async function discordChannelReadInbox(ctx, max) { const n = Math.max(1, Math.min(50, Math.floor(Number(max) || 12))) const inbox = await discordChannelReadList(ctx, BARE_OS_DISCORD_INBOX_PATH) return { ok: true, count: inbox.length, messages: inbox.slice(-n) } } async function discordChannelDrainOutbox(ctx) { const client = (ctx && ctx.bareOsDiscordClient) || (ctx && ctx.discordClient) || null if (!client) return { ok: false, drained: 0, reason: 'no_client' } const list = await discordChannelReadList(ctx, BARE_OS_DISCORD_OUTBOX_PATH) if (!list.length) return { ok: true, drained: 0 } const keep = [] let drained = 0 for (let i = 0; i < list.length; i++) { const item = list[i] if (!item || !item.text) continue const userId = discordChannelSnowflake(item.userId) if (!userId || !discordUserAllowed(ctx, userId, discordChannelDmGate(userId))) { continue } const payload = discordChannelPayload(item.text) if (!payload) continue try { await discordClientSendDm(client, userId, payload) drained++ const rec = await discordChannelLoad(ctx) rec.userId = userId rec.open = true rec.lastOutboundAt = new Date().toISOString() rec.lastOutboundPreview = discordChannelPreview(item.text) await discordChannelSave(ctx, rec) } catch { keep.push(item) } } await discordChannelWriteList(ctx, BARE_OS_DISCORD_OUTBOX_PATH, keep) return { ok: true, drained: drained, remaining: keep.length } } function discordChannelBindHook(ctx) { if (!ctx) return ctx.bareOsDiscordSendDm = function (opts) { return discordChannelSend(ctx, opts || {}) } ctx.bareOsDiscordChannelStatus = function () { return discordChannelStatus(ctx) } } function discordChannelStartPump(ctx) { if (BARE_OS_DISCORD_CHANNEL_TIMER) return BARE_OS_DISCORD_CHANNEL_TIMER = setInterval(function () { const live = ctx Promise.resolve(discordChannelDrainOutbox(live)).catch(function () {}) }, 1500) if ( BARE_OS_DISCORD_CHANNEL_TIMER && typeof BARE_OS_DISCORD_CHANNEL_TIMER.unref === 'function' ) { BARE_OS_DISCORD_CHANNEL_TIMER.unref() } } function discordChannelStopPump() { if (!BARE_OS_DISCORD_CHANNEL_TIMER) return clearInterval(BARE_OS_DISCORD_CHANNEL_TIMER) BARE_OS_DISCORD_CHANNEL_TIMER = null } function discordInteractionUserId(interaction) { if (!interaction) return '' if (interaction.user && interaction.user.id) return String(interaction.user.id) const member = interaction.member if (member && member.user && member.user.id) return String(member.user.id) if (member && member.id) return String(member.id) return '' } function discordCallFlag(interaction, name) { return Boolean( interaction && typeof interaction[name] === 'function' && interaction[name]() ) } function discordIsAutocomplete(interaction) { return discordCallFlag(interaction, 'isAutocomplete') } function discordIsModalSubmit(interaction) { return discordCallFlag(interaction, 'isModalSubmit') } function discordIsMessageComponent(interaction) { if (!interaction) return false if (discordCallFlag(interaction, 'isMessageComponent')) return true if (discordCallFlag(interaction, 'isButton')) return true if (discordCallFlag(interaction, 'isAnySelectMenu')) return true if (discordCallFlag(interaction, 'isStringSelectMenu')) return true if (discordCallFlag(interaction, 'isUserSelectMenu')) return true if (discordCallFlag(interaction, 'isRoleSelectMenu')) return true if (discordCallFlag(interaction, 'isMentionableSelectMenu')) return true if (discordCallFlag(interaction, 'isChannelSelectMenu')) return true return false } function discordInteractionOwnerId(interaction) { const msg = interaction && interaction.message if (msg) { const meta = msg.interactionMetadata || msg.interaction_metadata if (meta && meta.user && meta.user.id) return String(meta.user.id) if (msg.interaction && msg.interaction.user && msg.interaction.user.id) { return String(msg.interaction.user.id) } const mid = msg.id != null ? String(msg.id) : '' if (mid && BARE_OS_DISCORD_LIVE[mid] && BARE_OS_DISCORD_LIVE[mid].userId) { return String(BARE_OS_DISCORD_LIVE[mid].userId) } } return '' } function discordFilterChoices(items, q, toChoice) { const needle = String(q || '').toLowerCase() const out = [] for (let i = 0; i < items.length && out.length < 25; i++) { const raw = items[i] const choice = toChoice ? toChoice(raw) : { name: String(raw), value: String(raw) } if (!choice || !choice.value) continue const hay = (choice.name + ' ' + choice.value).toLowerCase() if (needle && hay.indexOf(needle) < 0) continue out.push({ name: String(choice.name).slice(0, 100), value: String(choice.value).slice(0, 100) }) } return out } async function discordSuggestUnits(ctx, q) { const names = BARE_OS_DISCORD_KNOWN_UNITS.slice() if (typeof ctx.bareOsRunSystemctlCli === 'function') { const out = await discordCmdCapture(ctx, function () { return ctx.bareOsRunSystemctlCli(['systemctl', 'list']) }) const lines = String(out || '').split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const m = /^([A-Za-z0-9_@.:-]+)\b/.exec(lines[i].trim()) if (!m) continue const n = m[1].replace(/\.service$/, '') if (names.indexOf(n) < 0) names.push(n) } } return discordFilterChoices(names, q) } async function discordSuggestMan(ctx, q) { const db = await discordCmdReadJson(ctx, '/share/man/man.json') const pages = db && Array.isArray(db.pages) ? db.pages : [] const names = [] for (let i = 0; i < pages.length; i++) { if (pages[i] && pages[i].name) names.push(String(pages[i].name)) } if (!names.length) { names.push('uname', 'whoami', 'login', 'discord-bot', 'systemctl', 'help') } return discordFilterChoices(names, q) } function discordShKey(interaction) { return discordInteractionUserId(interaction) || 'anon' } function discordShGc() { const now = Date.now() for (const k in BARE_OS_DISCORD_SH_SESSIONS) { const r = BARE_OS_DISCORD_SH_SESSIONS[k] if (!r || now - r.atMs > BARE_OS_DISCORD_SH_TTL_MS) delete BARE_OS_DISCORD_SH_SESSIONS[k] } } function discordShGet(interaction, ctx) { discordShGc() const key = discordShKey(interaction) let rec = BARE_OS_DISCORD_SH_SESSIONS[key] const env = discordCmdEnv(ctx) const home = env.HOME || '/home/' + discordSessionUser(ctx) if (!rec) { rec = { cwd: env.PWD || home || '~', oldpwd: home, hist: [], lastCmd: '', lastExit: 0, lastMs: 0, atMs: Date.now() } BARE_OS_DISCORD_SH_SESSIONS[key] = rec } rec.atMs = Date.now() if (!rec.cwd) rec.cwd = env.PWD || home || '~' return rec } function discordShPrettyCwd(ctx, cwd) { const env = discordCmdEnv(ctx) const home = env.HOME || '/home/' + discordSessionUser(ctx) const c = String(cwd || '~') if (c === '~' || c === home) return '~' if (home && c.indexOf(home + '/') === 0) return '~' + c.slice(home.length) return c } function discordShPrompt(ctx, rec) { return ( discordSessionUser(ctx) + '@' + (discordCmdEnv(ctx).HOSTNAME || 'bare-os') + ':' + discordShPrettyCwd(ctx, rec.cwd) ) } async function discordShPrepare(ctx, rec) { const env = discordCmdEnv(ctx) const cwd = rec.cwd || env.HOME || '~' rec.cwd = cwd env.PWD = cwd if (ctx.env && ctx.env !== env) ctx.env.PWD = cwd if (ctx.vfs && typeof ctx.vfs.chdir === 'function') { try { await ctx.vfs.chdir(cwd) } catch { /* keep env PWD */ } } } function discordShHarvest(ctx, rec) { const env = discordCmdEnv(ctx) if (env.PWD) rec.cwd = String(env.PWD) if (ctx.vfs && ctx.vfs.env && ctx.vfs.env.PWD) rec.cwd = String(ctx.vfs.env.PWD) } function discordShPushHist(rec, cmd, exit, ms, preview) { rec.lastCmd = cmd rec.lastExit = exit rec.lastMs = ms rec.hist = rec.hist.filter(function (h) { return h && h.cmd !== cmd }) rec.hist.unshift({ cmd: cmd, exit: exit, ms: ms, preview: String(preview || '').slice(0, 80) }) if (rec.hist.length > 25) rec.hist.length = 25 } async function discordShCommandNames(ctx) { const now = Date.now() if (BARE_OS_DISCORD_SH_BINS && now - BARE_OS_DISCORD_SH_BINS_AT < 60000) { return BARE_OS_DISCORD_SH_BINS } const set = Object.create(null) for (let i = 0; i < BARE_OS_DISCORD_SH_BUILTINS.length; i++) { set[BARE_OS_DISCORD_SH_BUILTINS[i]] = 1 } for (const k in BARE_OS_DISCORD_RUN_ALLOW) { if (Object.prototype.hasOwnProperty.call(BARE_OS_DISCORD_RUN_ALLOW, k) && k.indexOf(' ') < 0) { set[k] = 1 } } if (ctx && ctx.shellAliases && typeof ctx.shellAliases === 'object') { const aks = Object.keys(ctx.shellAliases) for (let i = 0; i < aks.length; i++) set[aks[i]] = 1 } try { if (ctx && ctx.vfs && typeof ctx.vfs.readdir === 'function') { const n = await ctx.vfs.readdir('/bin') if (Array.isArray(n)) { for (let i = 0; i < n.length; i++) set[String(n[i])] = 1 } } } catch { /* ignore */ } const man = await discordCmdReadText(ctx, '/share/man/man.json') const j = discordTryJson(man) if (j && Array.isArray(j.pages)) { for (let i = 0; i < j.pages.length; i++) { if (j.pages[i] && j.pages[i].name) set[String(j.pages[i].name)] = 1 } } const names = Object.keys(set).sort() BARE_OS_DISCORD_SH_BINS = names BARE_OS_DISCORD_SH_BINS_AT = now return names } function discordShSplitLine(q) { const s = String(q || '') const m = /^(.*?)(\S*)$/.exec(s) return { prefix: m ? m[1] : '', token: m ? m[2] : s } } function discordShFirstWord(q) { const s = String(q || '').trim() const i = s.search(/\s/) return i < 0 ? s : s.slice(0, i) } async function discordShCompletePath(ctx, rec, token, dirsOnly) { let dir = rec.cwd || '~' let base = token const slash = token.lastIndexOf('/') if (slash >= 0) { const head = token.slice(0, slash + 1) base = token.slice(slash + 1) if (head.charAt(0) === '/' || head.charAt(0) === '~') dir = head === '/' ? '/' : head.replace(/\/$/, '') || '/' else dir = discordJoinPath(rec.cwd, head.replace(/\/$/, '') || '.') } let names = [] try { if (ctx.vfs && typeof ctx.vfs.readdir === 'function') { const n = await ctx.vfs.readdir(dir) if (Array.isArray(n)) names = n } } catch { names = [] } const out = [] const want = String(base || '').toLowerCase() for (let i = 0; i < names.length && out.length < 20; i++) { const name = String(names[i]) if (want && name.toLowerCase().indexOf(want) !== 0) continue const full = discordJoinPath(dir, name) let isDir = false try { const st = await discordFmStat(ctx, full) isDir = discordIsDirStat(st) } catch { isDir = false } if (dirsOnly && !isDir) continue const shown = (slash >= 0 ? token.slice(0, slash + 1) : '') + name + (isDir ? '/' : '') out.push({ label: shown, value: shown, description: isDir ? 'directory' : 'file' }) } return out } function discordShPushChoice(out, seen, name, value, description) { const v = String(value || '').slice(0, 100) if (!v || seen[v]) return if (out.length >= 25) return seen[v] = 1 out.push({ name: String(name || v).slice(0, 100), value: v, description: description ? String(description).slice(0, 100) : undefined }) } async function discordSuggestRun(ctx, interaction, q) { const rec = discordShGet(interaction, ctx) const raw = String(q || '') const split = discordShSplitLine(raw) const first = discordShFirstWord(raw) const out = [] const seen = Object.create(null) if (!raw.trim() || (!split.prefix && first === split.token)) { for (let i = 0; i < rec.hist.length && out.length < 6; i++) { const h = rec.hist[i] if (!h || !h.cmd) continue if (split.token && h.cmd.indexOf(split.token) !== 0) continue discordShPushChoice(out, seen, '↵ ' + h.cmd, h.cmd, 'history · exit ' + h.exit) } const cmds = await discordShCommandNames(ctx) const needle = split.token.toLowerCase() for (let i = 0; i < cmds.length && out.length < 25; i++) { if (needle && cmds[i].toLowerCase().indexOf(needle) !== 0) continue const kind = BARE_OS_DISCORD_SH_TTY[cmds[i]] ? 'TUI (needs a real TTY)' : ctx.shellAliases && ctx.shellAliases[cmds[i]] ? 'alias' : BARE_OS_DISCORD_SH_BUILTINS.indexOf(cmds[i]) >= 0 ? 'builtin' : 'command' discordShPushChoice(out, seen, cmds[i], cmds[i], kind) } return out } const flags = BARE_OS_DISCORD_SH_FLAGS[first] if (split.token.charAt(0) === '-' && flags) { for (let i = 0; i < flags.length && out.length < 25; i++) { if (split.token && flags[i].indexOf(split.token) !== 0) continue const line = (split.prefix + flags[i]).slice(0, 100) discordShPushChoice(out, seen, line, line, first + ' flag') } return out } const pathish = !split.token || split.token.charAt(0) === '.' || split.token.charAt(0) === '/' || split.token.charAt(0) === '~' || split.token.indexOf('/') >= 0 || first === 'cd' || first === 'ls' || first === 'cat' || first === 'rm' || first === 'mv' || first === 'cp' || first === 'mkdir' || first === 'stat' || first === 'head' || first === 'tail' || first === 'edit' if (pathish) { const paths = await discordShCompletePath(ctx, rec, split.token, first === 'cd') for (let i = 0; i < paths.length && out.length < 25; i++) { const line = (split.prefix + paths[i].value).slice(0, 100) discordShPushChoice(out, seen, line, line, paths[i].description) } } if (!out.length && split.token) { const cmds = await discordShCommandNames(ctx) const needle = split.token.toLowerCase() for (let i = 0; i < cmds.length && out.length < 25; i++) { if (cmds[i].toLowerCase().indexOf(needle) !== 0) continue const line = (split.prefix + cmds[i]).slice(0, 100) discordShPushChoice(out, seen, line, line, 'command') } } return out } function discordFmKey(interaction) { return discordInteractionUserId(interaction) || 'anon' } function discordFmGc() { const now = Date.now() for (const k in BARE_OS_DISCORD_FM_SESSIONS) { const rec = BARE_OS_DISCORD_FM_SESSIONS[k] if (!rec || now - rec.atMs > BARE_OS_DISCORD_FM_TTL_MS) { delete BARE_OS_DISCORD_FM_SESSIONS[k] } } } function discordFmGet(interaction, ctx) { discordFmGc() let rec = BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)] if (!rec) { rec = { cwd: '~', page: 0, sel: '', confirm: '', atMs: Date.now() } BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)] = rec } rec.atMs = Date.now() if (ctx) discordSyncSessionIdentity(ctx) return rec } function discordJoinPath(dir, name) { const n = String(name || '').replace(/^\/+/, '') if (!n || n === '.') return dir || '~' if (n === '..') return discordParentPath(dir) const d = String(dir || '~').replace(/\/+$/, '') || '/' if (d === '/') return '/' + n if (d === '~') return '~/' + n return d + '/' + n } function discordParentPath(p) { const s = String(p || '~').replace(/\/+$/, '') || '/' if (s === '/' || s === '~') return s const i = s.lastIndexOf('/') if (i <= 0) return s.charAt(0) === '~' ? '~' : '/' const parent = s.slice(0, i) return parent || (s.charAt(0) === '~' ? '~' : '/') } function discordBaseName(p) { const s = String(p || '').replace(/\/+$/, '') const i = s.lastIndexOf('/') return i >= 0 ? s.slice(i + 1) : s } function discordIsDirStat(st) { if (!st) return false if (st.isDirectory === true) return true if (st.type === 'directory') return true return false } async function discordFmList(ctx, cwd) { const p = discordCmdPathOk(cwd || '~') || '~' if (typeof ctx.vfs?.readdir !== 'function') return { path: p, names: [], err: 'readdir unavailable' } try { const raw = await ctx.vfs.readdir(p) const names = [] const list = Array.isArray(raw) ? raw : [] for (let i = 0; i < list.length; i++) { const n = String(list[i] || '') if (!n || n === '.bareos_empty') continue names.push(n) } names.sort(function (a, b) { return a < b ? -1 : a > b ? 1 : 0 }) return { path: p, names: names, err: '' } } catch (err) { return { path: p, names: [], err: (err && err.message) || String(err) } } } async function discordFmStat(ctx, path) { const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat) if (typeof stfn !== 'function') return null try { return await stfn.call(ctx.vfs, path) } catch { return null } } async function discordFmView(ctx, interaction) { const rec = discordFmGet(interaction, ctx) const listing = await discordFmList(ctx, rec.cwd) rec.cwd = listing.path const names = listing.names const pages = Math.max(1, Math.ceil(names.length / BARE_OS_DISCORD_FM_PAGE)) if (rec.page >= pages) rec.page = pages - 1 if (rec.page < 0) rec.page = 0 const start = rec.page * BARE_OS_DISCORD_FM_PAGE const slice = names.slice(start, start + BARE_OS_DISCORD_FM_PAGE) if (rec.sel && names.indexOf(rec.sel) < 0) rec.sel = '' const writable = Boolean(discordCmdPathWriteOk(ctx, rec.cwd)) const fields = [] const options = [] if (rec.cwd !== '/' && rec.cwd !== '~') { options.push({ label: '⬆ .. (parent)', value: '..', description: discordParentPath(rec.cwd) }) } for (let i = 0; i < slice.length; i++) { const name = slice[i] const full = discordJoinPath(rec.cwd, name) const st = await discordFmStat(ctx, full) const dir = discordIsDirStat(st) const size = st && st.size != null && !dir ? discordPrettyBytes(st.size) : dir ? 'dir' : '' const mark = rec.sel === name ? '▸ ' : '' fields.push( discordField( mark + (dir ? '📁 ' : '📄 ') + name, size || (dir ? 'folder' : 'file'), true ) ) options.push({ label: (dir ? '📁 ' : '📄 ') + name, value: name.slice(0, 100), description: size || (dir ? 'open folder' : 'file') }) } const sel = discordSelect( 'fm:pick', slice.length ? 'Open an entry…' : 'Empty folder', options ) const nav = discordButtons([ { id: 'fm:up', label: 'Parent' }, { id: 'fm:home', label: 'Home', style: 1 }, { id: 'fm:goto', label: 'Go to…' }, { id: 'fm:prev', label: '◀' }, { id: 'fm:next', label: '▶' } ]) const acts = discordButtons( writable ? [ { id: 'fm:open', label: 'Open', style: 1 }, { id: 'fm:edit', label: 'Edit' }, { id: 'fm:new', label: 'New file', style: 3 }, { id: 'fm:mkdir', label: 'New folder' }, { id: 'fm:more', label: 'Manage…' } ] : [ { id: 'fm:open', label: 'Open', style: 1 }, { id: 'nav:panel', label: 'Panel' } ] ) const manage = rec.confirm ? discordButtons([ { id: 'fm:ok', label: 'Confirm delete', style: 4 }, { id: 'fm:no', label: 'Cancel', style: 2 } ]) : writable ? discordButtons([ { id: 'fm:ren', label: 'Rename' }, { id: 'fm:copy', label: 'Copy' }, { id: 'fm:del', label: 'Delete', style: 4 }, { id: 'fm:ref', label: 'Refresh' }, { id: 'nav:panel', label: 'Panel' } ]) : discordButtons([{ id: 'fm:ref', label: 'Refresh' }]) const rows = [sel, nav, acts, manage].filter(Boolean) let desc = '`' + rec.cwd + '` · ' + names.length + ' items · page ' + (rec.page + 1) + '/' + pages desc += writable ? ' · writable' : ' · read-only' if (rec.sel) desc += '\nSelected: **' + rec.sel + '**' if (rec.confirm) desc += '\n**Delete `' + rec.confirm + '`?** This cannot be undone.' if (listing.err) desc += '\n' + listing.err return discordResult( discordEmbed({ title: 'Files', desc: desc, fields: fields.slice(0, 20), color: rec.confirm ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR, footer: rec.cwd + ' · ' + discordSessionUser(ctx) }), { components: rows, nav: false } ) } function discordSuggestEditPaths(ctx, q) { const user = discordSessionUser(ctx) const names = BARE_OS_DISCORD_EDIT_SUGGEST.concat([ '/home/' + user + '/notes.txt', '/tmp/' + user + '.txt' ]) return discordFilterChoices(names, q) } function discordSuggestPaths(ctx, q) { const e = discordCmdEnv(ctx) const home = e.HOME || '/home/' + discordSessionUser(ctx) const extra = [home, home + '/.discord', '/home/' + discordSessionUser(ctx)] const names = BARE_OS_DISCORD_FS_SUGGEST.concat(extra) return discordFilterChoices(names, q) } async function discordCmdStatus(ctx) { const e = discordCmdEnv(ctx) const os = await discordCmdOsRelease(ctx) const user = discordSessionUser(ctx) const ident = ctx && ctx.identity && ctx.identity.state ? String(ctx.identity.state) : e.BARE_OS_IDENTITY || 'guest' return discordResult( discordEmbed({ title: 'Bare OS session', color: ident === 'unlocked' ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN, author: { name: user + '@' + (e.HOSTNAME || e.NAME || 'bare-os') }, fields: [ discordField('OS', os.PRETTY_NAME || os.NAME || 'Bare OS', true), discordField('User', user, true), discordField('Identity', ident, true), discordField('Home', e.HOME || '/home/' + user, true), discordField('Shell', e.SHELL || '/bin/sh', true), discordField('UID', e.UID || '—', true), discordField('Version', os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || e.BARE_OS_RELEASE || '—', true), discordField('Arch', e.BARE_OS_ARCH || e.MACHINE || '—', true), discordField('Booter', e.BARE_OS_BOOTER_PACKAGE_VERSION || '—', true) ] }) ) } async function discordCmdUname(ctx) { const e = discordCmdEnv(ctx) const os = await discordCmdOsRelease(ctx) const line = [ os.NAME || 'BareOS', e.HOSTNAME || 'bare-os', os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || '0.1', e.BARE_OS_ARCH || e.MACHINE || 'unknown', os.PRETTY_NAME || e.BARE_OS_BUILD || 'bare-userland' ].join(' ') return discordResult( discordEmbed({ title: 'uname', desc: discordCmdFence(line) }) ) } async function discordCmdHandleBare(ctx, sub) { const e = discordCmdEnv(ctx) const user = discordSessionUser(ctx) if (sub === 'ping') { return discordResult( discordEmbed({ title: 'Pong', color: BARE_OS_DISCORD_COLOR_OK, desc: 'Bare OS is online as **' + user + '**.' }) ) } if (sub === 'about') { return discordResult( discordEmbed({ title: 'Bare OS Discord bot', desc: 'Slash control surface for **this logged-in session** (`' + user + '`). Commands run with that user’s `HOME`, VFS, and `systemctl`.' }) ) } if (sub === 'help') { return discordResult( discordEmbed({ 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( '/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'), discordField('/hdms', 'Hyperdrives — list, create, add, invite, pair, /mnt'), discordField('/holesail', 'Tunnels — list, add, edit, start/stop, enable, service'), discordField('/agent', 'Guest AI agent — ask, status, reset (when configured)'), 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') ] }) ) } if (sub === 'status') return discordCmdStatus(ctx) if (sub === 'uname') return discordCmdUname(ctx) if (sub === 'whoami') { return discordResult( discordEmbed({ title: 'Whoami', color: BARE_OS_DISCORD_COLOR_OK, fields: [ discordField('User', user, true), discordField('Home', '`' + (e.HOME || '/home/' + user) + '`', true), discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true) ] }) ) } if (sub === 'hostname') { return discordResult( discordEmbed({ title: 'Hostname', desc: '`' + String(e.HOSTNAME || e.NAME || 'bare-os') + '`' }) ) } if (sub === 'date') { return discordResult( discordEmbed({ title: 'Date', desc: '`' + new Date().toISOString() + '`' }) ) } if (sub === 'uptime') { const t = await discordCmdReadText(ctx, '/proc/uptime') return discordResult( discordEmbed({ title: 'Uptime', fields: [ discordField('Session', t ? discordPrettyUptime(t) : 'unavailable', true) ] }) ) } if (sub === 'motd') { const t = await discordCmdReadText(ctx, '/etc/motd') if (!t) { return discordResult(discordEmbed({ title: 'motd', desc: 'No `/etc/motd`.' })) } return { more: { title: 'motd', body: t, fence: true } } } return { text: 'unknown /bare subcommand', ephemeral: true } } function discordFormatHostDf(obj) { const host = obj && obj.host && typeof obj.host === 'object' ? obj.host : obj const sv = obj && obj.statvfs && typeof obj.statvfs === 'object' ? obj.statvfs : null const fields = [] if (host) { if (host.platform || host.arch) { fields.push( discordField( 'Host', [host.platform, host.arch, host.hostname || host.machine] .filter(Boolean) .join(' · '), true ) ) } if (host.totalmem != null) { const used = host.freemem != null ? Number(host.totalmem) - Number(host.freemem) : null fields.push(discordField('RAM', discordPrettyBytes(host.totalmem), true)) if (host.freemem != null) { fields.push(discordField('Free RAM', discordPrettyBytes(host.freemem), true)) } if (used != null && host.totalmem) { const pct = Math.round((used / Number(host.totalmem)) * 100) fields.push(discordField('RAM used', pct + '%', true)) } } if (host.availableParallelism != null) { fields.push(discordField('CPUs', String(host.availableParallelism), true)) } if (host.release) fields.push(discordField('Release', host.release, true)) } if (obj && obj.peers != null) fields.push(discordField('Peers', String(obj.peers), true)) if (sv) { fields.push(discordField('Volume', sv.volume_class || sv.f_basetype || '—', true)) if (sv.pwd_logical) fields.push(discordField('PWD', sv.pwd_logical, true)) const bsize = Number(sv.f_frsize || sv.f_bsize || 4096) if (sv.f_blocks != null) { fields.push(discordField('System size', discordPrettyBytes(Number(sv.f_blocks) * bsize), true)) } if (sv.f_bavail != null) { fields.push(discordField('System avail', discordPrettyBytes(Number(sv.f_bavail) * bsize), true)) } const pers = sv.personalDrive if (pers && pers.f_blocks != null) { const pb = Number(pers.f_frsize || pers.f_bsize || bsize) fields.push( discordField('Personal size', discordPrettyBytes(Number(pers.f_blocks) * pb), true) ) } } if (obj && obj.pipeline && obj.pipeline.maxBytes != null) { fields.push(discordField('Pipe cap', discordPrettyBytes(obj.pipeline.maxBytes), true)) } return fields } function discordFormatMem(raw) { const json = discordTryJson(raw) if (json && json.host) { return discordFormatHostDf(json) } const mi = discordParseMeminfo(raw) const fields = [] const pick = [ ['MemTotal', 'Total'], ['MemFree', 'Free'], ['MemAvailable', 'Available'], ['Buffers', 'Buffers'], ['Cached', 'Cached'], ['SwapTotal', 'Swap'], ['SwapFree', 'Swap free'] ] for (let i = 0; i < pick.length; i++) { if (mi[pick[i][0]] != null) { fields.push(discordField(pick[i][1], discordPrettyBytes(mi[pick[i][0]]), true)) } } if (mi.MemTotal && mi.MemAvailable != null) { const pct = Math.round((1 - mi.MemAvailable / mi.MemTotal) * 100) fields.push(discordField('Used', pct + '%', true)) } return fields } function discordFormatRlimits(obj) { const lim = obj && obj.rlimits && typeof obj.rlimits === 'object' ? obj.rlimits : obj const fields = [] if (!lim || typeof lim !== 'object') return fields const names = { NOFILE: 'Open files', NPROC: 'Processes', AS: 'Address space', DATA: 'Data', STACK: 'Stack', CORE: 'Core dump', RSS: 'RSS', CPU: 'CPU time', FSIZE: 'File size', NOVM: 'No VM', MEMLOCK: 'Locked mem' } const keys = Object.keys(lim) for (let i = 0; i < keys.length && fields.length < 18; i++) { const raw = keys[i] const v = lim[raw] if (v && typeof v === 'object' && (v.cur != null || v.max != null)) { const short = raw.replace(/^RLIMIT_/, '') const label = names[short] || discordHumanLabel(short) const cur = discordIsByteishKey(raw) ? discordPrettyBytes(v.cur) : String(v.cur) const max = discordIsByteishKey(raw) ? discordPrettyBytes(v.max) : String(v.max) fields.push(discordField(label, cur + ' / ' + max, true)) } } if (obj && obj.bareOsExecMaxDepth != null) { fields.push(discordField('Exec depth', String(obj.bareOsExecMaxDepth), true)) } return fields } function discordFormatFeatures(obj) { const feat = obj && obj.features && typeof obj.features === 'object' ? obj.features : obj if (!feat || typeof feat !== 'object') return [] const on = [] const off = [] const keys = Object.keys(feat).sort() for (let i = 0; i < keys.length; i++) { const v = feat[keys[i]] if (v === true || v === 1 || v === '1') on.push('`' + keys[i] + '`') else if (v === false || v === 0 || v === '0') off.push('`' + keys[i] + '`') } const fields = [] if (on.length) fields.push(discordField('On', on.slice(0, 24).join(' '))) if (off.length) fields.push(discordField('Off', off.slice(0, 16).join(' '))) return fields } function discordFormatDoctor(obj) { if (!obj || typeof obj !== 'object') { return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR_WARN } } const pa = obj.peerAdmission && typeof obj.peerAdmission === 'object' ? obj.peerAdmission : {} const hn = obj.hostnameMutation && typeof obj.hostnameMutation === 'object' ? obj.hostnameMutation : {} const kh = obj.keyHandlePolicy && typeof obj.keyHandlePolicy === 'object' ? obj.keyHandlePolicy : {} const fields = [] if (pa.allowlistConfigured != null) { fields.push(discordField('Peer allowlist', pa.allowlistConfigured ? 'configured' : 'open', true)) } if (pa.denylistConfigured != null) { fields.push(discordField('Peer denylist', pa.denylistConfigured ? 'configured' : 'none', true)) } if (pa.requireCapsConfigured != null) { const n = pa.requireCapsTokenCount fields.push( discordField( 'Require caps', pa.requireCapsConfigured ? (n ? n + ' tokens' : 'yes') : 'no', true ) ) } if (hn.enabled != null) { fields.push(discordField('Hostname set', hn.enabled ? 'allowed' : 'locked', true)) } if (kh.defaultTtlMs) { fields.push(discordField('Key TTL', discordPrettyValue('defaultTtlMs', kh.defaultTtlMs), true)) } if (Array.isArray(obj.mfaExtensionPoints) && obj.mfaExtensionPoints.length) { fields.push(discordField('MFA hooks', obj.mfaExtensionPoints.join(' · '), false)) } const bits = [] if (pa.allowlistConfigured) bits.push('peer allowlist on') else if (pa.allowlistConfigured === false) bits.push('peer allowlist **open**') if (hn.enabled === true) bits.push('hostname mutation on') else if (hn.enabled === false) bits.push('hostname locked') return { fields: fields, desc: bits.join(' · '), color: pa.allowlistConfigured ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN } } async function discordCmdHandleSys(ctx, sub) { if (sub === 'df') { const t = (await discordCmdReadText(ctx, '/proc/bare_os_resources')) || (await discordCmdReadText(ctx, '/proc/bare_os/resources')) || (await discordCmdReadText(ctx, '/proc/bare_os/host_os.json')) const obj = discordTryJson(t) const fields = obj ? discordFormatHostDf(obj) : [] if (fields.length) { return discordResult( discordEmbed({ title: 'Disk / host', fields: fields }) ) } return discordPrettySnapshot('Disk / host', t) } if (sub === 'mem') { const t = (await discordCmdReadText(ctx, '/proc/meminfo')) || (await discordCmdReadText(ctx, '/proc/bare_os/host_os.json')) const fields = discordFormatMem(t) if (fields.length) { return discordResult(discordEmbed({ title: 'Memory', fields: fields })) } return discordPrettySnapshot('Memory', t) } if (sub === 'ps') { const table = await discordCmdReadJson(ctx, '/proc/bare_os/process_table.json') const rows = table && Array.isArray(table.processes) ? table.processes : [] const fields = [] for (let i = 0; i < Math.min(rows.length, 18); i++) { const r = rows[i] || {} const name = String(r.name || r.comm || r.cmd || r.id || 'proc') const st = String(r.state || r.status || '') const pid = String(r.pid || r.id || i) fields.push(discordField(name, 'pid ' + pid + (st ? ' · ' + st : ''), true)) } return discordResult( discordEmbed({ title: 'Processes · ' + rows.length, desc: rows.length ? rows.length + ' logical processes in this session.' : 'No logical processes in the session table.', fields: fields }) ) } if (sub === 'env') { const e = discordCmdEnv(ctx) const fields = [ discordField('User', e.USER, true), discordField('Home', e.HOME, true), discordField('Identity', e.BARE_OS_IDENTITY, true), discordField('Shell', e.SHELL, true), discordField('Pwd', e.PWD, true), discordField( 'UID / GID', e.UID || e.GID ? String(e.UID || '—') + ' / ' + String(e.GID || '—') : '', true ) ] const prefer = [ 'HOSTNAME', 'PATH', 'TERM', 'TZ', 'LANG', 'EDITOR', 'PAGER', 'BARE_OS_THEME', 'BARE_OS_COLOR_DEPTH' ] const skip = /TOKEN|SECRET|PASSWORD|PASSWD|KEY|PRIVATE|CREDENTIAL|^USER$|^HOME$|^SHELL$|^PWD$|^LOGNAME$|^UID$|^GID$|^GROUP$|^BARE_OS_IDENTITY$/i const seen = Object.create(null) for (let i = 0; i < prefer.length; i++) { const k = prefer[i] seen[k] = 1 if (e[k] == null || e[k] === '') continue fields.push(discordField(discordHumanLabel(k.replace(/^BARE_OS_/, '')), String(e[k]).slice(0, 80), true)) } const keys = Object.keys(e).sort() for (let i = 0; i < keys.length && fields.length < 15; i++) { if (seen[keys[i]] || skip.test(keys[i])) continue if (e[keys[i]] == null || e[keys[i]] === '') continue fields.push(discordField(keys[i], String(e[keys[i]]).slice(0, 72), true)) } return discordResult( discordEmbed({ title: 'Environment', desc: 'Session env for **' + discordSessionUser(ctx) + '** (secrets omitted).', fields: fields }), { ephemeral: true } ) } if (sub === 'doctor') { const t = (await discordCmdReadText(ctx, '/proc/bare_os/security_posture.json')) || (await discordCmdReadText(ctx, '/proc/bare_os/debug.json')) const obj = discordTryJson(t) const fmt = obj ? discordFormatDoctor(obj) : null if (fmt && fmt.fields.length) { return discordResult( discordEmbed({ title: 'Doctor', desc: fmt.desc, fields: fmt.fields, color: fmt.color }) ) } return discordPrettySnapshot('Doctor', t, { color: fmt && fmt.color }) } if (sub === 'features') { const t = (await discordCmdReadText(ctx, '/proc/bare_os/features')) || (await discordCmdReadText(ctx, '/proc/bare_os_features')) || (await discordCmdReadText(ctx, '/proc/bare_os/features.json')) const obj = discordTryJson(t) const fields = obj ? discordFormatFeatures(obj) : [] if (fields.length) { return discordResult( discordEmbed({ title: 'Features', desc: 'Guest capability flags on this booter.', fields: fields }) ) } return discordPrettySnapshot('Features', t) } if (sub === 'rlimits') { const t = await discordCmdReadText(ctx, '/proc/bare_os/rlimits.json') const obj = discordTryJson(t) const fields = obj ? discordFormatRlimits(obj) : [] if (fields.length) { return discordResult( discordEmbed({ title: 'Resource limits', desc: 'Soft / hard · Bare OS runtime caps (not Linux rlimits).', fields: fields }) ) } return discordPrettySnapshot('Resource limits', t) } return { text: 'unknown /sys subcommand', ephemeral: true } } async function discordCmdHandleSvc(ctx, sub, unit) { const name = String(unit || '').replace(/\.service$/, '') if (typeof ctx.bareOsRunSystemctlCli !== 'function') { return { text: 'systemctl is not available on this ctx', ephemeral: true } } if (sub === 'list') { const out = await discordCmdCapture(ctx, function () { return ctx.bareOsRunSystemctlCli(['systemctl', 'list']) }) const parsed = discordParseSystemctlList(out) const fields = [] for (let i = 0; i < parsed.length && fields.length < 20; i++) { const r = parsed[i] const bits = [] if (r.active) bits.push('**' + r.active + '**') if (r.sub && r.sub !== r.active) bits.push(r.sub) if (r.preset && r.preset !== 'enabled') bits.push(r.preset) fields.push(discordField(r.unit, bits.join(' · ') || 'unknown', true)) } const units = await discordSuggestUnits(ctx, '') const sel = discordSelect( 'svc:pick', 'Inspect a unit…', units.map(function (c) { return { label: c.name, value: c.value } }) ) return discordResult( discordEmbed({ title: 'Services · ' + (parsed.length || 0), desc: parsed.length ? 'Pick a unit to inspect, start, or stop.' : out ? discordCmdClip(out, 800) : 'No units registered in this session.', fields: fields, color: BARE_OS_DISCORD_COLOR }), { components: sel ? [sel] : [] } ) } if (!name) return { text: 'unit name required — pick from autocomplete', ephemeral: true } const argv = ['systemctl', sub === 'logs' ? 'status' : sub, name] if (sub === 'logs') { return discordCmdHandleJournal(ctx, name) } const out = await discordCmdCapture(ctx, function () { return ctx.bareOsRunSystemctlCli(argv) }) const act = discordButtons([ { id: 'svc:status:' + name, label: 'Status', style: 1 }, { id: 'svc:start:' + name, label: 'Start', style: 3 }, { id: 'svc:stop:' + name, label: 'Stop', style: 4 }, { id: 'svc:restart:' + name, label: 'Restart' }, { id: 'svc:logs:' + name, label: 'Logs' } ]) const color = sub === 'stop' ? BARE_OS_DISCORD_COLOR_WARN : sub === 'start' || sub === 'restart' ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR if (!out) { return discordResult( discordEmbed({ title: name, desc: 'No output from `systemctl ' + sub + '`.', color: color }), { components: act ? [act] : [] } ) } return { more: { title: name, lead: '`' + sub + '`', body: discordCmdRedact(out), fence: true, color: color, components: act ? [act] : [] } } } async function discordCmdHandleFs(ctx, sub, rawPath, nlines) { const p = discordCmdPathOk(rawPath || '~') if (!p) { return { text: 'path not allowed (no `..`; stay under /proc /etc /var/log /run /home ~ /share)', ephemeral: true } } if (sub === 'ls') { if (typeof ctx.vfs?.readdir !== 'function') return { text: 'readdir unavailable', ephemeral: true } try { const names = await ctx.vfs.readdir(p) const list = Array.isArray(names) ? names : [] const shown = list.slice(0, 60) const desc = shown.length ? shown .map(function (n) { return '`' + String(n).replace(/`/g, "'") + '`' }) .join(' ') : '(empty)' return { more: { title: 'Listing', lead: '`' + p + '` · ' + list.length + ' ' + (list.length === 1 ? 'entry' : 'entries'), body: desc === '(empty)' ? '' : shown.join('\n'), fence: desc !== '(empty)', footer: discordSessionUser(ctx) } } } catch (err) { return { text: 'ls failed: ' + ((err && err.message) || err), ephemeral: true } } } if (sub === 'stat') { const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat) if (typeof stfn !== 'function') return { text: 'stat unavailable', ephemeral: true } try { const st = await stfn.call(ctx.vfs, p) const fields = [] const kind = st.isDirectory ? 'directory' : st.isFile ? 'file' : st.isSymbolicLink ? 'symlink' : st.type || 'entry' fields.push(discordField('Type', String(kind), true)) if (st.size != null) fields.push(discordField('Size', discordPrettyBytes(st.size), true)) if (st.mode != null) fields.push(discordField('Mode', String(st.mode), true)) if (st.mtime || st.mtimeMs) { const ms = st.mtimeMs || Date.parse(st.mtime) fields.push( discordField('Modified', Number.isFinite(ms) ? new Date(ms).toISOString() : String(st.mtime), true) ) } if (st.path && st.path !== p) fields.push(discordField('Resolved', String(st.path), false)) return discordResult( discordEmbed({ title: 'Stat', desc: '`' + p + '`', fields: fields.slice(0, 12) }) ) } catch (err) { return { text: 'stat failed: ' + ((err && err.message) || err), ephemeral: true } } } const text = await discordCmdReadText(ctx, p) if (!text) { return discordResult(discordEmbed({ title: 'File', desc: '`' + p + '` is empty or unreadable.' })) } const writable = Boolean(discordCmdPathWriteOk(ctx, p)) const editRow = writable ? discordButtons([{ id: 'edit:open', label: 'Edit in modal', style: 1 }]) : null const extra = { components: editRow ? [editRow] : [], editPath: writable ? p : '' } const asJson = discordTryJson(text) if (asJson && typeof asJson === 'object' && !Array.isArray(asJson)) { return discordPrettySnapshot(p, asJson, extra) } if (sub === 'head') { const n = Math.max(1, Math.min(80, Number(nlines) || 12)) const head = text.split(/\r?\n/).slice(0, n).join('\n') return { more: { title: 'Head', lead: '`' + p + '` · first ' + n + ' lines', body: head, fence: true, components: extra.components, editPath: extra.editPath } } } return { more: { title: 'File', lead: '`' + p + '`', body: text.slice(0, BARE_OS_DISCORD_FS_MAX), fence: true, footer: text.length + ' bytes · ' + discordSessionUser(ctx), components: extra.components, editPath: extra.editPath } } } function discordFormatNetSummary(obj) { if (!obj || typeof obj !== 'object') { return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR } } const rq = obj.replicationQueue && typeof obj.replicationQueue === 'object' ? obj.replicationQueue : {} const st = obj.stagingSlot && typeof obj.stagingSlot === 'object' ? obj.stagingSlot : {} const fields = [] const peers = obj.peerCount const role = obj.seedRole || rq.role || st.role const proto = rq.protocol || st.protocol || obj.protocol if (peers != null) fields.push(discordField('Peers', String(peers), true)) if (role) fields.push(discordField('Role', String(role), true)) if (proto) fields.push(discordField('Protocol', '`' + String(proto) + '`', true)) if (obj.topicHex) { const hex = String(obj.topicHex) fields.push( discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true) ) } if (obj.replicationQueueDepth != null) { fields.push(discordField('Queue depth', String(obj.replicationQueueDepth), true)) } if (rq.queueDepthEstimate != null) { fields.push(discordField('Queue estimate', String(rq.queueDepthEstimate), true)) } if (rq.hypercoreLengthHint != null) { fields.push(discordField('Core length', String(rq.hypercoreLengthHint), true)) } if (rq.manifestPathCount != null) { fields.push(discordField('Manifests', String(rq.manifestPathCount), true)) } if (rq.localRamBlockCount != null) { fields.push(discordField('RAM blocks', String(rq.localRamBlockCount), true)) } if (st.activeSlot) fields.push(discordField('Active slot', String(st.activeSlot), true)) const fwA = obj.peerFirewallAcceptedTotal const fwR = obj.peerFirewallRejectedTotal const fwI = obj.peerFirewallInboundTotal const fwO = obj.peerFirewallOutboundTotal if (fwA != null || fwR != null || fwI != null || fwO != null) { fields.push( discordField( 'Firewall', 'accept ' + (fwA == null ? '0' : fwA) + ' · reject ' + (fwR == null ? '0' : fwR) + '\nin ' + (fwI == null ? '0' : fwI) + ' · out ' + (fwO == null ? '0' : fwO), true ) ) } if (obj.seedHandshakeError) { fields.push(discordField('Handshake', String(obj.seedHandshakeError), false)) } const peerN = Number(peers) const head = [] if (Number.isFinite(peerN)) head.push('**' + peerN + '** peer' + (peerN === 1 ? '' : 's')) if (role) head.push(String(role)) if (proto) head.push('`' + proto + '`') let desc = head.join(' · ') const note = rq.snapshotWorkflowNote || obj.note if (note) desc += (desc ? '\n' : '') + '*' + String(note).slice(0, 220) + '*' const at = rq.atMs || obj.atMs return { fields: fields, desc: desc, color: Number.isFinite(peerN) && peerN > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN, footer: at ? 'Updated ' + new Date(Number(at)).toISOString() + ' · expires after 2m idle' : '' } } function discordFormatSwarm(obj) { if (!obj || typeof obj !== 'object') return [] const fields = [] if (obj.peerCount != null) fields.push(discordField('Peers', String(obj.peerCount), true)) if (obj.protocol) fields.push(discordField('Protocol', '`' + String(obj.protocol) + '`', true)) if (obj.topicCount != null) fields.push(discordField('Topics', String(obj.topicCount), true)) if (obj.topicHex) { const hex = String(obj.topicHex) fields.push( discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true) ) } const lc = obj.lifecycle if (lc && typeof lc === 'object') { const bits = [] const lk = Object.keys(lc) for (let i = 0; i < lk.length && bits.length < 6; i++) { if (typeof lc[lk[i]] === 'object' || discordValueEmpty(lc[lk[i]])) continue bits.push('**' + discordHumanLabel(lk[i]) + '** ' + discordPrettyValue(lk[i], lc[lk[i]])) } if (bits.length) fields.push(discordField('Lifecycle', bits.join('\n'), false)) } const sc = obj.peerScoringAggregates if (sc && typeof sc === 'object') { const banned = sc.bannedActiveCount const hi = sc.highLatencyEwmaCount const low = sc.lowSuccessRateBucketCount if (banned != null || hi != null || low != null) { fields.push( discordField( 'Scoring', 'banned ' + String(banned || 0) + ' · high-lat ' + String(hi || 0) + ' · low-ok ' + String(low || 0), true ) ) } } if (Array.isArray(obj.peers) && obj.peers.length) { fields.push(discordField('Peer list', String(obj.peers.length), true)) } return fields } async function discordCmdHandleNet(ctx, sub) { if (sub === 'peers' || sub === 'swarm') { const t = (await discordCmdReadText(ctx, '/proc/bare_os/swarm')) || (await discordCmdReadText(ctx, '/proc/bare_os/swarm.json')) || (await discordCmdReadText(ctx, '/proc/bare_os_swarm')) const obj = discordTryJson(t) const fields = obj ? discordFormatSwarm(obj) : [] if (fields.length) { const n = obj && obj.peerCount return discordResult( discordEmbed({ title: 'Swarm', desc: n != null ? '**' + n + '** peer' + (Number(n) === 1 ? '' : 's') + (obj.protocol ? ' · `' + obj.protocol + '`' : '') : '', fields: fields, color: obj && obj.peerCount > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN }) ) } return discordPrettySnapshot('Swarm', t) } const t = (await discordCmdReadText(ctx, '/proc/bare_os/net_summary.json')) || (await discordCmdReadText(ctx, '/proc/net/dev')) const obj = discordTryJson(t) if (obj) { const fmt = discordFormatNetSummary(obj) if (fmt.fields.length) { return discordResult( discordEmbed({ title: 'Network', desc: fmt.desc, fields: fmt.fields, color: fmt.color, footer: fmt.footer || undefined }) ) } return discordPrettySnapshot('Network', obj) } if (t && t.indexOf('Inter-|') >= 0) { const lines = t.split(/\r?\n/).filter(Boolean) return discordResult( discordEmbed({ title: 'Network interfaces', desc: discordCmdFence(lines.slice(0, 16).join('\n')) }) ) } return discordPrettySnapshot('Network', t) } async function discordCmdHandleMan(ctx, page) { const name = String(page || '').replace(/[^a-zA-Z0-9._+-]/g, '') if (!name) return { text: 'man page name required — start typing for autocomplete', ephemeral: true } if (typeof ctx.execLine === 'function') { const out = await discordCmdCapture(ctx, function () { return ctx.execLine('man ' + name) }) if (out) { const clean = String(out) .replace(/\x1b\[[0-9;]*m/g, '') .trim() return { more: { title: 'man ' + name, body: clean, fence: false } } } } const t = await discordCmdReadText(ctx, '/share/man/man.json') if (!t) return { text: 'man database unavailable' } try { const db = JSON.parse(t) const pages = (db && db.pages) || [] for (let i = 0; i < pages.length; i++) { if (pages[i] && pages[i].name === name) { const p = pages[i] return { more: { title: 'man ' + (p.title || name), lead: p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`' : '', body: String(p.description || ''), fence: false } } } } } catch { /* fall through */ } return { text: 'no man page for ' + name, ephemeral: true } } function discordCmdSayBox(text) { const s = String(text || '').slice(0, 200) const lines = s.split(/\r?\n/).slice(0, 6) let w = 8 for (let i = 0; i < lines.length; i++) { if (lines[i].length > w) w = lines[i].length } if (w > 48) w = 48 const bar = '+' + Array(w + 3).join('-') + '+' const body = lines.map(function (ln) { const t = ln.slice(0, w) return '| ' + t + Array(w - t.length + 1).join(' ') + ' |' }) return [bar, body.join('\n'), bar, ' \\', ' cow-ish · bare-os'].join('\n') } function discordShHud(ctx, interaction, note) { const rec = discordShGet(interaction, ctx) const fields = [ discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true), discordField('Last exit', rec.lastCmd ? String(rec.lastExit) : '—', true), discordField('User', discordSessionUser(ctx), true) ] if (rec.lastCmd) { fields.push(discordField('Last command', '`' + rec.lastCmd + '`', false)) } const hist = rec.hist.slice(0, 6) if (hist.length) { fields.push( discordField( 'History', hist .map(function (h) { return '`' + h.cmd + '` · ' + h.exit }) .join('\n') ) ) } const histSel = discordSelect( 'sh:hist', 'Replay a command…', rec.hist.slice(0, 20).map(function (h, i) { return { label: h.cmd.slice(0, 90), value: String(i), description: 'exit ' + h.exit + (h.ms ? ' · ' + h.ms + 'ms' : '') } }) ) const acts = discordButtons([ { id: 'sh:again', label: 'Repeat', style: 1 }, { id: 'sh:up', label: 'cd ..' }, { id: 'sh:home', label: 'cd ~' }, { id: 'sh:pwd', label: 'pwd' } ]) return discordResult( discordEmbed({ title: 'Shell · ' + discordShPrompt(ctx, rec), desc: (note ? note + '\n' : '') + 'Type **`/r cmd:`** and keep going — `cd`, pipes, `&&`, redirects, and `$VAR` all work.\n' + 'Working directory sticks for this Discord user.', fields: fields, color: rec.lastCmd && rec.lastExit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK, footer: discordShPrompt(ctx, rec) + ' · /r cmd:' }), { components: [histSel, acts].filter(Boolean) } ) } async function discordCmdHandleRun(ctx, interaction, raw) { const rec = discordShGet(interaction, ctx) const cmd = String(raw || '').trim() if (!cmd) return discordShHud(ctx, interaction, '') if (cmd.length > 4000) { return { text: 'Command line is too long (max 4000).', ephemeral: true } } const first = discordShFirstWord(cmd) if (first === 'exit') { rec.lastCmd = cmd rec.lastExit = 0 return discordShHud(ctx, interaction, 'Stayed attached — `exit` does not stop the Discord bot.') } if (first === 'logout' || first === 'login') { return { text: '`login` / `logout` need the guest TTY. This Discord shell stays as **' + discordSessionUser(ctx) + '**.', ephemeral: true } } if (typeof ctx.execLine !== 'function') { return { text: 'execLine unavailable in this session', ephemeral: true } } await discordShPrepare(ctx, rec) const t0 = Date.now() let out = '' let errNote = '' try { out = await discordCmdCapture(ctx, function () { return ctx.execLine(cmd, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS }) }) } catch (err) { errNote = String((err && err.message) || err) out = (out ? out + '\n' : '') + errNote } const ms = Date.now() - t0 const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0 discordShHarvest(ctx, rec) discordShPushHist(rec, cmd, exit, ms, out) const tty = BARE_OS_DISCORD_SH_TTY[first] if (!String(out || '').trim() && (first === 'cd' || first === 'export' || first === 'unset' || first === 'alias')) { return discordShHud(ctx, interaction, '`$ ' + cmd + '` · exit **' + exit + '** · ' + ms + 'ms') } const parsed = discordTryJson(out) if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && String(out).length < 800) { return discordPrettySnapshot(discordShPrompt(ctx, rec) + ' $ ' + cmd, parsed, { color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK }) } const lead = '`' + discordShPrompt(ctx, rec) + '`\n`$ ' + cmd + '`' + (tty ? '\n_Interactive TUI — needs a real terminal; output may be empty._' : '') if (!String(out || '').trim()) { return discordResult( discordEmbed({ title: 'Shell', desc: lead + '\n(no output)', color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK, footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd) }), { components: [ discordButtons([ { id: 'sh:again', label: 'Repeat', style: 1 }, { id: 'sh:hud', label: 'Shell' } ]) ] } ) } return { more: { title: 'Shell', lead: lead, body: discordCmdRedact(out), fence: true, color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK, footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd), components: [ discordButtons([ { id: 'sh:again', label: 'Repeat', style: 1 }, { id: 'sh:up', label: 'cd ..' }, { id: 'sh:home', label: 'cd ~' }, { id: 'sh:hud', label: 'Shell' } ]) ] } } } async function discordShAction(ctx, interaction, id) { const rec = discordShGet(interaction, ctx) if (id === 'sh:hud' || id === 'run:compose') { return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, '')) } if (id === 'sh:pwd') { return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'pwd')) } if (id === 'sh:again') { if (!rec.lastCmd) { return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, 'No command to repeat.')) } return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, rec.lastCmd)) } if (id === 'sh:up') { return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd ..')) } if (id === 'sh:home') { return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd')) } if (id === 'sh:hist') { const i = Number((interaction.values && interaction.values[0]) || 0) const hit = rec.hist[i] if (!hit) return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, '')) return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, hit.cmd)) } return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, '')) } async function discordCmdHandleUpload(ctx, interaction, destRaw) { const att = discordInteractionAttachment(interaction, 'file') const url = att && (att.url || att.proxyURL || att.proxy_url) if (!att || !url) { return { text: 'Attach a file to `/upload`. Discord hosts it, then Bare OS `wget`s it into the VFS.', ephemeral: true } } const filename = discordUploadSafeName(att.name || att.filename || 'upload.bin') const dest = await discordUploadResolveDest(ctx, interaction, destRaw, filename) const allowed = discordCmdPathWriteOk(ctx, dest) if (!allowed) { return { text: 'path not writable (stay under `~/` or `/tmp`; no `..`, no `~/.discord`)', ephemeral: true } } if (!/^https:\/\//i.test(String(url))) { return { text: 'Attachment URL is not https.', ephemeral: true } } const rec = discordShGet(interaction, ctx) await discordShPrepare(ctx, rec) const line = 'wget -T 60 -O ' + discordShQuote(allowed) + ' ' + discordShQuote(url) if (typeof ctx.execLine !== 'function' && typeof ctx.bareOsRunWgetCli !== 'function') { return { text: 'wget unavailable in this session (`execLine` / `bareOsRunWgetCli`)', ephemeral: true } } const t0 = Date.now() let out = '' let errNote = '' try { out = await discordCmdCapture(ctx, function () { if (typeof ctx.execLine === 'function') { return ctx.execLine(line, { timeoutMs: BARE_OS_DISCORD_UPLOAD_MS }) } return ctx.bareOsRunWgetCli(['wget', '-T', '60', '-O', allowed, String(url)]) }) } catch (err) { errNote = String((err && err.message) || err) out = (out ? out + '\n' : '') + errNote } const ms = Date.now() - t0 const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0 discordShHarvest(ctx, rec) discordShPushHist(rec, line, exit, ms, out) let size = att.size try { const st = await discordFmStat(ctx, allowed) if (st && st.size != null) size = st.size } catch { /* keep attachment size */ } const fields = [ discordField('Saved as', '`' + allowed + '`', false), discordField('Name', filename, true), discordField('Size', size != null ? discordPrettyBytes(size) : '—', true), discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true) ] if (att.contentType || att.content_type) { fields.push(discordField('Type', String(att.contentType || att.content_type), true)) } const note = String(out || '').trim() const desc = '`$ wget -O ' + allowed + ' `\n' + (exit ? 'wget failed. If `BARE_OS_HTTP_ALLOWLIST` is set, add `cdn.discordapp.com` and `media.discordapp.net`.\n' : 'Downloaded from the Discord CDN via `/bin/wget`.\n') + (note ? discordCmdFence(note, '', 1800) : '') return discordResult( discordEmbed({ title: exit ? 'Upload failed' : 'Uploaded', desc: desc, fields: fields, color: exit ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR_OK, footer: 'exit ' + exit + ' · ' + ms + 'ms · wget' }) ) } 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) { return discordInteractionUserId(interaction) || 'anon' } function discordHdmsGet(interaction) { const k = discordHdmsKey(interaction) let rec = BARE_OS_DISCORD_HDMS_SESSIONS[k] if (!rec) { rec = { sel: '', confirm: '', atMs: Date.now() } BARE_OS_DISCORD_HDMS_SESSIONS[k] = rec } rec.atMs = Date.now() return rec } function discordHdmsUnlocked(ctx) { if (ctx && ctx.identity && ctx.identity.state === 'unlocked') return true const e = discordCmdEnv(ctx) const id = String(e.BARE_OS_IDENTITY || '') return Boolean(id && id !== 'guest') } function discordHdmsLabelOk(label) { return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(String(label || '')) } function discordHdmsRedact(text) { return discordCmdRedact(String(text == null ? '' : text)) .replace(/("writerSecretHex"\s*:\s*")[^"]*"/gi, '$1[redacted]"') .replace(/("secretKey"\s*:\s*")[^"]*"/gi, '$1[redacted]"') } function discordParseHdmsList(text) { const rows = [] const lines = String(text || '').split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const line = lines[i].trim() if (!line || line.indexOf('(no extra drives)') >= 0) continue if (/^hdms:/i.test(line) || /log in/i.test(line) || /unavailable/i.test(line)) continue const parts = line.split('\t') const label = String(parts[0] || '').trim() if (!label || !discordHdmsLabelOk(label)) continue rows.push({ label: label, mode: String(parts[1] || '').trim() || '—', key: String(parts[2] || '').trim(), extra: String(parts[3] || '').trim() }) } return rows } async function discordHdmsCli(ctx, argv) { if (typeof ctx.runHdms !== 'function') { return { text: '', error: 'HDMS unavailable (`ctx.runHdms` missing). Log in on the guest TTY.' } } let out = '' try { out = await discordCmdCapture(ctx, function () { return ctx.runHdms(argv) }) } catch (err) { return { text: String((err && err.message) || err), error: String((err && err.message) || err) } } const fail = ctx.exitCode && Number(ctx.exitCode) !== 0 return { text: String(out || ''), error: fail ? String(out || 'hdms failed').trim() : '' } } async function discordHdmsListRows(ctx) { const got = await discordHdmsCli(ctx, ['hdms', 'list']) if (got.error && !got.text) return { rows: [], error: got.error } return { rows: discordParseHdmsList(got.text), error: got.error, raw: got.text } } async function discordSuggestHdmsLabels(ctx, q) { const got = await discordHdmsListRows(ctx) const names = got.rows.map(function (r) { return r.label }) return discordFilterChoices(names, q) } async function discordDefer(interaction) { if (!interaction || interaction.deferred || interaction.replied) return false if (typeof interaction.deferReply !== 'function') return false try { await interaction.deferReply() return true } catch { return false } } function discordHdmsUnavailable(ctx, note) { const e = discordCmdEnv(ctx) return discordResult( discordEmbed({ title: 'HDMS', color: BARE_OS_DISCORD_COLOR_WARN, desc: (note || 'Hyperdrive management needs an **unlocked** session and `ctx.runHdms`.') + '\nIdentity: **' + (e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || 'guest') + '**. Use the guest TTY: `login`.', fields: [ discordField('Health', '`/proc/bare_os/hdms_health.json`', false), discordField('Hints', '`/proc/bare_os/hdms_hints.json`', false) ] }) ) } async function discordHdmsHealthView(ctx) { const t = (await discordCmdReadText(ctx, '/proc/bare_os/hdms_health.json')) || (await discordCmdReadText(ctx, '/proc/bare_os_hdms_health.json')) const obj = discordTryJson(t) if (obj && typeof obj === 'object') { const labels = Array.isArray(obj.labels) ? obj.labels : [] const fields = [ discordField('Active', obj.active ? 'yes' : 'no', true), discordField('Mounts', String(obj.mountCount != null ? obj.mountCount : labels.length), true), discordField('Identity', discordHdmsUnlocked(ctx) ? 'unlocked' : 'guest', true) ] for (let i = 0; i < labels.length && fields.length < 20; i++) { const L = labels[i] && typeof labels[i] === 'object' ? labels[i] : { label: labels[i] } fields.push(discordField(String(L.label || 'drive'), String(L.mode || 'mounted'), true)) } const th = obj.thresholdHints if (th && typeof th === 'object') { if (th.diskPressureWarnPercent != null) { fields.push(discordField('Disk warn', String(th.diskPressureWarnPercent) + '%', true)) } if (th.pairingBackoffMs != null) { fields.push(discordField('Pair backoff', String(th.pairingBackoffMs) + 'ms', true)) } } return discordResult( discordEmbed({ title: 'HDMS health', desc: obj.note || 'Registry metadata only — no keys.', fields: fields, color: obj.active ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN }) ) } return discordPrettySnapshot('HDMS health', t) } async function discordHdmsHintsView(ctx) { const t = (await discordCmdReadText(ctx, '/proc/bare_os/hdms_hints.json')) || (await discordCmdReadText(ctx, '/proc/bare_os_hdms_hints.json')) const obj = discordTryJson(t) if (obj && typeof obj === 'object') { const fields = [] const keys = Object.keys(obj) for (let i = 0; i < keys.length && fields.length < 20; i++) { const k = keys[i] if (/secret|token|invite|key/i.test(k) && k !== 'schema') { fields.push(discordField(k, '[redacted]', false)) continue } let v = obj[k] if (v && typeof v === 'object') v = JSON.stringify(v) fields.push(discordField(k, String(v == null ? '—' : v).slice(0, 200), false)) } return discordResult( discordEmbed({ title: 'HDMS hints', desc: 'Operator pairing hints. The guest does **not** open invite URLs by itself.', fields: fields.length ? fields : [discordField('hints', 'None')] }) ) } return discordPrettySnapshot('HDMS hints', t) } async function discordHdmsHud(ctx, interaction, note) { if (!discordHdmsUnlocked(ctx) || typeof ctx.runHdms !== 'function') { return discordHdmsUnavailable(ctx) } const rec = discordHdmsGet(interaction) const listed = await discordHdmsListRows(ctx) const rows = listed.rows const labels = {} for (let i = 0; i < rows.length; i++) labels[rows[i].label] = 1 if (rec.sel && !labels[rec.sel]) rec.sel = '' if (rec.confirm && !labels[rec.confirm]) rec.confirm = '' const fields = [] for (let i = 0; i < rows.length && fields.length < 20; i++) { const d = rows[i] const mark = rec.sel === d.label ? '▸ ' : '' const bits = [d.mode] if (d.extra) bits.push(d.extra) if (d.key && d.key !== '(local)') bits.push('`' + d.key.slice(0, 18) + '…`') else if (d.key) bits.push(d.key) fields.push(discordField(mark + d.label, bits.join(' · ') || 'mounted', false)) } const sel = discordSelect( 'hdms:pick', rows.length ? 'Select a drive…' : 'No extra drives', rows.map(function (d) { return { label: d.label, value: d.label, description: (d.mode + (d.extra ? ' · ' + d.extra : '')).slice(0, 100) } }) ) const top = discordButtons([ { id: 'hdms:refresh', label: 'Refresh', style: 1 }, { id: 'hdms:create', label: 'Create' }, { id: 'hdms:add', label: 'Add RO' }, { id: 'hdms:pair', label: 'Pair' }, { id: 'hdms:health', label: 'Health' } ]) const acts = rec.sel ? rec.confirm === rec.sel ? discordButtons([ { id: 'hdms:ok', label: 'Confirm remove', style: 4 }, { id: 'hdms:no', label: 'Cancel' } ]) : discordButtons([ { id: 'hdms:show', label: 'Show', style: 1 }, { id: 'hdms:browse', label: 'Browse /mnt' }, { id: 'hdms:invite', label: 'Invite' }, { id: 'hdms:inviterw', label: 'Invite RW', style: 3 }, { id: 'hdms:rm', label: 'Remove', style: 4 } ]) : discordButtons([ { id: 'hdms:invite0', label: 'Invite (no drive)' }, { id: 'hdms:hints', label: 'Hints' } ]) let desc = 'Extra Hyperdrives under `/mnt/