Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-discord-commands-guest.cjs
T
2026-08-18 15:58:42 -04:00

9829 lines
316 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Bare OS Discord slash-command catalog (guest-safe: no import/export).
* Used by /bin/discord-bot (prepended) and the bare-os-discord initd unit.
* Host/pack must static-import this .cjs (createRequire fails under app.bundle).
*/
var BARE_OS_DISCORD_REPLY_MAX = 1900
var BARE_OS_DISCORD_FS_MAX = 12 * 1024
/** Discord API hard caps (message content / embed parts / combined embed). */
var BARE_OS_DISCORD_LIMIT = {
content: 2000,
title: 256,
desc: 4096,
fieldName: 256,
fieldValue: 1024,
footer: 2048,
author: 256,
fields: 25,
embedTotal: 6000,
pageChars: 3000
}
var BARE_OS_DISCORD_MORE_SESSIONS = Object.create(null)
/** discord.js MessageFlags.Ephemeral (1 << 6). Avoid the deprecated `ephemeral` option. */
var BARE_OS_DISCORD_FLAG_EPHEMERAL = 64
var BARE_OS_DISCORD_WHITELIST_DENY =
'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.'
var BARE_OS_DISCORD_OWNER_DENY = 'This control belongs to another operator.'
/** ApplicationIntegrationType: GUILD_INSTALL / USER_INSTALL */
var BARE_OS_DISCORD_INTEGRATION_GUILD = 0
var BARE_OS_DISCORD_INTEGRATION_USER = 1
/** InteractionContextType: GUILD / BOT_DM / PRIVATE_CHANNEL */
var BARE_OS_DISCORD_CONTEXT_GUILD = 0
var BARE_OS_DISCORD_CONTEXT_BOT_DM = 1
var BARE_OS_DISCORD_CONTEXT_PRIVATE = 2
/** View Channel + Send Messages + Embed Links + Attach Files + Read History + Use App Commands */
var BARE_OS_DISCORD_GUILD_INSTALL_PERMISSIONS = '2147597312'
var BARE_OS_DISCORD_COLOR = 0x5865f2
var BARE_OS_DISCORD_COLOR_OK = 0x57f287
var BARE_OS_DISCORD_COLOR_WARN = 0xfee75c
var BARE_OS_DISCORD_COLOR_ERR = 0xed4245
var BARE_OS_DISCORD_SH_TTL_MS = 30 * 60 * 1000
var BARE_OS_DISCORD_SH_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_SH_BINS = null
var BARE_OS_DISCORD_SH_BINS_AT = 0
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
var BARE_OS_DISCORD_UPLOAD_MS = 60000
var BARE_OS_DISCORD_SH_BUILTINS = [
'alias',
'unalias',
'barerc',
'cd',
'export',
'unset',
'set',
'true',
'false',
'echo',
'pwd',
'type',
'command',
'umask',
'readonly',
'test',
'[',
'jobs',
'wait',
':',
'help'
]
var BARE_OS_DISCORD_SH_TTY = {
edit: 1,
nano: 1,
vim: 1,
vi: 1,
btop: 1,
baretop: 1,
irc: 1,
chat: 1,
dhttop: 1,
swarmmap: 1,
summon: 1,
'holepunch-view': 1,
routeview: 1
}
var BARE_OS_DISCORD_SH_FLAGS = {
ls: ['-a', '-l', '-la', '-lh', '-h', '-1', '-F'],
grep: ['-i', '-v', '-n', '-r', '-E', '-F'],
find: ['-name', '-type', '-size', '-path'],
rm: ['-r', '-f', '-rf', '-v'],
cp: ['-r', '-v', '-n'],
mv: ['-v', '-n'],
cat: ['-n', '-A'],
head: ['-n'],
tail: ['-n', '-f'],
chmod: ['-R'],
chown: ['-R'],
df: ['-h', '-T'],
du: ['-h', '-s', '-a'],
ps: ['-e', '-f'],
mkdir: ['-p'],
tar: ['-xvf', '-cvf', '-tzf']
}
var BARE_OS_DISCORD_RUN_ALLOW = {
uname: 1,
whoami: 1,
hostname: 1,
date: 1,
uptime: 1,
id: 1,
pwd: 1,
arch: 1,
nproc: 1,
help: 1,
motd: 1,
true: 1,
false: 1,
df: 1,
ps: 1,
procstat: 1,
'uname -a': 1
}
var BARE_OS_DISCORD_KNOWN_UNITS = [
'bare-os-discord',
'bare-os-www',
'bare-os-chat',
'bare-holesail',
'kernel-logger'
]
var BARE_OS_DISCORD_EDIT_CHUNK = 4000
var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5
var BARE_OS_DISCORD_IDLE_MS = 2 * 60 * 1000
var BARE_OS_DISCORD_EDIT_TTL_MS = BARE_OS_DISCORD_IDLE_MS
var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_FM_PAGE = 20
var BARE_OS_DISCORD_FM_TTL_MS = BARE_OS_DISCORD_IDLE_MS
var BARE_OS_DISCORD_FM_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_LIVE = Object.create(null)
var BARE_OS_DISCORD_IDLE_SEQ = 0
var BARE_OS_DISCORD_EDIT_SUGGEST = [
'~/notes.txt',
'~/.barerc',
'~/TODO.md',
'/tmp/scratch.txt'
]
var BARE_OS_DISCORD_FS_SUGGEST = [
'~',
'/etc/os-release',
'/etc/motd',
'/proc/uptime',
'/proc/meminfo',
'/proc/bare_os/features',
'/proc/bare_os/security_posture.json',
'/proc/bare_os/process_table.json',
'/proc/bare_os/net_summary.json',
'/var/log/bare-os/discord.log',
'/var/log/bare-os/kernel-console.log',
'/mnt',
'/proc/bare_os/hdms_health.json',
'/proc/bare_os/hdms_hints.json',
'/share/man/man.json',
'/tmp'
]
/**
* bare-process has no process.emitWarning. discord.js 14 calls it whenever
* reply options include the deprecated `ephemeral` key (even when false).
*/
function discordInstallProcessEmitWarning(proc) {
const p =
proc ||
(typeof globalThis.process !== 'undefined' ? globalThis.process : null)
if (!p || typeof p.emitWarning === 'function') return p
p.emitWarning = function emitWarning(warning, type, code) {
let name = 'Warning'
let id = ''
let msg = ''
if (warning && typeof warning === 'object' && warning.type && !(warning instanceof Error)) {
name = String(warning.type || 'Warning')
id = String(warning.code || '')
msg = String(warning.message || warning)
} else if (type && typeof type === 'object') {
name = String(type.type || 'Warning')
id = String(type.code || '')
msg = warning instanceof Error ? warning.message : String(warning)
} else {
name = typeof type === 'string' ? type : 'Warning'
id = typeof code === 'string' ? code : ''
msg = warning instanceof Error ? warning.message : String(warning)
}
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg
try {
if (typeof p.emit === 'function') {
const err = warning instanceof Error ? warning : new Error(msg)
err.name = name
if (id) err.code = id
p.emit('warning', err)
}
} catch {
/* ignore */
}
try {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(line)
}
} catch {
/* ignore */
}
}
return p
}
discordInstallProcessEmitWarning()
function discordStripAnsi(text) {
let s = String(text == null ? '' : text)
s = s.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
s = s.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
s = s.replace(/\u009b[0-?]*[ -/]*[@-~]/g, '')
s = s.replace(/\u001b[@-Z\\-_]/g, '')
s = s.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u009b]/g, '')
return s
}
function discordCmdClip(text, max) {
const s = discordStripAnsi(text)
const n = max || BARE_OS_DISCORD_REPLY_MAX
if (s.length <= n) return s
return discordClipSmart(s, n, '\n…(truncated)').text
}
function discordClipSmart(text, max, suffix) {
const s = String(text == null ? '' : text)
const tail = suffix == null ? '\n…' : suffix
if (s.length <= max) return { text: s, truncated: false, hidden: 0 }
const room = Math.max(8, max - tail.length)
let cut = s.lastIndexOf('\n', room)
if (cut < room * 0.55) {
const sp = s.lastIndexOf(' ', room)
if (sp > room * 0.55) cut = sp
}
if (cut < Math.floor(room * 0.4)) cut = room
return { text: s.slice(0, cut) + tail, truncated: true, hidden: s.length - cut }
}
function discordSafeDesc(desc, max) {
const s = String(desc == null ? '' : desc)
if (max <= 0) return ''
if (s.length <= max) return s
const open = s.indexOf('```')
if (open >= 0) {
const nl = s.indexOf('\n', open)
const close = s.lastIndexOf('```')
if (nl > open && close > nl) {
const head = s.slice(0, nl + 1)
const after = s.slice(close + 3)
const hint = '\n…'
const budget = max - head.length - 4 - after.length - hint.length
if (budget >= 16) {
const body = s.slice(nl + 1, close).replace(/\n+$/, '')
const fit = discordClipSmart(body, budget, hint)
return head + fit.text.replace(/```/g, '`ˋ`') + '\n```' + after
}
}
}
return discordClipSmart(s, max, '\n…').text
}
function discordCmdRedact(text) {
return discordStripAnsi(text)
.replace(/[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{20,}/g, '[token]')
.replace(/(DISCORD_TOKEN|BOT_TOKEN|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*[=:]\s*\S+/gi, '$1=[redacted]')
}
function discordCmdFence(text, lang, max) {
const tag = lang ? String(lang) : ''
const wrap = 8 + tag.length
const cap = Math.max(32, (max || BARE_OS_DISCORD_LIMIT.desc) - wrap)
const fit = discordClipSmart(discordCmdRedact(text).replace(/```/g, '`ˋ`'), cap, '\n…')
return '```' + tag + '\n' + fit.text + '\n```'
}
function discordTextPages(text, pageChars) {
const s = String(text == null ? '' : text)
const size = Math.max(200, pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
if (!s) return ['']
if (s.length <= size) return [s]
const pages = []
let i = 0
while (i < s.length) {
if (s.length - i <= size) {
pages.push(s.slice(i))
break
}
let cut = s.lastIndexOf('\n', i + size)
if (cut <= i + Math.floor(size * 0.4)) cut = i + size
pages.push(s.slice(i, cut))
i = cut
while (s.charAt(i) === '\n') i++
}
return pages
}
function discordMarkdownCloseFences(text) {
const s = String(text == null ? '' : text)
const n = (s.match(/```/g) || []).length
if (n % 2 === 1) return s.replace(/\s*$/, '') + '\n```'
return s
}
function discordMarkdownPages(text, pageChars) {
const s = String(text == null ? '' : text)
const size = Math.max(200, pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
if (!s) return ['']
const raw = discordTextPages(s, size)
const pages = []
let open = false
for (let i = 0; i < raw.length; i++) {
let chunk = raw[i]
const marks = chunk.match(/```/g)
const count = marks ? marks.length : 0
if (open) chunk = '```\n' + chunk
const nowOpen = (open && count % 2 === 0) || (!open && count % 2 === 1)
if (nowOpen) chunk = discordMarkdownCloseFences(chunk)
open = nowOpen
pages.push(chunk)
}
return pages.length ? pages : ['']
}
function discordEmbedSize(e) {
if (!e) return 0
let n = 0
if (e.title) n += String(e.title).length
if (e.description) n += String(e.description).length
if (e.footer && e.footer.text) n += String(e.footer.text).length
if (e.author && e.author.name) n += String(e.author.name).length
const fs = e.fields || []
for (let i = 0; i < fs.length; i++) {
n += String(fs[i].name || '').length + String(fs[i].value || '').length
}
return n
}
function discordPackEmbed(opts) {
const o = opts || {}
const L = BARE_OS_DISCORD_LIMIT
const title = o.title ? discordStripAnsi(String(o.title)).slice(0, L.title) : ''
const footer = discordStripAnsi(String(o.footer || 'Bare OS · expires after 2m idle')).slice(0, L.footer)
const authorName =
o.author && o.author.name ? discordStripAnsi(String(o.author.name)).slice(0, L.author) : ''
const chrome = title.length + footer.length + authorName.length
const preferDesc = o.prefer === 'desc'
const rawFields = []
if (o.fields && o.fields.length) {
for (let i = 0; i < o.fields.length && rawFields.length < L.fields; i++) {
const f = o.fields[i]
if (!f) continue
let v = String(f.value == null ? '' : f.value).trim()
if (!v || v === '—') continue
v = discordCmdRedact(v)
if (v.length > L.fieldValue) v = discordClipSmart(v, L.fieldValue, '\n…').text
rawFields.push({
name: String(f.name || '·').slice(0, L.fieldName),
value: v,
inline: Boolean(f.inline)
})
}
}
let fieldBytes = 0
for (let i = 0; i < rawFields.length; i++) {
fieldBytes += rawFields[i].name.length + rawFields[i].value.length
}
let descRoom
if (preferDesc) {
descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - Math.min(fieldBytes, 900)))
} else {
descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - fieldBytes))
}
let desc = o.desc ? discordCmdRedact(String(o.desc)) : ''
if (desc) desc = discordSafeDesc(desc, descRoom)
let fields = rawFields.slice()
let used = chrome + desc.length + fieldBytes
if (used > L.embedTotal && fields.length) {
const keep = []
let acc = chrome + desc.length
for (let i = 0; i < fields.length; i++) {
const add = fields[i].name.length + fields[i].value.length
if (acc + add > L.embedTotal - 48) break
keep.push(fields[i])
acc += add
}
const omitted = fields.length - keep.length
if (omitted > 0) {
const note = '_' + omitted + ' more not shown_'
if (keep.length && acc + note.length + 8 <= L.embedTotal) {
keep.push({ name: '…', value: note, inline: false })
} else if (desc.length + 2 + note.length <= L.desc) {
desc = (desc ? desc + '\n' : '') + note
}
}
fields = keep
used = chrome + desc.length
for (let i = 0; i < fields.length; i++) used += fields[i].name.length + fields[i].value.length
}
if (used > L.embedTotal && desc) {
desc = discordSafeDesc(desc, Math.max(0, desc.length - (used - L.embedTotal)))
}
const embed = {
color: o.color == null ? BARE_OS_DISCORD_COLOR : o.color,
timestamp: new Date().toISOString(),
footer: { text: footer }
}
if (title) embed.title = title
if (desc) embed.description = desc
if (fields.length) embed.fields = fields
if (o.author) embed.author = { name: authorName || 'Bare OS' }
return {
embed: embed,
truncated: fields.length < rawFields.length || !!(o.desc && desc.length < String(o.desc).length),
size: discordEmbedSize(embed)
}
}
function discordTryJson(raw) {
if (raw && typeof raw === 'object') return raw
const s = String(raw || '').trim()
if (!s || (s.charAt(0) !== '{' && s.charAt(0) !== '[')) return null
try {
return JSON.parse(s)
} catch {
return null
}
}
function discordPrettyBytes(n) {
const x = Number(n)
if (!Number.isFinite(x) || x < 0) return String(n)
if (x < 1024) return String(Math.round(x)) + ' B'
const units = ['KiB', 'MiB', 'GiB', 'TiB']
let v = x / 1024
let i = 0
while (v >= 1024 && i < units.length - 1) {
v /= 1024
i++
}
return (v >= 10 ? v.toFixed(0) : v.toFixed(1)) + ' ' + units[i]
}
function discordIsByteishKey(k) {
return /byte|bytes|mem|rss|size|freemem|totalmem|avail|queued|f_bsize|f_frsize/i.test(
String(k || '')
)
}
function discordPrettyValue(key, val) {
if (val == null || val === '') return '—'
if (typeof val === 'boolean') return val ? 'yes' : 'no'
if (typeof val === 'number') {
if (discordIsByteishKey(key) && val >= 1024) return discordPrettyBytes(val)
if (key === 'atMs' || /AtMs$/.test(key)) {
if (val > 1e12) return new Date(val).toISOString()
return String(val)
}
if (/Ms$/.test(key) && val >= 1000) {
const s = Math.round(val / 1000)
if (s < 60) return s + 's'
return Math.floor(s / 60) + 'm ' + (s % 60) + 's'
}
return String(val)
}
if (typeof val === 'string') {
const t = discordCmdRedact(val)
if (/^[0-9a-f]{24,}$/i.test(t)) return '`' + t.slice(0, 16) + (t.length > 16 ? '…' : '') + '`'
return t.length > 180 ? t.slice(0, 177) + '…' : t
}
if (Array.isArray(val)) {
if (!val.length) return '(none)'
if (val.every(function (x) { return x == null || typeof x !== 'object' })) {
const shown = val.slice(0, 10).map(function (x) {
return String(x)
})
return shown.join(', ') + (val.length > 10 ? ' +' + (val.length - 10) : '')
}
return String(val.length) + ' items'
}
if (typeof val === 'object') return Object.keys(val).length + ' keys'
return String(val)
}
function discordSkipPrettyKey(k) {
return /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|API_KEY/i.test(k) ||
k === 'note' ||
k === 'schema' ||
k === 'schemaVersion'
}
function discordValueEmpty(val) {
if (val == null) return true
if (val === '') return true
if (val === '—') return true
if (Array.isArray(val) && !val.length) return true
if (typeof val === 'object' && !Array.isArray(val) && !Object.keys(val).length) return true
return false
}
function discordHumanLabel(key) {
const known = {
schemaVersion: 'Schema',
topicHex: 'Topic',
peerCount: 'Peers',
seedHandshakeError: 'Handshake',
seedRole: 'Role',
replicationQueueDepth: 'Queue depth',
replicationQueue: 'Replication',
stagingSlot: 'Staging',
snapshotHints: 'Snapshot',
peerFirewallStats: 'Firewall',
peerFirewallAcceptedTotal: 'Accepted',
peerFirewallRejectedTotal: 'Rejected',
peerFirewallInboundTotal: 'Inbound',
peerFirewallOutboundTotal: 'Outbound',
peerFirewallE2e: 'Firewall e2e',
manifestPathCount: 'Manifests',
localRamBlockCount: 'RAM blocks',
hypercoreLengthHint: 'Core length',
queueDepthEstimate: 'Queue estimate',
snapshotWorkflowNote: 'Note',
activeSlot: 'Active slot',
pendingSlot: 'Pending',
previousSlot: 'Previous',
canarySlot: 'Canary',
drainDeadlineMs: 'Drain',
atMs: 'Updated'
}
if (known[key]) return known[key]
const s = String(key || '')
.replace(/[_-]+/g, ' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/\s+/g, ' ')
.trim()
if (!s) return String(key || '')
return s.charAt(0).toUpperCase() + s.slice(1)
}
function discordCollectFields(obj, prefix, out, depth) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj) || depth > 2) return
const keys = Object.keys(obj)
for (let i = 0; i < keys.length && out.length < 18; i++) {
const k = keys[i]
if (discordSkipPrettyKey(k)) continue
const val = obj[k]
if (discordValueEmpty(val)) continue
const label = prefix ? prefix + ' · ' + discordHumanLabel(k) : discordHumanLabel(k)
if (val && typeof val === 'object' && !Array.isArray(val) && depth < 2) {
const lines = []
const sub = Object.keys(val)
for (let j = 0; j < sub.length && lines.length < 8; j++) {
if (discordSkipPrettyKey(sub[j])) continue
const sv = val[sub[j]]
if (discordValueEmpty(sv)) continue
if (sv && typeof sv === 'object') continue
const pretty = discordPrettyValue(sub[j], sv)
if (pretty === '—' || pretty === '(none)') continue
lines.push('**' + discordHumanLabel(sub[j]) + '** ' + pretty)
}
if (lines.length) out.push(discordField(discordHumanLabel(k), lines.join('\n'), false))
} else {
const pretty = discordPrettyValue(k, val)
if (pretty === '—' || pretty === '(none)') continue
out.push(discordField(label, pretty, true))
}
}
}
function discordPrettyUptime(raw) {
const s = String(raw || '').trim()
const sec = Number(s.split(/\s+/)[0])
if (!Number.isFinite(sec) || sec < 0) return s || '—'
const d = Math.floor(sec / 86400)
const h = Math.floor((sec % 86400) / 3600)
const m = Math.floor((sec % 3600) / 60)
const parts = []
if (d) parts.push(d + 'd')
if (h || d) parts.push(h + 'h')
parts.push(m + 'm')
return parts.join(' ') + ' (' + Math.round(sec) + 's)'
}
function discordParseMeminfo(text) {
const out = {}
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Za-z0-9_()]+):\s+(\d+)/.exec(lines[i])
if (!m) continue
out[m[1]] = Number(m[2]) * 1024
}
return out
}
function discordParseSystemctlList(text) {
const rows = []
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (!line || /^UNIT\b/.test(line) || line.indexOf('LOAD') === 0) continue
const parts = line.split(/\s+/)
if (parts.length < 4) continue
rows.push({
unit: parts[0].replace(/\.service$/, ''),
load: parts[1] || '',
preset: parts[2] || '',
active: parts[3] || '',
sub: parts[4] || '',
desc: parts.slice(5).join(' ')
})
}
return rows
}
function discordPrettySnapshot(title, raw, extras) {
const parsed = discordTryJson(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
const text = raw == null ? '' : String(raw)
if (!text) {
return discordResult(discordEmbed({ title: title, desc: 'unavailable' }), extras)
}
return {
more: {
title: title,
body: text,
fence: true,
components: extras && extras.components,
editPath: extras && extras.editPath,
created: extras && extras.created,
color: extras && extras.color
}
}
}
const fields = []
discordCollectFields(parsed, '', fields, 0)
const note = typeof parsed.note === 'string' ? parsed.note.slice(0, 280) : ''
return discordResult(
discordEmbed({
title: title,
desc: note,
fields: fields.slice(0, 18),
color: extras && extras.color
}),
extras
)
}
function discordHex12(ctx, u8) {
if (!u8 || typeof u8.length !== 'number') return ''
if (ctx && ctx.b4a && typeof ctx.b4a.toString === 'function') {
try {
return String(ctx.b4a.toString(u8, 'hex') || '').slice(0, 12)
} catch {
/* fall through */
}
}
let hex = ''
const n = Math.min(u8.length, 6)
for (let i = 0; i < n; i++) {
const h = (u8[i] & 0xff).toString(16)
hex += h.length < 2 ? '0' + h : h
}
return hex.slice(0, 12)
}
/** Prefer live VFS env (identity mutates this). Keep ctx.env in sync. */
function discordSyncSessionIdentity(ctx) {
if (!ctx) return
const vfsEnv = ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
const env = vfsEnv || (ctx.env && typeof ctx.env === 'object' ? ctx.env : null)
if (!env) return
if (ctx.env && vfsEnv && ctx.env !== vfsEnv) {
const keys = [
'USER',
'LOGNAME',
'HOME',
'PWD',
'UID',
'GID',
'GROUP',
'SHELL',
'BARE_OS_IDENTITY',
'BARE_OS_PUBLIC_KEY'
]
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
if (vfsEnv[k] != null && String(vfsEnv[k]) !== '') ctx.env[k] = vfsEnv[k]
}
}
const ident = ctx.identity
if (!ident || ident.state !== 'unlocked') return
let name = String(env.USER || env.LOGNAME || '').trim()
if (!name || name === 'guest') {
name = discordHex12(ctx, ident.publicKey)
if (!name) return
env.USER = name
env.LOGNAME = name
env.GROUP = name
if (ctx.env && ctx.env !== env) {
ctx.env.USER = name
ctx.env.LOGNAME = name
}
}
if (!env.HOME || env.HOME === '/home/guest') env.HOME = '/home/' + name
if (!env.PWD || env.PWD === '/home/guest') env.PWD = env.HOME
env.BARE_OS_IDENTITY = 'unlocked'
}
function discordCmdEnv(ctx) {
discordSyncSessionIdentity(ctx)
if (ctx && ctx.vfs && ctx.vfs.env) return ctx.vfs.env
if (ctx && ctx.env) return ctx.env
return {}
}
function discordSessionUser(ctx) {
const e = discordCmdEnv(ctx)
const u = String(e.USER || e.LOGNAME || e.USERNAME || '').trim()
return u || 'guest'
}
function discordUniqueComponents(rows) {
const seen = Object.create(null)
const out = []
if (!Array.isArray(rows)) return out
for (let i = 0; i < rows.length && out.length < 5; i++) {
const row = rows[i]
if (!row || !Array.isArray(row.components)) continue
const comps = []
for (let j = 0; j < row.components.length; j++) {
const c = row.components[j]
if (!c) continue
const id = c.custom_id != null ? String(c.custom_id) : ''
if (id) {
if (seen[id]) continue
seen[id] = 1
}
comps.push(c)
}
if (!comps.length) continue
out.push({
type: row.type || 1,
components: comps
})
}
return out
}
function discordCmdReplyPayload(result) {
const payload = {}
if (result && result.embeds && result.embeds.length) payload.embeds = result.embeds
if (result && result.components && result.components.length) {
payload.components = discordUniqueComponents(result.components)
if (!payload.components.length) delete payload.components
}
if (result && result.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 =
o.fence === false
? discordMarkdownPages(body, o.pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
: discordTextPages(body, o.pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
const extraComps = o.components || []
if (pages.length > 1) {
let lead = o.lead ? String(o.lead) : ''
if (lead && !/\n$/.test(lead)) lead += '\n'
discordMorePut(interaction, {
title: o.title,
pages: pages,
page: 0,
fence: o.fence !== false,
lang: o.lang || '',
lead: lead,
color: o.color,
footer: o.footer,
extra: extraComps,
total: body.length
})
const view = discordMoreResult(interaction)
if (o.editPath) view.editPath = o.editPath
if (o.created) view.created = true
return view
}
const inner = o.fence === false ? body : discordCmdFence(body, o.lang, 3800)
let lead = o.lead ? String(o.lead) : ''
if (lead && inner && !/\n$/.test(lead)) lead += '\n'
const out = discordResult(
discordEmbed({
title: o.title,
desc: lead + inner,
color: o.color,
footer: o.footer,
prefer: 'desc'
}),
{ components: extraComps, editPath: o.editPath, created: o.created }
)
return out
}
function discordButtons(items) {
const row = { type: 1, components: [] }
for (let i = 0; i < items.length && row.components.length < 5; i++) {
const it = items[i]
if (!it || !it.id) continue
row.components.push({
type: 2,
style: it.style || 2,
label: String(it.label || it.id).slice(0, 80),
custom_id: String(it.id).slice(0, 100)
})
}
return row.components.length ? row : null
}
function discordSelect(id, placeholder, options) {
const opts = []
for (let i = 0; i < options.length && opts.length < 25; i++) {
const o = options[i]
if (!o) continue
const value = String(o.value || o.label || '').slice(0, 100)
if (!value) continue
const item = {
label: String(o.label || value).slice(0, 100),
value: value
}
if (o.description) item.description = String(o.description).slice(0, 100)
opts.push(item)
}
if (!opts.length) return null
return {
type: 1,
components: [
{
type: 3,
custom_id: String(id).slice(0, 100),
placeholder: String(placeholder || 'Choose…').slice(0, 150),
min_values: 1,
max_values: 1,
options: opts
}
]
}
}
var BARE_OS_DISCORD_NAV_IDS = {
'nav:menu': 1,
'nav:panel': 1,
'bare:status': 1,
'bare:whoami': 1,
'bare:help': 1,
'sys:doctor': 1,
'svc:list': 1,
'sys:ps': 1,
'net:summary': 1,
'say:compose': 1,
'hdms:home': 1,
'holesail:home': 1,
'agent:home': 1,
'run:compose': 1,
'edit:new': 1,
'create:new': 1,
'fm:home': 1,
'set:home': 1
}
function discordCompCustomId(c) {
if (!c) return ''
if (c.custom_id != null) return String(c.custom_id)
if (c.customId != null) return String(c.customId)
if (c.data && c.data.custom_id != null) return String(c.data.custom_id)
return ''
}
function discordNormalizeRows(rows) {
const out = []
if (!Array.isArray(rows)) return out
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (!row) continue
const raw =
typeof row.toJSON === 'function'
? row.toJSON()
: {
type: row.type || 1,
components: row.components || (row.data && row.data.components) || []
}
const comps = []
const list = raw.components || []
for (let j = 0; j < list.length; j++) {
const c = list[j]
if (!c) continue
const json = typeof c.toJSON === 'function' ? c.toJSON() : c
if (json) comps.push(json)
}
if (comps.length) out.push({ type: raw.type || 1, components: comps })
}
return out
}
function discordNormalizeEmbeds(embeds) {
if (!Array.isArray(embeds) || !embeds.length) return null
const out = []
for (let i = 0; i < embeds.length; i++) {
const e = embeds[i]
if (!e) continue
out.push(typeof e.toJSON === 'function' ? e.toJSON() : e)
}
return out.length ? out : null
}
function discordNavIsExpanded(rows) {
const list = discordNormalizeRows(rows)
for (let i = 0; i < list.length; i++) {
const comps = list[i].components || []
for (let j = 0; j < comps.length; j++) {
if (discordCompCustomId(comps[j]) === 'nav:panel') return true
}
}
return false
}
function discordRowIsNavChrome(comps) {
if (!comps || !comps.length) return false
for (let i = 0; i < comps.length; i++) {
const id = discordCompCustomId(comps[i])
if (!id || !BARE_OS_DISCORD_NAV_IDS[id]) return false
}
return true
}
function discordStripNavIds(rows) {
const out = []
const list = discordNormalizeRows(rows)
for (let i = 0; i < list.length; i++) {
const src = list[i].components || []
if (discordRowIsNavChrome(src)) continue
const comps = []
for (let j = 0; j < src.length; j++) {
if (discordCompCustomId(src[j]) === 'nav:menu') continue
comps.push(src[j])
}
if (comps.length) out.push({ type: list[i].type || 1, components: comps })
}
return out
}
function discordMergeMenu(extraRows, menuRows) {
const extra = (extraRows || []).filter(Boolean)
const menu = menuRows && menuRows[0]
const btn = menu && menu.components && menu.components[0]
if (!btn) return extra
if (!extra.length) return menuRows
const last = extra[extra.length - 1]
const comps = last.components || []
if (last.type === 1 && comps.length && comps.length < 5 && comps[0].type === 2) {
const copy = extra.slice()
copy[copy.length - 1] = {
type: 1,
components: comps.concat([btn])
}
return copy
}
return extra.concat(menuRows)
}
function discordNavRows(open) {
if (!open) {
const row = discordButtons([{ id: 'nav:menu', label: 'Menu', style: 2 }])
return row ? [row] : []
}
const rows = []
const a = discordButtons([
{ id: 'nav:menu', label: 'Hide menu', style: 2 },
{ id: 'nav:panel', label: 'Panel', style: 1 },
{ id: 'bare:status', label: 'Status', style: 1 },
{ id: 'bare:whoami', label: 'Whoami' },
{ id: 'bare:help', label: 'Help' }
])
const b = discordButtons([
{ id: 'sys:doctor', label: 'Doctor' },
{ id: 'svc:list', label: 'Services' },
{ id: 'holesail:home', label: 'Holesail', style: 1 },
{ id: 'net:summary', label: 'Network' },
{ id: 'hdms:home', label: 'HDMS', style: 1 }
])
const c = discordButtons([
{ id: 'run:compose', label: 'Shell' },
{ id: 'edit:new', label: 'Edit file…', style: 1 },
{ id: 'create:new', label: 'Create file…', style: 3 },
{ id: 'fm:home', label: 'Files', style: 1 },
{ id: 'set:home', label: 'Settings' }
])
const d = discordButtons([{ id: 'agent:home', label: 'Agent', style: 1 }])
if (a) rows.push(a)
if (b) rows.push(b)
if (c) rows.push(c)
if (d) rows.push(d)
return rows
}
function discordApplyNav(rows, open) {
const base = discordStripNavIds(rows)
const nav = discordNavRows(open)
if (!open) return discordMergeMenu(base, nav)
const room = 5 - nav.length
const keep = room > 0 ? base.slice(0, room) : []
return keep.concat(nav)
}
function discordResult(embed, extra) {
const out = { embeds: [embed] }
const extraRows = extra && extra.components ? extra.components : []
const rows =
extra && extra.nav === false
? discordUniqueComponents(extraRows)
: discordUniqueComponents(discordApplyNav(extraRows, false))
if (rows.length) out.components = rows
if (extra && extra.ephemeral) out.ephemeral = true
if (extra && extra.text) out.text = extra.text
if (extra && extra.editPath) out.editPath = extra.editPath
if (extra && extra.created) out.created = true
return out
}
async function discordCmdReadText(ctx, logicalPath) {
if (!logicalPath || typeof ctx.vfs?.readFile !== 'function') return ''
try {
const buf = await ctx.vfs.readFile(logicalPath)
if (!buf) return ''
if (typeof ctx.b4a?.toString === 'function') return ctx.b4a.toString(buf)
return String(buf)
} catch {
return ''
}
}
async function discordCmdReadJson(ctx, logicalPath) {
const t = await discordCmdReadText(ctx, logicalPath)
if (!t) return null
try {
return JSON.parse(t)
} catch {
return null
}
}
function discordCmdPathOk(raw) {
const p = String(raw || '').trim() || '.'
if (!p || p.indexOf('\0') >= 0) return null
if (p.indexOf('..') >= 0) return null
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
const allow =
p === '.' ||
p === '~' ||
p.charAt(0) === '~' ||
p.indexOf('/proc') === 0 ||
p.indexOf('/etc') === 0 ||
p.indexOf('/var/log') === 0 ||
p.indexOf('/run') === 0 ||
p.indexOf('/home') === 0 ||
p.indexOf('/usr/share') === 0 ||
p.indexOf('/share') === 0 ||
p.indexOf('/tmp') === 0 ||
p === '/mnt' ||
p.indexOf('/mnt/') === 0
return allow ? p : null
}
function discordCmdPathWriteOk(ctx, raw) {
const p = discordCmdPathOk(raw)
if (!p) return null
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
if (p.indexOf('~/.discord') === 0) return null
const user = discordSessionUser(ctx)
const home = '/home/' + user
const allow =
p === '~' ||
p.indexOf('~/') === 0 ||
p.indexOf('/tmp/') === 0 ||
p === '/tmp' ||
p === home ||
p.indexOf(home + '/') === 0 ||
(p.indexOf('/mnt/') === 0 && p.length > 5)
return allow ? p : null
}
async function discordFileExists(ctx, p) {
if (!ctx || !ctx.vfs) return false
const stfn = ctx.vfs.lstat || ctx.vfs.stat
if (typeof stfn === 'function') {
try {
const st = await stfn.call(ctx.vfs, p)
return Boolean(st)
} catch {
return false
}
}
if (typeof ctx.vfs.readFile !== 'function') return false
try {
await ctx.vfs.readFile(p)
return true
} catch {
return false
}
}
function discordLooksBinary(text) {
const s = String(text || '')
if (s.indexOf('\0') >= 0) return true
let bad = 0
const n = Math.min(s.length, 800)
for (let i = 0; i < n; i++) {
const c = s.charCodeAt(i)
if (c < 9 || (c > 13 && c < 32)) bad++
}
return bad > 8
}
function discordTextToBuf(ctx, text) {
const s = String(text == null ? '' : text)
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(s)
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf8')
const out = new Uint8Array(s.length)
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff
return out
}
function discordEditGc() {
const now = Date.now()
for (const k in BARE_OS_DISCORD_EDIT_SESSIONS) {
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[k]
if (!rec || now - rec.atMs > BARE_OS_DISCORD_EDIT_TTL_MS) {
delete BARE_OS_DISCORD_EDIT_SESSIONS[k]
}
}
}
function discordEditKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordEditPut(interaction, rec) {
discordEditGc()
BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] = {
path: rec.path,
origLen: rec.origLen || 0,
truncated: !!rec.truncated,
created: !!rec.created,
atMs: Date.now()
}
}
function discordEditGet(interaction) {
discordEditGc()
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
return rec || null
}
function discordEditClear(interaction) {
delete BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
}
function discordEditChunks(text) {
const s = String(text == null ? '' : text)
const cap = BARE_OS_DISCORD_EDIT_CHUNK * BARE_OS_DISCORD_EDIT_MAX_CHUNKS
const truncated = s.length > cap
const body = truncated ? s.slice(0, cap) : s
const chunks = []
if (!body.length) chunks.push('')
else {
for (let i = 0; i < body.length; i += BARE_OS_DISCORD_EDIT_CHUNK) {
chunks.push(body.slice(i, i + BARE_OS_DISCORD_EDIT_CHUNK))
}
}
return {
chunks: chunks.slice(0, BARE_OS_DISCORD_EDIT_MAX_CHUNKS),
truncated: truncated,
origLen: s.length
}
}
function discordEditModalPayload(path, chunks) {
const base = String(path || 'file').split('/').pop() || String(path)
const n = Math.max(1, chunks.length)
const components = []
for (let i = 0; i < n; i++) {
const label = n === 1 ? 'Contents (save closes the form)' : 'Part ' + (i + 1) + ' / ' + n
const field = {
type: 4,
custom_id: 'c' + i,
label: label.slice(0, 45),
style: 2,
required: false,
max_length: BARE_OS_DISCORD_EDIT_CHUNK
}
const v = String(chunks[i] || '').slice(0, BARE_OS_DISCORD_EDIT_CHUNK)
if (v) field.value = v
components.push({
type: 1,
components: [field]
})
}
return {
custom_id: 'edit:save',
title: ('Edit ' + base).slice(0, 45),
components: components
}
}
var BARE_OS_DISCORD_CAPTURE_STACK = []
var BARE_OS_DISCORD_STDIO_HOOK = null
function discordCaptureChunkToText(chunk, enc) {
if (chunk == null) return ''
if (typeof chunk === 'string') return chunk
const encoding = typeof enc === 'string' && enc ? enc : 'utf8'
if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(chunk)) {
try {
return chunk.toString(encoding)
} catch {
return chunk.toString('utf8')
}
}
if (chunk instanceof Uint8Array || (chunk && typeof chunk.length === 'number' && chunk.buffer)) {
try {
if (typeof TextDecoder === 'function') {
return new TextDecoder(encoding === 'utf8' ? 'utf-8' : encoding).decode(chunk)
}
} catch {
/* fall through */
}
let s = ''
for (let i = 0; i < chunk.length; i++) s += String.fromCharCode(chunk[i] & 255)
return s
}
return String(chunk)
}
function discordCaptureAppend(text) {
const n = BARE_OS_DISCORD_CAPTURE_STACK.length
if (!n) return false
const top = BARE_OS_DISCORD_CAPTURE_STACK[n - 1]
top.buf += text
return true
}
function discordCaptureConsoleLine() {
const parts = []
for (let i = 0; i < arguments.length; i++) {
const a = arguments[i]
parts.push(typeof a === 'string' ? a : String(a))
}
discordCaptureAppend(parts.join(' ') + '\n')
}
function discordCaptureRaw(chunk, enc) {
discordCaptureAppend(discordCaptureChunkToText(chunk, enc))
}
function discordCaptureHookStdio() {
if (BARE_OS_DISCORD_STDIO_HOOK) return
const proc = typeof globalThis.process !== 'undefined' ? globalThis.process : null
if (!proc) return
const so = proc.stdout
const se = proc.stderr
function wrapWrite() {
return function (chunk, enc, cb) {
discordCaptureRaw(chunk, typeof enc === 'string' ? enc : undefined)
const done = typeof enc === 'function' ? enc : typeof cb === 'function' ? cb : null
if (done) {
try {
done()
} catch {
/* ignore */
}
}
return true
}
}
BARE_OS_DISCORD_STDIO_HOOK = {
stdout: so,
stderr: se,
outWrite: so && so.write,
errWrite: se && se.write,
outIsTTY: so ? so.isTTY : undefined,
errIsTTY: se ? se.isTTY : undefined
}
if (so && typeof so.write === 'function') so.write = wrapWrite()
if (se && typeof se.write === 'function') se.write = wrapWrite()
try {
if (so) so.isTTY = false
} catch {
/* ignore */
}
try {
if (se) se.isTTY = false
} catch {
/* ignore */
}
}
function discordCaptureUnhookStdio() {
if (BARE_OS_DISCORD_CAPTURE_STACK.length) return
const h = BARE_OS_DISCORD_STDIO_HOOK
if (!h) return
if (h.stdout && h.outWrite) h.stdout.write = h.outWrite
if (h.stderr && h.errWrite) h.stderr.write = h.errWrite
try {
if (h.stdout && h.outIsTTY !== undefined) h.stdout.isTTY = h.outIsTTY
} catch {
/* ignore */
}
try {
if (h.stderr && h.errIsTTY !== undefined) h.stderr.isTTY = h.errIsTTY
} catch {
/* ignore */
}
BARE_OS_DISCORD_STDIO_HOOK = null
}
var BARE_OS_DISCORD_COLOR_ENV = ['NO_COLOR', 'CLICOLOR', 'CLICOLOR_FORCE', 'FORCE_COLOR', 'TERM']
function discordCaptureColorOff(env, saved) {
if (!env || typeof env !== 'object') return
for (let i = 0; i < BARE_OS_DISCORD_COLOR_ENV.length; i++) {
const k = BARE_OS_DISCORD_COLOR_ENV[i]
saved[k] = Object.prototype.hasOwnProperty.call(env, k) ? env[k] : undefined
}
env.NO_COLOR = '1'
env.CLICOLOR = '0'
env.CLICOLOR_FORCE = '0'
env.FORCE_COLOR = '0'
env.TERM = 'dumb'
}
function discordCaptureColorRestore(env, saved) {
if (!env || typeof env !== 'object' || !saved) return
for (let i = 0; i < BARE_OS_DISCORD_COLOR_ENV.length; i++) {
const k = BARE_OS_DISCORD_COLOR_ENV[i]
if (saved[k] === undefined) delete env[k]
else env[k] = saved[k]
}
}
function discordCaptureFinish(buf) {
return discordStripAnsi(String(buf || ''))
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n$/, '')
}
async function discordCmdCapture(ctx, fn) {
const rec = { buf: '' }
BARE_OS_DISCORD_CAPTURE_STACK.push(rec)
if (!ctx.console || typeof ctx.console !== 'object') ctx.console = {}
const cons = ctx.console
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : null
const vfsEnv =
ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
const saved = {
log: cons.log,
error: cons.error,
info: cons.info,
warn: cons.warn,
debug: cons.debug,
binWrite: ctx.bareOsBinWrite,
writeScreen: ctx.writeScreen,
captured: ctx.bareOsStdoutCaptured,
env: {},
vfsEnv: {}
}
discordCaptureColorOff(env, saved.env)
discordCaptureColorOff(vfsEnv, saved.vfsEnv)
ctx.bareOsStdoutCaptured = true
cons.log = discordCaptureConsoleLine
cons.error = discordCaptureConsoleLine
cons.info = discordCaptureConsoleLine
cons.warn = discordCaptureConsoleLine
cons.debug = discordCaptureConsoleLine
ctx.bareOsBinWrite = function (chunk) {
discordCaptureRaw(chunk)
}
ctx.writeScreen = function (chunk) {
discordCaptureRaw(chunk)
}
discordCaptureHookStdio()
try {
await fn()
} finally {
cons.log = saved.log
cons.error = saved.error
cons.info = saved.info
cons.warn = saved.warn
cons.debug = saved.debug
if (saved.binWrite === undefined) delete ctx.bareOsBinWrite
else ctx.bareOsBinWrite = saved.binWrite
if (saved.writeScreen === undefined) delete ctx.writeScreen
else ctx.writeScreen = saved.writeScreen
if (saved.captured === undefined) delete ctx.bareOsStdoutCaptured
else ctx.bareOsStdoutCaptured = saved.captured
discordCaptureColorRestore(env, saved.env)
discordCaptureColorRestore(vfsEnv, saved.vfsEnv)
BARE_OS_DISCORD_CAPTURE_STACK.pop()
discordCaptureUnhookStdio()
}
return discordCaptureFinish(rec.buf)
}
async function discordCmdOsRelease(ctx) {
const t = await discordCmdReadText(ctx, '/etc/os-release')
const out = {}
const lines = String(t).split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Z0-9_]+)=(.*)$/.exec(lines[i].trim())
if (!m) continue
out[m[1]] = m[2].replace(/^"|"$/g, '')
}
return out
}
function discordParseIdWhitelist(raw) {
const ids = Object.create(null)
const s = String(raw == null ? '' : raw).trim()
if (!s) return ids
const parts = s.split(',')
for (let i = 0; i < parts.length; i++) {
let id = String(parts[i] || '').trim()
if (!id) continue
if (id.charAt(0) === '<' && id.charAt(id.length - 1) === '>') {
id = id.slice(1, -1)
if (id.charAt(0) === '@') id = id.slice(1)
if (id.charAt(0) === '!') id = id.slice(1)
id = id.trim()
}
if (id) ids[id] = 1
}
return ids
}
function discordEnvLookup(ctx, keys) {
const maps = []
if (ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object') {
maps.push(ctx.vfs.env)
}
if (ctx && ctx.env && typeof ctx.env === 'object' && ctx.env !== maps[0]) {
maps.push(ctx.env)
}
for (let i = 0; i < maps.length; i++) {
const e = maps[i]
for (let j = 0; j < keys.length; j++) {
const v = e[keys[j]]
if (v != null && String(v).trim()) return String(v)
}
}
return ''
}
function discordWhitelistRaw(ctx) {
return discordEnvLookup(ctx, [
'DISCORD_ID_WHITELIST',
'BARE_OS_DISCORD_ID_WHITELIST'
])
}
function discordWhitelistCount(ctx) {
const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx))
let n = 0
for (const k in ids) {
if (Object.prototype.hasOwnProperty.call(ids, k)) n++
}
return n
}
/**
* User-installable profile app (Discord "Add App" → User Install).
* Default on. Set DISCORD_USER_INSTALL=0 to keep a guild-only bot.
*/
function discordUserInstallEnabled(ctx) {
const raw = String(
discordEnvLookup(ctx, ['DISCORD_USER_INSTALL', 'BARE_OS_DISCORD_USER_INSTALL']) ||
'1'
)
.trim()
.toLowerCase()
return raw !== '0' && raw !== 'false' && raw !== 'no' && raw !== 'off'
}
/**
* True when this interaction was authorized by a user-install (profile app)
* or used in Bot DM / private channel. Those surfaces are world-reachable
* once the app is user-installable — whitelist is mandatory.
*/
function discordIsUserInstallInteraction(interaction) {
if (!interaction) return false
const owners =
interaction.authorizingIntegrationOwners ||
interaction.authorizing_integration_owners
if (owners && typeof owners === 'object') {
if (
owners[BARE_OS_DISCORD_INTEGRATION_USER] != null ||
owners[String(BARE_OS_DISCORD_INTEGRATION_USER)] != null
) {
return true
}
}
const c = interaction.context
if (
c === BARE_OS_DISCORD_CONTEXT_BOT_DM ||
c === BARE_OS_DISCORD_CONTEXT_PRIVATE ||
c === String(BARE_OS_DISCORD_CONTEXT_BOT_DM) ||
c === String(BARE_OS_DISCORD_CONTEXT_PRIVATE)
) {
return true
}
return false
}
function discordStampUserInstallCommand(json, ctx) {
if (!json || typeof json !== 'object') return json
if (!discordUserInstallEnabled(ctx)) return json
json.integration_types = [
BARE_OS_DISCORD_INTEGRATION_GUILD,
BARE_OS_DISCORD_INTEGRATION_USER
]
json.contexts = [
BARE_OS_DISCORD_CONTEXT_GUILD,
BARE_OS_DISCORD_CONTEXT_BOT_DM,
BARE_OS_DISCORD_CONTEXT_PRIVATE
]
return json
}
function discordStampUserInstallCommands(body, ctx) {
const list = Array.isArray(body) ? body : []
for (let i = 0; i < list.length; i++) {
discordStampUserInstallCommand(list[i], ctx)
}
return list
}
function discordUserInstallAuthorizeUrl(appId) {
const id = String(appId || '').trim()
if (!id) return ''
return 'https://discord.com/oauth2/authorize?client_id=' + encodeURIComponent(id)
}
function discordUserInstallAppConfig() {
const cfg = {}
cfg[String(BARE_OS_DISCORD_INTEGRATION_GUILD)] = {
oauth2_install_params: {
scopes: ['applications.commands', 'bot'],
permissions: BARE_OS_DISCORD_GUILD_INSTALL_PERMISSIONS
}
}
cfg[String(BARE_OS_DISCORD_INTEGRATION_USER)] = {
oauth2_install_params: {
scopes: ['applications.commands']
}
}
return { integration_types_config: cfg }
}
async function discordEnableUserInstallApp(rest, Routes) {
if (!rest || typeof rest.patch !== 'function') return false
if (!Routes || typeof Routes.currentApplication !== 'function') return false
await rest.patch(Routes.currentApplication(), {
body: discordUserInstallAppConfig()
})
return true
}
/**
* User-install commands must be global. Also PUT guild commands when
* DISCORD_GUILD_ID is set so the home server updates immediately.
*/
async function discordPutSlashCommands(rest, Routes, appId, body, opts) {
opts = opts || {}
const guildId = String(opts.guildId || '').trim()
const userInstall = opts.userInstall !== false
const out = { global: false, guild: false }
if (!rest || typeof rest.put !== 'function' || !Routes || !appId) return out
if (userInstall || !guildId) {
if (typeof Routes.applicationCommands === 'function') {
await rest.put(Routes.applicationCommands(appId), { body: body })
out.global = true
}
}
if (guildId && typeof Routes.applicationGuildCommands === 'function') {
await rest.put(Routes.applicationGuildCommands(appId, guildId), {
body: body
})
out.guild = true
}
return out
}
function discordUserAllowed(ctx, userId, interaction) {
const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx))
let n = 0
for (const k in ids) {
if (Object.prototype.hasOwnProperty.call(ids, k)) n++
}
const id = String(userId == null ? '' : userId).trim()
const userInstallIx = discordIsUserInstallInteraction(interaction)
// Profile-install / DM / private-channel: never open to the world.
if (n === 0) return !userInstallIx
return Boolean(id && ids[id])
}
function discordInteractionUserId(interaction) {
if (!interaction) return ''
if (interaction.user && interaction.user.id) return String(interaction.user.id)
const member = interaction.member
if (member && member.user && member.user.id) return String(member.user.id)
if (member && member.id) return String(member.id)
return ''
}
function discordCallFlag(interaction, name) {
return Boolean(
interaction && typeof interaction[name] === 'function' && interaction[name]()
)
}
function discordIsAutocomplete(interaction) {
return discordCallFlag(interaction, 'isAutocomplete')
}
function discordIsModalSubmit(interaction) {
return discordCallFlag(interaction, 'isModalSubmit')
}
function discordIsMessageComponent(interaction) {
if (!interaction) return false
if (discordCallFlag(interaction, 'isMessageComponent')) return true
if (discordCallFlag(interaction, 'isButton')) return true
if (discordCallFlag(interaction, 'isAnySelectMenu')) return true
if (discordCallFlag(interaction, 'isStringSelectMenu')) return true
if (discordCallFlag(interaction, 'isUserSelectMenu')) return true
if (discordCallFlag(interaction, 'isRoleSelectMenu')) return true
if (discordCallFlag(interaction, 'isMentionableSelectMenu')) return true
if (discordCallFlag(interaction, 'isChannelSelectMenu')) return true
return false
}
function discordInteractionOwnerId(interaction) {
const msg = interaction && interaction.message
if (msg) {
const meta = msg.interactionMetadata || msg.interaction_metadata
if (meta && meta.user && meta.user.id) return String(meta.user.id)
if (msg.interaction && msg.interaction.user && msg.interaction.user.id) {
return String(msg.interaction.user.id)
}
const mid = msg.id != null ? String(msg.id) : ''
if (mid && BARE_OS_DISCORD_LIVE[mid] && BARE_OS_DISCORD_LIVE[mid].userId) {
return String(BARE_OS_DISCORD_LIVE[mid].userId)
}
}
return ''
}
function discordFilterChoices(items, q, toChoice) {
const needle = String(q || '').toLowerCase()
const out = []
for (let i = 0; i < items.length && out.length < 25; i++) {
const raw = items[i]
const choice = toChoice ? toChoice(raw) : { name: String(raw), value: String(raw) }
if (!choice || !choice.value) continue
const hay = (choice.name + ' ' + choice.value).toLowerCase()
if (needle && hay.indexOf(needle) < 0) continue
out.push({ name: String(choice.name).slice(0, 100), value: String(choice.value).slice(0, 100) })
}
return out
}
async function discordSuggestUnits(ctx, q) {
const names = BARE_OS_DISCORD_KNOWN_UNITS.slice()
if (typeof ctx.bareOsRunSystemctlCli === 'function') {
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
})
const lines = String(out || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const m = /^([A-Za-z0-9_@.:-]+)\b/.exec(lines[i].trim())
if (!m) continue
const n = m[1].replace(/\.service$/, '')
if (names.indexOf(n) < 0) names.push(n)
}
}
return discordFilterChoices(names, q)
}
async function discordSuggestMan(ctx, q) {
const db = await discordCmdReadJson(ctx, '/share/man/man.json')
const pages = db && Array.isArray(db.pages) ? db.pages : []
const names = []
for (let i = 0; i < pages.length; i++) {
if (pages[i] && pages[i].name) names.push(String(pages[i].name))
}
if (!names.length) {
names.push('uname', 'whoami', 'login', 'discord-bot', 'systemctl', 'help')
}
return discordFilterChoices(names, q)
}
function discordShKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordShGc() {
const now = Date.now()
for (const k in BARE_OS_DISCORD_SH_SESSIONS) {
const r = BARE_OS_DISCORD_SH_SESSIONS[k]
if (!r || now - r.atMs > BARE_OS_DISCORD_SH_TTL_MS) delete BARE_OS_DISCORD_SH_SESSIONS[k]
}
}
function discordShGet(interaction, ctx) {
discordShGc()
const key = discordShKey(interaction)
let rec = BARE_OS_DISCORD_SH_SESSIONS[key]
const env = discordCmdEnv(ctx)
const home = env.HOME || '/home/' + discordSessionUser(ctx)
if (!rec) {
rec = {
cwd: env.PWD || home || '~',
oldpwd: home,
hist: [],
lastCmd: '',
lastExit: 0,
lastMs: 0,
atMs: Date.now()
}
BARE_OS_DISCORD_SH_SESSIONS[key] = rec
}
rec.atMs = Date.now()
if (!rec.cwd) rec.cwd = env.PWD || home || '~'
return rec
}
function discordShPrettyCwd(ctx, cwd) {
const env = discordCmdEnv(ctx)
const home = env.HOME || '/home/' + discordSessionUser(ctx)
const c = String(cwd || '~')
if (c === '~' || c === home) return '~'
if (home && c.indexOf(home + '/') === 0) return '~' + c.slice(home.length)
return c
}
function discordShPrompt(ctx, rec) {
return (
discordSessionUser(ctx) +
'@' +
(discordCmdEnv(ctx).HOSTNAME || 'bare-os') +
':' +
discordShPrettyCwd(ctx, rec.cwd)
)
}
async function discordShPrepare(ctx, rec) {
const env = discordCmdEnv(ctx)
const cwd = rec.cwd || env.HOME || '~'
rec.cwd = cwd
env.PWD = cwd
if (ctx.env && ctx.env !== env) ctx.env.PWD = cwd
if (ctx.vfs && typeof ctx.vfs.chdir === 'function') {
try {
await ctx.vfs.chdir(cwd)
} catch {
/* keep env PWD */
}
}
}
function discordShHarvest(ctx, rec) {
const env = discordCmdEnv(ctx)
if (env.PWD) rec.cwd = String(env.PWD)
if (ctx.vfs && ctx.vfs.env && ctx.vfs.env.PWD) rec.cwd = String(ctx.vfs.env.PWD)
}
function discordShPushHist(rec, cmd, exit, ms, preview) {
rec.lastCmd = cmd
rec.lastExit = exit
rec.lastMs = ms
rec.hist = rec.hist.filter(function (h) {
return h && h.cmd !== cmd
})
rec.hist.unshift({
cmd: cmd,
exit: exit,
ms: ms,
preview: String(preview || '').slice(0, 80)
})
if (rec.hist.length > 25) rec.hist.length = 25
}
async function discordShCommandNames(ctx) {
const now = Date.now()
if (BARE_OS_DISCORD_SH_BINS && now - BARE_OS_DISCORD_SH_BINS_AT < 60000) {
return BARE_OS_DISCORD_SH_BINS
}
const set = Object.create(null)
for (let i = 0; i < BARE_OS_DISCORD_SH_BUILTINS.length; i++) {
set[BARE_OS_DISCORD_SH_BUILTINS[i]] = 1
}
for (const k in BARE_OS_DISCORD_RUN_ALLOW) {
if (Object.prototype.hasOwnProperty.call(BARE_OS_DISCORD_RUN_ALLOW, k) && k.indexOf(' ') < 0) {
set[k] = 1
}
}
if (ctx && ctx.shellAliases && typeof ctx.shellAliases === 'object') {
const aks = Object.keys(ctx.shellAliases)
for (let i = 0; i < aks.length; i++) set[aks[i]] = 1
}
try {
if (ctx && ctx.vfs && typeof ctx.vfs.readdir === 'function') {
const n = await ctx.vfs.readdir('/bin')
if (Array.isArray(n)) {
for (let i = 0; i < n.length; i++) set[String(n[i])] = 1
}
}
} catch {
/* ignore */
}
const man = await discordCmdReadText(ctx, '/share/man/man.json')
const j = discordTryJson(man)
if (j && Array.isArray(j.pages)) {
for (let i = 0; i < j.pages.length; i++) {
if (j.pages[i] && j.pages[i].name) set[String(j.pages[i].name)] = 1
}
}
const names = Object.keys(set).sort()
BARE_OS_DISCORD_SH_BINS = names
BARE_OS_DISCORD_SH_BINS_AT = now
return names
}
function discordShSplitLine(q) {
const s = String(q || '')
const m = /^(.*?)(\S*)$/.exec(s)
return { prefix: m ? m[1] : '', token: m ? m[2] : s }
}
function discordShFirstWord(q) {
const s = String(q || '').trim()
const i = s.search(/\s/)
return i < 0 ? s : s.slice(0, i)
}
async function discordShCompletePath(ctx, rec, token, dirsOnly) {
let dir = rec.cwd || '~'
let base = token
const slash = token.lastIndexOf('/')
if (slash >= 0) {
const head = token.slice(0, slash + 1)
base = token.slice(slash + 1)
if (head.charAt(0) === '/' || head.charAt(0) === '~') dir = head === '/' ? '/' : head.replace(/\/$/, '') || '/'
else dir = discordJoinPath(rec.cwd, head.replace(/\/$/, '') || '.')
}
let names = []
try {
if (ctx.vfs && typeof ctx.vfs.readdir === 'function') {
const n = await ctx.vfs.readdir(dir)
if (Array.isArray(n)) names = n
}
} catch {
names = []
}
const out = []
const want = String(base || '').toLowerCase()
for (let i = 0; i < names.length && out.length < 20; i++) {
const name = String(names[i])
if (want && name.toLowerCase().indexOf(want) !== 0) continue
const full = discordJoinPath(dir, name)
let isDir = false
try {
const st = await discordFmStat(ctx, full)
isDir = discordIsDirStat(st)
} catch {
isDir = false
}
if (dirsOnly && !isDir) continue
const shown = (slash >= 0 ? token.slice(0, slash + 1) : '') + name + (isDir ? '/' : '')
out.push({
label: shown,
value: shown,
description: isDir ? 'directory' : 'file'
})
}
return out
}
function discordShPushChoice(out, seen, name, value, description) {
const v = String(value || '').slice(0, 100)
if (!v || seen[v]) return
if (out.length >= 25) return
seen[v] = 1
out.push({
name: String(name || v).slice(0, 100),
value: v,
description: description ? String(description).slice(0, 100) : undefined
})
}
async function discordSuggestRun(ctx, interaction, q) {
const rec = discordShGet(interaction, ctx)
const raw = String(q || '')
const split = discordShSplitLine(raw)
const first = discordShFirstWord(raw)
const out = []
const seen = Object.create(null)
if (!raw.trim() || (!split.prefix && first === split.token)) {
for (let i = 0; i < rec.hist.length && out.length < 6; i++) {
const h = rec.hist[i]
if (!h || !h.cmd) continue
if (split.token && h.cmd.indexOf(split.token) !== 0) continue
discordShPushChoice(out, seen, '↵ ' + h.cmd, h.cmd, 'history · exit ' + h.exit)
}
const cmds = await discordShCommandNames(ctx)
const needle = split.token.toLowerCase()
for (let i = 0; i < cmds.length && out.length < 25; i++) {
if (needle && cmds[i].toLowerCase().indexOf(needle) !== 0) continue
const kind = BARE_OS_DISCORD_SH_TTY[cmds[i]]
? 'TUI (needs a real TTY)'
: ctx.shellAliases && ctx.shellAliases[cmds[i]]
? 'alias'
: BARE_OS_DISCORD_SH_BUILTINS.indexOf(cmds[i]) >= 0
? 'builtin'
: 'command'
discordShPushChoice(out, seen, cmds[i], cmds[i], kind)
}
return out
}
const flags = BARE_OS_DISCORD_SH_FLAGS[first]
if (split.token.charAt(0) === '-' && flags) {
for (let i = 0; i < flags.length && out.length < 25; i++) {
if (split.token && flags[i].indexOf(split.token) !== 0) continue
const line = (split.prefix + flags[i]).slice(0, 100)
discordShPushChoice(out, seen, line, line, first + ' flag')
}
return out
}
const pathish =
!split.token ||
split.token.charAt(0) === '.' ||
split.token.charAt(0) === '/' ||
split.token.charAt(0) === '~' ||
split.token.indexOf('/') >= 0 ||
first === 'cd' ||
first === 'ls' ||
first === 'cat' ||
first === 'rm' ||
first === 'mv' ||
first === 'cp' ||
first === 'mkdir' ||
first === 'stat' ||
first === 'head' ||
first === 'tail' ||
first === 'edit'
if (pathish) {
const paths = await discordShCompletePath(ctx, rec, split.token, first === 'cd')
for (let i = 0; i < paths.length && out.length < 25; i++) {
const line = (split.prefix + paths[i].value).slice(0, 100)
discordShPushChoice(out, seen, line, line, paths[i].description)
}
}
if (!out.length && split.token) {
const cmds = await discordShCommandNames(ctx)
const needle = split.token.toLowerCase()
for (let i = 0; i < cmds.length && out.length < 25; i++) {
if (cmds[i].toLowerCase().indexOf(needle) !== 0) continue
const line = (split.prefix + cmds[i]).slice(0, 100)
discordShPushChoice(out, seen, line, line, 'command')
}
}
return out
}
function discordFmKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordFmGc() {
const now = Date.now()
for (const k in BARE_OS_DISCORD_FM_SESSIONS) {
const rec = BARE_OS_DISCORD_FM_SESSIONS[k]
if (!rec || now - rec.atMs > BARE_OS_DISCORD_FM_TTL_MS) {
delete BARE_OS_DISCORD_FM_SESSIONS[k]
}
}
}
function discordFmGet(interaction, ctx) {
discordFmGc()
let rec = BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)]
if (!rec) {
rec = {
cwd: '~',
page: 0,
sel: '',
confirm: '',
atMs: Date.now()
}
BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)] = rec
}
rec.atMs = Date.now()
if (ctx) discordSyncSessionIdentity(ctx)
return rec
}
function discordJoinPath(dir, name) {
const n = String(name || '').replace(/^\/+/, '')
if (!n || n === '.') return dir || '~'
if (n === '..') return discordParentPath(dir)
const d = String(dir || '~').replace(/\/+$/, '') || '/'
if (d === '/') return '/' + n
if (d === '~') return '~/' + n
return d + '/' + n
}
function discordParentPath(p) {
const s = String(p || '~').replace(/\/+$/, '') || '/'
if (s === '/' || s === '~') return s
const i = s.lastIndexOf('/')
if (i <= 0) return s.charAt(0) === '~' ? '~' : '/'
const parent = s.slice(0, i)
return parent || (s.charAt(0) === '~' ? '~' : '/')
}
function discordBaseName(p) {
const s = String(p || '').replace(/\/+$/, '')
const i = s.lastIndexOf('/')
return i >= 0 ? s.slice(i + 1) : s
}
function discordIsDirStat(st) {
if (!st) return false
if (st.isDirectory === true) return true
if (st.type === 'directory') return true
return false
}
async function discordFmList(ctx, cwd) {
const p = discordCmdPathOk(cwd || '~') || '~'
if (typeof ctx.vfs?.readdir !== 'function') return { path: p, names: [], err: 'readdir unavailable' }
try {
const raw = await ctx.vfs.readdir(p)
const names = []
const list = Array.isArray(raw) ? raw : []
for (let i = 0; i < list.length; i++) {
const n = String(list[i] || '')
if (!n || n === '.bareos_empty') continue
names.push(n)
}
names.sort(function (a, b) {
return a < b ? -1 : a > b ? 1 : 0
})
return { path: p, names: names, err: '' }
} catch (err) {
return { path: p, names: [], err: (err && err.message) || String(err) }
}
}
async function discordFmStat(ctx, path) {
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
if (typeof stfn !== 'function') return null
try {
return await stfn.call(ctx.vfs, path)
} catch {
return null
}
}
async function discordFmView(ctx, interaction) {
const rec = discordFmGet(interaction, ctx)
const listing = await discordFmList(ctx, rec.cwd)
rec.cwd = listing.path
const names = listing.names
const pages = Math.max(1, Math.ceil(names.length / BARE_OS_DISCORD_FM_PAGE))
if (rec.page >= pages) rec.page = pages - 1
if (rec.page < 0) rec.page = 0
const start = rec.page * BARE_OS_DISCORD_FM_PAGE
const slice = names.slice(start, start + BARE_OS_DISCORD_FM_PAGE)
if (rec.sel && names.indexOf(rec.sel) < 0) rec.sel = ''
const writable = Boolean(discordCmdPathWriteOk(ctx, rec.cwd))
const fields = []
const options = []
if (rec.cwd !== '/' && rec.cwd !== '~') {
options.push({ label: '⬆ .. (parent)', value: '..', description: discordParentPath(rec.cwd) })
}
for (let i = 0; i < slice.length; i++) {
const name = slice[i]
const full = discordJoinPath(rec.cwd, name)
const st = await discordFmStat(ctx, full)
const dir = discordIsDirStat(st)
const size = st && st.size != null && !dir ? discordPrettyBytes(st.size) : dir ? 'dir' : ''
const mark = rec.sel === name ? '▸ ' : ''
fields.push(
discordField(
mark + (dir ? '📁 ' : '📄 ') + name,
size || (dir ? 'folder' : 'file'),
true
)
)
options.push({
label: (dir ? '📁 ' : '📄 ') + name,
value: name.slice(0, 100),
description: size || (dir ? 'open folder' : 'file')
})
}
const sel = discordSelect(
'fm:pick',
slice.length ? 'Open an entry…' : 'Empty folder',
options
)
const nav = discordButtons([
{ id: 'fm:up', label: 'Parent' },
{ id: 'fm:home', label: 'Home', style: 1 },
{ id: 'fm:goto', label: 'Go to…' },
{ id: 'fm:prev', label: '◀' },
{ id: 'fm:next', label: '▶' }
])
const acts = discordButtons(
writable
? [
{ id: 'fm:open', label: 'Open', style: 1 },
{ id: 'fm:edit', label: 'Edit' },
{ id: 'fm:new', label: 'New file', style: 3 },
{ id: 'fm:mkdir', label: 'New folder' },
{ id: 'fm:more', label: 'Manage…' }
]
: [
{ id: 'fm:open', label: 'Open', style: 1 },
{ id: 'nav:panel', label: 'Panel' }
]
)
const manage = rec.confirm
? discordButtons([
{ id: 'fm:ok', label: 'Confirm delete', style: 4 },
{ id: 'fm:no', label: 'Cancel', style: 2 }
])
: writable
? discordButtons([
{ id: 'fm:ren', label: 'Rename' },
{ id: 'fm:copy', label: 'Copy' },
{ id: 'fm:del', label: 'Delete', style: 4 },
{ id: 'fm:ref', label: 'Refresh' },
{ id: 'nav:panel', label: 'Panel' }
])
: discordButtons([{ id: 'fm:ref', label: 'Refresh' }])
const rows = [sel, nav, acts, manage].filter(Boolean)
let desc = '`' + rec.cwd + '` · ' + names.length + ' items · page ' + (rec.page + 1) + '/' + pages
desc += writable ? ' · writable' : ' · read-only'
if (rec.sel) desc += '\nSelected: **' + rec.sel + '**'
if (rec.confirm) desc += '\n**Delete `' + rec.confirm + '`?** This cannot be undone.'
if (listing.err) desc += '\n' + listing.err
return discordResult(
discordEmbed({
title: 'Files',
desc: desc,
fields: fields.slice(0, 20),
color: rec.confirm ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR,
footer: rec.cwd + ' · ' + discordSessionUser(ctx)
}),
{ components: rows, nav: false }
)
}
function discordSuggestEditPaths(ctx, q) {
const user = discordSessionUser(ctx)
const names = BARE_OS_DISCORD_EDIT_SUGGEST.concat([
'/home/' + user + '/notes.txt',
'/tmp/' + user + '.txt'
])
return discordFilterChoices(names, q)
}
function discordSuggestPaths(ctx, q) {
const e = discordCmdEnv(ctx)
const home = e.HOME || '/home/' + discordSessionUser(ctx)
const extra = [home, home + '/.discord', '/home/' + discordSessionUser(ctx)]
const names = BARE_OS_DISCORD_FS_SUGGEST.concat(extra)
return discordFilterChoices(names, q)
}
async function discordCmdStatus(ctx) {
const e = discordCmdEnv(ctx)
const os = await discordCmdOsRelease(ctx)
const user = discordSessionUser(ctx)
const ident = ctx && ctx.identity && ctx.identity.state ? String(ctx.identity.state) : e.BARE_OS_IDENTITY || 'guest'
return discordResult(
discordEmbed({
title: 'Bare OS session',
color: ident === 'unlocked' ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
author: { name: user + '@' + (e.HOSTNAME || e.NAME || 'bare-os') },
fields: [
discordField('OS', os.PRETTY_NAME || os.NAME || 'Bare OS', true),
discordField('User', user, true),
discordField('Identity', ident, true),
discordField('Home', e.HOME || '/home/' + user, true),
discordField('Shell', e.SHELL || '/bin/sh', true),
discordField('UID', e.UID || '—', true),
discordField('Version', os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || e.BARE_OS_RELEASE || '—', true),
discordField('Arch', e.BARE_OS_ARCH || e.MACHINE || '—', true),
discordField('Booter', e.BARE_OS_BOOTER_PACKAGE_VERSION || '—', true)
]
})
)
}
async function discordCmdUname(ctx) {
const e = discordCmdEnv(ctx)
const os = await discordCmdOsRelease(ctx)
const line = [
os.NAME || 'BareOS',
e.HOSTNAME || 'bare-os',
os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || '0.1',
e.BARE_OS_ARCH || e.MACHINE || 'unknown',
os.PRETTY_NAME || e.BARE_OS_BUILD || 'bare-userland'
].join(' ')
return discordResult(
discordEmbed({
title: 'uname',
desc: discordCmdFence(line)
})
)
}
async function discordCmdHandleBare(ctx, sub) {
const e = discordCmdEnv(ctx)
const user = discordSessionUser(ctx)
if (sub === 'ping') {
return discordResult(
discordEmbed({
title: 'Pong',
color: BARE_OS_DISCORD_COLOR_OK,
desc: 'Bare OS is online as **' + user + '**.'
})
)
}
if (sub === 'about') {
return discordResult(
discordEmbed({
title: 'Bare OS Discord bot',
desc:
'Slash control surface for **this logged-in session** (`' +
user +
'`). Commands run with that users `HOME`, VFS, and `systemctl`.'
})
)
}
if (sub === 'help') {
return discordResult(
discordEmbed({
title: 'Commands',
desc: 'Running as **' + user + '**. Open **Menu** for destinations, or use a slash command.',
fields: [
discordField('/bare', 'Session — status, whoami, hostname, date, uptime, motd, uname'),
discordField('/sys', 'Host — disk, memory, processes, env, doctor, features, limits'),
discordField('/svc', 'Services — list, start, stop, restart, logs'),
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
discordField('/net', 'Swarm — peers, summary'),
discordField('/files /edit /create /upload', 'Browse, edit, or wget an attachment into `~/` `/tmp`'),
discordField('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'),
discordField('/r /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins'),
discordField('/hdms', 'Hyperdrives — list, create, add, invite, pair, /mnt'),
discordField('/holesail', 'Tunnels — list, add, edit, start/stop, enable, service'),
discordField('/agent', 'Guest AI agent — ask, status, reset (when configured)')
]
})
)
}
if (sub === 'status') return discordCmdStatus(ctx)
if (sub === 'uname') return discordCmdUname(ctx)
if (sub === 'whoami') {
return discordResult(
discordEmbed({
title: 'Whoami',
color: BARE_OS_DISCORD_COLOR_OK,
fields: [
discordField('User', user, true),
discordField('Home', '`' + (e.HOME || '/home/' + user) + '`', true),
discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true)
]
})
)
}
if (sub === 'hostname') {
return discordResult(
discordEmbed({
title: 'Hostname',
desc: '`' + String(e.HOSTNAME || e.NAME || 'bare-os') + '`'
})
)
}
if (sub === 'date') {
return discordResult(
discordEmbed({
title: 'Date',
desc: '`' + new Date().toISOString() + '`'
})
)
}
if (sub === 'uptime') {
const t = await discordCmdReadText(ctx, '/proc/uptime')
return discordResult(
discordEmbed({
title: 'Uptime',
fields: [
discordField('Session', t ? discordPrettyUptime(t) : 'unavailable', true)
]
})
)
}
if (sub === 'motd') {
const t = await discordCmdReadText(ctx, '/etc/motd')
if (!t) {
return discordResult(discordEmbed({ title: 'motd', desc: 'No `/etc/motd`.' }))
}
return { more: { title: 'motd', body: t, fence: true } }
}
return { text: 'unknown /bare subcommand', ephemeral: true }
}
function discordFormatHostDf(obj) {
const host = obj && obj.host && typeof obj.host === 'object' ? obj.host : obj
const sv = obj && obj.statvfs && typeof obj.statvfs === 'object' ? obj.statvfs : null
const fields = []
if (host) {
if (host.platform || host.arch) {
fields.push(
discordField(
'Host',
[host.platform, host.arch, host.hostname || host.machine]
.filter(Boolean)
.join(' · '),
true
)
)
}
if (host.totalmem != null) {
const used =
host.freemem != null ? Number(host.totalmem) - Number(host.freemem) : null
fields.push(discordField('RAM', discordPrettyBytes(host.totalmem), true))
if (host.freemem != null) {
fields.push(discordField('Free RAM', discordPrettyBytes(host.freemem), true))
}
if (used != null && host.totalmem) {
const pct = Math.round((used / Number(host.totalmem)) * 100)
fields.push(discordField('RAM used', pct + '%', true))
}
}
if (host.availableParallelism != null) {
fields.push(discordField('CPUs', String(host.availableParallelism), true))
}
if (host.release) fields.push(discordField('Release', host.release, true))
}
if (obj && obj.peers != null) fields.push(discordField('Peers', String(obj.peers), true))
if (sv) {
fields.push(discordField('Volume', sv.volume_class || sv.f_basetype || '—', true))
if (sv.pwd_logical) fields.push(discordField('PWD', sv.pwd_logical, true))
const bsize = Number(sv.f_frsize || sv.f_bsize || 4096)
if (sv.f_blocks != null) {
fields.push(discordField('System size', discordPrettyBytes(Number(sv.f_blocks) * bsize), true))
}
if (sv.f_bavail != null) {
fields.push(discordField('System avail', discordPrettyBytes(Number(sv.f_bavail) * bsize), true))
}
const pers = sv.personalDrive
if (pers && pers.f_blocks != null) {
const pb = Number(pers.f_frsize || pers.f_bsize || bsize)
fields.push(
discordField('Personal size', discordPrettyBytes(Number(pers.f_blocks) * pb), true)
)
}
}
if (obj && obj.pipeline && obj.pipeline.maxBytes != null) {
fields.push(discordField('Pipe cap', discordPrettyBytes(obj.pipeline.maxBytes), true))
}
return fields
}
function discordFormatMem(raw) {
const json = discordTryJson(raw)
if (json && json.host) {
return discordFormatHostDf(json)
}
const mi = discordParseMeminfo(raw)
const fields = []
const pick = [
['MemTotal', 'Total'],
['MemFree', 'Free'],
['MemAvailable', 'Available'],
['Buffers', 'Buffers'],
['Cached', 'Cached'],
['SwapTotal', 'Swap'],
['SwapFree', 'Swap free']
]
for (let i = 0; i < pick.length; i++) {
if (mi[pick[i][0]] != null) {
fields.push(discordField(pick[i][1], discordPrettyBytes(mi[pick[i][0]]), true))
}
}
if (mi.MemTotal && mi.MemAvailable != null) {
const pct = Math.round((1 - mi.MemAvailable / mi.MemTotal) * 100)
fields.push(discordField('Used', pct + '%', true))
}
return fields
}
function discordFormatRlimits(obj) {
const lim = obj && obj.rlimits && typeof obj.rlimits === 'object' ? obj.rlimits : obj
const fields = []
if (!lim || typeof lim !== 'object') return fields
const names = {
NOFILE: 'Open files',
NPROC: 'Processes',
AS: 'Address space',
DATA: 'Data',
STACK: 'Stack',
CORE: 'Core dump',
RSS: 'RSS',
CPU: 'CPU time',
FSIZE: 'File size',
NOVM: 'No VM',
MEMLOCK: 'Locked mem'
}
const keys = Object.keys(lim)
for (let i = 0; i < keys.length && fields.length < 18; i++) {
const raw = keys[i]
const v = lim[raw]
if (v && typeof v === 'object' && (v.cur != null || v.max != null)) {
const short = raw.replace(/^RLIMIT_/, '')
const label = names[short] || discordHumanLabel(short)
const cur = discordIsByteishKey(raw) ? discordPrettyBytes(v.cur) : String(v.cur)
const max = discordIsByteishKey(raw) ? discordPrettyBytes(v.max) : String(v.max)
fields.push(discordField(label, cur + ' / ' + max, true))
}
}
if (obj && obj.bareOsExecMaxDepth != null) {
fields.push(discordField('Exec depth', String(obj.bareOsExecMaxDepth), true))
}
return fields
}
function discordFormatFeatures(obj) {
const feat =
obj && obj.features && typeof obj.features === 'object' ? obj.features : obj
if (!feat || typeof feat !== 'object') return []
const on = []
const off = []
const keys = Object.keys(feat).sort()
for (let i = 0; i < keys.length; i++) {
const v = feat[keys[i]]
if (v === true || v === 1 || v === '1') on.push('`' + keys[i] + '`')
else if (v === false || v === 0 || v === '0') off.push('`' + keys[i] + '`')
}
const fields = []
if (on.length) fields.push(discordField('On', on.slice(0, 24).join(' ')))
if (off.length) fields.push(discordField('Off', off.slice(0, 16).join(' ')))
return fields
}
function discordFormatDoctor(obj) {
if (!obj || typeof obj !== 'object') {
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR_WARN }
}
const pa = obj.peerAdmission && typeof obj.peerAdmission === 'object' ? obj.peerAdmission : {}
const hn = obj.hostnameMutation && typeof obj.hostnameMutation === 'object' ? obj.hostnameMutation : {}
const kh = obj.keyHandlePolicy && typeof obj.keyHandlePolicy === 'object' ? obj.keyHandlePolicy : {}
const fields = []
if (pa.allowlistConfigured != null) {
fields.push(discordField('Peer allowlist', pa.allowlistConfigured ? 'configured' : 'open', true))
}
if (pa.denylistConfigured != null) {
fields.push(discordField('Peer denylist', pa.denylistConfigured ? 'configured' : 'none', true))
}
if (pa.requireCapsConfigured != null) {
const n = pa.requireCapsTokenCount
fields.push(
discordField(
'Require caps',
pa.requireCapsConfigured ? (n ? n + ' tokens' : 'yes') : 'no',
true
)
)
}
if (hn.enabled != null) {
fields.push(discordField('Hostname set', hn.enabled ? 'allowed' : 'locked', true))
}
if (kh.defaultTtlMs) {
fields.push(discordField('Key TTL', discordPrettyValue('defaultTtlMs', kh.defaultTtlMs), true))
}
if (Array.isArray(obj.mfaExtensionPoints) && obj.mfaExtensionPoints.length) {
fields.push(discordField('MFA hooks', obj.mfaExtensionPoints.join(' · '), false))
}
const bits = []
if (pa.allowlistConfigured) bits.push('peer allowlist on')
else if (pa.allowlistConfigured === false) bits.push('peer allowlist **open**')
if (hn.enabled === true) bits.push('hostname mutation on')
else if (hn.enabled === false) bits.push('hostname locked')
return {
fields: fields,
desc: bits.join(' · '),
color: pa.allowlistConfigured ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
}
}
async function discordCmdHandleSys(ctx, sub) {
if (sub === 'df') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os_resources')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/resources')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatHostDf(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Disk / host',
fields: fields
})
)
}
return discordPrettySnapshot('Disk / host', t)
}
if (sub === 'mem') {
const t =
(await discordCmdReadText(ctx, '/proc/meminfo')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
const fields = discordFormatMem(t)
if (fields.length) {
return discordResult(discordEmbed({ title: 'Memory', fields: fields }))
}
return discordPrettySnapshot('Memory', t)
}
if (sub === 'ps') {
const table = await discordCmdReadJson(ctx, '/proc/bare_os/process_table.json')
const rows = table && Array.isArray(table.processes) ? table.processes : []
const fields = []
for (let i = 0; i < Math.min(rows.length, 18); i++) {
const r = rows[i] || {}
const name = String(r.name || r.comm || r.cmd || r.id || 'proc')
const st = String(r.state || r.status || '')
const pid = String(r.pid || r.id || i)
fields.push(discordField(name, 'pid ' + pid + (st ? ' · ' + st : ''), true))
}
return discordResult(
discordEmbed({
title: 'Processes · ' + rows.length,
desc: rows.length ? rows.length + ' logical processes in this session.' : 'No logical processes in the session table.',
fields: fields
})
)
}
if (sub === 'env') {
const e = discordCmdEnv(ctx)
const fields = [
discordField('User', e.USER, true),
discordField('Home', e.HOME, true),
discordField('Identity', e.BARE_OS_IDENTITY, true),
discordField('Shell', e.SHELL, true),
discordField('Pwd', e.PWD, true),
discordField(
'UID / GID',
e.UID || e.GID ? String(e.UID || '—') + ' / ' + String(e.GID || '—') : '',
true
)
]
const prefer = [
'HOSTNAME',
'PATH',
'TERM',
'TZ',
'LANG',
'EDITOR',
'PAGER',
'BARE_OS_THEME',
'BARE_OS_COLOR_DEPTH'
]
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|KEY|PRIVATE|CREDENTIAL|^USER$|^HOME$|^SHELL$|^PWD$|^LOGNAME$|^UID$|^GID$|^GROUP$|^BARE_OS_IDENTITY$/i
const seen = Object.create(null)
for (let i = 0; i < prefer.length; i++) {
const k = prefer[i]
seen[k] = 1
if (e[k] == null || e[k] === '') continue
fields.push(discordField(discordHumanLabel(k.replace(/^BARE_OS_/, '')), String(e[k]).slice(0, 80), true))
}
const keys = Object.keys(e).sort()
for (let i = 0; i < keys.length && fields.length < 15; i++) {
if (seen[keys[i]] || skip.test(keys[i])) continue
if (e[keys[i]] == null || e[keys[i]] === '') continue
fields.push(discordField(keys[i], String(e[keys[i]]).slice(0, 72), true))
}
return discordResult(
discordEmbed({
title: 'Environment',
desc: 'Session env for **' + discordSessionUser(ctx) + '** (secrets omitted).',
fields: fields
}),
{ ephemeral: true }
)
}
if (sub === 'doctor') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/security_posture.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/debug.json'))
const obj = discordTryJson(t)
const fmt = obj ? discordFormatDoctor(obj) : null
if (fmt && fmt.fields.length) {
return discordResult(
discordEmbed({
title: 'Doctor',
desc: fmt.desc,
fields: fmt.fields,
color: fmt.color
})
)
}
return discordPrettySnapshot('Doctor', t, { color: fmt && fmt.color })
}
if (sub === 'features') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/features')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_features')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/features.json'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatFeatures(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Features',
desc: 'Guest capability flags on this booter.',
fields: fields
})
)
}
return discordPrettySnapshot('Features', t)
}
if (sub === 'rlimits') {
const t = await discordCmdReadText(ctx, '/proc/bare_os/rlimits.json')
const obj = discordTryJson(t)
const fields = obj ? discordFormatRlimits(obj) : []
if (fields.length) {
return discordResult(
discordEmbed({
title: 'Resource limits',
desc: 'Soft / hard · Bare OS runtime caps (not Linux rlimits).',
fields: fields
})
)
}
return discordPrettySnapshot('Resource limits', t)
}
return { text: 'unknown /sys subcommand', ephemeral: true }
}
async function discordCmdHandleSvc(ctx, sub, unit) {
const name = String(unit || '').replace(/\.service$/, '')
if (typeof ctx.bareOsRunSystemctlCli !== 'function') {
return { text: 'systemctl is not available on this ctx', ephemeral: true }
}
if (sub === 'list') {
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
})
const parsed = discordParseSystemctlList(out)
const fields = []
for (let i = 0; i < parsed.length && fields.length < 20; i++) {
const r = parsed[i]
const bits = []
if (r.active) bits.push('**' + r.active + '**')
if (r.sub && r.sub !== r.active) bits.push(r.sub)
if (r.preset && r.preset !== 'enabled') bits.push(r.preset)
fields.push(discordField(r.unit, bits.join(' · ') || 'unknown', true))
}
const units = await discordSuggestUnits(ctx, '')
const sel = discordSelect(
'svc:pick',
'Inspect a unit…',
units.map(function (c) {
return { label: c.name, value: c.value }
})
)
return discordResult(
discordEmbed({
title: 'Services · ' + (parsed.length || 0),
desc: parsed.length
? 'Pick a unit to inspect, start, or stop.'
: out
? discordCmdClip(out, 800)
: 'No units registered in this session.',
fields: fields,
color: BARE_OS_DISCORD_COLOR
}),
{ components: sel ? [sel] : [] }
)
}
if (!name) return { text: 'unit name required — pick from autocomplete', ephemeral: true }
const argv = ['systemctl', sub === 'logs' ? 'status' : sub, name]
if (sub === 'logs') {
return discordCmdHandleJournal(ctx, name)
}
const out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(argv)
})
const act = discordButtons([
{ id: 'svc:status:' + name, label: 'Status', style: 1 },
{ id: 'svc:start:' + name, label: 'Start', style: 3 },
{ id: 'svc:stop:' + name, label: 'Stop', style: 4 },
{ id: 'svc:restart:' + name, label: 'Restart' },
{ id: 'svc:logs:' + name, label: 'Logs' }
])
const color =
sub === 'stop'
? BARE_OS_DISCORD_COLOR_WARN
: sub === 'start' || sub === 'restart'
? BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR
if (!out) {
return discordResult(
discordEmbed({
title: name,
desc: 'No output from `systemctl ' + sub + '`.',
color: color
}),
{ components: act ? [act] : [] }
)
}
return {
more: {
title: name,
lead: '`' + sub + '`',
body: discordCmdRedact(out),
fence: true,
color: color,
components: act ? [act] : []
}
}
}
async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
const p = discordCmdPathOk(rawPath || '~')
if (!p) {
return {
text: 'path not allowed (no `..`; stay under /proc /etc /var/log /run /home ~ /share)',
ephemeral: true
}
}
if (sub === 'ls') {
if (typeof ctx.vfs?.readdir !== 'function') return { text: 'readdir unavailable', ephemeral: true }
try {
const names = await ctx.vfs.readdir(p)
const list = Array.isArray(names) ? names : []
const shown = list.slice(0, 60)
const desc = shown.length
? shown
.map(function (n) {
return '`' + String(n).replace(/`/g, "'") + '`'
})
.join(' ')
: '(empty)'
return {
more: {
title: 'Listing',
lead:
'`' +
p +
'` · ' +
list.length +
' ' +
(list.length === 1 ? 'entry' : 'entries'),
body: desc === '(empty)' ? '' : shown.join('\n'),
fence: desc !== '(empty)',
footer: discordSessionUser(ctx)
}
}
} catch (err) {
return { text: 'ls failed: ' + ((err && err.message) || err), ephemeral: true }
}
}
if (sub === 'stat') {
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
if (typeof stfn !== 'function') return { text: 'stat unavailable', ephemeral: true }
try {
const st = await stfn.call(ctx.vfs, p)
const fields = []
const kind = st.isDirectory
? 'directory'
: st.isFile
? 'file'
: st.isSymbolicLink
? 'symlink'
: st.type || 'entry'
fields.push(discordField('Type', String(kind), true))
if (st.size != null) fields.push(discordField('Size', discordPrettyBytes(st.size), true))
if (st.mode != null) fields.push(discordField('Mode', String(st.mode), true))
if (st.mtime || st.mtimeMs) {
const ms = st.mtimeMs || Date.parse(st.mtime)
fields.push(
discordField('Modified', Number.isFinite(ms) ? new Date(ms).toISOString() : String(st.mtime), true)
)
}
if (st.path && st.path !== p) fields.push(discordField('Resolved', String(st.path), false))
return discordResult(
discordEmbed({
title: 'Stat',
desc: '`' + p + '`',
fields: fields.slice(0, 12)
})
)
} catch (err) {
return { text: 'stat failed: ' + ((err && err.message) || err), ephemeral: true }
}
}
const text = await discordCmdReadText(ctx, p)
if (!text) {
return discordResult(discordEmbed({ title: 'File', desc: '`' + p + '` is empty or unreadable.' }))
}
const writable = Boolean(discordCmdPathWriteOk(ctx, p))
const editRow = writable
? discordButtons([{ id: 'edit:open', label: 'Edit in modal', style: 1 }])
: null
const extra = {
components: editRow ? [editRow] : [],
editPath: writable ? p : ''
}
const asJson = discordTryJson(text)
if (asJson && typeof asJson === 'object' && !Array.isArray(asJson)) {
return discordPrettySnapshot(p, asJson, extra)
}
if (sub === 'head') {
const n = Math.max(1, Math.min(80, Number(nlines) || 12))
const head = text.split(/\r?\n/).slice(0, n).join('\n')
return {
more: {
title: 'Head',
lead: '`' + p + '` · first ' + n + ' lines',
body: head,
fence: true,
components: extra.components,
editPath: extra.editPath
}
}
}
return {
more: {
title: 'File',
lead: '`' + p + '`',
body: text.slice(0, BARE_OS_DISCORD_FS_MAX),
fence: true,
footer: text.length + ' bytes · ' + discordSessionUser(ctx),
components: extra.components,
editPath: extra.editPath
}
}
}
function discordFormatNetSummary(obj) {
if (!obj || typeof obj !== 'object') {
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR }
}
const rq =
obj.replicationQueue && typeof obj.replicationQueue === 'object' ? obj.replicationQueue : {}
const st = obj.stagingSlot && typeof obj.stagingSlot === 'object' ? obj.stagingSlot : {}
const fields = []
const peers = obj.peerCount
const role = obj.seedRole || rq.role || st.role
const proto = rq.protocol || st.protocol || obj.protocol
if (peers != null) fields.push(discordField('Peers', String(peers), true))
if (role) fields.push(discordField('Role', String(role), true))
if (proto) fields.push(discordField('Protocol', '`' + String(proto) + '`', true))
if (obj.topicHex) {
const hex = String(obj.topicHex)
fields.push(
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
)
}
if (obj.replicationQueueDepth != null) {
fields.push(discordField('Queue depth', String(obj.replicationQueueDepth), true))
}
if (rq.queueDepthEstimate != null) {
fields.push(discordField('Queue estimate', String(rq.queueDepthEstimate), true))
}
if (rq.hypercoreLengthHint != null) {
fields.push(discordField('Core length', String(rq.hypercoreLengthHint), true))
}
if (rq.manifestPathCount != null) {
fields.push(discordField('Manifests', String(rq.manifestPathCount), true))
}
if (rq.localRamBlockCount != null) {
fields.push(discordField('RAM blocks', String(rq.localRamBlockCount), true))
}
if (st.activeSlot) fields.push(discordField('Active slot', String(st.activeSlot), true))
const fwA = obj.peerFirewallAcceptedTotal
const fwR = obj.peerFirewallRejectedTotal
const fwI = obj.peerFirewallInboundTotal
const fwO = obj.peerFirewallOutboundTotal
if (fwA != null || fwR != null || fwI != null || fwO != null) {
fields.push(
discordField(
'Firewall',
'accept ' +
(fwA == null ? '0' : fwA) +
' · reject ' +
(fwR == null ? '0' : fwR) +
'\nin ' +
(fwI == null ? '0' : fwI) +
' · out ' +
(fwO == null ? '0' : fwO),
true
)
)
}
if (obj.seedHandshakeError) {
fields.push(discordField('Handshake', String(obj.seedHandshakeError), false))
}
const peerN = Number(peers)
const head = []
if (Number.isFinite(peerN)) head.push('**' + peerN + '** peer' + (peerN === 1 ? '' : 's'))
if (role) head.push(String(role))
if (proto) head.push('`' + proto + '`')
let desc = head.join(' · ')
const note = rq.snapshotWorkflowNote || obj.note
if (note) desc += (desc ? '\n' : '') + '*' + String(note).slice(0, 220) + '*'
const at = rq.atMs || obj.atMs
return {
fields: fields,
desc: desc,
color: Number.isFinite(peerN) && peerN > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
footer: at ? 'Updated ' + new Date(Number(at)).toISOString() + ' · expires after 2m idle' : ''
}
}
function discordFormatSwarm(obj) {
if (!obj || typeof obj !== 'object') return []
const fields = []
if (obj.peerCount != null) fields.push(discordField('Peers', String(obj.peerCount), true))
if (obj.protocol) fields.push(discordField('Protocol', '`' + String(obj.protocol) + '`', true))
if (obj.topicCount != null) fields.push(discordField('Topics', String(obj.topicCount), true))
if (obj.topicHex) {
const hex = String(obj.topicHex)
fields.push(
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
)
}
const lc = obj.lifecycle
if (lc && typeof lc === 'object') {
const bits = []
const lk = Object.keys(lc)
for (let i = 0; i < lk.length && bits.length < 6; i++) {
if (typeof lc[lk[i]] === 'object' || discordValueEmpty(lc[lk[i]])) continue
bits.push('**' + discordHumanLabel(lk[i]) + '** ' + discordPrettyValue(lk[i], lc[lk[i]]))
}
if (bits.length) fields.push(discordField('Lifecycle', bits.join('\n'), false))
}
const sc = obj.peerScoringAggregates
if (sc && typeof sc === 'object') {
const banned = sc.bannedActiveCount
const hi = sc.highLatencyEwmaCount
const low = sc.lowSuccessRateBucketCount
if (banned != null || hi != null || low != null) {
fields.push(
discordField(
'Scoring',
'banned ' +
String(banned || 0) +
' · high-lat ' +
String(hi || 0) +
' · low-ok ' +
String(low || 0),
true
)
)
}
}
if (Array.isArray(obj.peers) && obj.peers.length) {
fields.push(discordField('Peer list', String(obj.peers.length), true))
}
return fields
}
async function discordCmdHandleNet(ctx, sub) {
if (sub === 'peers' || sub === 'swarm') {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/swarm')) ||
(await discordCmdReadText(ctx, '/proc/bare_os/swarm.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_swarm'))
const obj = discordTryJson(t)
const fields = obj ? discordFormatSwarm(obj) : []
if (fields.length) {
const n = obj && obj.peerCount
return discordResult(
discordEmbed({
title: 'Swarm',
desc:
n != null
? '**' + n + '** peer' + (Number(n) === 1 ? '' : 's') + (obj.protocol ? ' · `' + obj.protocol + '`' : '')
: '',
fields: fields,
color: obj && obj.peerCount > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
})
)
}
return discordPrettySnapshot('Swarm', t)
}
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/net_summary.json')) ||
(await discordCmdReadText(ctx, '/proc/net/dev'))
const obj = discordTryJson(t)
if (obj) {
const fmt = discordFormatNetSummary(obj)
if (fmt.fields.length) {
return discordResult(
discordEmbed({
title: 'Network',
desc: fmt.desc,
fields: fmt.fields,
color: fmt.color,
footer: fmt.footer || undefined
})
)
}
return discordPrettySnapshot('Network', obj)
}
if (t && t.indexOf('Inter-|') >= 0) {
const lines = t.split(/\r?\n/).filter(Boolean)
return discordResult(
discordEmbed({
title: 'Network interfaces',
desc: discordCmdFence(lines.slice(0, 16).join('\n'))
})
)
}
return discordPrettySnapshot('Network', t)
}
async function discordCmdHandleMan(ctx, page) {
const name = String(page || '').replace(/[^a-zA-Z0-9._+-]/g, '')
if (!name) return { text: 'man page name required — start typing for autocomplete', ephemeral: true }
if (typeof ctx.execLine === 'function') {
const out = await discordCmdCapture(ctx, function () {
return ctx.execLine('man ' + name)
})
if (out) {
const clean = String(out)
.replace(/\x1b\[[0-9;]*m/g, '')
.trim()
return {
more: {
title: 'man ' + name,
body: clean,
fence: false
}
}
}
}
const t = await discordCmdReadText(ctx, '/share/man/man.json')
if (!t) return { text: 'man database unavailable' }
try {
const db = JSON.parse(t)
const pages = (db && db.pages) || []
for (let i = 0; i < pages.length; i++) {
if (pages[i] && pages[i].name === name) {
const p = pages[i]
return {
more: {
title: 'man ' + (p.title || name),
lead: p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`' : '',
body: String(p.description || ''),
fence: false
}
}
}
}
} catch {
/* fall through */
}
return { text: 'no man page for ' + name, ephemeral: true }
}
function discordCmdSayBox(text) {
const s = String(text || '').slice(0, 200)
const lines = s.split(/\r?\n/).slice(0, 6)
let w = 8
for (let i = 0; i < lines.length; i++) {
if (lines[i].length > w) w = lines[i].length
}
if (w > 48) w = 48
const bar = '+' + Array(w + 3).join('-') + '+'
const body = lines.map(function (ln) {
const t = ln.slice(0, w)
return '| ' + t + Array(w - t.length + 1).join(' ') + ' |'
})
return [bar, body.join('\n'), bar, ' \\', ' cow-ish · bare-os'].join('\n')
}
function discordShHud(ctx, interaction, note) {
const rec = discordShGet(interaction, ctx)
const fields = [
discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true),
discordField('Last exit', rec.lastCmd ? String(rec.lastExit) : '—', true),
discordField('User', discordSessionUser(ctx), true)
]
if (rec.lastCmd) {
fields.push(discordField('Last command', '`' + rec.lastCmd + '`', false))
}
const hist = rec.hist.slice(0, 6)
if (hist.length) {
fields.push(
discordField(
'History',
hist
.map(function (h) {
return '`' + h.cmd + '` · ' + h.exit
})
.join('\n')
)
)
}
const histSel = discordSelect(
'sh:hist',
'Replay a command…',
rec.hist.slice(0, 20).map(function (h, i) {
return {
label: h.cmd.slice(0, 90),
value: String(i),
description: 'exit ' + h.exit + (h.ms ? ' · ' + h.ms + 'ms' : '')
}
})
)
const acts = discordButtons([
{ id: 'sh:again', label: 'Repeat', style: 1 },
{ id: 'sh:up', label: 'cd ..' },
{ id: 'sh:home', label: 'cd ~' },
{ id: 'sh:pwd', label: 'pwd' }
])
return discordResult(
discordEmbed({
title: 'Shell · ' + discordShPrompt(ctx, rec),
desc:
(note ? note + '\n' : '') +
'Type **`/r cmd:`** and keep going — `cd`, pipes, `&&`, redirects, and `$VAR` all work.\n' +
'Working directory sticks for this Discord user.',
fields: fields,
color: rec.lastCmd && rec.lastExit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: discordShPrompt(ctx, rec) + ' · /r cmd:'
}),
{ components: [histSel, acts].filter(Boolean) }
)
}
async function discordCmdHandleRun(ctx, interaction, raw) {
const rec = discordShGet(interaction, ctx)
const cmd = String(raw || '').trim()
if (!cmd) return discordShHud(ctx, interaction, '')
if (cmd.length > 4000) {
return { text: 'Command line is too long (max 4000).', ephemeral: true }
}
const first = discordShFirstWord(cmd)
if (first === 'exit') {
rec.lastCmd = cmd
rec.lastExit = 0
return discordShHud(ctx, interaction, 'Stayed attached — `exit` does not stop the Discord bot.')
}
if (first === 'logout' || first === 'login') {
return {
text:
'`login` / `logout` need the guest TTY. This Discord shell stays as **' +
discordSessionUser(ctx) +
'**.',
ephemeral: true
}
}
if (typeof ctx.execLine !== 'function') {
return { text: 'execLine unavailable in this session', ephemeral: true }
}
await discordShPrepare(ctx, rec)
const t0 = Date.now()
let out = ''
let errNote = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.execLine(cmd, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
})
} catch (err) {
errNote = String((err && err.message) || err)
out = (out ? out + '\n' : '') + errNote
}
const ms = Date.now() - t0
const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0
discordShHarvest(ctx, rec)
discordShPushHist(rec, cmd, exit, ms, out)
const tty = BARE_OS_DISCORD_SH_TTY[first]
if (!String(out || '').trim() && (first === 'cd' || first === 'export' || first === 'unset' || first === 'alias')) {
return discordShHud(ctx, interaction, '`$ ' + cmd + '` · exit **' + exit + '** · ' + ms + 'ms')
}
const parsed = discordTryJson(out)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && String(out).length < 800) {
return discordPrettySnapshot(discordShPrompt(ctx, rec) + ' $ ' + cmd, parsed, {
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK
})
}
const lead =
'`' +
discordShPrompt(ctx, rec) +
'`\n`$ ' +
cmd +
'`' +
(tty ? '\n_Interactive TUI — needs a real terminal; output may be empty._' : '')
if (!String(out || '').trim()) {
return discordResult(
discordEmbed({
title: 'Shell',
desc: lead + '\n(no output)',
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd)
}),
{
components: [
discordButtons([
{ id: 'sh:again', label: 'Repeat', style: 1 },
{ id: 'sh:hud', label: 'Shell' }
])
]
}
)
}
return {
more: {
title: 'Shell',
lead: lead,
body: discordCmdRedact(out),
fence: true,
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd),
components: [
discordButtons([
{ id: 'sh:again', label: 'Repeat', style: 1 },
{ id: 'sh:up', label: 'cd ..' },
{ id: 'sh:home', label: 'cd ~' },
{ id: 'sh:hud', label: 'Shell' }
])
]
}
}
}
async function discordShAction(ctx, interaction, id) {
const rec = discordShGet(interaction, ctx)
if (id === 'sh:hud' || id === 'run:compose') {
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
}
if (id === 'sh:pwd') {
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'pwd'))
}
if (id === 'sh:again') {
if (!rec.lastCmd) {
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, 'No command to repeat.'))
}
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, rec.lastCmd))
}
if (id === 'sh:up') {
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd ..'))
}
if (id === 'sh:home') {
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd'))
}
if (id === 'sh:hist') {
const i = Number((interaction.values && interaction.values[0]) || 0)
const hit = rec.hist[i]
if (!hit) return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, hit.cmd))
}
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
}
async function discordCmdHandleUpload(ctx, interaction, destRaw) {
const att = discordInteractionAttachment(interaction, 'file')
const url = att && (att.url || att.proxyURL || att.proxy_url)
if (!att || !url) {
return {
text: 'Attach a file to `/upload`. Discord hosts it, then Bare OS `wget`s it into the VFS.',
ephemeral: true
}
}
const filename = discordUploadSafeName(att.name || att.filename || 'upload.bin')
const dest = await discordUploadResolveDest(ctx, interaction, destRaw, filename)
const allowed = discordCmdPathWriteOk(ctx, dest)
if (!allowed) {
return {
text: 'path not writable (stay under `~/` or `/tmp`; no `..`, no `~/.discord`)',
ephemeral: true
}
}
if (!/^https:\/\//i.test(String(url))) {
return { text: 'Attachment URL is not https.', ephemeral: true }
}
const rec = discordShGet(interaction, ctx)
await discordShPrepare(ctx, rec)
const line = 'wget -T 60 -O ' + discordShQuote(allowed) + ' ' + discordShQuote(url)
if (typeof ctx.execLine !== 'function' && typeof ctx.bareOsRunWgetCli !== 'function') {
return { text: 'wget unavailable in this session (`execLine` / `bareOsRunWgetCli`)', ephemeral: true }
}
const t0 = Date.now()
let out = ''
let errNote = ''
try {
out = await discordCmdCapture(ctx, function () {
if (typeof ctx.execLine === 'function') {
return ctx.execLine(line, { timeoutMs: BARE_OS_DISCORD_UPLOAD_MS })
}
return ctx.bareOsRunWgetCli(['wget', '-T', '60', '-O', allowed, String(url)])
})
} catch (err) {
errNote = String((err && err.message) || err)
out = (out ? out + '\n' : '') + errNote
}
const ms = Date.now() - t0
const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0
discordShHarvest(ctx, rec)
discordShPushHist(rec, line, exit, ms, out)
let size = att.size
try {
const st = await discordFmStat(ctx, allowed)
if (st && st.size != null) size = st.size
} catch {
/* keep attachment size */
}
const fields = [
discordField('Saved as', '`' + allowed + '`', false),
discordField('Name', filename, true),
discordField('Size', size != null ? discordPrettyBytes(size) : '—', true),
discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true)
]
if (att.contentType || att.content_type) {
fields.push(discordField('Type', String(att.contentType || att.content_type), true))
}
const note = String(out || '').trim()
const desc =
'`$ wget -O ' +
allowed +
' <cdn>`\n' +
(exit
? 'wget failed. If `BARE_OS_HTTP_ALLOWLIST` is set, add `cdn.discordapp.com` and `media.discordapp.net`.\n'
: 'Downloaded from the Discord CDN via `/bin/wget`.\n') +
(note ? discordCmdFence(note, '', 1800) : '')
return discordResult(
discordEmbed({
title: exit ? 'Upload failed' : 'Uploaded',
desc: desc,
fields: fields,
color: exit ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR_OK,
footer: 'exit ' + exit + ' · ' + ms + 'ms · wget'
})
)
}
var BARE_OS_DISCORD_HDMS_SESSIONS = Object.create(null)
function discordHdmsKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordHdmsGet(interaction) {
const k = discordHdmsKey(interaction)
let rec = BARE_OS_DISCORD_HDMS_SESSIONS[k]
if (!rec) {
rec = { sel: '', confirm: '', atMs: Date.now() }
BARE_OS_DISCORD_HDMS_SESSIONS[k] = rec
}
rec.atMs = Date.now()
return rec
}
function discordHdmsUnlocked(ctx) {
if (ctx && ctx.identity && ctx.identity.state === 'unlocked') return true
const e = discordCmdEnv(ctx)
const id = String(e.BARE_OS_IDENTITY || '')
return Boolean(id && id !== 'guest')
}
function discordHdmsLabelOk(label) {
return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(String(label || ''))
}
function discordHdmsRedact(text) {
return discordCmdRedact(String(text == null ? '' : text))
.replace(/("writerSecretHex"\s*:\s*")[^"]*"/gi, '$1[redacted]"')
.replace(/("secretKey"\s*:\s*")[^"]*"/gi, '$1[redacted]"')
}
function discordParseHdmsList(text) {
const rows = []
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (!line || line.indexOf('(no extra drives)') >= 0) continue
if (/^hdms:/i.test(line) || /log in/i.test(line) || /unavailable/i.test(line)) continue
const parts = line.split('\t')
const label = String(parts[0] || '').trim()
if (!label || !discordHdmsLabelOk(label)) continue
rows.push({
label: label,
mode: String(parts[1] || '').trim() || '—',
key: String(parts[2] || '').trim(),
extra: String(parts[3] || '').trim()
})
}
return rows
}
async function discordHdmsCli(ctx, argv) {
if (typeof ctx.runHdms !== 'function') {
return { text: '', error: 'HDMS unavailable (`ctx.runHdms` missing). Log in on the guest TTY.' }
}
let out = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.runHdms(argv)
})
} catch (err) {
return { text: String((err && err.message) || err), error: String((err && err.message) || err) }
}
const fail = ctx.exitCode && Number(ctx.exitCode) !== 0
return { text: String(out || ''), error: fail ? String(out || 'hdms failed').trim() : '' }
}
async function discordHdmsListRows(ctx) {
const got = await discordHdmsCli(ctx, ['hdms', 'list'])
if (got.error && !got.text) return { rows: [], error: got.error }
return { rows: discordParseHdmsList(got.text), error: got.error, raw: got.text }
}
async function discordSuggestHdmsLabels(ctx, q) {
const got = await discordHdmsListRows(ctx)
const names = got.rows.map(function (r) {
return r.label
})
return discordFilterChoices(names, q)
}
async function discordDefer(interaction) {
if (!interaction || interaction.deferred || interaction.replied) return false
if (typeof interaction.deferReply !== 'function') return false
try {
await interaction.deferReply()
return true
} catch {
return false
}
}
function discordHdmsUnavailable(ctx, note) {
const e = discordCmdEnv(ctx)
return discordResult(
discordEmbed({
title: 'HDMS',
color: BARE_OS_DISCORD_COLOR_WARN,
desc:
(note || 'Hyperdrive management needs an **unlocked** session and `ctx.runHdms`.') +
'\nIdentity: **' +
(e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || 'guest') +
'**. Use the guest TTY: `login`.',
fields: [
discordField('Health', '`/proc/bare_os/hdms_health.json`', false),
discordField('Hints', '`/proc/bare_os/hdms_hints.json`', false)
]
})
)
}
async function discordHdmsHealthView(ctx) {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/hdms_health.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_hdms_health.json'))
const obj = discordTryJson(t)
if (obj && typeof obj === 'object') {
const labels = Array.isArray(obj.labels) ? obj.labels : []
const fields = [
discordField('Active', obj.active ? 'yes' : 'no', true),
discordField('Mounts', String(obj.mountCount != null ? obj.mountCount : labels.length), true),
discordField('Identity', discordHdmsUnlocked(ctx) ? 'unlocked' : 'guest', true)
]
for (let i = 0; i < labels.length && fields.length < 20; i++) {
const L = labels[i] && typeof labels[i] === 'object' ? labels[i] : { label: labels[i] }
fields.push(discordField(String(L.label || 'drive'), String(L.mode || 'mounted'), true))
}
const th = obj.thresholdHints
if (th && typeof th === 'object') {
if (th.diskPressureWarnPercent != null) {
fields.push(discordField('Disk warn', String(th.diskPressureWarnPercent) + '%', true))
}
if (th.pairingBackoffMs != null) {
fields.push(discordField('Pair backoff', String(th.pairingBackoffMs) + 'ms', true))
}
}
return discordResult(
discordEmbed({
title: 'HDMS health',
desc: obj.note || 'Registry metadata only — no keys.',
fields: fields,
color: obj.active ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
})
)
}
return discordPrettySnapshot('HDMS health', t)
}
async function discordHdmsHintsView(ctx) {
const t =
(await discordCmdReadText(ctx, '/proc/bare_os/hdms_hints.json')) ||
(await discordCmdReadText(ctx, '/proc/bare_os_hdms_hints.json'))
const obj = discordTryJson(t)
if (obj && typeof obj === 'object') {
const fields = []
const keys = Object.keys(obj)
for (let i = 0; i < keys.length && fields.length < 20; i++) {
const k = keys[i]
if (/secret|token|invite|key/i.test(k) && k !== 'schema') {
fields.push(discordField(k, '[redacted]', false))
continue
}
let v = obj[k]
if (v && typeof v === 'object') v = JSON.stringify(v)
fields.push(discordField(k, String(v == null ? '—' : v).slice(0, 200), false))
}
return discordResult(
discordEmbed({
title: 'HDMS hints',
desc: 'Operator pairing hints. The guest does **not** open invite URLs by itself.',
fields: fields.length ? fields : [discordField('hints', 'None')]
})
)
}
return discordPrettySnapshot('HDMS hints', t)
}
async function discordHdmsHud(ctx, interaction, note) {
if (!discordHdmsUnlocked(ctx) || typeof ctx.runHdms !== 'function') {
return discordHdmsUnavailable(ctx)
}
const rec = discordHdmsGet(interaction)
const listed = await discordHdmsListRows(ctx)
const rows = listed.rows
const labels = {}
for (let i = 0; i < rows.length; i++) labels[rows[i].label] = 1
if (rec.sel && !labels[rec.sel]) rec.sel = ''
if (rec.confirm && !labels[rec.confirm]) rec.confirm = ''
const fields = []
for (let i = 0; i < rows.length && fields.length < 20; i++) {
const d = rows[i]
const mark = rec.sel === d.label ? '▸ ' : ''
const bits = [d.mode]
if (d.extra) bits.push(d.extra)
if (d.key && d.key !== '(local)') bits.push('`' + d.key.slice(0, 18) + '…`')
else if (d.key) bits.push(d.key)
fields.push(discordField(mark + d.label, bits.join(' · ') || 'mounted', false))
}
const sel = discordSelect(
'hdms:pick',
rows.length ? 'Select a drive…' : 'No extra drives',
rows.map(function (d) {
return {
label: d.label,
value: d.label,
description: (d.mode + (d.extra ? ' · ' + d.extra : '')).slice(0, 100)
}
})
)
const top = discordButtons([
{ id: 'hdms:refresh', label: 'Refresh', style: 1 },
{ id: 'hdms:create', label: 'Create' },
{ id: 'hdms:add', label: 'Add RO' },
{ id: 'hdms:pair', label: 'Pair' },
{ id: 'hdms:health', label: 'Health' }
])
const acts = rec.sel
? rec.confirm === rec.sel
? discordButtons([
{ id: 'hdms:ok', label: 'Confirm remove', style: 4 },
{ id: 'hdms:no', label: 'Cancel' }
])
: discordButtons([
{ id: 'hdms:show', label: 'Show', style: 1 },
{ id: 'hdms:browse', label: 'Browse /mnt' },
{ id: 'hdms:invite', label: 'Invite' },
{ id: 'hdms:inviterw', label: 'Invite RW', style: 3 },
{ id: 'hdms:rm', label: 'Remove', style: 4 }
])
: discordButtons([
{ id: 'hdms:invite0', label: 'Invite (no drive)' },
{ id: 'hdms:hints', label: 'Hints' }
])
let desc =
'Extra Hyperdrives under `/mnt/<label>`. Unlocked as **' +
discordSessionUser(ctx) +
'**.'
if (note) desc = note + '\n' + desc
if (listed.error) desc += '\n' + listed.error
if (rec.sel) desc += '\nSelected: **' + rec.sel + '** (`/mnt/' + rec.sel + '`)'
if (rec.confirm) desc += '\n**Remove `' + rec.confirm + '`?** Registry + mount go away.'
if (!rows.length) desc += '\nNo extra drives. **Create** a writable one, **Add RO** a key, or **Pair** an invite.'
return discordResult(
discordEmbed({
title: 'HDMS · ' + rows.length,
desc: desc,
fields: fields,
color: listed.error ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR
}),
{ components: [sel, top, acts].filter(Boolean) }
)
}
async function discordHdmsShowView(ctx, interaction, label) {
const got = await discordHdmsCli(ctx, ['hdms', 'show', label])
const raw = discordHdmsRedact(got.text || got.error || '')
const obj = discordTryJson(raw)
if (obj && typeof obj === 'object') {
const fields = [
discordField('Label', String(obj.label || label), true),
discordField('Mode', String(obj.mode || '—'), true),
discordField('Mount', '`/mnt/' + label + '`', true)
]
if (obj.key) fields.push(discordField('Key', '`' + String(obj.key) + '`', false))
if (obj.ns) fields.push(discordField('Namespace', String(obj.ns), false))
if (obj.ephemeral) fields.push(discordField('Lifetime', 'ephemeral', true))
if (obj.id) fields.push(discordField('Id', String(obj.id), true))
return discordResult(
discordEmbed({
title: 'HDMS · ' + label,
desc: got.error || 'Drive record (secrets redacted).',
fields: fields,
color: got.error ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR_OK
}),
{
components: [
discordButtons([
{ id: 'hdms:browse', label: 'Browse /mnt', style: 1 },
{ id: 'hdms:invite', label: 'Invite' },
{ id: 'hdms:home', label: 'HDMS' }
])
]
}
)
}
return {
more: {
title: 'HDMS · ' + label,
body: raw || '(empty)',
fence: true,
components: [
discordButtons([{ id: 'hdms:home', label: 'HDMS' }])
]
}
}
}
async function discordCmdHandleHdms(ctx, interaction, sub, opt) {
const s = String(sub || 'list')
if (s === 'help') {
return discordResult(
discordEmbed({
title: 'HDMS',
desc:
'Manage extra Hyperdrives (`/mnt/<label>`). Requires **login**.\n' +
'Same surface as `/bin/hdms`: list, create, add, remove, show, invite, pair.',
fields: [
discordField('/hdms', 'Interactive manager (default)'),
discordField('/hdms create', '`label` — new writable drive'),
discordField('/hdms add', '`label` + `key` — mount a read-only z32 key'),
discordField('/hdms remove / show', '`label`'),
discordField('/hdms invite', 'optional `label`, `mode` pair / readonly / rw'),
discordField('/hdms pair', '`invite` token; `persist` default on'),
discordField('/hdms health / hints', '`/proc/bare_os/hdms_*.json`')
]
})
)
}
if (s === 'health') return discordHdmsHealthView(ctx)
if (s === 'hints') return discordHdmsHintsView(ctx)
if (!discordHdmsUnlocked(ctx) || typeof ctx.runHdms !== 'function') {
return discordHdmsUnavailable(ctx)
}
if (s === 'create') {
const label = String(opt('label') || '').trim()
if (!label) return { modal: 'hdms:create' }
if (!discordHdmsLabelOk(label)) {
return { text: 'Invalid label (alphanumeric, then . _ - ; max 63).', ephemeral: true }
}
const got = await discordHdmsCli(ctx, ['hdms', 'create', label])
const rec = discordHdmsGet(interaction)
rec.sel = label
return discordHdmsHud(ctx, interaction, got.error || got.text || 'Created `' + label + '`.')
}
if (s === 'add') {
const label = String(opt('label') || '').trim()
const key = String(opt('key') || '').trim()
if (!label || !key) return { modal: 'hdms:add' }
if (!discordHdmsLabelOk(label)) {
return { text: 'Invalid label (alphanumeric, then . _ - ; max 63).', ephemeral: true }
}
const got = await discordHdmsCli(ctx, ['hdms', 'add', label, key])
const rec = discordHdmsGet(interaction)
rec.sel = label
return discordHdmsHud(ctx, interaction, got.error || got.text || 'Added `' + label + '`.')
}
if (s === 'remove') {
const label = String(opt('label') || '').trim()
if (!label) return discordHdmsHud(ctx, interaction, 'Pick a drive to remove.')
const got = await discordHdmsCli(ctx, ['hdms', 'remove', label])
const rec = discordHdmsGet(interaction)
rec.sel = ''
rec.confirm = ''
return discordHdmsHud(ctx, interaction, got.error || got.text || 'Removed `' + label + '`.')
}
if (s === 'show') {
const label = String(opt('label') || '').trim() || discordHdmsGet(interaction).sel
if (!label) return discordHdmsHud(ctx, interaction, 'Pick a drive to show.')
discordHdmsGet(interaction).sel = label
return discordHdmsShowView(ctx, interaction, label)
}
if (s === 'invite') {
const label = String(opt('label') || '').trim()
const mode = String(opt('mode') || 'pair').toLowerCase()
const argv = ['hdms', 'invite']
if (mode === 'readonly' || mode === 'ro' || mode === 'read-only') argv.push('--read-only')
if (mode === 'rw' || mode === 'readwrite' || mode === 'read-write') argv.push('--rw')
if (label) argv.push(label)
await discordDefer(interaction)
const got = await discordHdmsCli(ctx, argv)
const body = discordHdmsRedact(got.text || got.error || '')
const first = body.split(/\r?\n/).filter(Boolean)[0] || ''
if (label) discordHdmsGet(interaction).sel = label
return discordResult(
discordEmbed({
title: got.error && !first ? 'Invite failed' : 'HDMS invite',
desc:
(got.error && !/invite/i.test(body) ? got.error + '\n' : '') +
(first ? 'Token:\n' + discordCmdFence(first, '', 900) : body ? discordCmdFence(body, '', 1800) : 'No invite.'),
color: got.error && !first ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR_OK,
footer: 'Peer: hdms pair <token> · same HYPERSWARM_BOOTSTRAP'
}),
{ components: [discordButtons([{ id: 'hdms:home', label: 'HDMS' }])] }
)
}
if (s === 'pair') {
const invite = String(opt('invite') || '').trim()
if (!invite) return { modal: 'hdms:pair' }
const persist = String(opt('persist') || 'yes').toLowerCase()
const argv = ['hdms', 'pair']
if (persist === 'no' || persist === '0' || persist === 'ephemeral' || persist === 'false') {
argv.push('--no-persist')
}
argv.push(invite)
await discordDefer(interaction)
const got = await discordHdmsCli(ctx, argv)
return discordHdmsHud(ctx, interaction, got.error || got.text || 'Paired.')
}
return discordHdmsHud(ctx, interaction, '')
}
async function discordHdmsAction(ctx, interaction, id) {
const rec = discordHdmsGet(interaction)
if (id === 'hdms:home' || id === 'hdms:refresh' || id === 'hdms:no') {
if (id === 'hdms:no') rec.confirm = ''
return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, ''))
}
if (id === 'hdms:pick') {
rec.sel = String((interaction.values && interaction.values[0]) || '')
rec.confirm = ''
return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, ''))
}
if (id === 'hdms:health') {
return discordSendResult(ctx, interaction, await discordHdmsHealthView(ctx))
}
if (id === 'hdms:hints') {
return discordSendResult(ctx, interaction, await discordHdmsHintsView(ctx))
}
if (id === 'hdms:create') {
return discordSendResult(ctx, interaction, { modal: 'hdms:create' })
}
if (id === 'hdms:add') {
return discordSendResult(ctx, interaction, { modal: 'hdms:add' })
}
if (id === 'hdms:pair') {
return discordSendResult(ctx, interaction, { modal: 'hdms:pair' })
}
if (id === 'hdms:show') {
if (!rec.sel) return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, 'Pick a drive first.'))
return discordSendResult(ctx, interaction, await discordHdmsShowView(ctx, interaction, rec.sel))
}
if (id === 'hdms:browse') {
if (!rec.sel) return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, 'Pick a drive first.'))
return discordSendResult(ctx, interaction, { files: '/mnt/' + rec.sel })
}
if (id === 'hdms:rm') {
if (!rec.sel) return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, 'Pick a drive first.'))
rec.confirm = rec.sel
return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, ''))
}
if (id === 'hdms:ok') {
const label = rec.confirm || rec.sel
rec.confirm = ''
rec.sel = ''
if (!label) return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, ''))
const empty = function () {
return ''
}
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHdms(ctx, interaction, 'remove', function (k) {
return k === 'label' ? label : empty()
})
)
}
if (id === 'hdms:invite' || id === 'hdms:inviterw' || id === 'hdms:invite0') {
const label = id === 'hdms:invite0' ? '' : rec.sel
const mode = id === 'hdms:inviterw' ? 'rw' : 'pair'
if (id !== 'hdms:invite0' && !label) {
return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, 'Pick a drive first.'))
}
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHdms(ctx, interaction, 'invite', function (k) {
if (k === 'label') return label
if (k === 'mode') return mode
return ''
})
)
}
return discordSendResult(ctx, interaction, await discordHdmsHud(ctx, interaction, ''))
}
var BARE_OS_DISCORD_HOLESAIL_SESSIONS = Object.create(null)
var BARE_OS_DISCORD_HOLESAIL_UNIT = 'bare-holesail'
function discordHolesailKey(interaction) {
return discordInteractionUserId(interaction) || 'anon'
}
function discordHolesailGet(interaction) {
const k = discordHolesailKey(interaction)
let rec = BARE_OS_DISCORD_HOLESAIL_SESSIONS[k]
if (!rec) {
rec = { sel: '', confirm: '', atMs: Date.now() }
BARE_OS_DISCORD_HOLESAIL_SESSIONS[k] = rec
}
rec.atMs = Date.now()
return rec
}
function discordHolesailIdOk(id) {
return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(String(id || ''))
}
function discordHolesailRedact(text) {
return discordCmdRedact(String(text == null ? '' : text)).replace(
/\b[0-9a-f]{64}\b/gi,
'[redacted-seed]'
)
}
function discordHolesailBool(s) {
const v = String(s == null ? '' : s).trim().toLowerCase()
if (!v) return null
if (v === '1' || v === 'true' || v === 'yes' || v === 'on' || v === 'udp') return true
if (v === '0' || v === 'false' || v === 'no' || v === 'off' || v === 'tcp') return false
return null
}
function discordParseHolesailKv(parts) {
const kv = {}
for (let i = 0; i < parts.length; i++) {
const p = String(parts[i] || '')
const eq = p.indexOf('=')
if (eq > 0) kv[p.slice(0, eq)] = p.slice(eq + 1)
}
return kv
}
function discordParseHolesailList(text) {
const rows = []
let daemon = ''
let path = ''
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
if (/^state:\s*/i.test(line)) {
path = line.replace(/^state:\s*/i, '').trim()
continue
}
if (/^daemon:\s*/i.test(line)) {
daemon = line.replace(/^daemon:\s*/i, '').trim()
continue
}
if (/^connections:\s*/i.test(line)) continue
if (line.indexOf('(no connections)') >= 0) continue
if (/^holesail:/i.test(line)) continue
const parts = line.split('\t')
const id = String(parts[0] || '').trim()
if (!id || !discordHolesailIdOk(id)) continue
const kv = discordParseHolesailKv(parts.slice(2))
const invalid = parts.join('\t').indexOf('INVALID:') >= 0
rows.push({
id: id,
mode: String(parts[1] || kv.mode || '').trim() || '?',
enabled: String(kv.enabled || '') === 'true',
live: String(kv.live || '') === 'true',
port: String(kv.port || '').trim(),
host: String(kv.host || '').trim(),
udp: String(kv.udp || ''),
secure: String(kv.secure || ''),
url: String(kv.url || '').trim(),
invalid: invalid
})
}
return { rows: rows, daemon: daemon, path: path }
}
function discordParseHolesailShow(text) {
const out = {}
const lines = String(text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const tab = line.indexOf('\t')
if (tab < 1) continue
const k = line.slice(0, tab).trim()
const v = line.slice(tab + 1).trim()
if (k) out[k] = v
}
return out
}
async function discordHolesailCli(ctx, argv) {
if (typeof ctx.bareOsRunHolesailCli !== 'function') {
return {
text: '',
error: 'Holesail unavailable (`ctx.bareOsRunHolesailCli` missing).'
}
}
let out = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunHolesailCli(argv)
})
} catch (err) {
return { text: String((err && err.message) || err), error: String((err && err.message) || err) }
}
const fail = ctx.exitCode && Number(ctx.exitCode) !== 0
return {
text: String(out || ''),
error: fail ? String(out || 'holesail failed').trim() : ''
}
}
async function discordHolesailListRows(ctx) {
const got = await discordHolesailCli(ctx, ['holesail', 'list'])
const parsed = discordParseHolesailList(got.text)
return {
rows: parsed.rows,
daemon: parsed.daemon,
path: parsed.path,
error: got.error,
raw: got.text
}
}
async function discordSuggestHolesailIds(ctx, q) {
const got = await discordHolesailListRows(ctx)
const names = got.rows.map(function (r) {
return r.id
})
return discordFilterChoices(names, q)
}
function discordHolesailUnavailable(note) {
return discordResult(
discordEmbed({
title: 'Holesail',
color: BARE_OS_DISCORD_COLOR_WARN,
desc:
(note || 'Managed Holesail needs `ctx.bareOsRunHolesailCli` from the booter.') +
'\nUse `/bin/holesail` on the guest TTY, or start **bare-holesail**.',
fields: [
discordField('State', '`~/.holesail/state.json`', false),
discordField('Unit', '`systemctl status bare-holesail`', false)
]
})
)
}
function discordHolesailDaemonLive(listed) {
const d = String((listed && listed.daemon) || '').toLowerCase()
return d.indexOf('running') >= 0 && d.indexOf('no managed') < 0
}
function discordHolesailRowBits(row) {
const bits = [row.mode || '?']
bits.push(row.enabled ? 'enabled' : 'disabled')
bits.push(row.live ? 'live' : 'stopped')
if (row.host || row.port) {
bits.push((row.host || '') + (row.port ? ':' + row.port : ''))
}
if (row.udp === 'true') bits.push('udp')
if (row.secure === 'true') bits.push('secure')
if (row.invalid) bits.push('INVALID')
return bits.join(' · ')
}
async function discordHolesailHud(ctx, interaction, note) {
if (typeof ctx.bareOsRunHolesailCli !== 'function') {
return discordHolesailUnavailable()
}
const rec = discordHolesailGet(interaction)
const listed = await discordHolesailListRows(ctx)
const rows = listed.rows
const ids = {}
for (let i = 0; i < rows.length; i++) ids[rows[i].id] = 1
if (rec.sel && !ids[rec.sel]) rec.sel = ''
if (rec.confirm && !ids[rec.confirm]) rec.confirm = ''
const fields = []
for (let i = 0; i < rows.length && fields.length < 20; i++) {
const r = rows[i]
const mark = rec.sel === r.id ? '▸ ' : ''
let val = discordHolesailRowBits(r)
if (r.url) val += '\n`' + r.url.slice(0, 80) + (r.url.length > 80 ? '…' : '') + '`'
fields.push(discordField(mark + r.id, val || '—', false))
}
const sel = discordSelect(
'holesail:pick',
rows.length ? 'Select a tunnel…' : 'No connections',
rows.map(function (r) {
return {
label: r.id,
value: r.id,
description: discordHolesailRowBits(r).slice(0, 100)
}
})
)
const top = discordButtons([
{ id: 'holesail:refresh', label: 'Refresh', style: 1 },
{ id: 'holesail:addsrv', label: 'Add server' },
{ id: 'holesail:addcli', label: 'Add client' },
{ id: 'holesail:status', label: 'Status' },
{ id: 'holesail:logs', label: 'Logs' }
])
let acts
if (rec.sel && rec.confirm === rec.sel) {
acts = discordButtons([
{ id: 'holesail:ok', label: 'Confirm remove', style: 4 },
{ id: 'holesail:no', label: 'Cancel' }
])
} else if (rec.sel) {
const cur = rows.filter(function (r) {
return r.id === rec.sel
})[0]
acts = discordButtons([
{ id: 'holesail:show', label: 'Show', style: 1 },
{
id: cur && cur.live ? 'holesail:stop' : 'holesail:start',
label: cur && cur.live ? 'Stop' : 'Start',
style: cur && cur.live ? 4 : 3
},
{
id: cur && cur.enabled ? 'holesail:disable' : 'holesail:enable',
label: cur && cur.enabled ? 'Disable' : 'Enable'
},
{ id: 'holesail:edit', label: 'Edit' },
{ id: 'holesail:rm', label: 'Remove', style: 4 }
])
} else {
acts = discordButtons([
{ id: 'holesail:svcstart', label: 'Start unit', style: 3 },
{ id: 'holesail:svcstop', label: 'Stop unit', style: 4 },
{ id: 'holesail:svcrestart', label: 'Restart unit' },
{ id: 'holesail:path', label: 'Path' }
])
}
let desc =
'Managed tunnels (`/bin/holesail`). Daemon: **' +
(listed.daemon || 'unknown') +
'**.'
if (listed.path) desc += '\nState: `' + listed.path + '`'
if (note) desc = note + '\n' + desc
if (listed.error) desc += '\n' + listed.error
if (rec.sel) desc += '\nSelected: **' + rec.sel + '**'
if (rec.confirm) desc += '\n**Remove `' + rec.confirm + '`?** State row and live tunnel go away.'
if (!rows.length) {
desc +=
'\nNo connections. **Add server** to share a local port, or **Add client** to connect to an `hs://` URL.'
}
return discordResult(
discordEmbed({
title: 'Holesail · ' + rows.length,
desc: desc,
fields: fields,
color: listed.error
? BARE_OS_DISCORD_COLOR_WARN
: discordHolesailDaemonLive(listed)
? BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR
}),
{ components: [sel, top, acts].filter(Boolean) }
)
}
async function discordHolesailShowView(ctx, interaction, id) {
const got = await discordHolesailCli(ctx, ['holesail', 'show', id])
const raw = discordHolesailRedact(got.text || got.error || '')
const rec = discordParseHolesailShow(raw)
const fields = [
discordField('Id', String(rec.id || id), true),
discordField('Mode', String(rec.mode || '—'), true),
discordField('Enabled', String(rec.enabled || '—'), true),
discordField('Live', String(rec.live || '—'), true)
]
if (rec.port) fields.push(discordField('Port', String(rec.port), true))
if (rec.host) fields.push(discordField('Host', String(rec.host), true))
if (rec.udp) fields.push(discordField('UDP', String(rec.udp), true))
if (rec.secure) fields.push(discordField('Secure', String(rec.secure), true))
if (rec.log) fields.push(discordField('Log', String(rec.log), true))
if (rec.seed) fields.push(discordField('Seed set', String(rec.seed), true))
if (rec.daemon) fields.push(discordField('Daemon', String(rec.daemon), true))
if (rec.state) fields.push(discordField('State', '`' + String(rec.state) + '`', false))
if (rec.url) fields.push(discordField('URL', '`' + String(rec.url) + '`', false))
return discordResult(
discordEmbed({
title: 'Holesail · ' + id,
desc:
got.error ||
(rec.url
? 'Shareable URL is the `hs://` key. Seed material is never shown.'
: 'Connection record (seed redacted).'),
fields: fields,
color: got.error ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR_OK
}),
{
components: [
discordButtons([
{ id: 'holesail:url', label: 'Copy URL', style: 1 },
{ id: 'holesail:restart', label: 'Restart' },
{ id: 'holesail:home', label: 'Holesail' }
])
]
}
)
}
async function discordHolesailStatusView(ctx, interaction) {
const got = await discordHolesailCli(ctx, ['holesail', 'status'])
const parsed = discordParseHolesailList(got.text)
const counts = {}
let total = ''
const lines = String(got.text || '').split(/\r?\n/)
for (let i = 0; i < lines.length; i++) {
if (/^connections:\s*/i.test(lines[i])) {
const bits = lines[i].replace(/^connections:\s*/i, '').split(/\s+/)
if (bits[0] && bits[0].indexOf('=') < 0) total = bits[0]
Object.assign(counts, discordParseHolesailKv(bits))
}
}
const fields = [
discordField('Daemon', parsed.daemon || '—', false),
discordField('State', parsed.path ? '`' + parsed.path + '`' : '—', false),
discordField('Connections', String(total || parsed.rows.length || '0'), true),
discordField('Enabled', String(counts.enabled != null ? counts.enabled : '—'), true),
discordField('Live', String(counts.live != null ? counts.live : '—'), true),
discordField('Servers', String(counts.server != null ? counts.server : '—'), true),
discordField('Clients', String(counts.client != null ? counts.client : '—'), true)
]
return discordResult(
discordEmbed({
title: 'Holesail status',
desc: got.error || 'Managed daemon + persisted rows.',
fields: fields,
color: got.error
? BARE_OS_DISCORD_COLOR_ERR
: discordHolesailDaemonLive(parsed)
? BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR_WARN
}),
{
components: [
discordButtons([
{ id: 'holesail:home', label: 'Holesail', style: 1 },
{ id: 'holesail:svcstart', label: 'Start unit', style: 3 },
{ id: 'holesail:svcstop', label: 'Stop unit' },
{ id: 'holesail:svcrestart', label: 'Restart unit' },
{ id: 'holesail:logs', label: 'Logs' }
])
]
}
)
}
async function discordHolesailUrlView(ctx, interaction, id) {
const got = await discordHolesailCli(ctx, ['holesail', 'show', id])
const rec = discordParseHolesailShow(discordHolesailRedact(got.text || ''))
const url = String(rec.url || '').trim()
return discordResult(
discordEmbed({
title: 'Holesail URL · ' + id,
desc: url
? 'Share this with a **client**:\n' + discordCmdFence(url, '', 900)
: got.error || 'No `hs://` URL yet. Start a **server** tunnel and refresh after `ready()`.',
color: url ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
footer: 'Peer: holesail add ID --client --key <url> --port N'
}),
{
components: [
discordButtons([
{ id: 'holesail:show', label: 'Show' },
{ id: 'holesail:home', label: 'Holesail' }
])
]
}
)
}
async function discordHolesailService(ctx, action) {
const act = String(action || 'status').toLowerCase()
if (typeof ctx.bareOsRunSystemctlCli !== 'function') {
return { text: '', error: 'systemctl unavailable (`ctx.bareOsRunSystemctlCli` missing).' }
}
const argv =
act === 'logs'
? ['journalctl', '-u', BARE_OS_DISCORD_HOLESAIL_UNIT, '--lines', '30']
: ['systemctl', act, BARE_OS_DISCORD_HOLESAIL_UNIT]
let out = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.bareOsRunSystemctlCli(argv)
})
} catch (err) {
return { text: String((err && err.message) || err), error: String((err && err.message) || err) }
}
const fail = ctx.exitCode && Number(ctx.exitCode) !== 0
return {
text: String(out || ''),
error: fail ? String(out || 'systemctl failed').trim() : ''
}
}
function discordHolesailAddArgv(id, mode, opt) {
const argv = ['holesail', 'add', id, mode === 'client' ? '--client' : '--server']
const port = String(opt('port') || '').trim()
const host = String(opt('host') || '').trim()
const key = String(opt('key') || '').trim()
const udp = discordHolesailBool(opt('udp'))
const secure = discordHolesailBool(opt('secure'))
if (port) {
argv.push('--port')
argv.push(port)
}
if (host) {
argv.push('--host')
argv.push(host)
}
if (key) {
argv.push('--key')
argv.push(key)
}
if (udp === true) argv.push('--udp')
if (secure === true) argv.push('--secure')
if (secure === false) argv.push('--no-secure')
return argv
}
function discordHolesailEditArgv(id, opt) {
const argv = ['holesail', 'edit', id]
let touched = false
const port = String(opt('port') || '').trim()
const hostRaw = opt('host')
const host = hostRaw == null ? '' : String(hostRaw).trim()
const key = String(opt('key') || '').trim()
const udp = discordHolesailBool(opt('udp'))
const secure = discordHolesailBool(opt('secure'))
if (port) {
argv.push('--port')
argv.push(port)
touched = true
}
if (hostRaw != null && String(hostRaw) !== '') {
if (!host || host === '-' || host.toLowerCase() === 'clear') argv.push('--clear-host')
else {
argv.push('--host')
argv.push(host)
}
touched = true
}
if (key) {
argv.push('--key')
argv.push(key)
touched = true
}
if (udp === true) {
argv.push('--udp')
touched = true
} else if (udp === false) {
argv.push('--no-udp')
touched = true
}
if (secure === true) {
argv.push('--secure')
touched = true
} else if (secure === false) {
argv.push('--no-secure')
touched = true
}
return { argv: argv, touched: touched }
}
async function discordCmdHandleHolesail(ctx, interaction, sub, opt) {
const s = String(sub || 'list')
if (s === 'help') {
return discordResult(
discordEmbed({
title: 'Holesail',
desc:
'Manage persisted P2P tunnels (`~/.holesail/state.json`) and the **bare-holesail** unit.\n' +
'Same surface as `/bin/holesail`. **show** never prints seed material.',
fields: [
discordField('/holesail', 'Interactive manager (default)'),
discordField('/holesail add', '`id` + `mode` server|client; port, host, key, udp, secure'),
discordField('/holesail edit', '`id` plus fields to change (live tunnels restart)'),
discordField('/holesail show / url', '`id` — record or shareable `hs://` URL'),
discordField('/holesail start|stop|restart', '`id` — live tunnel'),
discordField('/holesail enable|disable|remove', '`id`'),
discordField('/holesail status / path / logs', 'Daemon snapshot, state file, journal'),
discordField('/holesail service', '`action` start|stop|restart|status of **bare-holesail**')
]
})
)
}
if (typeof ctx.bareOsRunHolesailCli !== 'function') {
return discordHolesailUnavailable()
}
if (s === 'status') return discordHolesailStatusView(ctx, interaction)
if (s === 'path') {
const got = await discordHolesailCli(ctx, ['holesail', 'path'])
return discordResult(
discordEmbed({
title: 'Holesail state path',
desc: discordCmdFence((got.text || got.error || '').trim() || '—', '', 900),
color: got.error ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR
}),
{ components: [discordButtons([{ id: 'holesail:home', label: 'Holesail' }])] }
)
}
if (s === 'logs') {
await discordDefer(interaction)
const got = await discordHolesailService(ctx, 'logs')
const body = discordHolesailRedact(got.text || got.error || '')
if (!String(body).trim()) {
return discordResult(
discordEmbed({
title: 'Holesail journal',
desc: 'Empty journal for `bare-holesail`.'
}),
{ components: [discordButtons([{ id: 'holesail:home', label: 'Holesail' }])] }
)
}
return {
more: {
title: 'Holesail journal',
body: body,
fence: true,
components: [discordButtons([{ id: 'holesail:home', label: 'Holesail' }])]
}
}
}
if (s === 'service') {
const action = String(opt('action') || 'status').toLowerCase()
if (action === 'logs') return discordCmdHandleHolesail(ctx, interaction, 'logs', opt)
await discordDefer(interaction)
const got = await discordHolesailService(ctx, action)
if (action === 'status') {
const body = discordHolesailRedact(got.text || got.error || '')
return {
more: {
title: 'bare-holesail',
body: body || '(empty)',
fence: true,
components: [discordButtons([{ id: 'holesail:home', label: 'Holesail' }])]
}
}
}
return discordHolesailHud(
ctx,
interaction,
got.error || got.text || action + ' **bare-holesail**.'
)
}
if (s === 'add') {
const id = String(opt('id') || '').trim()
const mode = String(opt('mode') || '').toLowerCase()
if (!id) {
return { modal: mode === 'client' ? 'holesail:addcli' : 'holesail:addsrv' }
}
if (!discordHolesailIdOk(id)) {
return { text: 'Invalid id (alphanumeric, then . _ - ; max 64).', ephemeral: true }
}
const inferred = mode || (String(opt('key') || '').trim() ? 'client' : 'server')
if (inferred === 'client' && !String(opt('key') || '').trim()) {
return { modal: 'holesail:addcli' }
}
await discordDefer(interaction)
const got = await discordHolesailCli(ctx, discordHolesailAddArgv(id, inferred, opt))
discordHolesailGet(interaction).sel = id
return discordHolesailHud(ctx, interaction, got.error || got.text || 'Added `' + id + '`.')
}
if (s === 'edit') {
const id = String(opt('id') || '').trim() || discordHolesailGet(interaction).sel
if (!id) return discordHolesailHud(ctx, interaction, 'Pick a tunnel to edit.')
if (!discordHolesailIdOk(id)) {
return { text: 'Invalid id (alphanumeric, then . _ - ; max 64).', ephemeral: true }
}
discordHolesailGet(interaction).sel = id
const built = discordHolesailEditArgv(id, opt)
if (!built.touched) return { modal: 'holesail:edit' }
await discordDefer(interaction)
const got = await discordHolesailCli(ctx, built.argv)
return discordHolesailHud(ctx, interaction, got.error || got.text || 'Updated `' + id + '`.')
}
if (s === 'remove') {
const id = String(opt('id') || '').trim()
if (!id) return discordHolesailHud(ctx, interaction, 'Pick a tunnel to remove.')
await discordDefer(interaction)
const got = await discordHolesailCli(ctx, ['holesail', 'remove', id])
const rec = discordHolesailGet(interaction)
rec.sel = ''
rec.confirm = ''
return discordHolesailHud(ctx, interaction, got.error || got.text || 'Removed `' + id + '`.')
}
if (s === 'show') {
const id = String(opt('id') || '').trim() || discordHolesailGet(interaction).sel
if (!id) return discordHolesailHud(ctx, interaction, 'Pick a tunnel to show.')
discordHolesailGet(interaction).sel = id
return discordHolesailShowView(ctx, interaction, id)
}
if (s === 'url') {
const id = String(opt('id') || '').trim() || discordHolesailGet(interaction).sel
if (!id) return discordHolesailHud(ctx, interaction, 'Pick a tunnel to show its URL.')
discordHolesailGet(interaction).sel = id
return discordHolesailUrlView(ctx, interaction, id)
}
if (s === 'start' || s === 'stop' || s === 'restart' || s === 'enable' || s === 'disable') {
const id = String(opt('id') || '').trim() || discordHolesailGet(interaction).sel
if (!id) return discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
discordHolesailGet(interaction).sel = id
await discordDefer(interaction)
const got = await discordHolesailCli(ctx, ['holesail', s, id])
return discordHolesailHud(ctx, interaction, got.error || got.text || s + ' `' + id + '`.')
}
return discordHolesailHud(ctx, interaction, '')
}
async function discordHolesailAction(ctx, interaction, id) {
const rec = discordHolesailGet(interaction)
if (id === 'holesail:home' || id === 'holesail:refresh' || id === 'holesail:no') {
if (id === 'holesail:no') rec.confirm = ''
return discordSendResult(ctx, interaction, await discordHolesailHud(ctx, interaction, ''))
}
if (id === 'holesail:pick') {
rec.sel = String((interaction.values && interaction.values[0]) || '')
rec.confirm = ''
return discordSendResult(ctx, interaction, await discordHolesailHud(ctx, interaction, ''))
}
if (id === 'holesail:status') {
return discordSendResult(ctx, interaction, await discordHolesailStatusView(ctx, interaction))
}
if (id === 'holesail:path') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'path', function () {
return ''
})
)
}
if (id === 'holesail:logs') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'logs', function () {
return ''
})
)
}
if (id === 'holesail:addsrv') {
return discordSendResult(ctx, interaction, { modal: 'holesail:addsrv' })
}
if (id === 'holesail:addcli') {
return discordSendResult(ctx, interaction, { modal: 'holesail:addcli' })
}
if (id === 'holesail:edit') {
if (!rec.sel) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
)
}
return discordSendResult(ctx, interaction, { modal: 'holesail:edit' })
}
if (id === 'holesail:show') {
if (!rec.sel) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
)
}
return discordSendResult(ctx, interaction, await discordHolesailShowView(ctx, interaction, rec.sel))
}
if (id === 'holesail:url') {
if (!rec.sel) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
)
}
return discordSendResult(ctx, interaction, await discordHolesailUrlView(ctx, interaction, rec.sel))
}
if (id === 'holesail:rm') {
if (!rec.sel) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
)
}
rec.confirm = rec.sel
return discordSendResult(ctx, interaction, await discordHolesailHud(ctx, interaction, ''))
}
if (id === 'holesail:ok') {
const conn = rec.confirm || rec.sel
rec.confirm = ''
rec.sel = ''
if (!conn) return discordSendResult(ctx, interaction, await discordHolesailHud(ctx, interaction, ''))
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'remove', function (k) {
return k === 'id' ? conn : ''
})
)
}
if (
id === 'holesail:start' ||
id === 'holesail:stop' ||
id === 'holesail:restart' ||
id === 'holesail:enable' ||
id === 'holesail:disable'
) {
const act = id.slice('holesail:'.length)
if (!rec.sel) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'Pick a tunnel first.')
)
}
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, act, function (k) {
return k === 'id' ? rec.sel : ''
})
)
}
if (id === 'holesail:svcstart' || id === 'holesail:svcstop' || id === 'holesail:svcrestart') {
const act = id === 'holesail:svcstart' ? 'start' : id === 'holesail:svcstop' ? 'stop' : 'restart'
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'service', function (k) {
return k === 'action' ? act : ''
})
)
}
return discordSendResult(ctx, interaction, await discordHolesailHud(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 } }
}
var BARE_OS_DISCORD_AGENT_MS = 13 * 60 * 1000
var BARE_OS_DISCORD_AGENT_POLL_MS = 2500
var BARE_OS_DISCORD_AGENT_RUNS = Object.create(null)
var BARE_OS_DISCORD_AGENT_SECRET_RE =
/api[_-]?key|token|password|secret|passwd|authorization/i
function discordAgentPaths(_ctx) {
return {
dir: '~/.agent',
config: '~/.agent/config.json',
history: '~/.agent/history.json',
progress: '~/.agent/progress.txt'
}
}
function discordAgentBackend(cfg) {
const b = String((cfg && cfg.backend) || '').trim().toLowerCase()
if (b === 'rest' || b === 'openai' || b === 'http') return 'rest'
if (b === 'qvac') return 'qvac'
const p = String((cfg && cfg.provider) || '').trim().toLowerCase()
if (p === 'qvac') return 'qvac'
if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') return 'rest'
if (cfg && cfg.rest_api_key && String(cfg.rest_api_key).trim()) return 'rest'
return 'qvac'
}
var BARE_OS_DISCORD_QVAC_MODELS =
typeof BARE_AGENT_QVAC_CHAT_MODELS !== 'undefined' && Array.isArray(BARE_AGENT_QVAC_CHAT_MODELS)
? BARE_AGENT_QVAC_CHAT_MODELS
: [
{ id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite' },
{ id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended' },
{ id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong' },
{ id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16 },
{ id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24 },
{ id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny' },
{ id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6 },
{ id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3 },
{ id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24 },
{ id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8 },
{ id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16 },
{ id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48 },
{ id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10 },
{ id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10 },
{ id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16 },
{ id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 8B multimodal Q4', tools: true, ramGb: 24 },
{ id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28 },
{ id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48 }
]
var BARE_OS_DISCORD_REST_MODEL_FALLBACKS = {
groq: [
'llama-3.3-70b-versatile',
'llama-3.1-8b-instant',
'openai/gpt-oss-120b',
'openai/gpt-oss-20b',
'qwen/qwen3-32b',
'moonshotai/kimi-k2-instruct',
'meta-llama/llama-4-scout-17b-16e-instruct',
'meta-llama/llama-4-maverick-17b-128e-instruct'
],
xai: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212', 'grok-2-vision-1212'],
openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3'],
custom: []
}
var BARE_OS_DISCORD_AGENT_MODELS = Object.create(null)
var BARE_OS_DISCORD_MODEL_PAGE = 23
function discordParseOpenAiModels(json) {
if (typeof bareAgentParseOpenAiModels === 'function') return bareAgentParseOpenAiModels(json)
const raw =
json && json.data && Array.isArray(json.data) ? json.data : Array.isArray(json) ? json : []
const skip = /embed|whisper|tts|dall-e|davinci|babbage|audio|moderation|realtime|image|sora/i
const out = []
const seen = Object.create(null)
for (let i = 0; i < raw.length; i++) {
const row = raw[i] && typeof raw[i] === 'object' ? raw[i] : null
if (!row) continue
const id = String(row.id || row.name || '').trim()
if (!id || seen[id] || skip.test(id)) continue
seen[id] = 1
out.push({
id: id,
label: id,
family: String(row.owned_by || 'api'),
owned_by: String(row.owned_by || '')
})
}
out.sort(function (a, b) {
return a.id.localeCompare(b.id)
})
return out
}
function discordRestModelsFallback(provider) {
if (typeof bareAgentRestModelsFallback === 'function') return bareAgentRestModelsFallback(provider)
const key = String(provider || 'groq').trim().toLowerCase()
const ids = BARE_OS_DISCORD_REST_MODEL_FALLBACKS[key] || BARE_OS_DISCORD_REST_MODEL_FALLBACKS.groq
return (ids || []).map(function (id) {
return { id: id, label: id, family: key, owned_by: key }
})
}
function discordQvacModelForProfile(profileId) {
if (typeof bareAgentQvacModelForProfile === 'function') return bareAgentQvacModelForProfile(profileId)
const id = String(profileId || '').trim().toLowerCase()
for (let i = 0; i < BARE_OS_DISCORD_QVAC_MODELS.length; i++) {
if (BARE_OS_DISCORD_QVAC_MODELS[i].profile === id) return BARE_OS_DISCORD_QVAC_MODELS[i]
}
return BARE_OS_DISCORD_QVAC_MODELS[1] || BARE_OS_DISCORD_QVAC_MODELS[0]
}
function discordFilterModels(models, family, query) {
if (typeof bareAgentFilterModelList === 'function') {
return bareAgentFilterModelList(models, { family: family, query: query })
}
const fam = String(family || 'all').toLowerCase()
const q = String(query || '').toLowerCase()
const out = []
for (let i = 0; i < models.length; i++) {
const m = models[i]
if (fam !== 'all' && String(m.family || '').toLowerCase() !== fam) continue
if (q && (String(m.id) + ' ' + String(m.label || '')).toLowerCase().indexOf(q) === -1) continue
out.push(m)
}
return out
}
function discordAgentFetchFn(ctx) {
if (ctx && typeof ctx.httpFetch === 'function') return ctx.httpFetch.bind(ctx)
if (ctx && ctx.bare && typeof ctx.bare.fetch === 'function') return ctx.bare.fetch.bind(ctx.bare)
if (typeof fetch === 'function') return fetch
return null
}
async function discordFetchRestModels(ctx, cfg) {
const provider = String((cfg && cfg.provider) || 'groq').trim().toLowerCase()
const base = String((cfg && cfg.rest_base_url) || '').trim()
const key = String((cfg && cfg.rest_api_key) || '').trim()
const fetchFn = discordAgentFetchFn(ctx)
if (fetchFn && base) {
try {
if (typeof bareAgentFetchRestModels === 'function') {
return {
models: await bareAgentFetchRestModels(fetchFn, { baseUrl: base, apiKey: key }),
source: 'live',
error: ''
}
}
const res = await fetchFn(base.replace(/\/+$/, '') + '/models', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: key ? 'Bearer ' + key : ''
}
})
if (res && res.ok !== false) {
const json = typeof res.json === 'function' ? await res.json() : JSON.parse(await res.text())
const models = discordParseOpenAiModels(json)
if (models.length) return { models: models, source: 'live', error: '' }
}
} catch (err) {
return {
models: discordRestModelsFallback(provider),
source: 'fallback',
error: String((err && err.message) || err)
}
}
}
return {
models: discordRestModelsFallback(provider),
source: fetchFn ? 'fallback' : 'fallback',
error: fetchFn ? (base ? 'empty_or_denied' : 'no_base_url') : 'no_fetch'
}
}
function discordAgentModelKey(interaction, ctx) {
return discordAgentRunKey(interaction, ctx)
}
function discordAgentModelGet(interaction, ctx) {
const k = discordAgentModelKey(interaction, ctx)
if (!BARE_OS_DISCORD_AGENT_MODELS[k]) {
BARE_OS_DISCORD_AGENT_MODELS[k] = {
page: 0,
family: 'all',
query: '',
source: '',
error: '',
models: [],
backend: ''
}
}
return BARE_OS_DISCORD_AGENT_MODELS[k]
}
async function discordAgentLoadModelChoices(ctx, cfg, rec, force) {
const backend = discordAgentBackend(cfg)
rec.backend = backend
if (backend === 'qvac') {
rec.models = BARE_OS_DISCORD_QVAC_MODELS.slice()
rec.source = 'qvac-catalog'
rec.error = ''
return rec
}
if (!force && rec.models && rec.models.length && rec.source === 'live' && rec.backend === 'rest') {
return rec
}
const got = await discordFetchRestModels(ctx, cfg)
rec.models = got.models
rec.source = got.source
rec.error = got.error || ''
return rec
}
async function discordAgentApplyModel(ctx, modelId) {
const id = String(modelId || '').trim()
if (!id) throw new Error('model id required')
const cfg = (await discordAgentLoadConfig(ctx)) || {}
const backend = discordAgentBackend(cfg)
if (backend === 'qvac') {
cfg.backend = 'qvac'
cfg.provider = 'qvac'
cfg.qvac_model = id
cfg.model = id
const hit = discordQvacModelForProfile('')
for (let i = 0; i < BARE_OS_DISCORD_QVAC_MODELS.length; i++) {
if (BARE_OS_DISCORD_QVAC_MODELS[i].id === id && BARE_OS_DISCORD_QVAC_MODELS[i].profile) {
cfg.qvac_profile = BARE_OS_DISCORD_QVAC_MODELS[i].profile
break
}
}
void hit
} else {
cfg.backend = 'rest'
cfg.model = id
}
const clean = discordSanitizeAgentConfig(cfg)
await discordWriteJsonFile(ctx, '~/.agent/config.json', clean)
return clean
}
async function discordAgentModelView(ctx, interaction, note) {
const cfg = (await discordAgentLoadConfig(ctx)) || {}
const rec = discordAgentModelGet(interaction, ctx)
await discordAgentLoadModelChoices(ctx, cfg, rec, false)
const backend = discordAgentBackend(cfg)
const current = backend === 'qvac' ? String(cfg.qvac_model || cfg.model || '') : String(cfg.model || '')
const filtered = discordFilterModels(rec.models, rec.family, rec.query)
const pages = Math.max(1, Math.ceil(filtered.length / BARE_OS_DISCORD_MODEL_PAGE) || 1)
rec.page = Math.min(Math.max(0, rec.page | 0), pages - 1)
const slice = filtered.slice(rec.page * BARE_OS_DISCORD_MODEL_PAGE, rec.page * BARE_OS_DISCORD_MODEL_PAGE + BARE_OS_DISCORD_MODEL_PAGE)
const fams = { all: 1 }
for (let i = 0; i < rec.models.length; i++) {
const f = String(rec.models[i].family || '').trim()
if (f) fams[f] = 1
}
const famOpts = Object.keys(fams)
.sort()
.map(function (f) {
return { label: f === 'all' ? 'All families' : f, value: f }
})
const modelOpts = slice.map(function (m) {
const mark = m.id === current ? '▸ ' : ''
const extra =
backend === 'qvac'
? (m.tools ? 'tools' : 'no-tools') + (m.ramGb ? ' · ~' + m.ramGb + 'GB' : '')
: String(m.owned_by || m.family || '')
return {
label: (mark + (m.label || m.id)).slice(0, 100),
value: m.id,
description: extra.slice(0, 100)
}
})
const src =
rec.source === 'live'
? 'Live from `' + String(cfg.rest_base_url || '').replace(/^https?:\/\//, '') + '`'
: rec.source === 'qvac-catalog'
? 'QVAC chat catalog (registry ids the host can load)'
: 'Curated fallback' + (rec.error ? ' (' + rec.error + ')' : '')
const fields = [
discordField('Backend', backend, true),
discordField('Current', current || '(unset)', true),
discordField('Shown', String(filtered.length) + ' / ' + String(rec.models.length), true)
]
let desc =
backend === 'qvac'
? 'Pick a **QVAC** chat model. Large ids download on first load.'
: 'Pick a **REST** model. Live list comes from `GET /models` on the configured API.'
desc += '\n' + src + '.'
if (rec.query) desc += '\nFilter: `' + rec.query + '`'
if (note) desc += '\n' + note
const comps = [
discordSelect('agent:mdlfam', 'Family', famOpts),
discordSelect('agent:mdl', 'Model · p' + (rec.page + 1) + '/' + pages, modelOpts),
discordButtons([
{ id: 'agent:mdlprev', label: '← Prev', style: 2 },
{ id: 'agent:mdlnext', label: 'Next →', style: 2 },
{ id: 'agent:mdlref', label: 'Refresh' },
{ id: 'agent:mdltype', label: 'Custom…', style: 1 },
{ id: 'agent:home', label: 'Agent' }
])
].filter(Boolean)
return discordResult(
discordEmbed({
title: backend === 'qvac' ? 'QVAC models' : 'REST models',
desc: desc,
fields: fields,
footer: 'Page ' + (rec.page + 1) + '/' + pages
}),
{ components: comps }
)
}
function discordAgentQvacReady(ctx) {
if (!ctx) return false
if (typeof ctx.bareOsQvacAvailable === 'function') {
try {
return Boolean(ctx.bareOsQvacAvailable())
} catch {
return false
}
}
return (
typeof ctx.bareOsQvacComplete === 'function' ||
typeof ctx.bareOsQvacStatus === 'function'
)
}
async function discordAgentLoadConfig(ctx) {
const paths = discordAgentPaths(ctx)
const raw = await discordCmdReadJson(ctx, paths.config)
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
return raw
}
function discordAgentReady(ctx, cfg) {
if (!cfg) {
return {
ok: false,
backend: '',
model: '',
reason:
'Agent is not configured. Run `agent --setup` on the guest TTY, or set knobs in **/settings → Agent**.'
}
}
const backend = discordAgentBackend(cfg)
if (backend === 'rest') {
if (!String(cfg.rest_api_key || '').trim()) {
return {
ok: false,
backend: backend,
model: String(cfg.model || ''),
reason: 'REST backend has no API key. Run `agent --setup` (not stored in Discord).'
}
}
if (!String(cfg.rest_base_url || '').trim()) {
return {
ok: false,
backend: backend,
model: String(cfg.model || ''),
reason:
'REST backend has no base URL. Set `rest_base_url` in **/settings → Agent** or run `agent --setup`.'
}
}
return {
ok: true,
backend: backend,
model: String(cfg.model || ''),
reason: ''
}
}
const model = String(cfg.qvac_model || cfg.model || '').trim()
if (!model) {
return {
ok: false,
backend: backend,
model: '',
reason: 'QVAC backend has no model. Set `qvac_model` in **/settings → Agent** or run `agent --setup`.'
}
}
if (!discordAgentQvacReady(ctx)) {
return {
ok: false,
backend: backend,
model: model,
reason:
'QVAC host bridge is unavailable (`BARE_OS_SKIP_QVAC` or missing `@qvac/sdk`). Switch to REST via `agent --config`.'
}
}
return { ok: true, backend: backend, model: model, reason: '' }
}
function discordAgentPublicConfig(cfg) {
const out = {}
if (!cfg || typeof cfg !== 'object') return out
const keys = Object.keys(cfg)
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
if (DISCORD_SETTINGS_IS_SECRET(k) || BARE_OS_DISCORD_AGENT_SECRET_RE.test(k)) {
out[k] = cfg[k] ? '[set]' : '[empty]'
continue
}
const v = cfg[k]
if (v != null && typeof v === 'object') continue
out[k] = v
}
return out
}
function DISCORD_SETTINGS_IS_SECRET(k) {
return typeof discordSettingsIsSecretKey === 'function'
? discordSettingsIsSecretKey(k)
: /token|secret|password|api_key/i.test(String(k || ''))
}
function discordAgentRunKey(interaction, ctx) {
return (
discordInteractionUserId(interaction) ||
discordSessionUser(ctx) ||
'anon'
)
}
function discordAgentGetRun(interaction, ctx) {
return BARE_OS_DISCORD_AGENT_RUNS[discordAgentRunKey(interaction, ctx)] || null
}
function discordAgentSetRun(interaction, ctx, rec) {
const k = discordAgentRunKey(interaction, ctx)
if (!rec) {
delete BARE_OS_DISCORD_AGENT_RUNS[k]
return
}
BARE_OS_DISCORD_AGENT_RUNS[k] = rec
}
function discordAgentProgressTail(text, n) {
const lines = String(text || '')
.split(/\r?\n/)
.filter(Boolean)
return lines.slice(-(n || 8)).join('\n')
}
function discordAgentParseProgress(text) {
const lines = String(text || '')
.split(/\r?\n/)
.map(function (l) {
return l.replace(/^\d{4}-\d{2}-\d{2}T\S+\s+/, '').trim()
})
.filter(Boolean)
const steps = []
let iteration = 0
let tools = []
let done = ''
for (let i = 0; i < lines.length; i++) {
const body = lines[i]
const iter = /^iteration\s+(\d+)\s+tools\s+(\S+)/i.exec(body)
if (iter) {
iteration = Number(iter[1]) || 0
tools = String(iter[2] || '')
.split(',')
.map(function (t) {
return t.trim()
})
.filter(Boolean)
continue
}
if (/^task_complete:/i.test(body)) {
done = body.replace(/^task_complete:\s*/i, '').trim()
continue
}
if (/^autonomous /i.test(body) || /^provider_shape_keys\b/i.test(body)) continue
const sp = body.indexOf(' ')
const tool = sp === -1 ? body : body.slice(0, sp)
const detail = sp === -1 ? '' : body.slice(sp + 1)
if (tool) steps.push({ tool: tool, detail: detail })
}
return {
steps: steps,
iteration: iteration,
tools: tools,
done: done,
current: steps.length ? steps[steps.length - 1] : null
}
}
function discordAgentSplitStdout(raw) {
const text = discordStripAnsi(String(raw == null ? '' : raw))
const cleaned = text.replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<think>[\s\S]*$/i, '')
const lines = cleaned.split(/\r?\n/)
const answer = []
const process = []
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (/^→\s+\S/.test(line) || /^\[process\]/.test(line) || /^Done:\s/.test(line)) {
process.push(line)
continue
}
if (/^[.…·•]+$/.test(line.trim())) continue
if (/^\[harness\]/.test(line)) continue
answer.push(line)
}
return {
answer: answer.join('\n').replace(/^\s+|\s+$/g, ''),
process: process
}
}
function discordAgentHeadingsToBold(text) {
return String(text || '').replace(/^#{1,6}\s+(.+)$/gm, '**$1**')
}
function discordAgentFormatProcess(parsed, limit) {
const steps = (parsed && parsed.steps) || []
const keep = Math.max(3, Number(limit) || 8)
const slice = steps.length > keep ? steps.slice(-keep) : steps
if (!slice.length) return ''
const omitted = steps.length - slice.length
const lines = []
if (omitted > 0) lines.push('_+' + String(omitted) + ' earlier steps_')
for (let i = 0; i < slice.length; i++) {
const s = slice[i]
const detail = String(s.detail || '').replace(/\s+/g, ' ').slice(0, 80)
lines.push('- `' + s.tool + '`' + (detail ? ' ' + detail : ''))
}
return lines.join('\n')
}
async function discordAgentHistoryCount(ctx) {
const paths = discordAgentPaths(ctx)
const raw = await discordCmdReadJson(ctx, paths.history)
return Array.isArray(raw) ? raw.length : 0
}
function discordAgentSleep(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms)
})
}
function discordAgentEnvPatch(ctx, key, val) {
const prev = []
function setOn(obj) {
if (!obj || typeof obj !== 'object') return
prev.push([obj, key, obj[key]])
obj[key] = val
}
setOn(ctx && ctx.env)
setOn(ctx && ctx.vfs && ctx.vfs.env)
return function restore() {
for (let i = 0; i < prev.length; i++) {
const row = prev[i]
if (row[2] === undefined) delete row[0][row[1]]
else row[0][row[1]] = row[2]
}
}
}
async function discordAgentHud(ctx, interaction, note) {
const cfg = await discordAgentLoadConfig(ctx)
const ready = discordAgentReady(ctx, cfg)
const run = discordAgentGetRun(interaction, ctx)
const hist = await discordAgentHistoryCount(ctx)
const paths = discordAgentPaths(ctx)
const progress = discordAgentProgressTail(
await discordCmdReadText(ctx, paths.progress),
4
)
let qvac = ''
if (typeof ctx.bareOsQvacStatus === 'function') {
try {
const st = ctx.bareOsQvacStatus()
if (st && typeof st === 'object') {
qvac =
String(st.status || (st.available ? 'available' : 'unavailable')) +
(st.modelId ? ' · ' + st.modelId : '')
}
} catch {
qvac = ''
}
}
const fields = [
discordField('Ready', ready.ok ? '**yes**' : '**no**', true),
discordField('Backend', ready.backend || '—', true),
discordField('Model', ready.model || '—', true),
discordField('History', String(hist) + ' msgs', true),
discordField(
'Run',
run
? '**working**' + (run.currentTool ? ' · `' + run.currentTool + '`' : '')
: 'idle',
true
),
discordField(
'Plan mode',
cfg && cfg.plan_mode_active ? '**on**' : 'off',
true
),
discordField('Compaction', String((cfg && cfg.context_compaction) || 'auto'), true),
discordField(
'Autonomous',
cfg && cfg.autonomous_mode_enabled === false ? 'off' : 'on',
true
)
]
if (qvac) fields.push(discordField('QVAC', qvac, true))
const label = cfg && cfg.agent_label ? String(cfg.agent_label) : ''
const owner = cfg && cfg.owner_name ? String(cfg.owner_name) : ''
if (label || owner) {
fields.push(discordField('Identity', (label || 'agent') + (owner ? ' · ' + owner : ''), true))
}
let desc = ready.ok
? 'Talk to the guest **agent** (`~/.agent`). Same session as `/bin/agent` — history, skills, todos, plan, and memory are shared.'
: ready.reason
if (run && run.prompt) desc += '\nCurrent: _' + String(run.prompt).slice(0, 180) + '_'
if (note) desc += '\n' + note
if (progress) desc += '\n' + discordCmdFence(progress, '', 700)
const btns = []
if (ready.ok) {
btns.push({ id: 'agent:ask', label: 'Ask…', style: 1 })
if (run) btns.push({ id: 'agent:stop', label: 'Stop', style: 4 })
btns.push({ id: 'agent:reset', label: 'Reset chat', style: 4 })
}
btns.push({ id: 'agent:models', label: 'Models' })
btns.push({ id: 'agent:status', label: 'Status' })
btns.push({ id: 'agent:config', label: 'Config' })
if (ready.ok) {
btns.push({ id: 'agent:recap', label: 'Recap' })
btns.push({ id: 'agent:compact', label: 'Compact' })
}
btns.push({ id: 'agent:refresh', label: 'Refresh' })
const rows = []
for (let i = 0; i < btns.length; i += 5) {
const row = discordButtons(btns.slice(i, i + 5))
if (row) rows.push(row)
}
if (ready.ok) {
const inspect = discordSelect('agent:inspect', 'Inspect / session…', [
{ label: 'Skills catalog', value: 'skills', description: 'agent skills' },
{ label: 'Todos', value: 'todos', description: 'agent todos' },
{ label: 'Plan.md', value: 'plan', description: 'agent plan' },
{ label: 'History search…', value: 'history', description: 'agent history [query]' },
{ label: 'Recap last turn', value: 'recap', description: 'agent recap' },
{ label: 'Hooks', value: 'hooks', description: 'agent hooks' },
{ label: 'Undo last edit', value: 'undo', description: 'agent undo' },
{ label: 'Rewind last user turn', value: 'rewind', description: 'agent rewind 1' },
{ label: 'Compact history', value: 'compact', description: 'agent compact' },
{ label: 'Export transcript', value: 'export', description: 'agent export' },
{ label: 'Remember a fact…', value: 'remember', description: 'agent remember TEXT' }
])
if (inspect) rows.push(inspect)
}
return discordResult(
discordEmbed({
title: 'Agent',
color: ready.ok
? run
? BARE_OS_DISCORD_COLOR_WARN
: BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR_WARN,
desc: desc,
fields: fields,
footer: paths.config
}),
{ components: rows }
)
}
async function discordAgentConfigView(ctx) {
const cfg = await discordAgentLoadConfig(ctx)
const pub = discordAgentPublicConfig(cfg)
const keys = Object.keys(pub)
const fields = []
for (let i = 0; i < keys.length && fields.length < 20; i++) {
fields.push(discordField(keys[i], '`' + String(pub[keys[i]]) + '`', true))
}
return discordResult(
discordEmbed({
title: 'Agent config',
desc:
'Non-secret keys from `~/.agent/config.json`. API keys stay on the guest drive. Edit via **/settings → Agent**.',
fields: fields.length
? fields
: [discordField('config', 'Missing. Run `agent --setup` on the TTY.')],
footer: discordAgentPaths(ctx).config
}),
{ components: [discordButtons([{ id: 'agent:home', label: 'Agent' }])] }
)
}
async function discordAgentReset(ctx, interaction) {
const paths = discordAgentPaths(ctx)
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
return { text: 'vfs.writeFile unavailable — cannot reset agent history.', ephemeral: true }
}
if (typeof ctx.vfs.mkdir === 'function') {
try {
await ctx.vfs.mkdir(paths.dir, { recursive: true })
} catch {
/* ignore */
}
}
const emptyHist =
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
? ctx.b4a.from('[]\n')
: typeof Buffer !== 'undefined'
? Buffer.from('[]\n')
: '[]\n'
const emptyProg =
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
? ctx.b4a.from('')
: typeof Buffer !== 'undefined'
? Buffer.from('')
: ''
await ctx.vfs.writeFile(paths.history, emptyHist)
try {
await ctx.vfs.writeFile(paths.progress, emptyProg)
} catch {
/* optional */
}
return discordAgentHud(ctx, interaction, 'Chat history cleared (`history.json` + `progress.txt`). Config kept.')
}
async function discordAgentStop(ctx, interaction) {
const run = discordAgentGetRun(interaction, ctx)
if (!run) return discordAgentHud(ctx, interaction, 'No agent run is active.')
run.stop = true
try {
if (typeof ctx.bareOsAbortActiveAgent === 'function') ctx.bareOsAbortActiveAgent()
} catch {
/* ignore */
}
return discordAgentHud(ctx, interaction, 'Stop requested. The in-flight turn may take a moment to unwind.')
}
var BARE_OS_DISCORD_AGENT_INSPECT = {
skills: { title: 'Skills', fence: true },
todos: { title: 'Todos', fence: false },
plan: { title: 'Plan', fence: false },
hooks: { title: 'Hooks', fence: true },
history: { title: 'History', fence: true },
recap: { title: 'Recap', fence: false },
undo: { title: 'Undo', fence: false },
rewind: { title: 'Rewind', fence: false },
compact: { title: 'Compact', fence: false },
export: { title: 'Export', fence: false },
remember: { title: 'Remember', fence: false }
}
async function discordAgentInspect(ctx, interaction, sub, extra) {
const name = String(sub || '').trim()
const spec = BARE_OS_DISCORD_AGENT_INSPECT[name]
if (!spec) return discordAgentHud(ctx, interaction, 'Unknown inspect command.')
if (name === 'remember' && !String(extra || '').trim()) {
return { modal: 'agent:remember' }
}
if (name === 'history' && extra === undefined) {
return { modal: 'agent:history' }
}
if (typeof ctx.execLine !== 'function') {
return { text: 'execLine unavailable — cannot run `agent ' + name + '`.', ephemeral: true }
}
const extraArg = extra == null ? '' : String(extra).trim()
let line = 'agent ' + name
if (extraArg) line += ' ' + discordShQuote(extraArg)
let out = ''
let errNote = ''
try {
out = await discordCmdCapture(ctx, function () {
return ctx.execLine(line, { timeoutMs: 45000 })
})
} catch (err) {
errNote = String((err && err.message) || err)
out = (out ? out + '\n' : '') + errNote
}
const raw = discordStripAnsi(String(out || '')).replace(/^\s+|\s+$/g, '')
const body = discordAgentHeadingsToBold(discordCmdRedact(raw || '(empty)'))
const comps = [
discordButtons([
{ id: 'agent:home', label: 'Agent' },
{ id: 'agent:ask', label: 'Ask…', style: 1 }
])
]
if (spec.fence) {
return discordResult(
discordEmbed({
title: 'Agent · ' + spec.title,
desc: discordCmdFence(body, '', 3500),
color: errNote ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'guest /bin/agent ' + name
}),
{ components: comps }
)
}
return {
more: {
title: 'Agent · ' + spec.title,
lead: '',
body: body,
fence: false,
color: errNote ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'guest /bin/agent ' + name,
components: comps
}
}
}
async function discordAgentAsk(ctx, interaction, prompt, flags) {
const task = String(prompt || '').trim()
if (!task) {
return {
text: 'Prompt is required. Use `/agent ask` and fill **prompt** — Discord will not let you submit an empty one.',
ephemeral: true
}
}
if (task.length > 4000) {
return { text: 'Prompt is too long (max 4000).', ephemeral: true }
}
const wantNew = Boolean(flags && (flags.fresh || flags.new))
const wantPlan = Boolean(flags && flags.plan)
const wantAuto = Boolean(flags && (flags.auto || flags.autonomous))
const wantCompact = Boolean(flags && flags.compact)
const maxTurns = Math.max(0, Math.floor(Number(flags && flags.maxTurns) || 0))
const modelOverride = flags && flags.model ? String(flags.model).trim() : ''
const cfg = await discordAgentLoadConfig(ctx)
const ready = discordAgentReady(ctx, cfg)
if (!ready.ok) {
return discordAgentHud(ctx, interaction, ready.reason)
}
if (discordAgentGetRun(interaction, ctx)) {
return {
text: 'An agent turn is already running for you. Use **Stop** first.',
ephemeral: true
}
}
if (typeof ctx.execLine !== 'function') {
return { text: 'execLine unavailable in this session.', ephemeral: true }
}
await discordDefer(interaction)
const rec = { prompt: task, stop: false, started: Date.now(), currentTool: '' }
discordAgentSetRun(interaction, ctx, rec)
const restoreDiscord = discordAgentEnvPatch(ctx, 'BARE_OS_AGENT_DISCORD', '1')
const restoreThink = discordAgentEnvPatch(ctx, 'BARE_OS_AGENT_HIDE_THINK', '1')
let out = ''
let errNote = ''
let polling = true
const paths = discordAgentPaths(ctx)
try {
if (ctx.vfs && typeof ctx.vfs.writeFile === 'function') {
const empty =
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
? ctx.b4a.from('')
: typeof Buffer !== 'undefined'
? Buffer.from('')
: ''
await ctx.vfs.writeFile(paths.progress, empty)
}
} catch {
/* optional */
}
const poll = (async function () {
while (polling) {
const t0 = Date.now()
while (polling && Date.now() - t0 < BARE_OS_DISCORD_AGENT_POLL_MS) {
await discordAgentSleep(200)
}
if (!polling) break
try {
const parsed = discordAgentParseProgress(await discordCmdReadText(ctx, paths.progress))
if (parsed.current) rec.currentTool = parsed.current.tool
const elapsed = Math.round((Date.now() - rec.started) / 1000)
const proc = discordAgentFormatProcess(parsed, 8)
const fields = [
discordField('Status', rec.stop ? 'stopping' : '**working**', true),
discordField('Backend', ready.backend + ' · `' + (ready.model || 'model') + '`', true),
discordField('Elapsed', String(elapsed) + 's', true),
discordField(
'Current',
parsed.current ? '`' + parsed.current.tool + '`' : '_starting…_',
true
)
]
if (parsed.iteration) {
fields.push(discordField('Iteration', String(parsed.iteration), true))
}
if (proc) fields.push(discordField('Process', proc))
await discordSendResult(
ctx,
interaction,
discordResult(
discordEmbed({
title: 'Agent · working',
color: BARE_OS_DISCORD_COLOR_WARN,
desc: '_' + task.slice(0, 280) + '_',
fields: fields,
footer: 'Live from ~/.agent/progress.txt · Stop anytime'
}),
{
components: [
discordButtons([
{ id: 'agent:stop', label: 'Stop', style: 4 },
{ id: 'agent:home', label: 'Agent' }
])
]
}
)
)
} catch {
/* keep polling */
}
}
})()
try {
out = await discordCmdCapture(ctx, function () {
let line = 'agent'
if (wantNew) line += ' --new'
if (wantPlan) line += ' --plan'
if (wantAuto) line += ' --auto'
if (wantCompact) line += ' --compact'
if (maxTurns > 0) line += ' --max-turns ' + String(maxTurns)
if (modelOverride) line += ' --model ' + discordShQuote(modelOverride)
line += ' ' + discordShQuote(task)
return ctx.execLine(line, {
timeoutMs: BARE_OS_DISCORD_AGENT_MS
})
})
} catch (err) {
errNote = String((err && err.message) || err)
out = (out ? out + '\n' : '') + errNote
} finally {
polling = false
restoreDiscord()
restoreThink()
discordAgentSetRun(interaction, ctx, null)
}
try {
await poll
} catch {
/* ignore */
}
const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0
const parsed = discordAgentParseProgress(await discordCmdReadText(ctx, paths.progress))
const split = discordAgentSplitStdout(out)
let answer = discordAgentHeadingsToBold(discordCmdRedact(split.answer || ''))
if (!answer && parsed.done) answer = discordCmdRedact(parsed.done)
const elapsed = Math.round((Date.now() - rec.started) / 1000)
const proc = discordAgentFormatProcess(parsed, 10)
const leadParts = [
'**' + ready.backend + '** · `' + (ready.model || 'model') + '` · ' + elapsed + 's',
'_' + task.slice(0, 240) + '_'
]
if (proc) leadParts.push('', proc, '')
const lead = leadParts.join('\n')
const comps = [
discordButtons([
{ id: 'agent:ask', label: 'Ask again…', style: 1 },
{ id: 'agent:reset', label: 'Reset chat', style: 4 },
{ id: 'agent:home', label: 'Agent' }
])
]
if (!answer) {
return discordResult(
discordEmbed({
title: rec.stop ? 'Agent · stopped' : 'Agent',
desc: lead + '\n_(no markdown reply)_',
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'exit ' + exit
}),
{ components: comps }
)
}
return {
more: {
title: rec.stop ? 'Agent · stopped' : 'Agent',
lead: lead + (lead && answer ? '\n' : ''),
body: answer,
fence: false,
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
footer: 'exit ' + exit + ' · Discord markdown · shared ~/.agent/history.json',
components: comps
}
}
}
async function discordCmdHandleAgent(ctx, interaction, sub, opt) {
const s = String(sub || 'list')
if (s === 'help') {
return discordResult(
discordEmbed({
title: 'Agent',
desc:
'Guest `/bin/agent` from Discord. Uses `~/.agent/config.json` and the same history as the TTY.\n' +
'Hidden until configured (QVAC model + host bridge, or REST + API key).',
fields: [
discordField('/agent', 'Interactive manager (default)'),
discordField(
'/agent ask',
'**prompt** (required). Optional `new` / `plan` / `auto` / `compact` / `max_turns` / `model`.'
),
discordField('/agent status', 'Ready / backend / model / QVAC / plan / autonomous'),
discordField('/agent config', 'Non-secret ~/.agent/config.json'),
discordField('/agent models', 'QVAC catalog or live REST /models picker'),
discordField(
'/agent skills | todos | plan | hooks | history | recap',
'Same inspect surface as `/bin/agent`'
),
discordField(
'/agent undo | rewind | compact | export | remember',
'Session edits, rewind, compaction, transcript, MEMORY.md'
),
discordField('/agent reset', 'Clear history.json (keeps config)'),
discordField('/agent stop', 'Abort the in-flight turn')
]
})
)
}
if (s === 'status' || s === 'list') return discordAgentHud(ctx, interaction, '')
if (s === 'config') return discordAgentConfigView(ctx)
if (s === 'models' || s === 'model') return discordAgentModelView(ctx, interaction, '')
if (s === 'reset') return discordAgentReset(ctx, interaction)
if (s === 'stop') return discordAgentStop(ctx, interaction)
if (s === 'ask') {
return discordAgentAsk(ctx, interaction, opt('prompt') || opt('text') || '', {
fresh: opt('new') === 'true',
plan: opt('plan') === 'true',
auto: opt('auto') === 'true' || opt('autonomous') === 'true',
compact: opt('compact') === 'true',
maxTurns: opt('max_turns') || opt('max-turns'),
model: opt('model')
})
}
if (BARE_OS_DISCORD_AGENT_INSPECT[s]) {
let extra = ''
if (s === 'remember') extra = opt('text') || opt('note') || ''
else if (s === 'history') extra = opt('query') || opt('q') || ''
else if (s === 'rewind') extra = opt('steps') || opt('n') || ''
else if (s === 'export') extra = opt('path') || ''
return discordAgentInspect(ctx, interaction, s, extra)
}
return discordAgentHud(ctx, interaction, '')
}
async function discordAgentAction(ctx, interaction, id) {
if (id === 'agent:home' || id === 'agent:refresh' || id === 'agent:status') {
return discordSendResult(ctx, interaction, await discordAgentHud(ctx, interaction, ''))
}
if (id === 'agent:ask') {
return discordSendResult(ctx, interaction, { modal: 'agent:ask' })
}
if (id === 'agent:config') {
return discordSendResult(ctx, interaction, await discordAgentConfigView(ctx))
}
if (id === 'agent:reset') {
return discordSendResult(ctx, interaction, await discordAgentReset(ctx, interaction))
}
if (id === 'agent:stop') {
return discordSendResult(ctx, interaction, await discordAgentStop(ctx, interaction))
}
if (id === 'agent:recap' || id === 'agent:compact' || id === 'agent:undo') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, id.slice('agent:'.length), '')
)
}
if (id === 'agent:inspect') {
const which = String((interaction.values && interaction.values[0]) || '')
if (which === 'remember') {
return discordSendResult(ctx, interaction, { modal: 'agent:remember' })
}
if (which === 'history') {
return discordSendResult(ctx, interaction, { modal: 'agent:history' })
}
if (BARE_OS_DISCORD_AGENT_INSPECT[which]) {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, which, which === 'rewind' ? '1' : '')
)
}
return discordSendResult(ctx, interaction, await discordAgentHud(ctx, interaction, ''))
}
if (
id === 'agent:models' ||
id === 'agent:mdl' ||
id === 'agent:mdlfam' ||
id === 'agent:mdlprev' ||
id === 'agent:mdlnext' ||
id === 'agent:mdlref' ||
id === 'agent:mdltype'
) {
const rec = discordAgentModelGet(interaction, ctx)
if (id === 'agent:mdlfam') {
rec.family = String((interaction.values && interaction.values[0]) || 'all')
rec.page = 0
} else if (id === 'agent:mdl') {
const mid = String((interaction.values && interaction.values[0]) || '')
try {
await discordAgentApplyModel(ctx, mid)
return discordSendResult(
ctx,
interaction,
await discordAgentModelView(ctx, interaction, 'Now using **' + mid + '**.')
)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Could not set model: ' + ((err && err.message) || err),
ephemeral: true
})
}
} else if (id === 'agent:mdlprev') {
rec.page = Math.max(0, (rec.page | 0) - 1)
} else if (id === 'agent:mdlnext') {
rec.page = (rec.page | 0) + 1
} else if (id === 'agent:mdlref') {
rec.models = []
rec.source = ''
const cfg = (await discordAgentLoadConfig(ctx)) || {}
await discordAgentLoadModelChoices(ctx, cfg, rec, true)
} else if (id === 'agent:mdltype') {
return discordSendResult(ctx, interaction, { modal: 'agent:mdltype' })
}
return discordSendResult(ctx, interaction, await discordAgentModelView(ctx, interaction, ''))
}
return discordSendResult(ctx, interaction, await discordAgentHud(ctx, interaction, ''))
}
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) +
'`\nOpen **Menu** for destinations, 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)
]
}),
{
components: [
discordButtons([
{ id: 'agent:home', label: 'Agent', style: 1 },
{ id: 'hdms:home', label: 'HDMS' },
{ id: 'holesail:home', label: 'Holesail' },
{ id: 'run:compose', label: 'Shell' }
])
]
}
)
}
function discordCmdOpt(s, name, desc, required, autocomplete, extra) {
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)
const min = extra && Number(extra.min)
const max = extra && Number(extra.max)
if (min > 0 && typeof o.setMinLength === 'function') o.setMinLength(Math.floor(min))
if (max > 0 && typeof o.setMaxLength === 'function') o.setMaxLength(Math.floor(max))
return o
})
}
function discordCmdBool(s, name, desc) {
if (!s || typeof s.addBooleanOption !== 'function') return s
return s.addBooleanOption(function (o) {
o.setName(name).setDescription(desc)
return o
})
}
function discordCmdInt(s, name, desc, extra) {
if (!s || typeof s.addIntegerOption !== 'function') {
return discordCmdOpt(s, name, desc, false)
}
return s.addIntegerOption(function (o) {
o.setName(name).setDescription(desc)
if (extra && extra.min != null && typeof o.setMinValue === 'function') {
o.setMinValue(Math.floor(Number(extra.min)))
}
if (extra && extra.max != null && typeof o.setMaxValue === 'function') {
o.setMaxValue(Math.floor(Number(extra.max)))
}
return o
})
}
function discordCmdAtt(s, name, desc, required) {
if (!s || typeof s.addAttachmentOption !== 'function') return s
return s.addAttachmentOption(function (o) {
o.setName(name).setDescription(desc)
if (required && typeof o.setRequired === 'function') o.setRequired(true)
return o
})
}
function discordShQuote(s) {
return "'" + String(s == null ? '' : s).replace(/'/g, "'\\''") + "'"
}
function discordUploadSafeName(name) {
let n = String(name || '').replace(/\\/g, '/')
const slash = n.lastIndexOf('/')
if (slash >= 0) n = n.slice(slash + 1)
n = n.replace(/[\u0000-\u001f]/g, '').trim()
if (!n || n === '.' || n === '..') n = 'upload.bin'
if (n.length > 180) {
const i = n.lastIndexOf('.')
const ext = i > 0 && i > n.length - 12 ? n.slice(i) : ''
n = n.slice(0, 180 - ext.length) + ext
}
return n
}
function discordInteractionAttachment(interaction, name) {
const key = name || 'file'
if (!interaction || !interaction.options) return null
if (typeof interaction.options.getAttachment === 'function') {
try {
const a = interaction.options.getAttachment(key)
if (a) return a
} catch {
/* ignore */
}
}
return null
}
async function discordUploadResolveDest(ctx, interaction, destRaw, filename) {
const rec = discordShGet(interaction, ctx)
const cwd = rec.cwd || '~'
const raw = String(destRaw || '').trim()
if (!raw || raw === '.' || raw === './') return discordJoinPath(cwd, filename)
if (raw.charAt(raw.length - 1) === '/') {
return discordJoinPath(raw.replace(/\/+$/, '') || cwd, filename)
}
try {
const st = await discordFmStat(ctx, raw)
if (discordIsDirStat(st)) return discordJoinPath(raw, filename)
} catch {
/* treat as a file path */
}
if (raw.indexOf('/') < 0 && raw.charAt(0) !== '~') return discordJoinPath(cwd, raw)
return raw
}
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,
r: 1,
journal: 1,
edit: 1,
create: 1,
files: 1,
browse: 1,
settings: 1,
panel: 1,
ping: 1,
plugins: 1,
upload: 1,
hdms: 1,
holesail: 1,
agent: 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-, 132)')
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, ctx) {
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(discordStampUserInstallCommand(c.toJSON(), ctx))
} 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('r')
.setDescription('Run a Bare OS shell command (cwd, pipes, cd, history)')
const journal = new B2()
.setName('journal')
.setDescription('Tail a unit or system log')
const edit = new B2()
.setName('edit')
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
const create = new B2()
.setName('create')
.setDescription('Create a new text file (pick a path, then enter contents)')
const files = new B2()
.setName('files')
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
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 upload = new B2()
.setName('upload')
.setDescription('Upload an attachment; wget saves it in the /r cwd or path')
const hdms = new B2()
.setName('hdms')
.setDescription('Hyperdrive management (list, create, add, invite, pair)')
const holesail = new B2()
.setName('holesail')
.setDescription('Holesail tunnels (list, add, edit, start/stop, service)')
const agent = new B2()
.setName('agent')
.setDescription('Guest AI agent — full /bin/agent harness (ask, inspect, session)')
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)
}]
]) &&
discordCmdAddSubs(hdms, [
['list', 'Interactive drive manager'],
['help', 'HDMS command map'],
['health', 'hdms_health.json snapshot'],
['hints', 'Pairing hints (no secrets)'],
['show', 'Show a drive record', function (s) {
discordCmdOpt(s, 'label', 'Drive label', true, true)
}],
['create', 'Create a writable drive', function (s) {
discordCmdOpt(s, 'label', 'New label (mounted at /mnt/<label>)', true, false)
}],
['add', 'Mount a read-only z32 key', function (s) {
discordCmdOpt(s, 'label', 'Mount label', true, false)
discordCmdOpt(s, 'key', 'Hyperdrive public key (z32)', true, false)
}],
['remove', 'Unmount and drop a registry entry', function (s) {
discordCmdOpt(s, 'label', 'Drive label', true, true)
}],
['invite', 'Mint an Autopass invite', function (s) {
discordCmdOpt(s, 'label', 'Optional writable drive to share', false, true)
discordCmdOpt(s, 'mode', 'pair (default), readonly, or rw', false, false)
}],
['pair', 'Accept an invite', function (s) {
discordCmdOpt(s, 'invite', 'Invite token from the peer', true, false)
discordCmdOpt(s, 'persist', 'yes (default) or ephemeral', false, false)
}]
]) &&
discordCmdAddSubs(holesail, [
['list', 'Interactive tunnel manager'],
['help', 'Holesail command map'],
['status', 'Daemon + connection counts'],
['path', 'state.json path'],
['logs', 'bare-holesail journal'],
['show', 'Show a connection (no seed)', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['url', 'Shareable hs:// URL', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['add', 'Add a server or client', function (s) {
discordCmdOpt(s, 'id', 'New connection id', false, false)
discordCmdOpt(s, 'mode', 'server or client', false, false)
discordCmdOpt(s, 'port', 'Local port', false, false)
discordCmdOpt(s, 'host', 'Bind / connect host', false, false)
discordCmdOpt(s, 'key', 'hs:// URL (client)', false, false)
discordCmdOpt(s, 'udp', 'yes or no', false, false)
discordCmdOpt(s, 'secure', 'yes or no', false, false)
}],
['edit', 'Update fields in place', function (s) {
discordCmdOpt(s, 'id', 'Connection id', false, true)
discordCmdOpt(s, 'port', 'Local port', false, false)
discordCmdOpt(s, 'host', 'Host, or clear', false, false)
discordCmdOpt(s, 'key', 'hs:// URL', false, false)
discordCmdOpt(s, 'udp', 'yes or no', false, false)
discordCmdOpt(s, 'secure', 'yes or no', false, false)
}],
['remove', 'Drop a connection', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['start', 'Start a live tunnel', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['stop', 'Stop a live tunnel', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['restart', 'Restart a live tunnel', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['enable', 'Enable and start if the daemon is up', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['disable', 'Disable and stop if live', function (s) {
discordCmdOpt(s, 'id', 'Connection id', true, true)
}],
['service', 'Control the bare-holesail unit', function (s) {
discordCmdOpt(s, 'action', 'start, stop, restart, or status', false, false)
}]
]) &&
discordCmdAddSubs(agent, [
['list', 'Interactive agent manager'],
['help', 'Agent command map'],
['status', 'Ready / backend / model / QVAC / plan / autonomous'],
['config', 'Non-secret ~/.agent/config.json'],
['models', 'Pick QVAC or REST model'],
['ask', 'Run one agent turn', function (s) {
discordCmdOpt(
s,
'prompt',
'What should the agent do? Required — cannot be empty.',
true,
false,
{ min: 1, max: 4000 }
)
discordCmdBool(s, 'new', 'Start a fresh chat (clear history first)')
discordCmdBool(s, 'plan', 'Start in plan mode (read-only until exit_plan_mode)')
discordCmdBool(s, 'auto', 'Autonomous until task_complete / timebox')
discordCmdBool(s, 'compact', 'Compact history before this turn')
discordCmdInt(s, 'max_turns', 'Cap ReAct iterations for this run', { min: 1, max: 200 })
discordCmdOpt(s, 'model', 'Override QVAC / REST model for this run', false, false)
}],
['skills', 'List discovered SKILL.md ids'],
['todos', 'Print session todos'],
['plan', 'Print ~/.agent/plan.md'],
['hooks', 'List ~/.agent/hooks/*.json'],
['history', 'Search or tail chat history', function (s) {
discordCmdOpt(s, 'query', 'Optional keyword filter', false, false)
}],
['recap', 'Last user + assistant pair'],
['undo', 'Restore the last file edit snapshot'],
['rewind', 'Drop the last N user turns', function (s) {
discordCmdInt(s, 'steps', 'How many user turns to drop (default 1)', { min: 1, max: 50 })
}],
['compact', 'Compact history now'],
['export', 'Write a Markdown transcript', function (s) {
discordCmdOpt(s, 'path', 'Dest path (default ~/.agent/export.md)', false, false)
}],
['remember', 'Append a FACT to MEMORY.md', function (s) {
discordCmdOpt(s, 'text', 'What to remember', true, false, { min: 1, max: 1000 })
}],
['reset', 'Clear chat history (keeps config)'],
['stop', 'Abort the in-flight turn']
])
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)', true, true)
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
discordCmdOpt(files, 'path', 'Directory to open', false, true)
discordCmdAtt(upload, 'file', 'File to upload (Discord CDN, then wget into the VFS)', true)
discordCmdOpt(upload, 'path', 'VFS dest (file or dir; default: /r cwd + name)', false, true)
const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping, upload, hdms, holesail, agent].map(
function (c) {
return discordStampUserInstallCommand(c.toJSON(), ctx)
}
)
return stock.concat(discordPluginsSlash(dj, ctx))
}
if (ctx && typeof ctx.then !== 'function') {
return Promise.resolve(discordPluginsEnsure(ctx)).then(finish)
}
return finish()
}
function discordShowForm(interaction, spec) {
if (!interaction || typeof interaction.showModal !== 'function') return false
const fields = spec.fields && spec.fields.length
? spec.fields
: [
{
id: spec.field || 'text',
label: spec.label || 'Value',
paragraph: spec.paragraph,
max: spec.max,
placeholder: spec.placeholder,
required: spec.required
}
]
const components = []
for (let i = 0; i < fields.length && components.length < 5; i++) {
const f = fields[i]
if (!f) continue
const input = {
type: 4,
custom_id: String(f.id || 'text').slice(0, 100),
label: String(f.label || 'Value').slice(0, 45),
style: f.paragraph ? 2 : 1,
required: f.required !== false,
max_length: f.max || 200,
placeholder: f.placeholder || ''
}
if (f.min > 0) input.min_length = Math.floor(Number(f.min))
components.push({
type: 1,
components: [input]
})
}
if (!components.length) return false
return interaction.showModal({
custom_id: spec.id,
title: String(spec.title || 'Bare OS').slice(0, 45),
components: components
})
}
function discordShowModal(interaction, spec) {
return discordShowForm(interaction, spec)
}
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 === 'r' || name === 'run') return { run: opt('cmd') || '' }
if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
if (name === 'edit') {
const path = opt('path')
if (!path) return { modal: 'edit:path' }
return { edit: path }
}
if (name === 'create') {
const path = opt('path')
if (!path) return { createPick: true }
return { create: path }
}
if (name === '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 (20000 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, user-install (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: 'user_install',
group: 'discord',
label: 'User-installable app',
kind: 'bool',
env: 'DISCORD_USER_INSTALL',
persist: 'discord',
live: 'next bot start'
},
{
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_provider',
group: 'agent',
label: 'REST provider',
kind: 'enum',
jsonKey: 'provider',
values: ['groq', 'xai', 'openai', 'custom'],
persist: 'agent',
backend: 'rest',
live: 'next agent'
},
{
id: 'ag_profile',
group: 'agent',
label: 'QVAC profile',
kind: 'enum',
jsonKey: 'qvac_profile',
values: ['recommended', 'strong', 'tool-tiny', 'lite'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'ag_device',
group: 'agent',
label: 'QVAC device',
kind: 'enum',
jsonKey: 'qvac_device',
values: ['auto', 'cpu', 'gpu'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'ag_ctx',
group: 'agent',
label: 'QVAC context size',
kind: 'enum',
jsonKey: 'qvac_ctx_size',
values: ['0', '8192', '16384', '32768', '131072'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'ag_model',
group: 'agent',
label: 'Model',
kind: 'model',
jsonKey: 'model',
persist: 'agent',
live: 'opens model picker'
},
{
id: 'ag_access',
group: 'agent',
label: 'Access policy',
kind: 'enum',
jsonKey: 'access_policy',
values: ['full', 'restricted'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_compact',
group: 'agent',
label: 'Compaction',
kind: 'enum',
jsonKey: 'context_compaction',
values: ['auto', 'aggressive', 'off'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_auto',
group: 'agent',
label: 'Autonomous mode',
kind: 'bool',
jsonKey: 'autonomous_mode_enabled',
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',
backend: 'rest',
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: 'ag_emerg',
group: 'agent',
label: 'Emergency stop mutations',
kind: 'bool',
jsonKey: 'emergency_stop_mutations',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_nudge',
group: 'agent',
label: 'Todo nudge',
kind: 'bool',
jsonKey: 'todo_nudge_enabled',
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_parallel',
group: 'agent',
label: 'Tool parallelism',
kind: 'enum',
jsonKey: 'tool_parallelism',
values: ['1', '2', '4'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_timeout',
group: 'agent',
label: 'Request timeout ms',
kind: 'enum',
jsonKey: 'request_timeout_ms',
values: ['60000', '120000', '180000', '300000'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_keep',
group: 'agent',
label: 'Compaction keep recent',
kind: 'enum',
jsonKey: 'compaction_keep_recent',
values: ['4', '8', '12', '16'],
persist: 'agent',
live: 'next agent'
},
{
id: 'ag_gpulayers',
group: 'agent',
label: 'QVAC GPU layers',
kind: 'enum',
jsonKey: 'qvac_gpu_layers',
values: ['-1', '0', '8', '16', '32', '64'],
persist: 'agent',
backend: 'qvac',
live: 'next agent'
},
{
id: 'ag_maingpu',
group: 'agent',
label: 'QVAC main GPU',
kind: 'enum',
jsonKey: 'qvac_main_gpu',
values: ['auto', '0', '1', '2'],
persist: 'agent',
backend: 'qvac',
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, backend) {
const out = []
const be = backend === 'rest' || backend === 'qvac' ? backend : ''
for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) {
const spec = BARE_OS_DISCORD_SETTINGS[i]
if (spec.group !== group) continue
if (be && spec.backend && spec.backend !== be) continue
out.push(spec)
}
return out
}
function discordSanitizeAgentConfig(cfg) {
const next = cfg && typeof cfg === 'object' ? cfg : {}
const backend = String(next.backend || '').trim().toLowerCase() === 'rest' ? 'rest' : 'qvac'
next.backend = backend
if (backend === 'rest') {
delete next.qvac_model
delete next.qvac_profile
delete next.qvac_ctx_size
delete next.qvac_device
delete next.qvac_main_gpu
delete next.qvac_gpu_layers
const p = String(next.provider || '').trim().toLowerCase()
if (!p || p === 'qvac') next.provider = 'groq'
if (!next.rest_base_url) next.rest_base_url = 'https://api.groq.com/openai/v1'
} else {
delete next.rest_api_key
delete next.rest_base_url
next.provider = 'qvac'
}
return next
}
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 if (spec.jsonKey === 'qvac_ctx_size') cfg.qvac_ctx_size = Number(v) || 0
else if (
spec.jsonKey === 'tool_parallelism' ||
spec.jsonKey === 'request_timeout_ms' ||
spec.jsonKey === 'compaction_keep_recent' ||
spec.jsonKey === 'qvac_gpu_layers'
) {
cfg[spec.jsonKey] = Number(v)
} else if (spec.jsonKey === 'qvac_profile') {
cfg.qvac_profile = String(v)
const mapped = discordQvacModelForProfile(v)
if (mapped && mapped.id) {
cfg.qvac_model = mapped.id
cfg.model = mapped.id
}
} else cfg[spec.jsonKey] = v
const clean = discordSanitizeAgentConfig(cfg)
await discordWriteJsonFile(ctx, '~/.agent/config.json', clean)
} 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 {
let items = discordSettingsInGroup(group)
if (group === 'agent') {
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
const be = String(cfg.backend || '').toLowerCase() === 'rest' ? 'rest' : 'qvac'
items = discordSettingsInGroup(group, be)
}
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 === 'model') {
return discordSendResult(ctx, interaction, await discordAgentModelView(ctx, interaction, ''))
}
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]
delete BARE_OS_DISCORD_HDMS_SESSIONS[userId]
delete BARE_OS_DISCORD_HOLESAIL_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, ''))
}
if (result && result.modal === 'hdms:create') {
await discordShowForm(interaction, {
id: 'hdms:create',
title: 'Create HDMS drive',
fields: [
{
id: 'label',
label: 'Label (mounted at /mnt/<label>)',
placeholder: 'notes',
max: 63
}
]
})
return
}
if (result && result.modal === 'hdms:add') {
await discordShowForm(interaction, {
id: 'hdms:add',
title: 'Add read-only HDMS drive',
fields: [
{ id: 'label', label: 'Label', placeholder: 'peerdrive', max: 63 },
{ id: 'key', label: 'Hyperdrive key (z32)', placeholder: 'z32…', max: 80 }
]
})
return
}
if (result && result.modal === 'hdms:pair') {
await discordShowForm(interaction, {
id: 'hdms:pair',
title: 'Pair HDMS invite',
fields: [
{
id: 'invite',
label: 'Invite token',
paragraph: true,
placeholder: 'Paste the z32 invite from the peer',
max: 400
}
]
})
return
}
if (result && result.modal === 'holesail:addsrv') {
await discordShowForm(interaction, {
id: 'holesail:addsrv',
title: 'Add Holesail server',
fields: [
{ id: 'id', label: 'Connection id', placeholder: 'web-9000', max: 64 },
{ id: 'port', label: 'Local port', placeholder: '8088', max: 8 },
{ id: 'host', label: 'Bind host', placeholder: '127.0.0.1', max: 80, required: false },
{ id: 'udp', label: 'UDP (yes/no)', placeholder: 'no', max: 8, required: false },
{ id: 'secure', label: 'Secure (yes/no)', placeholder: 'no', max: 8, required: false }
]
})
return
}
if (result && result.modal === 'holesail:addcli') {
await discordShowForm(interaction, {
id: 'holesail:addcli',
title: 'Add Holesail client',
fields: [
{ id: 'id', label: 'Connection id', placeholder: 'peer-web', max: 64 },
{ id: 'key', label: 'hs:// URL', paragraph: true, placeholder: 'hs://…', max: 200 },
{ id: 'port', label: 'Local port', placeholder: '8088', max: 8 },
{ id: 'host', label: 'Connect host', placeholder: '127.0.0.1', max: 80, required: false }
]
})
return
}
if (result && result.modal === 'agent:ask') {
await discordShowForm(interaction, {
id: 'agent:ask',
title: 'Ask the agent',
fields: [
{
id: 'prompt',
label: 'What should the agent do?',
paragraph: true,
placeholder: 'e.g. list /bin and summarize /proc/bare_os/features',
min: 1,
max: 4000,
required: true
}
]
})
return
}
if (result && result.modal === 'agent:remember') {
await discordShowForm(interaction, {
id: 'agent:remember',
title: 'Remember a fact',
fields: [
{
id: 'text',
label: 'Fact to store in MEMORY.md',
paragraph: true,
placeholder: 'e.g. Holesail keys live in ~/.holesail',
min: 1,
max: 1000,
required: true
}
]
})
return
}
if (result && result.modal === 'agent:history') {
await discordShowForm(interaction, {
id: 'agent:history',
title: 'Search agent history',
fields: [
{
id: 'query',
label: 'Keyword (empty = count only)',
placeholder: 'holesail',
max: 200,
required: false
}
]
})
return
}
if (result && result.modal === 'agent:mdltype') {
await discordShowForm(interaction, {
id: 'agent:mdltype',
title: 'Custom model id',
fields: [
{
id: 'model',
label: 'Model id',
placeholder: 'QWEN3_8B_INST_Q4_K_M or llama-3.3-70b-versatile',
max: 120
}
]
})
return
}
if (result && result.modal === 'holesail:edit') {
await discordShowForm(interaction, {
id: 'holesail:edit',
title: 'Edit Holesail tunnel',
fields: [
{ id: 'port', label: 'Port (blank = keep)', placeholder: '8088', max: 8, required: false },
{ id: 'host', label: 'Host (clear to drop)', placeholder: '127.0.0.1', max: 80, required: false },
{ id: 'key', label: 'hs:// URL (blank = keep)', paragraph: true, placeholder: 'hs://…', max: 200, required: false },
{ id: 'udp', label: 'UDP (yes/no/blank)', placeholder: 'no', max: 8, required: false },
{ id: 'secure', label: 'Secure (yes/no/blank)', placeholder: 'no', max: 8, required: false }
]
})
return
}
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 discordReplyEphemeralText(ctx, interaction, text) {
const payload = discordCmdReplyPayload({ text: text, ephemeral: true })
try {
if (interaction.deferred && typeof interaction.editReply === 'function') {
await interaction.editReply(payload)
return
}
if (interaction.replied && typeof interaction.followUp === 'function') {
await interaction.followUp(payload)
return
}
if (typeof interaction.reply === 'function') {
await interaction.reply(payload)
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
ctx.console.error(
'discord-bot: deny reply failed: ' + ((err && err.message) || err)
)
}
}
}
async function discordReplyWhitelistDenied(ctx, interaction) {
await discordReplyEphemeralText(ctx, interaction, BARE_OS_DISCORD_WHITELIST_DENY)
}
async function discordReplyOwnerDenied(ctx, interaction) {
await discordReplyEphemeralText(ctx, interaction, BARE_OS_DISCORD_OWNER_DENY)
}
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' || cmd === 'upload'
? discordSuggestEditPaths(ctx, q)
: cmd === 'hdms'
? await discordSuggestHdmsLabels(ctx, q)
: discordSuggestPaths(ctx, q)
} else if (fname === 'label' && String(interaction.commandName || '') === 'hdms') {
choices = await discordSuggestHdmsLabels(ctx, q)
} else if (fname === 'id' && String(interaction.commandName || '') === 'holesail') {
choices = await discordSuggestHolesailIds(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.indexOf('hdms:') === 0) {
await discordHdmsAction(ctx, interaction, id)
return true
}
if (id.indexOf('holesail:') === 0) {
await discordHolesailAction(ctx, interaction, id)
return true
}
if (id.indexOf('agent:') === 0) {
await discordAgentAction(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:menu') {
const msg = interaction.message || {}
const open = !discordNavIsExpanded(msg.components)
const payload = {
components: discordUniqueComponents(discordApplyNav(msg.components, open))
}
const embeds = discordNormalizeEmbeds(msg.embeds)
if (embeds) payload.embeds = embeds
let sent = null
try {
if (
typeof interaction.update === 'function' &&
!interaction.replied &&
!interaction.deferred
) {
sent = await interaction.update(payload)
} else if (typeof interaction.reply === 'function') {
sent = await interaction.reply(payload)
}
} catch (err) {
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
ctx.console.error(
'discord-bot: menu toggle failed: ' + ((err && err.message) || err)
)
}
return true
}
try {
await discordIdleWatch(interaction, payload, sent)
} catch {
/* ignore */
}
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 = ''
}
function field(name) {
try {
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
return String(interaction.fields.getTextInputValue(name) || '')
}
} catch {
/* missing */
}
return ''
}
if (id === 'hdms:create') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHdms(ctx, interaction, 'create', function (k) {
return k === 'label' ? field('label') || value : ''
})
)
}
if (id === 'hdms:add') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHdms(ctx, interaction, 'add', function (k) {
if (k === 'label') return field('label')
if (k === 'key') return field('key') || value
return ''
})
)
}
if (id === 'hdms:pair') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHdms(ctx, interaction, 'pair', function (k) {
if (k === 'invite') return field('invite') || value
return ''
})
)
}
if (id === 'holesail:addsrv' || id === 'holesail:addcli') {
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'add', function (k) {
if (k === 'id') return field('id') || value
if (k === 'mode') return id === 'holesail:addcli' ? 'client' : 'server'
if (k === 'port') return field('port')
if (k === 'host') return field('host')
if (k === 'key') return field('key')
if (k === 'udp') return field('udp')
if (k === 'secure') return field('secure')
return ''
})
)
}
if (id === 'holesail:edit') {
const sel = discordHolesailGet(interaction).sel
const port = field('port')
const host = field('host')
const key = field('key')
const udp = field('udp')
const secure = field('secure')
if (!port && !host && !key && !udp && !secure) {
return discordSendResult(
ctx,
interaction,
await discordHolesailHud(ctx, interaction, 'No fields changed.')
)
}
return discordSendResult(
ctx,
interaction,
await discordCmdHandleHolesail(ctx, interaction, 'edit', function (k) {
if (k === 'id') return sel
if (k === 'port') return port
if (k === 'host') return host
if (k === 'key') return key
if (k === 'udp') return udp
if (k === 'secure') return secure
return ''
})
)
}
if (id === 'agent:ask') {
return discordSendResult(
ctx,
interaction,
await discordAgentAsk(ctx, interaction, field('prompt') || value)
)
}
if (id === 'agent:remember') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, 'remember', field('text') || value)
)
}
if (id === 'agent:history') {
return discordSendResult(
ctx,
interaction,
await discordAgentInspect(ctx, interaction, 'history', field('query') || value)
)
}
if (id === 'agent:mdltype') {
const mid = field('model') || value
try {
await discordAgentApplyModel(ctx, mid)
return discordSendResult(
ctx,
interaction,
await discordAgentModelView(ctx, interaction, 'Now using **' + String(mid).trim() + '**.')
)
} catch (err) {
return discordSendResult(ctx, interaction, {
text: 'Could not set model: ' + ((err && err.message) || err),
ephemeral: true
})
}
}
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)
discordIdleSweep()
const uid = discordInteractionUserId(interaction)
if (discordIsAutocomplete(interaction)) {
if (!discordUserAllowed(ctx, uid, interaction)) {
try {
if (typeof interaction.respond === 'function') await interaction.respond([])
} catch {
/* ignore */
}
return true
}
await discordPluginsEnsure(ctx)
return discordDispatchAutocomplete(ctx, interaction)
}
if (!discordUserAllowed(ctx, uid, interaction)) {
if (ctx && ctx.console && typeof ctx.console.log === 'function') {
ctx.console.log('discord-bot: denied user ' + (uid || '?'))
}
await discordReplyWhitelistDenied(ctx, interaction)
return true
}
const isComp = discordIsMessageComponent(interaction)
const isModal = discordIsModalSubmit(interaction)
if (isComp || isModal) {
const owner = discordInteractionOwnerId(interaction)
if (owner && owner !== uid) {
if (ctx && ctx.console && typeof ctx.console.log === 'function') {
ctx.console.log('discord-bot: denied owner mismatch ' + (uid || '?'))
}
await discordReplyOwnerDenied(ctx, interaction)
return true
}
discordIdleTouch(interaction)
if (isComp) {
await discordDispatchComponent(ctx, interaction)
return true
}
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.getBoolean === 'function') {
try {
const b = interaction.options.getBoolean(key)
if (b === true) return 'true'
if (b === false) return 'false'
} catch {
/* not a boolean option */
}
}
if (typeof interaction.options.getInteger === 'function') {
try {
const n = interaction.options.getInteger(key)
if (n != null) return String(n)
} catch {
/* not an integer option */
}
}
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 if (name === 'upload') {
result = await discordCmdHandleUpload(ctx, interaction, opt('path'))
} else if (name === 'hdms') {
result = await discordCmdHandleHdms(ctx, interaction, sub, opt)
} else if (name === 'holesail') {
result = await discordCmdHandleHolesail(ctx, interaction, sub, opt)
} else if (name === 'agent') {
result = await discordCmdHandleAgent(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,
captureOutput: discordCmdCapture,
stripAnsi: discordStripAnsi,
parseIdWhitelist: discordParseIdWhitelist,
whitelistCount: discordWhitelistCount,
userAllowed: discordUserAllowed,
userInstallEnabled: discordUserInstallEnabled,
isUserInstallInteraction: discordIsUserInstallInteraction,
stampUserInstallCommands: discordStampUserInstallCommands,
userInstallAuthorizeUrl: discordUserInstallAuthorizeUrl,
userInstallAppConfig: discordUserInstallAppConfig,
enableUserInstallApp: discordEnableUserInstallApp,
putSlashCommands: discordPutSlashCommands,
interactionUserId: discordInteractionUserId,
interactionOwnerId: discordInteractionOwnerId,
isMessageComponent: discordIsMessageComponent,
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,
qvacChatModels: BARE_OS_DISCORD_QVAC_MODELS,
parseOpenAiModels: discordParseOpenAiModels,
restModelsFallback: discordRestModelsFallback,
filterModels: discordFilterModels,
applyAgentModel: discordAgentApplyModel,
fetchRestModels: discordFetchRestModels,
parseAgentProgress: discordAgentParseProgress,
splitAgentStdout: discordAgentSplitStdout,
formatAgentProcess: discordAgentFormatProcess,
inspectAgent: discordAgentInspect,
agentReady: discordAgentReady,
markdownPages: discordMarkdownPages,
settingsApply: discordSettingsApply,
settingsCurrent: discordSettingsCurrent,
uniqueComponents: discordUniqueComponents,
navRows: discordNavRows,
applyNav: discordApplyNav,
parseHdmsList: discordParseHdmsList,
parseHolesailList: discordParseHolesailList,
parseHolesailShow: discordParseHolesailShow,
pathOk: discordCmdPathOk,
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
}