/** * 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_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_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', '/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 discordCmdClip(text, max) { const s = String(text == null ? '' : 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 String(text == null ? '' : 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 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 ? String(o.title).slice(0, L.title) : '' const footer = String(o.footer || 'Bare OS · expires after 2m idle').slice(0, L.footer) const authorName = o.author && o.author.name ? 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.text) payload.content = discordCmdClip(result.text) if (!payload.content && !payload.embeds) 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 = 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 } ] } } function discordNavRows() { const rows = [] const a = discordButtons([ { id: 'nav:panel', label: 'Panel', style: 1 }, { id: 'bare:status', label: 'Status', style: 1 }, { id: 'bare:whoami', label: 'Whoami' }, { id: 'bare:help', label: 'Help' }, { id: 'sys:doctor', label: 'Doctor' } ]) const b = discordButtons([ { id: 'svc:list', label: 'Services' }, { id: 'sys:ps', label: 'Processes' }, { id: 'net:summary', label: 'Network' }, { id: 'say:compose', label: 'Say…', style: 3 }, { id: 'run:compose', label: 'Shell' } ]) const c = discordButtons([ { 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' } ]) if (a) rows.push(a) if (b) rows.push(b) if (c) rows.push(c) return rows } function discordResult(embed, extra) { const out = { embeds: [embed] } const extraRows = extra && extra.components ? extra.components : [] const nav = extra && extra.nav === false ? [] : discordNavRows() const rows = discordUniqueComponents(extraRows.concat(nav)) 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 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 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 } } async function discordCmdCapture(ctx, fn) { const lines = [] const cons = ctx.console || {} const ol = cons.log const oe = cons.error cons.log = function (s) { lines.push(String(s)) } cons.error = function (s) { lines.push(String(s)) } try { await fn() } finally { cons.log = ol cons.error = oe } return lines.join('\n') } 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 discordWhitelistRaw(ctx) { const e = discordCmdEnv(ctx) return e.DISCORD_ID_WHITELIST || e.BARE_OS_DISCORD_ID_WHITELIST || '' } function discordUserAllowed(ctx, userId) { const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx)) let n = 0 for (const k in ids) { if (Object.prototype.hasOwnProperty.call(ids, k)) n++ } if (n === 0) return true const id = String(userId == null ? '' : userId).trim() return Boolean(id && ids[id]) } 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 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 + '**. Use the buttons below, or slash commands.', 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', 'Browse and edit files under `~/` and `/tmp`'), discordField('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'), discordField('/run /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'), discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins') ] }) ) } 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 **`/run 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) + ' · /run 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 discordCmdHandleJournal(ctx, unit) { if (typeof ctx.bareOsRunSystemctlCli === 'function' && unit) { const out = await discordCmdCapture(ctx, function () { return ctx.bareOsRunSystemctlCli([ 'journalctl', '-u', String(unit), '--lines', '30' ]) }) const body = discordCmdRedact(String(out || '')) if (!body.trim()) { return discordResult( discordEmbed({ title: 'Journal · ' + unit, desc: 'Empty journal for `' + unit + '`.' }) ) } return { more: { title: 'Journal · ' + unit, body: body, fence: true } } } const t = (await discordCmdReadText(ctx, '/var/log/bare-os/discord.log')) || (await discordCmdReadText(ctx, '/var/log/bare-os/kernel-console.log')) || (await discordCmdReadText(ctx, '/var/log/messages')) const body = discordCmdRedact(String(t || '')) if (!body.trim()) { return discordResult(discordEmbed({ title: 'Journal', desc: 'No journal in `/var/log/bare-os`.' })) } return { more: { title: 'Journal', body: body, fence: true } } } function discordPanel(ctx) { const user = discordSessionUser(ctx) const e = discordCmdEnv(ctx) return discordResult( discordEmbed({ title: 'Bare OS control panel', color: BARE_OS_DISCORD_COLOR, desc: 'Signed in as **' + user + '** · `' + (e.HOME || '/home/' + user) + '`\nPick a button, or use a slash command.', fields: [ discordField('User', user, true), discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true), discordField('Host', e.HOSTNAME || 'bare-os', true) ] }) ) } function discordCmdOpt(s, name, desc, required, autocomplete) { if (!s || typeof s.addStringOption !== 'function') return s return s.addStringOption(function (o) { o.setName(name).setDescription(desc) if (required && typeof o.setRequired === 'function') o.setRequired(true) if (autocomplete && typeof o.setAutocomplete === 'function') o.setAutocomplete(true) return o }) } function discordCmdAddSubs(builder, items) { if (!builder || typeof builder.addSubcommand !== 'function') return false for (let i = 0; i < items.length; i++) { const it = items[i] builder.addSubcommand(function (s) { s.setName(it[0]).setDescription(it[1]) if (it[2]) it[2](s) return s }) } return true } var BARE_OS_DISCORD_PLUGIN_DIR = '~/.discord/plugins' var BARE_OS_DISCORD_PLUGIN_MAX = 40 var BARE_OS_DISCORD_PLUGIN_SRC_MAX = 64 * 1024 var BARE_OS_DISCORD_PLUGIN_RESERVED = { bare: 1, sys: 1, svc: 1, fs: 1, net: 1, man: 1, say: 1, run: 1, journal: 1, edit: 1, create: 1, files: 1, browse: 1, settings: 1, panel: 1, ping: 1, plugins: 1 } var BARE_OS_DISCORD_PLUGINS = [] var BARE_OS_DISCORD_PLUGINS_READY = false var BARE_OS_DISCORD_PLUGIN_REREGISTER = null function discordPluginNameOk(name) { return /^[a-z0-9][a-z0-9-]{0,31}$/.test(String(name || '')) } function discordPluginsSetRegistrar(fn) { BARE_OS_DISCORD_PLUGIN_REREGISTER = typeof fn === 'function' ? fn : null } function discordPluginFind(name) { const n = String(name || '') for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) { if (BARE_OS_DISCORD_PLUGINS[i].name === n && BARE_OS_DISCORD_PLUGINS[i].ok) { return BARE_OS_DISCORD_PLUGINS[i] } } return null } function discordPluginExpand(tpl, opt, env) { return String(tpl == null ? '' : tpl).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, function (_, a, def, b) { const key = a || b if (opt) { const v = opt(key) if (v != null && v !== '') return String(v) } if (env && env[key] != null && env[key] !== '') return String(env[key]) return def != null ? String(def) : '' }) } function discordPluginSdk(ctx, plug) { const prefix = 'plug:' + plug.name + ':' const bot = { name: plug.name, version: plug.manifest && plug.manifest.version, command: function (spec) { if (spec && typeof spec === 'object') plug.handlers.push(spec) return bot }, embed: function (opts) { return discordEmbed(opts) }, field: discordField, result: function (embed, extra) { return discordResult(embed, extra) }, fence: discordCmdFence, redact: discordCmdRedact, more: function (opts) { return { more: opts } }, id: function (rest) { return prefix + String(rest || '').replace(/^plug:[^:]+:/, '') }, user: function () { return discordSessionUser(ctx) }, home: function () { return discordCmdEnv(ctx).HOME || '/home/' + discordSessionUser(ctx) }, env: function () { const e = discordCmdEnv(ctx) const out = {} const skip = /TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE|CREDENTIAL/i const keys = Object.keys(e) for (let i = 0; i < keys.length; i++) { if (skip.test(keys[i])) continue out[keys[i]] = e[keys[i]] } return out }, read: function (path) { return discordCmdReadText(ctx, path) }, write: function (path, text) { const p = discordCmdPathWriteOk(ctx, path) if (!p) return Promise.reject(new Error('not writable: ' + path)) if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { return Promise.reject(new Error('writeFile unavailable')) } return ctx.vfs.writeFile(p, discordTextToBuf(ctx, String(text == null ? '' : text))) }, ls: async function (path) { if (!ctx.vfs || typeof ctx.vfs.readdir !== 'function') return [] const names = await ctx.vfs.readdir(path || '~') return Array.isArray(names) ? names : [] }, sh: function (line) { if (typeof ctx.execLine !== 'function') { return Promise.reject(new Error('execLine unavailable')) } return discordCmdCapture(ctx, function () { return ctx.execLine(String(line || ''), { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS }) }) } } return bot } function discordPluginNormalize(raw, file) { const m = raw && typeof raw === 'object' ? raw : {} const name = String(m.name || '').toLowerCase() return { name: name, description: String(m.description || name || 'plugin'), version: String(m.version || '1.0.0'), enabled: m.enabled !== false, options: Array.isArray(m.options) ? m.options : [], subcommands: Array.isArray(m.subcommands) ? m.subcommands : [], run: typeof m.run === 'string' ? m.run : m.shell || '', title: m.title || '', readFile: m.file || '', embed: m.embed && typeof m.embed === 'object' ? m.embed : null, ephemeral: !!m.ephemeral, source: file || '', handlers: [], onComponent: typeof m.onComponent === 'function' ? m.onComponent : null, onModal: typeof m.onModal === 'function' ? m.onModal : null, onAutocomplete: typeof m.onAutocomplete === 'function' ? m.onAutocomplete : null, runFn: typeof m.run === 'function' ? m.run : null, manifest: m, ok: false, error: '', kind: 'json' } } async function discordPluginEvalJs(ctx, plug, source) { const bot = discordPluginSdk(ctx, plug) let AsyncFn try { AsyncFn = Object.getPrototypeOf(async function () {}).constructor } catch { AsyncFn = Function } const body = String(source) + '\n;' + 'if (typeof register === "function") await register(bot);\n' + 'else if (typeof plugin === "object" && plugin) bot.command(plugin);\n' const fn = new AsyncFn('bot', 'sdk', 'register', body) function register(spec) { bot.command(spec) } await fn(bot, bot, register) if (plug.handlers.length) { const first = plug.handlers[0] if (!plug.name && first.name) plug.name = String(first.name).toLowerCase() if (first.description && plug.description === plug.name) plug.description = first.description if (first.options) plug.options = first.options if (first.subcommands) plug.subcommands = first.subcommands if (typeof first.run === 'function') plug.runFn = first.run if (typeof first.onComponent === 'function') plug.onComponent = first.onComponent if (typeof first.onModal === 'function') plug.onModal = first.onModal if (typeof first.onAutocomplete === 'function') plug.onAutocomplete = first.onAutocomplete } plug.kind = 'js' } async function discordPluginsDisabledSet(ctx) { const text = await discordCmdReadText(ctx, BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt') const set = Object.create(null) const lines = String(text || '').split(/\r?\n/) for (let i = 0; i < lines.length; i++) { const n = lines[i].trim() if (!n || n.charAt(0) === '#') continue set[n] = 1 } return set } async function discordPluginsWriteDisabled(ctx, set) { const names = Object.keys(set).sort() const body = names.length ? names.join('\n') + '\n' : '' if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') throw new Error('writeFile unavailable') await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR) await ctx.vfs.writeFile(BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt', discordTextToBuf(ctx, body)) } async function discordPluginsLoad(ctx) { BARE_OS_DISCORD_PLUGINS = [] BARE_OS_DISCORD_PLUGINS_READY = true if (!ctx || !ctx.vfs) return BARE_OS_DISCORD_PLUGINS await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR) let names = [] try { if (typeof ctx.vfs.readdir === 'function') { names = await ctx.vfs.readdir(BARE_OS_DISCORD_PLUGIN_DIR) } } catch { names = [] } if (!Array.isArray(names)) names = [] const disabled = await discordPluginsDisabledSet(ctx) const seen = Object.create(null) for (let i = 0; i < names.length && BARE_OS_DISCORD_PLUGINS.length < BARE_OS_DISCORD_PLUGIN_MAX; i++) { const entry = String(names[i] || '') if (!entry || entry.charAt(0) === '.' || entry === 'disabled.txt' || entry === 'README.md') continue if (/\.disabled$/.test(entry)) continue let id = entry.replace(/\.(json|js)$/i, '') let kind = '' let srcPath = '' let jsonPath = '' if (/\.json$/i.test(entry)) { kind = 'json' jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry } else if (/\.js$/i.test(entry)) { kind = 'js' srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry } else { jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/plugin.json' srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/index.js' const hasJson = await discordFileExists(ctx, jsonPath) const hasJs = await discordFileExists(ctx, srcPath) if (hasJson) kind = 'dir-json' else if (hasJs) kind = 'dir-js' else continue if (!hasJson) jsonPath = '' if (!hasJs) srcPath = '' } if (seen[id]) continue seen[id] = 1 const plug = discordPluginNormalize({ name: id }, entry) plug.source = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry if (disabled[id]) { plug.enabled = false plug.error = 'disabled' BARE_OS_DISCORD_PLUGINS.push(plug) continue } try { if (jsonPath) { const raw = await discordCmdReadText(ctx, jsonPath) if (raw.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('manifest too large') const j = discordTryJson(raw) if (!j) throw new Error('invalid JSON') const n = discordPluginNormalize(j, entry) plug.name = n.name || id plug.description = n.description plug.version = n.version plug.enabled = n.enabled plug.options = n.options plug.subcommands = n.subcommands plug.run = n.run plug.title = n.title plug.readFile = n.readFile plug.embed = n.embed plug.ephemeral = n.ephemeral plug.manifest = j } if (srcPath) { const src = await discordCmdReadText(ctx, srcPath) if (src.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('script too large') await discordPluginEvalJs(ctx, plug, src) } if (!discordPluginNameOk(plug.name)) throw new Error('bad command name (use a-z0-9-, 1–32)') if (BARE_OS_DISCORD_PLUGIN_RESERVED[plug.name]) throw new Error('name is reserved: /' + plug.name) if (plug.enabled === false) { plug.error = 'disabled in manifest' BARE_OS_DISCORD_PLUGINS.push(plug) continue } plug.ok = true BARE_OS_DISCORD_PLUGINS.push(plug) } catch (err) { plug.ok = false plug.error = String((err && err.message) || err) BARE_OS_DISCORD_PLUGINS.push(plug) } } return BARE_OS_DISCORD_PLUGINS } async function discordPluginsEnsure(ctx) { if (!BARE_OS_DISCORD_PLUGINS_READY) await discordPluginsLoad(ctx) return BARE_OS_DISCORD_PLUGINS } function discordPluginAddOptions(builder, opts) { if (!opts || !opts.length) return for (let i = 0; i < opts.length && i < 10; i++) { const o = opts[i] if (!o || !o.name) continue discordCmdOpt( builder, String(o.name).slice(0, 32), String(o.description || o.name).slice(0, 100), !!o.required, !!o.autocomplete ) } } function discordPluginsSlash(dj) { const B = dj && dj.SlashCommandBuilder if (typeof B !== 'function') return [] const out = [] for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) { const p = BARE_OS_DISCORD_PLUGINS[i] if (!p.ok) continue try { const c = new B().setName(p.name).setDescription(String(p.description || p.name).slice(0, 100)) if (p.subcommands && p.subcommands.length) { const items = [] for (let s = 0; s < p.subcommands.length && s < 10; s++) { const sc = p.subcommands[s] if (!sc || !sc.name) continue items.push([ String(sc.name).slice(0, 32), String(sc.description || sc.name).slice(0, 100), sc.options ? function (b) { discordPluginAddOptions(b, sc.options) } : null ]) } discordCmdAddSubs(c, items) } else { discordPluginAddOptions(c, p.options) } out.push(c.toJSON()) } catch { /* skip bad slash shape */ } } return out } function discordPluginEvent(ctx, interaction, plug, sub, opt) { const bot = discordPluginSdk(ctx, plug) return { name: plug.name, sub: sub || '', opt: opt || function () { return '' }, interaction: interaction, userId: discordInteractionUserId(interaction), user: discordSessionUser(ctx), ctx: ctx, bot: bot, embed: bot.embed, field: bot.field, fence: bot.fence, sh: bot.sh, read: bot.read, write: bot.write, ls: bot.ls } } async function discordPluginInvoke(ctx, interaction, plug, sub, opt) { const event = discordPluginEvent(ctx, interaction, plug, sub, opt) let spec = plug if (sub && plug.subcommands) { for (let i = 0; i < plug.subcommands.length; i++) { if (plug.subcommands[i] && plug.subcommands[i].name === sub) { spec = plug.subcommands[i] break } } } const runFn = spec.runFn || plug.runFn || (typeof spec.run === 'function' ? spec.run : null) if (typeof runFn === 'function') { const out = await runFn(event) if (out == null) return { text: '(no output)', ephemeral: true } if (typeof out === 'string') return { text: discordCmdRedact(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) } if (out.embeds || out.text || out.more || out.modal || out.components) { if ((spec.ephemeral || plug.ephemeral) && out.ephemeral == null) out.ephemeral = true return out } if (out.title || out.desc || out.fields) { return discordResult(discordEmbed(out), { ephemeral: !!(spec.ephemeral || plug.ephemeral) }) } return { text: String(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) } } if (spec.readFile || spec.file || (plug.embed && plug.embed.file)) { const path = spec.readFile || spec.file || plug.embed.file const body = await discordCmdReadText(ctx, path) return { more: { title: spec.title || plug.title || plug.name, body: body || '(empty)', fence: true } } } const shell = typeof spec.run === 'string' && spec.run ? spec.run : plug.run if (shell) { const line = discordPluginExpand(shell, opt, discordCmdEnv(ctx)) const out = await discordCmdCapture(ctx, function () { if (typeof ctx.execLine !== 'function') throw new Error('execLine unavailable') return ctx.execLine(line, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS }) }) return { more: { title: spec.title || plug.title || '/' + plug.name, lead: '`' + line + '`', body: out || '(no output)', fence: true, footer: 'plugin · ' + plug.name + ' · ' + discordSessionUser(ctx) } } } if (spec.embed || plug.embed) { const em = spec.embed || plug.embed return discordResult( discordEmbed({ title: em.title || plug.title || plug.name, desc: em.desc || em.description || '', fields: em.fields }), { ephemeral: !!(spec.ephemeral || plug.ephemeral) } ) } return { text: 'Plugin /' + plug.name + ' has nothing to run.', ephemeral: true } } async function discordPluginComponent(ctx, interaction, id) { const parts = String(id || '').split(':') const name = parts[1] || '' const rest = parts.slice(2).join(':') const plug = discordPluginFind(name) if (!plug || typeof plug.onComponent !== 'function') { return discordSendResult(ctx, interaction, { text: 'Plugin button expired or unknown.', ephemeral: true }) } const event = discordPluginEvent(ctx, interaction, plug, '', function () { return '' }) event.id = rest event.values = interaction.values || [] const out = await plug.onComponent(event) if (out == null) return discordSendResult(ctx, interaction, { text: 'ok', ephemeral: true }) return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out) } async function discordPluginsAdmin(ctx, interaction, sub, opt) { await discordPluginsEnsure(ctx) const action = sub || 'list' if (action === 'reload') { await discordPluginsLoad(ctx) let note = 'Reloaded **' + BARE_OS_DISCORD_PLUGINS.length + '** plugin file(s).' if (typeof BARE_OS_DISCORD_PLUGIN_REREGISTER === 'function') { try { await BARE_OS_DISCORD_PLUGIN_REREGISTER() note += ' Slash commands re-registered.' } catch (err) { note += ' Reload dispatch ok; slash re-register failed: ' + ((err && err.message) || err) } } else { note += ' Restart `discord-bot` (or the initd unit) to publish new slash names to Discord.' } return discordPluginsAdmin(ctx, interaction, 'list', opt).then(function (res) { if (res && res.embeds && res.embeds[0]) { res.embeds[0].description = note + (res.embeds[0].description ? '\n' + res.embeds[0].description : '') } return res }) } if (action === 'disable' || action === 'enable') { const want = String(opt('name') || '').toLowerCase() if (!want) return { text: 'plugin name required', ephemeral: true } const set = await discordPluginsDisabledSet(ctx) if (action === 'disable') set[want] = 1 else delete set[want] await discordPluginsWriteDisabled(ctx, set) await discordPluginsLoad(ctx) return { text: (action === 'disable' ? 'Disabled' : 'Enabled') + ' `' + want + '`. Restart or `/plugins reload` to refresh slash names.', ephemeral: true } } if (action === 'info') { const want = String(opt('name') || '').toLowerCase() let plug = null for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) { if (BARE_OS_DISCORD_PLUGINS[i].name === want) plug = BARE_OS_DISCORD_PLUGINS[i] } if (!plug) return { text: 'No plugin named `' + want + '`.', ephemeral: true } return discordResult( discordEmbed({ title: '/' + plug.name, desc: plug.description, fields: [ discordField('Status', plug.ok ? 'loaded' : plug.error || 'error', true), discordField('Kind', plug.kind, true), discordField('Version', plug.version, true), discordField('File', plug.source || plug.file || '—', false) ], color: plug.ok ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN }) ) } const fields = [] for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length && fields.length < 20; i++) { const p = BARE_OS_DISCORD_PLUGINS[i] fields.push( discordField( '/' + p.name, (p.ok ? '**on**' : '**off**') + ' · ' + (p.error || p.description || p.kind) ) ) } return discordResult( discordEmbed({ title: 'Plugins', desc: 'Drop JSON or JS in `~/.discord/plugins`. See **developer-guide ch.21** for the SDK.\n' + BARE_OS_DISCORD_PLUGINS.length + ' discovered.', fields: fields.length ? fields : [discordField('plugins', 'None yet. Add `~/.discord/plugins/hello.json`.')] }) ) } function discordBuildSlashCommands(dj, ctx) { const B = dj && dj.SlashCommandBuilder if (typeof B !== 'function') return [] const finish = function () { const B2 = dj.SlashCommandBuilder const bare = new B2().setName('bare').setDescription('Bare OS session (logged-in user)') const sys = new B2().setName('sys').setDescription('Bare OS system snapshots') const svc = new B2().setName('svc').setDescription('systemctl units') const fsCmd = new B2().setName('fs').setDescription('Read-only VFS') const net = new B2().setName('net').setDescription('Swarm / network') const man = new B2().setName('man').setDescription('Look up a man page') const say = new B2().setName('say').setDescription('Speak as Bare OS (or open a compose form)') const run = new B2().setName('run').setDescription('Non-interactive Bare OS shell (cwd, pipes, cd, history)') const journal = new B2() .setName('journal') .setDescription('Tail a unit or system log') const edit = new B2() .setName('edit') .setDescription('Edit a text file in a Discord modal (home or /tmp)') const create = new B2() .setName('create') .setDescription('Create a new text file (pick a path, then enter contents)') const files = new B2() .setName('files') .setDescription('Browse and manage files (list, open, mkdir, rename, delete)') const settings = new B2() .setName('settings') .setDescription('Live session settings (theme, shell, discord, agent, aliases)') const plugins = new B2() .setName('plugins') .setDescription('List, reload, and manage ~/.discord/plugins') const panel = new B2() .setName('panel') .setDescription('Interactive Bare OS control panel') const ping = new B2().setName('ping').setDescription('Reply pong') const ok = discordCmdAddSubs(bare, [ ['ping', 'Latency / liveness'], ['about', 'What this bot is'], ['help', 'Command map'], ['status', 'Session snapshot (logged-in user)'], ['whoami', 'Session user'], ['hostname', 'Guest hostname'], ['date', 'Clock'], ['uptime', '/proc/uptime'], ['motd', '/etc/motd'], ['uname', 'uname -a style'] ]) && discordCmdAddSubs(sys, [ ['df', 'Disk / host resources'], ['mem', 'Memory info'], ['ps', 'Process table'], ['env', 'Redacted environment'], ['doctor', 'Security / debug posture'], ['features', '/proc/bare_os_features'], ['rlimits', 'Resource limits'] ]) && discordCmdAddSubs(svc, [ ['list', 'systemctl list'], ['status', 'Unit status', function (s) { discordCmdOpt(s, 'unit', 'Unit name', true, true) }], ['start', 'Start a unit', function (s) { discordCmdOpt(s, 'unit', 'Unit name', true, true) }], ['stop', 'Stop a unit', function (s) { discordCmdOpt(s, 'unit', 'Unit name', true, true) }], ['restart', 'Restart a unit', function (s) { discordCmdOpt(s, 'unit', 'Unit name', true, true) }], ['logs', 'Unit logs', function (s) { discordCmdOpt(s, 'unit', 'Unit name', true, true) }] ]) && discordCmdAddSubs(fsCmd, [ ['ls', 'List a directory', function (s) { discordCmdOpt(s, 'path', 'VFS path', false, true) }], ['cat', 'Read a file', function (s) { discordCmdOpt(s, 'path', 'VFS path', true, true) }], ['stat', 'Stat a path', function (s) { discordCmdOpt(s, 'path', 'VFS path', true, true) }], ['head', 'First lines of a file', function (s) { discordCmdOpt(s, 'path', 'VFS path', true, true) discordCmdOpt(s, 'lines', 'Line count', false, false) }] ]) && discordCmdAddSubs(net, [ ['peers', 'Swarm peers'], ['swarm', 'Swarm snapshot'], ['summary', 'net_summary'] ]) && discordCmdAddSubs(plugins, [ ['list', 'Loaded plugins'], ['reload', 'Rescan ~/.discord/plugins'], ['info', 'Plugin details', function (s) { discordCmdOpt(s, 'name', 'Plugin name', true, false) }], ['disable', 'Disable a plugin', function (s) { discordCmdOpt(s, 'name', 'Plugin name', true, false) }], ['enable', 'Enable a plugin', function (s) { discordCmdOpt(s, 'name', 'Plugin name', true, false) }] ]) if (!ok) { return [ping.toJSON()] } discordCmdOpt(man, 'page', 'Command name (e.g. uname)', true, true) discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false) discordCmdOpt(run, 'cmd', 'Shell line (cd, pipes, &&, redirects, $VAR)', false, true) discordCmdOpt(journal, 'unit', 'Unit name', false, true) discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true) discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true) discordCmdOpt(files, 'path', 'Directory to open', false, true) const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping].map( function (c) { return c.toJSON() } ) return stock.concat(discordPluginsSlash(dj)) } if (ctx && typeof ctx.then !== 'function') { return Promise.resolve(discordPluginsEnsure(ctx)).then(finish) } return finish() } function discordShowModal(interaction, spec) { if (!interaction || typeof interaction.showModal !== 'function') return false return interaction.showModal({ custom_id: spec.id, title: String(spec.title || 'Bare OS').slice(0, 45), components: [ { type: 1, components: [ { type: 4, custom_id: spec.field || 'text', label: String(spec.label || 'Value').slice(0, 45), style: spec.paragraph ? 2 : 1, required: true, max_length: spec.max || 200, placeholder: spec.placeholder || '' } ] } ] }) } async function discordRouteCommand(ctx, name, sub, opt) { if (name === 'panel') return discordPanel(ctx) if (name === 'ping' || (name === 'bare' && (sub === 'ping' || !sub))) { return discordCmdHandleBare(ctx, 'ping') } if (name === 'bare') return discordCmdHandleBare(ctx, sub) if (name === 'sys') return discordCmdHandleSys(ctx, sub) if (name === 'svc') return discordCmdHandleSvc(ctx, sub, opt('unit')) if (name === 'fs') return discordCmdHandleFs(ctx, sub, opt('path'), opt('lines')) if (name === 'net') return discordCmdHandleNet(ctx, sub) if (name === 'man') return discordCmdHandleMan(ctx, opt('page')) if (name === 'say') { const text = opt('text') if (!text) return { modal: 'say' } return discordResult( discordEmbed({ title: 'say', desc: discordCmdFence(discordCmdSayBox(text)) }) ) } if (name === 'run') return { run: opt('cmd') || '' } if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit')) if (name === 'edit') { const path = opt('path') if (!path) return { modal: 'edit:path' } return { edit: path } } if (name === 'create') { const path = opt('path') if (!path) return { createPick: true } return { create: path } } if (name === 'files' || name === 'browse') { return { files: opt('path') || '' } } if (name === 'settings') return { settings: true } return { text: 'unknown command: /' + name, ephemeral: true } } async function discordBeginEdit(ctx, interaction, rawPath) { const p = discordCmdPathWriteOk(ctx, rawPath) if (!p) { return discordSendResult(ctx, interaction, { text: 'Cannot edit that path. Writable locations: `~/…`, `/home/' + discordSessionUser(ctx) + '/…`, `/tmp/…`. Token files are blocked.', ephemeral: true }) } if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { return discordSendResult(ctx, interaction, { text: 'vfs.writeFile is unavailable in this session', ephemeral: true }) } let text = await discordCmdReadText(ctx, p) const created = !text if (text && discordLooksBinary(text)) { return discordSendResult(ctx, interaction, { text: 'Refusing to open a binary file in a Discord modal: `' + p + '`', ephemeral: true }) } const split = discordEditChunks(text) discordEditPut(interaction, { path: p, origLen: split.origLen, truncated: split.truncated, created: created }) if (typeof interaction.showModal !== 'function') { return discordSendResult(ctx, interaction, { text: 'This client cannot show Discord modals.', ephemeral: true }) } try { await interaction.showModal(discordEditModalPayload(p, split.chunks)) } catch (err) { if (ctx.console && typeof ctx.console.error === 'function') { ctx.console.error( 'discord-bot: edit modal failed: ' + ((err && err.message) || err) ) } return discordSendResult(ctx, interaction, { text: 'Could not open the editor modal: ' + ((err && err.message) || err), ephemeral: true }) } } async function discordEditSaveFromModal(ctx, interaction) { const rec = discordEditGet(interaction) if (!rec || !rec.path) { return { text: 'Edit session expired (15 minutes). Run `/edit` again.', ephemeral: true } } const p = discordCmdPathWriteOk(ctx, rec.path) if (!p) { return { text: 'Write not allowed for `' + rec.path + '`', ephemeral: true } } if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { return { text: 'vfs.writeFile is unavailable', ephemeral: true } } const parts = [] for (let i = 0; i < BARE_OS_DISCORD_EDIT_MAX_CHUNKS; i++) { try { if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') { parts.push(String(interaction.fields.getTextInputValue('c' + i) || '')) } } catch { /* missing chunk */ } } const body = parts.join('') if (rec.created && (await discordFileExists(ctx, p))) { return { text: '`' + p + '` already exists. Use `/edit` to change it.', ephemeral: true } } try { await ctx.vfs.writeFile(p, discordTextToBuf(ctx, body)) } catch (err) { return { text: 'Save failed: ' + ((err && err.message) || err), ephemeral: true } } discordEditClear(interaction) const lines = body ? body.split(/\r?\n/).length : 0 return discordResult( discordEmbed({ title: rec.created ? 'Created ' + p : 'Saved ' + p, color: BARE_OS_DISCORD_COLOR_OK, fields: [ discordField('Path', p, false), discordField('Bytes', String(body.length), true), discordField('Lines', String(lines), true), discordField('User', discordSessionUser(ctx), true) ], desc: rec.truncated ? 'Original file was larger than the Discord modal cap (20 000 characters). Only the loaded window was saved.' : 'Written through `ctx.vfs.writeFile` as **' + discordSessionUser(ctx) + '**.' }), { components: [ discordButtons([ { id: 'edit:open', label: 'Edit again', style: 1 }, { id: 'edit:new', label: 'Edit another…' }, { id: 'create:new', label: 'Create another…', style: 3 } ]) ].filter(Boolean), editPath: p } ) } function discordCreatePicker(ctx) { const choices = discordSuggestEditPaths(ctx, '') const sel = discordSelect( 'create:pick', 'Choose a new file path…', choices.map(function (c) { return { label: c.name, value: c.value, description: 'Create this path' } }) ) return discordResult( discordEmbed({ title: 'Create a file', desc: 'Pick a suggested path, or **Custom path…** to type one. Then enter the contents in a Discord modal.\nWritable: `~/…`, `/tmp/…`, `/home/' + discordSessionUser(ctx) + '/…`.' }), { components: [ sel, discordButtons([ { id: 'create:custom', label: 'Custom path…', style: 1 } ]) ].filter(Boolean) } ) } var BARE_OS_DISCORD_SET_SESSIONS = Object.create(null) var BARE_OS_DISCORD_SET_TTL_MS = BARE_OS_DISCORD_IDLE_MS var BARE_OS_DISCORD_THEME_FALLBACK = [ 'catppuccin_mocha', 'default', 'dracula', 'github_dark', 'gruvbox_dark', 'nord', 'solarized_dark', 'tokyo_night' ] var BARE_OS_DISCORD_SET_PAGE = 8 var BARE_OS_DISCORD_SET_SECRET_RE = /token|secret|password|passwd|api[_-]?key|authorization|sasl/i var BARE_OS_DISCORD_SET_GROUPS = [ { id: 'appearance', label: 'Appearance', description: 'Theme, colors, TUI' }, { id: 'shell', label: 'Shell', description: 'Live flags + completion' }, { id: 'session', label: 'Session', description: 'Hostname, locale, editor' }, { id: 'discord', label: 'Discord', description: 'Guild, whitelist (not token)' }, { id: 'agent', label: 'Agent', description: '~/.agent/config.json' }, { id: 'irc', label: 'IRC', description: '~/.irc/config.json' }, { id: 'aliases', label: 'Aliases', description: 'shellAliases + ~/.barerc' } ] var BARE_OS_DISCORD_SETTINGS = [ { id: 'theme', group: 'appearance', label: 'Theme', kind: 'theme', env: 'BARE_OS_THEME', persist: 'theme', live: 'now (prompt / ls)' }, { id: 'color_depth', group: 'appearance', label: 'Color depth', kind: 'enum', env: 'BARE_OS_COLOR_DEPTH', values: ['truecolor', '256', '16', '8'], persist: 'export', live: 'now (reapply theme)' }, { id: 'ls_lock', group: 'appearance', label: 'Lock LS_COLORS', kind: 'bool', env: 'BARE_OS_LS_COLORS_LOCKED', persist: 'export', live: 'now (reapply theme)' }, { id: 'tui_noalt', group: 'appearance', label: 'TUI no altscreen', kind: 'bool', env: 'BARE_OS_TUI_NO_ALTSCREEN', persist: 'export', live: 'next TUI' }, { id: 'no_color', group: 'appearance', label: 'NO_COLOR', kind: 'bool', env: 'NO_COLOR', persist: 'export', live: 'now (reapply theme)' }, { id: 'dircolors', group: 'appearance', label: 'dircolors file', kind: 'string', env: 'BARE_OS_DIRCOLORS', persist: 'export', live: 'now (reapply theme)' }, { id: 'compact', group: 'shell', label: 'Compact completion', kind: 'bool', env: 'BARE_OS_COMPACT_MENU', persist: 'export', live: 'next Tab' }, { id: 'errexit', group: 'shell', label: 'errexit (set -e)', kind: 'bool', env: 'BARE_OS_SHELL_ERREXIT', persist: 'export', live: 'next command' }, { id: 'nounset', group: 'shell', label: 'nounset (set -u)', kind: 'bool', env: 'BARE_OS_SHELL_NOUNSET', persist: 'export', live: 'next command' }, { id: 'noglob', group: 'shell', label: 'noglob (set -f)', kind: 'bool', env: 'BARE_OS_SHELL_NOGLOB', persist: 'export', live: 'next command' }, { id: 'pipefail', group: 'shell', label: 'pipefail', kind: 'bool', env: 'BARE_OS_SHELL_PIPEFAIL', persist: 'export', live: 'next pipeline' }, { id: 'pipestatus', group: 'shell', label: 'PIPESTATUS', kind: 'bool', env: 'BARE_OS_SHELL_PIPESTATUS', persist: 'export', live: 'next pipeline' }, { id: 'posix', group: 'shell', label: 'POSIX mode', kind: 'bool', env: 'BARE_OS_SHELL_POSIX_MODE', persist: 'export', live: 'next command' }, { id: 'grouping', group: 'shell', label: 'Grouping ( … )', kind: 'bool', env: 'BARE_OS_SHELL_GROUPING', persist: 'export', live: 'next command' }, { id: 'dbracket', group: 'shell', label: '[[ … ]] tests', kind: 'bool', env: 'BARE_OS_SHELL_DOUBLE_BRACKET', persist: 'export', live: 'next command' }, { id: 'cmdsubst', group: 'shell', label: 'Command subst', kind: 'bool', env: 'BARE_OS_SHELL_CMDSUBST', persist: 'export', live: 'next $(…)' }, { id: 'streaming', group: 'shell', label: 'Pipeline streaming', kind: 'bool', env: 'BARE_OS_SHELL_STREAMING', persist: 'export', live: 'next pipeline' }, { id: 'brace', group: 'shell', label: 'Brace expansion', kind: 'bool', env: 'BARE_OS_SHELL_BRACE_EXPANSION', persist: 'export', live: 'next command' }, { id: 'paramexp', group: 'shell', label: 'Param expansion', kind: 'bool', env: 'BARE_OS_SHELL_PARAM_EXPANSION', persist: 'export', live: 'next ${…}' }, { id: 'until', group: 'shell', label: 'until loops', kind: 'bool', env: 'BARE_OS_SHELL_UNTIL', persist: 'export', live: 'next command' }, { id: 'loopctl', group: 'shell', label: 'break / continue', kind: 'bool', env: 'BARE_OS_SHELL_LOOP_CONTROL', persist: 'export', live: 'next loop' }, { id: 'readb', group: 'shell', label: 'read builtin', kind: 'bool', env: 'BARE_OS_SHELL_READ_BUILTIN', persist: 'export', live: 'next read' }, { id: 'hostname', group: 'session', label: 'Hostname', kind: 'string', env: 'HOSTNAME', persist: 'hostname', live: 'now (needs BARE_OS_HOSTNAME_SET=1)' }, { id: 'tz', group: 'session', label: 'Timezone (TZ)', kind: 'string', env: 'TZ', persist: 'export', live: 'next date' }, { id: 'lang', group: 'session', label: 'Locale (LANG)', kind: 'string', env: 'LANG', persist: 'export', live: 'next command' }, { id: 'editor', group: 'session', label: 'EDITOR', kind: 'string', env: 'EDITOR', persist: 'export', live: 'next editor' }, { id: 'pager', group: 'session', label: 'PAGER', kind: 'string', env: 'PAGER', persist: 'export', live: 'next pager' }, { id: 'guild', group: 'discord', label: 'Guild id', kind: 'string', env: 'DISCORD_GUILD_ID', persist: 'discord', live: 'next slash register' }, { id: 'whitelist', group: 'discord', label: 'User id whitelist', kind: 'string', env: 'DISCORD_ID_WHITELIST', persist: 'discord', live: 'next command' }, { id: 'dbg', group: 'discord', label: 'Debug logs', kind: 'bool', env: 'DISCORD_DEBUG', persist: 'discord', live: 'next bot start' }, { id: 'msgc', group: 'discord', label: 'Message Content', kind: 'bool', env: 'DISCORD_MESSAGE_CONTENT', persist: 'discord', live: 'next bot start' }, { id: 'loginms', group: 'discord', label: 'Login timeout ms', kind: 'number', env: 'DISCORD_LOGIN_TIMEOUT_MS', persist: 'discord', live: 'next bot start' }, { id: 'ag_backend', group: 'agent', label: 'Backend', kind: 'enum', jsonKey: 'backend', values: ['qvac', 'rest'], persist: 'agent', live: 'next agent' }, { id: 'ag_profile', group: 'agent', label: 'QVAC profile', kind: 'enum', jsonKey: 'qvac_profile', values: ['recommended', 'strong', 'tool-tiny'], persist: 'agent', live: 'next agent' }, { id: 'ag_model', group: 'agent', label: 'Model', kind: 'string', jsonKey: 'model', persist: 'agent', live: 'next agent' }, { id: 'ag_qvac_model', group: 'agent', label: 'QVAC model', kind: 'string', jsonKey: 'qvac_model', persist: 'agent', live: 'next agent' }, { id: 'ag_owner', group: 'agent', label: 'Owner name', kind: 'string', jsonKey: 'owner_name', persist: 'agent', live: 'next agent' }, { id: 'ag_label', group: 'agent', label: 'Agent label', kind: 'string', jsonKey: 'agent_label', persist: 'agent', live: 'next agent' }, { id: 'ag_url', group: 'agent', label: 'REST base URL', kind: 'string', jsonKey: 'rest_base_url', persist: 'agent', live: 'next agent' }, { id: 'ag_temp', group: 'agent', label: 'Temperature', kind: 'number', jsonKey: 'temperature', persist: 'agent', live: 'next agent' }, { id: 'ag_maxtok', group: 'agent', label: 'Max tokens', kind: 'number', jsonKey: 'max_tokens', persist: 'agent', live: 'next agent' }, { id: 'ag_maxiter', group: 'agent', label: 'Max iterations', kind: 'number', jsonKey: 'max_iterations', persist: 'agent', live: 'next agent' }, { id: 'ag_stream', group: 'agent', label: 'Stream', kind: 'bool', jsonKey: 'stream', persist: 'agent', live: 'next agent' }, { id: 'ag_del', group: 'agent', label: 'Allow delete', kind: 'bool', jsonKey: 'allow_delete', persist: 'agent', live: 'next agent' }, { id: 'ag_reason', group: 'agent', label: 'Show reasoning', kind: 'bool', jsonKey: 'show_reasoning', persist: 'agent', live: 'next agent' }, { id: 'ag_reason_mode', group: 'agent', label: 'Reasoning mode', kind: 'enum', jsonKey: 'reasoning_mode', values: ['off', 'summary', 'trace'], persist: 'agent', live: 'next agent' }, { id: 'ag_bridge', group: 'agent', label: 'Bridge mutations', kind: 'bool', jsonKey: 'allow_bridge_mutations', persist: 'agent', live: 'next agent' }, { id: 'ag_notify', group: 'agent', label: 'Host notifications', kind: 'bool', jsonKey: 'allow_host_notifications', persist: 'agent', live: 'next agent' }, { id: 'ag_hostact', group: 'agent', label: 'Host actions', kind: 'bool', jsonKey: 'allow_host_actions', persist: 'agent', live: 'next agent' }, { id: 'irc_nick', group: 'irc', label: 'Nick', kind: 'string', jsonKey: 'nick', persist: 'irc', live: 'next irc' }, { id: 'irc_join', group: 'irc', label: 'Autojoin', kind: 'string', jsonKey: 'autojoin', persist: 'irc', live: 'next irc' }, { id: 'irc_share', group: 'irc', label: 'Share channels', kind: 'bool', jsonKey: 'shareChannels', persist: 'irc', live: 'next irc' } ] function discordSetKey(interaction) { return discordInteractionUserId(interaction) || 'anon' } function discordSetGet(interaction) { const now = Date.now() for (const k in BARE_OS_DISCORD_SET_SESSIONS) { const r = BARE_OS_DISCORD_SET_SESSIONS[k] if (!r || now - r.atMs > BARE_OS_DISCORD_SET_TTL_MS) delete BARE_OS_DISCORD_SET_SESSIONS[k] } let rec = BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)] if (!rec) { rec = { group: 'appearance', sel: '', page: 0, pending: '', atMs: now } BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)] = rec } rec.atMs = now if (rec.page == null) rec.page = 0 if (rec.pending == null) rec.pending = '' return rec } function discordSettingsGet(interaction) { return discordSetGet(interaction) } function discordSettingsSpec(id) { for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) { if (BARE_OS_DISCORD_SETTINGS[i].id === id) return BARE_OS_DISCORD_SETTINGS[i] } return null } function discordSettingsFind(id) { return discordSettingsSpec(id) } function discordSettingsInGroup(group) { const out = [] for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) { if (BARE_OS_DISCORD_SETTINGS[i].group === group) out.push(BARE_OS_DISCORD_SETTINGS[i]) } return out } function discordSettingsPageOf(items, page) { const size = BARE_OS_DISCORD_SET_PAGE const pages = Math.max(1, Math.ceil(items.length / size) || 1) const p = Math.min(Math.max(0, page | 0), pages - 1) return { slice: items.slice(p * size, p * size + size), page: p, pages: pages } } function discordThemeNames(ctx) { if (ctx && typeof ctx.bareOsListThemes === 'function') { try { const n = ctx.bareOsListThemes() if (Array.isArray(n) && n.length) return n.slice().sort() } catch { /* fall through */ } } return BARE_OS_DISCORD_THEME_FALLBACK.slice() } function discordEnvOn(raw) { if (raw === true) return true const v = String(raw == null ? '' : raw).trim().toLowerCase() return v === '1' || v === 'true' || v === 'yes' || v === 'on' } function discordSettingsIsSecretKey(key) { return BARE_OS_DISCORD_SET_SECRET_RE.test(String(key || '')) } function discordSettingsAliasName(raw) { const t = String(raw || '').trim() const eq = t.indexOf('=') const name = (eq >= 0 ? t.slice(0, eq) : t).trim() if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) return '' return name } function discordSettingsAliasValue(raw) { const t = String(raw || '').trim() const eq = t.indexOf('=') if (eq < 0) return '' let v = t.slice(eq + 1).trim() if ( (v.charAt(0) === "'" && v.charAt(v.length - 1) === "'") || (v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') ) { v = v.slice(1, -1) } return v } function discordSettingsQuoteAlias(value) { const v = String(value == null ? '' : value) if (/^[A-Za-z0-9_./:+@%-]+$/.test(v)) return v return "'" + v.replace(/'/g, "'\\''") + "'" } async function discordEnsureDir(ctx, path) { if (!ctx || !ctx.vfs || typeof ctx.vfs.mkdir !== 'function') return try { await ctx.vfs.mkdir(path, { recursive: true }) } catch { /* already exists or vfs without mkdir */ } } async function discordReadJsonFile(ctx, path) { const t = await discordCmdReadText(ctx, path) const j = discordTryJson(t) return j && typeof j === 'object' && !Array.isArray(j) ? j : {} } async function discordWriteJsonFile(ctx, path, obj) { if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { throw new Error('writeFile unavailable') } const parent = discordParentPath(path) if (parent && parent !== path) await discordEnsureDir(ctx, parent) const sanitized = {} const keys = Object.keys(obj || {}) for (let i = 0; i < keys.length; i++) { const k = keys[i] if (discordSettingsIsSecretKey(k)) { sanitized[k] = obj[k] continue } sanitized[k] = obj[k] } const body = JSON.stringify(sanitized, null, 2) + '\n' await ctx.vfs.writeFile(path, discordTextToBuf(ctx, body)) } async function discordPersistBarercLine(ctx, matcher, line) { if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { throw new Error('writeFile unavailable') } const text = await discordCmdReadText(ctx, '~/.barerc') const lines = text ? text.split(/\r?\n/) : [] const drop = line == null || line === '' let replaced = false const out = [] for (let i = 0; i < lines.length; i++) { if (matcher.test(lines[i])) { if (!replaced && !drop) { out.push(line) replaced = true } } else out.push(lines[i]) } if (!replaced && !drop) { if (out.length && String(out[out.length - 1]).trim() !== '') out.push('') out.push(line) } while (out.length && String(out[out.length - 1]).trim() === '') out.pop() let next = out.join('\n') if (next) next += '\n' await ctx.vfs.writeFile('~/.barerc', discordTextToBuf(ctx, next)) } async function discordPersistDiscordEnvKey(ctx, key, value) { if (discordSettingsIsSecretKey(key) || key === 'DISCORD_TOKEN') { throw new Error('refusing to write a secret key') } const path = '~/.discord/.env' let text = await discordCmdReadText(ctx, path) const line = key + '=' + String(value) const re = new RegExp('^\\s*(?:export\\s+)?' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=.*$', 'm') if (re.test(text)) text = text.replace(re, line) else text = (text && !/\n$/.test(text) ? text + '\n' : text || '') + line + '\n' await discordEnsureDir(ctx, '~/.discord') await ctx.vfs.writeFile(path, discordTextToBuf(ctx, text)) } async function discordSettingsCurrent(ctx, spec) { const env = discordCmdEnv(ctx) if (spec.persist === 'agent') { const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json') return cfg[spec.jsonKey] } if (spec.persist === 'irc') { const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json') const v = cfg[spec.jsonKey] if (Array.isArray(v)) return v.join(',') return v } if (spec.env) return env[spec.env] return '' } function discordSettingsDisplay(spec, raw) { if (spec.kind === 'bool') return discordEnvOn(raw) ? 'on' : 'off' if (raw == null || raw === '') return '(unset)' return String(raw) } function discordSettingsCoerce(spec, value) { if (spec.kind === 'bool') { const on = value === true || value === 'on' || discordEnvOn(value) if (spec.persist === 'agent' || spec.persist === 'irc') return on return on ? '1' : '0' } if (spec.kind === 'number') { const n = Number(value) if (!Number.isFinite(n)) throw new Error('not a number') return n } if (spec.kind === 'theme') { const name = String(value || '') .toLowerCase() .trim() .replace(/\s+/g, '_') if (!/^[a-z0-9_.-]+$/.test(name)) throw new Error('invalid theme name') return name } if (spec.kind === 'enum') { const want = String(value == null ? '' : value) const ok = spec.values && spec.values.indexOf(want) >= 0 if (!ok) throw new Error('invalid value') return want } return String(value == null ? '' : value) } async function discordSettingsApply(ctx, spec, value) { if (!spec) throw new Error('unknown setting') if (spec.env && discordSettingsIsSecretKey(spec.env)) { throw new Error('refusing to write a secret') } if (spec.jsonKey && discordSettingsIsSecretKey(spec.jsonKey)) { throw new Error('refusing to write a secret') } const env = discordCmdEnv(ctx) let v = discordSettingsCoerce(spec, value) if (spec.kind === 'hostname' || spec.persist === 'hostname') { const host = String(v).trim() if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$/.test(host)) throw new Error('invalid hostname') v = host } if (spec.env) { env[spec.env] = String(v) if (ctx.env && ctx.env !== env) ctx.env[spec.env] = String(v) } if (spec.persist === 'export') { await discordPersistBarercLine( ctx, new RegExp('^\\s*export\\s+' + spec.env + '='), 'export ' + spec.env + '=' + v ) } else if (spec.persist === 'theme') { await discordPersistBarercLine(ctx, /^\s*theme\s+/, 'theme ' + v) if (typeof ctx.bareOsApplyTheme === 'function') await ctx.bareOsApplyTheme() } else if (spec.persist === 'discord') { await discordPersistDiscordEnvKey(ctx, spec.env, String(v)) } else if (spec.persist === 'hostname') { try { if (typeof ctx.bareOsSetSessionHostname === 'function') { ctx.bareOsSetSessionHostname(String(v)) } else { env.HOSTNAME = String(v) env.COMPUTERNAME = String(v) if (ctx.env && ctx.env !== env) { ctx.env.HOSTNAME = String(v) ctx.env.COMPUTERNAME = String(v) } } } catch { env.HOSTNAME = String(v) env.COMPUTERNAME = String(v) if (ctx.env && ctx.env !== env) { ctx.env.HOSTNAME = String(v) ctx.env.COMPUTERNAME = String(v) } } await discordPersistBarercLine(ctx, /^\s*export\s+HOSTNAME=/, 'export HOSTNAME=' + v) } else if (spec.persist === 'agent') { const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json') if (spec.jsonKey === 'backend') cfg.backend = String(v) === 'rest' ? 'rest' : 'qvac' else cfg[spec.jsonKey] = v await discordWriteJsonFile(ctx, '~/.agent/config.json', cfg) } else if (spec.persist === 'irc') { const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json') if (spec.jsonKey === 'autojoin') { cfg.autojoin = String(v) .split(',') .map(function (s) { return s.trim() }) .filter(Boolean) } else cfg[spec.jsonKey] = v await discordWriteJsonFile(ctx, '~/.irc/config.json', cfg) } if ( spec.persist === 'theme' || spec.id === 'color_depth' || spec.id === 'ls_lock' || spec.id === 'no_color' || spec.id === 'dircolors' ) { if (spec.persist !== 'theme' && typeof ctx.bareOsApplyTheme === 'function') { await ctx.bareOsApplyTheme() } } } async function discordSettingsSetAlias(ctx, raw) { const name = discordSettingsAliasName(raw) const value = discordSettingsAliasValue(raw) if (!name || !value) throw new Error('use name=command') if (!ctx.shellAliases || typeof ctx.shellAliases !== 'object') ctx.shellAliases = {} ctx.shellAliases[name] = value await discordPersistBarercLine( ctx, new RegExp('^\\s*unalias\\s+' + name + '\\b'), '' ) await discordPersistBarercLine( ctx, new RegExp('^\\s*alias\\s+' + name + '='), 'alias ' + name + '=' + discordSettingsQuoteAlias(value) ) } async function discordSettingsRemoveAlias(ctx, name) { const n = discordSettingsAliasName(name) if (!n) throw new Error('bad alias name') if (ctx.shellAliases && n) delete ctx.shellAliases[n] await discordPersistBarercLine(ctx, new RegExp('^\\s*alias\\s+' + n + '='), '') await discordPersistBarercLine(ctx, new RegExp('^\\s*unalias\\s+' + n + '\\b'), 'unalias ' + n) } function discordSettingsChoices(ctx, spec) { if (spec.kind === 'theme') { return discordThemeNames(ctx) .slice(0, 25) .map(function (n) { return { label: n, value: n } }) } if (spec.kind === 'enum' && spec.values) { return spec.values.map(function (n) { return { label: String(n), value: String(n) } }) } if (spec.kind === 'bool') { return [ { label: 'on', value: '1' }, { label: 'off', value: '0' } ] } return [] } function discordSettingsGroupMeta(id) { for (let i = 0; i < BARE_OS_DISCORD_SET_GROUPS.length; i++) { if (BARE_OS_DISCORD_SET_GROUPS[i].id === id) return BARE_OS_DISCORD_SET_GROUPS[i] } return { id: id, label: id, description: '' } } function discordSettingsAliasEntries(ctx) { const al = (ctx.shellAliases && typeof ctx.shellAliases === 'object' && ctx.shellAliases) || {} return Object.keys(al) .sort() .map(function (k) { return { name: k, value: String(al[k]) } }) } async function discordSettingsView(ctx, interaction) { const rec = discordSetGet(interaction) const group = rec.group || 'appearance' const meta = discordSettingsGroupMeta(group) const fields = [] let pages = 1 let page = 0 let itemOpts = [] if (group === 'aliases') { const entries = discordSettingsAliasEntries(ctx) const pg = discordSettingsPageOf(entries, rec.page) rec.page = pg.page pages = pg.pages page = pg.page if (!entries.length) fields.push(discordField('Aliases', 'None defined. Add one from the menu.')) for (let i = 0; i < pg.slice.length; i++) { const e = pg.slice[i] fields.push(discordField((rec.sel === e.name ? '▸ ' : '') + e.name, '`' + (e.value || '') + '`')) } itemOpts = [{ label: 'Add alias', value: 'alias_add', description: 'name=command' }].concat( pg.slice.map(function (e) { return { label: 'Remove ' + e.name, value: 'alias_rm:' + e.name, description: e.value.slice(0, 80) } }) ) } else { const items = discordSettingsInGroup(group) const pg = discordSettingsPageOf(items, rec.page) rec.page = pg.page pages = pg.pages page = pg.page for (let i = 0; i < pg.slice.length; i++) { const spec = pg.slice[i] const cur = await discordSettingsCurrent(ctx, spec) fields.push( discordField( (rec.sel === spec.id ? '▸ ' : '') + spec.label, '**' + discordSettingsDisplay(spec, cur) + '**\n' + spec.live ) ) } itemOpts = pg.slice.map(function (s) { return { label: s.label, value: s.id, description: s.live } }) } const gsel = discordSelect( 'set:group', 'Category', BARE_OS_DISCORD_SET_GROUPS.map(function (g) { return { label: g.label, value: g.id, description: g.description } }) ) const isel = discordSelect('set:item', pages > 1 ? 'Setting… p' + (page + 1) + '/' + pages : 'Setting…', itemOpts) const acts = discordButtons([ { id: 'set:edit', label: 'Edit', style: 1 }, { id: 'set:toggle', label: 'Toggle' }, { id: 'set:reload', label: 'Reload barerc' }, { id: 'nav:panel', label: 'Panel' } ]) const pageRow = pages > 1 ? discordButtons([ { id: 'set:prev', label: '← Prev', style: 2 }, { id: 'set:next', label: 'Next →', style: 2 } ]) : null return discordResult( discordEmbed({ title: 'Settings · ' + meta.label + (pages > 1 ? ' · ' + (page + 1) + '/' + pages : ''), desc: meta.description + '. Secrets are never shown. Pick a row, then **Edit** or **Toggle**.', fields: fields.slice(0, 20), footer: discordSessionUser(ctx) + ' · expires after 2m idle' }), { components: [gsel, isel, acts, pageRow].filter(Boolean), nav: false } ) } async function discordSettingsOpenEditor(ctx, interaction, spec) { if (!spec) { return discordSendResult(ctx, interaction, { text: 'Pick a setting from the menu first.', ephemeral: true }) } const rec = discordSetGet(interaction) rec.sel = spec.id rec.pending = spec.id if (spec.kind === 'enum' || spec.kind === 'theme' || spec.kind === 'bool') { const opts = discordSettingsChoices(ctx, spec) const cur = await discordSettingsCurrent(ctx, spec) return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: spec.label, desc: 'Current: **' + discordSettingsDisplay(spec, cur) + '**. Applies live where possible and persists. ' + spec.live + '.' }), { components: [discordSelect('set:val', spec.label, opts), discordButtons([{ id: 'set:back', label: 'Back' }])], nav: false } ) ) } const cur = await discordSettingsCurrent(ctx, spec) await discordShowModal(interaction, { id: 'set:str', title: spec.label.slice(0, 45), label: spec.label.slice(0, 45), placeholder: discordSettingsDisplay(spec, cur).slice(0, 80), max: 200 }) } async function discordSettingsAction(ctx, interaction, id) { const rec = discordSetGet(interaction) if (id === 'set:home') { rec.group = 'appearance' rec.sel = '' rec.page = 0 rec.pending = '' return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:group') { rec.group = String((interaction.values && interaction.values[0]) || rec.group) rec.sel = '' rec.page = 0 rec.pending = '' return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:prev') { rec.page = Math.max(0, (rec.page | 0) - 1) return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:next') { rec.page = (rec.page | 0) + 1 return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:item') { const v = String((interaction.values && interaction.values[0]) || '') if (v === 'alias_add') { rec.sel = 'alias_add' rec.pending = 'alias_add' await discordShowModal(interaction, { id: 'set:alias', title: 'Add alias', label: 'name=command', placeholder: 'll=ls -la', max: 80 }) return } if (v.indexOf('alias_rm:') === 0) { try { await discordSettingsRemoveAlias(ctx, v.slice(9)) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Remove alias failed: ' + ((err && err.message) || err), ephemeral: true }) } rec.sel = '' return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } rec.sel = v rec.pending = v return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(v)) } if (id === 'set:val') { const spec = discordSettingsSpec(rec.sel) const v = String((interaction.values && interaction.values[0]) || '') if (spec) { try { await discordSettingsApply(ctx, spec, v) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Apply failed: ' + ((err && err.message) || err), ephemeral: true }) } } rec.pending = '' return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:toggle') { const spec = discordSettingsSpec(rec.sel) if (!spec || spec.kind !== 'bool') { return discordSendResult(ctx, interaction, { text: 'Select a boolean setting first, then Toggle.', ephemeral: true }) } const cur = await discordSettingsCurrent(ctx, spec) const next = discordEnvOn(cur) ? '0' : '1' try { await discordSettingsApply(ctx, spec, next) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Toggle failed: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:edit') { if (rec.group === 'aliases') { rec.pending = 'alias_add' await discordShowModal(interaction, { id: 'set:alias', title: 'Add alias', label: 'name=command', placeholder: 'll=ls -la', max: 80 }) return } return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(rec.sel)) } if (id === 'set:reload') { if (typeof ctx.execLine === 'function') { try { await ctx.execLine('barerc reload') } catch { /* ignore */ } } return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:back') { rec.pending = '' return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } async function discordBeginCreate(ctx, interaction, rawPath) { const p = discordCmdPathWriteOk(ctx, rawPath) if (!p) { return discordSendResult(ctx, interaction, { text: 'Cannot create that path. Use `~/file`, `/tmp/file`, or `/home/' + discordSessionUser(ctx) + '/file`. Token files are blocked.', ephemeral: true }) } if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') { return discordSendResult(ctx, interaction, { text: 'vfs.writeFile is unavailable in this session', ephemeral: true }) } if (await discordFileExists(ctx, p)) { discordEditPut(interaction, { path: p }) return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: 'Already exists', color: BARE_OS_DISCORD_COLOR_WARN, desc: '`' + p + '` is already on disk. Use **Edit** to change it, or pick another path.' }), { components: [ discordButtons([ { id: 'edit:open', label: 'Edit instead', style: 1 }, { id: 'create:new', label: 'Different path…', style: 3 } ]) ], editPath: p } ) ) } const split = discordEditChunks('') discordEditPut(interaction, { path: p, origLen: 0, truncated: false, created: true }) if (typeof interaction.showModal !== 'function') { return discordSendResult(ctx, interaction, { text: 'This client cannot show Discord modals.', ephemeral: true }) } try { const modal = discordEditModalPayload(p, split.chunks) modal.custom_id = 'create:save' modal.title = ('Create ' + (String(p).split('/').pop() || p)).slice(0, 45) await interaction.showModal(modal) } catch (err) { if (ctx.console && typeof ctx.console.error === 'function') { ctx.console.error( 'discord-bot: create modal failed: ' + ((err && err.message) || err) ) } return discordSendResult(ctx, interaction, { text: 'Could not open the create modal: ' + ((err && err.message) || err), ephemeral: true }) } } function discordIdleNow() { return Date.now() } function discordIdleSchedule(fn, ms) { const tfn = typeof setTimeout === 'function' ? setTimeout : globalThis.setTimeout if (typeof tfn !== 'function') return null const t = tfn(fn, ms) if (t && typeof t.unref === 'function') t.unref() return t } function discordIdleClearTimer(t) { const cfn = typeof clearTimeout === 'function' ? clearTimeout : globalThis.clearTimeout if (t != null && typeof cfn === 'function') cfn(t) } function discordIdleExpiredEmbeds(embeds) { const out = [] if (!Array.isArray(embeds)) return out for (let i = 0; i < embeds.length; i++) { const e = embeds[i] || {} const n = {} for (const k in e) n[k] = e[k] n.footer = { text: 'Expired after 2 minutes idle' } n.color = BARE_OS_DISCORD_COLOR_WARN out.push(n) } return out } function discordIdleForgetSessions(userId) { if (!userId) return for (const k in BARE_OS_DISCORD_LIVE) { const r = BARE_OS_DISCORD_LIVE[k] if (r && r.userId === userId) return } delete BARE_OS_DISCORD_EDIT_SESSIONS[userId] delete BARE_OS_DISCORD_FM_SESSIONS[userId] delete BARE_OS_DISCORD_SET_SESSIONS[userId] delete BARE_OS_DISCORD_MORE_SESSIONS[userId] delete BARE_OS_DISCORD_SH_SESSIONS[userId] } function discordUnwrapMessage(sent) { if (!sent || typeof sent !== 'object') return null if (sent.resource && sent.resource.message) return sent.resource.message if (sent.message && (sent.message.id || typeof sent.message.edit === 'function')) { return sent.message } if (sent.id || typeof sent.delete === 'function' || typeof sent.edit === 'function') return sent return null } async function discordResolvePostedMessage(interaction, sent) { const unwrapped = discordUnwrapMessage(sent) if (unwrapped) return unwrapped if (interaction && interaction.message && (interaction.message.id || typeof interaction.message.delete === 'function')) { return interaction.message } if (interaction && typeof interaction.fetchReply === 'function') { try { const m = await interaction.fetchReply() return discordUnwrapMessage(m) || m } catch { return null } } return null } function discordIdleArm(msg, payload, userId) { if (!msg) return null const hasUi = (payload && payload.embeds && payload.embeds.length) || (payload && payload.components && payload.components.length) if (!hasUi) return null const id = String(msg.id || '') || 'anon:' + ++BARE_OS_DISCORD_IDLE_SEQ const prev = BARE_OS_DISCORD_LIVE[id] if (prev) discordIdleClearTimer(prev.timer) const rec = { id: id, message: msg, userId: userId || '', embeds: payload && payload.embeds ? payload.embeds : null, atMs: discordIdleNow(), timer: null } BARE_OS_DISCORD_LIVE[id] = rec rec.timer = discordIdleSchedule(function () { Promise.resolve(discordIdleExpire(id)).catch(function () {}) }, BARE_OS_DISCORD_IDLE_MS) return rec } async function discordIdleExpire(id) { const rec = BARE_OS_DISCORD_LIVE[id] if (!rec) return false delete BARE_OS_DISCORD_LIVE[id] discordIdleClearTimer(rec.timer) rec.timer = null const msg = rec.message let cleaned = false if (msg && typeof msg.delete === 'function') { try { await msg.delete() cleaned = true } catch { cleaned = false } } if (!cleaned && msg && typeof msg.edit === 'function') { try { const body = { components: [] } if (rec.embeds && rec.embeds.length) body.embeds = discordIdleExpiredEmbeds(rec.embeds) await msg.edit(body) cleaned = true } catch { cleaned = false } } discordIdleForgetSessions(rec.userId) return cleaned } function discordIdleSweep() { const now = discordIdleNow() const due = [] for (const k in BARE_OS_DISCORD_LIVE) { const r = BARE_OS_DISCORD_LIVE[k] if (!r || now - r.atMs >= BARE_OS_DISCORD_IDLE_MS) due.push(k) } for (let i = 0; i < due.length; i++) { Promise.resolve(discordIdleExpire(due[i])).catch(function () {}) } } function discordIdleTouch(interaction) { const mid = interaction && interaction.message && interaction.message.id if (!mid) return const rec = BARE_OS_DISCORD_LIVE[String(mid)] if (!rec) return if (discordIdleNow() - rec.atMs >= BARE_OS_DISCORD_IDLE_MS) return rec.atMs = discordIdleNow() } async function discordIdleWatch(interaction, payload, sent) { if (!payload) return if ( payload.flags === BARE_OS_DISCORD_FLAG_EPHEMERAL && !(payload.embeds && payload.embeds.length) && !(payload.components && payload.components.length) ) { return } const msg = await discordResolvePostedMessage(interaction, sent) discordIdleArm(msg, payload, discordInteractionUserId(interaction)) } async function discordSendResult(ctx, interaction, result) { if (result && result.run !== undefined) { return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, result.run)) } if (result && result.more) { return discordSendResult(ctx, interaction, discordLongTextResult(interaction, result.more)) } if (result && result.editPath) { const prev = discordEditGet(interaction) discordEditPut(interaction, { path: result.editPath, created: !!(result.created || (prev && prev.created)) }) } if (result && result.edit) { await discordBeginEdit(ctx, interaction, result.edit) return } if (result && result.create) { await discordBeginCreate(ctx, interaction, result.create) return } if (result && result.createPick) { return discordSendResult(ctx, interaction, discordCreatePicker(ctx)) } if (result && result.settings) { return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (result && result.files !== undefined) { const rec = discordFmGet(interaction, ctx) const want = String(result.files || '').trim() if (want) { const p = discordCmdPathOk(want) if (p) { rec.cwd = p rec.page = 0 rec.sel = '' rec.confirm = '' } } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } if (result && result.modal === 'create:path') { await discordShowModal(interaction, { id: 'create:path', title: 'New file path', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return } if (result && result.modal === 'edit:path') { await discordShowModal(interaction, { id: 'edit:path', title: 'Open a file to edit', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return } if (result && result.modal === 'say') { await discordShowModal(interaction, { id: 'say:modal', title: 'Say as Bare OS', label: 'Message', paragraph: true, placeholder: 'Hello from Bare OS' }) return } if (result && result.modal === 'run') { return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, '')) } const payload = discordCmdReplyPayload(result) const isComp = (typeof interaction.isButton === 'function' && interaction.isButton()) || (typeof interaction.isStringSelectMenu === 'function' && interaction.isStringSelectMenu()) let sent = null try { if (isComp && typeof interaction.update === 'function' && !interaction.replied && !interaction.deferred) { sent = await interaction.update(payload) } else if (interaction.deferred && typeof interaction.editReply === 'function') { sent = await interaction.editReply(payload) } else if (interaction.replied && typeof interaction.followUp === 'function') { sent = await interaction.followUp(payload) } else { sent = await interaction.reply(payload) } } catch (err) { if (ctx && ctx.console && typeof ctx.console.error === 'function') { ctx.console.error( 'discord-bot: reply failed: ' + ((err && err.message) || err) ) } return } try { await discordIdleWatch(interaction, payload, sent) } catch { /* tracking must not break the bot */ } } async function discordReplyWhitelistDenied(ctx, interaction) { await discordSendResult(ctx, interaction, { text: BARE_OS_DISCORD_WHITELIST_DENY, ephemeral: true }) } async function discordDispatchAutocomplete(ctx, interaction) { let focused = { name: '', value: '' } try { if (interaction.options && typeof interaction.options.getFocused === 'function') { const f = interaction.options.getFocused(true) if (f && typeof f === 'object') focused = f else focused = { name: 'cmd', value: String(f || '') } } } catch { focused = { name: '', value: '' } } const q = focused.value const fname = String(focused.name || '') let choices = [] try { if (fname === 'unit') choices = await discordSuggestUnits(ctx, q) else if (fname === 'page') choices = await discordSuggestMan(ctx, q) else if (fname === 'cmd') choices = await discordSuggestRun(ctx, interaction, q) else if (fname === 'path') { const cmd = String(interaction.commandName || '') choices = cmd === 'edit' || cmd === 'create' ? discordSuggestEditPaths(ctx, q) : discordSuggestPaths(ctx, q) } else if (fname === 'name' && String(interaction.commandName || '') === 'plugins') { await discordPluginsEnsure(ctx) const names = [] for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) names.push(BARE_OS_DISCORD_PLUGINS[i].name) choices = discordFilterChoices(names, q) } else { const plug = discordPluginFind(interaction.commandName) if (plug && typeof plug.onAutocomplete === 'function') { const ev = discordPluginEvent(ctx, interaction, plug, '', function () { return q }) ev.focused = fname ev.query = q const got = await plug.onAutocomplete(ev) if (Array.isArray(got)) choices = discordFilterChoices(got, q) } } } catch { choices = [] } try { if (typeof interaction.respond === 'function') await interaction.respond(choices.slice(0, 25)) } catch { /* ignore */ } return true } async function discordFmDelete(ctx, path) { if (typeof ctx.vfs.rm === 'function') { await ctx.vfs.rm(path, { recursive: true, force: true }) return } const st = await discordFmStat(ctx, path) if (discordIsDirStat(st) && typeof ctx.vfs.rmdir === 'function') { await ctx.vfs.rmdir(path) return } if (typeof ctx.vfs.unlink === 'function') await ctx.vfs.unlink(path) else throw new Error('unlink unavailable') } async function discordFmCopyFile(ctx, from, to) { if (typeof ctx.vfs.readFile !== 'function' || typeof ctx.vfs.writeFile !== 'function') { throw new Error('copy requires readFile/writeFile') } const buf = await ctx.vfs.readFile(from) await ctx.vfs.writeFile(to, buf) } async function discordFmAction(ctx, interaction, id) { const rec = discordFmGet(interaction, ctx) if (id === 'fm:goto') { await discordShowModal(interaction, { id: 'fm:goto', title: 'Go to path', label: 'Directory', placeholder: rec.cwd || '~', max: 200 }) return } if (id === 'fm:mkdir') { await discordShowModal(interaction, { id: 'fm:mkdir', title: 'New folder in ' + discordBaseName(rec.cwd || '~'), label: 'Folder name', placeholder: 'docs', max: 80 }) return } if (id === 'fm:ren') { if (!rec.sel) { return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } await discordShowModal(interaction, { id: 'fm:ren', title: 'Rename ' + rec.sel, label: 'New name', placeholder: rec.sel, max: 80 }) return } if (id === 'fm:copy') { if (!rec.sel) { return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } await discordShowModal(interaction, { id: 'fm:copy', title: 'Copy ' + rec.sel, label: 'Destination path', placeholder: discordJoinPath(rec.cwd, rec.sel + '.copy'), max: 200 }) return } if (id === 'fm:new') { const name = 'new-' + String(Date.now()).slice(-6) + '.txt' await discordBeginCreate(ctx, interaction, discordJoinPath(rec.cwd, name)) return } if (id === 'fm:edit') { if (!rec.sel) { return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } await discordBeginEdit(ctx, interaction, discordJoinPath(rec.cwd, rec.sel)) return } if (id === 'fm:up') { rec.cwd = discordParentPath(rec.cwd) rec.sel = '' rec.page = 0 rec.confirm = '' } else if (id === 'fm:home') { rec.cwd = '~' rec.sel = '' rec.page = 0 rec.confirm = '' } else if (id === 'fm:prev') { rec.page-- rec.confirm = '' } else if (id === 'fm:next') { rec.page++ rec.confirm = '' } else if (id === 'fm:ref' || id === 'fm:more' || id === 'fm:no') { rec.confirm = '' } else if (id === 'fm:pick') { const v = String((interaction.values && interaction.values[0]) || '') rec.confirm = '' if (v === '..') { rec.cwd = discordParentPath(rec.cwd) rec.sel = '' rec.page = 0 } else if (v) { const full = discordJoinPath(rec.cwd, v) const st = await discordFmStat(ctx, full) if (discordIsDirStat(st)) { rec.cwd = full rec.sel = '' rec.page = 0 } else { rec.sel = v return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full)) } } } else if (id === 'fm:open') { if (!rec.sel) { /* stay */ } else { const full = discordJoinPath(rec.cwd, rec.sel) const st = await discordFmStat(ctx, full) if (discordIsDirStat(st)) { rec.cwd = full rec.sel = '' rec.page = 0 } else { return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full)) } } } else if (id === 'fm:del') { rec.confirm = rec.sel || '' } else if (id === 'fm:ok') { const name = rec.confirm || rec.sel rec.confirm = '' if (name) { const full = discordJoinPath(rec.cwd, name) if (!discordCmdPathWriteOk(ctx, full)) { return discordSendResult(ctx, interaction, { text: 'Delete not allowed: `' + full + '`', ephemeral: true }) } try { await discordFmDelete(ctx, full) rec.sel = '' } catch (err) { return discordSendResult(ctx, interaction, { text: 'Delete failed: ' + ((err && err.message) || err), ephemeral: true }) } } } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } async function discordDispatchComponent(ctx, interaction) { const id = String(interaction.customId || '') const opt = function () { return '' } if (id.indexOf('plug:') === 0) { await discordPluginComponent(ctx, interaction, id) return true } if (id.indexOf('sh:') === 0 || id === 'run:compose') { await discordShAction(ctx, interaction, id) return true } if (id === 'more:prev' || id === 'more:next') { const rec = discordMoreGet(interaction) if (!rec) { return discordSendResult(ctx, interaction, { text: 'This view expired after 2 minutes idle.', ephemeral: true }) } rec.page += id === 'more:next' ? 1 : -1 return discordSendResult(ctx, interaction, discordMoreResult(interaction)) } if (id.indexOf('set:') === 0) { await discordSettingsAction(ctx, interaction, id) return true } if (id.indexOf('fm:') === 0) { await discordFmAction(ctx, interaction, id) return true } if (id === 'nav:panel') return discordSendResult(ctx, interaction, discordPanel(ctx)) if (id === 'say:compose') { await discordShowModal(interaction, { id: 'say:modal', title: 'Say as Bare OS', label: 'Message', paragraph: true, placeholder: 'Hello from Bare OS' }) return true } if (id === 'edit:new') { await discordShowModal(interaction, { id: 'edit:path', title: 'Open a file to edit', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return true } if (id === 'create:new' || id === 'create:custom') { await discordShowModal(interaction, { id: 'create:path', title: 'New file path', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return true } if (id === 'create:pick') { const values = interaction.values || [] const path = values[0] ? String(values[0]) : '' if (!path) { return discordSendResult(ctx, interaction, discordCreatePicker(ctx)) } await discordBeginCreate(ctx, interaction, path) return true } if (id === 'create:open') { const rec = discordEditGet(interaction) const path = rec && rec.path if (!path) { await discordShowModal(interaction, { id: 'create:path', title: 'New file path', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return true } await discordBeginCreate(ctx, interaction, path) return true } if (id === 'edit:open') { const rec = discordEditGet(interaction) const path = rec && rec.path if (!path) { await discordShowModal(interaction, { id: 'edit:path', title: 'Open a file to edit', label: 'Path under ~ or /tmp', placeholder: '~/notes.txt', max: 200 }) return true } await discordBeginEdit(ctx, interaction, path) return true } if (id.indexOf('bare:') === 0) { return discordSendResult(ctx, interaction, await discordCmdHandleBare(ctx, id.slice(5))) } if (id.indexOf('sys:') === 0) { return discordSendResult(ctx, interaction, await discordCmdHandleSys(ctx, id.slice(4))) } if (id.indexOf('net:') === 0) { return discordSendResult(ctx, interaction, await discordCmdHandleNet(ctx, id.slice(4))) } if (id === 'svc:list') { return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'list', '')) } if (id === 'svc:pick') { const values = interaction.values || [] const unit = values[0] ? String(values[0]) : '' return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'status', unit)) } const svcAct = /^svc:(status|start|stop|restart|logs):(.+)$/.exec(id) if (svcAct) { return discordSendResult( ctx, interaction, await discordCmdHandleSvc(ctx, svcAct[1], svcAct[2]) ) } return discordSendResult(ctx, interaction, await discordRouteCommand(ctx, 'panel', '', opt)) } async function discordDispatchModal(ctx, interaction) { const id = String(interaction.customId || '') if (id.indexOf('plug:') === 0) { const parts = id.split(':') const plug = discordPluginFind(parts[1] || '') if (plug && typeof plug.onModal === 'function') { const ev = discordPluginEvent(ctx, interaction, plug, '', function () { return '' }) ev.id = parts.slice(2).join(':') ev.value = '' try { if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') { ev.value = String(interaction.fields.getTextInputValue('text') || '') } } catch { ev.value = '' } const out = await plug.onModal(ev) return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out || { text: 'ok' }) } } let value = '' try { if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') { value = String(interaction.fields.getTextInputValue('text') || '') } } catch { value = '' } if (id === 'fm:goto') { const rec = discordFmGet(interaction, ctx) const p = discordCmdPathOk(value) if (p) { rec.cwd = p rec.page = 0 rec.sel = '' rec.confirm = '' } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } if (id === 'fm:mkdir') { const rec = discordFmGet(interaction, ctx) const name = String(value || '').replace(/[\/\0]/g, '') const dest = discordJoinPath(rec.cwd, name) if (!name || !discordCmdPathWriteOk(ctx, dest)) { return discordSendResult(ctx, interaction, { text: 'Cannot create folder `' + (name || '?') + '` here.', ephemeral: true }) } try { if (typeof ctx.vfs.mkdir !== 'function') throw new Error('mkdir unavailable') await ctx.vfs.mkdir(dest, { recursive: true }) rec.sel = name } catch (err) { return discordSendResult(ctx, interaction, { text: 'mkdir failed: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } if (id === 'fm:ren') { const rec = discordFmGet(interaction, ctx) const name = String(value || '').replace(/[\/\0]/g, '') const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : '' const to = discordJoinPath(rec.cwd, name) if (!from || !name || !discordCmdPathWriteOk(ctx, from) || !discordCmdPathWriteOk(ctx, to)) { return discordSendResult(ctx, interaction, { text: 'Rename not allowed.', ephemeral: true }) } try { const st = await discordFmStat(ctx, from) if (discordIsDirStat(st)) throw new Error('renaming folders is not supported') if (await discordFileExists(ctx, to)) throw new Error('destination exists') await discordFmCopyFile(ctx, from, to) await discordFmDelete(ctx, from) rec.sel = name } catch (err) { return discordSendResult(ctx, interaction, { text: 'Rename failed: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } if (id === 'fm:copy') { const rec = discordFmGet(interaction, ctx) const to = discordCmdPathWriteOk(ctx, value) const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : '' if (!from || !to || !discordCmdPathWriteOk(ctx, from)) { return discordSendResult(ctx, interaction, { text: 'Copy not allowed.', ephemeral: true }) } try { const st = await discordFmStat(ctx, from) if (discordIsDirStat(st)) throw new Error('copying folders is not supported') if (await discordFileExists(ctx, to)) throw new Error('destination exists') await discordFmCopyFile(ctx, from, to) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Copy failed: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction)) } if (id === 'set:str') { const rec = discordSettingsGet(interaction) const spec = discordSettingsFind(rec.pending || rec.sel) rec.pending = '' if (!spec || (spec.kind !== 'string' && spec.kind !== 'number')) { return discordSendResult(ctx, interaction, { text: 'No pending setting.', ephemeral: true }) } try { await discordSettingsApply(ctx, spec, value) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Could not apply `' + spec.id + '`: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'set:alias') { const rec = discordSettingsGet(interaction) rec.pending = '' rec.group = 'aliases' try { await discordSettingsSetAlias(ctx, value) rec.sel = discordSettingsAliasName(value) } catch (err) { return discordSendResult(ctx, interaction, { text: 'Alias failed: ' + ((err && err.message) || err), ephemeral: true }) } return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction)) } if (id === 'edit:save' || id === 'create:save') { if (id === 'create:save') { const rec = discordEditGet(interaction) if (rec) rec.created = true } return discordSendResult(ctx, interaction, await discordEditSaveFromModal(ctx, interaction)) } if (id === 'create:path') { const p = discordCmdPathWriteOk(ctx, value) if (!p) { return discordSendResult(ctx, interaction, { text: 'Not writable: `' + (value || '(empty)') + '`. Use `~/file`, `/tmp/file`, or `/home/' + discordSessionUser(ctx) + '/file`.', ephemeral: true }) } if (await discordFileExists(ctx, p)) { discordEditPut(interaction, { path: p }) return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: 'Already exists', color: BARE_OS_DISCORD_COLOR_WARN, desc: '`' + p + '` is already on disk. Edit it, or choose another name.' }), { components: [ discordButtons([ { id: 'edit:open', label: 'Edit instead', style: 1 }, { id: 'create:custom', label: 'Different path…', style: 3 } ]) ], editPath: p } ) ) } discordEditPut(interaction, { path: p, created: true }) return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: 'Ready to create', desc: '`' + p + '`\nDiscord cannot open a second modal from this form. Click **Compose contents**.', color: BARE_OS_DISCORD_COLOR }), { components: [ discordButtons([{ id: 'create:open', label: 'Compose contents', style: 3 }]) ], editPath: p, created: true } ) ) } if (id === 'edit:path') { const p = discordCmdPathWriteOk(ctx, value) if (!p) { return discordSendResult(ctx, interaction, { text: 'Not writable: `' + (value || '(empty)') + '`. Use `~/file`, `/tmp/file`, or `/home/' + discordSessionUser(ctx) + '/file`.', ephemeral: true }) } discordEditPut(interaction, { path: p }) const exists = Boolean(await discordCmdReadText(ctx, p)) return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: exists ? 'Ready to edit' : 'New file', desc: '`' + p + '`\nDiscord cannot open a second modal from this form. Click **Open editor**.', color: BARE_OS_DISCORD_COLOR }), { components: [ discordButtons([{ id: 'edit:open', label: 'Open editor', style: 1 }]) ], editPath: p } ) ) } if (id === 'say:modal') { return discordSendResult( ctx, interaction, discordResult( discordEmbed({ title: 'say', desc: discordCmdFence(discordCmdSayBox(value)) }) ) ) } if (id === 'run:modal') { return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, value)) } return discordSendResult(ctx, interaction, discordPanel(ctx)) } async function discordDispatchInteraction(ctx, interaction) { if (!interaction) return false discordSyncSessionIdentity(ctx) discordIdleTouch(interaction) discordIdleSweep() if (typeof interaction.isAutocomplete === 'function' && interaction.isAutocomplete()) { if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) { try { if (typeof interaction.respond === 'function') await interaction.respond([]) } catch { /* ignore */ } return true } await discordPluginsEnsure(ctx) return discordDispatchAutocomplete(ctx, interaction) } if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) { if (ctx && ctx.console && typeof ctx.console.log === 'function') { ctx.console.log( 'discord-bot: denied user ' + (discordInteractionUserId(interaction) || '?') ) } await discordReplyWhitelistDenied(ctx, interaction) return true } if (typeof interaction.isButton === 'function' && interaction.isButton()) { await discordDispatchComponent(ctx, interaction) return true } if (typeof interaction.isStringSelectMenu === 'function' && interaction.isStringSelectMenu()) { await discordDispatchComponent(ctx, interaction) return true } if (typeof interaction.isModalSubmit === 'function' && interaction.isModalSubmit()) { await discordDispatchModal(ctx, interaction) return true } if (typeof interaction.isChatInputCommand !== 'function' || !interaction.isChatInputCommand()) { return false } const name = String(interaction.commandName || '') let sub = '' let opt = function () { return '' } if (interaction.options) { if (typeof interaction.options.getSubcommand === 'function') { try { sub = String(interaction.options.getSubcommand(false) || '') } catch { sub = '' } } opt = function (key) { if (typeof interaction.options.getString === 'function') { const v = interaction.options.getString(key) return v == null ? '' : String(v) } return '' } } let result try { await discordPluginsEnsure(ctx) if (name === 'plugins') { result = await discordPluginsAdmin(ctx, interaction, sub, opt) } else { const plug = discordPluginFind(name) if (plug) result = await discordPluginInvoke(ctx, interaction, plug, sub, opt) else result = await discordRouteCommand(ctx, name, sub, opt) } } catch (err) { result = { text: 'error: ' + ((err && err.message) || String(err)), ephemeral: true } } await discordSendResult(ctx, interaction, result) return true } var bareOsDiscordCommands = { buildSlashCommands: discordBuildSlashCommands, dispatchInteraction: discordDispatchInteraction, parseIdWhitelist: discordParseIdWhitelist, userAllowed: discordUserAllowed, installProcessEmitWarning: discordInstallProcessEmitWarning, replyPayload: discordCmdReplyPayload, syncSessionIdentity: discordSyncSessionIdentity, sessionUser: discordSessionUser, FLAG_EPHEMERAL: BARE_OS_DISCORD_FLAG_EPHEMERAL, prettySnapshot: discordPrettySnapshot, prettyBytes: discordPrettyBytes, parseMeminfo: discordParseMeminfo, parseSystemctlList: discordParseSystemctlList, formatRlimits: discordFormatRlimits, formatFeatures: discordFormatFeatures, formatDoctor: discordFormatDoctor, formatSwarm: discordFormatSwarm, formatNetSummary: discordFormatNetSummary, formatHostDf: discordFormatHostDf, pathWriteOk: discordCmdPathWriteOk, editChunks: discordEditChunks, editPut: discordEditPut, editGet: discordEditGet, editModalPayload: discordEditModalPayload, joinPath: discordJoinPath, parentPath: discordParentPath, settingsSpecs: BARE_OS_DISCORD_SETTINGS, settingsApply: discordSettingsApply, settingsCurrent: discordSettingsCurrent, uniqueComponents: discordUniqueComponents, navRows: discordNavRows, result: discordResult, packEmbed: discordPackEmbed, textPages: discordTextPages, clipSmart: discordClipSmart, embedSize: discordEmbedSize, LIMIT: BARE_OS_DISCORD_LIMIT, idleMs: BARE_OS_DISCORD_IDLE_MS, idleExpire: discordIdleExpire, idleSweep: discordIdleSweep, idleArm: discordIdleArm, idleLive: function () { return BARE_OS_DISCORD_LIVE }, loadPlugins: discordPluginsLoad, listPlugins: function () { return BARE_OS_DISCORD_PLUGINS }, setPluginRegistrar: discordPluginsSetRegistrar } if (typeof module !== 'undefined' && module.exports) { module.exports = bareOsDiscordCommands }