/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** ANSI helpers for /bin/edit (preamble; no import in src). */ const EDIT_ANSI_RESET = '\x1b[0m' /** * @param {Record} [ctx] * @returns {import('stream').Writable | undefined} */ function bareEditResolveStdout(ctx) { if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx) const out = c.replStdout || c.stdout || globalThis.process?.stdout return /** @type {import('stream').Writable | undefined} */ (out) } /** * @param {Record} [ctx] */ function bareEditUseColor(ctx) { const env = ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false const out = bareEditResolveStdout(ctx) return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY) } /** * @param {'keyword'|'string'|'comment'|'number'|'status'|'inverse'|'dim'} cls * @param {boolean} on */ function bareEditSgr(cls, on) { if (!on) return '' switch (cls) { case 'keyword': return '\x1b[36m' case 'string': return '\x1b[32m' case 'comment': return '\x1b[90m' case 'number': return '\x1b[33m' case 'status': return '\x1b[44m\x1b[97m' case 'inverse': return '\x1b[7m' case 'dim': return '\x1b[2m' default: return '' } } /** * @param {string} fullLine * @param {Array<{ start: number, end: number, cls: string }>} spans offsets into fullLine * @param {number} visStart first column (0-based) * @param {number} maxLen max code units to show * @param {boolean} useColor */ function bareEditPaintLineWindow(fullLine, spans, visStart, maxLen, useColor) { const slice = fullLine.slice(visStart, visStart + maxLen) if (!useColor) return slice const n = slice.length const relSpans = spans .map((s) => ({ start: Math.max(0, s.start - visStart), end: Math.min(n, s.end - visStart), cls: s.cls })) .filter((s) => s.end > 0 && s.start < n) .sort((a, b) => a.start - b.start) let out = '' let pos = 0 for (const sp of relSpans) { if (sp.start > pos) out += slice.slice(pos, sp.start) out += bareEditSgr(/** @type {'keyword'} */ (sp.cls), true) + slice.slice(sp.start, sp.end) + EDIT_ANSI_RESET pos = sp.end } if (pos < n) out += slice.slice(pos) return out } /** * Move cursor (1-based row/col, DEC origin). Clamp to sane bounds for escape parsing. * @param {number} row1 * @param {number} col1 */ function bareEditCup(row1, col1) { const r = Math.max(1, Math.min(Math.floor(row1), 9999)) const c = Math.max(1, Math.min(Math.floor(col1), 9999)) return '\x1b[' + r + ';' + c + 'H' } /** * @param {Record} ctx * @param {import('stream').Writable} stdout * @param {string} s */ function bareEditWrite(ctx, stdout, s) { if (!stdout || typeof stdout.write !== 'function') return try { stdout.write(s) } catch { try { ctx.console?.error?.('edit: stdout write failed') } catch { /* ignore */ } } } /** TTY key parsing for /bin/edit: consume one logical key from a mutable byte queue. */ /** * @param {number} b1 */ function bareEditUtf8TrailCount(b1) { if (b1 >= 0xc0 && b1 < 0xe0) return 1 if (b1 >= 0xe0 && b1 < 0xf0) return 2 if (b1 >= 0xf0) return 3 return 0 } /** * @param {number[]} bytes */ function bareEditUtf8DecodeKey(bytes) { try { const u = new Uint8Array(bytes) if (typeof TextDecoder !== 'undefined') { return new TextDecoder('utf-8', { fatal: false }).decode(u) } } catch { /* fall through */ } let s = '' for (const b of bytes) s += String.fromCharCode(b) return s } /** * @param {string} seq CSI payload after ESC [, including final byte (e.g. "A", "1;5A", "3~") */ function bareEditCsiToEvent(seq) { if (seq === '3~') return { type: 'ctrl', code: 'delete' } if (seq === '5~') return { type: 'nav', key: 'pageup' } if (seq === '6~') return { type: 'nav', key: 'pagedown' } const last = seq.charAt(seq.length - 1) if (last === '~') { if (seq === '1~' || seq === '7~') return { type: 'nav', key: 'home' } if (seq === '4~' || seq === '8~') return { type: 'nav', key: 'end' } const tildeNum = /^(\d+)~$/.exec(seq) if (tildeNum) { const n = parseInt(tildeNum[1], 10) const fnMap = { 11: 1, 12: 2, 13: 3, 14: 4, 15: 5, 17: 6, 18: 7, 19: 8, 20: 9, 21: 10, 23: 11, 24: 12 } if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] } } return { type: 'unknown' } } if (last === 'Z') return { type: 'nav', key: 'stab' } if (last === 'A' || last === 'B' || last === 'C' || last === 'D') { const map = { A: 'up', B: 'down', C: 'right', D: 'left' } return { type: 'nav', key: map[last] } } if (last === 'H') return { type: 'nav', key: 'home' } if (last === 'F') return { type: 'nav', key: 'end' } return { type: 'unknown' } } /** * @param {number} b3 byte after ESC O */ function bareEditSs3ToEvent(b3) { if (b3 === 72) return { type: 'nav', key: 'home' } if (b3 === 70) return { type: 'nav', key: 'end' } if (b3 === 65) return { type: 'nav', key: 'up' } if (b3 === 66) return { type: 'nav', key: 'down' } if (b3 === 67) return { type: 'nav', key: 'right' } if (b3 === 68) return { type: 'nav', key: 'left' } if (b3 === 80) return { type: 'fn', n: 1 } if (b3 === 81) return { type: 'fn', n: 2 } if (b3 === 82) return { type: 'fn', n: 3 } if (b3 === 83) return { type: 'fn', n: 4 } return { type: 'unknown' } } /** ESC [ 200 ~ */ const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126] /** ESC [ 201 ~ */ const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126] /** * @param {number[]} q * @param {number[]} prefix */ function bareEditStartsWithBytes(q, prefix) { if (q.length < prefix.length) return false for (let i = 0; i < prefix.length; i++) { if (q[i] !== prefix[i]) return false } return true } /** * True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes. * @param {number[]} q */ function bareEditCouldBeBracketPastePrefix(q) { const pre = BARE_EDIT_BRACKET_PASTE_START const n = Math.min(q.length, pre.length) for (let i = 0; i < n; i++) { if (q[i] !== pre[i]) return false } return true } /** * @param {number[]} bytes */ function bareEditDecodePasteInner(bytes) { try { if (typeof TextDecoder !== 'undefined') { return new TextDecoder('utf-8', { fatal: false }).decode( new Uint8Array(bytes) ) } } catch { /* fall through */ } let s = '' for (const b of bytes) s += String.fromCharCode(b) return s } /** * If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text. * @param {number[]} q * @returns {string|undefined} */ function bareEditFinalizeBracketedPasteOnEof(q) { const SL = BARE_EDIT_BRACKET_PASTE_START.length if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) { const inner = q.slice(SL) q.length = 0 return bareEditDecodePasteInner(inner) } return undefined } /** * xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~ * @param {number[]} q mutable queue * @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front */ function bareEditTryConsumeBracketedPaste(q) { const SL = BARE_EDIT_BRACKET_PASTE_START.length const EL = BARE_EDIT_BRACKET_PASTE_END.length if (!q.length) return undefined if (q.length < SL) { return bareEditCouldBeBracketPastePrefix(q) ? null : undefined } if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) { return undefined } for (let i = SL; i <= q.length - EL; i++) { let ok = true for (let j = 0; j < EL; j++) { if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) { ok = false break } } if (ok) { const inner = q.slice(SL, i) q.splice(0, i + EL) return bareEditDecodePasteInner(inner) } } return null } /** * @param {number[]} q mutable queue (front = index 0) * @returns {Record | null} null if more bytes needed */ function bareEditTryConsumeKey(q) { if (!q.length) return null const b1 = q[0] if (b1 === 3) { q.shift() return { type: 'ctrl', code: 'interrupt' } } if (b1 === 8 || b1 === 127) { q.shift() return { type: 'ctrl', code: 'backspace' } } if (b1 === 13 || b1 === 10) { q.shift() return { type: 'key', ch: '\n' } } if (b1 === 9) { q.shift() return { type: 'key', ch: '\t' } } if (b1 === 27) { if (q.length < 2) return null const b2 = q[1] if (b2 === 91) { let i = 2 while (i < q.length) { const b = q[i] if (b >= 0x40 && b <= 0x7e) { const seq = String.fromCharCode.apply(null, q.slice(2, i + 1)) q.splice(0, i + 1) return bareEditCsiToEvent(seq) } i++ } return null } if (b2 === 79) { if (q.length < 3) return null const b3 = q[2] q.splice(0, 3) return bareEditSs3ToEvent(b3) } q.splice(0, 2) return { type: 'key', ch: String.fromCharCode(b2) } } if (b1 < 0x20) { q.shift() return { type: 'ctrl', code: b1 } } const need = bareEditUtf8TrailCount(b1) if (q.length < 1 + need) return null const chunk = q.splice(0, 1 + need) return { type: 'key', ch: bareEditUtf8DecodeKey(chunk) } } /** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */ /** * @param {unknown} chunk * @returns {number[]} */ function bareEditChunkBytes(chunk) { if (chunk == null) return [] if (typeof chunk === 'string') { const out = [] for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff) return out } const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length const out = [] for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff) return out } /** * @param {{ nextByte: () => Promise, dispose?: () => void, _keyq?: number[] }} reader */ async function bareEditReadKey(reader) { reader._keyq = reader._keyq || [] const q = reader._keyq for (;;) { const pasteText = bareEditTryConsumeBracketedPaste(q) if (pasteText !== undefined && pasteText !== null) { return { type: 'paste', text: pasteText } } if (pasteText === null) { const b = await reader.nextByte() if (b === undefined) { const eofPaste = bareEditFinalizeBracketedPasteOnEof(q) if (eofPaste !== undefined) { return { type: 'paste', text: eofPaste } } if (!q.length) return { type: 'eof' } if (q.length === 1 && q[0] === 27) { q.length = 0 return { type: 'key', ch: '\x1b' } } const lone = q.shift() if (lone !== undefined && lone < 0x20) { return { type: 'ctrl', code: lone } } if (lone !== undefined) { return { type: 'key', ch: String.fromCharCode(lone) } } return { type: 'eof' } } q.push(b) continue } const ev = bareEditTryConsumeKey(q) if (ev) return ev const b = await reader.nextByte() if (b === undefined) { const eofPaste = bareEditFinalizeBracketedPasteOnEof(q) if (eofPaste !== undefined) { return { type: 'paste', text: eofPaste } } if (!q.length) return { type: 'eof' } if (q.length === 1 && q[0] === 27) { q.length = 0 return { type: 'key', ch: '\x1b' } } const lone = q.shift() if (lone !== undefined && lone < 0x20) { return { type: 'ctrl', code: lone } } if (lone !== undefined) { return { type: 'key', ch: String.fromCharCode(lone) } } return { type: 'eof' } } q.push(b) } } /** * @param {import('stream').Readable} stdin */ function bareEditCreateStdinReader(stdin) { /** @type {number[]} */ const bytes = [] /** @type {(() => void)[]} */ const waiters = [] function drain() { while (waiters.length && bytes.length) { const w = waiters.shift() if (w) w() } } /** @param {unknown} chunk */ function onData(chunk) { bytes.push(...bareEditChunkBytes(chunk)) drain() } stdin.on('data', onData) return { nextByte() { if (bytes.length) return Promise.resolve(bytes.shift()) return new Promise((resolve) => { waiters.push(() => resolve(bytes.shift())) }) }, dispose() { stdin.removeListener('data', onData) } } } /** Minimal AbortController for Bare/Pear when globalThis lacks it (drive-resident /bin preamble). */ function bareAgentEnsureAbortPolyfill() { const g = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : /** @type {Record} */ ({}) if (typeof g.AbortController === 'function') return function BareAbortSignal() { /** @type {boolean} */ this.aborted = false /** @type {unknown} */ this.reason = undefined /** @type {{ fn: () => void, once: boolean }[]} */ this._listeners = [] } BareAbortSignal.prototype.addEventListener = function (type, fn, opts) { if (type !== 'abort' || typeof fn !== 'function') return const once = !!(opts && opts.once) if (this.aborted) { if (once) { try { fn.call(this) } catch { /* ignore */ } } return } this._listeners.push({ fn: /** @type {() => void} */ (fn), once }) } BareAbortSignal.prototype.removeEventListener = function (type, fn) { if (type !== 'abort' || typeof fn !== 'function') return this._listeners = this._listeners.filter((x) => x.fn !== fn) } BareAbortSignal.prototype.throwIfAborted = function () { if (!this.aborted) return const DOMException = g.DOMException if (typeof DOMException === 'function') { throw new DOMException('Aborted', 'AbortError') } const e = new Error('Aborted') e.name = 'AbortError' throw e } function BareAbortController() { this.signal = new BareAbortSignal() } BareAbortController.prototype.abort = function (reason) { const s = this.signal if (s.aborted) return s.aborted = true s.reason = reason const list = s._listeners.slice() s._listeners.length = 0 for (const x of list) { try { x.fn.call(s) } catch { /* ignore */ } } } if (typeof globalThis !== 'undefined') { globalThis.AbortController = BareAbortController globalThis.AbortSignal = BareAbortSignal } else { g.AbortController = BareAbortController g.AbortSignal = BareAbortSignal } } bareAgentEnsureAbortPolyfill() /** TextEncoder/TextDecoder when missing on globalThis (Bare/Pear guest eval). */ /** * @param {Uint8Array} bytes */ function bareAgentUtf8Decode(bytes) { let out = '' let i = 0 const len = bytes.length while (i < len) { const b0 = bytes[i++] if (b0 < 0x80) { out += String.fromCharCode(b0) continue } if ((b0 & 0xe0) === 0xc0) { if (i >= len || (bytes[i] & 0xc0) !== 0x80) { out += '\ufffd' continue } const b1 = bytes[i++] const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f) if (cp < 0x80) out += '\ufffd' else out += String.fromCharCode(cp) continue } if ((b0 & 0xf0) === 0xe0) { if (i + 1 >= len || (bytes[i] & 0xc0) !== 0x80 || (bytes[i + 1] & 0xc0) !== 0x80) { out += '\ufffd' continue } const b1 = bytes[i++] const b2 = bytes[i++] let cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f) if (cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff)) out += '\ufffd' else out += String.fromCharCode(cp) continue } if ((b0 & 0xf8) === 0xf0) { if ( i + 2 >= len || (bytes[i] & 0xc0) !== 0x80 || (bytes[i + 1] & 0xc0) !== 0x80 || (bytes[i + 2] & 0xc0) !== 0x80 ) { out += '\ufffd' continue } const b1 = bytes[i++] const b2 = bytes[i++] const b3 = bytes[i++] let cp = ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f) if (cp < 0x10000 || cp > 0x10ffff) out += '\ufffd' else { cp -= 0x10000 out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)) } continue } out += '\ufffd' } return out } /** * @param {string} str */ function bareAgentUtf8Encode(str) { const out = [] for (let i = 0; i < str.length; i++) { let c = str.charCodeAt(i) if (c < 0x80) { out.push(c) } else if (c < 0x800) { out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)) } else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) { const c2 = str.charCodeAt(i + 1) if ((c2 & 0xfc00) === 0xdc00) { i++ const cp = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff) out.push( 0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f) ) } else { out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)) } } else if (c < 0xd800 || c >= 0xe000) { out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)) } } return new Uint8Array(out) } function bareAgentEnsureTextCodecPolyfill() { const g = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : /** @type {Record} */ ({}) if (typeof g.TextDecoder !== 'function') { function BareTextDecoder() { /** @type {'utf-8'} */ this.encoding = 'utf-8' } BareTextDecoder.prototype.decode = function (input, options) { let u8 if (input instanceof Uint8Array) u8 = input else if (typeof ArrayBuffer !== 'undefined' && input instanceof ArrayBuffer) u8 = new Uint8Array(input) else if (input != null && typeof input === 'object' && 'length' in input) { u8 = new Uint8Array(/** @type {ArrayLike} */ (input)) } else if (input == null || input === undefined) { return '' } else { return '' } const stream = !!(options && options.stream) void stream return bareAgentUtf8Decode(u8) } if (typeof globalThis !== 'undefined') globalThis.TextDecoder = BareTextDecoder else g.TextDecoder = BareTextDecoder } if (typeof g.TextEncoder !== 'function') { function BareTextEncoder() { this.encoding = 'utf-8' } BareTextEncoder.prototype.encode = function (input) { return bareAgentUtf8Encode(String(input == null ? '' : input)) } if (typeof globalThis !== 'undefined') globalThis.TextEncoder = BareTextEncoder else g.TextEncoder = BareTextEncoder } } bareAgentEnsureTextCodecPolyfill() /** Shared helpers for /bin/agent tools (preamble for agent bundle). */ /** * Paths allowed for rename/delete via agent tools (stricter than general read paths). * @param {string} absPath * @returns {boolean} */ function bareAgentPathAllowedMutate(absPath) { const p = String(absPath || '').replace(/\\/g, '/') if (!p.startsWith('/') || p.includes('..')) return false return ( p.startsWith('/home/') || p.startsWith('/tmp/') || p === '/tmp' || p.startsWith('/root/') || p.startsWith('/mnt/') ) } /** @type {readonly string[]} */ var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([ '/proc/bare_os/metrics_live.json', '/proc/bare_os/features.json', '/proc/bare_os/swarm.json', '/proc/bare_os/capabilities.json' ]) /** * @param {unknown} statObj * @param {string} path */ function bareAgentSerializeStat(statObj, path) { if (!statObj || typeof statObj !== 'object') return { path, error: 'no_stat' } const s = /** @type {Record} */ (statObj) /** @type {'file' | 'dir' | 'symlink' | 'other'} */ let kind = 'other' try { if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir' else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink' else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file' else if (typeof s.mode === 'number') { const M = Number(s.mode) if ((M & 0o170000) === 0o040000) kind = 'dir' else if ((M & 0o170000) === 0o120000) kind = 'symlink' else if ((M & 0o170000) === 0o100000) kind = 'file' } } catch { /* ignore */ } /** @type {Record} */ const out = { path, kind, size: typeof s.size === 'number' ? s.size : undefined, mode: typeof s.mode === 'number' ? s.mode : undefined, mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined, uid: typeof s.uid === 'number' ? s.uid : undefined, gid: typeof s.gid === 'number' ? s.gid : undefined } if (typeof s.target === 'string') out.target = s.target return out } /** * @param {string} text * @param {number} maxChars */ function bareAgentTruncateChars(text, maxChars) { const t = String(text || '') const n = Math.floor(maxChars) if (!Number.isFinite(n) || n <= 0) return '' if (t.length <= n) return t return t.slice(0, n) + '\n… truncated' } /** * @param {Record} page * @param {number} maxChars */ function bareAgentManExtractPageSlice(page, maxChars) { if (!page || typeof page !== 'object') return { error: 'bad_page' } const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000) const name = typeof page.name === 'string' ? page.name : '' const section = typeof page.section === 'number' ? page.section : 0 const title = typeof page.title === 'string' ? page.title : '' const synopsis = Array.isArray(page.synopsis) ? page.synopsis.map((x) => String(x)).join('\n') : '' const description = bareAgentTruncateChars( typeof page.description === 'string' ? page.description : '', Math.floor(m * 0.55) ) let opts = '' if (Array.isArray(page.options)) { const lines = [] for (const o of page.options) { if (!o || typeof o !== 'object') continue const fl = typeof o.flag === 'string' ? o.flag : '' const me = typeof o.meaning === 'string' ? o.meaning : '' if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me) } opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35)) } const blob = name + '(' + section + ') — ' + title + '\n\nSYNOPSIS\n' + synopsis + '\n\nDESCRIPTION\n' + description + (opts ? '\n\nOPTIONS\n' + opts : '') return { name, section, title, synopsis, description, options_text: opts || undefined, text: bareAgentTruncateChars(blob, m) } } /** * Same semantics as `man -k`: substring match on indexed keywords (merged DB). * @param {unknown} db * @param {string} needle * @param {number} maxHits */ function bareAgentManAproposHits(db, needle, maxHits) { const n = String(needle || '').toLowerCase() const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200) if (!n || !db || typeof db !== 'object') return /** @type {{ lines: string[], truncated: boolean }} */ ({ lines: [], truncated: false }) const d = /** @type {Record} */ (db) const pages = Array.isArray(d.pages) ? d.pages : [] const apropos = Array.isArray(d.apropos) ? d.apropos : [] const seen = new Set() /** @type {string[]} */ const lines = [] let truncated = false for (const row of apropos) { if (!row || typeof row !== 'object') continue const kw = typeof row.kw === 'string' ? row.kw : '' if (!kw.includes(n)) continue const idx = row.pageRef if (typeof idx !== 'number' || !pages[idx]) continue if (seen.has(idx)) continue seen.add(idx) const p = /** @type {Record} */ (pages[idx]) const name = typeof p.name === 'string' ? p.name : '' const sec = typeof p.section === 'number' ? p.section : 0 const title = typeof p.title === 'string' ? p.title : '' lines.push(name + '(' + sec + ') - ' + title) if (lines.length >= max) { truncated = true break } } lines.sort() return { lines, truncated } } /** * @param {Record} ctx * @param {unknown} vfs * @param {{ db: unknown | null }} cacheRef * @returns {Promise} */ async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) { if (cacheRef.db) return cacheRef.db if (!vfs || typeof vfs.readFile !== 'function') return null try { const buf = await vfs.readFile('/share/man/man.json') if (!buf || !buf.length) return null const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const parsed = JSON.parse(t) cacheRef.db = parsed return parsed } catch { return null } } /** * Resolve one manual page from merged DB (like `man [[section] name]`). * @param {unknown} db * @param {string} topic * @param {number | null} sectionExplicit */ function bareAgentManResolvePage(db, topic, sectionExplicit) { const name = String(topic || '').toLowerCase() if (!name || !db || typeof db !== 'object') return { error: 'not_found' } const d = /** @type {Record} */ (db) const index = d.index if (!index || typeof index !== 'object') return { error: 'not_found' } const idx = /** @type {Record} */ (index)[name] if (typeof idx !== 'number') return { error: 'not_found' } const pages = Array.isArray(d.pages) ? d.pages : [] const page = pages[idx] if (!page || typeof page !== 'object') return { error: 'not_found' } const sec = typeof page.section === 'number' ? page.section : 0 if (sectionExplicit !== null && sectionExplicit !== sec) { return { error: 'wrong_section', foundSection: sec } } return { page: /** @type {Record} */ (page) } } /** * @param {string} absPath * @returns {boolean} */ function bareAgentProcReadPathAllowed(absPath) { const p = String(absPath || '').replace(/\\/g, '/') if (!p.startsWith('/') || p.includes('..')) return false for (let i = 0; i < BARE_AGENT_PROC_READ_ALLOWLIST.length; i++) { if (p === BARE_AGENT_PROC_READ_ALLOWLIST[i]) return true } return false } /** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */ /** * @param {Record} ctx * @returns {string} */ function bareAgentResolveHome(ctx) { const env = ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const vfsHome = ctx && typeof ctx === 'object' && ctx.vfs && typeof ctx.vfs === 'object' && typeof /** @type {{ home?: string }} */ (ctx.vfs).home === 'string' ? String(/** @type {{ home?: string }} */ (ctx.vfs).home) : '' const h = env.HOME || vfsHome || '/home/guest' return h.replace(/\/+$/, '') || '/home/guest' } /** * @param {string} home */ function bareAgentPaths(home) { const base = home + '/.agent' return { dir: base, config: base + '/config.json', history: base + '/history.json', progress: base + '/progress.txt', instructions: base + '/instructions.md', context: base + '/context.md', cmdOut: base + '/last_command_out.txt' } } function bareAgentDefaultConfig() { return { rest_base_url: 'https://api.groq.com/openai/v1', rest_api_key: '', model: 'llama3-70b-8192', max_tokens: 4096, temperature: 0.7, provider: 'groq', max_iterations: 64, stream: true, tool_parallelism: 1, request_timeout_ms: 120000, extra_headers: /** @type {Record} */ ({}), allow_delete: false, require_confirm_token: '' } } /** * @param {unknown} v * @returns {v is Record} */ function bareAgentIsPlainObject(v) { return v != null && typeof v === 'object' && !Array.isArray(v) } /** * Shallow merge known keys from src into defaults. * @param {Record} defaults * @param {Record} src */ function bareAgentMergeConfig(defaults, src) { const out = { ...defaults } const known = new Set([ 'rest_base_url', 'rest_api_key', 'model', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'allow_delete', 'require_confirm_token' ]) for (const k of Object.keys(src)) { if (k.startsWith('x-')) continue if (!known.has(k)) continue const val = src[k] if (k === 'extra_headers' && bareAgentIsPlainObject(val)) { out.extra_headers = /** @type {Record} */ ({ ...val }) continue } if ( k === 'rest_base_url' || k === 'rest_api_key' || k === 'model' || k === 'provider' ) { out[k] = String(val ?? '') continue } if ( k === 'max_tokens' || k === 'temperature' || k === 'max_iterations' || k === 'tool_parallelism' || k === 'request_timeout_ms' ) { const n = Number(val) out[k] = Number.isFinite(n) ? n : defaults[k] continue } if (k === 'stream' || k === 'allow_delete') { out[k] = Boolean(val) continue } if (k === 'require_confirm_token') { out.require_confirm_token = String(val ?? '') continue } } return out } /** * @param {Record} raw */ function bareAgentValidateConfigShape(raw) { if (!bareAgentIsPlainObject(raw)) throw new Error('config must be a JSON object') for (const k of Object.keys(raw)) { if (k.startsWith('x-')) continue const known = [ 'rest_base_url', 'rest_api_key', 'model', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'allow_delete', 'require_confirm_token' ] if (!known.includes(k)) { throw new Error('unknown config key: ' + k) } } } /** * @param {Record} ctx * @param {{ dir: string, config: string }} paths * @returns {Promise<{ config: ReturnType, created: boolean }>} */ async function bareAgentLoadOrCreateConfig(ctx, paths) { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function') throw new Error('agent: vfs.mkdir unavailable') await vfs.mkdir(paths.dir, { recursive: true }) let missingOrEmpty = false /** @type {Record} */ let raw = {} try { if (typeof vfs.readFile === 'function') { const buf = await vfs.readFile(paths.config) if (!buf || !buf.length) missingOrEmpty = true else { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) raw = JSON.parse(t) } } else missingOrEmpty = true } catch { missingOrEmpty = true raw = {} } if (!bareAgentIsPlainObject(raw)) raw = {} if (missingOrEmpty || Object.keys(raw).length === 0) { raw = bareAgentDefaultConfig() await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw)) return { config: /** @type {any} */ (raw), created: true } } bareAgentValidateConfigShape(raw) const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw) return { config: /** @type {any} */ (merged), created: false } } /** * @param {Record} ctx * @param {{ config: string }} paths * @param {Record} config */ async function bareAgentSaveConfig(ctx, paths, config) { const vfs = ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') throw new Error('agent: vfs.writeFile unavailable') bareAgentValidateConfigShape(config) const json = JSON.stringify(config, null, 2) + '\n' const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(json) : new TextEncoder().encode(json) await vfs.writeFile(paths.config, body) } /** * Drop middle messages until JSON size fits (keep first system + recent tail). * @param {unknown[]} msgs * @param {number} maxBytes */ function bareAgentTrimMessages(msgs, maxBytes) { if (!Array.isArray(msgs) || maxBytes <= 0) return [] /** @type {unknown[]} */ let out = msgs.slice() while (JSON.stringify(out).length > maxBytes && out.length > 3) { out.splice(2, 1) } return out } /** * Best-effort load instruction files. * @param {Record} ctx * @param {{ instructions: string, context: string }} paths */ async function bareAgentLoadInstructionFiles(ctx, paths) { const vfs = ctx.vfs const parts = [] if (!vfs || typeof vfs.readFile !== 'function') return '' try { const b = await vfs.readFile(paths.instructions) if (b && b.length) { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (t.trim()) parts.push('## User instructions (~/.agent/instructions.md)\n' + t.trim()) } } catch { /* ignore */ } try { const b = await vfs.readFile(paths.context) if (b && b.length) { const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (t.trim()) parts.push('## Agent context (~/.agent/context.md)\n' + t.trim()) } } catch { /* ignore */ } return parts.join('\n\n') } /** * One-time compact index from man.json for system prompt. * @param {Record} ctx */ async function bareAgentManDigest(ctx) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' try { const buf = await vfs.readFile('/share/man/man.json') if (!buf || !buf.length) return '' const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const j = JSON.parse(t) const pages = Array.isArray(j.pages) ? j.pages : [] /** @type {string[]} */ const lines = [] const max = Math.min(pages.length, 400) for (let i = 0; i < max; i++) { const p = pages[i] if (!p || typeof p !== 'object') continue const name = typeof p.name === 'string' ? p.name : '' const title = typeof p.title === 'string' ? p.title : '' if (name) lines.push('- ' + name + (title ? ': ' + title : '')) } let s = lines.join('\n') if (s.length > 24000) s = s.slice(0, 24000) + '\n…' return ( 'Manual page index (see `man ` for full text). Sample entries:\n' + s ) } catch { return '(man digest unavailable)' } } /** * @param {Record} ctx * @param {string} path */ async function bareAgentAppendProgress(ctx, path, line) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return let prev = '' try { const b = await vfs.readFile(path) if (b && b.length) { prev = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) } } catch { /* ignore */ } const chunk = new Date().toISOString() + ' ' + line.replace(/\r?\n/g, ' ') + '\n' const maxKeep = 120_000 let next = prev + chunk if (next.length > maxKeep) next = next.slice(-maxKeep) const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(next) : new TextEncoder().encode(next) await vfs.writeFile(path, body) } /** * @param {Record} ctx * @param {string} path * @param {unknown[]} messages */ async function bareAgentSaveHistory(ctx, path, messages) { const vfs = ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') return const trimmed = bareAgentTrimMessages(messages, 500_000) const json = JSON.stringify(trimmed, null, 2) + '\n' const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(json) : new TextEncoder().encode(json) await vfs.writeFile(path, body) } /** * @param {Record} ctx * @param {string} path * @returns {Promise} */ async function bareAgentLoadHistory(ctx, path) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return [] try { const buf = await vfs.readFile(path) if (!buf || !buf.length) return [] const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const j = JSON.parse(t) return Array.isArray(j) ? j : [] } catch { return [] } } /** * Clear persisted chat session (history + progress log; keeps config and instructions). * @param {Record} ctx * @param {ReturnType} paths * @param {string} argv0 */ async function bareAgentResetChatSession(ctx, paths, argv0) { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.writeFile !== 'function') { throw new Error('agent reset: vfs unavailable') } await vfs.mkdir(paths.dir, { recursive: true }) const emptyHist = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from('[]\n') : new TextEncoder().encode('[]\n') await vfs.writeFile(paths.history, emptyHist) const stamp = new Date().toISOString() + ' chat session reset\n' const progBody = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(stamp) : new TextEncoder().encode(stamp) await vfs.writeFile(paths.progress, progBody) try { const z = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from('') : new TextEncoder().encode('') await vfs.writeFile(paths.cmdOut, z) } catch { /* ignore */ } try { ctx.console.log( argv0 + ': chat session cleared (' + paths.history + ', ' + paths.progress + ')' ) } catch { /* ignore */ } } /** SSE line split + data payload parse (shared by agent-openai + tests). */ /** * Parse one SSE `data:` JSON line after the `data: ` prefix. * @param {string} dataLine content after `data: ` prefix */ function bareAgentParseSseDataPayload(dataLine) { const t = String(dataLine).trim() if (t === '[DONE]') return { kind: 'done' } try { const j = JSON.parse(t) return { kind: 'json', value: j } } catch { return { kind: 'raw', value: t } } } /** * Split SSE buffer into lines (keep incomplete tail). * @param {string} buf * @returns {{ lines: string[], rest: string }} */ function bareAgentSplitSseLines(buf) { const lines = [] let start = 0 for (let i = 0; i < buf.length; i++) { if (buf.charCodeAt(i) === 10) { lines.push(buf.slice(start, i)) start = i + 1 } } return { lines, rest: buf.slice(start) } } /** OpenAI-compatible chat/completions HTTP + SSE (preamble for /bin/agent). */ /** * @param {Record} ctx * @returns {typeof fetch | null} */ function bareAgentResolveFetch(ctx) { if (typeof ctx.httpFetch === 'function') return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx)) const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null let f = bare && typeof bare.fetch === 'function' ? bare.fetch : bare && bare.default && typeof bare.default === 'object' && typeof bare.default.fetch === 'function' ? bare.default.fetch : null if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare)) if (typeof globalThis.fetch === 'function') return globalThis.fetch.bind(globalThis) return null } /** * @param {string} base */ function bareAgentNormalizeBaseUrl(base) { let s = String(base || '').trim() while (s.endsWith('/')) s = s.slice(0, -1) return s } /** * @param {Record} obj * @param {string} key */ function bareAgentDeepGet(obj, key) { const parts = key.split('.') let cur = obj for (const p of parts) { if (cur == null || typeof cur !== 'object') return undefined cur = /** @type {Record} */ (cur)[p] } return cur } /** * Stream chat/completions; invoke onEvent for each parsed chunk. * @param {{ * fetchFn: typeof fetch, * url: string, * headers: Record, * body: Record, * signal?: AbortSignal | null, * onEvent: (ev: Record) => void * }} opts */ async function bareAgentStreamChatCompletions(opts) { const { fetchFn, url, headers, body, signal, onEvent } = opts const res = await fetchFn(url, { method: 'POST', headers, body: JSON.stringify(body), signal: signal || undefined }) if (!res.ok) { let errText = '' try { errText = await res.text() } catch { /* ignore */ } throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800)) } const stream = res.body if (!stream || typeof stream.getReader !== 'function') { throw new Error('agent: response body is not a readable stream') } const reader = stream.getReader() const dec = new TextDecoder() let buf = '' try { for (;;) { const { done, value } = await reader.read() if (done) break buf += dec.decode(value, { stream: true }) const sp = bareAgentSplitSseLines(buf) buf = sp.rest for (const line of sp.lines) { if (!line.trim()) continue if (line.startsWith(':')) continue if (!line.startsWith('data:')) continue const payload = line.slice(5).replace(/^\s/, '') const parsed = bareAgentParseSseDataPayload(payload) if (parsed.kind === 'done') { onEvent({ type: 'sse_done' }) continue } if (parsed.kind !== 'json' || !parsed.value || typeof parsed.value !== 'object') continue const j = /** @type {Record} */ (parsed.value) const choices = bareAgentDeepGet(j, 'choices') const ch0 = Array.isArray(choices) && choices[0] && typeof choices[0] === 'object' ? /** @type {Record} */ (choices[0]) : null const delta = ch0 && typeof ch0.delta === 'object' ? /** @type {Record} */ (ch0.delta) : null const finishReason = typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : '' const usage = typeof j.usage === 'object' && j.usage ? j.usage : undefined if (usage) { onEvent({ type: 'usage', usage }) } if (delta) { const c = delta.content if (typeof c === 'string' && c.length) { onEvent({ type: 'delta_content', content: c }) } const toolCalls = delta.tool_calls if (toolCalls !== undefined) onEvent({ type: 'delta_tool_calls', tool_calls: toolCalls }) } if (finishReason) onEvent({ type: 'finish_reason', finish_reason: finishReason }) } } } finally { try { reader.releaseLock() } catch { /* ignore */ } } } /** * Non-streaming completion (same endpoint, stream:false). */ async function bareAgentCompleteOnce(opts) { const { fetchFn, url, headers, body, signal } = opts const res = await fetchFn(url, { method: 'POST', headers, body: JSON.stringify({ ...body, stream: false }), signal: signal || undefined }) if (!res.ok) { let errText = '' try { errText = await res.text() } catch { /* ignore */ } throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800)) } const j = /** @type {Record} */ (await res.json()) return j } /** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */ /** * @param {string} s */ function bareAgentShellQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'" } /** * @param {unknown} v * @returns {string} */ function bareAgentJsonResult(v) { try { return JSON.stringify(v) } catch { return '{"error":"json_stringify_failed"}' } } /** * @returns {unknown[]} */ function bareAgentToolDefinitions() { return [ { type: 'function', function: { name: 'read_file', description: 'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute file path' }, max_bytes: { type: 'integer', description: 'Max bytes to read (default 256000)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'write_file', description: 'Create or overwrite a file. Parent directories are created as needed.', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string', description: 'Full file contents' } }, required: ['path', 'content'] } } }, { type: 'function', function: { name: 'edit_file', description: 'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string', description: 'If set (non-empty), full file replacement' }, old_string: { type: 'string', description: 'Search string (used with new_string)' }, new_string: { type: 'string', description: 'Replacement text' } }, required: ['path'] } } }, { type: 'function', function: { name: 'create_directory', description: 'Create a directory (recursive).', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } }, { type: 'function', function: { name: 'search_files', description: 'Run grep -R to list paths matching a pattern (bounded). Uses the shell.', parameters: { type: 'object', properties: { pattern: { type: 'string' }, root: { type: 'string', description: 'Directory to search (default /home)' }, max_lines: { type: 'integer', description: 'Default 200' } }, required: ['pattern'] } } }, { type: 'function', function: { name: 'run_command', description: 'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT: line.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Full command string (e.g. ls -la /bin)' }, timeout_ms: { type: 'integer' }, capture_exit: { type: 'boolean', description: 'If true, append last line EXIT: to capture (default false)' } }, required: ['command'] } } }, { type: 'function', function: { name: 'run_js_script', description: 'REQUIRED to run agent-authored JavaScript: Node is not installed. Writes code to ~/.agent/_tmp_agent_run.mjs and runs it by absolute path (Bare kernel — same as /bin scripts). Do not use run_command with node/npm/npx. Prefer async function run(ctx, argv). stdout/stderr captured.', parameters: { type: 'object', properties: { code: { type: 'string', description: 'Full ESM/CommonJS script body' } }, required: ['code'] } } }, { type: 'function', function: { name: 'get_system_info', description: 'Lightweight context: API version, uname, optional resource hook. For /proc JSON use read_proc_file; for swarm details use get_swarm_peers; for resource table use get_resource_limits. want=capabilities|swarm still returns those blobs when needed.', parameters: { type: 'object', properties: { want: { type: 'string', enum: ['summary', 'capabilities', 'swarm'], description: 'Optional focus (default summary)' } } } } }, { type: 'function', function: { name: 'edit_agent_config', description: 'Merge keys into ~/.agent/config.json (shallow merge for known keys only).', parameters: { type: 'object', properties: { patch: { type: 'object', description: 'Partial config object (rest_base_url, model, temperature, …)' } }, required: ['patch'] } } }, { type: 'function', function: { name: 'list_bin', description: 'List Tier-1 utilities in /bin via VFS.', parameters: { type: 'object', properties: { limit: { type: 'integer', description: 'Max names (default 400)' } } } } }, { type: 'function', function: { name: 'list_directory', description: 'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute directory path' }, max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' }, include_stat: { type: 'boolean', description: 'If true, call stat on each entry (slower; default false)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'file_stat', description: 'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).', parameters: { type: 'object', properties: { path: { type: 'string' }, follow_symlinks: { type: 'boolean', description: 'If true, use stat (follow); if false, lstat (default false)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'move_path', description: 'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.', parameters: { type: 'object', properties: { from_path: { type: 'string' }, to_path: { type: 'string' } }, required: ['from_path', 'to_path'] } } }, { type: 'function', function: { name: 'delete_path', description: 'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.', parameters: { type: 'object', properties: { path: { type: 'string' }, recursive: { type: 'boolean', description: 'Remove directories recursively (default false)' }, confirm_token: { type: 'string', description: 'Must match config require_confirm_token when that key is non-empty' } }, required: ['path'] } } }, { type: 'function', function: { name: 'read_man_page', description: 'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.', parameters: { type: 'object', properties: { topic: { type: 'string', description: 'Page name (e.g. grep, agent)' }, section: { type: 'integer', description: 'Manual section 1–8 if disambiguating (optional)' }, max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' } }, required: ['topic'] } } }, { type: 'function', function: { name: 'apropos_man', description: 'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines.', parameters: { type: 'object', properties: { keyword: { type: 'string' }, max_results: { type: 'integer', description: 'Default 40, max 200' } }, required: ['keyword'] } } }, { type: 'function', function: { name: 'read_proc_file', description: 'Read a small allowlisted /proc/bare_os/*.json file (bounded). Use instead of shelling cat.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'One of: /proc/bare_os/metrics_live.json, features.json, swarm.json, capabilities.json' }, max_bytes: { type: 'integer', description: 'Default 256000' } }, required: ['path'] } } }, { type: 'function', function: { name: 'get_swarm_peers', description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'get_resource_limits', description: 'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'run_js_script_at_path', description: 'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path to .mjs file' } }, required: ['path'] } } }, { type: 'function', function: { name: 'task_complete', description: 'Call when the user task is fully done. Provide a concise summary.', parameters: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] } } } ] } /** * @param {Record} ctx * @param {string} absPath */ function bareAgentPathAllowed(absPath) { const p = String(absPath || '').replace(/\\/g, '/') if (!p.startsWith('/')) return false const ok = p.startsWith('/home/') || p.startsWith('/tmp/') || p === '/tmp' || p.startsWith('/root/') || p.startsWith('/mnt/') || p.startsWith('/bin/') || p === '/bin' || p.startsWith('/etc/') || p.startsWith('/share/') || p.startsWith('/usr/') || p.startsWith('/var/') || p.startsWith('/proc/') || p.startsWith('/boot/') || p.startsWith('/lib/') || p.startsWith('/dev/') return ok } /** * @param {{ * ctx: Record, * toolName: string, * argsJson: string, * paths: { dir: string, config: string, cmdOut: string }, * signal?: AbortSignal, * appendProgress: (line: string) => void, * home: string, * configRef: { current: Record }, * manCacheRef?: { db: unknown | null }, * onTaskComplete: (summary: string) => void * }} o */ async function bareAgentDispatchTool(o) { const { ctx, toolName, argsJson, paths, signal, appendProgress, home, configRef, manCacheRef, onTaskComplete } = o const manDbCache = manCacheRef || { db: null } /** @type {Record} */ let args = {} try { args = /** @type {Record} */ (JSON.parse(argsJson || '{}')) } catch { return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) } const vfs = ctx.vfs const execLine = typeof ctx.execLine === 'function' ? /** @type {(s: string, opts?: unknown) => Promise} */ ( ctx.execLine.bind(ctx) ) : null /** * @param {string} line * @param {number | undefined} timeoutMs * @param {{ captureExit?: boolean }} [captureOpts] */ async function captureExec(line, timeoutMs, captureOpts) { const outPath = paths.cmdOut const captureExit = Boolean(captureOpts && captureOpts.captureExit) const wrapped = captureExit ? '{ ' + line + ' ; } > ' + bareAgentShellQuote(outPath) + ' 2>&1; printf "\\nEXIT:%s\\n" $? >> ' + bareAgentShellQuote(outPath) : line + ' > ' + bareAgentShellQuote(outPath) + ' 2>&1' const opts = signal || timeoutMs ? { signal, timeoutMs: timeoutMs || undefined } : undefined try { if (opts) await execLine(wrapped, opts) else await execLine(wrapped) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return { ok: false, exitNote: msg } } let captured = '' try { if (vfs && typeof vfs.readFile === 'function') { const buf = await vfs.readFile(outPath) if (buf && buf.length) { captured = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) } } } catch { /* ignore */ } const max = 120_000 if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated' return { ok: true, stdout_stderr: captured } } try { if (toolName === 'task_complete') { const summary = typeof args.summary === 'string' ? args.summary : '' appendProgress('task_complete: ' + summary.slice(0, 200)) onTaskComplete(summary || '(done)') return bareAgentJsonResult({ ok: true, completed: true, summary }) } if (toolName === 'read_file') { const path = typeof args.path === 'string' ? args.path : '' const maxB = typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes) ? Math.min(Math.floor(args.max_bytes), 1_000_000) : 256_000 if (!bareAgentPathAllowed(path)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('read_file ' + path) const buf = await vfs.readFile(path) if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' }) let t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated' return bareAgentJsonResult({ ok: true, path, content: t }) } if (toolName === 'write_file') { const path = typeof args.path === 'string' ? args.path : '' const content = typeof args.content === 'string' ? args.content : '' if (!path.startsWith('/') || path.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'bad_path' }) } if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('write_file ' + path) let dir = path.replace(/\/[^/]+$/, '') if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true }) const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(content) : new TextEncoder().encode(content) await vfs.writeFile(path, body) return bareAgentJsonResult({ ok: true, bytes: body.length }) } if (toolName === 'edit_file') { const path = typeof args.path === 'string' ? args.path : '' if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) { return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' }) } appendProgress('edit_file ' + path) const buf = await vfs.readFile(path) let prev = buf && buf.length ? typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) : '' const full = typeof args.content === 'string' ? args.content : '' const oldStr = typeof args.old_string === 'string' ? args.old_string : '' const newStr = typeof args.new_string === 'string' ? args.new_string : '' let next = prev if (full.length > 0) next = full else if (oldStr) { if (!prev.includes(oldStr)) { return bareAgentJsonResult({ ok: false, error: 'old_string not found' }) } next = prev.replace(oldStr, newStr) } else { return bareAgentJsonResult({ ok: false, error: 'need content or old_string+new_string' }) } const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(next) : new TextEncoder().encode(next) let dir = path.replace(/\/[^/]+$/, '') if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true }) await vfs.writeFile(path, body) return bareAgentJsonResult({ ok: true, bytes: body.length }) } if (toolName === 'create_directory') { const path = typeof args.path === 'string' ? args.path : '' if (!path.startsWith('/')) { return bareAgentJsonResult({ ok: false, error: 'bad_path' }) } if (!vfs || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('mkdir ' + path) await vfs.mkdir(path, { recursive: true }) return bareAgentJsonResult({ ok: true }) } if (toolName === 'search_files') { const pattern = typeof args.pattern === 'string' ? args.pattern : '' const root = typeof args.root === 'string' ? args.root : '/home' const maxLines = typeof args.max_lines === 'number' && Number.isFinite(args.max_lines) ? Math.min(Math.floor(args.max_lines), 500) : 200 if (!execLine) { return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) } appendProgress('search_files ' + pattern + ' @ ' + root) const cmd = 'grep -Rnl -- ' + bareAgentShellQuote(pattern) + ' ' + bareAgentShellQuote(root) + ' 2>/dev/null | head -n ' + maxLines const r = await captureExec(cmd, 60000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'run_command') { const command = typeof args.command === 'string' ? args.command : '' const timeoutMs = typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms) ? Math.min(Math.floor(args.timeout_ms), 600000) : 120000 const captureExit = Boolean(args.capture_exit) if (!execLine) { return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) } appendProgress('run_command ' + command.slice(0, 160)) const r = await captureExec(command, timeoutMs, { captureExit }) return bareAgentJsonResult( r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'run_js_script') { const code = typeof args.code === 'string' ? args.code : '' const scriptPath = paths.dir + '/_tmp_agent_run.mjs' if (!vfs?.writeFile || !execLine) { return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' }) } appendProgress('run_js_script (' + code.length + ' chars)') const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(code) : new TextEncoder().encode(code) await vfs.writeFile(scriptPath, body) /** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */ const cmd = bareAgentShellQuote(scriptPath) const r = await captureExec(cmd, 60000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'get_system_info') { /** @type {Record} */ const info = {} try { info.ctxApiVersion = typeof ctx.ctxApiVersion === 'string' ? ctx.ctxApiVersion : typeof ctx.ctxApiVersion === 'number' ? String(ctx.ctxApiVersion) : undefined } catch { /* ignore */ } const want = typeof args.want === 'string' ? args.want : 'summary' if (want === 'summary') { info.discovery_hint = 'Prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.' } if (want === 'capabilities' && vfs?.readFile) { try { const b = await vfs.readFile('/proc/bare_os/capabilities.json') if (b && b.length) { info.capabilities_json = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) } } catch { /* ignore */ } } if (want === 'swarm' && vfs?.readFile) { try { const b = await vfs.readFile('/proc/bare_os/swarm.json') if (b && b.length) { info.swarm = typeof ctx.b4a !== 'undefined' && ctx.b4a && ctx.b4a.toString ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) } } catch { /* ignore */ } } try { if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1') if (vfs?.readFile) { const buf = await vfs.readFile(paths.cmdOut) if (buf && buf.length) { info.uname = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf).trim() : String(new TextDecoder().decode(buf)).trim() } } } catch { /* ignore */ } appendProgress('get_system_info ' + want) return bareAgentJsonResult({ ok: true, want, info }) } if (toolName === 'edit_agent_config') { const patch = args.patch if (!patch || typeof patch !== 'object' || Array.isArray(patch)) { return bareAgentJsonResult({ ok: false, error: 'bad patch' }) } const merged = bareAgentMergeConfigPatch(configRef.current, patch) configRef.current = merged await bareAgentSaveConfigFromTools(ctx, paths, merged) appendProgress('edit_agent_config') return bareAgentJsonResult({ ok: true, saved: true }) } if (toolName === 'list_bin') { const limit = typeof args.limit === 'number' && Number.isFinite(args.limit) ? Math.min(Math.floor(args.limit), 800) : 400 if (!vfs || typeof vfs.readdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' }) } appendProgress('list_bin') try { const names = await vfs.readdir('/bin') const arr = Array.isArray(names) ? [...names].slice(0, limit) : [] arr.sort() return bareAgentJsonResult({ ok: true, count: arr.length, names: arr }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'list_directory') { const dir = typeof args.path === 'string' ? args.path : '' const maxEnt = typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) ? Math.min(Math.floor(args.max_entries), 2000) : 500 const includeStat = Boolean(args.include_stat) if (!bareAgentPathAllowed(dir)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs || typeof vfs.readdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' }) } appendProgress('list_directory ' + dir) try { const names = await vfs.readdir(dir) const arr = Array.isArray(names) ? [...names] : [] arr.sort() const slice = arr.slice(0, maxEnt) const base = dir.replace(/\/+$/, '') || '/' /** @type {{ name: string, stat?: Record }[]} */ const entries = [] for (const n of slice) { const entry = { name: n } if (includeStat && (vfs.lstat || vfs.stat)) { try { const full = base + '/' + n const st = typeof vfs.lstat === 'function' ? await vfs.lstat(full) : await vfs.stat(full) entry.stat = bareAgentSerializeStat(st, full) } catch { /* ignore per-entry stat errors */ } } entries.push(entry) } return bareAgentJsonResult({ ok: true, path: dir, count: entries.length, truncated: arr.length > maxEnt, entries }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'file_stat') { const path = typeof args.path === 'string' ? args.path : '' const follow = Boolean(args.follow_symlinks) if (!bareAgentPathAllowed(path)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs) { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('file_stat ' + path) try { /** @type {unknown} */ let st = null if (follow && typeof vfs.stat === 'function') st = await vfs.stat(path) else if (typeof vfs.lstat === 'function') st = await vfs.lstat(path) else if (typeof vfs.stat === 'function') st = await vfs.stat(path) if (!st) return bareAgentJsonResult({ ok: false, error: 'stat unavailable' }) const serialized = bareAgentSerializeStat(st, path) if ( serialized.kind === 'symlink' && typeof vfs.readlink === 'function' ) { try { serialized.target = await vfs.readlink(path) } catch { /* ignore */ } } return bareAgentJsonResult({ ok: true, stat: serialized }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'move_path') { const from = typeof args.from_path === 'string' ? args.from_path : '' const to = typeof args.to_path === 'string' ? args.to_path : '' if ( !bareAgentPathAllowedMutate(from) || !bareAgentPathAllowedMutate(to) || from.includes('..') || to.includes('..') ) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!execLine) { return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) } appendProgress('move_path') const cmd = 'mv -- ' + bareAgentShellQuote(from) + ' ' + bareAgentShellQuote(to) const r = await captureExec(cmd, 120000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'delete_path') { const path = typeof args.path === 'string' ? args.path : '' const recursive = Boolean(args.recursive) const token = typeof args.confirm_token === 'string' ? args.confirm_token : '' const cfg = configRef.current const allowDel = Boolean(cfg && cfg.allow_delete) const reqTok = cfg && typeof cfg.require_confirm_token === 'string' ? String(cfg.require_confirm_token) : '' if (!allowDel) { return bareAgentJsonResult({ ok: false, error: 'delete_disabled', hint: 'set allow_delete true in ~/.agent/config.json' }) } if (reqTok && token !== reqTok) { return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' }) } if (!bareAgentPathAllowedMutate(path) || path.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs) { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('delete_path ' + path) try { /** @type {unknown} */ let st = null if (typeof vfs.lstat === 'function') st = await vfs.lstat(path) else if (typeof vfs.stat === 'function') st = await vfs.stat(path) const isDir = st && typeof st === 'object' && typeof /** @type {{ isDirectory?: () => boolean }} */ (st).isDirectory === 'function' && st.isDirectory() if (isDir && recursive && typeof vfs.rm === 'function') { await vfs.rm(path, { recursive: true }) return bareAgentJsonResult({ ok: true, removed: 'directory', recursive: true }) } if (isDir && !recursive) { return bareAgentJsonResult({ ok: false, error: 'is_directory', hint: 'pass recursive true to remove a directory tree' }) } if (typeof vfs.unlink !== 'function') { return bareAgentJsonResult({ ok: false, error: 'unlink unavailable' }) } await vfs.unlink(path) return bareAgentJsonResult({ ok: true, removed: isDir ? 'directory' : 'file' }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'read_man_page') { const topic = typeof args.topic === 'string' ? args.topic : '' const maxC = typeof args.max_chars === 'number' && Number.isFinite(args.max_chars) ? Math.min(Math.floor(args.max_chars), 64_000) : 12_000 let secExplicit = null if ( typeof args.section === 'number' && Number.isFinite(args.section) && args.section >= 1 && args.section <= 8 ) { secExplicit = Math.floor(args.section) } const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache) if (!db) { return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' }) } appendProgress('read_man_page ' + topic) const resolved = bareAgentManResolvePage(db, topic, secExplicit) if ('error' in resolved && resolved.error === 'wrong_section') { return bareAgentJsonResult({ ok: false, error: 'wrong_section', foundSection: resolved.foundSection }) } if (!resolved.page) { return bareAgentJsonResult({ ok: false, error: 'not_found' }) } const slice = bareAgentManExtractPageSlice(resolved.page, maxC) return bareAgentJsonResult({ ok: true, ...slice }) } if (toolName === 'apropos_man') { const kw = typeof args.keyword === 'string' ? args.keyword : '' const maxRes = typeof args.max_results === 'number' && Number.isFinite(args.max_results) ? Math.floor(args.max_results) : 40 const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache) if (!db) { return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' }) } appendProgress('apropos_man ' + kw) const { lines, truncated } = bareAgentManAproposHits(db, kw, maxRes) return bareAgentJsonResult({ ok: true, count: lines.length, truncated, lines }) } if (toolName === 'read_proc_file') { const path = typeof args.path === 'string' ? args.path : '' const maxB = typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes) ? Math.min(Math.floor(args.max_bytes), 500_000) : 256_000 if (!bareAgentProcReadPathAllowed(path)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', allowlist: [...BARE_AGENT_PROC_READ_ALLOWLIST] }) } if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('read_proc_file ' + path) try { const buf = await vfs.readFile(path) if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' }) let t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) let parsed = null try { parsed = JSON.parse(t) } catch { parsed = null } if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated' return bareAgentJsonResult({ ok: true, path, text: t, json: parsed }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'get_swarm_peers') { appendProgress('get_swarm_peers') if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } try { const buf = await vfs.readFile('/proc/bare_os/swarm.json') if (!buf || !buf.length) { return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' }) } const txt = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) /** @type {unknown} */ let j = null try { j = JSON.parse(txt) } catch { return bareAgentJsonResult({ ok: true, raw: txt.slice(0, 120_000) }) } return bareAgentJsonResult({ ok: true, swarm: j }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'get_resource_limits') { appendProgress('get_resource_limits') try { if (typeof ctx.bareOsGetResourceStatus !== 'function') { return bareAgentJsonResult({ ok: false, error: 'bareOsGetResourceStatus unavailable' }) } const r = ctx.bareOsGetResourceStatus() return bareAgentJsonResult({ ok: true, resources: r }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'run_js_script_at_path') { const scriptPath = typeof args.path === 'string' ? args.path : '' if (!bareAgentPathAllowed(scriptPath) || scriptPath.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs?.readFile || !execLine) { return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' }) } appendProgress('run_js_script_at_path ' + scriptPath) try { await vfs.readFile(scriptPath) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: 'cannot_read_script', detail: msg }) } const cmd = bareAgentShellQuote(scriptPath) const r = await captureExec(cmd, 60000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr } ) } return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200)) return bareAgentJsonResult({ ok: false, error: msg }) } } /** * @param {Record} base * @param {Record} patch */ function bareAgentMergeConfigPatch(base, patch) { const out = { ...base } const keys = [ 'rest_base_url', 'rest_api_key', 'model', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'allow_delete', 'require_confirm_token' ] const numKeys = new Set([ 'max_tokens', 'temperature', 'max_iterations', 'tool_parallelism', 'request_timeout_ms' ]) for (const k of keys) { if (Object.prototype.hasOwnProperty.call(patch, k)) { /** @type {unknown} */ const v = patch[k] if (numKeys.has(k)) { const n = Number(v) if (Number.isFinite(n)) out[k] = n } else if (k === 'stream' || k === 'allow_delete') { out[k] = Boolean(v) } else if (k === 'require_confirm_token') { out[k] = String(v ?? '') } else { out[k] = String(v ?? '') } } } if ( patch.extra_headers && typeof patch.extra_headers === 'object' && !Array.isArray(patch.extra_headers) ) { out.extra_headers = { .../** @type {Record} */ (patch.extra_headers) } } return out } /** * @param {Record} ctx * @param {{ config: string }} paths * @param {Record} config */ async function bareAgentSaveConfigFromTools(ctx, paths, config) { const vfs = ctx.vfs if (!vfs?.writeFile) return const json = JSON.stringify(config, null, 2) + '\n' const body = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(json) : new TextEncoder().encode(json) await vfs.writeFile(paths.config, body) } /** Agent ReAct session: stream, tools, SIGINT, repl suspend (preamble for /bin/agent). */ /** * @param {Record} ctx * @param {string} s */ function bareAgentLog(ctx, s) { try { ctx.console.log(s) } catch { /* ignore */ } } /** * @param {Record} ctx * @param {string} s */ function bareAgentErr(ctx, s) { try { ctx.console.error(s) } catch { /* ignore */ } } /** * @param {import('stream').Writable | undefined} out * @param {string} s */ function bareAgentWriteOut(out, s) { if (out && typeof out.write === 'function') { try { out.write(s) } catch { /* ignore */ } } else { try { process.stdout.write(s) } catch { /* ignore */ } } } const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS autonomous agent inside a JavaScript POSIX-like environment on Hyperdrive + Hyperswarm (Pear/Bare runtime). JavaScript execution on this OS: **Node.js is not installed.** The \`node\`, \`npm\`, and \`npx\` commands **do not exist** and must never appear in plans or in run_command. To run JS as part of your agent work, **you must call the run_js_script tool** (writes under ~/.agent and executes via the Bare kernel). Optional: once a script exists on disk, run_command may invoke it by **absolute path** (e.g. \`/home/.../script.mjs\`)—same mechanism as \`/bin\` scripts—not via \`node\`. Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/readdir/chmod. Paths under /home (personal Hyperdrive), /mnt, /tmp are writable where policy allows; /bin, /etc are system drive. Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done. Discovery: man , /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits; use list_directory instead of \`ls\` in run_command when only listing. Prefer tools over guessing for filesystem and shell facts.` /** * Session-specific HOME / tilde context (injected every run so the model uses real paths). * @param {Record} ctx * @param {string} home from bareAgentResolveHome * @param {{ dir: string, config: string }} paths */ function bareAgentSessionHomeBlock(ctx, home, paths) { const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const homeEnv = String(env.HOME || '').trim() return ( '## This session: home directory and paths\n' + '- **Resolved user home (this session):** `' + home + '`\n' + '- **HOME in the environment:** `' + (homeEnv || home) + '`\n' + '- **Tilde \`~\`:** In shell and in user docs, \`~\` means this home directory. Examples: \`~/.agent\` == `' + paths.dir + '`, agent config `' + paths.config + '`. Always expand \`~\` to `' + home + '\` when constructing absolute paths for tools.\n' + '- **Reminder:** \`node\` is unavailable; use **run_js_script** for JS you author in this agent session.\n' ) } /** * @param {unknown} tc */ function bareAgentMergeToolCallDelta(acc, tc) { if (!tc || typeof tc !== 'object') return const o = /** @type {Record} */ (tc) const idx = typeof o.index === 'number' ? o.index : typeof o.index === 'string' ? Number.parseInt(o.index, 10) : 0 let cur = acc.get(idx) || /** @type {{ id: string, name: string, args: string }} */ ({ id: '', name: '', args: '' }) if (typeof o.id === 'string' && o.id) cur.id = o.id const fn = o.function && typeof o.function === 'object' ? o.function : null if (fn && typeof fn === 'object') { const nm = /** @type {Record} */ (fn).name const ar = /** @type {Record} */ (fn).arguments if (typeof nm === 'string') cur.name += nm if (typeof ar === 'string') cur.args += ar } acc.set(idx, cur) } /** * @param {Map} acc */ function bareAgentFinalizeToolCalls(acc) { const indices = [...acc.keys()].sort((a, b) => a - b) /** @type {unknown[]} */ const arr = [] for (const i of indices) { const c = acc.get(i) if (!c || !c.name) continue arr.push({ id: c.id || 'call_' + i + '_' + String(Math.random()).slice(2, 10), type: 'function', function: { name: c.name, arguments: c.args || '{}' } }) } return arr } /** * @param {Record} ctx * @param {string} argv0 * @param {object} opts * @param {boolean} opts.setupFlag * @param {boolean} opts.interactiveSetup */ async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) { bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json') const readLine = typeof ctx.readLine === 'function' ? /** @type {(p: string) => Promise} */ ( ctx.readLine.bind(ctx) ) : null if (!readLine) { bareAgentErr( ctx, argv0 + ': interactive setup requires ctx.readLine (TTY session recommended). Edit ' + paths.config + ' manually.' ) return config } const url = (await readLine(`REST base URL [${config.rest_base_url}]: `)) || '' if (url.trim()) config.rest_base_url = url.trim() const keyRaw = (await readLine(`REST API key (paste; may echo) [leave empty to skip]: `)) || '' if (keyRaw.trim()) config.rest_api_key = keyRaw.trim() const modelRaw = (await readLine(`Model [${config.model}]: `)) || '' if (modelRaw.trim()) config.model = modelRaw.trim() const provRaw = (await readLine(`Provider label [${config.provider}]: `)) || '' if (provRaw.trim()) config.provider = provRaw.trim() await bareAgentSaveConfig(ctx, paths, config) bareAgentLog(ctx, 'Configuration saved.') return config } /** * Interactive setup only (`agent --setup`). * @param {Record} ctx * @param {string} argv0 */ async function bareAgentRunSetupOnly(ctx, argv0) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) let { config } = await bareAgentLoadOrCreateConfig(ctx, paths) config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, { setupFlag: true, interactiveSetup: true }) try { if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600) } catch { /* ignore */ } void config } /** * @param {Record} ctx * @param {string} argv0 * @param {string} task * @param {{ setupFlag?: boolean }} [runOpts] */ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) { const setupFlag = Boolean(runOpts && runOpts.setupFlag) const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) let { config } = await bareAgentLoadOrCreateConfig(ctx, paths) const readLine = typeof ctx.readLine === 'function' ? /** @type {(p: string) => Promise} */ ( ctx.readLine.bind(ctx) ) : null const stdin = /** @type {{ isTTY?: boolean } | undefined} */ (ctx.replStdin) const isTTY = Boolean(stdin && stdin.isTTY) if ( setupFlag || (!(config.rest_api_key && String(config.rest_api_key).trim()) && readLine && isTTY) ) { config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, { setupFlag, interactiveSetup: true }) } if (!config.rest_api_key || !String(config.rest_api_key).trim()) { bareAgentErr( ctx, argv0 + ': set rest_api_key in ' + paths.config + ' or run `' + argv0 + ' --setup`.' ) ctx.exitCode = 1 return } try { if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600) } catch { /* ignore — optional */ } const fetchFn = bareAgentResolveFetch(ctx) if (!fetchFn) { bareAgentErr(ctx, argv0 + ': no fetch (ctx.httpFetch / bare.fetch / global fetch)') ctx.exitCode = 1 return } /** @type {unknown[]} */ let messages = await bareAgentLoadHistory(ctx, paths.history) const instructions = await bareAgentLoadInstructionFiles(ctx, paths) const manDigest = await bareAgentManDigest(ctx) let systemContent = BARE_AGENT_STATIC_SYSTEM + '\n\n' + bareAgentSessionHomeBlock(ctx, home, paths) + '\n\n' + manDigest.slice(0, 12000) if (instructions) systemContent += '\n\n## Session notes\n' + instructions.slice(0, 8000) if (!messages.length) { messages = [ { role: 'system', content: systemContent }, { role: 'user', content: task } ] } else { const hasSystem = messages[0] && typeof messages[0] === 'object' && /** @type {{ role?: string }} */ (messages[0]).role === 'system' if (!hasSystem) { messages = [{ role: 'system', content: systemContent }, ...messages] } else { messages[0] = { role: 'system', content: systemContent } } messages.push({ role: 'user', content: task }) } const stdout = /** @type {import('stream').Writable | undefined} */ ( ctx.replStdout || ctx.stdout ) const useColor = bareEditUseColor(ctx) /** @type {{ current: Record }} */ const configRef = { current: { ...config } } /** @type {{ db: unknown | null }} */ const manCacheRef = { db: null } let completed = false let taskSummary = '' function onTaskComplete(summary) { completed = true taskSummary = summary } function appendProgress(line) { void bareAgentAppendProgress(ctx, paths.progress, line) } const url = bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) + '/chat/completions' const tools = bareAgentToolDefinitions() let suspended = false try { if (typeof ctx.suspendReplForSubprocess === 'function') { ctx.suspendReplForSubprocess() suspended = true } const masterAbort = new AbortController() /** @type {(() => void) | null} */ let offSigint = null if (globalThis.process && typeof globalThis.process.on === 'function') { const fn = () => { masterAbort.abort() bareAgentWriteOut( stdout, bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n' ) } globalThis.process.on('SIGINT', fn) offSigint = () => { try { globalThis.process.off('SIGINT', fn) } catch { /* ignore */ } } } const maxIter = Number(configRef.current.max_iterations) || 64 let iter = 0 for (;;) { if (completed) break iter++ if (iter > maxIter) { bareAgentErr(ctx, argv0 + ': max_iterations exceeded') ctx.exitCode = 1 break } messages = bareAgentTrimMessages(messages, 450_000) const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + String(configRef.current.rest_api_key || '') } const eh = configRef.current.extra_headers if (eh && typeof eh === 'object' && !Array.isArray(eh)) { for (const [k, v] of Object.entries(eh)) { if (typeof v === 'string') headers[k] = v } } /** @type {Record} */ const body = { model: String(configRef.current.model || ''), stream: Boolean(configRef.current.stream !== false), messages, tools, tool_choice: 'auto', max_tokens: Number(configRef.current.max_tokens) || 4096, temperature: Number(configRef.current.temperature) ?? 0.7 } let assistantContent = '' /** @type {Map} */ const toolAcc = new Map() /** @type {unknown} */ let usageOut = null let finishReason = '' try { await bareAgentStreamChatCompletions({ fetchFn, url, headers, body, signal: masterAbort.signal, onEvent: (ev) => { const e = /** @type {Record} */ (ev) if (e.type === 'delta_content') { const chunk = typeof e.content === 'string' ? e.content : '' assistantContent += chunk bareAgentWriteOut( stdout, bareEditSgr('string', useColor) + chunk + EDIT_ANSI_RESET ) } else if (e.type === 'delta_tool_calls') { const arr = e.tool_calls if (Array.isArray(arr)) { for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc) } } else if (e.type === 'usage') { usageOut = e.usage } else if (e.type === 'finish_reason') { finishReason = String(e.finish_reason || '') } } }) } catch (e) { const msg = e && typeof e === 'object' && 'name' in e && e.name === 'AbortError' ? 'aborted' : e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) bareAgentErr(ctx, argv0 + ': ' + msg) ctx.exitCode = 130 break } const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc) const hasTools = toolCallsArr.length > 0 /** @type {Record} */ const assistantMsg = { role: 'assistant', content: assistantContent || null, tool_calls: hasTools ? toolCallsArr : undefined } messages.push(assistantMsg) if (usageOut && typeof usageOut === 'object') { const u = /** @type {Record} */ (usageOut) const pt = u.prompt_tokens const ct = u.completion_tokens bareAgentWriteOut( stdout, '\n' + bareEditSgr('dim', useColor) + 'tokens: prompt=' + String(pt ?? '?') + ' completion=' + String(ct ?? '?') + EDIT_ANSI_RESET + '\n' ) } if (!hasTools) { await bareAgentSaveHistory(ctx, paths.history, messages) bareAgentWriteOut(stdout, '\n') break } appendProgress( 'iteration ' + iter + ' tools ' + toolCallsArr.map((x) => /** @type {{ function?: { name?: string } }} */ (x).function?.name ).join(',') ) for (const tc of toolCallsArr) { const fn = /** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ ( tc ).function const id = /** @type {{ id?: string }} */ (tc).id || '' const name = fn?.name || '' const argsStr = fn?.arguments || '{}' bareAgentWriteOut( stdout, '\n' + bareEditSgr('keyword', useColor) + '[tool] ' + name + EDIT_ANSI_RESET + '\n' ) const spinner = bareEditSgr('dim', useColor) + '… running ' + name + EDIT_ANSI_RESET bareAgentWriteOut(stdout, spinner + '\r') const resultStr = await bareAgentDispatchTool({ ctx, toolName: name, argsJson: argsStr, paths, signal: masterAbort.signal, appendProgress, home, configRef, manCacheRef, onTaskComplete }) bareAgentWriteOut(stdout, '\x1b[K') messages.push({ role: 'tool', tool_call_id: id, content: resultStr }) if (completed) break } await bareAgentSaveHistory(ctx, paths.history, messages) if (completed) { bareAgentWriteOut( stdout, bareEditSgr('dim', useColor) + '\nDone: ' + taskSummary + EDIT_ANSI_RESET + '\n' ) break } } if (offSigint) offSigint() } finally { if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') { ctx.resumeReplAfterSubprocess() } } } /** * Autonomous AI agent: OpenAI-compatible REST, ReAct loop, ~/.agent/config.json. */ async function run(ctx, argv) { const argv0 = argv[0] || 'agent' const args = argv.slice(1) const wantHelp = args.includes('-h') || args.includes('--help') if (wantHelp || args.length === 0) { ctx.console.log( 'usage: ' + argv0 + ' [--setup | --reset] YOUR_REQUEST_HERE\n' + ' ' + argv0 + ' --setup\n' + ' ' + argv0 + ' --reset\n' + ' ' + argv0 + ' reset\n' + '\n' + 'Runs an autonomous coding/OS agent against any OpenAI-compatible HTTPS API.\n' + 'Configuration: ~/.agent/config.json on your personal drive (created on first run).\n' + 'Use --setup to interactively set API URL, key, model, and provider label.\n' + 'Use --reset or `reset` to clear ~/.agent/history.json and start a fresh chat session.\n' + '\n' + 'Examples:\n' + ' ' + argv0 + ' "summarize ~/README and list five files in /bin"\n' + ' ' + argv0 + ' --setup\n' + ' ' + argv0 + ' --reset\n' + '\n' + 'See man agent.' ) ctx.exitCode = wantHelp ? 0 : 1 return } let setupFlag = false let resetFlag = false /** @type {string[]} */ const rest = [] for (let i = 0; i < args.length; i++) { const a = args[i] if (a === '--setup') setupFlag = true else if (a === '--reset') resetFlag = true else rest.push(a) } const wantReset = resetFlag || (rest.length === 1 && rest[0] === 'reset') if (wantReset) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) try { await bareAgentResetChatSession(ctx, paths, argv0) ctx.exitCode = 0 } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) ctx.console.error(argv0 + ': ' + msg) ctx.exitCode = 1 } return } const task = rest.join(' ').trim() if (!task && !setupFlag) { ctx.console.error(argv0 + ': missing task (or use --setup)') ctx.exitCode = 1 return } if (setupFlag && !task) { await bareAgentRunSetupOnly(ctx, argv0) return } await bareOsRunAgentSession(ctx, argv0, task, { setupFlag }) }