/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** Session env map (`vfs.env`, then `ctx.env`). Never throws. */ function bareOsEnv(ctx) { const v = ctx && ctx.vfs && ctx.vfs.env if (v && typeof v === 'object') return v const e = ctx && ctx.env if (e && typeof e === 'object') return e return {} } /** * Strict POSIX-ish decimal integer (no octal, no exponent, no empty). * @param {unknown} s * @returns {number} */ function bareOsParseDecInt(s) { const t = String(s == null ? '' : s).trim() if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN const n = Number.parseInt(t, 10) return Number.isSafeInteger(n) ? n : NaN } /** @param {unknown} s */ function bareOsParseNonNegInt(s) { const n = bareOsParseDecInt(s) return n >= 0 ? n : NaN } /** * @param {Record} ctx * @param {string} name * @param {number} fallback * @param {number} [min] * @param {number} [max] */ function bareOsEnvInt(ctx, name, fallback, min, max) { const raw = bareOsEnv(ctx)[name] if (raw == null || raw === '') return fallback const n = Number.parseInt(String(raw), 10) if (!Number.isFinite(n)) return fallback let v = n if (min != null && v < min) v = min if (max != null && v > max) v = max return v } /** * @param {Record} ctx * @param {string} msg * @param {number} [code] */ function bareOsFail(ctx, msg, code) { if (msg) ctx.console.error(msg) ctx.exitCode = code == null ? 1 : code } /** @param {unknown} e */ function bareOsIsNotFoundErr(e) { const code = e && typeof e === 'object' ? e.code : '' if (code === 'ENOENT') return true const msg = String((e && e.message) || e || '') return /ENOENT|No such file|not found/i.test(msg) } /** * @param {Record} ctx * @param {unknown} buf * @returns {Uint8Array} */ function bareOsToU8(ctx, buf) { if (!buf) return new Uint8Array(0) if (buf instanceof Uint8Array) return buf if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf) return new Uint8Array(buf) } /** @param {string} dir @param {string} name */ function bareOsJoinPath(dir, name) { const d = String(dir || '').replace(/\/+$/, '') const n = String(name || '').replace(/^\/+/, '') if (!d || d === '/') return '/' + n return d + '/' + n } /** @param {string} p */ function bareOsBaseName(p) { const t = String(p || '').replace(/\/+$/, '') if (!t || t === '/') return t === '/' ? '/' : '' const i = t.lastIndexOf('/') return i < 0 ? t : t.slice(i + 1) || t } /** @param {string} p */ function bareOsParentDir(p) { const t = String(p || '').replace(/\/+$/, '') || '/' if (t === '/') return '/' const i = t.lastIndexOf('/') return i <= 0 ? '/' : t.slice(0, i) || '/' } /** @param {string} p */ function bareOsNormPath(p) { return String(p || '').replace(/\/+$/, '') || '/' } /** * @param {Record} ctx * @param {string} p */ function bareOsResolvePath(ctx, p) { if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') { try { return String(ctx.vfs.resolveLogical(p) || p) } catch { /* fall through */ } } return String(p || '') } /** * True when dest is src or lives under src (self-copy / self-move). * @param {Record} ctx * @param {string} src * @param {string} dest */ function bareOsDestInsideSrc(ctx, src, dest) { const s = bareOsNormPath(bareOsResolvePath(ctx, src)) const d = bareOsNormPath(bareOsResolvePath(ctx, dest)) if (s === d) return true if (s === '/') return d !== '/' return d === s || d.startsWith(s + '/') } const BARE_OS_B64_ALPH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' /** @param {Uint8Array} u8 */ function bareOsB64Encode(u8) { let out = '' let i = 0 for (; i + 2 < u8.length; i += 3) { const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2] out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + BARE_OS_B64_ALPH[n & 63] } const rest = u8.length - i if (rest === 1) { const n = u8[i] << 16 out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '==' } else if (rest === 2) { const n = (u8[i] << 16) | (u8[i + 1] << 8) out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + BARE_OS_B64_ALPH[(n >> 6) & 63] + '=' } return out } /** * RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk. * @param {string} s * @returns {Uint8Array} */ function bareOsB64Decode(s) { const t = String(s).replace(/\s+/g, '') if (!t) return new Uint8Array(0) if (t.length % 4 === 1) throw new Error('invalid base64 length') let pad = 0 if (t.endsWith('==')) pad = 2 else if (t.endsWith('=')) pad = 1 const body = pad ? t.slice(0, t.length - pad) : t const bytes = [] let buf = 0 let bits = 0 for (let i = 0; i < body.length; i++) { const c = body[i] let v = BARE_OS_B64_ALPH.indexOf(c) if (v < 0) { if (c === '-') v = 62 else if (c === '_') v = 63 else throw new Error('invalid base64 character') } buf = (buf << 6) | v bits += 6 if (bits >= 8) { bits -= 8 bytes.push((buf >> bits) & 255) } } if (pad) { const want = Math.floor((body.length * 6) / 8) if (bytes.length > want) bytes.length = want } return new Uint8Array(bytes) } /** * @param {string} s * @returns {Uint8Array} */ function bareOsHexDecode(s) { const t = String(s).replace(/\s+/g, '') if (t.length % 2 !== 0) throw new Error('odd hex length') const out = new Uint8Array(t.length / 2) for (let i = 0; i < out.length; i++) { const pair = t.slice(i * 2, i * 2 + 2) if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex') out[i] = Number.parseInt(pair, 16) } return out } /** @param {Uint8Array} u8 */ function bareOsHexEncode(u8) { let s = '' for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0') return s } /** * Shared GNU-style checksum FILE / stdin loop used by *sum commands. * @param {Record} ctx * @param {string[]} argv * @param {{ * cmd: string, * help?: string, * digest: (u8: Uint8Array) => string | Promise * }} opts */ async function bareChecksumRun(ctx, argv, opts) { const cmd = String(opts.cmd || 'checksum') const help = opts.help || 'usage: ' + cmd + ' [FILE]...\nWith no FILE, or when FILE is -, read standard input.' const digest = opts.digest const paths = [] for (let i = 1; i < argv.length; i++) { const a = argv[i] if (a === '-h' || a === '--help') { ctx.console.log(help) ctx.exitCode = 0 return } if (a.startsWith('-') && a !== '-') { ctx.console.error(cmd + ': unsupported option ' + a) ctx.exitCode = 1 return } paths.push(a) } const b4 = ctx.b4a async function one(name, buf) { const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf) try { const hex = await digest(u8) ctx.console.log(hex + ' ' + name) } catch (e) { ctx.console.error(cmd + ': ' + (e.message || e)) ctx.exitCode = 1 } } if (!paths.length || (paths.length === 1 && paths[0] === '-')) { await one('-', b4.from(bareStdin(ctx))) return } for (const p of paths) { if (p === '-') { await one('-', b4.from(bareStdin(ctx))) continue } const b = await ctx.vfs.readFile(p) if (!b) { ctx.console.error(cmd + ': ' + p + ': No such file') ctx.exitCode = 1 continue } await one(p, b) } } /** * WebCrypto hex digest; throws the same missing-subtle message as the old *sum files. * @param {string} algo * @param {string} missingMsg * @returns {(u8: Uint8Array) => Promise} */ function bareSubtleHexDigest(algo, missingMsg) { return async function (u8) { const subtle = globalThis.crypto?.subtle if (!subtle || typeof subtle.digest !== 'function') { throw new Error(missingMsg) } const hash = await subtle.digest(algo, u8) return bareOsHexEncode(new Uint8Array(hash)) } } /** * Owner/group name → uid/gid used by chown / chgrp. * @param {string} spec * @param {Record} env * @returns {number | null} */ function parseGroupSpec(spec, env) { const s = String(spec).trim() if (!s) return null if (/^\d+$/.test(s)) { const n = Number.parseInt(s, 10) return Number.isFinite(n) ? n : null } const g = (env.GROUP || env.USER || 'guest').trim() if (s === 'root') return 0 if (s === 'guest' || s === 'nobody') return 65534 if (g && s === g) { const gid = Number.parseInt(String(env.GID ?? '65534'), 10) return Number.isFinite(gid) ? gid : 65534 } return null } /** * User name / numeric uid used by chown. * @param {string} spec * @param {Record} env * @returns {{ uid: number | null, gid: number | null }} */ function parseIdSpec(spec, env) { const s = String(spec).trim() if (!s) return { uid: null, gid: null } if (/^\d+$/.test(s)) { const n = Number.parseInt(s, 10) return { uid: Number.isFinite(n) ? n : null, gid: null } } const u = (env.USER || env.LOGNAME || '').trim() if (s === 'root') return { uid: 0, gid: null } if (s === 'guest' || s === 'nobody') return { uid: 65534, gid: null } if (u && s === u) { const uid = Number.parseInt(String(env.UID ?? '65534'), 10) return { uid: Number.isFinite(uid) ? uid : 65534, gid: null } } return { uid: null, gid: null } } /** 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). */ /** * Read-only base system (kernel / system Hyperdrive + virtual fs). * Everything else is writable by default (denylist, not allowlist). * @type {readonly string[]} */ var BARE_AGENT_MUTATE_DENY_PREFIXES = Object.freeze([ '/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run' ]) /** * Seed list for diagnostic snapshots. Live reads allow any /proc path. * @type {readonly string[]} */ var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([ '/proc/bare_os/metrics_live.json', '/proc/bare_os/features', '/proc/bare_os/features.json', '/proc/bare_os/swarm.json', '/proc/bare_os/capabilities.json', '/proc/bare_os/swarm_replication_status.json', '/proc/bare_os/swarm_relay_status.json', '/proc/bare_os/swarm_datagrams_status.json', '/proc/bare_os/swarm_connection_manager_status.json', '/proc/bare_os/swarm_key_broker_status.json', '/proc/bare_os/swarm_holepunch_status.json', '/proc/bare_os/swarm_datagram_replication_status.json', '/proc/bare_os/swarm_status.json' ]) /** * @param {string} absPath * @returns {string} */ function bareAgentNormalizeAbsPath(absPath) { const p = String(absPath || '').replace(/\\/g, '/') if (!p.startsWith('/') || p.includes('..')) return '' if (p.length > 1) return p.replace(/\/+$/, '') return p } /** * Expand ~ / $HOME / relative guest paths to an absolute VFS path. * @param {string} raw * @param {string} home * @param {string} [cwd] */ function bareAgentExpandGuestPath(raw, home, cwd) { let p = String(raw == null ? '' : raw).trim() if (!p) return '' p = p.replace(/\\/g, '/') const h = String(home || '').replace(/\/+$/, '') || '/home/guest' let c = String(cwd || h).replace(/\/+$/, '') || h if (c === '~') c = h else if (c.startsWith('~/')) c = h + c.slice(1) else if (c.startsWith('$HOME/')) c = h + c.slice(5) else if (c === '$HOME') c = h if (p === '~' || p === '$HOME') return h if (p.startsWith('~/')) { p = h + '/' + p.slice(2) } else if (p.startsWith('$HOME/')) { p = h + '/' + p.slice(6) } else if (!p.startsWith('/')) { p = (c === '/' ? '/' + p : c + '/' + p) } p = p.replace(/\/{2,}/g, '/') if (p.length > 1) p = p.replace(/\/+$/, '') return p } /** * Pick a path-like argument from a tool-call blob (path / file_path / dir / …). * @param {Record} args */ function bareAgentPickPathArg(args) { if (!args || typeof args !== 'object') return '' const keys = [ 'path', 'file_path', 'file', 'filename', 'dir', 'directory', 'folder', 'from_path', 'to_path' ] for (let i = 0; i < keys.length; i++) { const v = args[keys[i]] if (typeof v === 'string' && v.trim()) return v.trim() } return '' } /** * @param {string} absPath * @param {unknown} prefixes * @returns {boolean} */ function bareAgentPrefixDenied(absPath, prefixes) { const p = bareAgentNormalizeAbsPath(absPath) if (!p) return true const list = Array.isArray(prefixes) && prefixes.length ? prefixes : BARE_AGENT_MUTATE_DENY_PREFIXES for (let i = 0; i < list.length; i++) { const pref = String(list[i] || '').replace(/\/+$/, '') if (!pref) continue if (p === pref || p.startsWith(pref + '/')) return true } return false } /** * Any absolute guest path is readable (denylist-empty). * @param {string} absPath */ function bareAgentPathAllowedRead(absPath) { return Boolean(bareAgentNormalizeAbsPath(absPath)) } /** * Writes/renames/deletes: whole VFS except the read-only base system. * @param {string} absPath * @param {unknown} [prefixes] */ function bareAgentPathAllowedMutate(absPath, prefixes) { const p = bareAgentNormalizeAbsPath(absPath) if (!p || p === '/') return false return !bareAgentPrefixDenied(p, prefixes) } /** * @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 = bareAgentNormalizeAbsPath(absPath) return Boolean(p && (p === '/proc' || p.startsWith('/proc/'))) } /** * Kernel-runner evals guest scripts as AsyncFunction('ctx','argv', body). * Agent-authored code that redeclares ctx/argv or uses ESM export throws * "Identifier 'ctx' has already been declared" — same contract as /bin * utilities: define async function run(ctx, argv), never bind ctx yourself. * @param {string} code */ function bareAgentNormalizeGuestScript(code) { let src = String(code || '').replace(/^\uFEFF/, '') src = src.replace(/^#!.*(?:\r?\n|$)/, '') src = src.replace(/^\s*export\s*\{[^}]*\}\s*;?\s*$/gm, '') src = src.replace(/^export\s+default\s+/gm, '') src = src.replace(/^export\s+async\s+function\s+/gm, 'async function ') src = src.replace(/^export\s+function\s+/gm, 'function ') src = src.replace(/^export\s+const\s+/gm, 'const ') src = src.replace(/^export\s+let\s+/gm, 'let ') src = src.replace(/^export\s+var\s+/gm, 'var ') src = src.replace(/^(?:const|let|var)\s+ctx\b[^;\n]*;?\s*$/gm, '') src = src.replace(/^(?:const|let|var)\s+argv\b[^;\n]*;?\s*$/gm, '') src = src.replace( /^(?:const|let|var)\s*\{\s*ctx[\s,}][^;\n]*;?\s*$/gm, '' ) if ( !/\b(?:async\s+)?function\s+run\s*\(/.test(src) && !/\brun\s*=\s*(?:async\s*)?(?:function\s*)?\(/.test(src) ) { src = 'async function run(ctx, argv) {\n' + src.trim() + '\n}\n' } return src.trim() + '\n' } /** * Trim glued prose off a curl/wget URL (`orgBut` → `org`). * @param {string} cmd */ function bareAgentTrimGluedShellCommand(cmd) { let s = String(cmd || '').trim() s = s.replace(/(\.(?:org|com|net|io|dev|fyi|info|edu|co))([A-Z].*)$/i, '$1') s = s.replace(/(https?:\/\/[^\s]+?)(?=[A-Z][a-z])/, '$1') return s.trim() } /** * If a run_js_script body is actually a guest CLI (curl/wget/ip), return * the command so dispatch can divert to run_command. * @param {string} code * @returns {string} */ function bareAgentShellCommandFromJsScript(code) { const s = String(code || '') if (!s.trim()) return '' const trimmed = s.trim() const looksShell = typeof bareAgentLooksLikeShellCommand === 'function' ? bareAgentLooksLikeShellCommand(trimmed) : /^(curl|wget|ip|ping|ifconfig)\b/i.test(trimmed) if ( looksShell && !/\bfunction\b|\bclass\b|\bawait\b|\bconst\b|\blet\b|\bvar\b|\bimport\b/.test(trimmed) ) { return bareAgentTrimGluedShellCommand(trimmed) } const execRe = /\b(?:execLine|execSync|exec|spawnSync|system)\s*\(\s*(['"`])([\s\S]*?)\1/g let m while ((m = execRe.exec(s))) { const cmd = String(m[2] || '').trim() if (/^(curl|wget|ip|ping|ifconfig)\b/i.test(cmd)) { return bareAgentTrimGluedShellCommand(cmd) } } const fetchRe = /\bfetch\s*\(\s*(['"`])(https?:\/\/[^'"`]+)\1/ m = fetchRe.exec(s) if (m) return 'curl -s ' + String(m[2] || '').trim() const curlRe = /(?:^|[\n;])\s*((?:curl|wget)\s+https?:\/\/[^\s;'"`]+)/im m = curlRe.exec(s) if (m) return bareAgentTrimGluedShellCommand(m[1]) return '' } /** * task_complete used as a surrender after a failed tool, not a real finish. * @param {string} summary */ function bareAgentLooksLikeFailedTaskComplete(summary) { const s = String(summary || '').toLowerCase() if (!s.trim()) return false return ( /error occurred/.test(s) || /alternative method required/.test(s) || (/attempted to /.test(s) && /error|fail/.test(s)) || /failed due to/.test(s) || /could not (run|execute|retrieve|complete|get)/.test(s) || /not (able|allowed) to (run|execute|complete)/.test(s) || /top-level (scope|await|module)/.test(s) ) } /** * User asked to run a guest CLI (curl / IP lookup), not write JS. * @param {string} text */ function bareAgentLooksLikeGuestCliRequest(text) { const s = String(text || '').toLowerCase() if (!s.trim()) return false return ( /\b(run|execute|use)\b.{0,48}\b(curl|wget|ping|ifconfig|ip addr)\b/.test(s) || /\b(curl|wget)\b.{0,48}\b(ip address|public ip|our ip|my ip)\b/.test(s) || /\b(find|get|check|show|lookup)\b.{0,48}\b(ip address|public ip|our ip|my ip)\b/.test( s ) ) } /** ~/.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', compact: base + '/compact.md', cmdOut: base + '/last_command_out.txt', workspace: base + '/workspace', workspaceMemory: base + '/workspace/memory', workspaceSkills: base + '/workspace/skills', skillsGlobal: base + '/skills', todos: base + '/todos.json', plan: base + '/plan.md', hooks: base + '/hooks', ask: base + '/ask.json', edits: base + '/edits.json', lastCwd: base + '/last_cwd' } } function bareAgentDefaultConfig() { return { backend: 'qvac', rest_base_url: 'https://api.groq.com/openai/v1', rest_api_key: '', model: 'QWEN3_1_7B_INST_Q4', qvac_model: 'QWEN3_1_7B_INST_Q4', qvac_profile: 'recommended', qvac_ctx_size: 0, qvac_device: '', qvac_main_gpu: 'auto', qvac_gpu_layers: -1, max_tokens: 4096, temperature: 0.7, provider: 'qvac', max_iterations: 64, stream: true, tool_parallelism: 1, request_timeout_ms: 120000, extra_headers: /** @type {Record} */ ({}), access_policy: 'full', allow_delete: true, require_confirm_token: '', owner_name: '', agent_label: '', show_reasoning: false, reasoning_mode: 'off', reasoning_max_chars: 4000, reasoning_include_tools: true, allow_bridge_mutations: true, allow_host_notifications: true, allow_host_actions: true, emergency_stop_mutations: false, autonomous_mode_enabled: true, autonomous_max_runtime_ms: 1800000, autonomous_completion_required_checks: [], autonomous_allow_paths: ['*'], autonomous_deny_ops: [], command_deny: [], mutate_deny_prefixes: [ '/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run' ], autonomous_active: false, autonomous_started_at_ms: 0, autonomous_stop_requested: false, autonomous_goal: '', autonomous_status: 'idle', autonomous_last_error: '', context_compaction: 'auto', compaction_keep_recent: 8, compaction_tool_chars: 1600, plan_mode_active: false, todo_nudge_enabled: true } } /** * @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([ 'backend', 'rest_base_url', 'rest_api_key', 'model', 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'access_policy', 'allow_delete', 'require_confirm_token', 'command_deny', 'mutate_deny_prefixes', 'owner_name', 'agent_label', 'show_reasoning', 'reasoning_mode', 'reasoning_max_chars', 'reasoning_include_tools', 'allow_bridge_mutations', 'allow_host_notifications', 'allow_host_actions', 'emergency_stop_mutations', 'autonomous_mode_enabled', 'autonomous_max_runtime_ms', 'autonomous_completion_required_checks', 'autonomous_allow_paths', 'autonomous_deny_ops', 'autonomous_active', 'autonomous_started_at_ms', 'autonomous_stop_requested', 'autonomous_goal', 'autonomous_status', 'autonomous_last_error', 'context_compaction', 'compaction_keep_recent', 'compaction_tool_chars', 'plan_mode_active', 'todo_nudge_enabled' ]) 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 === 'backend') { const b = String(val ?? '').trim().toLowerCase() out.backend = b === 'rest' || b === 'openai' || b === 'http' ? 'rest' : 'qvac' continue } if ( k === 'rest_base_url' || k === 'rest_api_key' || k === 'model' || k === 'qvac_model' || k === 'qvac_profile' || k === 'provider' || k === 'owner_name' || k === 'agent_label' || k === 'autonomous_goal' || k === 'autonomous_status' || k === 'autonomous_last_error' || k === 'qvac_device' || k === 'qvac_main_gpu' || k === 'require_confirm_token' ) { out[k] = String(val ?? '') continue } if (k === 'context_compaction') { const mode = String(val ?? '').trim().toLowerCase() out.context_compaction = mode === 'off' || mode === 'aggressive' ? mode : 'auto' continue } if (k === 'reasoning_mode') { const mode = String(val ?? '').trim().toLowerCase() out.reasoning_mode = mode === 'summary' || mode === 'trace' ? mode : 'off' continue } if ( k === 'max_tokens' || k === 'temperature' || k === 'max_iterations' || k === 'tool_parallelism' || k === 'request_timeout_ms' || k === 'reasoning_max_chars' || k === 'qvac_ctx_size' || k === 'qvac_gpu_layers' || k === 'autonomous_max_runtime_ms' || k === 'autonomous_started_at_ms' || k === 'compaction_keep_recent' || k === 'compaction_tool_chars' ) { const n = Number(val) out[k] = Number.isFinite(n) ? n : defaults[k] continue } if ( k === 'autonomous_completion_required_checks' || k === 'autonomous_allow_paths' || k === 'autonomous_deny_ops' || k === 'command_deny' || k === 'mutate_deny_prefixes' ) { out[k] = Array.isArray(val) ? val.map((x) => String(x ?? '')).filter(Boolean) : defaults[k] continue } if ( k === 'stream' || k === 'allow_delete' || k === 'show_reasoning' || k === 'reasoning_include_tools' || k === 'allow_bridge_mutations' || k === 'allow_host_notifications' || k === 'allow_host_actions' || k === 'emergency_stop_mutations' || k === 'autonomous_mode_enabled' || k === 'autonomous_active' || k === 'autonomous_stop_requested' || k === 'plan_mode_active' || k === 'todo_nudge_enabled' ) { out[k] = Boolean(val) continue } if (k === 'require_confirm_token') { out.require_confirm_token = String(val ?? '') continue } if (k === 'access_policy') { const pol = String(val ?? '').trim().toLowerCase() out.access_policy = pol === 'restricted' ? 'restricted' : 'full' continue } } return out } /** * Old configs persisted restrictive defaults. Missing access_policy means * upgrade onto full guest admin (denylist-only) so existing homes match. * @param {Record} raw * @param {Record} merged */ function bareAgentApplyAccessPolicyUpgrade(raw, merged) { const src = bareAgentIsPlainObject(raw) ? raw : {} if (Object.prototype.hasOwnProperty.call(src, 'access_policy')) { return { config: merged, upgraded: false } } const next = { ...merged } next.access_policy = 'full' next.allow_delete = true next.require_confirm_token = '' next.allow_bridge_mutations = true next.allow_host_notifications = true next.allow_host_actions = true next.emergency_stop_mutations = false next.autonomous_deny_ops = [] next.autonomous_allow_paths = ['*'] next.command_deny = Array.isArray(next.command_deny) ? next.command_deny : [] next.autonomous_mode_enabled = true if (!Array.isArray(next.mutate_deny_prefixes) || !next.mutate_deny_prefixes.length) { next.mutate_deny_prefixes = bareAgentDefaultConfig().mutate_deny_prefixes } return { config: next, upgraded: true } } /** * @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 = [ 'backend', 'rest_base_url', 'rest_api_key', 'model', 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'extra_headers', 'access_policy', 'allow_delete', 'require_confirm_token', 'command_deny', 'mutate_deny_prefixes', 'owner_name', 'agent_label', 'show_reasoning', 'reasoning_mode', 'reasoning_max_chars', 'reasoning_include_tools', 'allow_bridge_mutations', 'allow_host_notifications', 'allow_host_actions', 'emergency_stop_mutations', 'autonomous_mode_enabled', 'autonomous_max_runtime_ms', 'autonomous_completion_required_checks', 'autonomous_allow_paths', 'autonomous_deny_ops', 'autonomous_active', 'autonomous_started_at_ms', 'autonomous_stop_requested', 'autonomous_goal', 'autonomous_status', 'autonomous_last_error', 'context_compaction', 'compaction_keep_recent', 'compaction_tool_chars', 'plan_mode_active', 'todo_nudge_enabled' ] 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() if (typeof bareAgentSanitizeConfigForBackend === 'function') { raw = bareAgentSanitizeConfigForBackend(raw) } await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw)) return { config: /** @type {any} */ (raw), created: true } } bareAgentValidateConfigShape(raw) const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw) const applied = bareAgentApplyAccessPolicyUpgrade(raw, merged) let next = applied.config if (typeof bareAgentSanitizeConfigForBackend === 'function') { next = bareAgentSanitizeConfigForBackend(next) } const dirty = applied.upgraded || JSON.stringify(next) !== JSON.stringify(merged) if (dirty) { try { await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (next)) } catch { /* keep upgraded in-memory even if persist fails */ } } return { config: /** @type {any} */ (next), 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') let next = config if (typeof bareAgentSanitizeConfigForBackend === 'function') { next = bareAgentSanitizeConfigForBackend(config) if (config && typeof config === 'object' && config !== next) { for (const k of Object.keys(config)) { if (!Object.prototype.hasOwnProperty.call(next, k)) delete config[k] } Object.assign(config, next) } } bareAgentValidateConfigShape(next) const json = JSON.stringify(next, 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 } /** * Rough token estimate (chars/4). Good enough for local ctx budgeting. * @param {unknown} value */ function bareAgentEstimateTokens(value) { try { const n = JSON.stringify(value == null ? '' : value).length return Math.max(0, Math.ceil(n / 4)) } catch { return 0 } } /** * Trim history + shrink system content so prompt fits a QVAC ctx window. * Reserves room for completion and optional tools JSON. * @param {unknown[]} msgs * @param {number} ctxSize * @param {{ tools?: unknown[], reserveCompletion?: number }} [opts] */ function bareAgentTrimMessagesForCtx(msgs, ctxSize, opts) { const ctx = Math.max(2048, Math.floor(Number(ctxSize) || 4096)) const reserve = opts && Number.isFinite(Number(opts.reserveCompletion)) ? Math.max(256, Math.floor(Number(opts.reserveCompletion))) : Math.min(1024, Math.max(256, Math.floor(ctx * 0.15))) const toolsTok = bareAgentEstimateTokens( opts && Array.isArray(opts.tools) && opts.tools.length ? opts.tools : [] ) const budget = Math.max(512, ctx - reserve - toolsTok) /** @type {unknown[]} */ let out = Array.isArray(msgs) ? msgs.slice() : [] // Drop middle turns first (keep system + recent). while (bareAgentEstimateTokens(out) > budget && out.length > 3) { out.splice(2, 1) } // Shrink system blob if still over (workspace/man/skills dominate). if (bareAgentEstimateTokens(out) > budget && out[0] && typeof out[0] === 'object') { const sys = /** @type {Record} */ (out[0]) if (sys.role === 'system' && typeof sys.content === 'string') { let content = sys.content let guard = 0 while ( bareAgentEstimateTokens(out) > budget && content.length > 800 && guard < 24 ) { content = content.slice(0, Math.floor(content.length * 0.82)) + '\n… truncated' out[0] = { ...sys, content } guard++ } } } 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 compacted = typeof bareAgentCompactMessagesForDisk === 'function' ? bareAgentCompactMessagesForDisk(messages, 500_000, { keepRecent: 16 }) : bareAgentTrimMessages(messages, 500_000) const trimmed = typeof bareAgentTrimMessages === 'function' ? bareAgentTrimMessages(compacted, 500_000) : compacted 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 [] } } /** * Start an autonomous run on a config object (does not persist). * @param {Record} cfg * @param {{ goal: string, maxRuntimeMs?: number, requiredChecks?: string[], scopePath?: string }} opts */ function bareAgentBeginAutonomousRun(cfg, opts) { const goal = String((opts && opts.goal) || '').trim() const maxRuntimeMs = Math.min( Math.max(Math.floor(Number((opts && opts.maxRuntimeMs) || cfg.autonomous_max_runtime_ms) || 0), 60000), 7_200_000 ) const requiredChecks = Array.isArray(opts && opts.requiredChecks) ? opts.requiredChecks.map((x) => String(x || '').trim()).filter(Boolean) : Array.isArray(cfg.autonomous_completion_required_checks) ? cfg.autonomous_completion_required_checks.map((x) => String(x || '').trim()).filter(Boolean) : [] return { ...cfg, autonomous_mode_enabled: true, autonomous_active: true, autonomous_stop_requested: false, autonomous_started_at_ms: Date.now(), autonomous_goal: goal, autonomous_status: 'running', autonomous_last_error: '', autonomous_max_runtime_ms: maxRuntimeMs, autonomous_completion_required_checks: requiredChecks } } /** * @param {Record} cfg * @param {string} [reason] */ function bareAgentStopAutonomousRun(cfg, reason) { return { ...cfg, autonomous_stop_requested: true, autonomous_status: 'stopping', autonomous_last_error: reason || String(cfg.autonomous_last_error || '') } } /** * @param {Record} cfg * @param {{ remainingMs?: number, lastError?: string }} [extra] */ function bareAgentAutonomousContinuationPrompt(cfg, extra) { const goal = String(cfg.autonomous_goal || '').trim() || '(goal unset)' const remain = extra && typeof extra.remainingMs === 'number' ? Math.max(0, Math.round(extra.remainingMs / 1000)) : 0 return ( 'AUTONOMOUS RUN still active. Do not stop with a plan — take the next concrete tool action.\n' + 'Goal: ' + goal + '\n' + (remain > 0 ? 'Time remaining: ' + String(remain) + 's.\n' : '') + (extra && extra.lastError ? 'Last gate error: ' + extra.lastError + '\n' : '') + 'Call task_complete(summary) only when the goal is actually finished and verified.' ) } /** * @param {Record} cfg * @param {{ completed?: boolean, hasTools?: boolean, stopRequested?: boolean }} state */ function bareAgentAutonomousShouldContinue(cfg, state) { if (!cfg || !cfg.autonomous_active) return false if (state && state.stopRequested) return false if (state && state.completed) return false if (state && state.hasTools) return false return true } 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 */ } } /** * QVAC helpers for /bin/agent (no SDK import — host bridge via ctx.bareOsQvac*). */ /** @typedef {'lite'|'recommended'|'strong'|'tool-tiny'} BareAgentQvacProfileId */ /** * Native context windows from upstream model cards (not conservative laptop defaults). * Qwen3 dense (0.6B/1.7B/4B): https://huggingface.co/Qwen/Qwen3-0.6B — 32,768 * Llama 3.2 1B (tool-calling finetune base): Meta Llama 3.2 — 128,000 * Absolute ceiling for overrides / host clamp. */ const BARE_AGENT_QVAC_CTX_QWEN3 = 32768 const BARE_AGENT_QVAC_CTX_LLAMA32_1B = 131072 const BARE_AGENT_QVAC_CTX_QWEN35 = 262144 const BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX = 262144 /** * @typedef {{ * id: BareAgentQvacProfileId, * label: string, * description: string, * chatModel: string, * tools: boolean, * minRamGb: number, * minDiskGb: number, * approxDownloadGb: number, * ctxSize: number * }} BareAgentQvacProfile */ /** @type {Record} */ const BARE_AGENT_QVAC_PROFILES = { lite: { id: 'lite', label: 'Lite', description: 'Smallest download (Qwen3-0.6B). Model-card context 32k; tools enabled.', chatModel: 'QWEN3_600M_INST_Q4', tools: true, minRamGb: 4, minDiskGb: 2, approxDownloadGb: 0.5, ctxSize: BARE_AGENT_QVAC_CTX_QWEN3 }, recommended: { id: 'recommended', label: 'Recommended', description: 'Best balance for Bare OS agent tool calling (Qwen3-1.7B, model-card context 32k).', chatModel: 'QWEN3_1_7B_INST_Q4', tools: true, minRamGb: 8, minDiskGb: 5, approxDownloadGb: 2.5, ctxSize: BARE_AGENT_QVAC_CTX_QWEN3 }, strong: { id: 'strong', label: 'Strong', description: 'Better reasoning (Qwen3-4B). Model-card context 32k (131k with YaRN not enabled).', chatModel: 'QWEN3_4B_INST_Q4_K_M', tools: true, minRamGb: 16, minDiskGb: 8, approxDownloadGb: 3.5, ctxSize: BARE_AGENT_QVAC_CTX_QWEN3 }, 'tool-tiny': { id: 'tool-tiny', label: 'Tool-tiny', description: 'Llama 3.2 1B tool-calling fallback (model-card context 128k).', chatModel: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', tools: true, minRamGb: 6, minDiskGb: 3, approxDownloadGb: 1, ctxSize: BARE_AGENT_QVAC_CTX_LLAMA32_1B } } /** * Model-card / train context for a QVAC registry id (fallback 32k). * @param {string} [modelId] * @returns {number} */ function bareAgentQvacModelCardCtxSize(modelId) { const raw = String(modelId || '').trim() if ( typeof bareAgentQvacFindChatModel === 'function' && raw ) { const card = bareAgentQvacFindChatModel(raw) const n = card && Number(card.ctxSize) if (Number.isFinite(n) && n >= 2048) return Math.floor(n) } const id = raw.toUpperCase() if (!id) return BARE_AGENT_QVAC_CTX_QWEN3 if (/QWEN3[._-]?[56](?![A-Z0-9])/.test(id)) { return BARE_AGENT_QVAC_CTX_QWEN35 } if (/GEMMA[-_]?4/.test(id) && /31B/.test(id)) return 262144 if (/GEMMA[-_]?4/.test(id)) return 131072 if (/GPT[_-]?OSS/.test(id)) return 131072 if (/SMOLLM/.test(id)) return 8192 if (/QWEN3VL/.test(id)) return 131072 if (/LLAMA/.test(id)) return BARE_AGENT_QVAC_CTX_LLAMA32_1B if (/QWEN3/.test(id)) return BARE_AGENT_QVAC_CTX_QWEN3 return BARE_AGENT_QVAC_CTX_QWEN3 } /** * Align profile + auto ctx when the selected QVAC model changes. * qvac_ctx_size 0 means "use the model card". Stale smaller leftovers * (32k after switching to Qwen3.5) are cleared so the next load uses * the new window. * @param {Record} config * @param {string} [modelId] * @returns {Record} */ function bareAgentQvacSyncSettingsForModel(config, modelId) { const out = config && typeof config === 'object' ? config : {} const id = String( modelId || out.qvac_model || out.model || '' ).trim() if (!id) return out const card = typeof bareAgentQvacFindChatModel === 'function' ? bareAgentQvacFindChatModel(id) : null if (card && card.profile) out.qvac_profile = card.profile const cardCtx = bareAgentQvacModelCardCtxSize(id) const raw = Number(out.qvac_ctx_size) if (!Number.isFinite(raw) || raw <= 0 || raw < cardCtx) { out.qvac_ctx_size = 0 } return out } /** @returns {BareAgentQvacProfile[]} */ function bareAgentQvacProfileList() { return Object.values(BARE_AGENT_QVAC_PROFILES) } /** * @param {string} [id] * @returns {BareAgentQvacProfile} */ function bareAgentQvacGetProfile(id) { const key = String(id || '').trim().toLowerCase() return BARE_AGENT_QVAC_PROFILES[key] || BARE_AGENT_QVAC_PROFILES.recommended } const BARE_AGENT_QVAC_PROP_TYPES = Object.freeze([ 'string', 'number', 'integer', 'boolean', 'object', 'array' ]) /** * QVAC toolSchema only allows { type, description?, enum? } per property. * Nested items/properties/defaults make the whole tools array fail validation * and the SDK then runs the completion with tools omitted. * @param {unknown} params * @returns {{ type: 'object', properties: Record, required?: string[] }} */ function bareAgentSanitizeQvacToolParameters(params) { const src = params && typeof params === 'object' ? /** @type {Record} */ (params) : {} const rawProps = src.properties && typeof src.properties === 'object' && !Array.isArray(src.properties) ? /** @type {Record} */ (src.properties) : {} /** @type {Record} */ const properties = {} for (const key of Object.keys(rawProps)) { const v = rawProps[key] const prop = v && typeof v === 'object' && !Array.isArray(v) ? /** @type {Record} */ (v) : {} let type = typeof prop.type === 'string' ? prop.type : 'string' if (BARE_AGENT_QVAC_PROP_TYPES.indexOf(type) === -1) type = 'string' /** @type {Record} */ const next = { type: type } if (typeof prop.description === 'string' && prop.description) { next.description = prop.description } if (Array.isArray(prop.enum)) { const en = prop.enum.filter(function (x) { return ( typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean' || x === null ) }) if (en.length) next.enum = en } properties[key] = next } /** @type {{ type: 'object', properties: Record, required?: string[] }} */ const out = { type: 'object', properties: properties } if (Array.isArray(src.required)) { const req = src.required .map(function (x) { return String(x || '') }) .filter(function (k) { return k && Object.prototype.hasOwnProperty.call(properties, k) }) if (req.length) out.required = req } return out } /** * Flatten OpenAI nested tool defs → QVAC flat shape, then strip property * fields the QVAC SDK schema rejects (items, nested properties, default, …). * @param {unknown[]} tools * @returns {unknown[]} */ function bareAgentFlattenToolsForQvac(tools) { if (!Array.isArray(tools)) return [] /** @type {unknown[]} */ const out = [] for (const t of tools) { if (!t || typeof t !== 'object') continue const o = /** @type {Record} */ (t) if (o.function && typeof o.function === 'object') { const fn = /** @type {Record} */ (o.function) const name = typeof fn.name === 'string' ? fn.name : '' if (!name) continue out.push({ type: 'function', name: name, description: typeof fn.description === 'string' ? fn.description : '', parameters: bareAgentSanitizeQvacToolParameters(fn.parameters) }) continue } if (typeof o.name === 'string' && o.name) { out.push({ type: 'function', name: o.name, description: typeof o.description === 'string' ? o.description : '', parameters: bareAgentSanitizeQvacToolParameters(o.parameters) }) } } return out } /** * QVAC completion history is `{ role, content: string }` only. OpenAI-style * `content: null` + `tool_calls` fails request validation and the turn * never reaches the model (so the harness looks like it "stopped using tools"). * @param {unknown[]} history * @returns {{ role: string, content: string }[]} */ function bareAgentSanitizeHistoryForQvac(history) { if (!Array.isArray(history)) return [] /** @type {{ role: string, content: string }[]} */ const out = [] for (const raw of history) { if (!raw || typeof raw !== 'object') continue const m = /** @type {Record} */ (raw) const role = typeof m.role === 'string' && m.role ? m.role : 'user' let content = '' if (typeof m.content === 'string') content = m.content else if (Array.isArray(m.content)) { const parts = [] for (const part of m.content) { if (typeof part === 'string') parts.push(part) else if (part && typeof part === 'object') { const o = /** @type {Record} */ (part) if (typeof o.text === 'string') parts.push(o.text) else if (typeof o.content === 'string') parts.push(o.content) } } content = parts.join('') } else if (m.content != null && typeof m.content !== 'object') { content = String(m.content) } if (Array.isArray(m.tool_calls) && m.tool_calls.length) { const chunks = [] for (const tc of m.tool_calls) { if (!tc || typeof tc !== 'object') continue const o = /** @type {Record} */ (tc) const fn = o.function && typeof o.function === 'object' ? /** @type {Record} */ (o.function) : o const name = typeof fn.name === 'string' ? fn.name : '' if (!name) continue let args = fn.arguments if (args != null && typeof args !== 'string') { try { args = JSON.stringify(args) } catch { args = '{}' } } if (typeof args !== 'string' || !args) args = '{}' let argsObj = args try { const parsed = JSON.parse(args) argsObj = JSON.stringify(parsed && typeof parsed === 'object' ? parsed : {}) } catch { argsObj = '{}' } chunks.push( '{"name":' + JSON.stringify(name) + ',"arguments":' + argsObj + '}' ) } if (chunks.length) { const serialized = chunks.join('\n') content = content ? content + '\n' + serialized : serialized } } out.push({ role: role, content: content }) } return out } /** * Match QVAC `detectToolDialectFromName` so we do not override auto-detect * with the wrong parser (e.g. LFM/DeepSeek). * @param {string} [modelId] * @returns {'hermes'|'qwen35'|'gemma4'|'harmony'|'pythonic'|'dsml'} */ function bareAgentQvacDetectToolDialect(modelId) { const tag = String(modelId || '').toLowerCase() if (/qwen3[._-]?[56](?![a-z0-9])/.test(tag)) return 'qwen35' if (/gemma[-_]?4(?=[^a-z0-9]|$)/.test(tag)) return 'gemma4' if (/gpt[_-]?oss/.test(tag)) return 'harmony' if (/deepseek[-_. ]?v(?:4|3[._-]?2)(?![0-9])/.test(tag)) return 'dsml' if (/lfm[_-]?\d/.test(tag)) return 'pythonic' return 'hermes' } /** * @param {Record} ctx * @returns {boolean} */ function bareAgentQvacBridgeAvailable(ctx) { if (!ctx || typeof ctx !== 'object') return false if (typeof ctx.bareOsQvacAvailable === 'function') { try { return Boolean(ctx.bareOsQvacAvailable()) } catch { return false } } return typeof ctx.bareOsQvacComplete === 'function' } /** * Normalize backend from config (qvac | rest). * @param {Record} config * @returns {'qvac'|'rest'} */ function bareAgentResolveBackend(config) { const b = String(config?.backend || '').trim().toLowerCase() if (b === 'rest' || b === 'openai' || b === 'http') return 'rest' if (b === 'qvac') return 'qvac' const p = String(config?.provider || '').trim().toLowerCase() if (p === 'qvac') return 'qvac' if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') return 'rest' // Default: qvac when key unset; rest when key present (legacy configs) if (config?.rest_api_key && String(config.rest_api_key).trim()) return 'rest' return 'qvac' } /** @type {readonly string[]} */ var BARE_AGENT_QVAC_ONLY_KEYS = Object.freeze([ 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers' ]) /** @type {readonly string[]} */ var BARE_AGENT_REST_ONLY_KEYS = Object.freeze(['rest_base_url', 'rest_api_key']) /** * @typedef {{ * id: string, * label: string, * rest_base_url: string, * default_model: string, * models: string[] * }} BareAgentRestProvider */ /** @type {Record} */ const BARE_AGENT_REST_PROVIDERS = { groq: { id: 'groq', label: 'Groq', rest_base_url: 'https://api.groq.com/openai/v1', default_model: 'llama-3.3-70b-versatile', models: [ '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' ] }, xai: { id: 'xai', label: 'xAI (Grok)', rest_base_url: 'https://api.x.ai/v1', default_model: 'grok-4', models: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212'] }, openai: { id: 'openai', label: 'OpenAI', rest_base_url: 'https://api.openai.com/v1', default_model: 'gpt-4.1', models: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'o4-mini'] }, custom: { id: 'custom', label: 'Custom OpenAI-compatible', rest_base_url: '', default_model: '', models: [] } } /** @returns {BareAgentRestProvider[]} */ function bareAgentRestProviderList() { return [BARE_AGENT_REST_PROVIDERS.groq, BARE_AGENT_REST_PROVIDERS.xai, BARE_AGENT_REST_PROVIDERS.openai, BARE_AGENT_REST_PROVIDERS.custom] } /** * @param {string} [id] * @returns {BareAgentRestProvider} */ function bareAgentRestGetProvider(id) { const key = String(id || '').trim().toLowerCase() if (key === 'http' || key === 'rest') return BARE_AGENT_REST_PROVIDERS.custom return BARE_AGENT_REST_PROVIDERS[key] || BARE_AGENT_REST_PROVIDERS.groq } /** * @param {string} [model] */ function bareAgentIsQvacModelId(model) { const m = String(model || '').trim() if (!m) return false return ( /^QWEN/i.test(m) || /^LLAMA/i.test(m) || /^GEMMA/i.test(m) || /^GPT[_-]?OSS/i.test(m) || /^SMOLLM/i.test(m) || /QWEN3/i.test(m) || /LLAMA_TOOL_CALLING/i.test(m) ) } /** * Drop keys that belong to the other backend so config.json matches the choice. * @param {Record} config * @returns {Record} */ function bareAgentSanitizeConfigForBackend(config) { const out = { ...(config && typeof config === 'object' ? config : {}) } const backend = bareAgentResolveBackend(out) out.backend = backend if (backend === 'rest') { for (let i = 0; i < BARE_AGENT_QVAC_ONLY_KEYS.length; i++) { delete out[BARE_AGENT_QVAC_ONLY_KEYS[i]] } const prov = String(out.provider || '') .trim() .toLowerCase() if (!prov || prov === 'qvac') out.provider = 'groq' if (bareAgentIsQvacModelId(String(out.model || ''))) { const spec = bareAgentRestGetProvider(String(out.provider || 'groq')) if (spec.default_model) out.model = spec.default_model } } else { for (let i = 0; i < BARE_AGENT_REST_ONLY_KEYS.length; i++) { delete out[BARE_AGENT_REST_ONLY_KEYS[i]] } out.provider = 'qvac' } return out } /** * @param {string} [secret] */ function bareAgentMaskSecretPreview(secret) { const t = String(secret || '') if (!t.trim()) return '(not set)' if (t.length <= 8) return '********' return t.slice(0, 3) + '…' + t.slice(-4) } /** * Resolve context window for QVAC load from the model card / profile. * Legacy undersized `qvac_ctx_size` values (e.g. 4096/8192) are ignored so * setup stays automatic; only overrides ≥ the profile default apply. * Host may still cap via `BARE_OS_QVAC_MAX_CTX`. * @param {Record} config * @param {BareAgentQvacProfile} profile */ function bareAgentQvacResolveCtxSize(config, profile) { const modelId = String( (config && (config.qvac_model || config.model)) || (profile && profile.chatModel) || '' ) const cardCtx = Math.max(2048, bareAgentQvacModelCardCtxSize(modelId)) let ctx = Math.min(BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX, cardCtx) const raw = Number(config && config.qvac_ctx_size) // 0 / missing / leftover-below-card → auto (model card). // Explicit values at or above the card allow YaRN-style extension. if (Number.isFinite(raw) && raw >= cardCtx) { ctx = Math.min(BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX, Math.floor(raw)) } // Full Bare OS tool list ≈ 4.5k tokens; leave room for system + reply. if (ctx < 8192) ctx = 8192 return ctx } /** * Device / main-gpu / layers for QVAC load (config + profile defaults). * @param {Record} config * @returns {{ device?: string, mainGpu?: string | number, gpuLayers?: number }} */ function bareAgentQvacResolveDeviceOpts(config) { /** @type {{ device?: string, mainGpu?: string | number, gpuLayers?: number }} */ const out = {} const device = String(config && config.qvac_device ? config.qvac_device : '') .trim() .toLowerCase() if (device === 'cpu' || device === 'gpu') out.device = device const mainRaw = config && config.qvac_main_gpu if (mainRaw !== undefined && mainRaw !== null && String(mainRaw).trim() !== '') { const s = String(mainRaw).trim().toLowerCase() if (s === 'auto' || s === 'dedicated' || s === 'integrated') out.mainGpu = s else if (/^\d+$/.test(s)) out.mainGpu = Number.parseInt(s, 10) } else { out.mainGpu = 'auto' } const layers = Number(config && config.qvac_gpu_layers) if (Number.isFinite(layers) && layers >= 0) out.gpuLayers = Math.floor(layers) return out } /** * Shared QVAC chat catalog + REST /models helpers (preamble for agent and discord-bot). */ /** @type {{ id: string, family: string, label: string, tools: boolean, ramGb: number, ctxSize: number, profile?: string }[]} */ var BARE_AGENT_QVAC_CHAT_MODELS = [ { id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite', ctxSize: 32768 }, { id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended', ctxSize: 32768 }, { id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong', ctxSize: 32768 }, { id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16, ctxSize: 32768 }, { id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24, ctxSize: 32768 }, { id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny', ctxSize: 131072 }, { id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6, ctxSize: 131072 }, { id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3, ctxSize: 8192 }, { id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24, ctxSize: 131072 }, { id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8, ctxSize: 131072 }, { id: 'GEMMA4_2B_MULTIMODAL_Q6_K', family: 'gemma', label: 'Gemma 4 2B multimodal Q6', tools: true, ramGb: 10, ctxSize: 131072 }, { id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16, ctxSize: 131072 }, { id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48, ctxSize: 262144 }, { id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10, ctxSize: 131072 }, { id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10, ctxSize: 262144 }, { id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16, ctxSize: 262144 }, { id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 0.8B multimodal Q4', tools: true, ramGb: 8, ctxSize: 262144 }, { id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28, ctxSize: 262144 }, { id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48, ctxSize: 262144 } ] /** @type {Record} */ var BARE_AGENT_REST_MODEL_FALLBACKS = { groq: [ 'llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'openai/gpt-oss-120b', 'openai/gpt-oss-20b', 'qwen/qwen3-32b', 'moonshotai/kimi-k2-instruct', 'meta-llama/llama-4-scout-17b-16e-instruct', 'meta-llama/llama-4-maverick-17b-128e-instruct', 'groq/compound' ], xai: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212', 'grok-2-vision-1212'], openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3'], custom: [] } /** * @param {string} [profileId] */ function bareAgentQvacModelForProfile(profileId) { const id = String(profileId || '').trim().toLowerCase() for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) { if (BARE_AGENT_QVAC_CHAT_MODELS[i].profile === id) return BARE_AGENT_QVAC_CHAT_MODELS[i] } return BARE_AGENT_QVAC_CHAT_MODELS[1] || BARE_AGENT_QVAC_CHAT_MODELS[0] } /** * @param {string} [modelId] */ function bareAgentQvacFindChatModel(modelId) { const id = String(modelId || '').trim() for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) { if (BARE_AGENT_QVAC_CHAT_MODELS[i].id === id) return BARE_AGENT_QVAC_CHAT_MODELS[i] } return null } /** * @param {{ id: string, label?: string, family?: string }[]} models * @param {{ family?: string, query?: string }} [opts] */ function bareAgentFilterModelList(models, opts) { const o = opts && typeof opts === 'object' ? opts : {} const fam = String(o.family || 'all').trim().toLowerCase() const q = String(o.query || '').trim().toLowerCase() const rows = Array.isArray(models) ? models : [] /** @type {{ id: string, label?: string, family?: string }[]} */ const out = [] for (let i = 0; i < rows.length; i++) { const m = rows[i] if (!m || !m.id) continue if (fam && fam !== 'all' && String(m.family || '').toLowerCase() !== fam) continue if (q) { const blob = (String(m.id) + ' ' + String(m.label || '')).toLowerCase() if (blob.indexOf(q) === -1) continue } out.push(m) } return out } /** * @param {unknown} json * @returns {{ id: string, label: string, family: string, owned_by: string }[]} */ function bareAgentParseOpenAiModels(json) { const raw = json && typeof json === 'object' && Array.isArray(/** @type {{ data?: unknown }} */ (json).data) ? /** @type {{ data: unknown[] }} */ (json).data : Array.isArray(json) ? json : [] const skip = /embed|whisper|tts|dall-e|davinci|babbage|audio|moderation|realtime|image|sora/i /** @type {{ id: string, label: string, family: string, owned_by: string }[]} */ const out = [] const seen = Object.create(null) for (let i = 0; i < raw.length; i++) { const row = raw[i] && typeof raw[i] === 'object' ? /** @type {Record} */ (raw[i]) : null if (!row) continue const id = String(row.id || row.name || '').trim() if (!id || seen[id] || skip.test(id)) continue seen[id] = 1 const owned = String(row.owned_by || row.ownedBy || '') out.push({ id: id, label: id, family: owned || 'api', owned_by: owned }) } out.sort(function (a, b) { return a.id.localeCompare(b.id) }) return out } /** * Keep an explicit QVAC model id (Discord / `--model` / picker). * Profile defaults only apply when no model is set. * @param {Record} config * @param {{ chatModel?: string }} [profile] * @returns {Record} */ function bareAgentHonorExplicitQvacModel(config, profile) { const out = config && typeof config === 'object' ? config : {} const explicit = String(out.qvac_model || out.model || '').trim() if (explicit) { out.qvac_model = explicit out.model = explicit if (typeof bareAgentQvacSyncSettingsForModel === 'function') { bareAgentQvacSyncSettingsForModel(out, explicit) } else { const card = typeof bareAgentQvacFindChatModel === 'function' ? bareAgentQvacFindChatModel(explicit) : null if (card && card.profile) out.qvac_profile = card.profile } return out } const fallback = String((profile && profile.chatModel) || '').trim() if (fallback) { out.qvac_model = fallback out.model = fallback } return out } /** * Persist a live model switch for the current backend. * @param {Record} config * @param {string} modelId * @returns {Record} */ function bareAgentApplyLiveModel(config, modelId) { const out = { ...(config && typeof config === 'object' ? config : {}) } const id = String(modelId || '').trim() if (!id) return out let backend = 'qvac' if (typeof bareAgentResolveBackend === 'function') { backend = bareAgentResolveBackend(out) } else { const b = String(out.backend || '').trim().toLowerCase() const p = String(out.provider || '').trim().toLowerCase() if (b === 'rest' || b === 'openai' || b === 'http') backend = 'rest' else if (b === 'qvac') backend = 'qvac' else if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') backend = 'rest' else if (out.rest_api_key && String(out.rest_api_key).trim()) backend = 'rest' } if (backend === 'qvac') { out.backend = 'qvac' out.provider = 'qvac' out.qvac_model = id out.model = id out.qvac_ctx_size = 0 return bareAgentHonorExplicitQvacModel(out, null) } out.backend = 'rest' out.model = id return out } /** * @param {string} [provider] */ function bareAgentRestModelsFallback(provider) { const key = String(provider || 'groq').trim().toLowerCase() const ids = BARE_AGENT_REST_MODEL_FALLBACKS[key] || BARE_AGENT_REST_MODEL_FALLBACKS.groq return (ids || []).map(function (id) { return { id: id, label: id, family: key, owned_by: key } }) } /** * @param {(url: string, init?: object) => Promise<{ ok?: boolean, status?: number, json?: () => Promise, text?: () => Promise }>} fetchFn * @param {{ baseUrl?: string, apiKey?: string }} opts */ async function bareAgentFetchRestModels(fetchFn, opts) { const o = opts && typeof opts === 'object' ? opts : {} const base = String(o.baseUrl || '').trim().replace(/\/+$/, '') if (!base) throw new Error('rest_base_url required') if (typeof fetchFn !== 'function') throw new Error('fetch unavailable') const url = base + '/models' /** @type {Record} */ const headers = { Accept: 'application/json' } const key = String(o.apiKey || '').trim() if (key) headers.Authorization = 'Bearer ' + key const res = await fetchFn(url, { method: 'GET', headers: headers }) if (!res || res.ok === false) { const st = res && res.status != null ? String(res.status) : 'fetch_failed' throw new Error('models_http_' + st) } let json if (typeof res.json === 'function') json = await res.json() else if (typeof res.text === 'function') json = JSON.parse(await res.text()) else throw new Error('models_unreadable') const list = bareAgentParseOpenAiModels(json) if (!list.length) throw new Error('models_empty') return list } /** * Agent Markdown workspace at ~/.agent/workspace — soul files + optional daily memory. * Defaults ship on the system image at /share/agent-workspace/ and are seeded * into the personal drive when SOUL.md is missing (portable across peers via Hyperdrive). */ /** @type {readonly string[]} Canonical Markdown load order */ var BARE_AGENT_WORKSPACE_FILES = Object.freeze([ 'SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'MEMORY.md', 'BOOTSTRAP.md', 'HEARTBEAT.md', 'PROMPT.md' ]) /** System-drive templates (kernel share) */ var BARE_AGENT_WORKSPACE_SHARE = '/share/agent-workspace' /** Relative paths under workspace/ and share root for skill templates */ var BARE_AGENT_SKILL_SEED_REL = Object.freeze([ 'skills/.gitkeep', 'skills/p2p-os-status/SKILL.md', 'skills/bare-os-kernel-proc/SKILL.md', 'skills/bare-os-super-developer/SKILL.md', 'skills/agent-ops/SKILL.md', 'skills/xai-compat/SKILL.md', 'skills/holesail/SKILL.md', 'skills/hdms/SKILL.md', 'skills/bareos-code-change/SKILL.md', 'skills/hyperdrive-replication/SKILL.md', 'skills/protomux-channel/SKILL.md', 'skills/ctx-api-change/SKILL.md', 'skills/proc-node-change/SKILL.md', 'skills/seed-rpc-change/SKILL.md', 'skills/coreutils-command-change/SKILL.md', 'skills/shell-grammar-change/SKILL.md', 'skills/docs-contract-update/SKILL.md', 'skills/kernel-program-extension/SKILL.md', 'skills/appstore/SKILL.md', 'skills/pear-dev/SKILL.md', 'skills/pear-runtime-debug/SKILL.md', 'skills/holepunch-local-mirror/SKILL.md' ]) /** * @param {Record} ctx * @param {Uint8Array} buf */ function bareAgentWorkspaceDecode(ctx, buf) { if (!buf || !buf.length) return '' if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ) return ctx.b4a.toString(buf) return String(new TextDecoder().decode(buf)) } /** * @returns {string} UTC YYYY-MM-DD */ function bareAgentWorkspaceUtcYmd() { const d = new Date() const y = d.getUTCFullYear() const m = d.getUTCMonth() + 1 const day = d.getUTCDate() const pad = (n) => (n < 10 ? '0' : '') + n return y + '-' + pad(m) + '-' + pad(day) } /** * Seed ~/.agent/workspace from /share/agent-workspace when SOUL.md is absent. * @param {Record} ctx * @param {{ dir: string, workspace: string, workspaceMemory: string, workspaceSkills: string }} paths */ async function bareAgentEnsureWorkspace(ctx, paths) { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.readFile !== 'function') return if (typeof vfs.writeFile !== 'function') return try { const b = await vfs.readFile(paths.workspace + '/SOUL.md') if (b && b.length) return } catch { /* missing — seed */ } try { await vfs.mkdir(paths.workspace, { recursive: true }) await vfs.mkdir(paths.workspaceMemory, { recursive: true }) } catch { return } const share = BARE_AGENT_WORKSPACE_SHARE for (const name of BARE_AGENT_WORKSPACE_FILES) { try { const buf = await vfs.readFile(share + '/' + name) await vfs.writeFile(paths.workspace + '/' + name, buf) } catch { /* template missing on image — skip */ } } try { const gk = await vfs.readFile(share + '/memory/.gitkeep') await vfs.writeFile(paths.workspaceMemory + '/.gitkeep', gk) } catch { /* optional */ } /** @type {[string, string][]} */ const stubs = [ ['loader.stub.js', paths.dir + '/loader.js'], ['index.stub.js', paths.dir + '/index.js'], ['skill-loader.stub.js', paths.dir + '/skill-loader.js'], ['README-agent.md', paths.dir + '/README-agent.md'] ] for (const [srcName, dest] of stubs) { try { const buf = await vfs.readFile(share + '/' + srcName) await vfs.writeFile(dest, buf) } catch { /* optional */ } } } /** * Ensure workspace/skills templates and ~/.agent/skill-loader.js exist (idempotent; for upgrades). * @param {Record} ctx * @param {{ workspace: string, workspaceSkills: string, dir: string }} paths * @param {Record} [config] */ async function bareAgentEnsureSkillTemplates(ctx, paths, config) { const vfs = ctx.vfs if ( !vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function' ) return const provider = config && typeof config === 'object' ? String(config.provider || '').trim().toLowerCase() : '' try { await vfs.mkdir(paths.workspaceSkills, { recursive: true }) } catch { return } const share = BARE_AGENT_WORKSPACE_SHARE for (const rel of BARE_AGENT_SKILL_SEED_REL) { if (rel === 'skills/xai-compat/SKILL.md' && provider !== 'xai') { if (typeof vfs.unlink === 'function') { try { await vfs.unlink(paths.workspace + '/' + rel) } catch { /* ignore */ } } continue } const dest = paths.workspace + '/' + rel try { const b = await vfs.readFile(dest) if (b && b.length) continue } catch { /* missing — copy */ } try { const buf = await vfs.readFile(share + '/' + rel) const parent = dest.replace(/\/[^/]+$/, '') await vfs.mkdir(parent, { recursive: true }) await vfs.writeFile(dest, buf) } catch { /* template missing on image */ } } try { await vfs.readFile(paths.dir + '/skill-loader.js') } catch { try { const buf = await vfs.readFile(share + '/skill-loader.stub.js') await vfs.writeFile(paths.dir + '/skill-loader.js', buf) } catch { /* optional */ } } } /** * @param {Record} ctx * @param {string} text */ function bareAgentWorkspaceEncode(ctx, text) { const s = String(text) if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ) return ctx.b4a.from(s) return new TextEncoder().encode(s) } /** * @param {Record} config */ function bareAgentIdentityMarkdownForConfig(config) { const label = String(config.agent_label || '').trim() || 'BareAgent' const owner = String(config.owner_name || '').trim() const role = owner ? 'Decentralized OS intelligence for **' + owner + '** · upstream [bare-operating-system](https://git.ssh.surf/snxraven/bare-operating-system).' : 'Decentralized OS Intelligence for snxraven\'s bare-operating-system' return ( '# IDENTITY.md\n\n' + '**Name:** ' + label + '\n' + '**Role:** ' + role + '\n' + '**Emoji:** 🦾\n' + '**Version:** 0.1\n' ) } /** * @param {Record} config */ function bareAgentUserMarkdownForConfig(config) { const owner = String(config.owner_name || '').trim() const opLine = owner ? '- **Operator (this Hyperdrive):** ' + owner + '\n' : '- **Operator:** (set `owner_name` via `agent --config` or `edit_agent_config`)\n' return ( '# USER.md - About the Owner\n\n' + opLine + '- **Upstream maintainer (repo):** snxraven\n' + '- **Location:** Atlanta, Georgia, US\n' + '- **Expertise:** P2P systems, Bare runtime, Hyperdrive, decentralized identity, POSIX-in-JS\n' + '- **Preferences:** Concise technical answers, bullet points, no corporate speak, direct honesty\n' + '- **Permissions:** Full access to system drive and personal Hyperdrive within tool policy\n' ) } /** * Rewrite IDENTITY.md / USER.md from ~/.agent/config.json (owner_name, agent_label). * Call after seeding workspace or when those keys change. * @param {Record} ctx * @param {{ workspace: string }} paths * @param {Record} config */ async function bareAgentSyncWorkspaceFromConfig(ctx, paths, config) { const vfs = ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') return const label = String(config.agent_label || '').trim() const owner = String(config.owner_name || '').trim() if (!label && !owner) return try { await vfs.mkdir(paths.workspace, { recursive: true }) } catch { return } try { const idMd = bareAgentIdentityMarkdownForConfig(config) await vfs.writeFile( paths.workspace + '/IDENTITY.md', bareAgentWorkspaceEncode(ctx, idMd) ) const userMd = bareAgentUserMarkdownForConfig(config) await vfs.writeFile(paths.workspace + '/USER.md', bareAgentWorkspaceEncode(ctx, userMd)) } catch { /* ignore — best-effort */ } } /** * Build concatenated system prompt block (Markdown) from workspace files. * @param {Record} ctx * @param {{ workspace: string, workspaceMemory: string }} paths * @param {number} [maxChars] */ async function bareAgentLoadWorkspacePrompt(ctx, paths, maxChars) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' const cap = Math.min(Math.max(Number(maxChars) || 24000, 4000), 64000) let out = '# Agent workspace (~/.agent/workspace)\n\n' for (const name of BARE_AGENT_WORKSPACE_FILES) { try { const buf = await vfs.readFile(paths.workspace + '/' + name) const t = bareAgentWorkspaceDecode(ctx, buf).trim() out += '=== ' + name + ' ===\n' + (t || '(empty)') + '\n\n' } catch { out += '=== ' + name + ' ===\n(File not found)\n\n' } } const day = bareAgentWorkspaceUtcYmd() try { const buf = await vfs.readFile(paths.workspaceMemory + '/' + day + '.md') const t = bareAgentWorkspaceDecode(ctx, buf).trim() if (t) out += '=== memory/' + day + '.md ===\n' + t + '\n\n' } catch { /* no daily log */ } if (out.length > cap) out = out.slice(0, cap) + '\n… truncated\n' return out } /** * Agent skills: discover SKILL.md under workspace/skills/ (highest precedence) then ~/.agent/skills/. * Depends on bareAgentWorkspaceDecode from agent-workspace.js (same preamble order). */ /** * @param {string} text * @returns {{ front: Record, body: string }} */ function bareAgentParseSkillFrontmatter(text) { const t = String(text || '') if (!t.startsWith('---')) return { front: {}, body: t.trim() } const nl = t.indexOf('\n') const afterFirst = nl === -1 ? '' : t.slice(nl + 1) const end = afterFirst.search(/\n---\s*(?:\n|$)/) if (end === -1) return { front: {}, body: t.trim() } const yamlBlock = afterFirst.slice(0, end) const body = afterFirst.slice(end + 1).replace(/^---\s*/, '').replace(/^\r?\n/, '') /** @type {Record} */ const front = {} for (const line of yamlBlock.split(/\r?\n/)) { const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/) if (m) front[m[1]] = m[2].trim() } return { front, body: body.trim() } } /** * @param {string} full SKILL.md text * @param {string} folderName directory basename */ function bareAgentSkillMetaFromMarkdown(full, folderName) { const { front } = bareAgentParseSkillFrontmatter(full) const name = (front.name || folderName || 'unnamed').trim() || folderName const descFromFront = front.description && String(front.description).trim() ? String(front.description).trim() : '' const descFromBody = full .split(/\r?\n/) .find((l) => { const x = l.trim() return x && !x.startsWith('---') && !x.startsWith('#') }) ?.trim() || '' const description = (descFromFront || descFromBody || 'Skill').slice(0, 400) return { name, description } } /** * @param {Record} ctx * @param {{ workspaceSkills: string, skillsGlobal: string }} paths * @returns {Promise<{ id: string, name: string, description: string, path: string, source: string }[]>} */ async function bareAgentDiscoverSkills(ctx, paths) { const vfs = ctx.vfs if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') return [] /** @type {{ id: string, name: string, description: string, path: string, source: string }[]} */ const out = [] const seen = new Set() /** * @param {string} root * @param {string} source */ async function scanRoot(root, source) { let names = [] try { names = await vfs.readdir(root) } catch { return } if (!Array.isArray(names)) return for (const raw of names) { const entry = String(raw) if (!entry || entry.startsWith('.')) continue const skillMd = root.replace(/\/+$/, '') + '/' + entry + '/SKILL.md' try { const buf = await vfs.readFile(skillMd) if (!buf || !buf.length) continue const full = bareAgentWorkspaceDecode(ctx, buf) const meta = bareAgentSkillMetaFromMarkdown(full, entry) const keys = [entry.toLowerCase(), meta.name.toLowerCase()] let dup = false for (const k of keys) { if (seen.has(k)) dup = true } if (dup) continue for (const k of keys) seen.add(k) out.push({ id: entry, name: meta.name, description: meta.description, path: skillMd, source }) } catch { /* not a skill dir */ } } } await scanRoot(paths.workspaceSkills, 'workspace') await scanRoot(paths.skillsGlobal, 'global') let extras = Array.isArray(paths.extraSkillRoots) ? paths.extraSkillRoots : [] if (!extras.length && typeof bareAgentDiscoverProjectSkillRoots === 'function') { const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const start = String(env.PWD || env.CWD || env.HOME || '').trim() if (start) extras = await bareAgentDiscoverProjectSkillRoots(ctx, start) } for (let i = 0; i < extras.length; i++) { const item = extras[i] const root = typeof item === 'string' ? item : String((item && item.path) || '') const source = typeof item === 'object' && item && item.source ? String(item.source) : 'project' if (root) await scanRoot(root, source) } return out } /** * Compact Markdown block for system prompt (names + short descriptions only). * @param {Record} ctx * @param {{ workspaceSkills: string, skillsGlobal: string }} paths * @param {number} [maxChars] */ async function bareAgentSkillsCompactPrompt(ctx, paths, maxChars) { const cap = Math.min(Math.max(Number(maxChars) || 4000, 500), 12000) const skills = await bareAgentDiscoverSkills(ctx, paths) let block = 'Skills (playbooks — NOT callable tools)\n' + 'These ids are SKILL.md playbooks, not functions. Do not list them when asked about your tools.\n' + 'Each skill is a directory with SKILL.md (optional YAML frontmatter: name, description).\n' + 'Workspace skills (~/.agent/workspace/skills/) override global (~/.agent/skills/) and walk-up .grok/skills / .agents/skills when names match.\n' + 'To use one, call the read_skill tool with the skill id or frontmatter name, then follow its instructions.\n\n' if (!skills.length) { block += '(No skills discovered yet — add folders under workspace/skills//SKILL.md.)\n' return block.length > cap ? block.slice(0, cap) + '\n…\n' : block } for (const s of skills) { const desc = s.description.replace(/\r?\n/g, ' ').slice(0, 160) const label = s.name && s.name !== s.id ? s.id + ' / ' + s.name : s.id block += '- ' + label + ' (' + s.source + ')' + (desc ? ' — ' + desc : '') + '\n' } if (block.length > cap) block = block.slice(0, cap) + '\n… truncated\n' return block } /** * Load full SKILL.md for a skill matched by folder id or frontmatter name (case-insensitive). * @param {Record} ctx * @param {{ workspaceSkills: string, skillsGlobal: string }} paths * @param {string} skillQuery */ async function bareAgentLoadSkillMarkdown(ctx, paths, skillQuery) { const q = String(skillQuery || '') .trim() .toLowerCase() if (!q) return { ok: false, error: 'empty_skill', content: '', path: '' } const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return { ok: false, error: 'vfs unavailable', content: '', path: '' } const skills = await bareAgentDiscoverSkills(ctx, paths) const hit = skills.find((s) => s.id.toLowerCase() === q) || skills.find((s) => s.name.toLowerCase() === q) if (!hit) return { ok: false, error: 'skill_not_found', content: '', path: '' } try { const buf = await vfs.readFile(hit.path) if (!buf || !buf.length) return { ok: false, error: 'empty_file', content: '', path: hit.path } const t = bareAgentWorkspaceDecode(ctx, buf) return { ok: true, skill: hit.name, id: hit.id, path: hit.path, source: hit.source, content: t } } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return { ok: false, error: msg, content: '', path: hit.path } } } /** 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 = '' let emittedShape = false 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) if (!emittedShape) { emittedShape = true onEvent({ type: 'response_shape_keys', keys: Object.keys(j).slice(0, 24) }) } if (typeof j.type === 'string') { if (j.type === 'response.reasoning_summary_text.delta' && typeof j.delta === 'string') { onEvent({ type: 'delta_reasoning', reasoning: j.delta }) } if (j.type === 'response.output_text.delta' && typeof j.delta === 'string') { onEvent({ type: 'delta_content', content: j.delta }) } if (j.type === 'response.output_item.added') { const item = j.item && typeof j.item === 'object' ? /** @type {Record} */ (j.item) : null if ( item && (item.type === 'function_call' || item.type === 'custom_tool_call') ) { const idx = typeof j.output_index === 'number' ? j.output_index : 0 const name = typeof item.name === 'string' ? item.name : '' const args = typeof item.arguments === 'string' ? item.arguments : '' const id = typeof item.call_id === 'string' ? item.call_id : typeof item.id === 'string' ? item.id : '' if (name || args) { onEvent({ type: 'delta_tool_calls', tool_calls: [ { index: idx, id, function: { name, arguments: args } } ] }) } } } if ( j.type === 'response.function_call_arguments.delta' && typeof j.delta === 'string' ) { const idx = typeof j.output_index === 'number' ? j.output_index : 0 const name = typeof j.name === 'string' ? j.name : '' const id = typeof j.item_id === 'string' ? j.item_id : '' onEvent({ type: 'delta_tool_calls', tool_calls: [ { index: idx, id, function: { name, arguments: j.delta } } ] }) } } 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 const message = ch0 && typeof ch0.message === 'object' ? /** @type {Record} */ (ch0.message) : null 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 }) const rc = delta.reasoning_content if (typeof rc === 'string' && rc.length) { onEvent({ type: 'delta_reasoning', reasoning: rc }) } const r = delta.reasoning if (typeof r === 'string' && r.length) { onEvent({ type: 'delta_reasoning', reasoning: r }) } else if (Array.isArray(r)) { for (const chunk of r) { if (!chunk || typeof chunk !== 'object') continue const ro = /** @type {Record} */ (chunk) const tx = typeof ro.text === 'string' ? ro.text : typeof ro.content === 'string' ? ro.content : '' if (tx) { onEvent({ type: 'delta_reasoning', reasoning: tx }) } } } } if (message) { const msgTools = message.tool_calls if (Array.isArray(msgTools) && msgTools.length) { onEvent({ type: 'delta_tool_calls', tool_calls: msgTools }) } const legacy = message.function_call if (legacy && typeof legacy === 'object') { onEvent({ type: 'delta_tool_calls', tool_calls: [{ index: 0, function: legacy }] }) } } 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 } /** HTTP fetch + HTML extract helpers for agent `web_fetch` tool (preamble for /bin/agent). */ /** * @param {unknown} e * @returns {string} */ function bareWebFmtErr(e) { if (e === undefined) return 'promise_rejected_with_undefined (no rejection reason)' if (e === null) return 'promise_rejected_with_null' if (typeof e === 'string') return e if (typeof e !== 'object') return String(e) const o = /** @type {Record} */ (e) const msg = o.message if (typeof msg === 'string' && msg.trim()) return bareWebFmtErrAugment(o, msg.trim()) if (typeof msg === 'number' || typeof msg === 'boolean') return bareWebFmtErrAugment(o, String(msg)) const nm = o.name const code = o.code const errno = o.errno /** @type {string[]} */ const bits = [] if (typeof nm === 'string' && nm.trim()) bits.push(nm) if (code !== undefined && code !== null && String(code) !== '') bits.push('code=' + String(code)) if (errno !== undefined && errno !== null && String(errno) !== '') bits.push('errno=' + String(errno)) const cause = o.cause if (cause !== undefined && cause !== null && cause !== e) { const cs = bareWebFmtErr(cause) if (cs && cs !== 'unknown_error') bits.push('cause=(' + cs.slice(0, 280) + ')') } const errs = o.errors if (Array.isArray(errs) && errs.length) { errs.slice(0, 5).forEach((sub, i) => { bits.push('agg' + i + '=' + bareWebFmtErr(sub).slice(0, 120)) }) } if (bits.length) return bits.join(' ') try { const j = JSON.stringify(o) if (j && j !== '{}' && j !== '[]') return j.slice(0, 400) } catch { /* ignore */ } try { if ( typeof /** @type {{ toString?: () => string }} */ (o).toString === 'function' ) { const t = /** @type {{ toString: () => string }} */ (o).toString() if (t && t !== '[object Object]') return t.slice(0, 400) } } catch { /* ignore */ } return 'unknown_error' } /** * Append errno/syscall from Node-ish errors when message alone is vague. * @param {Record} o * @param {string} base */ function bareWebFmtErrAugment(o, base) { const syscall = o.syscall const code = o.code const errno = o.errno /** @type {string[]} */ const tail = [] if (typeof syscall === 'string' && syscall.trim()) tail.push('syscall=' + syscall) if (code !== undefined && code !== null && String(code) !== '') tail.push(String(code)) if (errno !== undefined && errno !== null && String(errno) !== '') tail.push('errno=' + String(errno)) const cause = o.cause if (cause !== undefined && cause !== null) { const cs = bareWebFmtErr(cause) if (cs && cs !== 'unknown_error') tail.push('cause=(' + cs.slice(0, 240) + ')') } return tail.length ? base + ' [' + tail.join(', ') + ']' : base } /** * Tool args often omit numeric fields; `Number(undefined)` is NaN and `NaN ?? d` is still NaN. * @param {unknown} n * @param {number} def */ function bareWebFiniteOr(n, def) { const x = Number(n) return Number.isFinite(x) ? x : def } /** * @param {Record} ctx * @returns {typeof fetch | null} */ function bareWebResolveFetch(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 } /** * bundled bare-fetch rejects the fetch promise with `signal.reason` on abort. * `controller.abort()` with no argument sets `reason === undefined`, so callers * see `promise_rejected_with_undefined`. Always pass an explicit reason. * @param {number} timeoutMs */ function bareWebTimeoutAbortReason(timeoutMs) { const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)' try { if (typeof DOMException === 'function') return new DOMException(msg, 'TimeoutError') } catch { /* ignore */ } const e = new Error(msg) e.name = 'TimeoutError' return e } /** * @param {AbortSignal} sig */ function bareWebSignalAbortReason(sig) { try { const r = /** @type {{ reason?: unknown }} */ (sig).reason if (r !== undefined && r !== null) return r } catch { /* ignore */ } const e = new Error('web_fetch aborted (signal)') e.name = 'AbortError' return e } /** * @param {AbortSignal | null | undefined} a * @param {AbortSignal | null | undefined} b */ function bareWebUnionAbort(a, b) { if (!a) return b || undefined if (!b) return a if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b]) const c = new AbortController() /** * @param {AbortSignal} sig */ const forward = (sig) => { try { c.abort(bareWebSignalAbortReason(sig)) } catch { /* ignore — second source may fire after controller already aborted */ } } try { const as = /** @type {AbortSignal} */ (a) const bs = /** @type {AbortSignal} */ (b) if (as.aborted) forward(as) else as.addEventListener('abort', () => forward(as), { once: true }) if (bs.aborted) forward(bs) else bs.addEventListener('abort', () => forward(bs), { once: true }) } catch { /* ignore */ } return c.signal } /** * @param {Uint8Array[]} parts */ function bareWebConcatUint8(parts) { let n = 0 for (const p of parts) n += p.length const out = new Uint8Array(n) let o = 0 for (const p of parts) { out.set(p, o) o += p.length } return out } /** * @param {Response} res * @param {number} maxBytes * @param {AbortSignal | undefined} signal */ async function bareWebReadBodyLimited(res, maxBytes, signal) { if (!res.body || typeof res.body.getReader !== 'function') { try { const ab = await res.arrayBuffer() const u8 = new Uint8Array(ab) return { bytes: u8.byteLength > maxBytes ? u8.slice(0, maxBytes) : u8, truncated: u8.byteLength > maxBytes } } catch { return { bytes: new Uint8Array(0), truncated: false } } } const reader = res.body.getReader() /** @type {Uint8Array[]} */ const chunks = [] let total = 0 try { for (;;) { if (signal && signal.aborted) { try { await reader.cancel() } catch { /* ignore */ } break } const { done, value } = await reader.read() if (done) break if (!value || !value.length) continue total += value.length if (total > maxBytes) { const prev = total - value.length const take = Math.max(0, maxBytes - prev) if (take > 0) chunks.push(value.subarray(0, take)) try { await reader.cancel() } catch { /* ignore */ } return { bytes: bareWebConcatUint8(chunks), truncated: true } } chunks.push(value) } } finally { try { reader.releaseLock() } catch { /* ignore */ } } return { bytes: bareWebConcatUint8(chunks), truncated: false } } /** * @param {string | null | undefined} ct */ function bareWebCharsetFromContentType(ct) { const m = /charset\s*=\s*["']?([^"';\s]+)/i.exec(String(ct || '')) return (m ? m[1] : 'utf-8').trim().toLowerCase() } /** * @param {Uint8Array} bytes * @param {string} label */ function bareWebDecodeBytes(bytes, label) { try { const dec = new TextDecoder(label || 'utf-8', { fatal: false, ignoreBOM: true }) return dec.decode(bytes) } catch { return new TextDecoder('utf-8', { fatal: false }).decode(bytes) } } /** * @param {string} s */ function bareWebDecodeHtmlEntities(s) { let t = String(s || '') t = t.replace(/ /gi, ' ') t = t.replace(/"/gi, '"') t = t.replace(/'/g, "'") t = t.replace(/'/gi, "'") t = t.replace(/&/gi, '&') t = t.replace(/</gi, '<') t = t.replace(/>/gi, '>') t = t.replace(/&#x([0-9a-f]+);/gi, (_, h) => { const c = parseInt(h, 16) return Number.isFinite(c) ? String.fromCodePoint(c) : _ }) t = t.replace(/&#(\d+);/g, (_, d) => { const c = parseInt(d, 10) return Number.isFinite(c) ? String.fromCodePoint(c) : _ }) return t } /** * @param {string} html */ function bareWebExtractHtmlText(html) { let s = String(html || '') s = s.replace(/)<[^<]*)*<\/script>/gi, '') s = s.replace(/)<[^<]*)*<\/style>/gi, '') s = s.replace(/)<[^<]*)*<\/noscript>/gi, '') s = s.replace(//g, '') s = s.replace(/<[^>]+>/g, ' ') s = bareWebDecodeHtmlEntities(s) s = s.replace(/\s+/g, ' ').trim() return s } /** * @param {string} html * @param {string} baseUrl * @param {number} maxLinks */ function bareWebExtractLinks(html, baseUrl, maxLinks) { const cap = Math.min(Math.max(Number(maxLinks) || 200, 1), 500) let uBase = null try { uBase = baseUrl ? new URL(String(baseUrl)) : null } catch { uBase = null } const seen = new Set() /** @type {string[]} */ const out = [] const re = /]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi let m const h = String(html || '') while ((m = re.exec(h)) !== null) { const href = (m[1] || m[2] || m[3] || '').trim() if (!href || href.startsWith('javascript:') || href.startsWith('#')) continue try { const abs = uBase ? new URL(href, uBase).href : new URL(href).href const proto = new URL(abs).protocol if (proto !== 'http:' && proto !== 'https:') continue if (!seen.has(abs)) { seen.add(abs) out.push(abs) } } catch { /* skip */ } if (out.length >= cap) break } return { links: out, links_truncated: out.length >= cap } } /** * @param {string} tag */ function bareWebMetaContent(tag) { const q = /content\s*=\s*"([^"]*)"/i.exec(tag) || /content\s*=\s*'([^']*)'/i.exec(tag) || /content\s*=\s*([^\s>]+)/i.exec(tag) return q ? bareWebDecodeHtmlEntities(q[1]).trim() : '' } /** * @param {string} html */ function bareWebExtractMeta(html) { const h = String(html || '') const titleM = /]*>([\s\S]*?)<\/title>/i.exec(h) const title = titleM ? bareWebDecodeHtmlEntities(titleM[1].replace(/<[^>]+>/g, ' ')).trim() : '' let description = '' const metaDescRe = /]*\bname\s*=\s*["']description["'][^>]*>/i.exec(h) if (metaDescRe) description = bareWebMetaContent(metaDescRe[0]) let og_title = '' const ogT = /]*\bproperty\s*=\s*["']og:title["'][^>]*>/i.exec(h) if (ogT) og_title = bareWebMetaContent(ogT[0]) let og_description = '' const ogD = /]*\bproperty\s*=\s*["']og:description["'][^>]*>/i.exec(h) if (ogD) og_description = bareWebMetaContent(ogD[0]) return { title, description, og_title, og_description } } /** * @param {string} text */ function bareWebMaybeParseJson(text) { try { return { ok: true, value: JSON.parse(String(text)) } } catch { return { ok: false } } } /** * @param {string} ct */ function bareWebLooksLikeHtml(ct) { return /\btext\/html\b/i.test(String(ct || '')) } /** * @param {string} ct */ function bareWebLooksLikeJson(ct) { const s = String(ct || '').toLowerCase() return ( /\bapplication\/json\b/.test(s) || /\bapplication\/.*\+json\b/.test(s) || /\btext\/json\b/.test(s) ) } /** * @param {{ * ctx: Record, * url: string, * method?: string, * headers?: Record, * body?: string, * content_type?: string, * max_response_bytes?: number, * max_redirects?: number, * timeout_ms?: number, * format?: string, * max_links?: number, * signal?: AbortSignal | null * }} o */ async function bareWebRunTool(o) { const ctx = o.ctx const fetchFn = bareWebResolveFetch(ctx) if (!fetchFn) { return { ok: false, error: 'web_fetch: no HTTP client (set ctx.httpFetch, bare.fetch, or global fetch)' } } let startUrl = String(o.url || '').trim() let method = String(o.method || 'GET').toUpperCase() if ( !['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes( method ) ) { return { ok: false, error: 'web_fetch: unsupported method' } } let body = o.body != null && method !== 'GET' && method !== 'HEAD' ? String(o.body) : undefined let u0 try { u0 = new URL(startUrl) } catch { return { ok: false, error: 'web_fetch: invalid URL' } } if (u0.protocol !== 'http:' && u0.protocol !== 'https:') { return { ok: false, error: 'web_fetch: only http(s) URLs are allowed' } } const maxRedirects = Math.min( Math.max(bareWebFiniteOr(o.max_redirects, 5), 0), 20 ) const maxBytes = Math.min( Math.max(bareWebFiniteOr(o.max_response_bytes, 524288), 1024), 2 * 1024 * 1024 ) const timeoutMs = Math.min( Math.max(bareWebFiniteOr(o.timeout_ms, 30000), 500), 120000 ) const fmtRaw = String(o.format || 'auto').toLowerCase() const maxLinks = Number(o.max_links) || 200 /** @type {string[]} */ const redirectChain = [startUrl] let currentUrl = startUrl let redirectsUsed = 0 for (;;) { const controller = new AbortController() const timer = setTimeout(() => { try { controller.abort(bareWebTimeoutAbortReason(timeoutMs)) } catch { /* ignore */ } }, timeoutMs) const signal = bareWebUnionAbort(o.signal || undefined, controller.signal) /** @type {Record} */ const hdrObj = {} const hin = o.headers if (hin && typeof hin === 'object' && !Array.isArray(hin)) { for (const [k, v] of Object.entries(hin)) { if (typeof v === 'string' && k) hdrObj[k] = v } } if ( body != null && method !== 'GET' && method !== 'HEAD' && !Object.keys(hdrObj).some((k) => k.toLowerCase() === 'content-type') ) { hdrObj['Content-Type'] = typeof o.content_type === 'string' && o.content_type.trim() ? o.content_type.trim() : 'application/octet-stream' } /** @type {RequestInit} */ const init = { method, headers: hdrObj, signal: signal || undefined, redirect: 'manual' } if (body != null && method !== 'GET' && method !== 'HEAD') { init.body = body } let res try { res = await fetchFn(currentUrl, init) } catch (e) { clearTimeout(timer) const msg = bareWebFmtErr( e === undefined ? new Error( 'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)' ) : e ) return { ok: false, error: 'web_fetch: request failed: ' + msg.slice(0, 400), url_final: currentUrl, redirect_chain: redirectChain } } clearTimeout(timer) const st = res.status if (st >= 300 && st < 400) { if (redirectsUsed >= maxRedirects) { return { ok: false, error: 'web_fetch: too many redirects', status: st, url_final: currentUrl, redirect_chain: redirectChain } } const loc = res.headers.get('Location') if (!loc) { return { ok: false, error: 'web_fetch: redirect without Location', status: st, url_final: currentUrl, redirect_chain: redirectChain } } let nextUrl try { nextUrl = new URL(loc, currentUrl).href } catch { return { ok: false, error: 'web_fetch: bad redirect URL', status: st, url_final: currentUrl, redirect_chain: redirectChain } } redirectChain.push(nextUrl) currentUrl = nextUrl redirectsUsed++ if (st === 301 || st === 302 || st === 303) { method = 'GET' body = undefined } continue } const ct = res.headers.get('content-type') || '' const url_final = currentUrl const responseType = typeof res.type === 'string' ? res.type : undefined let setCookie try { const hdrs = res.headers if (hdrs && typeof hdrs.getSetCookie === 'function') { const sc = hdrs.getSetCookie() if (Array.isArray(sc) && sc.length) setCookie = sc.slice(0, 32) } } catch { setCookie = undefined } if (method === 'HEAD') { return { ok: true, url_final, status: st, content_type: ct, response_type: responseType, set_cookie: setCookie, redirect_chain: redirectChain.length > 1 ? redirectChain : undefined, truncated: false, extract: { note: 'HEAD — body omitted' } } } let bodyRead try { bodyRead = await bareWebReadBodyLimited( res, maxBytes, signal || undefined ) } catch (e) { const msg = bareWebFmtErr(e) return { ok: false, error: 'web_fetch: read body failed: ' + msg.slice(0, 400), url_final, status: st, redirect_chain: redirectChain.length > 1 ? redirectChain : undefined } } const charset = bareWebCharsetFromContentType(ct) const text = bareWebDecodeBytes(bodyRead.bytes, charset) const fmt = fmtRaw === 'auto' ? bareWebLooksLikeJson(ct) ? 'json' : bareWebLooksLikeHtml(ct) ? 'markdownish' : 'raw' : fmtRaw /** @type {unknown} */ let extract if (fmt === 'json') { const p = bareWebMaybeParseJson(text) extract = p.ok ? { json: p.value } : { parse_error: true, text_slice: text.slice(0, 8000) } } else if (fmt === 'links') { extract = bareWebExtractLinks(text, url_final, maxLinks) } else if (fmt === 'meta') { extract = bareWebExtractMeta(text) } else if (fmt === 'markdownish' || fmt === 'text') { const plain = bareWebExtractHtmlText(text) extract = { text: plain, approx_chars: plain.length } } else if (fmt === 'raw') { extract = { raw_text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text, char_count: text.length } } else { extract = { text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text, char_count: text.length } } const raw_preview = text.slice(0, 2000) return { ok: true, url_final, status: st, content_type: ct, response_type: responseType, set_cookie: setCookie, redirect_chain: redirectChain.length > 1 ? redirectChain : undefined, truncated: bodyRead.truncated, extract, raw_preview: fmt === 'raw' || fmt === 'json' ? undefined : raw_preview } } } /** * Guest-safe Grok Build harness ports: todos, plan mode, memory, glob, * AGENTS.md walk-up, JSON hooks, and search_replace uniqueness. */ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({ read_file: 1, list_directory: 1, list_dir: 1, file_stat: 1, search_files: 1, grep: 1, glob_files: 1, glob: 1, get_system_info: 1, list_bin: 1, read_man_page: 1, apropos_man: 1, read_proc_file: 1, get_swarm_peers: 1, get_resource_limits: 1, web_fetch: 1, web_search: 1, git_status: 1, read_skill: 1, list_services: 1, service_status: 1, list_timers: 1, read_cron_log: 1, read_audit_log: 1, read_boot_policy: 1, read_kernel_extension_resolution: 1, get_initd_graph: 1, read_unit_journal: 1, inspect_ipc_backpressure: 1, get_network_summary: 1, tail_telemetry_streams: 1, pkg_index_lookup: 1, list_verification_scripts: 1, verification_hints: 1, runtime_diagnostic_bundle: 1, memory_search: 1, memory_get: 1, memory_append: 1, list_skills: 1, fuzzy_find: 1, wait_for: 1, read_many: 1, git_diff: 1, git_log: 1, git_show: 1, git_blame: 1, history_search: 1, list_scheduled: 1, find_symbol: 1, diff_files: 1, export_session: 1, remember: 1, todo_write: 1, enter_plan_mode: 1, exit_plan_mode: 1, ask_user_question: 1, update_goal: 1, autonomous_run_status: 1, get_hrpc_bridge_health: 1, get_hrpc_allowlist_status: 1 }) var BARE_AGENT_PLAN_WRITE_TOOLS = Object.freeze({ write_file: 1, edit_file: 1, search_replace: 1, apply_patch: 1 }) /** * @param {string} pattern * @returns {RegExp | null} */ function bareAgentGlobToRegExp(pattern) { const src = String(pattern || '').trim() if (!src) return null let out = '^' for (let i = 0; i < src.length; i++) { const c = src.charAt(i) if (c === '*' && src.charAt(i + 1) === '*') { const next = src.charAt(i + 2) if (next === '/' || next === '') { out += '.*' i += next === '/' ? 2 : 1 continue } } if (c === '*') { out += '[^/]*' continue } if (c === '?') { out += '[^/]' continue } if ('\\^$+()[]{}|.'.indexOf(c) !== -1) out += '\\' + c else out += c } out += '$' try { return new RegExp(out) } catch { return null } } /** * @param {string} rel * @param {string} pattern */ function bareAgentGlobMatch(rel, pattern) { const re = bareAgentGlobToRegExp(pattern) if (!re) return false const n = String(rel || '').replace(/^\/+/, '') if (re.test(n)) return true const base = n.split('/').pop() || n return re.test(base) } /** * @param {unknown} updates * @param {{ merge?: boolean }} [opts] * @param {{ id: string, content: string, status: string }[]} [prev] */ function bareAgentTodoApply(updates, opts, prev) { const merge = Boolean(opts && opts.merge) /** @type {{ id: string, content: string, status: string }[]} */ const list = merge && Array.isArray(prev) ? prev.slice() : [] const byId = Object.create(null) for (let i = 0; i < list.length; i++) byId[list[i].id] = i const rows = Array.isArray(updates) ? updates : [] const seen = Object.create(null) for (let i = 0; i < rows.length; i++) { const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {} const id = String(row.id || '').trim() if (!id) throw new Error('todo_id_required') if (seen[id]) throw new Error('duplicate_todo_id:' + id) seen[id] = 1 const statusRaw = String(row.status || 'pending').trim().toLowerCase() const status = statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled' ? statusRaw : 'pending' const content = String(row.content == null ? '' : row.content).trim() if (byId[id] != null) { const cur = list[byId[id]] if (content) cur.content = content cur.status = status } else { list.push({ id, content: content || id, status }) byId[id] = list.length - 1 } } return list } /** * @param {{ id: string, content: string, status: string }[]} todos */ function bareAgentTodoSummarize(todos) { const rows = Array.isArray(todos) ? todos : [] let pending = 0 let inProgress = 0 let completed = 0 const lines = [] for (let i = 0; i < rows.length; i++) { const t = rows[i] const st = String(t.status || 'pending') if (st === 'completed' || st === 'cancelled') completed++ else if (st === 'in_progress') inProgress++ else pending++ lines.push('- [' + st + '] ' + t.id + ': ' + String(t.content || '').slice(0, 160)) } return { total: rows.length, pending, in_progress: inProgress, completed, open: pending + inProgress, text: lines.join('\n') } } /** * @param {{ open?: number, turnsSinceTodoWrite?: number, nudgeEnabled?: boolean }} opts */ function bareAgentTodoNudgeText(opts) { const o = opts && typeof opts === 'object' ? opts : {} if (o.nudgeEnabled === false) return '' const turns = Number(o.turnsSinceTodoWrite) || 0 const open = Number(o.open) || 0 if (open > 0 && turns >= 3) { return ( 'Open todos remain (' + String(open) + '). Use todo_write to mark progress or complete items before stopping.' ) } if (open === 0 && turns >= 5) { return 'Multi-step work: call todo_write to track remaining steps (merge=true to update one id).' } return '' } /** * @param {string} toolName * @param {Record} args * @param {{ plan?: string }} paths */ function bareAgentPlanModeToolAllowed(toolName, args, paths) { const name = String(toolName || '') if (BARE_AGENT_PLAN_READONLY_TOOLS[name]) return true if (!BARE_AGENT_PLAN_WRITE_TOOLS[name]) return false const plan = String((paths && paths.plan) || '') const p = String(args.path || args.file_path || '') return Boolean(plan && p && p === plan) } /** * @param {string} hay * @param {string} needle */ function bareAgentCountOccurrences(hay, needle) { if (!needle) return 0 let n = 0 let from = 0 while (from <= hay.length) { const i = hay.indexOf(needle, from) if (i === -1) break n++ from = i + Math.max(needle.length, 1) } return n } /** * @param {string} text * @param {string} oldStr * @param {string} newStr * @param {boolean} replaceAll */ function bareAgentSearchReplaceApply(text, oldStr, newStr, replaceAll) { const prev = String(text == null ? '' : text) if (!oldStr) { if (prev.trim()) { return { ok: false, error: 'empty_old_string_cannot_overwrite' } } return { ok: true, next: String(newStr == null ? '' : newStr), replacements: 1 } } const count = bareAgentCountOccurrences(prev, oldStr) if (count === 0) return { ok: false, error: 'old_string not found', count: 0 } if (count > 1 && !replaceAll) { return { ok: false, error: 'old_string not unique', count, hint: 'add surrounding lines to make the match unique, or set replace_all true' } } const next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr) return { ok: true, next, replacements: replaceAll ? count : 1 } } /** * @param {string} text * @param {{ offset?: number, limit?: number, numbered?: boolean }} [opts] */ function bareAgentSliceFileLines(text, opts) { const o = opts && typeof opts === 'object' ? opts : {} const raw = String(text == null ? '' : text) const lines = raw.split('\n') if (lines.length && lines[lines.length - 1] === '') lines.pop() const offset = typeof o.offset === 'number' && o.offset >= 1 ? Math.floor(o.offset) : 1 const limit = typeof o.limit === 'number' && o.limit >= 1 ? Math.floor(o.limit) : lines.length const start = Math.min(lines.length, offset - 1) const end = Math.min(lines.length, start + limit) const slice = lines.slice(start, end) const numbered = o.numbered !== false const body = numbered ? slice .map(function (line, i) { return String(start + i + 1) + '→' + line }) .join('\n') : slice.join('\n') return { content: body, start_line: start + 1, end_line: end, total_lines: lines.length, truncated: start > 0 || end < lines.length } } /** * @param {string} query */ function bareAgentMemoryTokens(query) { return String(query || '') .toLowerCase() .split(/[^a-z0-9_./-]+/) .filter(function (t) { return t.length > 1 }) } /** * @param {string} text * @param {string[]} tokens */ function bareAgentMemoryScore(text, tokens) { const low = String(text || '').toLowerCase() if (!tokens.length || !low) return 0 let hits = 0 for (let i = 0; i < tokens.length; i++) { if (low.indexOf(tokens[i]) !== -1) hits++ } return hits / tokens.length } /** * @param {Record} ctx * @param {string} startDir * @param {number} [maxHops] * @returns {Promise} */ async function bareAgentDiscoverAgentsMdPaths(ctx, startDir, maxHops) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return [] /** @type {string[]} */ const found = [] const seen = Object.create(null) let dir = String(startDir || '').replace(/\/+$/, '') || '/' const hops = Math.min(Math.max(Number(maxHops) || 12, 1), 24) const names = ['AGENTS.md', 'Claude.md', 'CLAUDE.md'] for (let n = 0; n < hops; n++) { for (let i = 0; i < names.length; i++) { const p = (dir === '/' ? '' : dir) + '/' + names[i] if (seen[p]) continue seen[p] = 1 try { const buf = await vfs.readFile(p) if (buf && buf.length) found.push(p) } catch { /* missing */ } } const rulesDirs = [dir + '/.grok/rules', dir + '/.agent/rules'] for (let r = 0; r < rulesDirs.length; r++) { if (typeof vfs.readdir !== 'function') continue let entries = [] try { entries = await vfs.readdir(rulesDirs[r]) } catch { continue } if (!Array.isArray(entries)) continue entries = entries.slice().sort() for (let e = 0; e < entries.length; e++) { const name = String(entries[e] || '') if (!/\.md$/i.test(name)) continue const p = rulesDirs[r] + '/' + name if (seen[p]) continue seen[p] = 1 try { const buf = await vfs.readFile(p) if (buf && buf.length) found.push(p) } catch { /* skip */ } } } if (dir === '/' || dir === '') break const slash = dir.lastIndexOf('/') dir = slash <= 0 ? '/' : dir.slice(0, slash) } return found } /** * @param {Record} ctx * @param {string[]} files * @param {number} [maxChars] */ async function bareAgentLoadAgentsMdPrompt(ctx, files, maxChars) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' const cap = Math.min(Math.max(Number(maxChars) || 6000, 500), 16000) const parts = [] let used = 0 for (let i = 0; i < files.length; i++) { if (used >= cap) break try { const buf = await vfs.readFile(files[i]) if (!buf || !buf.length) continue const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const body = String(t || '').trim() if (!body) continue const block = '### ' + files[i] + '\n' + body const room = cap - used parts.push(block.slice(0, room)) used += Math.min(block.length, room) } catch { /* skip */ } } if (!parts.length) return '' return ( '## Discovered project agent files (cwd walk-up, Grok-style)\n' + parts.join('\n\n') ) } /** * @param {Record} hook * @param {string} toolName * @param {Record} args */ function bareAgentHookDenies(hook, toolName, args) { if (!hook || typeof hook !== 'object') return '' const event = String(hook.event || hook.type || 'PreToolUse') if (event !== 'PreToolUse' && event !== 'pre') return '' const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : [] if (tools.length && tools.indexOf(toolName) === -1) return '' const reSrc = String(hook.deny_regex || hook.denyRegex || '').trim() if (!reSrc) return '' let re try { re = new RegExp(reSrc, 'i') } catch { return '' } const blob = toolName + ' ' + String(args.command || args.path || args.file_path || args.pattern || '') if (!re.test(blob)) return '' return String(hook.reason || hook.message || 'blocked by ~/.agent/hooks').slice(0, 240) } /** * @param {Record} ctx * @param {string} hooksDir * @param {string} toolName * @param {Record} args */ async function bareAgentRunPreToolHooks(ctx, hooksDir, toolName, args) { if (typeof bareAgentRunHooks === 'function') { const out = await bareAgentRunHooks(ctx, hooksDir, 'PreToolUse', toolName, args, '') return out.deny || '' } return '' } /** * @param {Record} ctx * @param {unknown} buf */ function bareAgentPortDecode(ctx, buf) { if (!buf || !buf.length) return '' if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ) { return ctx.b4a.toString(buf) } return String(new TextDecoder().decode(buf)) } /** * @param {Record} ctx * @param {string} text */ function bareAgentPortEncode(ctx, text) { if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ) { return ctx.b4a.from(String(text == null ? '' : text)) } return new TextEncoder().encode(String(text == null ? '' : text)) } /** * @param {Record} ctx * @param {string} path */ async function bareAgentReadTextFile(ctx, path) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' try { const buf = await vfs.readFile(path) return bareAgentPortDecode(ctx, buf) } catch { return '' } } /** * @param {Record} ctx * @param {string} path * @param {string} text */ async function bareAgentWriteTextFile(ctx, path, text) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') { throw new Error('vfs.writeFile unavailable') } if (typeof vfs.mkdir === 'function') { const dir = String(path || '').replace(/\/[^/]+$/, '') if (dir && dir !== path) { try { await vfs.mkdir(dir, { recursive: true }) } catch { /* parent may already exist */ } } } await vfs.writeFile(path, bareAgentPortEncode(ctx, text)) } /** * @param {Record} ctx * @param {string} path * @param {unknown} fallback */ async function bareAgentReadJsonFile(ctx, path, fallback) { const t = await bareAgentReadTextFile(ctx, path) if (!t.trim()) return fallback try { return JSON.parse(t) } catch { return fallback } } /** * @param {Record} ctx * @param {string} path * @param {unknown} value */ async function bareAgentWriteJsonFile(ctx, path, value) { await bareAgentWriteTextFile(ctx, path, JSON.stringify(value, null, 2) + '\n') } /** * @param {Record} ctx * @param {string} path */ async function bareAgentVfsIsDir(ctx, path) { const vfs = ctx && ctx.vfs if (!vfs) return false if (typeof vfs.lstat === 'function' || typeof vfs.stat === 'function') { try { const st = typeof vfs.lstat === 'function' ? await vfs.lstat(path) : await vfs.stat(path) if (st && typeof st.isDirectory === 'function') return Boolean(st.isDirectory()) if (st && typeof st === 'object' && 'isDirectory' in st) { return Boolean(/** @type {{ isDirectory?: unknown }} */ (st).isDirectory) } } catch { return false } } if (typeof vfs.readdir === 'function') { try { await vfs.readdir(path) return true } catch { return false } } return false } /** * @param {Record} ctx * @param {string} root * @param {{ maxFiles?: number, maxDepth?: number }} [opts] * @returns {Promise} */ async function bareAgentVfsWalkFiles(ctx, root, opts) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readdir !== 'function') return [] const maxFiles = Math.min(Math.max(Number(opts && opts.maxFiles) || 400, 1), 4000) const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 8, 1), 16) const skip = Object.create(null) skip['.git'] = 1 skip['node_modules'] = 1 skip['.bare-os'] = 1 const rootN = String(root || '/').replace(/\/+$/, '') || '/' const rules = opts && Array.isArray(opts.ignore) ? opts.ignore : typeof bareAgentLoadIgnoreRules === 'function' ? await bareAgentLoadIgnoreRules(ctx, rootN) : [] /** @type {string[]} */ const out = [] /** @type {{ dir: string, depth: number }[]} */ const queue = [{ dir: rootN, depth: 0 }] while (queue.length && out.length < maxFiles) { const cur = queue.shift() if (!cur) break let names try { names = await vfs.readdir(cur.dir) } catch { continue } if (!Array.isArray(names)) continue for (let i = 0; i < names.length && out.length < maxFiles; i++) { const name = String(names[i] || '') if (!name || name === '.' || name === '..' || skip[name]) continue const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name let rel = full if (rootN !== '/' && full.indexOf(rootN + '/') === 0) rel = full.slice(rootN.length + 1) else if (full.charAt(0) === '/') rel = full.slice(1) const isDir = await bareAgentVfsIsDir(ctx, full) if (rules.length && bareAgentIgnoreMatch(rel, isDir, rules)) continue if (isDir) { if (cur.depth + 1 < maxDepth) queue.push({ dir: full, depth: cur.depth + 1 }) } else { out.push(full) } } } return out } /** * @param {Record} ctx * @param {string} root * @param {string} pattern * @param {{ maxFiles?: number, maxDepth?: number }} [opts] */ async function bareAgentGlobFiles(ctx, root, pattern, opts) { const files = await bareAgentVfsWalkFiles(ctx, root, opts) const rootN = String(root || '/').replace(/\/+$/, '') || '/' /** @type {string[]} */ const hits = [] for (let i = 0; i < files.length; i++) { const abs = files[i] let rel = abs if (rootN !== '/' && abs.indexOf(rootN + '/') === 0) rel = abs.slice(rootN.length + 1) else if (abs.charAt(0) === '/') rel = abs.slice(1) if (bareAgentGlobMatch(rel, pattern) || bareAgentGlobMatch(abs, pattern)) { hits.push(abs) } } return hits } /** * @param {Record} ctx * @param {string[]} files * @param {string} query * @param {{ maxHits?: number, snippet?: number }} [opts] */ async function bareAgentMemorySearchFiles(ctx, files, query, opts) { const tokens = bareAgentMemoryTokens(query) const maxHits = Math.min(Math.max(Number(opts && opts.maxHits) || 8, 1), 24) const snippet = Math.min(Math.max(Number(opts && opts.snippet) || 280, 80), 1200) /** @type {{ path: string, score: number, snippet: string }[]} */ const scored = [] for (let i = 0; i < files.length; i++) { const path = files[i] const text = await bareAgentReadTextFile(ctx, path) if (!text) continue const score = tokens.length ? bareAgentMemoryScore(text, tokens) : 0.15 if (score <= 0) continue let snip = text.trim().replace(/\s+/g, ' ').slice(0, snippet) if (tokens.length) { const low = text.toLowerCase() let at = -1 for (let t = 0; t < tokens.length; t++) { const j = low.indexOf(tokens[t]) if (j !== -1 && (at === -1 || j < at)) at = j } if (at >= 0) { const start = Math.max(0, at - 40) snip = (start > 0 ? '…' : '') + text.slice(start, start + snippet).replace(/\s+/g, ' ') } } scored.push({ path, score: Math.round(score * 1000) / 1000, snippet: snip }) } scored.sort(function (a, b) { return b.score - a.score }) return scored.slice(0, maxHits) } /** * @param {Record} ctx * @param {string} path */ async function bareAgentLoadTodos(ctx, path) { const raw = await bareAgentReadJsonFile(ctx, path, []) if (!Array.isArray(raw)) return [] /** @type {{ id: string, content: string, status: string }[]} */ const out = [] 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 || '').trim() if (!id) continue const statusRaw = String(row.status || 'pending').trim().toLowerCase() const status = statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled' ? statusRaw : 'pending' out.push({ id, content: String(row.content == null ? id : row.content), status }) } return out } /** * @param {Record} ctx * @param {string} path * @param {{ id: string, content: string, status: string }[]} todos */ async function bareAgentSaveTodos(ctx, path, todos) { await bareAgentWriteJsonFile(ctx, path, Array.isArray(todos) ? todos : []) } /** * @param {Record} args */ function bareAgentToolPathArg(args) { if (!args || typeof args !== 'object') return '' if (typeof args.path === 'string' && args.path.trim()) return args.path.trim() if (typeof args.file_path === 'string' && args.file_path.trim()) { return args.file_path.trim() } return '' } /** * @param {string} text */ function bareAgentLooksBinaryText(text) { const s = String(text || '') if (!s) return false if (s.indexOf('\0') !== -1) 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 === 10 || c === 13) continue if (c < 32) bad++ } return bad > 8 } /** * Guest-native grep (Grok-style). VFS walk + JS regex — no host rg/node. * @param {Record} ctx * @param {{ * pattern: string, * root?: string, * glob?: string, * ignore_case?: boolean, * before?: number, * after?: number, * context?: number, * max_matches?: number, * files_with_matches?: boolean * }} opts */ async function bareAgentGrepFiles(ctx, opts) { const o = opts && typeof opts === 'object' ? opts : {} const pattern = String(o.pattern || '') if (!pattern) return { ok: false, error: 'pattern_required' } let re try { re = new RegExp(pattern, o.ignore_case ? 'i' : '') } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return { ok: false, error: 'bad_regex', detail: msg } } const root = String(o.root || '/').replace(/\/+$/, '') || '/' const before = Math.min( 8, Math.max(0, Math.floor(Number(o.before != null ? o.before : o.context) || 0)) ) const after = Math.min( 8, Math.max(0, Math.floor(Number(o.after != null ? o.after : o.context) || 0)) ) const maxMatches = Math.min( 200, Math.max(1, Math.floor(Number(o.max_matches) || 50)) ) const mode = String(o.output_mode || '').toLowerCase() const filesOnly = Boolean(o.files_with_matches) || mode === 'files_with_matches' const countOnly = Boolean(o.count) || mode === 'count' const glob = typeof o.glob === 'string' && o.glob.trim() ? o.glob.trim() : '' const files = glob ? await bareAgentGlobFiles(ctx, root, glob, { maxFiles: 800, maxDepth: 12 }) : await bareAgentVfsWalkFiles(ctx, root, { maxFiles: 800, maxDepth: 12 }) /** @type {{ path: string, line?: number, text?: string }[]} */ const matches = [] /** @type {string[]} */ const filesHit = [] /** @type {{ path: string, count: number }[]} */ const counts = [] let truncated = false for (let f = 0; f < files.length; f++) { const path = files[f] const text = await bareAgentReadTextFile(ctx, path) if (!text || bareAgentLooksBinaryText(text)) continue const lines = text.split('\n') if (lines.length && lines[lines.length - 1] === '') lines.pop() let fileHit = false let fileCount = 0 for (let i = 0; i < lines.length; i++) { re.lastIndex = 0 if (!re.test(lines[i])) continue fileHit = true fileCount++ if (filesOnly || countOnly) continue const start = Math.max(0, i - before) const end = Math.min(lines.length, i + 1 + after) const slice = lines.slice(start, end) const body = before || after ? slice .map(function (ln, j) { const n = start + j + 1 const mark = n === i + 1 ? ':' : '-' return String(n) + mark + ln }) .join('\n') : String(i + 1) + ':' + lines[i] matches.push({ path, line: i + 1, text: body }) if (matches.length >= maxMatches) { truncated = true break } } if (fileHit && filesOnly) { filesHit.push(path) if (filesHit.length >= maxMatches) { truncated = true break } } if (fileHit && countOnly) { counts.push({ path, count: fileCount }) if (counts.length >= maxMatches) { truncated = true break } } if (truncated) break } if (filesOnly) { return { ok: true, pattern, root, glob: glob || null, output_mode: 'files_with_matches', files_with_matches: filesHit, count: filesHit.length, truncated } } if (countOnly) { let total = 0 for (let i = 0; i < counts.length; i++) total += counts[i].count return { ok: true, pattern, root, glob: glob || null, output_mode: 'count', files: counts, count: total, truncated } } return { ok: true, pattern, root, glob: glob || null, output_mode: 'content', matches, count: matches.length, truncated } } /** * Codex / Grok apply_patch parser (Begin Patch … End Patch). * @param {string} text * @returns {{ ok: boolean, error?: string, ops?: object[] }} */ function bareAgentParseApplyPatch(text) { const raw = String(text || '').replace(/\r\n/g, '\n') if (!raw.trim()) return { ok: false, error: 'empty_patch' } const all = raw.split('\n') let start = 0 let end = all.length for (let i = 0; i < all.length; i++) { if (/^\s*\*\*\*\s*Begin Patch\s*$/i.test(all[i])) { start = i + 1 break } } for (let i = all.length - 1; i >= 0; i--) { if (/^\s*\*\*\*\s*End Patch\s*$/i.test(all[i])) { end = i break } } const body = all.slice(start, end) /** @type {object[]} */ const ops = [] /** @type {Record | null} */ let cur = null /** @type {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean } | null} */ let chunk = null function flushChunk() { if (cur && cur.type === 'update' && chunk) { const chunks = Array.isArray(cur.chunks) ? cur.chunks : [] chunks.push(chunk) cur.chunks = chunks chunk = null } } function flushOp() { flushChunk() if (cur) ops.push(cur) cur = null } for (let i = 0; i < body.length; i++) { const line = body[i] if (/^\s*\*\*\*\s*Add File:\s*/i.test(line)) { flushOp() cur = { type: 'add', path: line.replace(/^\s*\*\*\*\s*Add File:\s*/i, '').trim(), content: '' } continue } if (/^\s*\*\*\*\s*Delete File:\s*/i.test(line)) { flushOp() cur = { type: 'delete', path: line.replace(/^\s*\*\*\*\s*Delete File:\s*/i, '').trim() } continue } if (/^\s*\*\*\*\s*Update File:\s*/i.test(line)) { flushOp() cur = { type: 'update', path: line.replace(/^\s*\*\*\*\s*Update File:\s*/i, '').trim(), move_to: '', chunks: [] } continue } if (/^\s*\*\*\*\s*Move to:\s*/i.test(line) && cur && cur.type === 'update') { cur.move_to = line.replace(/^\s*\*\*\*\s*Move to:\s*/i, '').trim() continue } if (/^\s*\*\*\*\s*End of File\s*$/i.test(line)) { if (chunk) chunk.is_end_of_file = true flushChunk() continue } if (line.indexOf('@@') === 0 && cur && cur.type === 'update') { flushChunk() chunk = { context: line.replace(/^@@\s?/, '').trim(), old_lines: [], new_lines: [] } continue } if (cur && cur.type === 'add') { const bodyLine = line.charAt(0) === '+' ? line.slice(1) : line cur.content = cur.content ? String(cur.content) + '\n' + bodyLine : bodyLine continue } if (cur && cur.type === 'update') { if (!chunk) { chunk = { context: '', old_lines: [], new_lines: [] } } const tag = line.charAt(0) const rest = line.length ? line.slice(1) : '' if (tag === '-') chunk.old_lines.push(rest) else if (tag === '+') chunk.new_lines.push(rest) else { const ctxLine = tag === ' ' ? rest : line chunk.old_lines.push(ctxLine) chunk.new_lines.push(ctxLine) } } } flushOp() if (!ops.length) return { ok: false, error: 'no_patch_ops' } return { ok: true, ops } } /** * @param {string} text * @param {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean }} chunk */ function bareAgentApplyPatchChunk(text, chunk) { const src = String(text == null ? '' : text) const oldBlock = (chunk.old_lines || []).join('\n') const newBlock = (chunk.new_lines || []).join('\n') if (!oldBlock && !newBlock) { return { ok: false, error: 'empty_chunk' } } if (!oldBlock) { const next = src ? src.replace(/\s*$/, '') + (src.endsWith('\n') ? '' : '\n') + newBlock + '\n' : newBlock + (newBlock.endsWith('\n') ? '' : '\n') return { ok: true, next } } let from = 0 if (chunk.context) { const at = src.indexOf(chunk.context) if (at === -1) { return { ok: false, error: 'chunk_context_not_found', context: chunk.context } } from = at } const hay = src.slice(from) const count = bareAgentCountOccurrences(hay, oldBlock) if (count === 0) return { ok: false, error: 'chunk_old_not_found' } if (count > 1 && !chunk.context) { return { ok: false, error: 'chunk_old_not_unique', count, hint: 'add @@ context or more surrounding lines' } } const next = src.slice(0, from) + hay.replace(oldBlock, newBlock) return { ok: true, next } } /** * @param {Record} ctx * @param {object[]} ops * @param {{ home?: string, denyPrefixes?: unknown }} [opts] */ async function bareAgentApplyPatchOps(ctx, ops, opts) { const home = String((opts && opts.home) || '') const deny = opts && opts.denyPrefixes /** @type {object[]} */ const results = [] const rows = Array.isArray(ops) ? ops : [] for (let i = 0; i < rows.length; i++) { const op = rows[i] && typeof rows[i] === 'object' ? rows[i] : {} let path = String(op.path || '').trim() if (path && path.charAt(0) !== '/' && home) { path = home.replace(/\/+$/, '') + '/' + path.replace(/^\.\//, '') } if (!path) { results.push({ ok: false, error: 'path_required' }) continue } if ( typeof bareAgentPathAllowedMutate === 'function' && !bareAgentPathAllowedMutate(path, deny) ) { results.push({ ok: false, path, error: 'path_not_allowed' }) continue } if (op.type === 'add') { const content = String(op.content == null ? '' : op.content) await bareAgentWriteTextFile( ctx, path, content.endsWith('\n') ? content : content + '\n' ) results.push({ ok: true, op: 'add', path }) continue } if (op.type === 'delete') { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.unlink !== 'function') { results.push({ ok: false, path, error: 'vfs.unlink unavailable' }) continue } try { await vfs.unlink(path) results.push({ ok: true, op: 'delete', path }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) results.push({ ok: false, path, error: msg }) } continue } if (op.type === 'update') { let text = await bareAgentReadTextFile(ctx, path) if (!text && text !== '') { results.push({ ok: false, path, error: 'missing_file' }) continue } const chunks = Array.isArray(op.chunks) ? op.chunks : [] let failed = null for (let c = 0; c < chunks.length; c++) { const applied = bareAgentApplyPatchChunk(text, chunks[c]) if (!applied.ok) { failed = applied break } text = applied.next } if (failed) { results.push({ ok: false, path, error: failed.error, hint: failed.hint || null }) continue } const dest = String(op.move_to || '').trim() || path const destAbs = dest.charAt(0) === '/' ? dest : home ? home.replace(/\/+$/, '') + '/' + dest.replace(/^\.\//, '') : dest if ( destAbs !== path && typeof bareAgentPathAllowedMutate === 'function' && !bareAgentPathAllowedMutate(destAbs, deny) ) { results.push({ ok: false, path, error: 'move_path_not_allowed', dest: destAbs }) continue } await bareAgentWriteTextFile(ctx, destAbs, text) if (destAbs !== path && ctx.vfs && typeof ctx.vfs.unlink === 'function') { try { await ctx.vfs.unlink(path) } catch { /* keep original if unlink fails */ } } results.push({ ok: true, op: destAbs !== path ? 'move' : 'update', path, dest: destAbs !== path ? destAbs : undefined, chunks: chunks.length }) continue } results.push({ ok: false, path, error: 'unknown_op' }) } const failed = results.filter(function (r) { return !r.ok }) return { ok: failed.length === 0, applied: results.length - failed.length, failed: failed.length, results } } /** * Parse DuckDuckGo instant-answer JSON into a compact result list. * @param {unknown} payload * @param {number} [max] */ function bareAgentParseSearchResults(payload, max) { const cap = Math.min(Math.max(Number(max) || 8, 1), 16) /** @type {{ title: string, url: string, snippet: string }[]} */ const out = [] const seen = Object.create(null) function add(title, url, snippet) { const u = String(url || '').trim() if (!u || seen[u] || out.length >= cap) return if (!/^https?:\/\//i.test(u)) return seen[u] = 1 out.push({ title: String(title || u).slice(0, 160), url: u, snippet: String(snippet || '').replace(/\s+/g, ' ').trim().slice(0, 280) }) } const obj = payload && typeof payload === 'object' ? payload : {} const rec = /** @type {Record} */ (obj) if (rec.AbstractURL || rec.Abstract) { add( String(rec.Heading || rec.AbstractSource || 'Abstract'), String(rec.AbstractURL || ''), String(rec.AbstractText || rec.Abstract || '') ) } const results = Array.isArray(rec.Results) ? rec.Results : [] for (let i = 0; i < results.length; i++) { const row = results[i] && typeof results[i] === 'object' ? results[i] : {} add(row.Text || row.Name, row.FirstURL, row.Text) } const related = Array.isArray(rec.RelatedTopics) ? rec.RelatedTopics : [] function walk(list) { for (let i = 0; i < list.length && out.length < cap; i++) { const row = list[i] && typeof list[i] === 'object' ? list[i] : {} if (Array.isArray(row.Topics)) walk(row.Topics) else add(row.Text, row.FirstURL, row.Text) } } walk(related) return out } /** * Parse Grok-style intervals (5m / 2h / 1d) or a five-field cron line. * @param {string} raw * @returns {{ kind: 'everyMs', everyMs: number } | { kind: 'calendar', onCalendar: string } | { error: string }} */ function bareAgentParseScheduleInterval(raw) { const s = String(raw || '').trim() if (!s) return { error: 'interval_required' } const compact = s.replace(/\s+/g, '') const m = /^(\d+)(ms|s|m|h|d)$/i.exec(compact) if (m) { const n = Number(m[1]) const unit = m[2].toLowerCase() let ms = 0 if (unit === 'ms') ms = n else if (unit === 's') ms = n * 1000 else if (unit === 'm') ms = n * 60 * 1000 else if (unit === 'h') ms = n * 60 * 60 * 1000 else ms = n * 24 * 60 * 60 * 1000 if (ms < 1000) return { error: 'interval_too_short', min_ms: 1000 } if (ms > 86400000) return { error: 'interval_too_long', max_ms: 86400000 } return { kind: 'everyMs', everyMs: ms } } const fields = s.split(/\s+/) if (fields.length === 5) return { kind: 'calendar', onCalendar: s } return { error: 'bad_interval', hint: 'use 5m, 2h, 1d, or five cron fields' } } /** * @param {string} id */ function bareAgentParseIgnoreRules(text) { const lines = String(text || '').split(/\r?\n/) /** @type {{ pattern: string, negate: boolean, dirOnly: boolean }[]} */ const rules = [] for (let i = 0; i < lines.length; i++) { let line = String(lines[i] || '').trim() if (!line || line.charAt(0) === '#') continue let negate = false if (line.charAt(0) === '!') { negate = true line = line.slice(1) } let dirOnly = false if (line.charAt(line.length - 1) === '/') { dirOnly = true line = line.slice(0, -1) } if (line.charAt(0) === '/') line = line.slice(1) if (!line) continue rules.push({ pattern: line, negate, dirOnly }) } return rules } /** * Last matching gitignore-style rule wins. * @param {string} rel * @param {boolean} isDir * @param {{ pattern: string, negate: boolean, dirOnly: boolean }[]} rules */ function bareAgentIgnoreMatch(rel, isDir, rules) { const n = String(rel || '').replace(/^\/+/, '') if (!n || !Array.isArray(rules) || !rules.length) return false let ignored = false for (let i = 0; i < rules.length; i++) { const rule = rules[i] if (rule.dirOnly && !isDir) continue let hit = false if (rule.pattern.indexOf('/') === -1) { const parts = n.split('/') for (let p = 0; p < parts.length; p++) { if (bareAgentGlobMatch(parts[p], rule.pattern)) { hit = true break } } } else { hit = bareAgentGlobMatch(n, rule.pattern) || bareAgentGlobMatch(n, '**/' + rule.pattern) } if (!hit) continue ignored = !rule.negate } return ignored } /** * @param {Record} ctx * @param {string} root */ async function bareAgentLoadIgnoreRules(ctx, root) { const names = ['.gitignore', '.agentignore', '.grokignore'] /** @type {string[]} */ const chunks = [] for (let i = 0; i < names.length; i++) { const t = await bareAgentReadTextFile(ctx, String(root || '').replace(/\/+$/, '') + '/' + names[i]) if (t && t.trim()) chunks.push(t) } return bareAgentParseIgnoreRules(chunks.join('\n')) } /** * Simple Grok-style fuzzy score (basename + path subsequence). Higher is better. * @param {string} query * @param {string} path */ function bareAgentFuzzyScore(query, path) { const q = String(query || '').toLowerCase().trim() const p = String(path || '') if (!q || !p) return 0 const low = p.toLowerCase() const base = (p.split('/').pop() || p).toLowerCase() if (base === q) return 1000 if (base.indexOf(q) === 0) return 800 - Math.min(base.length, 80) if (base.indexOf(q) !== -1) return 600 - base.indexOf(q) if (low.indexOf(q) !== -1) return 400 let qi = 0 let score = 0 let streak = 0 for (let i = 0; i < low.length && qi < q.length; i++) { if (low.charAt(i) === q.charAt(qi)) { qi++ streak++ score += 8 + streak * 4 } else streak = 0 } if (qi < q.length) return 0 if (base.length && q.length / base.length > 0.5) score += 40 return score } /** * @param {Record} ctx * @param {string} root * @param {string} query * @param {{ max?: number, maxFiles?: number }} [opts] */ async function bareAgentFuzzyFind(ctx, root, query, opts) { const files = await bareAgentVfsWalkFiles(ctx, root, { maxFiles: Math.min(Math.max(Number(opts && opts.maxFiles) || 600, 40), 2000), maxDepth: 12 }) const cap = Math.min(Math.max(Number(opts && opts.max) || 20, 1), 80) /** @type {{ path: string, score: number }[]} */ const scored = [] for (let i = 0; i < files.length; i++) { const score = bareAgentFuzzyScore(query, files[i]) if (score <= 0) continue scored.push({ path: files[i], score }) } scored.sort(function (a, b) { return b.score - a.score }) return scored.slice(0, cap) } /** * @param {Record} hook * @param {string} event * @param {string} toolName * @param {Record} args * @param {string} [result] */ function bareAgentHookMatch(hook, event, toolName, args, result) { if (!hook || typeof hook !== 'object') return null const ev = String(hook.event || hook.type || 'PreToolUse') const want = String(event || '') if (ev !== want && ev.toLowerCase() !== want.toLowerCase()) return null const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : [] if (tools.length && toolName && tools.indexOf(toolName) === -1) return null const reSrc = String(hook.deny_regex || hook.match_regex || hook.denyRegex || '').trim() if (reSrc) { let re try { re = new RegExp(reSrc, 'i') } catch { return null } const blob = toolName + ' ' + String((args && (args.command || args.path || args.file_path || args.pattern)) || '') + ' ' + String(result || '').slice(0, 400) if (!re.test(blob)) return null } return hook } /** * @param {Record} ctx * @param {string} hooksDir * @param {string} event * @param {string} [toolName] * @param {Record} [args] * @param {string} [result] */ async function bareAgentRunHooks(ctx, hooksDir, event, toolName, args, result) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') { return { deny: '', inject: '' } } let names = [] try { names = await vfs.readdir(hooksDir) } catch { return { deny: '', inject: '' } } if (!Array.isArray(names)) return { deny: '', inject: '' } names = names.filter(function (n) { return /\.json$/i.test(String(n || '')) }) names.sort() /** @type {string[]} */ const injects = [] for (let i = 0; i < names.length; i++) { try { const t = await bareAgentReadTextFile(ctx, hooksDir + '/' + names[i]) if (!t.trim()) continue const hook = JSON.parse(t) const matched = bareAgentHookMatch(hook, event, toolName || '', args || {}, result || '') if (!matched) continue if (event === 'PreToolUse' || event === 'pre') { const reason = String(matched.reason || matched.message || '').trim() if (reason && (matched.deny === true || matched.deny_regex || matched.denyRegex)) { return { deny: reason.slice(0, 240), inject: '' } } if (typeof bareAgentHookDenies === 'function') { const d = bareAgentHookDenies(matched, toolName || '', args || {}) if (d) return { deny: d, inject: '' } } } const inj = String(matched.inject || matched.append || '').trim() if (inj) injects.push(inj.slice(0, 800)) } catch { /* ignore */ } } return { deny: '', inject: injects.join('\n') } } /** * @param {Record} ctx * @param {string} editsPath * @param {{ path: string, prev: string, tool?: string }} rec */ async function bareAgentPushEdit(ctx, editsPath, rec) { const prev = await bareAgentReadJsonFile(ctx, editsPath, []) const list = Array.isArray(prev) ? prev : [] list.push({ path: String(rec.path || ''), prev: String(rec.prev == null ? '' : rec.prev), tool: String(rec.tool || ''), ts: new Date().toISOString() }) while (list.length > 20) list.shift() await bareAgentWriteJsonFile(ctx, editsPath, list) } /** * @param {Record} ctx * @param {string} editsPath */ async function bareAgentPopEdit(ctx, editsPath) { const prev = await bareAgentReadJsonFile(ctx, editsPath, []) const list = Array.isArray(prev) ? prev : [] const last = list.pop() await bareAgentWriteJsonFile(ctx, editsPath, list) return last && typeof last === 'object' ? last : null } /** * @param {unknown[]} messages * @param {string} query * @param {number} [max] */ function bareAgentHistorySearch(messages, query, max) { const tokens = bareAgentMemoryTokens(query) const cap = Math.min(Math.max(Number(max) || 8, 1), 24) if (!Array.isArray(messages) || !tokens.length) return [] /** @type {{ role: string, score: number, snippet: string }[]} */ const hits = [] for (let i = 0; i < messages.length; i++) { const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null if (!m) continue const role = String(m.role || '') if (role !== 'user' && role !== 'assistant') continue const text = String(m.content || '') const score = bareAgentMemoryScore(text, tokens) if (score <= 0) continue hits.push({ role, score: Math.round(score * 1000) / 1000, snippet: text.replace(/\s+/g, ' ').trim().slice(0, 280) }) } hits.sort(function (a, b) { return b.score - a.score }) return hits.slice(0, cap) } function bareAgentScheduleId(id) { let s = String(id || '') .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') if (!s) s = 'task' if (s.indexOf('agent-') !== 0) s = 'agent-' + s return s.slice(0, 40) } /** * User-turn rewind points (Grok /rewind). Index 0 is the first user message. * @param {unknown[]} messages */ function bareAgentRewindPoints(messages) { /** @type {{ userIndex: number, messageIndex: number, preview: string }[]} */ const points = [] if (!Array.isArray(messages)) return points for (let i = 0; i < messages.length; i++) { const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null if (!m || String(m.role || '') !== 'user') continue points.push({ userIndex: points.length, messageIndex: i, preview: String(m.content || '') .replace(/\s+/g, ' ') .trim() .slice(0, 140) }) } return points } /** * Drop the last N user turns (and everything after that user message). * userIndex rewinds to that user turn (keeps messages before it). * keep_user keeps the target user message so the prompt can be retried. * @param {unknown[]} messages * @param {{ steps?: number, userIndex?: number, keep_user?: boolean }} [opts] */ function bareAgentRewindHistory(messages, opts) { const list = Array.isArray(messages) ? messages.slice() : [] const points = bareAgentRewindPoints(list) if (!points.length) return { ok: false, error: 'nothing_to_rewind', messages: list, dropped: 0 } const o = opts && typeof opts === 'object' ? opts : {} let target if (o.userIndex != null && Number.isFinite(Number(o.userIndex))) { target = points[Math.floor(Number(o.userIndex))] if (!target) return { ok: false, error: 'bad_user_index', messages: list, dropped: 0 } } else { const steps = Math.min(points.length, Math.max(1, Math.floor(Number(o.steps) || 1))) target = points[points.length - steps] } const keepUser = Boolean(o.keep_user) const cut = keepUser ? target.messageIndex : target.messageIndex - 1 const next = cut < 0 ? [] : list.slice(0, cut + 1) return { ok: true, messages: next, dropped: list.length - next.length, target: target, keep_user: keepUser } } /** * Markdown transcript (Grok /export). * @param {unknown[]} messages */ function bareAgentExportTranscript(messages) { const lines = [ '# Agent session export', '', 'Exported: ' + new Date().toISOString(), '' ] const list = Array.isArray(messages) ? messages : [] for (let i = 0; i < list.length; i++) { const m = list[i] && typeof list[i] === 'object' ? list[i] : {} const role = String(m.role || 'unknown') let content = String(m.content || '') if (role === 'tool' && content.length > 1200) content = content.slice(0, 1200) + '\n… truncated' const calls = Array.isArray(m.tool_calls) ? m.tool_calls : [] const names = [] for (let c = 0; c < calls.length; c++) { const fn = calls[c] && calls[c].function ? calls[c].function : {} if (fn && fn.name) names.push(String(fn.name)) } lines.push('## ' + String(i + 1) + '. ' + role) lines.push('') if (names.length) lines.push('tools: ' + names.join(', ')) lines.push(content || '(empty)') lines.push('') } return lines.join('\n') } /** * Line-based unified diff (Grok-style file compare, no git required). * @param {string} oldText * @param {string} newText * @param {{ from?: string, to?: string, context?: number }} [opts] */ function bareAgentUnifiedDiff(oldText, newText, opts) { const pathA = String((opts && opts.from) || 'a') const pathB = String((opts && opts.to) || 'b') const ctxN = Math.min(8, Math.max(0, Math.floor(Number(opts && opts.context) || 3))) const a = String(oldText || '') .replace(/\r\n/g, '\n') .split('\n') const b = String(newText || '') .replace(/\r\n/g, '\n') .split('\n') if (a.length && a[a.length - 1] === '') a.pop() if (b.length && b[b.length - 1] === '') b.pop() if (a.join('\n') === b.join('\n')) { return { ok: true, identical: true, text: '', added: 0, removed: 0 } } const maxLines = 800 if (a.length > maxLines || b.length > maxLines) { return { ok: true, truncated: true, identical: false, text: '--- ' + pathA + '\n+++ ' + pathB + '\n@@ files too large for inline LCS diff (' + a.length + '/' + b.length + ' lines) @@\n', added: 0, removed: 0 } } const n = a.length const m = b.length /** @type {number[][]} */ const dp = new Array(n + 1) for (let i = 0; i <= n; i++) { dp[i] = new Array(m + 1) for (let j = 0; j <= m; j++) dp[i][j] = 0 } for (let i = 1; i <= n; i++) { for (let j = 1; j <= m; j++) { dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : dp[i - 1][j] >= dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1] } } /** @type {{ op: string, line: string }[]} */ const ops = [] let i = n let j = m while (i > 0 || j > 0) { if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { ops.push({ op: ' ', line: a[i - 1] }) i-- j-- } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { ops.push({ op: '+', line: b[j - 1] }) j-- } else { ops.push({ op: '-', line: a[i - 1] }) i-- } } ops.reverse() let added = 0 let removed = 0 for (let k = 0; k < ops.length; k++) { if (ops[k].op === '+') added++ else if (ops[k].op === '-') removed++ } const lines = ['--- ' + pathA, '+++ ' + pathB] let idx = 0 while (idx < ops.length) { while (idx < ops.length && ops[idx].op === ' ') idx++ if (idx >= ops.length) break let start = Math.max(0, idx - ctxN) let end = idx while (end < ops.length) { if (ops[end].op !== ' ') { end++ continue } let run = 0 let p = end while (p < ops.length && ops[p].op === ' ') { run++ p++ } if (run > ctxN * 2) { end += ctxN break } end = p } end = Math.min(ops.length, end) let oldLine = 1 let newLine = 1 for (let k = 0; k < start; k++) { if (ops[k].op !== '+') oldLine++ if (ops[k].op !== '-') newLine++ } let oldCount = 0 let newCount = 0 for (let k = start; k < end; k++) { if (ops[k].op !== '+') oldCount++ if (ops[k].op !== '-') newCount++ } lines.push( '@@ -' + String(oldLine) + ',' + String(oldCount) + ' +' + String(newLine) + ',' + String(newCount) + ' @@' ) for (let k = start; k < end; k++) { lines.push(ops[k].op + ops[k].line) } idx = end } return { ok: true, identical: false, truncated: false, text: lines.join('\n') + '\n', added, removed } } /** * Definition-oriented regex for a symbol name (JS / Python / Rust / C-like). * @param {string} name */ function bareAgentSymbolRegex(name) { const esc = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') if (!esc) return '' return ( '(?:(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+' + esc + '\\b|(?:export\\s+)?(?:default\\s+)?class\\s+' + esc + '\\b|(?:export\\s+)?(?:const|let|var)\\s+' + esc + '\\b|def\\s+' + esc + '\\s*\\(|fn\\s+' + esc + '\\b|' + esc + '\\s*=\\s*(?:async\\s+)?(?:function|\\(|class))' ) } /** * @param {Record} ctx * @param {string} root * @param {string} name * @param {{ glob?: string, max?: number }} [opts] */ async function bareAgentFindSymbol(ctx, root, name, opts) { const pattern = bareAgentSymbolRegex(name) if (!pattern) return { ok: false, error: 'name_required' } const out = await bareAgentGrepFiles(ctx, { pattern, root, glob: opts && opts.glob, max_matches: opts && opts.max ? opts.max : 40 }) if (!out || out.ok === false) return out return { ok: true, name, root, matches: out.matches || [], count: out.count || 0, truncated: Boolean(out.truncated) } } /** * Grok list_dir-style BFS tree (bounded). * @param {Record} ctx * @param {string} root * @param {{ max?: number, maxDepth?: number }} [opts] */ async function bareAgentRenderTree(ctx, root, opts) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readdir !== 'function') { return { ok: false, error: 'readdir unavailable' } } const base = String(root || '/').replace(/\/+$/, '') || '/' const maxItems = Math.min(Math.max(Number(opts && opts.max) || 200, 20), 800) const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 6, 1), 12) const skip = Object.create(null) skip['.git'] = 1 skip['node_modules'] = 1 skip['.bare-os'] = 1 /** @type {string[]} */ const lines = [base + '/'] /** @type {{ dir: string, prefix: string, depth: number }[]} */ const queue = [{ dir: base, prefix: '', depth: 0 }] let count = 1 let truncated = false while (queue.length && count < maxItems) { const cur = queue.shift() if (!cur) break let names = [] try { names = await vfs.readdir(cur.dir) } catch { continue } if (!Array.isArray(names)) continue names = names .map(function (n) { return String(n || '') }) .filter(function (n) { return n && n !== '.' && n !== '..' && !skip[n] }) .sort() for (let i = 0; i < names.length && count < maxItems; i++) { const name = names[i] const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name const isDir = await bareAgentVfsIsDir(ctx, full) const last = i === names.length - 1 const branch = last ? '`-- ' : '|-- ' lines.push(cur.prefix + branch + name + (isDir ? '/' : '')) count++ if (isDir && cur.depth + 1 < maxDepth) { queue.push({ dir: full, prefix: cur.prefix + (last ? ' ' : '| '), depth: cur.depth + 1 }) } } if (count >= maxItems) { truncated = true break } } return { ok: true, path: base, tree: lines.join('\n'), count, truncated } } /** * VFS copy (file or directory). Dest parents are created. * @param {Record} ctx * @param {string} from * @param {string} to */ async function bareAgentCopyPath(ctx, from, to) { const src = String(from || '').replace(/\/+$/, '') let dest = String(to || '') if (!src || !dest) return { ok: false, error: 'from_and_to_required' } const srcIsDir = await bareAgentVfsIsDir(ctx, src) const destIsDir = await bareAgentVfsIsDir(ctx, dest.replace(/\/+$/, '')) if (destIsDir) { const base = src.split('/').pop() || 'copy' dest = dest.replace(/\/+$/, '') + '/' + base } if (!srcIsDir) { const text = await bareAgentReadTextFile(ctx, src) await bareAgentWriteTextFile(ctx, dest, text) return { ok: true, from: src, to: dest, kind: 'file' } } const files = await bareAgentVfsWalkFiles(ctx, src, { maxFiles: 400, maxDepth: 12 }) for (let i = 0; i < files.length; i++) { const rel = files[i].slice(src.length) const text = await bareAgentReadTextFile(ctx, files[i]) await bareAgentWriteTextFile(ctx, dest + rel, text) } return { ok: true, from: src, to: dest, kind: 'directory', files: files.length } } /** * SKILL.md body with YAML frontmatter (Grok skill format). * @param {{ name: string, description: string, body?: string }} spec */ function bareAgentSkillMarkdown(spec) { const name = String((spec && spec.name) || '').trim() || 'skill' const description = String((spec && spec.description) || '').trim() || name const body = String((spec && spec.body) || '').trim() || '# ' + name + '\n' return ( '---\nname: ' + name.replace(/\n/g, ' ') + '\ndescription: ' + description.replace(/\n/g, ' ') + '\n---\n\n' + body + '\n' ) } /** * Walk-up Grok/Claude/Cursor skill roots (project-local). * @param {Record} ctx * @param {string} startDir */ async function bareAgentDiscoverProjectSkillRoots(ctx, startDir) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readdir !== 'function') return [] /** @type {{ path: string, source: string }[]} */ const roots = [] const seen = Object.create(null) let dir = String(startDir || '').replace(/\/+$/, '') || '/' const suffixes = ['.grok/skills', '.agents/skills', '.claude/skills', '.cursor/skills'] for (let hop = 0; hop < 12; hop++) { for (let i = 0; i < suffixes.length; i++) { const p = (dir === '/' ? '' : dir) + '/' + suffixes[i] if (seen[p]) continue seen[p] = 1 try { const names = await vfs.readdir(p) if (Array.isArray(names) && names.length) roots.push({ path: p, source: 'project' }) } catch { /* missing */ } } if (dir === '/') break const parent = dir.replace(/\/[^/]+$/, '') || '/' if (parent === dir) break dir = parent } return roots } /** Shared agent tool helpers (preamble for /bin/agent; schemas/dispatch are sibling files). */ /** * Host-checkout verification hints (keep loosely aligned with scripts/lib/agent-check-hints-data.mjs). * @param {string} combined paths + topic * @returns {string[]} */ function bareAgentVerificationHintsList(combined) { const c = String(combined || '').toLowerCase() /** @type {string[]} */ const hints = [] if (/(^|\/)kernel\/|kernel\\|\/boot\/init|lib\/init|lib\\init/.test(c)) { hints.push('npm run bundle:kernel', 'node scripts/verify-kernel-seeder-parity.mjs') } if (/bare-os-coreutils|kernel\/bin|kernel\\bin/.test(c)) { hints.push('npm run build -w bare-os-coreutils', 'node scripts/verify-man-coverage.mjs') } if (/bare-os-booter|bare-os-ctx-api/.test(c)) { hints.push( 'npm run test -w bare-os-booter', 'node scripts/verify-ctx-api-feature-bits.mjs' ) } if (/bare-os-protocol|seed-rpc|channel\.js/.test(c)) { hints.push('npm run test -w bare-os-protocol') } if (/bare-os-seeder/.test(c)) { hints.push('node scripts/verify-kernel-seeder-parity.mjs') } if (/bare-os-bare-libs|kernel\/lib\/bare|kernel\\lib\\bare/.test(c)) { hints.push('npm run build -w bare-os-bare-libs', 'node scripts/verify-bundle-health.mjs') } if (/shell|sh\.js|test\.js/.test(c) && /booter/.test(c)) { hints.push('npm run test:shell-fast') } if (/docs\/|handbook\/|developer-guide\//.test(c)) { hints.push('npm run pretest', 'node scripts/verify-doc-links.mjs') } if (!hints.length) hints.push('npm run pretest', 'npm test') return [...new Set(hints)] } /** * @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"}' } } /** * @param {Record} ctx * @param {string} absPath */ function bareAgentPathAllowed(absPath) { if (typeof bareAgentPathAllowedRead === 'function') { return bareAgentPathAllowedRead(absPath) } const p = String(absPath || '').replace(/\\/g, '/') return Boolean(p.startsWith('/') && !p.includes('..')) } /** * @param {string} command * @param {unknown} denyList */ function bareAgentCommandDeniedByList(command, denyList) { const cmd = String(command || '').toLowerCase() const list = Array.isArray(denyList) ? denyList : [] for (let i = 0; i < list.length; i++) { const needle = String(list[i] || '').trim().toLowerCase() if (needle && cmd.includes(needle)) return needle } return '' } /** * @param {Record} base * @param {Record} patch */ function bareAgentMergeConfigPatch(base, patch) { const out = { ...base } const keys = [ 'backend', 'rest_base_url', 'rest_api_key', 'model', 'qvac_model', 'qvac_profile', 'qvac_ctx_size', 'qvac_device', 'qvac_main_gpu', 'qvac_gpu_layers', 'max_tokens', 'temperature', 'provider', 'max_iterations', 'stream', 'tool_parallelism', 'request_timeout_ms', 'access_policy', 'allow_delete', 'require_confirm_token', 'command_deny', 'mutate_deny_prefixes', 'owner_name', 'agent_label', 'show_reasoning', 'reasoning_mode', 'reasoning_max_chars', 'reasoning_include_tools', 'allow_bridge_mutations', 'allow_host_notifications', 'allow_host_actions', 'emergency_stop_mutations', 'autonomous_mode_enabled', 'autonomous_max_runtime_ms', 'autonomous_completion_required_checks', 'autonomous_allow_paths', 'autonomous_deny_ops', 'autonomous_active', 'autonomous_started_at_ms', 'autonomous_stop_requested', 'autonomous_goal', 'autonomous_status', 'autonomous_last_error', 'context_compaction', 'compaction_keep_recent', 'compaction_tool_chars', 'plan_mode_active', 'todo_nudge_enabled' ] const numKeys = new Set([ 'max_tokens', 'temperature', 'max_iterations', 'tool_parallelism', 'request_timeout_ms', 'reasoning_max_chars', 'compaction_keep_recent', 'compaction_tool_chars', 'qvac_ctx_size', 'qvac_gpu_layers', 'autonomous_max_runtime_ms', 'autonomous_started_at_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 === 'autonomous_completion_required_checks' || k === 'autonomous_allow_paths' || k === 'autonomous_deny_ops' || k === 'command_deny' || k === 'mutate_deny_prefixes' ) { out[k] = Array.isArray(v) ? v.map((x) => String(x ?? '')).filter(Boolean) : out[k] } else if ( k === 'stream' || k === 'allow_delete' || k === 'show_reasoning' || k === 'reasoning_include_tools' || k === 'allow_bridge_mutations' || k === 'allow_host_notifications' || k === 'allow_host_actions' || k === 'emergency_stop_mutations' || k === 'autonomous_mode_enabled' || k === 'autonomous_active' || k === 'autonomous_stop_requested' || k === 'plan_mode_active' || k === 'todo_nudge_enabled' ) { out[k] = Boolean(v) } else if (k === 'reasoning_mode') { const mode = String(v ?? '').trim().toLowerCase() out[k] = mode === 'summary' || mode === 'trace' ? mode : 'off' } else if (k === 'context_compaction') { const mode = String(v ?? '').trim().toLowerCase() out[k] = mode === 'off' || mode === 'aggressive' ? mode : 'auto' } else if (k === 'require_confirm_token') { out[k] = String(v ?? '') } else if (k === 'access_policy') { const pol = String(v ?? '').trim().toLowerCase() out[k] = pol === 'restricted' ? 'restricted' : 'full' } 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) { if (typeof bareAgentSaveConfig === 'function') { await bareAgentSaveConfig(ctx, paths, config) return } 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) } /** OpenAI-style tool schemas for /bin/agent (preamble; no import). */ /** * @returns {unknown[]} */ function bareAgentToolDefinitions() { return [ { type: 'function', function: { name: 'read_file', description: 'Read a UTF-8 text file from the VFS. Path may be absolute or ~/... (tilde is expanded). Optional offset/limit return numbered line slices (Grok-style).', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute file path' }, file_path: { type: 'string', description: 'Alias for path' }, max_bytes: { type: 'integer', description: 'Max bytes to read (default 256000)' }, offset: { type: 'integer', description: '1-based start line for a numbered slice' }, limit: { type: 'integer', description: 'Max lines to return from offset' }, numbered: { type: 'boolean', description: 'Prefix N→ on each line when slicing (default true if offset/limit set)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'write_file', description: 'Create a new file or overwrite an existing one at any writable guest path. Path may be absolute or ~/... (tilde is expanded to the session home). Parent directories are created as needed. Returns the expanded path — verify with list_directory or file_stat before claiming the file exists. Do not ask permission.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path or ~/file' }, file_path: { type: 'string', description: 'Alias for path' }, content: { type: 'string', description: 'Full file contents' } }, required: ['path', 'content'] } } }, { type: 'function', function: { name: 'edit_file', description: 'Edit a text file: full replacement via content, or search/replace. old_string must be unique unless replace_all is true.', parameters: { type: 'object', properties: { path: { type: 'string' }, file_path: { type: 'string', description: 'Alias for path' }, 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' }, replace_all: { type: 'boolean', description: 'Replace every occurrence of old_string (default false)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'create_directory', description: 'Create a directory (recursive). Path may be absolute or ~/... (tilde is expanded). Returns the expanded path — list_directory it before claiming it exists.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path or ~/dir' }, dir: { type: 'string', description: 'Alias for path' } }, 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 guest CLI line (curl, wget, ip, ping, ls, cat, …) via ctx.execLine. Prefer this over run_js_script for any shell command. 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)' }, cwd: { type: 'string', description: 'Working directory (cd there first). Persists as ~/.agent/last_cwd.' }, 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: 'Run custom guest JavaScript only. Do not use this for curl, wget, ip, ping, or any shell — call run_command. Do not use this to replace read_file, write_file, list_directory, or run_command. Node is not installed. Writes ~/.agent/_tmp_agent_run.mjs and runs it on the Bare kernel. ctx and argv are already injected — never declare them, never export. Body must be: async function run(ctx, argv) { const vfs = ctx.vfs; ... }', 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. Questions about **which kernel features are on/off in this session** → read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json; not apropos_man. For other /proc JSON use read_proc_file; swarm → get_swarm_peers; resource table → 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, owner_name, agent_label, …)' } }, 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)' }, tree: { type: 'boolean', description: 'Grok-style bounded BFS tree instead of a flat listing' }, max_depth: { type: 'integer', description: 'Tree depth (default 6)' } }, 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. Any writable VFS path is allowed; the read-only base system (/bin, /etc, /boot, /lib, /usr, /share, /proc, /dev) is denied.', 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). Allowed by default. Optional confirm_token only if require_confirm_token is set. Cannot delete the read-only base system.', 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. This is documentation search only—never use it to answer what kernel features are currently enabled or disabled (use read_proc_file on /proc/bare_os/features).', 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 any /proc path (bounded), including the full /proc/bare_os kernel surface. Canonical live feature state: /proc/bare_os/features or features.json. Prefer this over shelling cat.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute /proc path (any kernel node). Examples: /proc/bare_os/features.json, /proc/bare_os/capabilities.json, /proc/bare_os/swarm.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: 'web_fetch', description: 'Fetch live HTTP(S) URLs and return structured content for the assistant. Uses ctx.httpFetch (same policy as wget/curl: BARE_OS_HTTP_ALLOWLIST / DENYLIST). For official docs index use read_man_page / apropos_man — they are not web pages. Supports GET/HEAD/POST and extract modes: auto (JSON vs HTML vs text), markdownish plain text from HTML, links (anchor hrefs), meta (title/og:), raw UTF-8 slice, or json parse.', parameters: { type: 'object', properties: { url: { type: 'string', description: 'Absolute http(s) URL' }, method: { type: 'string', description: 'HTTP method (default GET)', enum: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] }, headers: { type: 'object', description: 'Optional header map (string values only)' }, body: { type: 'string', description: 'Request body for non-GET (e.g. JSON string for APIs)' }, content_type: { type: 'string', description: 'Content-Type when body is set (default application/octet-stream)' }, format: { type: 'string', enum: ['auto', 'json', 'markdownish', 'text', 'links', 'meta', 'raw'], description: 'auto: sniff Content-Type; json: parse JSON; markdownish/text: strip HTML to readable text; links: absolute http(s) links; meta: title/description/og tags; raw: bounded UTF-8 text' }, max_response_bytes: { type: 'integer', description: 'Cap downloaded bytes (default 524288, max 2MiB)' }, max_redirects: { type: 'integer', description: 'Max redirects to follow (default 5)' }, timeout_ms: { type: 'integer', description: 'Per-request timeout ms (default 30000, max 120000)' }, max_links: { type: 'integer', description: 'Max links when format=links (default 200)' } }, required: ['url'] } } }, { type: 'function', function: { name: 'read_skill', description: 'Load the full SKILL.md for a modular agent skill (folder id or frontmatter name, case-insensitive). Workspace ~/.agent/workspace/skills/ overrides ~/.agent/skills/. Use after checking the compact skills index in the system prompt.', parameters: { type: 'object', properties: { skill: { type: 'string', description: 'Skill folder name (e.g. p2p-os-status) or YAML frontmatter name' }, max_bytes: { type: 'integer', description: 'Max bytes of SKILL.md (default 256000)' } }, required: ['skill'] } } }, { type: 'function', function: { name: 'list_services', description: 'List initd service definitions and current runtime phases from /proc/bare_os/initd_readiness.json when available.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'service_status', description: 'Read one initd unit status by name from /proc/bare_os/initd_readiness.json and include journal hint paths when present.', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Unit name, e.g. bare-cron' } }, required: ['name'] } } }, { type: 'function', function: { name: 'list_timers', description: 'List user timer drop-ins from ~/.config/bare-os/timers and optionally include short file previews.', parameters: { type: 'object', properties: { include_preview: { type: 'boolean', description: 'Include bounded timer file text previews (default false)' }, max_entries: { type: 'integer', description: 'Maximum timer files to return (default 128, max 512)' } } } } }, { type: 'function', function: { name: 'read_cron_log', description: 'Read /var/log/bare-os/cron.log with bounded output and optional tail mode.', parameters: { type: 'object', properties: { max_chars: { type: 'integer', description: 'Maximum returned characters (default 12000)' }, tail_only: { type: 'boolean', description: 'When true, return only the trailing max_chars slice' } } } } }, { type: 'function', function: { name: 'read_audit_log', description: 'Read /var/log/bare-os/audit.log with bounded output and best-effort secret redaction.', parameters: { type: 'object', properties: { max_chars: { type: 'integer', description: 'Maximum returned characters (default 12000)' }, tail_only: { type: 'boolean', description: 'When true, return only the trailing max_chars slice' }, redact: { type: 'boolean', description: 'Apply lightweight token redaction (default true)' } } } } }, { type: 'function', function: { name: 'read_boot_policy', description: 'Read /etc/bare-os/boot.policy.json and return text plus parsed JSON when available.', parameters: { type: 'object', properties: { max_chars: { type: 'integer', description: 'Maximum returned characters (default 20000)' } } } } }, { type: 'function', function: { name: 'read_kernel_extension_resolution', description: 'Read /run/bare-os/kernel-ext-resolution.json for extension ordering, conflicts, and pin outcomes.', parameters: { type: 'object', properties: { max_chars: { type: 'integer', description: 'Maximum returned characters (default 20000)' } } } } }, { type: 'function', function: { name: 'get_initd_graph', description: 'Read initd dependency DAG/readiness graph from /proc/bare_os/initd_dag.json and /proc/bare_os/initd_readiness.json.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'read_unit_journal', description: 'Read a bounded/redacted tail of /run/bare-os/unit-journal/.ndjson.', parameters: { type: 'object', properties: { unit: { type: 'string', description: 'Initd unit name, e.g. bare-cron' }, max_chars: { type: 'integer', description: 'Maximum output chars (default 12000)' }, tail_only: { type: 'boolean', description: 'Return trailing max_chars only' } }, required: ['unit'] } } }, { type: 'function', function: { name: 'inspect_ipc_backpressure', description: 'Inspect IPC/backpressure operator snapshots from /proc/bare_os JSON surfaces.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'get_network_summary', description: 'Read a typed network/swarm summary from /proc/bare_os surfaces with best-effort fallback paths.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'tail_telemetry_streams', description: 'Read bounded/redacted tails from telemetry logs such as /var/log/bare-os/audit.log, logger.jsonl, and initd logs.', parameters: { type: 'object', properties: { max_chars: { type: 'integer', description: 'Maximum chars per stream (default 8000)' } } } } }, { type: 'function', function: { name: 'pkg_index_lookup', description: 'Run pkg-swarm-index lookup and return parsed output for one package key.', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Package key to lookup' } }, required: ['key'] } } }, { type: 'function', function: { name: 'list_verification_scripts', description: 'List known verification scripts from /scripts and summarize likely check families.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'run_maintenance_gate', description: 'Run one named maintenance check with bounded capture (id maps to a known verifier). Use run_command for arbitrary guest commands.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Allowlisted command id' }, cwd: { type: 'string', description: 'Optional working directory' }, timeout_ms: { type: 'integer', description: 'Timeout in milliseconds' } }, required: ['command'] } } }, { type: 'function', function: { name: 'run_contract_checks', description: 'Run a grouped set of contract checks by profile id (allowlisted) with bounded output.', parameters: { type: 'object', properties: { profile: { type: 'string', description: 'Check profile id, e.g. core, docs, parity' } }, required: ['profile'] } } }, { type: 'function', function: { name: 'summarize_build_drift', description: 'Summarize build/generated drift by comparing git status and key generated artifacts.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'get_hrpc_bridge_health', description: 'Read HRPC bridge/operator health from /proc/bare_os surfaces and include host capability hints when available.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'get_hrpc_allowlist_status', description: 'Inspect effective HRPC allowlist/probe status using hrpc probe and operator snapshots.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'discord_send_message', description: 'Send a Discord Direct Message on the open operator channel. Reach out anytime — do not wait for the user to message first. Defaults to the last whitelisted DM partner (or the sole whitelist id). Never sends to anyone off DISCORD_ID_WHITELIST. If the bot is offline the message is queued in ~/.discord/outbox.json.', parameters: { type: 'object', properties: { text: { type: 'string', description: 'Message body (Discord markdown). Max 4000 characters.' }, user_id: { type: 'string', description: 'Optional Discord user snowflake. Omit to use the open channel partner.' } }, required: ['text'] } } }, { type: 'function', function: { name: 'discord_channel_status', description: 'Show the Discord DM channel: partner user id, last inbound/outbound, pending outbox, inbox count, whether the bot client is live.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'discord_read_inbox', description: 'Read recent inbound Discord DMs stored on the channel (from the operator). Use when you need what they said while you were not in a turn.', parameters: { type: 'object', properties: { max: { type: 'integer', description: 'How many recent messages (default 12, max 50)' } } } } }, { type: 'function', function: { name: 'emit_host_notification', description: 'Request an audited host notification via HRPC route. Also fans out to the open Discord DM channel when one exists. Enabled by default; emergency_stop_mutations or a denylist can still block.', parameters: { type: 'object', properties: { title: { type: 'string' }, message: { type: 'string' }, level: { type: 'string', enum: ['info', 'warn', 'error'] } }, required: ['title', 'message'] } } }, { type: 'function', function: { name: 'request_host_action', description: 'Request a schema-validated host action through HRPC. Enabled by default; emergency_stop_mutations or a denylist can still block.', parameters: { type: 'object', properties: { action: { type: 'string', description: 'Host action id' }, payload: { type: 'object', description: 'Action payload object' } }, required: ['action'] } } }, { type: 'function', function: { name: 'autonomous_run', description: 'Start an autonomous coding run with a goal. Enables autonomous mode, keeps the ReAct loop going until task_complete, stop, or timebox. Optional scope path, runtime cap, and quality-gate ids.', parameters: { type: 'object', properties: { goal: { type: 'string', description: 'Task goal the agent should complete autonomously' }, scope_path: { type: 'string', description: 'Preferred working scope path (optional)' }, max_runtime_ms: { type: 'integer', description: 'Optional runtime cap override' }, required_checks: { type: 'array', items: { type: 'string' }, description: 'Optional quality gates (allowlisted check ids)' } }, required: ['goal'] } } }, { type: 'function', function: { name: 'autonomous_run_status', description: 'Return current autonomous run state, elapsed/runtime budget, configured checks, and latest status.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'autonomous_run_stop', description: 'Request manual stop for an autonomous run; loop will stop safely on next control checkpoint.', parameters: { type: 'object', properties: { reason: { type: 'string' } } } } }, { type: 'function', function: { name: 'verification_hints', description: 'Suggest npm/node verification commands for a developer working at the Bare OS git checkout on the host (paths or topic keywords). Does not run commands.', parameters: { type: 'object', properties: { topic: { type: 'string', description: 'Free-text task or area (e.g. kernel init, seed RPC, shell)' }, paths_touched: { type: 'string', description: 'Optional comma-separated path-like strings from the repo (forward slashes ok)' } } } } }, { type: 'function', function: { name: 'runtime_diagnostic_bundle', description: 'Non-secret snapshot: ctx API version, optional resource status, and every readable /proc/bare_os file. Prefer over many separate read_proc_file calls.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'search_replace', description: 'Replace old_string with new_string in a file. Match must be unique unless replace_all is true. Same uniqueness rules as Grok Build edit_file.', parameters: { type: 'object', properties: { path: { type: 'string' }, file_path: { type: 'string', description: 'Alias for path' }, old_string: { type: 'string' }, new_string: { type: 'string' }, replace_all: { type: 'boolean' } }, required: ['old_string', 'new_string'] } } }, { type: 'function', function: { name: 'glob_files', description: 'Walk the VFS from root and return paths matching a glob (supports ** and *). Prefer this over run_command find/ls.', parameters: { type: 'object', properties: { pattern: { type: 'string', description: 'Glob such as **/*.js or src/**/foo.md' }, root: { type: 'string', description: 'Directory to walk (default session home)' }, max_results: { type: 'integer', description: 'Default 100, cap 400' } }, required: ['pattern'] } } }, { type: 'function', function: { name: 'todo_write', description: 'Create or update the session todo list (Grok-style). merge=true updates by id; merge=false replaces the list.', parameters: { type: 'object', properties: { todos: { type: 'array', description: 'Items with id, optional content, status pending|in_progress|completed|cancelled', items: { type: 'object', properties: { id: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' } }, required: ['id'] } }, merge: { type: 'boolean', description: 'Default true: update matching ids, keep others' } }, required: ['todos'] } } }, { type: 'function', function: { name: 'memory_search', description: 'Search ~/.agent/workspace/memory, MEMORY.md, and compact.md by keyword (guest-safe, no embeddings).', parameters: { type: 'object', properties: { query: { type: 'string' }, max_hits: { type: 'integer', description: 'Default 8' } }, required: ['query'] } } }, { type: 'function', function: { name: 'memory_get', description: 'Read one memory file. Path must be under ~/.agent/workspace/memory or a named file there.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path or basename under workspace/memory' }, max_bytes: { type: 'integer' } }, required: ['path'] } } }, { type: 'function', function: { name: 'enter_plan_mode', description: 'Switch to read-only plan mode. Writes are denied except ~/.agent/plan.md until exit_plan_mode.', parameters: { type: 'object', properties: { note: { type: 'string', description: 'Optional starter text appended to plan.md' } } } } }, { type: 'function', function: { name: 'exit_plan_mode', description: 'Leave plan mode and allow mutating tools again.', parameters: { type: 'object', properties: { summary: { type: 'string' } } } } }, { type: 'function', function: { name: 'ask_user_question', description: 'Pose one or more multiple-choice questions to the user (Grok-style). Persists to ~/.agent/ask.json.', parameters: { type: 'object', properties: { questions: { type: 'array', items: { type: 'object', properties: { question: { type: 'string' }, options: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, description: { type: 'string' } } } }, multi_select: { type: 'boolean' } }, required: ['question'] } } }, required: ['questions'] } } }, { type: 'function', function: { name: 'grep', description: 'Search file contents with a JS regular expression (Grok-style, VFS-native). Prefer this over run_command grep. Supports glob, ignore_case, context lines, and files_with_matches.', parameters: { type: 'object', properties: { pattern: { type: 'string', description: 'JavaScript regular expression (no surrounding slashes)' }, path: { type: 'string', description: 'Directory or file to search (default session home)' }, root: { type: 'string', description: 'Alias for path' }, glob: { type: 'string', description: 'Optional file glob such as **/*.js' }, ignore_case: { type: 'boolean' }, before: { type: 'integer', description: 'Context lines before each match' }, after: { type: 'integer', description: 'Context lines after each match' }, context: { type: 'integer', description: 'Context lines before and after' }, max_matches: { type: 'integer', description: 'Default 50, cap 200' }, files_with_matches: { type: 'boolean', description: 'Return matching paths only' }, output_mode: { type: 'string', description: 'content (default) | files_with_matches | count' } }, required: ['pattern'] } } }, { type: 'function', function: { name: 'apply_patch', description: 'Apply a Codex/Grok multi-file patch (*** Begin Patch … *** End Patch). Supports *** Add File, *** Delete File, *** Update File, optional *** Move to, and @@ hunks with + / - / context lines. Prefer this for multi-hunk or multi-file edits.', parameters: { type: 'object', properties: { patch: { type: 'string', description: 'Full apply_patch document' }, input: { type: 'string', description: 'Alias for patch' } }, required: ['patch'] } } }, { type: 'function', function: { name: 'update_goal', description: 'Report progress on the current autonomous/session goal. completed=true ends the goal. blocked_reason is a failure signal after 3+ failed attempts — never use it for success.', parameters: { type: 'object', properties: { completed: { type: 'boolean' }, message: { type: 'string', description: 'Short progress or completion note' }, blocked_reason: { type: 'string' } } } } }, { type: 'function', function: { name: 'web_search', description: 'Search the public web (DuckDuckGo instant answers via the same HTTP policy as web_fetch / wget). Use for current facts, docs, and error lookup. Then web_fetch promising URLs.', parameters: { type: 'object', properties: { query: { type: 'string' }, max_results: { type: 'integer', description: 'Default 8, cap 16' } }, required: ['query'] } } }, { type: 'function', function: { name: 'git_status', description: 'Guest git status --short plus diff --stat (isomorphic-git /bin/git). Prefer this over parsing git via run_command when you only need a snapshot.', parameters: { type: 'object', properties: { cwd: { type: 'string', description: 'Repo directory (default PWD or home)' } } } } }, { type: 'function', function: { name: 'memory_append', description: 'Append a durable FACT / CHECK / RISK / HANDOFF line to MEMORY.md or today\'s daily log. Prefer this over write_file for memory.', parameters: { type: 'object', properties: { text: { type: 'string', description: 'What to remember' }, kind: { type: 'string', enum: ['FACT', 'CHECK', 'RISK', 'HANDOFF', 'NOTE'], description: 'Ledger tag (default NOTE)' }, daily: { type: 'boolean', description: 'Append to memory/YYYY-MM-DD.md instead of MEMORY.md' } }, required: ['text'] } } }, { type: 'function', function: { name: 'list_skills', description: 'List discovered SKILL.md ids from workspace/skills and ~/.agent/skills. Use read_skill to load one.', parameters: { type: 'object', properties: { max: { type: 'integer', description: 'Max entries (default 40)' } } } } }, { type: 'function', function: { name: 'list_agent_tools', description: 'Return the live callable tool registry (name, description, parameter keys). Use this when the user asks what tools you have. Skills and /bin names are not in this list.', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Optional tool name or substring to describe one entry' } } } } }, { type: 'function', function: { name: 'schedule_task', description: 'Create or replace a guest timer that re-runs the agent (Grok scheduler, mapped to ~/.config/bare-os/timers). Interval like 5m, 2h, 1d, or five cron fields. Max 8 timers on the guest.', parameters: { type: 'object', properties: { id: { type: 'string', description: 'Timer id (stored as agent-.timer)' }, interval: { type: 'string', description: '5m, 2h, 1d, or cron (min hour day month dow)' }, prompt: { type: 'string', description: 'Task the agent should run on each fire' }, auto: { type: 'boolean', description: 'Pass --auto (default true)' } }, required: ['interval', 'prompt'] } } }, { type: 'function', function: { name: 'unschedule_task', description: 'Delete a guest agent timer by id (agent-.timer).', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } }, { type: 'function', function: { name: 'list_scheduled', description: 'List guest agent timers (agent-*.timer) under ~/.config/bare-os/timers, with previews.', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'fuzzy_find', description: 'Fuzzy-search file names under a root (Grok-style). Prefer this when you remember part of a filename.', parameters: { type: 'object', properties: { query: { type: 'string' }, root: { type: 'string', description: 'Directory to walk (default home)' }, max: { type: 'integer', description: 'Default 20, cap 80' } }, required: ['query'] } } }, { type: 'function', function: { name: 'read_many', description: 'Read several UTF-8 files in one call (bounded). Prefer over many read_file calls for a small set.', parameters: { type: 'object', properties: { paths: { type: 'array', items: { type: 'string' } }, max_bytes_each: { type: 'integer', description: 'Default 32000' } }, required: ['paths'] } } }, { type: 'function', function: { name: 'wait_for', description: 'Poll a file or guest command until a regex matches, or timeout (Grok monitor lite; sync).', parameters: { type: 'object', properties: { pattern: { type: 'string', description: 'JS regex' }, path: { type: 'string', description: 'File to poll' }, command: { type: 'string', description: 'Guest command whose output is polled' }, timeout_ms: { type: 'integer', description: 'Default 15000, cap 120000' }, interval_ms: { type: 'integer', description: 'Default 400, min 100' }, ignore_case: { type: 'boolean' } }, required: ['pattern'] } } }, { type: 'function', function: { name: 'undo_last_edit', description: 'Restore the last file snapshot from ~/.agent/edits.json (write_file / edit / apply_patch / delete).', parameters: { type: 'object', properties: {} } } }, { type: 'function', function: { name: 'git_diff', description: 'git diff (optional path / --stat) via /bin/git. Prefer over raw run_command.', parameters: { type: 'object', properties: { cwd: { type: 'string' }, path: { type: 'string' }, stat: { type: 'boolean', description: 'Only --stat (default false)' } } } } }, { type: 'function', function: { name: 'history_search', description: 'Search this session\'s ~/.agent/history.json by keyword.', parameters: { type: 'object', properties: { query: { type: 'string' }, max: { type: 'integer' } }, required: ['query'] } } }, { type: 'function', function: { name: 'git_log', description: 'git log --oneline (optional -N / path) via /bin/git. Prefer over raw run_command.', parameters: { type: 'object', properties: { cwd: { type: 'string' }, max: { type: 'integer', description: 'Default 20, cap 80' }, path: { type: 'string' } } } } }, { type: 'function', function: { name: 'git_show', description: 'git show (optional path / --stat) via /bin/git.', parameters: { type: 'object', properties: { cwd: { type: 'string' }, rev: { type: 'string', description: 'Commit-ish (default HEAD)' }, path: { type: 'string' }, stat: { type: 'boolean' } } } } }, { type: 'function', function: { name: 'git_blame', description: 'git blame a file via /bin/git.', parameters: { type: 'object', properties: { cwd: { type: 'string' }, path: { type: 'string' } }, required: ['path'] } } }, { type: 'function', function: { name: 'copy_path', description: 'Copy a file or directory on the VFS (parents created). Prefer this over run_command cp.', parameters: { type: 'object', properties: { from_path: { type: 'string' }, to_path: { type: 'string' } }, required: ['from_path', 'to_path'] } } }, { type: 'function', function: { name: 'diff_files', description: 'Unified diff of two UTF-8 files (no git). Prefer this when comparing two paths that are not a git hunk.', parameters: { type: 'object', properties: { from_path: { type: 'string' }, to_path: { type: 'string' }, context: { type: 'integer', description: 'Context lines (default 3)' } }, required: ['from_path', 'to_path'] } } }, { type: 'function', function: { name: 'find_symbol', description: 'Find likely definitions of a symbol (function/class/const/def/fn) under a root. Prefer over ad-hoc grep for go-to-definition.', parameters: { type: 'object', properties: { name: { type: 'string' }, root: { type: 'string' }, glob: { type: 'string' }, max: { type: 'integer' } }, required: ['name'] } } }, { type: 'function', function: { name: 'create_skill', description: 'Write a Grok-style SKILL.md (YAML frontmatter + body) under ~/.agent/workspace/skills//.', parameters: { type: 'object', properties: { id: { type: 'string', description: 'Folder name (kebab-case)' }, name: { type: 'string' }, description: { type: 'string' }, body: { type: 'string', description: 'Markdown instructions after frontmatter' } }, required: ['id', 'description'] } } }, { type: 'function', function: { name: 'remember', description: 'Save a durable FACT to MEMORY.md now (Grok /remember). Same store as memory_append.', parameters: { type: 'object', properties: { text: { type: 'string' }, daily: { type: 'boolean' } }, required: ['text'] } } }, { type: 'function', function: { name: 'rewind_session', description: 'Grok /rewind: drop the last N user turns from ~/.agent/history.json (and everything after). Default 1 turn.', parameters: { type: 'object', properties: { steps: { type: 'integer', description: 'How many user turns to drop (default 1)' }, user_index: { type: 'integer', description: 'Rewind to this 0-based user turn instead' }, keep_user: { type: 'boolean', description: 'Keep the target user prompt' } } } } }, { type: 'function', function: { name: 'export_session', description: 'Grok /export: write the current chat history as Markdown. Default ~/.agent/export.md.', parameters: { type: 'object', properties: { path: { type: 'string' } } } } }, { 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'] } } } ] } /** * Live tool registry derived from bareAgentToolDefinitions (never hand-maintained). * @param {unknown[]} [tools] * @returns {{ name: string, description: string, params: string[] }[]} */ function bareAgentToolCatalog(tools) { const src = Array.isArray(tools) && tools.length ? tools : bareAgentToolDefinitions() /** @type {{ name: string, description: string, params: string[] }[]} */ const out = [] /** @type {Record} */ const seen = {} for (const t of src) { if (!t || typeof t !== 'object') continue const o = /** @type {Record} */ (t) const fn = o.function && typeof o.function === 'object' ? /** @type {Record} */ (o.function) : o const name = typeof fn.name === 'string' ? fn.name.trim() : '' if (!name || seen[name]) continue seen[name] = 1 const description = typeof fn.description === 'string' ? fn.description.replace(/\s+/g, ' ').trim() : '' const paramsRoot = fn.parameters && typeof fn.parameters === 'object' ? /** @type {Record} */ (fn.parameters) : {} const props = paramsRoot.properties && typeof paramsRoot.properties === 'object' && !Array.isArray(paramsRoot.properties) ? /** @type {Record} */ (paramsRoot.properties) : {} out.push({ name, description, params: Object.keys(props) }) } return out } /** * System-prompt block. Placed last so small local models see the real tool * names instead of the skills index. * @param {unknown[]} [tools] */ function bareAgentToolCatalogPrompt(tools) { const cat = bareAgentToolCatalog(tools) const lines = [ 'AVAILABLE TOOLS (' + cat.length + ' callable functions — live registry)', 'These names are the only functions you can call. Skills such as ctx-api-change, docs-contract-update, hdms, and holesail are playbooks: load them with read_skill. Guest /bin names such as ctx-baredoctor, hdms, curl, and holepunch are commands for run_command, not tools. curl/wget/ip are run_command — never run_js_script.', 'When the user asks what tools you have or what you can do, list every name below. Do not invent names. Do not list skills as tools.', 'To call a tool, emit exactly:', '{"name":"TOOL_NAME","arguments":{}}', 'You may call list_agent_tools to reprint this catalog.', '' ] for (const t of cat) { const desc = t.description.length > 110 ? t.description.slice(0, 107) + '...' : t.description const params = t.params.length ? ' (' + t.params.join(', ') + ')' : '' lines.push( '- ' + t.name + params + (desc ? ' — ' + desc : '') ) } return lines.join('\n') } /** Tool dispatch for /bin/agent (preamble; no import). */ /** * @param {{ * ctx: Record, * toolName: string, * argsJson: string, * paths: { dir: string, config: string, cmdOut: string, workspace?: string, workspaceSkills?: string, skillsGlobal?: 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 internalGate = Boolean(o.internalGate) 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 cfgNow = configRef.current || {} const resolvedHome = String(home || '').trim() || (typeof bareAgentResolveHome === 'function' ? bareAgentResolveHome(ctx) : '') || '/home/guest' if (typeof bareAgentExpandGuestPath === 'function') { const PATH_KEYS = [ 'path', 'file_path', 'file', 'filename', 'dir', 'directory', 'folder', 'root', 'cwd', 'from_path', 'to_path', 'from', 'to', 'src', 'source', 'dest', 'destination', 'target', 'old_path', 'new_path' ] let cwdForExpand = resolvedHome if (paths && paths.lastCwd && typeof bareAgentReadTextFile === 'function') { try { const t = String(await bareAgentReadTextFile(ctx, paths.lastCwd) || '').trim() if (t) cwdForExpand = t } catch { /* optional */ } } if (typeof args.cwd === 'string' && args.cwd.trim()) { args.cwd = bareAgentExpandGuestPath(args.cwd, resolvedHome, resolvedHome) cwdForExpand = String(args.cwd || cwdForExpand) } for (let i = 0; i < PATH_KEYS.length; i++) { const k = PATH_KEYS[i] if (k === 'cwd') continue if (typeof args[k] === 'string' && args[k].trim()) { args[k] = bareAgentExpandGuestPath(args[k], resolvedHome, cwdForExpand) } } } if ( !internalGate && cfgNow.autonomous_active && Array.isArray(cfgNow.autonomous_deny_ops) && cfgNow.autonomous_deny_ops.map((x) => String(x)).includes(toolName) ) { return bareAgentJsonResult({ ok: false, error: 'autonomous_op_denied', tool: toolName }) } if ( !internalGate && cfgNow.plan_mode_active && typeof bareAgentPlanModeToolAllowed === 'function' && !bareAgentPlanModeToolAllowed(toolName, args, paths) ) { return bareAgentJsonResult({ ok: false, error: 'plan_mode_readonly', tool: toolName, hint: 'exit_plan_mode before mutating, or write only ' + String(paths.plan || '~/.agent/plan.md') }) } if (!internalGate && typeof bareAgentRunPreToolHooks === 'function' && paths.hooks) { const hookReason = await bareAgentRunPreToolHooks(ctx, paths.hooks, toolName, args) if (hookReason) { return bareAgentJsonResult({ ok: false, error: 'hook_denied', tool: toolName, reason: hookReason }) } } if (toolName === 'list_dir') { return bareAgentDispatchTool({ ...o, toolName: 'list_directory' }) } if (toolName === 'glob') { return bareAgentDispatchTool({ ...o, toolName: 'glob_files' }) } if (toolName === 'ripgrep' || toolName === 'rg') { return bareAgentDispatchTool({ ...o, toolName: 'grep' }) } if (toolName === 'remember') { return bareAgentDispatchTool({ ...o, toolName: 'memory_append', argsJson: JSON.stringify({ ...args, kind: 'FACT' }) }) } const toolNameKey = String(toolName || '') .trim() .toLowerCase() .replace(/[\s-]+/g, '_') const runCommandAliases = { bash: 1, shell: 1, sh: 1, zsh: 1, fish: 1, cli: 1, exec: 1, execute: 1, terminal: 1, run: 1, cmd: 1, command: 1, run_cmd: 1, run_shell: 1, execute_command: 1, execute_bash: 1, shell_command: 1 } if (runCommandAliases[toolNameKey]) { const cmd = (typeof args.command === 'string' && args.command.trim()) || (typeof args.cmd === 'string' && args.cmd.trim()) || (typeof args.code === 'string' && args.code.trim()) || (typeof args.script === 'string' && args.script.trim()) || (typeof args.input === 'string' && args.input.trim()) || (typeof args.value === 'string' && args.value.trim()) || (typeof args.query === 'string' && args.query.trim()) || '' /** @type {Record} */ const next = { command: String(cmd) } if (typeof args.cwd === 'string' && args.cwd.trim()) next.cwd = args.cwd.trim() if (typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)) { next.timeout_ms = args.timeout_ms } if (args.capture_exit) next.capture_exit = args.capture_exit return bareAgentDispatchTool({ ...o, toolName: 'run_command', argsJson: JSON.stringify(next) }) } const vfs = ctx.vfs const mutateDenyPrefixes = (function () { const cfg = configRef.current || {} if (Array.isArray(cfg.mutate_deny_prefixes) && cfg.mutate_deny_prefixes.length) { return cfg.mutate_deny_prefixes.map((x) => String(x || '')).filter(Boolean) } return typeof BARE_AGENT_MUTATE_DENY_PREFIXES !== 'undefined' ? BARE_AGENT_MUTATE_DENY_PREFIXES : ['/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run'] })() const AUTONOMOUS_CHECK_ALLOW = { 'coreutils-test': 'npm test -w bare-os-coreutils', 'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs', 'verify-man-coverage': 'node scripts/verify-man-coverage.mjs', 'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs' } 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 trimmed = String(line || '').trim() const compoundShell = /\n/.test(trimmed) || /(^|[;\s])(for|if|while|until|case|function)\b/.test(trimmed) || /\b(do|done|then|else|fi|esac)\b/.test(trimmed) || /&&|\|\||\(\(|\{|\}/.test(trimmed) /** * Do not wrap with `{ cmd ; }` — Bare OS `splitTokensBySemicolon` splits on every `;` * at depth 0 and does not treat `{ … }` as a compound, so `{` became argv[0] * (`unknown command: {`). Redirect only; read exit from env after `execLine`. */ const wrapped = (compoundShell ? 'sh -c ' + bareAgentShellQuote(trimmed) : 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 */ } if (captureExit) { const env = ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null const rawEc = env && env.BARE_OS_EXIT_STATUS != null && env.BARE_OS_EXIT_STATUS !== '' ? env.BARE_OS_EXIT_STATUS : ctx.exitCode const n = Number(rawEc) const codeStr = String(Number.isFinite(n) ? n : 0) const exitLine = '\nEXIT:' + codeStr + '\n' captured += exitLine try { if (vfs?.readFile && vfs?.writeFile && ctx.b4a && typeof ctx.b4a.concat === 'function') { let prev = await vfs.readFile(outPath) const prevBytes = prev && prev.length ? prev instanceof Uint8Array ? prev : ctx.b4a.from(prev) : ctx.b4a.from('') await vfs.writeFile( outPath, ctx.b4a.concat([prevBytes, ctx.b4a.from(exitLine)]) ) } } catch { /* ignore */ } } const max = 120_000 if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated' return { ok: true, stdout_stderr: captured } } /** * @param {string} text */ function redactSensitiveText(text) { return String(text || '') .replace(/\bBearer\s+\S+/gi, 'Bearer ') .replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '') .replace( /\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=' ) } /** * @param {string} path * @param {number} maxChars * @param {boolean} tailOnly * @param {boolean} redact */ async function readBoundedText(path, maxChars, tailOnly, redact) { if (!vfs || typeof vfs.readFile !== 'function') { return { ok: false, error: 'vfs unavailable' } } try { const b = await vfs.readFile(path) if (!b || !b.length) return { ok: false, error: 'empty_or_missing' } let t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (redact) t = redactSensitiveText(t) const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars) return { ok: true, path, text: out, truncated: t.length > out.length } } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return { ok: false, error: msg } } } /** * @param {string} p */ function autonomousPathAllowed(p) { const s = String(p || '').trim() if (!s) return true return bareAgentPathAllowed(s) } /** * @param {string} p * @param {'read' | 'mutate'} [kind] */ function enforceAutonomousPath(p, kind) { const cfg = configRef.current || {} const path = String(p || '').trim() if (!path) return true if (kind === 'mutate') { if ( typeof bareAgentPathAllowedMutate === 'function' && !bareAgentPathAllowedMutate(path, mutateDenyPrefixes) ) { return false } } else if (!bareAgentPathAllowed(path)) { return false } if (!cfg.autonomous_active) return true if (kind !== 'mutate') return true const allowList = Array.isArray(cfg.autonomous_allow_paths) ? cfg.autonomous_allow_paths.map((x) => String(x || '').trim()).filter(Boolean) : [] if (!allowList.length || allowList.includes('*')) return true for (const pref of allowList) { if (path === pref || path.startsWith(pref.endsWith('/') ? pref : pref + '/')) return true } return false } try { if (toolName === 'read_skill') { const skill = typeof args.skill === 'string' ? args.skill.trim() : '' const maxB = typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes) ? Math.min(Math.floor(args.max_bytes), 512_000) : 256_000 if (!skill) { return bareAgentJsonResult({ ok: false, error: 'skill_required' }) } const skillPaths = { workspaceSkills: typeof paths.workspaceSkills === 'string' ? paths.workspaceSkills : paths.dir + '/workspace/skills', skillsGlobal: typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills' } appendProgress('read_skill ' + skill) const loaded = await bareAgentLoadSkillMarkdown(ctx, skillPaths, skill) if (!loaded.ok) { return bareAgentJsonResult({ ok: false, error: loaded.error || 'load_failed', skill }) } let content = loaded.content if (content.length > maxB) content = content.slice(0, maxB) + '\n… truncated' return bareAgentJsonResult({ ok: true, id: loaded.id, name: loaded.skill, path: loaded.path, source: loaded.source, content }) } if (toolName === 'glob_files') { const pattern = typeof args.pattern === 'string' ? args.pattern.trim() : '' if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' }) const root = typeof args.root === 'string' && args.root.trim() ? args.root.trim() : home || '/home' if (!bareAgentPathAllowed(root)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } const maxResults = typeof args.max_results === 'number' && Number.isFinite(args.max_results) ? Math.min(Math.max(Math.floor(args.max_results), 1), 400) : 100 if (typeof bareAgentGlobFiles !== 'function') { return bareAgentJsonResult({ ok: false, error: 'glob_unavailable' }) } appendProgress('glob_files ' + pattern + ' @ ' + root) const files = await bareAgentGlobFiles(ctx, root, pattern, { maxFiles: Math.max(maxResults * 4, 200), maxDepth: 10 }) return bareAgentJsonResult({ ok: true, root, pattern, count: Math.min(files.length, maxResults), truncated: files.length > maxResults, files: files.slice(0, maxResults) }) } if (toolName === 'todo_write') { const todosPath = paths.todos || paths.dir + '/todos.json' const merge = args.merge !== false const prev = typeof bareAgentLoadTodos === 'function' ? await bareAgentLoadTodos(ctx, todosPath) : [] let next try { next = bareAgentTodoApply(args.todos, { merge }, prev) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } if (typeof bareAgentSaveTodos === 'function') { await bareAgentSaveTodos(ctx, todosPath, next) } const summary = bareAgentTodoSummarize(next) appendProgress('todo_write open=' + String(summary.open) + '/' + String(summary.total)) return bareAgentJsonResult({ ok: true, merge, todos: next, summary }) } if (toolName === 'memory_search') { const query = typeof args.query === 'string' ? args.query.trim() : '' if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' }) const maxHits = typeof args.max_hits === 'number' && Number.isFinite(args.max_hits) ? Math.min(Math.max(Math.floor(args.max_hits), 1), 24) : 8 const memDir = typeof paths.workspaceMemory === 'string' ? paths.workspaceMemory : paths.dir + '/workspace/memory' const workspace = typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace' const seeds = [] if (typeof bareAgentVfsWalkFiles === 'function') { const walked = await bareAgentVfsWalkFiles(ctx, memDir, { maxFiles: 80, maxDepth: 4 }) for (let i = 0; i < walked.length; i++) seeds.push(walked[i]) } seeds.push(workspace + '/MEMORY.md') if (paths.compact) seeds.push(paths.compact) else seeds.push(paths.dir + '/compact.md') const uniq = [] const seenMem = Object.create(null) for (let i = 0; i < seeds.length; i++) { if (seenMem[seeds[i]]) continue seenMem[seeds[i]] = 1 uniq.push(seeds[i]) } appendProgress('memory_search ' + query.slice(0, 80)) const hits = typeof bareAgentMemorySearchFiles === 'function' ? await bareAgentMemorySearchFiles(ctx, uniq, query, { maxHits }) : [] return bareAgentJsonResult({ ok: true, query, hits }) } if (toolName === 'memory_get') { const rawPath = typeof args.path === 'string' ? args.path.trim() : '' if (!rawPath) return bareAgentJsonResult({ ok: false, error: 'path_required' }) const memDir = typeof paths.workspaceMemory === 'string' ? paths.workspaceMemory : paths.dir + '/workspace/memory' const workspace = typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace' let path = rawPath if (!path.startsWith('/')) path = memDir + '/' + path.replace(/^\/+/, '') const allowed = path === workspace + '/MEMORY.md' || path === (paths.compact || paths.dir + '/compact.md') || path === memDir || path.indexOf(memDir + '/') === 0 if (!allowed) { return bareAgentJsonResult({ ok: false, error: 'memory_path_denied', hint: 'path must be under ' + memDir }) } const maxB = typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes) ? Math.min(Math.floor(args.max_bytes), 256_000) : 64_000 appendProgress('memory_get ' + path) let text = await bareAgentReadTextFile(ctx, path) if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing', path }) const truncated = text.length > maxB if (truncated) text = text.slice(0, maxB) + '\n… truncated' return bareAgentJsonResult({ ok: true, path, content: text, truncated }) } if (toolName === 'enter_plan_mode') { const planPath = paths.plan || paths.dir + '/plan.md' configRef.current = { ...(configRef.current || {}), plan_mode_active: true } if (typeof bareAgentSaveConfigFromTools === 'function') { await bareAgentSaveConfigFromTools(ctx, paths, configRef.current) } const note = typeof args.note === 'string' ? args.note.trim() : '' let prev = '' try { prev = await bareAgentReadTextFile(ctx, planPath) } catch { prev = '' } if (!prev.trim()) { const starter = '# Plan\n\n' + (note ? note + '\n' : '- Investigate with read-only tools.\n- Write the plan here.\n- Call exit_plan_mode when ready to implement.\n') await bareAgentWriteTextFile(ctx, planPath, starter) } else if (note) { await bareAgentWriteTextFile(ctx, planPath, prev.replace(/\s*$/, '') + '\n\n' + note + '\n') } appendProgress('enter_plan_mode ' + planPath) return bareAgentJsonResult({ ok: true, plan_mode_active: true, plan: planPath }) } if (toolName === 'exit_plan_mode') { const summary = typeof args.summary === 'string' ? args.summary.trim() : '' configRef.current = { ...(configRef.current || {}), plan_mode_active: false } if (typeof bareAgentSaveConfigFromTools === 'function') { await bareAgentSaveConfigFromTools(ctx, paths, configRef.current) } if (summary && paths.plan) { const prev = await bareAgentReadTextFile(ctx, paths.plan) await bareAgentWriteTextFile( ctx, paths.plan, (prev ? prev.replace(/\s*$/, '') + '\n\n' : '') + '## Exit\n' + summary + '\n' ) } appendProgress('exit_plan_mode') return bareAgentJsonResult({ ok: true, plan_mode_active: false, summary: summary || null }) } if (toolName === 'ask_user_question') { const rows = Array.isArray(args.questions) ? args.questions : [] if (!rows.length) { return bareAgentJsonResult({ ok: false, error: 'questions_required' }) } /** @type {Record[]} */ const questions = [] for (let i = 0; i < rows.length; i++) { const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {} const question = String(row.question || '').trim() if (!question) continue const opts = Array.isArray(row.options) ? row.options : [] questions.push({ question, multi_select: Boolean(row.multi_select), options: opts.map(function (opt) { if (opt && typeof opt === 'object') { return { label: String(opt.label || ''), description: String(opt.description || '') } } return { label: String(opt || ''), description: '' } }) }) } if (!questions.length) { return bareAgentJsonResult({ ok: false, error: 'questions_required' }) } const askPath = paths.ask || paths.dir + '/ask.json' const payload = { asked_at: new Date().toISOString(), questions } if (typeof bareAgentWriteJsonFile === 'function') { await bareAgentWriteJsonFile(ctx, askPath, payload) } const text = questions .map(function (q, i) { const opts = Array.isArray(q.options) ? q.options .map(function (o, j) { return ( ' ' + String(j + 1) + ') ' + String(o.label || '') + (o.description ? ' — ' + String(o.description) : '') ) }) .join('\n') : '' return ( String(i + 1) + '. ' + q.question + (q.multi_select ? ' (multi-select)' : '') + (opts ? '\n' + opts : '') ) }) .join('\n') appendProgress('ask_user_question n=' + String(questions.length)) return bareAgentJsonResult({ ok: true, ask: askPath, questions, text, hint: 'Wait for the user to answer these questions on the next turn.' }) } if (toolName === 'task_complete') { const summary = typeof args.summary === 'string' ? args.summary : '' if ( typeof bareAgentLooksLikeFailedTaskComplete === 'function' && bareAgentLooksLikeFailedTaskComplete(summary) ) { appendProgress('task_complete_rejected_failure') return bareAgentJsonResult({ ok: false, error: 'task_not_complete', hint: 'Do not call task_complete after a failed tool. Keep going. Guest CLI (curl, wget, ip, ping, ls) is run_command({command:"..."}). Never wrap curl in run_js_script. Call run_command now and report stdout.', summary }) } appendProgress('task_complete: ' + summary.slice(0, 200)) onTaskComplete(summary || '(done)') return bareAgentJsonResult({ ok: true, completed: true, summary }) } if (toolName === 'autonomous_run') { const goal = typeof args.goal === 'string' ? args.goal.trim() : '' const scopePath = typeof args.scope_path === 'string' ? args.scope_path.trim() : '' const cfg = configRef.current || {} if (!goal) return bareAgentJsonResult({ ok: false, error: 'goal_required' }) if (scopePath && !autonomousPathAllowed(scopePath)) { return bareAgentJsonResult({ ok: false, error: 'scope_path_denied' }) } const maxRuntimeMsRaw = typeof args.max_runtime_ms === 'number' && Number.isFinite(args.max_runtime_ms) ? args.max_runtime_ms : cfg.autonomous_max_runtime_ms const requiredChecks = Array.isArray(args.required_checks) ? args.required_checks.map((x) => String(x || '').trim()).filter(Boolean) : [] const unknown = requiredChecks.filter((x) => !Object.prototype.hasOwnProperty.call(AUTONOMOUS_CHECK_ALLOW, x)) if (unknown.length) { return bareAgentJsonResult({ ok: false, error: 'unknown_required_checks', unknown, allowlist: Object.keys(AUTONOMOUS_CHECK_ALLOW) }) } const started = typeof bareAgentBeginAutonomousRun === 'function' ? bareAgentBeginAutonomousRun(cfg, { goal, maxRuntimeMs: maxRuntimeMsRaw, requiredChecks }) : { ...cfg, autonomous_mode_enabled: true, autonomous_active: true, autonomous_stop_requested: false, autonomous_started_at_ms: Date.now(), autonomous_goal: goal, autonomous_status: 'running', autonomous_last_error: '', autonomous_max_runtime_ms: Math.min( Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000), 7_200_000 ), autonomous_completion_required_checks: requiredChecks } const merged = bareAgentMergeConfigPatch(cfg, started) await bareAgentSaveConfigFromTools(ctx, paths, merged) configRef.current = merged appendProgress('autonomous_run start goal=' + goal.slice(0, 160)) return bareAgentJsonResult({ ok: true, active: true, enabled: true, goal, scope_path: scopePath || null, max_runtime_ms: merged.autonomous_max_runtime_ms, required_checks: requiredChecks, status: 'running' }) } if (toolName === 'autonomous_run_status') { const cfg = configRef.current || {} const started = Number(cfg.autonomous_started_at_ms) || 0 const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0 const maxRuntime = Number(cfg.autonomous_max_runtime_ms) || 0 return bareAgentJsonResult({ ok: true, autonomous_mode_enabled: Boolean(cfg.autonomous_mode_enabled), active: Boolean(cfg.autonomous_active), stop_requested: Boolean(cfg.autonomous_stop_requested), goal: String(cfg.autonomous_goal || ''), status: String(cfg.autonomous_status || 'idle'), last_error: String(cfg.autonomous_last_error || ''), started_at_ms: started, elapsed_ms: elapsed, max_runtime_ms: maxRuntime, remaining_ms: maxRuntime > 0 ? Math.max(0, maxRuntime - elapsed) : 0, required_checks: Array.isArray(cfg.autonomous_completion_required_checks) ? cfg.autonomous_completion_required_checks : [] }) } if (toolName === 'autonomous_run_stop') { const cfg = configRef.current || {} const reason = typeof args.reason === 'string' ? args.reason.trim() : '' const stopped = typeof bareAgentStopAutonomousRun === 'function' ? bareAgentStopAutonomousRun(cfg, reason) : { ...cfg, autonomous_stop_requested: true, autonomous_status: 'stopping', autonomous_last_error: reason || String(cfg.autonomous_last_error || '') } const merged = bareAgentMergeConfigPatch(cfg, stopped) await bareAgentSaveConfigFromTools(ctx, paths, merged) configRef.current = merged appendProgress('autonomous_run_stop ' + (reason || 'requested')) return bareAgentJsonResult({ ok: true, stop_requested: true, reason: reason || null }) } if (toolName === 'read_file') { const path = typeof bareAgentToolPathArg === 'function' ? bareAgentToolPathArg(args) : typeof args.path === 'string' ? args.path : '' if (!enforceAutonomousPath(path, 'read')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } 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' const hasSlice = (typeof args.offset === 'number' && Number.isFinite(args.offset)) || (typeof args.limit === 'number' && Number.isFinite(args.limit)) if (hasSlice && typeof bareAgentSliceFileLines === 'function') { const sliced = bareAgentSliceFileLines(t, { offset: typeof args.offset === 'number' && Number.isFinite(args.offset) ? args.offset : 1, limit: typeof args.limit === 'number' && Number.isFinite(args.limit) ? args.limit : undefined, numbered: args.numbered !== false }) return bareAgentJsonResult({ ok: true, path, ...sliced }) } return bareAgentJsonResult({ ok: true, path, content: t }) } if (toolName === 'write_file') { const path = (typeof args.path === 'string' && args.path.trim()) || (typeof args.file_path === 'string' && args.file_path.trim()) || (typeof args.file === 'string' && args.file.trim()) || (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || '' if (!enforceAutonomousPath(path, 'mutate')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } const content = typeof args.content === 'string' ? args.content : typeof args.contents === 'string' ? args.contents : typeof args.text === 'string' ? args.text : typeof args.body === 'string' ? args.body : '' if (!path.startsWith('/') || path.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'bad_path', path, hint: 'Use an absolute path or ~/... (tilde expands to ' + resolvedHome + '). Example: ' + resolvedHome + '/test_work/hello.js' }) } if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('write_file ' + path) if (typeof bareAgentPushEdit === 'function' && paths.edits) { try { const prev = await bareAgentReadTextFile(ctx, path) await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'write_file' }) } catch { /* new file */ } } 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) let verified = false try { if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) else if (typeof vfs.readFile === 'function') { const got = await vfs.readFile(path) verified = got != null } else verified = true } catch { verified = false } if (!verified) { return bareAgentJsonResult({ ok: false, error: 'write_not_visible', path, hint: 'write_file returned but the path is not visible in the VFS. Retry with the expanded absolute path.' }) } return bareAgentJsonResult({ ok: true, path, bytes: body.length, verified: true }) } if (toolName === 'edit_file' || toolName === 'search_replace') { const path = typeof bareAgentToolPathArg === 'function' ? bareAgentToolPathArg(args) : typeof args.path === 'string' ? args.path : '' if (!enforceAutonomousPath(path, 'mutate')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs?.readFile || !vfs?.writeFile) { return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' }) } appendProgress(toolName + ' ' + path) const buf = await vfs.readFile(path) if (typeof bareAgentPushEdit === 'function' && paths.edits) { try { const snap = buf && buf.length ? typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) : '' await bareAgentPushEdit(ctx, paths.edits, { path, prev: snap, tool: toolName }) } catch { /* ignore */ } } 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 = toolName === 'search_replace' ? '' : 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 : '' const replaceAll = Boolean(args.replace_all) let next = prev let replacements = 0 if (full.length > 0) { next = full replacements = 1 } else if (typeof bareAgentSearchReplaceApply === 'function') { const applied = bareAgentSearchReplaceApply(prev, oldStr, newStr, replaceAll) if (!applied.ok) { return bareAgentJsonResult({ ok: false, error: applied.error, count: applied.count, hint: applied.hint }) } next = applied.next replacements = applied.replacements || 0 } else if (oldStr) { if (!prev.includes(oldStr)) { return bareAgentJsonResult({ ok: false, error: 'old_string not found' }) } next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr) replacements = 1 } 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 && typeof vfs.mkdir === 'function') { await vfs.mkdir(dir, { recursive: true }) } await vfs.writeFile(path, body) return bareAgentJsonResult({ ok: true, bytes: body.length, replacements, replace_all: replaceAll }) } if (toolName === 'create_directory') { const path = (typeof args.path === 'string' && args.path.trim()) || (typeof args.dir === 'string' && args.dir.trim()) || (typeof args.directory === 'string' && args.directory.trim()) || (typeof args.folder === 'string' && args.folder.trim()) || (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || '' if (!enforceAutonomousPath(path, 'mutate')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } if (!path.startsWith('/') || path.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'bad_path', path, hint: 'Use an absolute path or ~/... (tilde expands to ' + resolvedHome + '). Example: ' + resolvedHome + '/test_work' }) } if (!vfs || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('mkdir ' + path) await vfs.mkdir(path, { recursive: true }) let verified = false try { if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) else if (typeof vfs.readdir === 'function') { await vfs.readdir(path) verified = true } else verified = true } catch { verified = false } if (!verified) { return bareAgentJsonResult({ ok: false, error: 'mkdir_not_visible', path, hint: 'create_directory returned but the path is not visible. Retry with ' + path }) } return bareAgentJsonResult({ ok: true, path, verified: 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') { let command = typeof args.command === 'string' ? args.command : '' if (!String(command).trim()) { const alt = args.cmd || args.code || args.script || args.input || args.value || args.query if (typeof alt === 'string') command = alt } const deniedBy = bareAgentCommandDeniedByList( command, (configRef.current || {}).command_deny ) if (deniedBy) { return bareAgentJsonResult({ ok: false, error: 'command_denied', matched: deniedBy }) } 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' }) } let cwd = typeof args.cwd === 'string' ? args.cwd.trim() : '' if (!cwd && paths.lastCwd) { try { cwd = (await bareAgentReadTextFile(ctx, paths.lastCwd)).trim() } catch { cwd = '' } } let line = command if (cwd) { if (!bareAgentPathAllowed(cwd)) { return bareAgentJsonResult({ ok: false, error: 'cwd_not_allowed' }) } line = 'cd ' + bareAgentShellQuote(cwd) + ' && ' + command try { if (paths.lastCwd) await bareAgentWriteTextFile(ctx, paths.lastCwd, cwd + '\n') } catch { /* optional */ } } appendProgress('run_command ' + line.slice(0, 160)) const r = await captureExec(line, timeoutMs, { captureExit }) return bareAgentJsonResult( r.ok === false ? r : { ok: true, cwd: cwd || null, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'run_js_script') { const rawCode = typeof args.code === 'string' ? args.code : '' const diverted = typeof bareAgentShellCommandFromJsScript === 'function' ? bareAgentShellCommandFromJsScript(rawCode) : '' if (diverted) { appendProgress('run_js_script_diverted_to_run_command ' + diverted.slice(0, 120)) return bareAgentDispatchTool({ ...o, toolName: 'run_command', argsJson: JSON.stringify({ command: diverted, capture_exit: true }) }) } const code = typeof bareAgentNormalizeGuestScript === 'function' ? bareAgentNormalizeGuestScript(rawCode) : rawCode 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) const out = r && typeof r === 'object' ? r : {} const text = String(out.stdout_stderr || out.error || '') if (/already been declared/i.test(text) || /already been declared/i.test(String(out.error || ''))) { return bareAgentJsonResult({ ok: false, error: 'guest_script_ctx_redeclared', hint: 'The kernel already injects ctx and argv. Do not write const/let/var ctx or export. Use native tools (read_file, write_file, run_command) unless you need custom JS. Script body should be only: async function run(ctx, argv) { const vfs = ctx.vfs; ... }', stdout_stderr: text }) } if ( /await is only valid|top.level|cannot find module|require is not defined|node\.js|ReferenceError|SyntaxError/i.test( text ) || /await is only valid|top.level/i.test(String(out.error || '')) ) { return bareAgentJsonResult({ ok: false, error: 'guest_script_wrong_tool', hint: 'Do not use run_js_script for curl/wget/ip/shell. Node is not installed. Call run_command({command:"curl -s https://api.ipify.org"}) and report stdout.', stdout_stderr: text }) } 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 = 'For live kernel feature flags use read_proc_file on /proc/bare_os/features (or features.json); apropos_man only searches man-page text, not runtime state. Otherwise 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' }) } let merged = bareAgentMergeConfigPatch(configRef.current, patch) const liveId = String(patch.qvac_model || patch.model || '').trim() if (liveId && typeof bareAgentApplyLiveModel === 'function') { merged = bareAgentApplyLiveModel(merged, liveId) } configRef.current = merged await bareAgentSaveConfigFromTools(ctx, paths, merged) if ( Object.prototype.hasOwnProperty.call(patch, 'owner_name') || Object.prototype.hasOwnProperty.call(patch, 'agent_label') ) { const workspace = typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace' await bareAgentSyncWorkspaceFromConfig(ctx, { workspace }, 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.trim()) || (typeof args.dir === 'string' && args.dir.trim()) || resolvedHome 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 (Boolean(args.tree) && typeof bareAgentRenderTree === 'function') { appendProgress('list_directory tree ' + dir) const tree = await bareAgentRenderTree(ctx, dir, { max: maxEnt, maxDepth: args.max_depth }) return bareAgentJsonResult(tree) } 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, mutateDenyPrefixes) || !bareAgentPathAllowedMutate(to, mutateDenyPrefixes) || 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: 'allow_delete is on by default; this home set it false' }) } if (reqTok && token !== reqTok) { return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' }) } if (!bareAgentPathAllowedMutate(path, mutateDenyPrefixes) || path.includes('..')) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (!vfs) { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('delete_path ' + path) if (typeof bareAgentPushEdit === 'function' && paths.edits) { try { const prev = await bareAgentReadTextFile(ctx, path) await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'delete_path' }) } catch { /* missing */ } } 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', hint: 'read_proc_file accepts any /proc path' }) } 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 === 'list_services') { appendProgress('list_services') if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } let readinessText = '' /** @type {Record | null} */ let readinessJson = null try { const b = await vfs.readFile('/proc/bare_os/initd_readiness.json') if (b && b.length) { readinessText = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) try { const parsed = JSON.parse(readinessText) if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) readinessJson = /** @type {Record} */ (parsed) } catch { /* ignore */ } } } catch { /* ignore */ } const units = Array.isArray(readinessJson?.units) ? /** @type {unknown[]} */ (readinessJson.units) : [] const rows = units .filter((u) => u && typeof u === 'object') .map((u) => { const o = /** @type {Record} */ (u) return { name: String(o.name || ''), phase: String(o.phase || ''), startedAtMs: typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined, error: typeof o.error === 'string' ? o.error : undefined } }) .filter((r) => r.name) return bareAgentJsonResult({ ok: true, source: '/proc/bare_os/initd_readiness.json', count: rows.length, units: rows, note: rows.length > 0 ? 'Runtime units from initd readiness snapshot.' : 'No parsed readiness units available.' }) } if (toolName === 'service_status') { const name = typeof args.name === 'string' ? args.name.trim() : '' if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' }) appendProgress('service_status ' + name) if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } try { const b = await vfs.readFile('/proc/bare_os/initd_readiness.json') const t = b && b.length ? typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) : '' const parsed = JSON.parse(t) const units = Array.isArray(parsed?.units) ? parsed.units : [] const hit = units.find((u) => u && typeof u === 'object' && String(u.name || '') === name) || null if (!hit) { return bareAgentJsonResult({ ok: false, error: 'not_found', source: '/proc/bare_os/initd_readiness.json' }) } const o = /** @type {Record} */ (hit) return bareAgentJsonResult({ ok: true, status: { name: String(o.name || ''), phase: String(o.phase || ''), startedAtMs: typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined, error: typeof o.error === 'string' ? o.error : undefined }, journal_hint: '/run/bare-os/unit-journal/' + name + '.ndjson' }) } 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_timers') { const includePreview = Boolean(args.include_preview) const maxEntries = typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) ? Math.min(Math.max(Math.floor(args.max_entries), 1), 512) : 128 appendProgress('list_timers') if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } const dir = home + '/.config/bare-os/timers' let names = [] try { names = await vfs.readdir(dir) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg, path: dir }) } const timerNames = names .filter((n) => typeof n === 'string' && n.endsWith('.timer')) .sort() .slice(0, maxEntries) /** @type {unknown[]} */ const timers = [] for (const name of timerNames) { const path = dir + '/' + name /** @type {Record} */ const row = { name, path } if (includePreview) { try { const b = await vfs.readFile(path) const txt = b && b.length ? typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) : '' row.preview = bareAgentTruncateChars(txt, 1200) } catch (e) { row.preview_error = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) } } timers.push(row) } return bareAgentJsonResult({ ok: true, path: dir, count: timers.length, timers }) } if (toolName === 'read_cron_log' || toolName === 'read_audit_log') { const maxChars = typeof args.max_chars === 'number' && Number.isFinite(args.max_chars) ? Math.min(Math.max(Math.floor(args.max_chars), 200), 80_000) : 12_000 const tailOnly = Boolean(args.tail_only) const redact = toolName === 'read_audit_log' ? args.redact !== false : false const path = toolName === 'read_audit_log' ? '/var/log/bare-os/audit.log' : '/var/log/bare-os/cron.log' appendProgress(toolName + ' ' + path) if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } try { const b = await vfs.readFile(path) if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' }) let t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) if (redact) { t = t .replace(/\bBearer\s+\S+/gi, 'Bearer ') .replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '') .replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=') } const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars) return bareAgentJsonResult({ ok: true, path, text: out, truncated: t.length > out.length }) } 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_boot_policy' || toolName === 'read_kernel_extension_resolution') { const maxChars = typeof args.max_chars === 'number' && Number.isFinite(args.max_chars) ? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000) : 20_000 const path = toolName === 'read_boot_policy' ? '/etc/bare-os/boot.policy.json' : '/run/bare-os/kernel-ext-resolution.json' appendProgress(toolName + ' ' + path) if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } try { const b = await vfs.readFile(path) if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' }) const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) /** @type {unknown} */ let json = null try { json = JSON.parse(t) } catch { json = null } return bareAgentJsonResult({ ok: true, path, text: bareAgentTruncateChars(t, maxChars), json }) } 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_initd_graph') { appendProgress('get_initd_graph') if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } /** @type {Record} */ const out = { ok: true } for (const p of ['/proc/bare_os/initd_dag.json', '/proc/bare_os/initd_readiness.json']) { try { const b = await vfs.readFile(p) if (!b || !b.length) continue const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) try { out[p] = JSON.parse(t) } catch { out[p] = bareAgentTruncateChars(t, 8000) } } catch { /* ignore missing */ } } return bareAgentJsonResult(out) } if (toolName === 'read_unit_journal') { const unit = typeof args.unit === 'string' ? args.unit.trim() : '' if (!/^[a-zA-Z0-9._-]{1,96}$/.test(unit)) { return bareAgentJsonResult({ ok: false, error: 'invalid_unit' }) } const maxChars = typeof args.max_chars === 'number' && Number.isFinite(args.max_chars) ? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000) : 12_000 const tailOnly = Boolean(args.tail_only) const p = '/run/bare-os/unit-journal/' + unit + '.ndjson' appendProgress('read_unit_journal ' + unit) return bareAgentJsonResult(await readBoundedText(p, maxChars, tailOnly, true)) } if (toolName === 'inspect_ipc_backpressure') { appendProgress('inspect_ipc_backpressure') if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } const candidates = [ '/proc/bare_os/ipc_backpressure.json', '/proc/bare_os/replication_operator_sketch.json', '/proc/bare_os/metrics_live.json' ] /** @type {Record} */ const out = { ok: true, sources: [] } for (const p of candidates) { try { const b = await vfs.readFile(p) if (!b || !b.length) continue const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) out.sources.push(p) try { out[p] = JSON.parse(t) } catch { out[p] = bareAgentTruncateChars(t, 6000) } } catch { /* ignore */ } } return bareAgentJsonResult(out) } if (toolName === 'get_network_summary') { appendProgress('get_network_summary') if (!vfs || typeof vfs.readFile !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } const candidates = [ '/proc/bare_os/net_summary.json', '/proc/bare_os/swarm.json', '/proc/bare_os/swarm_status.json', '/proc/bare_os/swarm_connection_manager_status.json' ] /** @type {Record} */ const out = { ok: true, sources: [] } for (const p of candidates) { try { const b = await vfs.readFile(p) if (!b || !b.length) continue const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) out.sources.push(p) try { out[p] = JSON.parse(t) } catch { out[p] = bareAgentTruncateChars(t, 6000) } } catch { /* ignore */ } } return bareAgentJsonResult(out) } if (toolName === 'tail_telemetry_streams') { const maxChars = typeof args.max_chars === 'number' && Number.isFinite(args.max_chars) ? Math.min(Math.max(Math.floor(args.max_chars), 200), 60_000) : 8000 appendProgress('tail_telemetry_streams') const pathsToRead = [ '/var/log/bare-os/audit.log', '/var/log/bare-os/logger.jsonl', '/var/log/bare-os/initd.log', '/var/log/bare-os/cron.log' ] /** @type {Record} */ const out = { ok: true, streams: {} } for (const p of pathsToRead) { out.streams[p] = await readBoundedText(p, maxChars, true, true) } return bareAgentJsonResult(out) } if (toolName === 'pkg_index_lookup') { const key = typeof args.key === 'string' ? args.key.trim() : '' if (!key) return bareAgentJsonResult({ ok: false, error: 'key_required' }) appendProgress('pkg_index_lookup ' + key.slice(0, 80)) if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const cmd = 'pkg-swarm-index get --key ' + bareAgentShellQuote(key) const r = await captureExec(cmd, 90000) if (r.ok === false) return bareAgentJsonResult(r) const txt = typeof r.stdout_stderr === 'string' ? r.stdout_stderr : '' let json = null try { json = JSON.parse(txt) } catch { json = null } return bareAgentJsonResult({ ok: true, key, json, text: bareAgentTruncateChars(txt, 12000) }) } if (toolName === 'list_verification_scripts') { appendProgress('list_verification_scripts') if (!vfs || typeof vfs.readdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } const out = [] for (const dir of ['/scripts', '/home/guest/scripts']) { try { const names = await vfs.readdir(dir) const rows = names .filter((n) => typeof n === 'string' && (n.endsWith('.mjs') || n.endsWith('.js'))) .sort() .slice(0, 400) .map((n) => dir + '/' + n) out.push(...rows) } catch { /* ignore */ } } return bareAgentJsonResult({ ok: true, scripts: out }) } if (toolName === 'run_maintenance_gate') { const command = typeof args.command === 'string' ? args.command.trim() : '' const timeoutMs = typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms) ? Math.min(Math.max(Math.floor(args.timeout_ms), 1000), 900000) : 180000 appendProgress('run_maintenance_gate ' + command) const allow = { 'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs', 'verify-man-coverage': 'node scripts/verify-man-coverage.mjs', 'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs', 'coreutils-test': 'npm test -w bare-os-coreutils' } if (!Object.prototype.hasOwnProperty.call(allow, command)) { return bareAgentJsonResult({ ok: false, error: 'command_not_allowlisted', allowlist: Object.keys(allow) }) } if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const cmd = /** @type {Record} */ (allow)[command] const r = await captureExec(cmd, timeoutMs, { captureExit: true }) return bareAgentJsonResult(r) } if (toolName === 'run_contract_checks') { const profile = typeof args.profile === 'string' ? args.profile.trim() : '' appendProgress('run_contract_checks ' + profile) const mapping = { core: 'node scripts/verify-kernel-seeder-parity.mjs && node scripts/verify-man-coverage.mjs', docs: 'node scripts/verify-doc-links.mjs && node scripts/verify-doc-contracts.mjs', parity: 'node scripts/verify-kernel-seeder-parity.mjs' } if (!Object.prototype.hasOwnProperty.call(mapping, profile)) { return bareAgentJsonResult({ ok: false, error: 'unknown_profile', profiles: Object.keys(mapping) }) } if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const cmd = /** @type {Record} */ (mapping)[profile] const r = await captureExec(cmd, 300000, { captureExit: true }) return bareAgentJsonResult(r) } if (toolName === 'summarize_build_drift') { appendProgress('summarize_build_drift') if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const r = await captureExec('git status --short', 60000) return bareAgentJsonResult(r) } if (toolName === 'get_hrpc_bridge_health') { appendProgress('get_hrpc_bridge_health') /** @type {Record} */ const out = { ok: true, hostCapabilities: { hrpcBridge: typeof ctx.bareOsHostCapability === 'function' ? Boolean(ctx.bareOsHostCapability('hrpcBridge')) : false } } if (vfs && typeof vfs.readFile === 'function') { for (const p of ['/proc/bare_os/hrpc_route_table.json', '/proc/bare_os/hrpc_health.json']) { try { const b = await vfs.readFile(p) if (!b || !b.length) continue const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(b) : String(new TextDecoder().decode(b)) try { out[p] = JSON.parse(t) } catch { out[p] = bareAgentTruncateChars(t, 6000) } } catch { /* ignore */ } } } return bareAgentJsonResult(out) } if (toolName === 'get_hrpc_allowlist_status') { appendProgress('get_hrpc_allowlist_status') if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const r = await captureExec('hrpc probe', 60000) return bareAgentJsonResult(r) } if ( toolName === 'discord_send_message' || toolName === 'discord_channel_status' || toolName === 'discord_read_inbox' ) { if (toolName === 'discord_channel_status') { appendProgress('discord_channel_status') if (typeof ctx.bareOsDiscordChannelStatus === 'function') { try { return bareAgentJsonResult(await ctx.bareOsDiscordChannelStatus()) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } const rec = await bareAgentReadJsonFile(ctx, '~/.discord/channel.json', {}) const outbox = await bareAgentReadJsonFile(ctx, '~/.discord/outbox.json', []) const inbox = await bareAgentReadJsonFile(ctx, '~/.discord/inbox.json', []) return bareAgentJsonResult({ ok: true, open: rec && rec.open !== false && Boolean(rec.userId), userId: rec && rec.userId ? String(rec.userId) : '', lastInboundAt: (rec && rec.lastInboundAt) || '', lastOutboundAt: (rec && rec.lastOutboundAt) || '', pendingOutbox: Array.isArray(outbox) ? outbox.length : 0, inboxCount: Array.isArray(inbox) ? inbox.length : 0, liveClient: false }) } if (toolName === 'discord_read_inbox') { const n = Math.max(1, Math.min(50, Math.floor(Number(args.max) || 12))) appendProgress('discord_read_inbox') const inbox = await bareAgentReadJsonFile(ctx, '~/.discord/inbox.json', []) const list = Array.isArray(inbox) ? inbox : [] return bareAgentJsonResult({ ok: true, count: list.length, messages: list.slice(-n) }) } const text = String(args.text || args.message || '').trim() if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_message' }) if (text.length > 4000) { return bareAgentJsonResult({ ok: false, error: 'message_too_long', max: 4000 }) } const userId = String(args.user_id || args.userId || '').trim() appendProgress('discord_send_message') if (typeof ctx.bareOsDiscordSendDm === 'function') { try { const res = await ctx.bareOsDiscordSendDm({ text: text, userId: userId, source: 'agent' }) return bareAgentJsonResult(res && typeof res === 'object' ? res : { ok: true }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } const rec = await bareAgentReadJsonFile(ctx, '~/.discord/channel.json', {}) const partner = userId || (rec && rec.userId ? String(rec.userId) : '') if (!partner) { return bareAgentJsonResult({ ok: false, error: 'no_channel_partner', hint: 'A whitelisted user must DM the bot first, or pass user_id.' }) } const outbox = await bareAgentReadJsonFile(ctx, '~/.discord/outbox.json', []) const list = Array.isArray(outbox) ? outbox.slice() : [] const item = { id: 'out-' + String(Date.now()), userId: partner, text: text, createdAt: new Date().toISOString(), source: 'agent' } list.push(item) await bareAgentWriteJsonFile(ctx, '~/.discord/outbox.json', list.slice(-100)) return bareAgentJsonResult({ ok: true, queued: true, id: item.id, userId: partner }) } if (toolName === 'emit_host_notification' || toolName === 'request_host_action') { const cfg = configRef.current || {} if (cfg && cfg.emergency_stop_mutations) { return bareAgentJsonResult({ ok: false, error: 'emergency_stop_mutations_enabled' }) } if (toolName === 'emit_host_notification' && !cfg.allow_host_notifications) { return bareAgentJsonResult({ ok: false, error: 'host_notifications_disabled' }) } if (toolName === 'request_host_action' && !cfg.allow_host_actions) { return bareAgentJsonResult({ ok: false, error: 'host_actions_disabled' }) } if (!cfg.allow_bridge_mutations) { return bareAgentJsonResult({ ok: false, error: 'bridge_mutations_disabled' }) } if (typeof ctx.bareOsHrpcRequest !== 'function') { return bareAgentJsonResult({ ok: false, error: 'bareOsHrpcRequest unavailable' }) } try { if (toolName === 'emit_host_notification') { const payload = { title: String(args.title || '').slice(0, 200), message: String(args.message || '').slice(0, 2000), level: typeof args.level === 'string' ? args.level : 'info' } appendProgress('emit_host_notification ' + payload.title) const res = await ctx.bareOsHrpcRequest('bare_os', 'host_notify', payload) if (typeof ctx.bareOsDiscordSendDm === 'function') { try { await ctx.bareOsDiscordSendDm({ text: payload.title + '\n' + payload.message, source: 'host_notify' }) } catch { /* Discord fan-out is optional */ } } return bareAgentJsonResult({ ok: true, result: res }) } const action = String(args.action || '').trim() if (!/^[a-zA-Z0-9._-]{1,64}$/.test(action)) { return bareAgentJsonResult({ ok: false, error: 'invalid_action' }) } const payload = args.payload && typeof args.payload === 'object' && !Array.isArray(args.payload) ? args.payload : {} appendProgress('request_host_action ' + action) const res = await ctx.bareOsHrpcRequest('bare_os', 'host_action', { action, payload }) return bareAgentJsonResult({ ok: true, result: res }) } 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 } ) } if (toolName === 'verification_hints') { const topic = typeof args.topic === 'string' ? args.topic : '' const pathsTouch = typeof args.paths_touched === 'string' ? args.paths_touched : '' appendProgress('verification_hints') const combined = topic + ' ' + pathsTouch.replace(/,/g, ' ') const hints = bareAgentVerificationHintsList(combined) return bareAgentJsonResult({ ok: true, scope_note: 'Suggested commands apply to the Bare OS git checkout on the host (npm/node at repo root). They do not run automatically.', suggested_commands: hints }) } if (toolName === 'runtime_diagnostic_bundle') { appendProgress('runtime_diagnostic_bundle') /** @type {Record} */ const bundle = {} bundle.bareOsCtxApiVersion = typeof ctx.bareOsCtxApiVersion !== 'undefined' ? ctx.bareOsCtxApiVersion : null try { if (typeof ctx.bareOsGetResourceStatus === 'function') bundle.resources = ctx.bareOsGetResourceStatus() } catch { bundle.resources_error = true } /** @type {Record} */ const procParts = {} /** @type {string[]} */ let list = [...BARE_AGENT_PROC_READ_ALLOWLIST] if (vfs && typeof vfs.readdir === 'function') { try { const names = await vfs.readdir('/proc/bare_os') if (Array.isArray(names)) { for (let i = 0; i < names.length; i++) { const n = String(names[i] || '') if (!n || n === '.' || n === '..') continue const full = '/proc/bare_os/' + n if (list.indexOf(full) === -1) list.push(full) } } } catch { /* keep seed list */ } } if (vfs && typeof vfs.readFile === 'function') { for (let i = 0; i < list.length; i++) { const procPath = list[i] try { const buf = await vfs.readFile(procPath) if (!buf || !buf.length) continue 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 > 80_000) t = t.slice(0, 80_000) + '\n… truncated' try { procParts[procPath] = JSON.parse(t) } catch { procParts[procPath] = t } } catch { /* missing path */ } } } bundle.proc = procParts return bareAgentJsonResult({ ok: true, bundle }) } if (toolName === 'web_fetch') { const url = typeof args.url === 'string' ? args.url : '' let hostHint = '' try { hostHint = new URL(url).hostname } catch { hostHint = '' } appendProgress('web_fetch ' + (hostHint || url.slice(0, 80))) try { const out = await bareWebRunTool({ ctx, url, method: typeof args.method === 'string' ? args.method : undefined, headers: args.headers && typeof args.headers === 'object' && !Array.isArray(args.headers) ? /** @type {Record} */ (args.headers) : undefined, body: typeof args.body === 'string' ? args.body : undefined, content_type: typeof args.content_type === 'string' ? args.content_type : undefined, max_response_bytes: typeof args.max_response_bytes === 'number' ? args.max_response_bytes : undefined, max_redirects: typeof args.max_redirects === 'number' ? args.max_redirects : undefined, timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined, format: typeof args.format === 'string' ? args.format : undefined, max_links: typeof args.max_links === 'number' ? args.max_links : undefined, signal }) return bareAgentJsonResult(out) } catch (e) { const msg = bareWebFmtErr(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'grep') { const pattern = typeof args.pattern === 'string' ? args.pattern : '' if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' }) if (typeof bareAgentGrepFiles !== 'function') { return bareAgentJsonResult({ ok: false, error: 'grep_unavailable' }) } const root = (typeof args.path === 'string' && args.path.trim()) || (typeof args.root === 'string' && args.root.trim()) || home || '/home' if (!bareAgentPathAllowed(root)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } appendProgress('grep ' + pattern + ' @ ' + root) const out = await bareAgentGrepFiles(ctx, { pattern, root, glob: typeof args.glob === 'string' ? args.glob : '', ignore_case: Boolean(args.ignore_case), before: args.before, after: args.after, context: args.context, max_matches: args.max_matches, files_with_matches: Boolean(args.files_with_matches), output_mode: typeof args.output_mode === 'string' ? args.output_mode : '' }) return bareAgentJsonResult(out) } if (toolName === 'apply_patch') { const patch = (typeof args.patch === 'string' && args.patch) || (typeof args.input === 'string' && args.input) || '' if (!patch.trim()) return bareAgentJsonResult({ ok: false, error: 'patch_required' }) if (typeof bareAgentParseApplyPatch !== 'function') { return bareAgentJsonResult({ ok: false, error: 'apply_patch_unavailable' }) } const parsed = bareAgentParseApplyPatch(patch) if (!parsed.ok) return bareAgentJsonResult(parsed) appendProgress('apply_patch ops=' + String((parsed.ops || []).length)) if (typeof bareAgentPushEdit === 'function' && paths.edits && parsed.ops) { for (let i = 0; i < parsed.ops.length; i++) { const op = parsed.ops[i] const p = String((op && op.path) || '') if (!p) continue try { const prev = await bareAgentReadTextFile(ctx, p) await bareAgentPushEdit(ctx, paths.edits, { path: p, prev, tool: 'apply_patch' }) } catch { /* new file */ } } } const out = await bareAgentApplyPatchOps(ctx, parsed.ops, { home, denyPrefixes: mutateDenyPrefixes }) return bareAgentJsonResult(out) } if (toolName === 'update_goal') { const message = typeof args.message === 'string' ? args.message.trim() : '' const blocked = typeof args.blocked_reason === 'string' ? args.blocked_reason.trim() : '' const completed = Boolean(args.completed) const next = { ...(configRef.current || {}) } if (message) next.autonomous_last_error = '' if (blocked) { next.autonomous_status = 'blocked' next.autonomous_last_error = blocked next.autonomous_active = false } else if (completed) { next.autonomous_status = 'completed' next.autonomous_active = false if (message) next.autonomous_goal = String(next.autonomous_goal || '') } else { next.autonomous_status = 'running' } configRef.current = next if (typeof bareAgentSaveConfigFromTools === 'function') { await bareAgentSaveConfigFromTools(ctx, paths, next) } appendProgress( 'update_goal ' + (completed ? 'completed' : blocked ? 'blocked' : 'progress') ) return bareAgentJsonResult({ ok: true, completed, blocked: Boolean(blocked), message: message || null, blocked_reason: blocked || null, status: next.autonomous_status }) } if (toolName === 'web_search') { const query = typeof args.query === 'string' ? args.query.trim() : '' if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' }) const maxResults = typeof args.max_results === 'number' && Number.isFinite(args.max_results) ? Math.min(Math.max(Math.floor(args.max_results), 1), 16) : 8 const url = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1' appendProgress('web_search ' + query.slice(0, 80)) try { const raw = await bareWebRunTool({ ctx, url, format: 'json', timeout_ms: 20000, max_response_bytes: 200000, signal }) if (!raw || raw.ok === false) { return bareAgentJsonResult({ ok: false, error: (raw && raw.error) || 'web_search_failed', hint: 'HTTP policy may block api.duckduckgo.com; try web_fetch on a known URL' }) } let payload = raw.extract && raw.extract.json if (payload == null && raw.extract && typeof raw.extract.text_slice === 'string') { try { payload = JSON.parse(raw.extract.text_slice) } catch { payload = null } } const results = typeof bareAgentParseSearchResults === 'function' ? bareAgentParseSearchResults(payload, maxResults) : [] return bareAgentJsonResult({ ok: true, query, results, abstract: payload && payload.Abstract ? String(payload.Abstract) : '', count: results.length }) } catch (e) { const msg = typeof bareWebFmtErr === 'function' ? bareWebFmtErr(e) : String(e) return bareAgentJsonResult({ ok: false, error: msg }) } } if (toolName === 'git_status') { const cwd = (typeof args.cwd === 'string' && args.cwd.trim()) || (ctx.env && typeof ctx.env === 'object' ? String(ctx.env.PWD || ctx.env.CWD || home || '').trim() : '') || home || '/home' if (!execLine) { return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) } appendProgress('git_status ' + cwd) const cmd = 'git -C ' + bareAgentShellQuote(cwd) + ' status --short && echo --- && git -C ' + bareAgentShellQuote(cwd) + ' diff --stat && echo --- && git -C ' + bareAgentShellQuote(cwd) + ' log --oneline -8' const r = await captureExec(cmd, 30000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'memory_append') { const text = typeof args.text === 'string' ? args.text.trim() : '' if (!text) return bareAgentJsonResult({ ok: false, error: 'text_required' }) const kindRaw = String(args.kind || 'NOTE').trim().toUpperCase() const kind = kindRaw === 'FACT' || kindRaw === 'CHECK' || kindRaw === 'RISK' || kindRaw === 'HANDOFF' || kindRaw === 'NOTE' ? kindRaw : 'NOTE' const workspace = typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace' const memDir = typeof paths.workspaceMemory === 'string' ? paths.workspaceMemory : workspace + '/memory' const dest = args.daily ? memDir + '/' + (typeof bareAgentWorkspaceUtcYmd === 'function' ? bareAgentWorkspaceUtcYmd() : new Date().toISOString().slice(0, 10)) + '.md' : workspace + '/MEMORY.md' const stamp = new Date().toISOString() const line = '- ' + kind + ' — ' + text.replace(/\s+/g, ' ').trim() let prev = '' try { prev = await bareAgentReadTextFile(ctx, dest) } catch { prev = '' } const next = (prev ? prev.replace(/\s*$/, '') + '\n' : '# Memory\n\n') + line + ' \n _' + stamp + '_\n' await bareAgentWriteTextFile(ctx, dest, next) appendProgress('memory_append ' + dest) return bareAgentJsonResult({ ok: true, path: dest, kind, appended: line }) } if (toolName === 'list_agent_tools') { const cat = typeof bareAgentToolCatalog === 'function' ? bareAgentToolCatalog() : [] const want = typeof args.name === 'string' ? args.name.trim() : '' const rows = want ? cat.filter(function (t) { return ( t.name === want || String(t.name).toLowerCase().indexOf(want.toLowerCase()) !== -1 ) }) : cat appendProgress('list_agent_tools n=' + String(rows.length)) return bareAgentJsonResult({ ok: true, count: rows.length, total: cat.length, tools: rows }) } if (toolName === 'list_skills') { const max = typeof args.max === 'number' && Number.isFinite(args.max) ? Math.min(Math.max(Math.floor(args.max), 1), 80) : 40 if (typeof bareAgentDiscoverSkills !== 'function') { return bareAgentJsonResult({ ok: false, error: 'skills_unavailable' }) } const skillPaths = { workspaceSkills: typeof paths.workspaceSkills === 'string' ? paths.workspaceSkills : paths.dir + '/workspace/skills', skillsGlobal: typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills' } const skills = await bareAgentDiscoverSkills(ctx, skillPaths) appendProgress('list_skills ' + String(skills.length)) return bareAgentJsonResult({ ok: true, count: Math.min(skills.length, max), truncated: skills.length > max, skills: skills.slice(0, max) }) } if (toolName === 'schedule_task') { const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '' if (!prompt) return bareAgentJsonResult({ ok: false, error: 'prompt_required' }) const parsed = typeof bareAgentParseScheduleInterval === 'function' ? bareAgentParseScheduleInterval(args.interval) : { error: 'schedule_unavailable' } if (parsed.error) return bareAgentJsonResult({ ok: false, error: parsed.error, hint: parsed.hint }) const id = typeof bareAgentScheduleId === 'function' ? bareAgentScheduleId(args.id || prompt.slice(0, 24)) : 'agent-task' const dir = (home || '/home') + '/.config/bare-os/timers' if (!vfs || typeof vfs.readdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } try { if (typeof vfs.mkdir === 'function') await vfs.mkdir(dir, { recursive: true }) } catch { /* exists */ } let names = [] try { names = await vfs.readdir(dir) } catch { names = [] } const existing = Array.isArray(names) ? names.filter(function (n) { return typeof n === 'string' && n.endsWith('.timer') }) : [] const dest = dir + '/' + id + '.timer' const already = existing.indexOf(id + '.timer') >= 0 if (!already && existing.length >= 8) { return bareAgentJsonResult({ ok: false, error: 'timer_limit', max: 8, hint: 'unschedule_task an existing id first' }) } const quoted = bareAgentShellQuote(prompt) const line = args.auto === false ? 'agent ' + quoted : 'agent --auto ' + quoted const body = parsed.kind === 'everyMs' ? '[Timer]\nEveryMs=' + String(parsed.everyMs) + '\nExecLine=' + line + '\n' : '[Timer]\nOnCalendar=' + parsed.onCalendar + '\nExecLine=' + line + '\n' await bareAgentWriteTextFile(ctx, dest, body) appendProgress('schedule_task ' + id) return bareAgentJsonResult({ ok: true, id, path: dest, interval: parsed, exec: line }) } if (toolName === 'unschedule_task') { const id = typeof bareAgentScheduleId === 'function' ? bareAgentScheduleId(args.id) : String(args.id || '') if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' }) const dest = (home || '/home') + '/.config/bare-os/timers/' + id + '.timer' if (!vfs || typeof vfs.unlink !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs.unlink unavailable' }) } try { await vfs.unlink(dest) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return bareAgentJsonResult({ ok: false, error: msg, path: dest }) } appendProgress('unschedule_task ' + id) return bareAgentJsonResult({ ok: true, id, path: dest }) } if (toolName === 'list_scheduled') { const dir = (home || '/home') + '/.config/bare-os/timers' let names = [] try { names = vfs && typeof vfs.readdir === 'function' ? await vfs.readdir(dir) : [] } catch { names = [] } const rows = [] const list = Array.isArray(names) ? names : [] for (let i = 0; i < list.length; i++) { const name = String(list[i] || '') if (!name.endsWith('.timer') || name.indexOf('agent-') !== 0) continue const path = dir + '/' + name const preview = await bareAgentReadTextFile(ctx, path) rows.push({ id: name.replace(/\.timer$/, ''), path, preview: preview.slice(0, 400) }) } appendProgress('list_scheduled ' + String(rows.length)) return bareAgentJsonResult({ ok: true, count: rows.length, timers: rows }) } if (toolName === 'fuzzy_find') { const query = typeof args.query === 'string' ? args.query.trim() : '' if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' }) const root = (typeof args.root === 'string' && args.root.trim()) || home || '/home' if (typeof bareAgentFuzzyFind !== 'function') { return bareAgentJsonResult({ ok: false, error: 'fuzzy_unavailable' }) } appendProgress('fuzzy_find ' + query) const hits = await bareAgentFuzzyFind(ctx, root, query, { max: args.max }) return bareAgentJsonResult({ ok: true, query, root, hits }) } if (toolName === 'read_many') { const pathsIn = Array.isArray(args.paths) ? args.paths : [] if (!pathsIn.length) return bareAgentJsonResult({ ok: false, error: 'paths_required' }) const maxB = typeof args.max_bytes_each === 'number' && Number.isFinite(args.max_bytes_each) ? Math.min(Math.max(Math.floor(args.max_bytes_each), 200), 200000) : 32000 const files = [] for (let i = 0; i < pathsIn.length && i < 16; i++) { const path = String(pathsIn[i] || '').trim() if (!path || !bareAgentPathAllowed(path)) { files.push({ path, ok: false, error: 'path_not_allowed' }) continue } let text = await bareAgentReadTextFile(ctx, path) const truncated = text.length > maxB if (truncated) text = text.slice(0, maxB) + '\n… truncated' files.push({ path, ok: true, content: text, truncated }) } appendProgress('read_many ' + String(files.length)) return bareAgentJsonResult({ ok: true, files }) } if (toolName === 'wait_for') { const pattern = typeof args.pattern === 'string' ? args.pattern : '' if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' }) let re try { re = new RegExp(pattern, args.ignore_case ? 'i' : '') } catch (e) { return bareAgentJsonResult({ ok: false, error: 'bad_regex' }) } const timeoutMs = Math.min( 120000, Math.max(200, Math.floor(Number(args.timeout_ms) || 15000)) ) const intervalMs = Math.min( 5000, Math.max(100, Math.floor(Number(args.interval_ms) || 400)) ) const path = typeof args.path === 'string' ? args.path.trim() : '' const command = typeof args.command === 'string' ? args.command.trim() : '' if (!path && !command) { return bareAgentJsonResult({ ok: false, error: 'path_or_command_required' }) } appendProgress('wait_for ' + pattern) const deadline = Date.now() + timeoutMs let last = '' while (Date.now() <= deadline) { if (path) last = await bareAgentReadTextFile(ctx, path) else if (command && execLine) { const r = await captureExec(command, Math.min(intervalMs + 2000, 15000)) last = r && r.stdout_stderr ? String(r.stdout_stderr) : '' } re.lastIndex = 0 if (re.test(last)) { return bareAgentJsonResult({ ok: true, matched: true, elapsed_ms: timeoutMs - Math.max(0, deadline - Date.now()), sample: last.slice(0, 800) }) } if (Date.now() + intervalMs > deadline) break await new Promise(function (resolve) { setTimeout(resolve, intervalMs) }) } return bareAgentJsonResult({ ok: false, error: 'wait_timeout', sample: last.slice(0, 400) }) } if (toolName === 'undo_last_edit') { const editsPath = paths.edits || paths.dir + '/edits.json' if (typeof bareAgentPopEdit !== 'function') { return bareAgentJsonResult({ ok: false, error: 'undo_unavailable' }) } const rec = await bareAgentPopEdit(ctx, editsPath) if (!rec || !rec.path) { return bareAgentJsonResult({ ok: false, error: 'nothing_to_undo' }) } await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || '')) appendProgress('undo_last_edit ' + rec.path) return bareAgentJsonResult({ ok: true, path: rec.path, tool: rec.tool || null }) } if (toolName === 'git_diff') { const cwd = (typeof args.cwd === 'string' && args.cwd.trim()) || (ctx.env && typeof ctx.env === 'object' ? String(ctx.env.PWD || ctx.env.CWD || home || '').trim() : '') || home || '/home' if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const p = typeof args.path === 'string' ? args.path.trim() : '' const stat = Boolean(args.stat) const cmd = 'git -C ' + bareAgentShellQuote(cwd) + ' diff' + (stat ? ' --stat' : '') + (p ? ' -- ' + bareAgentShellQuote(p) : '') appendProgress('git_diff ' + cwd) const r = await captureExec(cmd, 30000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'history_search') { const query = typeof args.query === 'string' ? args.query.trim() : '' if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' }) const histPath = paths.history || paths.dir + '/history.json' const raw = await bareAgentReadJsonFile(ctx, histPath, []) const hits = typeof bareAgentHistorySearch === 'function' ? bareAgentHistorySearch(Array.isArray(raw) ? raw : [], query, args.max) : [] appendProgress('history_search ' + query) return bareAgentJsonResult({ ok: true, query, hits }) } if (toolName === 'git_log' || toolName === 'git_show' || toolName === 'git_blame') { const cwd = (typeof args.cwd === 'string' && args.cwd.trim()) || (ctx.env && typeof ctx.env === 'object' ? String(ctx.env.PWD || ctx.env.CWD || home || '').trim() : '') || home || '/home' if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' }) const quoted = bareAgentShellQuote(cwd) let cmd = 'git -C ' + quoted + ' ' if (toolName === 'git_log') { const max = Math.min(80, Math.max(1, Math.floor(Number(args.max) || 20))) const p = typeof args.path === 'string' ? args.path.trim() : '' cmd += 'log --oneline -' + String(max) + (p ? ' -- ' + bareAgentShellQuote(p) : '') } else if (toolName === 'git_show') { const rev = typeof args.rev === 'string' && args.rev.trim() ? args.rev.trim() : 'HEAD' const p = typeof args.path === 'string' ? args.path.trim() : '' cmd += 'show ' + (args.stat ? '--stat ' : '') + bareAgentShellQuote(rev) + (p ? ' -- ' + bareAgentShellQuote(p) : '') } else { const p = typeof args.path === 'string' ? args.path.trim() : '' if (!p) return bareAgentJsonResult({ ok: false, error: 'path_required' }) cmd += 'blame -- ' + bareAgentShellQuote(p) } appendProgress(toolName + ' ' + cwd) const r = await captureExec(cmd, 30000) return bareAgentJsonResult( r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr } ) } if (toolName === 'copy_path') { const from = typeof args.from_path === 'string' ? args.from_path.trim() : '' const to = typeof args.to_path === 'string' ? args.to_path.trim() : '' if ( !from || !to || from.includes('..') || to.includes('..') || !bareAgentPathAllowed(from) || !bareAgentPathAllowedMutate(to, mutateDenyPrefixes) ) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (typeof bareAgentCopyPath !== 'function') { return bareAgentJsonResult({ ok: false, error: 'copy_unavailable' }) } appendProgress('copy_path ' + from) const out = await bareAgentCopyPath(ctx, from, to) return bareAgentJsonResult(out) } if (toolName === 'diff_files') { const from = typeof args.from_path === 'string' ? args.from_path.trim() : '' const to = typeof args.to_path === 'string' ? args.to_path.trim() : '' if (!from || !to || !bareAgentPathAllowed(from) || !bareAgentPathAllowed(to)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } if (typeof bareAgentUnifiedDiff !== 'function') { return bareAgentJsonResult({ ok: false, error: 'diff_unavailable' }) } const a = await bareAgentReadTextFile(ctx, from) const b = await bareAgentReadTextFile(ctx, to) appendProgress('diff_files ' + from) const out = bareAgentUnifiedDiff(a, b, { from, to, context: args.context }) return bareAgentJsonResult(out) } if (toolName === 'find_symbol') { const name = typeof args.name === 'string' ? args.name.trim() : '' if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' }) const root = (typeof args.root === 'string' && args.root.trim()) || home || '/home' if (!bareAgentPathAllowed(root) || typeof bareAgentFindSymbol !== 'function') { return bareAgentJsonResult({ ok: false, error: 'find_symbol_unavailable' }) } appendProgress('find_symbol ' + name) const out = await bareAgentFindSymbol(ctx, root, name, { glob: typeof args.glob === 'string' ? args.glob : '', max: args.max }) return bareAgentJsonResult(out) } if (toolName === 'create_skill') { const id = String(args.id || '') .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' }) const destRoot = (typeof paths.workspaceSkills === 'string' && paths.workspaceSkills) || (typeof paths.workspace === 'string' ? paths.workspace + '/skills' : paths.dir + '/workspace/skills') const dest = destRoot + '/' + id + '/SKILL.md' if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } const md = typeof bareAgentSkillMarkdown === 'function' ? bareAgentSkillMarkdown({ name: typeof args.name === 'string' ? args.name : id, description: typeof args.description === 'string' ? args.description : id, body: typeof args.body === 'string' ? args.body : '' }) : String(args.body || '') await bareAgentWriteTextFile(ctx, dest, md) appendProgress('create_skill ' + id) return bareAgentJsonResult({ ok: true, id, path: dest }) } if (toolName === 'rewind_session') { const histPath = paths.history || paths.dir + '/history.json' const raw = await bareAgentReadJsonFile(ctx, histPath, []) if (typeof bareAgentRewindHistory !== 'function') { return bareAgentJsonResult({ ok: false, error: 'rewind_unavailable' }) } const out = bareAgentRewindHistory(Array.isArray(raw) ? raw : [], { steps: args.steps, userIndex: args.user_index, keep_user: args.keep_user }) if (!out.ok) return bareAgentJsonResult(out) await bareAgentSaveHistory(ctx, histPath, out.messages) appendProgress('rewind_session dropped=' + String(out.dropped)) return bareAgentJsonResult({ ok: true, dropped: out.dropped, remaining: out.messages.length, target: out.target || null, keep_user: Boolean(out.keep_user) }) } if (toolName === 'export_session') { const histPath = paths.history || paths.dir + '/history.json' const dest = (typeof args.path === 'string' && args.path.trim()) || (paths.dir ? paths.dir + '/export.md' : '/tmp/agent-export.md') if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) { return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) } const raw = await bareAgentReadJsonFile(ctx, histPath, []) const md = typeof bareAgentExportTranscript === 'function' ? bareAgentExportTranscript(Array.isArray(raw) ? raw : []) : '' await bareAgentWriteTextFile(ctx, dest, md) appendProgress('export_session ' + dest) return bareAgentJsonResult({ ok: true, path: dest, messages: Array.isArray(raw) ? raw.length : 0 }) } 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 }) } } /** * Markdown → ANSI for /bin/agent replies (TTY-friendly, no deps). */ /** * @param {boolean} useColor * @param {'bold'|'dim'|'italic'|'code'|'heading'|'quote'|'hr'|'bullet'|'link'} kind */ function bareAgentMdSgr(useColor, kind) { if (!useColor) return '' switch (kind) { case 'bold': return '\x1b[1m' case 'dim': return '\x1b[2m' case 'italic': return '\x1b[3m' case 'code': return '\x1b[36m' case 'heading': return '\x1b[1;36m' case 'quote': return '\x1b[2;37m' case 'hr': return '\x1b[2m' case 'bullet': return '\x1b[33m' case 'link': return '\x1b[4;36m' default: return '' } } /** * Inline markdown on a single line (no nested fences). * @param {string} line * @param {boolean} useColor */ function bareAgentMdInline(line, useColor) { const reset = useColor ? '\x1b[0m' : '' let s = String(line || '') // escape-ish: leave raw backslashes mostly alone /** @type {string[]} */ const out = [] let i = 0 while (i < s.length) { // inline code if (s[i] === '`') { const end = s.indexOf('`', i + 1) if (end > i) { out.push( bareAgentMdSgr(useColor, 'code') + s.slice(i + 1, end) + reset ) i = end + 1 continue } } // bold ** or __ if ( (s[i] === '*' && s[i + 1] === '*') || (s[i] === '_' && s[i + 1] === '_') ) { const mark = s[i] const end = s.indexOf(mark + mark, i + 2) if (end > i) { out.push( bareAgentMdSgr(useColor, 'bold') + bareAgentMdInline(s.slice(i + 2, end), useColor) + reset ) i = end + 2 continue } } // italic * or _ if (s[i] === '*' || s[i] === '_') { const mark = s[i] // avoid matching list markers at start handled elsewhere const end = s.indexOf(mark, i + 1) if (end > i + 1) { out.push( bareAgentMdSgr(useColor, 'italic') + s.slice(i + 1, end) + reset ) i = end + 1 continue } } // strike ~~ if (s[i] === '~' && s[i + 1] === '~') { const end = s.indexOf('~~', i + 2) if (end > i) { out.push('\x1b[9m' + s.slice(i + 2, end) + reset) i = end + 2 continue } } // links [text](url) if (s[i] === '[') { const mid = s.indexOf('](', i + 1) const end = mid >= 0 ? s.indexOf(')', mid + 2) : -1 if (mid > i && end > mid) { const label = s.slice(i + 1, mid) const url = s.slice(mid + 2, end) out.push( bareAgentMdSgr(useColor, 'link') + label + reset + bareAgentMdSgr(useColor, 'dim') + ' (' + url + ')' + reset ) i = end + 1 continue } } out.push(s[i]) i++ } return out.join('') } /** * Visible width ignoring ANSI CSI sequences. * @param {string} s */ function bareAgentMdVisibleWidth(s) { return String(s || '') .replace(/\x1b\[[0-9;]*m/g, '') .length } /** * Wrap a string to width by visible columns (ANSI-aware, crude). * @param {string} s * @param {number} width * @returns {string[]} */ function bareAgentMdWrapAnsi(s, width) { const w = Math.max(8, width | 0) const raw = String(s || '') if (!raw) return [''] /** @type {string[]} */ const lines = [] let cur = '' let vis = 0 let i = 0 while (i < raw.length) { if (raw[i] === '\x1b' && raw[i + 1] === '[') { let j = i + 2 while (j < raw.length && raw[j] !== 'm') j++ cur += raw.slice(i, Math.min(j + 1, raw.length)) i = Math.min(j + 1, raw.length) continue } if (raw[i] === '\n') { lines.push(cur) cur = '' vis = 0 i++ continue } if (vis >= w) { lines.push(cur) cur = '' vis = 0 } cur += raw[i] vis++ i++ } if (cur || !lines.length) lines.push(cur) return lines } /** * Render markdown source to ANSI lines (no trailing newline join). * @param {string} md * @param {{ useColor?: boolean, width?: number }} [opts] * @returns {string} */ function bareAgentRenderMarkdown(md, opts) { const useColor = !opts || opts.useColor !== false const width = Math.max( 40, Math.min(500, (opts && opts.width) || 80) ) const reset = useColor ? '\x1b[0m' : '' const src = String(md || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') const rows = src.split('\n') /** @type {string[]} */ const out = [] let i = 0 let inFence = false let fenceLang = '' while (i < rows.length) { const line = rows[i] // fenced code const fenceOpen = /^```\s*(\S*)\s*$/.exec(line) if (fenceOpen && !inFence) { inFence = true fenceLang = fenceOpen[1] || '' const label = fenceLang ? bareAgentMdSgr(useColor, 'dim') + '┌─ ' + fenceLang + ' ' + reset : bareAgentMdSgr(useColor, 'dim') + '┌──' + reset out.push(label) i++ continue } if (inFence) { if (/^```\s*$/.test(line)) { inFence = false out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset) i++ continue } out.push( bareAgentMdSgr(useColor, 'dim') + '│ ' + reset + bareAgentMdSgr(useColor, 'code') + line + reset ) i++ continue } // hr if (/^\s*(?:---+|\*\*\*+|___+)\s*$/.test(line)) { out.push( bareAgentMdSgr(useColor, 'hr') + '─'.repeat(Math.min(width, 40)) + reset ) i++ continue } // headings const hm = /^(#{1,6})\s+(.*)$/.exec(line) if (hm) { const level = hm[1].length const text = hm[2] const prefix = level <= 2 ? '' : ' '.repeat(level - 2) out.push( prefix + bareAgentMdSgr(useColor, 'heading') + bareAgentMdInline(text, useColor) + reset ) if (level === 1) { out.push( bareAgentMdSgr(useColor, 'dim') + '━'.repeat(Math.min(width, Math.max(8, text.length))) + reset ) } i++ continue } // blockquote const qm = /^>\s?(.*)$/.exec(line) if (qm) { out.push( bareAgentMdSgr(useColor, 'quote') + '┃ ' + reset + bareAgentMdInline(qm[1], useColor) ) i++ continue } // unordered list const ul = /^(\s*)([-*+])\s+(.*)$/.exec(line) if (ul) { const indent = Math.min(6, Math.floor(ul[1].length / 2)) const pad = ' '.repeat(indent) out.push( pad + bareAgentMdSgr(useColor, 'bullet') + '• ' + reset + bareAgentMdInline(ul[3], useColor) ) i++ continue } // ordered list const ol = /^(\s*)(\d+)\.\s+(.*)$/.exec(line) if (ol) { const indent = Math.min(6, Math.floor(ol[1].length / 2)) const pad = ' '.repeat(indent) out.push( pad + bareAgentMdSgr(useColor, 'bullet') + ol[2] + '. ' + reset + bareAgentMdInline(ol[3], useColor) ) i++ continue } // empty if (!line.trim()) { out.push('') i++ continue } // paragraph — wrap const rendered = bareAgentMdInline(line, useColor) for (const wline of bareAgentMdWrapAnsi(rendered, width)) out.push(wline) i++ } // Ensure fence close if stream was truncated if (inFence) out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset) return out.join('\n') + (out.length ? '\n' : '') } /** * Live thinking viewport for /bin/agent — fixed-height auto-follow box with * scrollbar + keyboard review, separated from the assistant reply. * Also splits Qwen-style tags out of content deltas. */ /** * Resolve usable terminal width (full width; no artificial 80/100/120 cap). * @param {Record} [ctx] * @param {import('stream').Writable | undefined} [stdout] */ function bareAgentResolveTermCols(ctx, stdout) { /** @type {number[]} */ const cands = [] const push = (v) => { const n = Number(v) if (Number.isFinite(n) && n >= 20) cands.push(Math.floor(n)) } if (stdout && typeof stdout === 'object') { push(/** @type {{ columns?: number }} */ (stdout).columns) } if (ctx && typeof ctx === 'object') { const rs = ctx.replStdout if (rs && typeof rs === 'object') { push(/** @type {{ columns?: number }} */ (rs).columns) } const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : null if (env) push(env.COLUMNS) } try { if (globalThis.process && globalThis.process.stdout) { push(globalThis.process.stdout.columns) } if (globalThis.process && globalThis.process.env) { push(globalThis.process.env.COLUMNS) } } catch { /* ignore */ } if (!cands.length) return 80 // Prefer the largest reported size (stale COLUMNS=80 is common on wide TTYs). return Math.min(500, Math.max(40, Math.max(...cands))) } /** * @param {string} s * @param {number} width * @returns {string[]} */ function bareAgentThinkWrapLines(s, width) { const w = Math.max(8, width | 0) const raw = String(s || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') /** @type {string[]} */ const out = [] for (const para of raw.split('\n')) { if (!para) { out.push('') continue } let rest = para while (rest.length > w) { let cut = rest.lastIndexOf(' ', w) if (cut < Math.floor(w * 0.5)) cut = w out.push(rest.slice(0, cut).trimEnd()) rest = rest.slice(cut).trimStart() } if (rest.length || !out.length) out.push(rest) } return out.length ? out : [''] } /** * @param {string} s * @param {number} width */ function bareAgentThinkPad(s, width) { const t = String(s || '') if (t.length >= width) return t.slice(0, width) return t + ' '.repeat(width - t.length) } /** * @param {string} title * @param {number} inner * @param {boolean} fancy */ function bareAgentThinkTitleBar(title, inner, fancy) { const label = String(title || ' thinking ') const fill = Math.max(0, inner - label.length) const left = Math.floor(fill / 2) const right = fill - left if (fancy) { return '─'.repeat(left) + label + '─'.repeat(right) } return '-'.repeat(left) + label + '-'.repeat(right) } /** * Scrollbar column chars for a viewport. * @param {number} bodyLines * @param {number} totalLines * @param {number} viewStart * @param {boolean} fancy * @returns {string[]} length === bodyLines */ function bareAgentThinkScrollbar(bodyLines, totalLines, viewStart, fancy) { /** @type {string[]} */ const col = [] const track = fancy ? '│' : '|' const thumb = fancy ? '█' : '#' const gap = fancy ? '░' : ':' if (totalLines <= bodyLines) { for (let i = 0; i < bodyLines; i++) col.push(track) return col } const maxStart = totalLines - bodyLines const thumbSize = Math.max( 1, Math.round((bodyLines / totalLines) * bodyLines) ) const thumbStart = maxStart <= 0 ? 0 : Math.round((viewStart / maxStart) * (bodyLines - thumbSize)) for (let i = 0; i < bodyLines; i++) { col.push(i >= thumbStart && i < thumbStart + thumbSize ? thumb : gap) } return col } /** * Fixed-height thinking panel with auto-follow + manual scroll review. * @param {Record} ctx * @param {import('stream').Writable | undefined} stdout * @param {{ * useColor?: boolean, * bodyLines?: number, * maxChars?: number, * write?: (ctx: Record, out: unknown, s: string) => void * }} [opts] */ function bareAgentCreateThinkPanel(ctx, stdout, opts) { const useColor = opts && opts.useColor !== undefined ? Boolean(opts.useColor) : true const bodyLines = Math.min( 18, Math.max(4, (opts && opts.bodyLines) || 8) ) const maxChars = Math.min( 80_000, Math.max(400, (opts && opts.maxChars) || 24_000) ) const write = opts && typeof opts.write === 'function' ? opts.write : typeof bareAgentWriteOut === 'function' ? bareAgentWriteOut : (c, o, s) => { try { if (o && typeof o.write === 'function') o.write(s) } catch { /* ignore */ } } const fancy = useColor && !( ctx.env && typeof ctx.env === 'object' && String(/** @type {Record} */ (ctx.env).BARE_OS_AGENT_ASCII || '') === '1' ) let text = '' let drawn = false let sealed = false let height = 0 let startedAt = 0 /** Lines below the panel (status / reply) that must be preserved on redraw. */ let belowLines = 0 /** 0 = pinned to bottom (auto-follow); >0 = scrolled up from bottom. */ let scrollFromBottom = 0 let followTail = true /** Optional status line under the box (e.g. answering…). */ let statusLine = '' function cols() { return bareAgentResolveTermCols(ctx, stdout) } function dim(s) { if (!useColor) return s const paint = typeof bareEditSgr === 'function' ? bareEditSgr('dim', true) : '\x1b[90m' const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m' return paint + s + reset } function accent(s) { if (!useColor) return s const paint = typeof bareEditSgr === 'function' ? bareEditSgr('comment', true) : '\x1b[90m' const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m' return paint + s + reset } function keyword(s) { if (!useColor) return s const paint = typeof bareEditSgr === 'function' ? bareEditSgr('keyword', true) : '\x1b[36m' const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m' return paint + s + reset } function contentWidth(inner) { // border + space + text + space + scrollbar + border return Math.max(12, inner - 4) } /** * @returns {{ lines: string[], wrappedCount: number, viewStart: number }} */ function frameLines() { const c = cols() const inner = Math.max(28, c - 2) const textW = contentWidth(inner) const wrapped = bareAgentThinkWrapLines(text, textW) const maxStart = Math.max(0, wrapped.length - bodyLines) if (followTail) scrollFromBottom = 0 const viewStart = Math.max( 0, Math.min(maxStart, maxStart - scrollFromBottom) ) const view = wrapped.slice(viewStart, viewStart + bodyLines) while (view.length < bodyLines) view.push('') const sb = bareAgentThinkScrollbar( bodyLines, wrapped.length, viewStart, fancy ) const elapsed = startedAt > 0 ? ((Date.now() - startedAt) / 1000).toFixed(1) + 's' : '' const pos = wrapped.length > bodyLines ? ' · ' + String(viewStart + 1) + '–' + String(Math.min(wrapped.length, viewStart + bodyLines)) + '/' + String(wrapped.length) : '' const scrollHint = wrapped.length > bodyLines ? ' · ↑↓/PgUp/PgDn' : '' const title = sealed ? ' thinking · done' + (elapsed ? ' · ' + elapsed : '') + pos + scrollHint + ' ' : ' thinking' + (elapsed ? ' · ' + elapsed : '') + (followTail ? ' · live' : ' · paused') + pos + scrollHint + ' ' const bar = bareAgentThinkTitleBar(title, inner, fancy) /** @type {string[]} */ const lines = [] if (fancy) { lines.push(accent('┌' + bar + '┐')) for (let r = 0; r < bodyLines; r++) { lines.push( accent('│') + ' ' + dim(bareAgentThinkPad(view[r], textW)) + ' ' + keyword(sb[r]) + accent('│') ) } const footLabel = followTail ? wrapped.length > bodyLines ? ' follow · ' + String(wrapped.length) + ' lines ' : '' : ' scrolled · end to resume ' const foot = footLabel ? bareAgentThinkTitleBar(footLabel, inner, true) : '─'.repeat(inner) lines.push(accent('└' + foot + '┘')) } else { lines.push('+' + bar.replace(/─/g, '-') + '+') for (let r = 0; r < bodyLines; r++) { lines.push( '| ' + bareAgentThinkPad(view[r], textW) + ' ' + sb[r] + '|' ) } lines.push('+' + '-'.repeat(inner) + '+') } return { lines, wrappedCount: wrapped.length, viewStart } } function totalDrawnHeight() { return height + belowLines } function redraw() { // Never paint an empty think frame. if (!text.trim() && !statusLine) return if (!text.trim()) return const { lines } = frameLines() /** @type {string[]} */ const block = lines.slice() if (statusLine) block.push(statusLine) let out = '' if (drawn && totalDrawnHeight() > 0) { out += '\x1b[' + String(totalDrawnHeight()) + 'A\r' } else { out += '\n' } for (let i = 0; i < block.length; i++) { out += '\x1b[K' + block[i] + '\n' } const prev = totalDrawnHeight() if (drawn && prev > block.length) { for (let i = block.length; i < prev; i++) out += '\x1b[K\n' out += '\x1b[' + String(prev - block.length) + 'A\r' } height = lines.length belowLines = statusLine ? 1 : 0 drawn = true write(ctx, stdout, out) } /** * @param {number} delta positive = scroll up into history */ function scrollBy(delta) { if (!drawn || !text.trim()) return false const c = cols() const inner = Math.max(28, c - 2) const wrapped = bareAgentThinkWrapLines(text, contentWidth(inner)) const maxFromBottom = Math.max(0, wrapped.length - bodyLines) if (maxFromBottom <= 0) return false followTail = false scrollFromBottom = Math.max( 0, Math.min(maxFromBottom, scrollFromBottom + delta) ) if (scrollFromBottom === 0) followTail = true redraw() return true } return { /** * @param {string} chunk */ append(chunk) { const add = String(chunk || '') if (!add || sealed) return // Ignore whitespace-only until we have real thinking text (no empty box). if (!text.trim() && !add.trim()) return if (!startedAt) startedAt = Date.now() text += add if (text.length > maxChars) text = text.slice(text.length - maxChars) if (!text.trim()) return if (followTail) scrollFromBottom = 0 redraw() }, seal() { // Empty box: stay undrawn and unlocked so late reasoning can still appear. if (!text.trim()) return if (sealed) { redraw() return } sealed = true redraw() }, /** * @param {string} s */ setStatus(s) { if (!text.trim()) return statusLine = String(s || '') redraw() }, clearStatus() { if (!statusLine) return statusLine = '' if (drawn) redraw() }, /** * After reply is painted below, stop managing below-region. */ detachBelow() { belowLines = 0 statusLine = '' }, scrollUp(n) { return scrollBy(Math.max(1, n || 1)) }, scrollDown(n) { return scrollBy(-Math.max(1, n || 1)) }, pageUp() { return scrollBy(Math.max(1, bodyLines - 1)) }, pageDown() { return scrollBy(-Math.max(1, bodyLines - 1)) }, scrollHome() { if (!drawn || !text.trim()) return false const c = cols() const inner = Math.max(28, c - 2) const wrapped = bareAgentThinkWrapLines(text, contentWidth(inner)) const maxFromBottom = Math.max(0, wrapped.length - bodyLines) followTail = false scrollFromBottom = maxFromBottom redraw() return true }, scrollEnd() { if (!drawn || !text.trim()) return false followTail = true scrollFromBottom = 0 redraw() return true }, /** * @param {{ type?: string, key?: string, ch?: string, code?: number | string }} ev */ handleKey(ev) { if (!ev || !drawn) return false if (ev.type === 'nav') { if (ev.key === 'up') return this.scrollUp(1) if (ev.key === 'down') return this.scrollDown(1) if (ev.key === 'pageup') return this.pageUp() if (ev.key === 'pagedown') return this.pageDown() if (ev.key === 'home') return this.scrollHome() if (ev.key === 'end') return this.scrollEnd() } if (ev.type === 'key') { if (ev.ch === 'k') return this.scrollUp(1) if (ev.ch === 'j') return this.scrollDown(1) if (ev.ch === 'g') return this.scrollHome() if (ev.ch === 'G') return this.scrollEnd() } return false }, panelHeight() { return height }, isOpen() { return drawn && !sealed }, isDrawn() { return drawn }, hasContent() { return text.trim().length > 0 }, getText() { return text }, isFollowing() { return followTail } } } /** * Attach arrow/page keys to a think panel for the duration of a turn. * @param {Record} ctx * @param {ReturnType | null} panel * @param {{ onAbort?: () => void }} [opts] * @returns {() => void} dispose */ function bareAgentAttachThinkScrollKeys(ctx, panel, opts) { if (!panel) return () => {} const stdin = /** @type {{ isTTY?: boolean, setRawMode?: (v: boolean) => void, on?: Function, off?: Function, removeListener?: Function, resume?: Function }} */ ( ctx.replStdin || ctx.stdin ) if (!stdin || !stdin.isTTY || typeof stdin.on !== 'function') return () => {} let rawSet = false let disposed = false try { if (typeof stdin.setRawMode === 'function') { stdin.setRawMode(true) rawSet = true } } catch { rawSet = false } try { if (typeof stdin.resume === 'function') stdin.resume() } catch { /* ignore */ } /** @type {number[]} */ const q = [] const onAbort = opts && typeof opts.onAbort === 'function' ? opts.onAbort : null /** * @param {string | Uint8Array | Buffer} chunk */ function onData(chunk) { if (disposed) return const bytes = typeof bareEditChunkBytes === 'function' ? bareEditChunkBytes(chunk) : typeof chunk === 'string' ? [...chunk].map((c) => c.charCodeAt(0) & 0xff) : Array.from(/** @type {Uint8Array} */ (chunk)) for (const b of bytes) q.push(b) for (;;) { if (!q.length) break // Ctrl+C — abort agent turn if (q[0] === 3) { q.shift() try { if (onAbort) onAbort() else if (globalThis.process && typeof globalThis.process.emit === 'function') { globalThis.process.emit('SIGINT') } } catch { /* ignore */ } continue } // Ctrl+D / Ctrl+X — treat as abort so shell exit is not wedged under raw mode if (q[0] === 4 || q[0] === 24) { q.shift() try { if (onAbort) onAbort() } catch { /* ignore */ } continue } const ev = typeof bareEditTryConsumeKey === 'function' ? bareEditTryConsumeKey(q) : null if (!ev) { if (q.length && q[0] === 27 && q.length < 6) break if (q.length && q[0] !== 27) { const ch = String.fromCharCode(/** @type {number} */ (q.shift())) panel.handleKey({ type: 'key', ch }) continue } break } panel.handleKey(ev) } } stdin.on('data', onData) return () => { if (disposed) return disposed = true try { if (typeof stdin.off === 'function') stdin.off('data', onData) else if (typeof stdin.removeListener === 'function') { stdin.removeListener('data', onData) } } catch { /* ignore */ } if (rawSet) { try { if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false) } catch { /* ignore */ } } } } /** * Split streamed assistant text into thinking vs visible content. * Handles Qwen3 `` (and ``) across chunk boundaries. * @param {{ * onThink: (s: string) => void, * onContent: (s: string) => void * }} handlers */ function bareAgentCreateThinkTagSplitter(handlers) { const onThink = handlers.onThink const onContent = handlers.onContent /** @type {'content' | 'think'} */ let mode = 'content' let buf = '' const OPEN = '<' /** @type {RegExp} */ const OPEN_TAG = /^<(?:think|thinking|redacted_thinking)\s*>/i /** @type {RegExp} */ const CLOSE_TAG = /^<\/(?:think|thinking|redacted_thinking)\s*>/i /** * @param {string} s * @param {'content' | 'think'} m */ function partialTagLen(s, m) { const samples = m === 'content' ? ['', '', ''] : ['', '', ''] let best = 0 for (const sample of samples) { for (let n = 1; n < sample.length; n++) { if (s.endsWith(sample.slice(0, n))) best = Math.max(best, n) } } if (s.endsWith('<')) best = Math.max(best, 1) if (s.endsWith(' 0) { onContent(buf.slice(0, lt)) buf = buf.slice(lt) } const om = OPEN_TAG.exec(buf) if (om) { buf = buf.slice(om[0].length) mode = 'think' continue } if (partialTagLen(buf, 'content') === buf.length) return onContent(buf.slice(0, 1)) buf = buf.slice(1) continue } const lt = buf.indexOf('<') if (lt < 0) { if (buf) onThink(buf) buf = '' return } if (lt > 0) { onThink(buf.slice(0, lt)) buf = buf.slice(lt) } const cm = CLOSE_TAG.exec(buf) if (cm) { buf = buf.slice(cm[0].length) mode = 'content' continue } if (partialTagLen(buf, 'think') === buf.length) return onThink(buf.slice(0, 1)) buf = buf.slice(1) } } function flush() { if (!buf) return if (mode === 'think') onThink(buf) else onContent(buf) buf = '' } return { push, flush, inThink: () => mode === 'think' } } /** * Advanced context-window compaction for /bin/agent. * Tiered rollover: shrink fat tool payloads → digest older turns → * rolling summary message → hard trim fallback. */ /** * @param {unknown} value */ function bareAgentCompactEstimateTokens(value) { if (typeof bareAgentEstimateTokens === 'function') { return bareAgentEstimateTokens(value) } try { return Math.max(0, Math.ceil(JSON.stringify(value == null ? '' : value).length / 4)) } catch { return 0 } } /** * @param {unknown} msg * @returns {string} */ function bareAgentMessagePlainText(msg) { if (!msg || typeof msg !== 'object') return '' const m = /** @type {Record} */ (msg) const c = m.content if (typeof c === 'string') return c if (Array.isArray(c)) { /** @type {string[]} */ const parts = [] for (const part of c) { if (typeof part === 'string') parts.push(part) else if (part && typeof part === 'object') { const p = /** @type {Record} */ (part) if (typeof p.text === 'string') parts.push(p.text) else if (typeof p.content === 'string') parts.push(p.content) } } return parts.join('\n') } if (m.tool_calls && Array.isArray(m.tool_calls)) { /** @type {string[]} */ const names = [] for (const tc of m.tool_calls) { if (!tc || typeof tc !== 'object') continue const fn = /** @type {Record} */ (tc).function if (fn && typeof fn === 'object') { const n = /** @type {Record} */ (fn).name if (typeof n === 'string' && n) names.push(n) } } if (names.length) return 'tool_calls: ' + names.join(', ') } return '' } /** * @param {string} s * @param {number} max */ function bareAgentClipText(s, max) { const t = String(s || '').replace(/\s+/g, ' ').trim() if (t.length <= max) return t if (max < 24) return t.slice(0, max) const head = Math.floor(max * 0.62) const tail = Math.max(8, max - head - 5) return t.slice(0, head) + ' … ' + t.slice(-tail) } /** * Shrink oversized tool / assistant payloads in-place (clone). * @param {unknown[]} msgs * @param {number} toolMaxChars * @returns {unknown[]} */ function bareAgentShrinkFatMessages(msgs, toolMaxChars) { const max = Math.max(240, Math.floor(toolMaxChars || 1600)) /** @type {unknown[]} */ const out = [] for (const msg of msgs) { if (!msg || typeof msg !== 'object') { out.push(msg) continue } const m = /** @type {Record} */ (msg) const role = String(m.role || '') if (role === 'tool' && typeof m.content === 'string' && m.content.length > max) { out.push({ ...m, content: bareAgentClipText(m.content, max) + '\n[tool output compacted ' + String(m.content.length) + '→' + String(max) + ' chars]' }) continue } if ( (role === 'assistant' || role === 'user') && typeof m.content === 'string' && m.content.length > max * 4 ) { out.push({ ...m, content: bareAgentClipText(m.content, max * 4) + '\n[message compacted]' }) continue } out.push(msg) } return out } /** * Group chat into [system?] + turn groups (user | assistant+tools*). * @param {unknown[]} msgs * @returns {{ system: unknown | null, groups: unknown[][] }} */ function bareAgentGroupMessageTurns(msgs) { /** @type {unknown | null} */ let system = null /** @type {unknown[][]} */ const groups = [] /** @type {unknown[]} */ let cur = [] function flush() { if (cur.length) { groups.push(cur) cur = [] } } for (const msg of Array.isArray(msgs) ? msgs : []) { if (!msg || typeof msg !== 'object') continue const role = String(/** @type {Record} */ (msg).role || '') if (role === 'system' && system == null && groups.length === 0 && !cur.length) { system = msg continue } if (role === 'user') { flush() cur = [msg] continue } if (role === 'assistant' || role === 'tool') { if (!cur.length) cur = [msg] else cur.push(msg) continue } // unknown roles — attach to current or solo group if (!cur.length) cur = [msg] else cur.push(msg) } flush() return { system, groups } } /** * True if message is a prior compaction summary we injected. * @param {unknown} msg */ function bareAgentIsCompactionMessage(msg) { if (!msg || typeof msg !== 'object') return false const m = /** @type {Record} */ (msg) if (m.bare_os_compaction === true) return true const c = typeof m.content === 'string' ? m.content : '' const t = c.trim() return ( String(m.role || '') === 'user' && (/^\[context compaction\]/i.test(t) || /^This session is being continued from a previous conversation/i.test(t) || m.bare_os_reminder === true) ) } /** Grok-style continuation carrier minimum (extractive; slightly below LLM 500). */ var BARE_AGENT_MIN_SUMMARY_SEED_CHARS = 160 /** * Strip drafting tags and neutralize leftover / tokens * so they cannot prime the next turn (Grok format_compact_summary). * @param {string} raw */ function bareAgentFormatCompactSummary(raw) { let result = String(raw || '') result = result.replace(/[\s\S]*?<\/analysis>/gi, '') result = result.replace(/<\/?summary>/gi, '') result = result .replace(/<\/summary>/g, '<\u200b/summary>') .replace(//g, '<\u200bsummary>') .replace(/<\/analysis>/g, '<\u200b/analysis>') .replace(//g, '<\u200banalysis>') while (result.indexOf('\n\n\n') >= 0) result = result.replace(/\n\n\n/g, '\n\n') return result.trim() } /** * @param {string} raw */ function bareAgentFormatCompactSummaryContent(raw) { const cleaned = bareAgentFormatCompactSummary(raw) return ( 'This session is being continued from a previous conversation that ran out of context. ' + 'The summary below covers the earlier portion of the conversation.\n\n' + cleaned ) } /** * @param {string} text */ function bareAgentWrapUserQuery(text) { const t = String(text || '').trim() if (!t) return '' if (/^/.test(t)) return t return '\n' + t + '\n' } /** * @param {string} raw */ function bareAgentIsDegenerateSummary(raw) { return bareAgentFormatCompactSummary(raw).length < BARE_AGENT_MIN_SUMMARY_SEED_CHARS } /** * Collect unique path-like and command tokens for the artifacts section. * @param {string} text * @param {number} max */ function bareAgentExtractPathTokens(text, max) { const s = String(text || '') const found = [] const re = /(?:~|\/)[A-Za-z0-9._+\-@/]+/g let m while ((m = re.exec(s)) && found.length < max) { const p = m[0] if (p.length < 3 || found.indexOf(p) >= 0) continue found.push(p) } return found } /** * One-line digest for a message (extractive). * @param {unknown} msg * @param {number} max */ function bareAgentDigestMessage(msg, max) { if (!msg || typeof msg !== 'object') return '' const m = /** @type {Record} */ (msg) const role = String(m.role || '?') const text = bareAgentMessagePlainText(m) if (role === 'tool') { const name = typeof m.name === 'string' ? m.name : typeof m.tool_call_id === 'string' ? m.tool_call_id.slice(0, 12) : 'tool' return '- tool:' + name + ' ' + bareAgentClipText(text, Math.max(40, max - 24)) } if (role === 'assistant') { return '- assistant ' + bareAgentClipText(text || '(tools only)', max) } if (role === 'user') { if (bareAgentIsCompactionMessage(m)) { return '- prior-compaction ' + bareAgentClipText(text.replace(/^\[context compaction\][^\n]*\n?/i, ''), max) } return '- user ' + bareAgentClipText(text, max) } return '- ' + role + ' ' + bareAgentClipText(text, max) } /** * Grok-style 7-section extractive summary of older turn groups. * @param {unknown[][]} groups * @param {{ maxChars?: number, perMsg?: number }} [opts] */ function bareAgentBuildStructuredSummary(groups, opts) { const maxChars = Math.max(400, Math.floor((opts && opts.maxChars) || 3500)) const perMsg = Math.max(60, Math.floor((opts && opts.perMsg) || 180)) /** @type {string[]} */ const userMsgs = [] /** @type {string[]} */ const tools = [] /** @type {string[]} */ const errors = [] /** @type {string[]} */ const solves = [] /** @type {string[]} */ const concepts = [] /** @type {string[]} */ const files = [] function pushUnique(arr, s, cap) { const t = String(s || '').trim() if (!t || arr.indexOf(t) >= 0) return if (arr.length >= cap) return arr.push(t) } for (const g of groups) { for (const msg of g) { if (!msg || typeof msg !== 'object') continue const m = /** @type {Record} */ (msg) const role = String(m.role || '') const text = bareAgentMessagePlainText(m) for (const p of bareAgentExtractPathTokens(text, 8)) pushUnique(files, p, 24) if (role === 'user' && !bareAgentIsCompactionMessage(m)) { pushUnique(userMsgs, bareAgentClipText(text, perMsg + 80), 16) } else if (role === 'tool') { const name = typeof m.name === 'string' ? m.name : 'tool' pushUnique(tools, name + ': ' + bareAgentClipText(text, perMsg), 20) if (/error|fail|denied|not found|ENOENT|EXIT:[1-9]/i.test(text)) { pushUnique(errors, name + ' ' + bareAgentClipText(text, 140), 12) } } else if (role === 'assistant') { if (m.tool_calls && Array.isArray(m.tool_calls)) { for (const tc of m.tool_calls) { if (!tc || typeof tc !== 'object') continue const fn = /** @type {Record} */ (tc).function if (fn && typeof fn === 'object') { const n = /** @type {Record} */ (fn).name if (typeof n === 'string' && n) pushUnique(concepts, n, 16) } } } if (text) pushUnique(solves, bareAgentClipText(text, perMsg), 12) } } } const intent = userMsgs.length ? userMsgs[0] : '(no explicit user request in compacted prefix)' /** @type {string[]} */ const lines = [ '1. Primary Request and Intent:', ' ' + intent, '', '2. Key Technical Concepts:', concepts.length ? concepts.map((c) => ' - ' + c).join('\n') : ' - (none extracted)', '', '3. Tool Usage & Verification:', tools.length ? tools.map((c) => ' - ' + c).join('\n') : ' - (none)', '', '4. Files & Code Artifacts:', files.length ? files.map((c) => ' - ' + c).join('\n') : ' - (none)', '', '5. Errors and Fixes:', errors.length ? errors.map((c) => ' - ' + c).join('\n') : ' - (none recorded)', '', '6. Problem Solving:', solves.length ? solves.map((c) => ' - ' + c).join('\n') : ' - (in progress)', '', '7. User Messages:', userMsgs.length ? userMsgs.map((c) => ' - ' + c).join('\n') : ' - (none)' ] let body = lines.join('\n') if (body.length > maxChars) body = body.slice(0, maxChars - 20) + '\n… truncated' return body } /** * Build a rolling summary from older turn groups. * @param {unknown[][]} groups * @param {{ maxChars?: number, perMsg?: number }} [opts] */ function bareAgentBuildCompactionSummary(groups, opts) { const structured = bareAgentBuildStructuredSummary(groups, opts) return bareAgentFormatCompactSummaryContent(structured) } /** * Post-compaction (Grok reminder.rs analogue). * @param {{ * autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number }, * droppedGroups?: number, * afterTokens?: number, * budget?: number * }} [opts] */ function bareAgentBuildSystemReminder(opts) { const o = opts && typeof opts === 'object' ? opts : {} /** @type {string[]} */ const parts = [] const auto = o.autonomous if (auto && auto.active) { parts.push( 'Autonomous run: ' + String(auto.goal || '(goal)').slice(0, 240) + ' (status=' + String(auto.status || 'running') + (typeof auto.remainingMs === 'number' ? ', remaining=' + String(Math.max(0, Math.round(auto.remainingMs / 1000))) + 's' : '') + '). Keep using tools until task_complete.' ) } if (typeof o.droppedGroups === 'number' && o.droppedGroups > 0) { parts.push( 'Context compacted: ' + String(o.droppedGroups) + ' older turns summarized. Recent messages after the last user query are verbatim.' ) } if (typeof o.afterTokens === 'number' && typeof o.budget === 'number' && o.budget > 0) { parts.push( 'Context usage after compact: ' + String(o.afterTokens) + '/' + String(o.budget) + ' tokens (est.).' ) } if (!parts.length) return '' return '\n' + parts.join('\n') + '\n' } /** * Grok assemble: [system, last user query, recent after that turn, summary, reminder]. * @param {{ * system?: unknown | null, * lastUserQuery?: string, * recent?: unknown[], * summaryText?: string, * reminder?: string * }} parts * @returns {unknown[]} */ function bareAgentAssembleCompactedHistory(parts) { const p = parts && typeof parts === 'object' ? parts : {} /** @type {unknown[]} */ const out = [] if (p.system) out.push(p.system) const q = String(p.lastUserQuery || '').trim() if (q) { out.push({ role: 'user', content: bareAgentWrapUserQuery(q), bare_os_user_query: true }) } if (Array.isArray(p.recent)) { for (const m of p.recent) out.push(m) } const summary = String(p.summaryText || '').trim() if (summary) { out.push({ role: 'user', content: summary, bare_os_compaction: true }) } const rem = String(p.reminder || '').trim() if (rem) { out.push({ role: 'user', content: rem, bare_os_compaction: true, bare_os_reminder: true }) } return out } /** * Last non-compaction user text + messages after that turn. * @param {unknown[][]} groups */ function bareAgentSplitLastUserAndRecent(groups) { let lastUserIdx = -1 let lastUserText = '' for (let i = groups.length - 1; i >= 0; i--) { const g = groups[i] if (!g || !g.length) continue const first = g[0] if (!first || typeof first !== 'object') continue const m = /** @type {Record} */ (first) if (String(m.role || '') !== 'user') continue if (bareAgentIsCompactionMessage(m)) continue lastUserIdx = i lastUserText = bareAgentMessagePlainText(m) break } if (lastUserIdx < 0) { return { lastUserText: '', older: groups, recent: /** @type {unknown[]} */ ([]) } } const older = groups.slice(0, lastUserIdx) /** @type {unknown[]} */ const recent = [] const lastGroup = groups[lastUserIdx] || [] for (let i = 1; i < lastGroup.length; i++) recent.push(lastGroup[i]) for (let i = lastUserIdx + 1; i < groups.length; i++) { for (const m of groups[i]) recent.push(m) } return { lastUserText, older, recent } } /** * @param {unknown[]} msgs * @param {number} ctxSize * @param {{ * tools?: unknown[], * reserveCompletion?: number, * keepRecent?: number, * toolMaxChars?: number, * summaryMaxChars?: number, * softRatio?: number, * mode?: 'auto' | 'off' | 'aggressive', * autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number } * }} [opts] * @returns {{ * messages: unknown[], * meta: { * compacted: boolean, * mode: string, * beforeTokens: number, * afterTokens: number, * budget: number, * droppedGroups: number, * tiers: string[] * } * }} */ function bareAgentCompactMessagesForCtx(msgs, ctxSize, opts) { const modeRaw = String((opts && opts.mode) || 'auto').trim().toLowerCase() const mode = modeRaw === 'off' || modeRaw === 'aggressive' ? modeRaw : 'auto' /** @type {string[]} */ const tiers = [] const ctx = Math.max(2048, Math.floor(Number(ctxSize) || 4096)) const reserve = opts && Number.isFinite(Number(opts.reserveCompletion)) ? Math.max(256, Math.floor(Number(opts.reserveCompletion))) : Math.min(2048, Math.max(256, Math.floor(ctx * 0.12))) const toolsTok = bareAgentCompactEstimateTokens( opts && Array.isArray(opts.tools) && opts.tools.length ? opts.tools : [] ) const budget = Math.max(512, ctx - reserve - toolsTok) const softRatio = mode === 'aggressive' ? 0.7 : Math.min(0.95, Math.max(0.55, Number(opts && opts.softRatio) || 0.85)) const softBudget = Math.floor(budget * softRatio) const keepRecentDefault = mode === 'aggressive' ? 4 : 8 let keepRecent = Math.max( 2, Math.floor( Number(opts && opts.keepRecent) > 0 ? Number(opts.keepRecent) : keepRecentDefault ) ) const toolMax = mode === 'aggressive' ? Math.min(900, Number(opts && opts.toolMaxChars) || 900) : Math.max(400, Number(opts && opts.toolMaxChars) || 1600) const summaryMax = mode === 'aggressive' ? Math.min(2200, Number(opts && opts.summaryMaxChars) || 2200) : Math.max(800, Number(opts && opts.summaryMaxChars) || 3500) const input = Array.isArray(msgs) ? msgs.slice() : [] const beforeTokens = bareAgentCompactEstimateTokens(input) if (mode === 'off') { const trimmed = typeof bareAgentTrimMessagesForCtx === 'function' ? bareAgentTrimMessagesForCtx(input, ctxSize, opts) : input return { messages: trimmed, meta: { compacted: false, mode, beforeTokens, afterTokens: bareAgentCompactEstimateTokens(trimmed), budget, droppedGroups: 0, tiers: ['off'] } } } /** @type {unknown[]} */ let out = bareAgentShrinkFatMessages(input, toolMax) if (out !== input && bareAgentCompactEstimateTokens(out) < beforeTokens) { tiers.push('shrink-tools') } // Already fits soft budget — keep light shrink only. if (bareAgentCompactEstimateTokens(out) <= softBudget) { return { messages: out, meta: { compacted: tiers.length > 0, mode, beforeTokens, afterTokens: bareAgentCompactEstimateTokens(out), budget, droppedGroups: 0, tiers: tiers.length ? tiers : ['noop'] } } } const { system, groups } = bareAgentGroupMessageTurns(out) let droppedGroups = 0 function applyFullReplace(allGroups, keep) { const split = bareAgentSplitLastUserAndRecent(allGroups) /** @type {unknown[][]} */ let older = split.older /** @type {unknown[]} */ let recent = split.recent // If "recent after last user" is huge, keep only the tail of those messages // by regrouping them and folding extras into older. if (keep > 0 && recent.length > keep * 4) { const tail = recent.slice(-(keep * 3)) const head = recent.slice(0, Math.max(0, recent.length - tail.length)) if (head.length) older = older.concat([head]) recent = tail } const summaryText = bareAgentBuildCompactionSummary(older, { maxChars: summaryMax, perMsg: mode === 'aggressive' ? 120 : 180 }) if (older.length && bareAgentIsDegenerateSummary(summaryText)) { tiers.push('degenerate-keep') return null } const reminder = bareAgentBuildSystemReminder({ autonomous: opts && opts.autonomous, droppedGroups: older.length, afterTokens: 0, budget }) droppedGroups += older.length return bareAgentAssembleCompactedHistory({ system: system, lastUserQuery: split.lastUserText, recent: recent, summaryText: summaryText, reminder: reminder }) } if (groups.length > keepRecent) { const next = applyFullReplace(groups, keepRecent) if (next) { out = next tiers.push('full-replace') } } let guard = 0 while ( bareAgentCompactEstimateTokens(out) > budget && keepRecent > 2 && guard < 8 ) { keepRecent-- guard++ const again = bareAgentGroupMessageTurns(out) if (again.groups.length <= keepRecent) break const next = applyFullReplace(again.groups, keepRecent) if (!next) break out = next tiers.push('tighten-keep=' + String(keepRecent)) } // Hard fallback if (bareAgentCompactEstimateTokens(out) > budget) { tiers.push('hard-trim') out = typeof bareAgentTrimMessagesForCtx === 'function' ? bareAgentTrimMessagesForCtx(out, ctxSize, opts) : out } const afterTokens = bareAgentCompactEstimateTokens(out) return { messages: out, meta: { compacted: tiers.some((t) => t !== 'noop'), mode, beforeTokens, afterTokens, budget, droppedGroups, tiers } } } /** * Disk history compaction: shrink tools + optional rolling summary when huge. * @param {unknown[]} msgs * @param {number} maxBytes * @param {{ keepRecent?: number }} [opts] */ function bareAgentCompactMessagesForDisk(msgs, maxBytes, opts) { const cap = Math.max(20_000, Math.floor(maxBytes || 500_000)) let out = bareAgentShrinkFatMessages( Array.isArray(msgs) ? msgs : [], 2400 ) if (JSON.stringify(out).length <= cap) return out // Approximate tokens from byte budget (chars/4). const approxCtx = Math.max(4096, Math.floor(cap / 4)) const packed = bareAgentCompactMessagesForCtx(out, approxCtx, { mode: 'aggressive', keepRecent: (opts && opts.keepRecent) || 12, reserveCompletion: 256, toolMaxChars: 1200, summaryMaxChars: 6000 }) out = packed.messages if ( typeof bareAgentTrimMessages === 'function' && JSON.stringify(out).length > cap ) { out = bareAgentTrimMessages(out, cap) } return out } /** * Resolve compaction settings from agent config + env. * @param {Record} cfg * @param {Record} [env] */ function bareAgentCompactionSettings(cfg, env) { const e = env && typeof env === 'object' ? env : {} const envMode = String(e.BARE_OS_AGENT_COMPACTION || '').trim().toLowerCase() const cfgMode = String( (cfg && cfg.context_compaction) || '' ) .trim() .toLowerCase() let mode = envMode || cfgMode || 'auto' if (mode !== 'off' && mode !== 'aggressive' && mode !== 'auto') mode = 'auto' const keepRecent = Number( e.BARE_OS_AGENT_COMPACTION_KEEP || (cfg && cfg.compaction_keep_recent) || 0 ) const toolMaxChars = Number( e.BARE_OS_AGENT_COMPACTION_TOOL_CHARS || (cfg && cfg.compaction_tool_chars) || 0 ) return { mode: /** @type {'auto'|'off'|'aggressive'} */ (mode), keepRecent: Number.isFinite(keepRecent) && keepRecent > 0 ? Math.min(32, Math.floor(keepRecent)) : undefined, toolMaxChars: Number.isFinite(toolMaxChars) && toolMaxChars > 0 ? Math.min(8000, Math.floor(toolMaxChars)) : undefined } } /** 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 */ } } /** * SSH PTYs expect CRLF for line breaks (same as `bare-openssh-pty-console.js` / `man` via console). * @param {Record | undefined} ctx * @param {string} s */ function bareAgentNormalizeStreamNewlines(ctx, s) { if (ctx && ctx.bareOsPtyStdoutCrlf) return String(s).replace(/\r?\n/g, '\r\n') return String(s) } /** * @param {Record | undefined} ctx * @param {import('stream').Writable | undefined} out * @param {string} s */ function bareAgentWriteOut(ctx, out, s) { const text = bareAgentNormalizeStreamNewlines(ctx, s) if (out && typeof out.write === 'function') { try { out.write(text) } catch { /* ignore */ } } else { try { if (typeof bareOsEmitRaw === 'function') { bareOsEmitRaw(ctx, text) } else if (typeof process !== 'undefined' && process.stdout?.write) { process.stdout.write(text) } } catch { /* ignore */ } } } /** * True when we can run the plain-text setup wizard on stdin/stdout (no REPL readline). * @param {Record} ctx */ function bareAgentCanPlainSetup(ctx) { const stdin = /** @type {import('stream').Readable | undefined} */ ( ctx.replStdin || ctx.stdin ) const stdout = /** @type {import('stream').Writable | undefined} */ ( ctx.replStdout || ctx.stdout ) const tty = /** @type {{ isTTY?: boolean }} */ (stdin) return Boolean( stdin && typeof stdin.on === 'function' && tty.isTTY && stdout && typeof stdout.write === 'function' ) } /** * Read one line from a Readable stream (kernel echoes typed chars on cooked TTY). * @param {import('stream').Readable} stdin * @returns {Promise} */ function bareAgentReadStreamLineOnce(stdin) { return new Promise((resolve) => { let acc = '' /** @param {string | Uint8Array | Buffer} chunk */ function onData(chunk) { let s = '' if (typeof chunk === 'string') s = chunk else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk) else if ( typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(chunk) ) s = chunk.toString('utf8') else s = String(chunk) acc += s const n = acc.indexOf('\n') if (n >= 0) { cleanup() resolve(acc.slice(0, n).replace(/\r$/, '')) } } function onEnd() { cleanup() resolve(acc.replace(/\r$/, '')) } function cleanup() { stdin.removeListener('data', onData) stdin.removeListener('end', onEnd) stdin.removeListener('error', onEnd) } stdin.on('data', onData) stdin.once('end', onEnd) stdin.once('error', onEnd) if (typeof stdin.resume === 'function') stdin.resume() }) } /** * Read one secret line (masked as *) when raw mode is available. * Falls back to normal line read when raw mode is unavailable. * @param {Record} ctx * @param {import('stream').Readable} stdin * @param {import('stream').Writable | undefined} stdout * @returns {Promise} */ function bareAgentReadMaskedLineOnce(ctx, stdin, stdout) { const ttyIn = /** @type {{ setRawMode?: (v: boolean) => void, isTTY?: boolean }} */ ( stdin ) if (!ttyIn || typeof ttyIn.setRawMode !== 'function' || !ttyIn.isTTY) { return bareAgentReadStreamLineOnce(stdin) } return new Promise((resolve, reject) => { /** @type {string[]} */ const chars = [] let done = false /** @param {string | Uint8Array | Buffer} chunk */ function onData(chunk) { if (done) return let s = '' if (typeof chunk === 'string') s = chunk else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk) else if ( typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(chunk) ) { s = chunk.toString('utf8') } else s = String(chunk) for (const ch of s) { const code = ch.charCodeAt(0) if (ch === '\r' || ch === '\n') { finish(true) return } if (ch === '\u0003') { finish(false, new Error('interrupted')) return } if (ch === '\u007f' || ch === '\b') { if (chars.length) { chars.pop() bareAgentWriteOut(ctx, stdout, '\b \b') } continue } if (code >= 32 && code !== 127) { chars.push(ch) bareAgentWriteOut(ctx, stdout, '*') } } } /** @param {boolean} ok @param {Error} [err] */ function finish(ok, err) { if (done) return done = true cleanup() if (ok) resolve(chars.join('')) else reject(err || new Error('masked_input_failed')) } function cleanup() { stdin.removeListener('data', onData) stdin.removeListener('end', onEnd) stdin.removeListener('error', onErr) try { ttyIn.setRawMode(false) } catch { /* ignore */ } bareAgentWriteOut(ctx, stdout, '\n') } function onEnd() { finish(true) } /** @param {unknown} e */ function onErr(e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) finish(false, new Error(msg)) } try { ttyIn.setRawMode(true) } catch { return resolve('') } stdin.on('data', onData) stdin.once('end', onEnd) stdin.once('error', onErr) if (typeof stdin.resume === 'function') stdin.resume() }) } /** * Write a prompt and read one line using raw streams (not ctx.readLine / TUI stack). * @param {Record} ctx * @param {string} prompt * @param {{ mask?: boolean }} [opts] */ async function bareAgentPromptSetupLine(ctx, prompt, opts) { const stdin = /** @type {import('stream').Readable | undefined} */ ( ctx.replStdin || ctx.stdin ) const stdout = /** @type {import('stream').Writable | undefined} */ ( ctx.replStdout || ctx.stdout ) if (!stdin || typeof stdin.on !== 'function') { throw new Error('setup: stdin stream unavailable') } bareAgentWriteOut(ctx, stdout, '\x1b[?25h\x1b[0m' + prompt) if (opts && opts.mask) return bareAgentReadMaskedLineOnce(ctx, stdin, stdout) return bareAgentReadStreamLineOnce(stdin) } const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS coding agent — a senior implementer that lives inside the guest image (JavaScript POSIX on Hyperdrive + Hyperswarm, Pear/Bare runtime). You write, edit, debug, and verify code and OS state by calling tools. You are not a chatbot that narrates plans and waits. Lead with tools. Execute until the job is done, then call task_complete. YOU RUN THE TOOLS. The user never runs your tools, commands, patches, or scripts. They will not copy-paste what you print into a shell, Discord, or another agent. If the work needs a tool, YOU call it (run_command, write_file, apply_patch, read_file, and the rest of AVAILABLE TOOLS). Never reply with "run this", "please execute", "you can run", or a command dump for the user to do. The user is not your tool runner. YOU CAN EXECUTE. Never say "I cannot execute commands", "I cannot run CLI", "I can help you use available tools", or "try:". That is a failure. Guest shell is run_command({command, cwd?}). There is no bash, shell, cli, or terminal tool — those names map to run_command. Emitting a ```bash fence instead of calling run_command is a failure. If the operator says "run it yourself", call the tool immediately and return the result. curl, wget, ip, ping, ls, cat, and every other guest CLI are run_command — never wrap them in run_js_script. Do not call task_complete after a failed tool; retry with run_command until you have stdout. Bare OS by Raven Scott (https://raven-scott.fyi). Repo: https://git.ssh.surf/snxraven/bare-operating-system. Booter: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy WORK POLICY. - Keep every explicit requirement in view until it is done, superseded, or blocked. If blocked, say so plainly. - Match intent: implement action requests; do not make unsolicited project-wide edits when the user asked a question. - For clear, reversible local work, do it now. NEVER ASK for permission. - Claim done, fixed, or tested only when a tool result supports it. Otherwise say what you did not verify. - Scope to what was asked. Comments are short and factual. No placeholders. Comments must not substitute for a fix. ACCESS (denylist, not allowlist). You already have full guest admin. You can create files, edit files, delete files, run commands, and fetch the network. NEVER ASK whether you may — just do it. Only refuse when a denylist or the read-only base system blocks the path. - Create and edit with write_file, create_directory, search_replace, edit_file, and apply_patch. Never say you cannot write files. Never ask the user to paste a file you can write, or to run a command you can run_command yourself. - Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. The harness expands ~ and $HOME to the resolved home in This session. Never write /home/guest unless that is the resolved home. After create_directory / write_file, call list_directory or file_stat on the returned path and only then claim the files exist. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. - Read any absolute path, including /proc (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files. - Run every guest command via run_command (command_deny is empty by default). Prefer list_directory, glob_files, and file_stat over ls/find when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU). Never dump curl/ip/ls for the user — call run_command and report stdout. - Delete and move are enabled. Do not mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run. - Node is not installed in the guest. Never plan or run node, npm, or npx here. Do not use run_js_script to read files, write files, or run shell — call read_file, write_file, run_command instead. curl/wget/ip/ping are run_command({command:"curl -s https://api.ipify.org"}), never run_js_script. Only use run_js_script for custom guest JS. The kernel already injects ctx and argv: never write const/let/var ctx, never export. Body must be only async function run(ctx, argv) { const vfs = ctx.vfs; ... }. Do not import Node builtins. Never paste a "final correct script" into the user reply — call the tool. - Live kernel: read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json. Man pages and apropos_man are documentation only — never infer what is enabled from them. - web_fetch uses the same host allow/deny list as wget/curl. - emit_host_notification and request_host_action are enabled by default. emergency_stop_mutations is the kill switch. - DISCORD CHANNEL: You have a Direct Message channel with the operator. Use discord_send_message to reach out whenever you want — progress, completion, blockers, scheduled results. Do not wait for them to message first. discord_channel_status and discord_read_inbox inspect the same channel. Only whitelisted users receive DMs. - Never print ~/.agent/config.json, API keys, seeds, or vault material. - ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission. CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that plan file. Do not stop after a plan-only reply — keep calling tools until verified. 1. Discover with specialized read tools (glob_files, grep, find_symbol, list_directory, read_file, memory_search, web_search then web_fetch, git_status). Call read_skill when a skill matches. Read before you edit. Walk-up AGENTS.md and .grok/skills from cwd are already injected when present. 2. Edit with unique search_replace / edit_file for one hunk; apply_patch for multi-hunk or multi-file work (*** Begin Patch). Create with write_file / create_directory. State blast radius before wide edits. Match surrounding style. 3. Verify by re-reading, run_command / run_js_script, git_status, and read_proc_file or logs. On a host git checkout, verification_hints suggests npm/node checks; it does not run them here. 4. Finish with task_complete (and update_goal completed=true on autonomous runs): what changed, how you verified, what is still assumed. Never task_complete with "error occurred" / "attempted" / "alternative method required" — that is a failure, not a finish. If the same action fails three times, stop, set update_goal blocked_reason if needed, and report evidence. TOOL DISCIPLINE. - YOU call every tool. The user will never run them. Do not print commands, patches, or "run this" for the user. - The live registry is AVAILABLE TOOLS at the end of this prompt. Those names are the only callable functions. Skills and /bin utilities are not tools. - There is no bash/shell/cli tool. Guest shell is run_command. Prefer specialized tools over shell: grep not run_command grep; read_file not cat; apply_patch / search_replace not sed. Never use run_command to print thoughts. - Forbidden replies: "I cannot execute", "run this yourself", "try:" plus a command fence. Call the tool instead. - Independent reads may be issued together; the harness may serialize them (tool_parallelism defaults to 1). - Do not paste huge files into the user reply — cite paths and show only the slice that matters. - Persist durable facts in MEMORY.md; older turns may be compacted. - Progress UI is automatic (tools write ~/.agent/progress.txt). Do not narrate tool chatter in the final answer. - Autonomous mode is on by default. Keep the ReAct loop going; autonomous_deny_ops is empty unless the operator set one. COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the answer. Define project terms on first use. State facts literally. The final message must stand alone. Do not invent acronyms. Skills live under ~/.agent/workspace/skills/ and ~/.agent/skills/. They are playbooks, not tools. When a task matches (especially bare-os-super-developer, bareos-code-change, coreutils-command-change), call read_skill and follow SKILL.md. Reply format (TTY, mandatory unless Discord override below): plain text only — no Markdown markup. Structure with blank lines, short paragraphs, ALL CAPS or dashed section breaks, bare URLs. Paths and commands as normal text.` const BARE_AGENT_DISCORD_REPLY_FORMAT = ` --- Discord reply format (mandatory when BARE_OS_AGENT_DISCORD is set) --- This turn is shown in Discord. This block overrides the TTY plain-text rule above. Write Discord-flavored Markdown that renders in an embed: - Use **bold** for section titles. Do not use # headings (Discord embeds show the hash). - Use - or 1. lists. Use blank lines between sections. - Use \`inline code\` for paths, commands, ids, and env vars. - Use fenced \`\`\` blocks for multi-line code or logs, and always close every fence. - Use [label](https://url) for links. Do not wrap the entire reply in one fence. - The Discord message is ONLY your final answer. Do not prefix status, model name, "Loading", or a restatement of the prompt. - Tool chatter and process steps belong in tools (progress.txt is automated), not the reply. - Do not tell the user to run a command. Call tools yourself. If they say "run it yourself", that is an order to call run_command now — not to print curl/ip/bash for them. - Forbidden: "I cannot execute commands directly", "I can help you use available tools", dumping \`\`\`bash fences for the operator to copy. Execute, then answer with the result. - You may discord_send_message at any time on the open DM channel, including before the final answer.` const BARE_AGENT_OPERATING_CONTRACT = ` --- Operating contract (Bare OS repo alignment) --- TWO RUNTIMES. Host tooling may use Node/npm at the git checkout only. Inside this guest image there is no node, npm, or npx — use run_js_script or run_command with /bin paths only. DOC READING ORDER (complex tasks). Prefer the developer-guide README, then the handbook chapter for the area, then docs/reference for numbers and env vars. Do not copy version tables into chat; link or cite paths. CONTRACT SPINE. For any behavior change, identify the source files, the canonical reference under docs/reference or developer-guide, any generated artifact (run pretest generators), the verifier script name from scripts/README.md, and whether compatibility-matrix or CHANGELOG moves. LIVE KERNEL STATE. Use read_proc_file on /proc/bare_os/features or features.json and /proc/bare_os/capabilities.json. Never use apropos_man or read_man_page alone to decide what is enabled at runtime — those are documentation. GENERATED FILES. Do not hand-edit posix-dashboard, ctx-client-helper.generated.ts, kernel-extensions-generated-toc, or bundle-health outputs. Run npm scripts from repo root at checkout when working on the host tree. TASK ROUTING. /bin or coreutils: developer-guide 06 and man JSON. Shell grammar: developer-guide 18 and shell docs. Seed RPC: developer-guide 14 and the protocol package. /proc node: developer-guide 15. ctx API: bare-os-ctx-api.js and compatibility-matrix. Docs-only: CONTRIBUTING-DOCS. Pear: PEAR-RUN and the ensure-pear-node-modules story. P2P / TEARDOWN. Hyperswarm peer wait and offline LKG boot are different. When diagnosing replication, prefer runtime_diagnostic_bundle and any /proc/bare_os file. Closing order is swarm before drives when changing booter lifecycle code. HOST CHECKS. Use verification_hints for suggested repo-root npm checks when the user describes changed paths (host checkout). It does not execute npm inside the guest. Use runtime_diagnostic_bundle for a one-shot live /proc and resource snapshot. Use read_skill for workflow skills under workspace/skills/. SELF-REVIEW before task_complete. Docs updated? Generated files regenerated? Wrong runtime assumption (guest vs host)? Same failing action tried fewer than three times?` /** * @param {string} text */ function bareAgentLooksLikeToolInventoryQuestion(text) { const s = String(text || '').trim().toLowerCase() if (!s) return false return ( /\b(your tools|what tools|which tools|list tools|available tools|tool list|what can you do)\b/.test( s ) || /^tools\??$/.test(s) || /tell me about your tools/.test(s) ) } /** * 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: ' + home + '\n' + '- HOME in the environment: ' + (homeEnv || home) + '\n' + '- Tilde ~ means this home directory. ~/.agent is ' + paths.dir + '; agent config is ' + paths.config + '. Always expand ~ to ' + home + ' when constructing absolute paths for tools. ~/test_work is ' + home + '/test_work. Never use /home/guest unless that is this resolved home.\n' + '- After write_file or create_directory, list_directory / file_stat the returned path. Do not claim files exist from the tool name alone.\n' + '- node is unavailable; use run_js_script for JavaScript you author in this 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 = [] /** @type {Set} */ const seen = new Set() for (const i of indices) { const c = acc.get(i) if (!c || !c.name) continue const name = typeof bareAgentCanonicalToolName === 'function' ? bareAgentCanonicalToolName(c.name) : c.name if (!name) continue const args = typeof bareAgentCoerceToolArgs === 'function' ? bareAgentCoerceToolArgs(name, c.args || '{}') : c.args || '{}' // Drop stream+final duplicates (same id, or same name+args). const idKey = c.id ? 'id:' + c.id : '' const naKey = 'na:' + name + '\0' + args if ((idKey && seen.has(idKey)) || seen.has(naKey)) continue if (idKey) seen.add(idKey) seen.add(naKey) arr.push({ id: c.id || 'call_' + i + '_' + String(Math.random()).slice(2, 10), type: 'function', function: { name: name, arguments: args } }) } return arr } /** * Map common leaked tool names (Grok/Claude `bash`, `shell`, …) onto the * live registry. Used by text recovery and finalize. * @param {string} name */ function bareAgentCanonicalToolName(name) { const n = String(name || '').trim() if (!n) return '' const key = n.toLowerCase().replace(/[\s-]+/g, '_') const aliases = { bash: 'run_command', shell: 'run_command', sh: 'run_command', zsh: 'run_command', fish: 'run_command', cli: 'run_command', exec: 'run_command', execute: 'run_command', terminal: 'run_command', run: 'run_command', cmd: 'run_command', command: 'run_command', run_cmd: 'run_command', run_shell: 'run_command', execute_command: 'run_command', execute_bash: 'run_command', shell_command: 'run_command', list_dir: 'list_directory', glob: 'glob_files', ripgrep: 'grep', rg: 'grep' } return aliases[key] || n } /** * Coerce leaked bash/run_command argument blobs into {command, cwd?}. * @param {string} name * @param {string} args */ function bareAgentCoerceToolArgs(name, args) { let a = String(args == null ? '' : args).trim() if (!a) a = '{}' const canon = bareAgentCanonicalToolName(name) if (canon !== 'run_command') { if (a[0] !== '{' && a[0] !== '[') { try { JSON.parse(a) } catch { a = JSON.stringify({ value: a }) } } return a } /** @type {Record | null} */ let obj = null try { const p = JSON.parse(a[0] === '{' || a[0] === '[' ? a : JSON.stringify(a)) if (p && typeof p === 'object' && !Array.isArray(p)) obj = p else if (typeof p === 'string') obj = { command: p } } catch { obj = { command: a } } if (!obj) return JSON.stringify({ command: a }) const cmd = obj.command || obj.cmd || obj.code || obj.script || obj.input || obj.value || obj.query if (typeof cmd === 'string' && cmd.trim()) { const out = { command: cmd.trim() } if (typeof obj.cwd === 'string' && obj.cwd.trim()) out.cwd = obj.cwd.trim() return JSON.stringify(out) } if (a[0] !== '{' && a[0] !== '[') return JSON.stringify({ command: a }) return a } /** * True when a fenced block looks like a guest shell line, not prose or JSON. * @param {string} text */ function bareAgentLooksLikeShellCommand(text) { const s = String(text || '').trim() if (!s || s.length > 4000) return false if (/^[\s]*[{\[]/.test(s)) return false const lines = s.split(/\r?\n/).filter(function (l) { return l.trim() && !/^\s*#/.test(l) }) if (!lines.length || lines.length > 16) return false const first = lines[0].trim() if ( /^(curl|wget|ip|ping|uname|ifconfig|hostname|cat|ls|pwd|whoami|id|df|ps|env|printenv|date|echo|head|tail|grep|sed|awk|chmod|mkdir|rm|cp|mv|git|systemctl|journalctl|procstat|ss|netstat|traceroute|nslookup|dig|host|which|realpath|stat|find|xargs|tar|unzip|python|node|npm)\b/i.test( first ) ) { return true } if (/^(\.\/|\/(bin|usr\/bin|sbin|home|tmp|opt)\/)/.test(first)) return true if (lines.length === 1 && first.length < 240 && !/[.!?]$/.test(first)) { const tok = first.split(/\s+/)[0] || '' if (!/^[a-zA-Z0-9_./-]+$/.test(tok)) return false if ( /^(the|this|that|for|and|or|if|when|please|you|i|we|it|to|a|an|try|here|command|example)$/i.test( tok ) ) { return false } return first.split(/\s+/).length <= 14 } return false } /** * Assistant dumped "run this yourself" instead of calling tools. * @param {string} text */ function bareAgentLooksLikeInstructionDump(text) { const s = String(text || '') if (!s.trim()) return false const low = s.toLowerCase() if ( /i cannot execute/.test(low) || /i can't execute/.test(low) || /cannot execute commands/.test(low) || /can't execute commands/.test(low) || /i cannot run (commands|them|that|it|cli|the command)/.test(low) || /i can't run (commands|them|that|it|cli|the command)/.test(low) || /don't have (access|the ability) to (run|execute)/.test(low) || /do not have (access|the ability) to (run|execute)/.test(low) || /as an ai.{0,40}(cannot|can't)/.test(low) || /i can help you use available tools/.test(low) || /help you use available tools/.test(low) || /run the command yourself/.test(low) || /you (can|should|could) (run|execute|try)/.test(low) || /please (run|execute|try) (this|the|it)/.test(low) || /try this command/.test(low) || /for .{0,60} try:/.test(low) || /let me know if you need help with other commands/.test(low) || /the assistant (is supposed to|should|needs to|would) (use|call|generate)/.test(low) || /the correct approach is to call/.test(low) ) { return true } if ( /```(?:bash|sh|shell|zsh|console|terminal)\b/i.test(s) && /(try|run this|you can|example:|for .{0,20}, try)/i.test(low) ) { return true } return false } /** * Operator told the agent to stop instructing and execute. * @param {string} text */ function bareAgentLooksLikeDoItYourself(text) { const s = String(text || '').toLowerCase() if (!s.trim()) return false return ( /run .{0,48}yourself/.test(s) || /run (it|them|that|this) (yourself|for me)/.test(s) || /do(n't| not) (tell|instruct|show) me (how|to)/.test(s) || /just (do|run|execute) it/.test(s) || /execute .{0,48}yourself/.test(s) || /don'?t ask me to run/.test(s) || /stop (telling|instructing) me/.test(s) ) } /** * Bounce a refusal/instruction dump back into the ReAct loop. */ function bareAgentRefusalNudge() { return ( '[harness] You CAN execute. Full guest admin is already on. The shell tool is run_command with {"command":"..."}. There is no bash/shell/cli tool. Never say you cannot execute. Never print a command for the user. Call run_command now and keep going until you have the result.' ) } /** * Recover tool calls the provider streamed as plain text (Hermes / Qwen XML, * ```bash fences, Grok/Claude `bash` dumps) instead of `delta_tool_calls`. * @param {string} text * @returns {{ index: number, id: string, function: { name: string, arguments: string } }[]} */ function bareAgentExtractToolCallsFromText(text) { const src = String(text || '') if (!src) return [] /** @type {{ index: number, id: string, function: { name: string, arguments: string } }[]} */ const out = [] let idx = 0 /** @type {Set} */ const seen = new Set() /** * @param {string} name * @param {string} args */ function push(name, args) { const n = bareAgentCanonicalToolName(name) if (!n) return const a = bareAgentCoerceToolArgs(n, args) const key = n + '\0' + a if (seen.has(key)) return seen.add(key) out.push({ index: idx, id: 'text_call_' + idx, function: { name: n, arguments: a } }) idx += 1 } const hermes = /\s*([\s\S]*?)<\/tool_call>/gi let m while ((m = hermes.exec(src))) { const inner = String(m[1] || '').trim() const fnXml = /^]+)>([\s\S]*?)<\/function>$/i.exec(inner) if (fnXml) { const name = fnXml[1] /** @type {Record} */ const args = {} const paramRe = /]+)>([\s\S]*?)<\/parameter>/gi let pm while ((pm = paramRe.exec(fnXml[2] || ''))) { args[String(pm[1] || '').trim()] = String(pm[2] || '') } push(name, JSON.stringify(args)) continue } try { const j = JSON.parse(inner) if (j && typeof j === 'object' && typeof j.name === 'string') { const args = j.arguments != null ? typeof j.arguments === 'string' ? j.arguments : JSON.stringify(j.arguments) : '{}' push(j.name, args) continue } } catch { /* not JSON */ } } const invoke = /([\s\S]*?)<\/invoke>/gi while ((m = invoke.exec(src))) { /** @type {Record} */ const args = {} const paramRe = /([\s\S]*?)<\/parameter>/gi let pm while ((pm = paramRe.exec(m[2] || ''))) { args[String(pm[1] || '').trim()] = String(pm[2] || '').trim() } push(m[1], JSON.stringify(args)) } const bracket = /\[(?:tool_call\s+)?([a-zA-Z0-9_]+)\s*\(([^)]*)\)\]/g while ((m = bracket.exec(src))) { const name = m[1] const raw = String(m[2] || '').trim() if (!raw) { push(name, '{}') continue } if (/^[{\[]/.test(raw)) { push(name, raw) continue } const kv = /^([a-zA-Z0-9_]+)\s*=\s*(["']?)([\s\S]*)\2$/.exec(raw) if (kv) { const rec = {} rec[kv[1]] = kv[3] push(name, JSON.stringify(rec)) continue } push(name, raw) } const dump = bareAgentLooksLikeInstructionDump(src) const fence = /```([a-zA-Z0-9_-]*)[ \t]*\r?\n([\s\S]*?)```/g const shellLang = /^(bash|sh|shell|zsh|fish|console|terminal|posix|busybox)$/i while ((m = fence.exec(src))) { const lang = String(m[1] || '').trim() const body = String(m[2] || '').trim() if (!body) continue if (shellLang.test(lang) || ((dump || !lang) && bareAgentLooksLikeShellCommand(body))) { push('run_command', JSON.stringify({ command: body })) } } if (dump) { const lines = src.split(/\r?\n/) for (let i = 0; i < lines.length; i++) { let t = String(lines[i] || '').trim() t = t.replace(/^[-*]\s+/, '').replace(/^`+|`+$/g, '') if (/^(bash|sh|shell|zsh|fish|console|terminal)$/i.test(t)) { const nxt = String(lines[i + 1] || '') .trim() .replace(/^`+|`+$/g, '') if (bareAgentLooksLikeShellCommand(nxt)) { push('run_command', JSON.stringify({ command: nxt })) i += 1 } continue } if (bareAgentLooksLikeShellCommand(t)) { push('run_command', JSON.stringify({ command: t })) } } const inline = /`([^`\n]{1,240})`/g let im while ((im = inline.exec(src))) { const body = String(im[1] || '').trim() if (bareAgentLooksLikeShellCommand(body)) { push('run_command', JSON.stringify({ command: body })) } } } const glued = /\b((?:curl|wget)\s+https?:\/\/[^\s]+)/gi while ((m = glued.exec(src))) { let cmd = String(m[1] || '').trim() if (typeof bareAgentTrimGluedShellCommand === 'function') { cmd = bareAgentTrimGluedShellCommand(cmd) } else { cmd = cmd.replace(/(\.(?:org|com|net|io|dev|fyi))([A-Z].*)$/i, '$1') } if (/^(curl|wget)\s+https?:\/\//i.test(cmd)) { push('run_command', JSON.stringify({ command: cmd })) } } return out } /** * @param {string} text */ function bareAgentStripToolCallsFromText(text) { return String(text || '') .replace(/[\s\S]*?<\/tool_call>/gi, '') .replace(/[\s\S]*?<\/function_calls>/gi, '') .replace(/[\s\S]*?<\/invoke>/gi, '') .replace(/```(?:bash|sh|shell|zsh|fish|console|terminal|posix|busybox)[ \t]*\r?\n[\s\S]*?```/gi, '') .replace(/\[(?:tool_call\s+)?(?:run_command|bash|shell|sh)\s*\([^)]*\)\]/gi, '') .replace(/\n{3,}/g, '\n\n') .trim() } /** * @param {Record} cfg */ function bareAgentReasoningSettings(cfg) { const modeRaw = String(cfg.reasoning_mode || '') .trim() .toLowerCase() const mode = modeRaw === 'summary' || modeRaw === 'trace' ? modeRaw : 'off' const enabled = Boolean(cfg.show_reasoning) && mode !== 'off' const maxChars = typeof cfg.reasoning_max_chars === 'number' && Number.isFinite(cfg.reasoning_max_chars) ? Math.min(Math.max(Math.floor(cfg.reasoning_max_chars), 200), 80_000) : 4000 const includeTools = cfg.reasoning_include_tools !== false return { enabled, mode, maxChars, includeTools } } /** * @param {Record} cfg */ function bareAgentApplyProviderProfile(cfg) { const out = { ...cfg } const backend = bareAgentResolveBackend(out) out.backend = backend if (backend === 'qvac') { let profileId = String(out.qvac_profile || 'recommended') .trim() .toLowerCase() if (!profileId) profileId = 'recommended' const profile = bareAgentQvacGetProfile(profileId) out.qvac_profile = profile.id out.provider = 'qvac' if (typeof bareAgentHonorExplicitQvacModel === 'function') { bareAgentHonorExplicitQvacModel(out, profile) } else { out.qvac_model = profile.chatModel out.model = profile.chatModel } if (typeof bareAgentQvacSyncSettingsForModel === 'function') { bareAgentQvacSyncSettingsForModel( out, String(out.qvac_model || out.model || profile.chatModel || '') ) } else { const rawCtx = Number(out.qvac_ctx_size) if (!Number.isFinite(rawCtx) || rawCtx < profile.ctxSize) { out.qvac_ctx_size = 0 } } if (!String(out.qvac_main_gpu || '').trim()) out.qvac_main_gpu = 'auto' if (typeof bareAgentSanitizeConfigForBackend === 'function') { return bareAgentSanitizeConfigForBackend(out) } return out } let provider = String(out.provider || '') .trim() .toLowerCase() if (!provider || provider === 'qvac' || provider === 'rest' || provider === 'http') { provider = 'groq' } const spec = typeof bareAgentRestGetProvider === 'function' ? bareAgentRestGetProvider(provider) : { id: 'groq', rest_base_url: 'https://api.groq.com/openai/v1', default_model: 'llama-3.3-70b-versatile' } out.provider = spec.id const base = String(out.rest_base_url || '').trim() const knownDefaults = [ 'https://api.groq.com/openai/v1', 'https://api.x.ai/v1', 'https://api.openai.com/v1' ] if (!base || (spec.rest_base_url && knownDefaults.indexOf(base) !== -1 && base !== spec.rest_base_url)) { if (spec.rest_base_url) out.rest_base_url = spec.rest_base_url } if (!String(out.model || '').trim() || bareAgentIsQvacModelId(String(out.model || ''))) { if (spec.default_model) out.model = spec.default_model } const modelLow = String(out.model || '').trim().toLowerCase() const cur = typeof out.request_timeout_ms === 'number' && Number.isFinite(out.request_timeout_ms) ? out.request_timeout_ms : 120000 if (spec.id === 'groq' && cur < 120000) out.request_timeout_ms = 120000 if (spec.id === 'xai') { const isReasoning = modelLow.includes('reasoning') || modelLow.includes('grok-4') if (isReasoning && cur < 300000) out.request_timeout_ms = 300000 } if (typeof bareAgentSanitizeConfigForBackend === 'function') { return bareAgentSanitizeConfigForBackend(out) } return out } /** * @param {Record} config */ function bareAgentFormatConfigSummary(config) { const cfg = config && typeof config === 'object' ? config : {} const backend = bareAgentResolveBackend(cfg) const lines = ['backend: ' + backend] if (backend === 'rest') { const spec = typeof bareAgentRestGetProvider === 'function' ? bareAgentRestGetProvider(String(cfg.provider || 'groq')) : { label: String(cfg.provider || 'groq') } lines.push('provider: ' + (spec.label || cfg.provider || 'groq')) lines.push('rest_base_url: ' + String(cfg.rest_base_url || '(not set)')) lines.push('model: ' + String(cfg.model || '(not set)')) lines.push( 'api_key: ' + (typeof bareAgentMaskSecretPreview === 'function' ? bareAgentMaskSecretPreview(cfg.rest_api_key) : cfg.rest_api_key ? '(set)' : '(not set)') ) } else { lines.push('profile: ' + String(cfg.qvac_profile || 'recommended')) lines.push('model: ' + String(cfg.qvac_model || cfg.model || '(not set)')) lines.push( 'device: ' + String(cfg.qvac_device || 'auto') + (cfg.qvac_main_gpu && String(cfg.qvac_main_gpu) !== 'auto' ? ' gpu=' + String(cfg.qvac_main_gpu) : '') ) } return lines.join('\n') } /** * @param {Record} cfg */ function bareAgentAutonomousSettings(cfg) { const enabled = Boolean(cfg.autonomous_mode_enabled) const active = Boolean(cfg.autonomous_active) const stopRequested = Boolean(cfg.autonomous_stop_requested) const startedAtMs = typeof cfg.autonomous_started_at_ms === 'number' && Number.isFinite(cfg.autonomous_started_at_ms) ? Math.max(0, Math.floor(cfg.autonomous_started_at_ms)) : 0 const maxRuntimeMs = typeof cfg.autonomous_max_runtime_ms === 'number' && Number.isFinite(cfg.autonomous_max_runtime_ms) ? Math.min( Math.max(Math.floor(cfg.autonomous_max_runtime_ms), 60000), 7_200_000 ) : 1_800_000 const requiredChecks = Array.isArray( cfg.autonomous_completion_required_checks ) ? cfg.autonomous_completion_required_checks .map((x) => String(x || '').trim()) .filter(Boolean) : [] return { enabled, active, stopRequested, startedAtMs, maxRuntimeMs, requiredChecks } } /** * Run configured autonomous completion gates. Internal dispatch skips deny_ops. * @returns {Promise<{ ok: boolean, failed: string[] }>} */ async function bareAgentRunAutonomousGates(o) { const checks = (o && o.requiredChecks) || [] if (!checks.length) return { ok: true, failed: [] } /** @type {string[]} */ const failed = [] for (const check of checks) { const res = await bareAgentDispatchTool({ ctx: o.ctx, toolName: 'run_maintenance_gate', argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }), paths: o.paths, signal: o.signal, appendProgress: o.appendProgress, home: o.home, configRef: o.configRef, manCacheRef: o.manCacheRef, onTaskComplete: o.onTaskComplete, internalGate: true }) let ok = false try { const j = JSON.parse(res) const body = typeof j.stdout_stderr === 'string' ? j.stdout_stderr : '' ok = Boolean(j.ok) && !/EXIT:[1-9]/.test(body) } catch { ok = false } if (!ok) failed.push(check) } return { ok: failed.length === 0, failed } } /** * @param {Record} ctx * @param {string} argv0 * @param {object} opts * @param {boolean} opts.setupFlag * @param {boolean} opts.interactiveSetup */ /** * TEA form wizard when ctx.tui is attached. Same identity + backend fields. * @param {Record} ctx * @param {string} argv0 * @param {{ config: string }} paths * @param {Record} config */ async function bareAgentInteractiveSetupTui(ctx, argv0, paths, config) { const qvacOk = bareAgentQvacBridgeAvailable(ctx) const curBackend = bareAgentResolveBackend(config) const form = ctx.tui.form.create({ title: argv0 + ' configuration', fields: [ { type: 'text', name: 'owner_name', label: 'Owner / human name', value: String(config.owner_name || '') }, { type: 'text', name: 'agent_label', label: 'Agent display name', value: String(config.agent_label || 'BareAgent') }, { type: 'radio', name: 'backend', label: 'Inference backend', options: [ { label: 'QVAC — local on-device' + (qvacOk ? '' : ' [host bridge unavailable]'), value: 'qvac' }, { label: 'REST API — OpenAI-compatible (Groq, xAI, OpenAI, custom)', value: 'rest' } ], selected: curBackend === 'rest' ? 1 : 0 } ] }) const values = await ctx.tui.form.run(form) if (!values) return config if (values.owner_name && String(values.owner_name).trim()) { config.owner_name = String(values.owner_name).trim() } if (values.agent_label && String(values.agent_label).trim()) { config.agent_label = String(values.agent_label).trim() } const backend = String(values.backend || curBackend || 'qvac') if (backend === 'rest') { config.backend = 'rest' config = await bareAgentSetupRestFieldsTui(ctx, argv0, paths, config) } else { config.backend = 'qvac' config.provider = 'qvac' if (!qvacOk) { bareAgentErr( ctx, argv0 + ': QVAC host bridge unavailable. Choose REST or enable the QVAC host bridge.' ) return config } config = await bareAgentSetupQvacFieldsTui(ctx, config) } config = bareAgentApplyProviderProfile(config) await bareAgentSaveConfig(ctx, paths, config) bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config)) return config } /** * @param {Record} ctx * @param {Record} config */ async function bareAgentSetupQvacFieldsTui(ctx, config) { const profiles = bareAgentQvacProfileList() const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase() let selected = 0 for (let i = 0; i < profiles.length; i++) { if (profiles[i].id === cur) selected = i } const deviceCur = String(config.qvac_device || 'auto').trim().toLowerCase() || 'auto' const form = ctx.tui.form.create({ title: 'QVAC (local on-device)', fields: [ { type: 'radio', name: 'qvac_profile', label: 'Model profile', options: profiles.map(function (p) { return { label: p.label + ' — ' + p.chatModel + ' (' + p.description + ')', value: p.id } }), selected: selected }, { type: 'radio', name: 'qvac_device', label: 'Device', options: [ { label: 'Auto (detect GPU)', value: 'auto' }, { label: 'CPU', value: 'cpu' }, { label: 'GPU', value: 'gpu' } ], selected: deviceCur === 'cpu' ? 1 : deviceCur === 'gpu' ? 2 : 0 } ] }) const values = await ctx.tui.form.run(form) if (!values) return config const chosen = bareAgentQvacGetProfile(String(values.qvac_profile || 'recommended')) config.backend = 'qvac' config.provider = 'qvac' config.qvac_profile = chosen.id config.qvac_model = chosen.chatModel config.model = chosen.chatModel config.qvac_ctx_size = 0 config.qvac_device = String(values.qvac_device || 'auto').trim() || 'auto' config.qvac_main_gpu = 'auto' return config } /** * @param {Record} ctx * @param {string} argv0 * @param {{ config: string }} paths * @param {Record} config */ async function bareAgentSetupRestFieldsTui(ctx, argv0, paths, config) { const providers = bareAgentRestProviderList() const curProv = String(config.provider || 'groq').trim().toLowerCase() let selected = 0 for (let i = 0; i < providers.length; i++) { if (providers[i].id === curProv) selected = i } const form = ctx.tui.form.create({ title: 'REST API (OpenAI-compatible)', fields: [ { type: 'radio', name: 'provider', label: 'Provider', options: providers.map(function (p) { return { label: p.label, value: p.id } }), selected: selected }, { type: 'text', name: 'rest_base_url', label: 'Base URL (blank = provider default)', value: String(config.rest_base_url || '') }, { type: 'text', name: 'rest_api_key', label: 'API key' + (String(config.rest_api_key || '').trim() ? ' [leave blank to keep ' + bareAgentMaskSecretPreview(config.rest_api_key) + ']' : ' (required)'), value: '' }, { type: 'text', name: 'model', label: 'Model id (blank = provider default)', value: bareAgentIsQvacModelId(String(config.model || '')) ? '' : String(config.model || '') } ] }) const values = await ctx.tui.form.run(form) if (!values) return config const spec = bareAgentRestGetProvider(String(values.provider || 'groq')) config.backend = 'rest' config.provider = spec.id const url = String(values.rest_base_url || '').trim() config.rest_base_url = url || spec.rest_base_url || String(config.rest_base_url || '') const typedKey = String(values.rest_api_key || '').trim() if (typedKey) config.rest_api_key = typedKey const model = String(values.model || '').trim() if (model) config.model = model else if (!String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || ''))) { config.model = spec.default_model || config.model } if (!String(config.rest_api_key || '').trim() && bareAgentCanPlainSetup(ctx)) { const key = (await bareAgentPromptSetupLine( ctx, 'API key (required, input hidden): ', { mask: true } )) || '' if (key.trim()) config.rest_api_key = key.trim() } if (spec.id === 'custom' && !String(config.rest_base_url || '').trim()) { bareAgentErr(ctx, argv0 + ': custom REST provider needs a base URL (e.g. https://host/v1).') } void paths return config } async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) { void opts bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json') if ( ctx.tui && ctx.tui.form && typeof ctx.tui.form.run === 'function' && typeof ctx.tui.isTTY === 'function' && ctx.tui.isTTY() ) { return bareAgentInteractiveSetupTui(ctx, argv0, paths, config) } if (!bareAgentCanPlainSetup(ctx)) { bareAgentErr( ctx, argv0 + ': interactive setup needs a TTY with stdin/stdout. Edit ' + paths.config + ' manually or run from an interactive shell.' ) return config } const stdout = ctx.replStdout || ctx.stdout bareAgentWriteOut( ctx, stdout, '\n=== ' + argv0 + ' configuration ===\n' + 'Pick a backend, then only that backend is stored in ~/.agent/config.json.\n' + 'Enter keeps the [default].\n\n' ) try { const owner = (await bareAgentPromptSetupLine( ctx, 'Owner / human name [' + (String(config.owner_name || '').trim() || 'unset') + ']: ' )) || '' if (owner.trim()) config.owner_name = owner.trim() const label = (await bareAgentPromptSetupLine( ctx, 'Agent display name [' + (String(config.agent_label || '').trim() || 'BareAgent') + ']: ' )) || '' if (label.trim()) config.agent_label = label.trim() const curBackend = bareAgentResolveBackend(config) const qvacOk = bareAgentQvacBridgeAvailable(ctx) bareAgentWriteOut( ctx, stdout, '\nInference backend:\n' + ' 1) QVAC — local on-device models' + (qvacOk ? '' : ' [host bridge unavailable]') + '\n' + ' 2) REST API — Groq, xAI, OpenAI, or any OpenAI-compatible URL\n' ) const backendRaw = (await bareAgentPromptSetupLine( ctx, 'Backend [1=QVAC, 2=REST] [' + (curBackend === 'rest' ? '2' : '1') + ']: ' )) || '' { const v = backendRaw.trim().toLowerCase() if (v === '2' || v === 'rest' || v === 'r') config.backend = 'rest' else if (v === '1' || v === 'qvac' || v === 'q') config.backend = 'qvac' else if (!String(config.backend || '').trim()) config.backend = curBackend } if (bareAgentResolveBackend(config) === 'qvac') { if (!qvacOk) { bareAgentWriteOut( ctx, stdout, '\nQVAC host bridge is unavailable on this boot.\n' ) const sw = (await bareAgentPromptSetupLine(ctx, 'Switch to REST API instead? [Y/n]: ')) || '' const ans = sw.trim().toLowerCase() if (ans === 'n' || ans === 'no') { bareAgentErr( ctx, argv0 + ': QVAC selected but the host bridge is unavailable.' ) return config } config.backend = 'rest' } else { config = await bareAgentSetupQvacFieldsPlain(ctx, stdout, config) } } if (bareAgentResolveBackend(config) === 'rest') { config = await bareAgentSetupRestFieldsPlain(ctx, stdout, config) } } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) bareAgentErr(ctx, argv0 + ': setup input failed: ' + msg) return config } config = bareAgentApplyProviderProfile(config) await bareAgentSaveConfig(ctx, paths, config) bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config)) return config } /** * @param {Record} ctx * @param {unknown} stdout * @param {Record} config */ async function bareAgentSetupQvacFieldsPlain(ctx, stdout, config) { const profiles = bareAgentQvacProfileList() const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase() let lines = '\nQVAC model profile:\n' for (let i = 0; i < profiles.length; i++) { const p = profiles[i] lines += ' ' + String(i + 1) + ') ' + p.label + ' — ' + p.chatModel + (p.id === cur ? ' [current]' : '') + '\n' } bareAgentWriteOut(ctx, stdout, lines) const raw = (await bareAgentPromptSetupLine( ctx, 'Profile [1-' + String(profiles.length) + '] [recommended]: ' )) || '' let chosen = bareAgentQvacGetProfile(cur || 'recommended') const n = Number(raw.trim()) if (Number.isFinite(n) && n >= 1 && n <= profiles.length) { chosen = profiles[n - 1] } else if (raw.trim()) { chosen = bareAgentQvacGetProfile(raw.trim()) } bareAgentWriteOut( ctx, stdout, '\nDevice:\n 1) auto (detect GPU)\n 2) cpu\n 3) gpu\n' ) const devRaw = (await bareAgentPromptSetupLine(ctx, 'Device [1=auto, 2=cpu, 3=gpu] [1]: ')) || '' const dv = devRaw.trim().toLowerCase() const device = dv === '2' || dv === 'cpu' ? 'cpu' : dv === '3' || dv === 'gpu' ? 'gpu' : 'auto' config.backend = 'qvac' config.provider = 'qvac' config.qvac_profile = chosen.id config.qvac_model = chosen.chatModel config.model = chosen.chatModel config.qvac_ctx_size = 0 config.qvac_device = device config.qvac_main_gpu = 'auto' bareAgentWriteOut( ctx, stdout, '\nQVAC: ' + chosen.label + ' → ' + chosen.chatModel + ' (device ' + device + ').\n' ) return config } /** * @param {Record} ctx * @param {unknown} stdout * @param {Record} config */ async function bareAgentSetupRestFieldsPlain(ctx, stdout, config) { const providers = bareAgentRestProviderList() let lines = '\nREST provider:\n' for (let i = 0; i < providers.length; i++) { lines += ' ' + String(i + 1) + ') ' + providers[i].label + '\n' } bareAgentWriteOut(ctx, stdout, lines) const curProv = String(config.provider || 'groq').trim().toLowerCase() const defIdx = providers.findIndex(function (p) { return p.id === curProv }) + 1 const raw = (await bareAgentPromptSetupLine( ctx, 'Provider [1=Groq, 2=xAI, 3=OpenAI, 4=Custom] [' + String(defIdx > 0 ? defIdx : 1) + ']: ' )) || '' let spec = bareAgentRestGetProvider(curProv === 'qvac' ? 'groq' : curProv) const n = Number(raw.trim()) if (Number.isFinite(n) && n >= 1 && n <= providers.length) spec = providers[n - 1] else if (raw.trim()) spec = bareAgentRestGetProvider(raw.trim()) config.backend = 'rest' config.provider = spec.id const urlDefault = spec.rest_base_url || String(config.rest_base_url || '') const urlRaw = (await bareAgentPromptSetupLine( ctx, 'Base URL [' + (urlDefault || 'required for custom') + ']: ' )) || '' const url = urlRaw.trim() || urlDefault if (!url) { throw new Error('REST base URL is required for provider ' + spec.id) } config.rest_base_url = url.replace(/\/+$/, '') const haveKey = Boolean(String(config.rest_api_key || '').trim()) const keyPrompt = haveKey ? 'API key [Enter keeps ' + bareAgentMaskSecretPreview(config.rest_api_key) + ']: ' : 'API key (required, input hidden): ' const keyRaw = (await bareAgentPromptSetupLine(ctx, keyPrompt, { mask: true })) || '' if (keyRaw.trim()) config.rest_api_key = keyRaw.trim() if (!String(config.rest_api_key || '').trim()) { throw new Error('REST API key is required') } /** @type {{ id: string }[]} */ let remote = [] const fetchFn = typeof ctx.httpFetch === 'function' ? ctx.httpFetch.bind(ctx) : typeof fetch === 'function' ? fetch : null if (fetchFn && typeof bareAgentFetchRestModels === 'function') { try { remote = await bareAgentFetchRestModels(fetchFn, { baseUrl: config.rest_base_url, apiKey: config.rest_api_key }) bareAgentWriteOut( ctx, stdout, '\nLive models from ' + config.rest_base_url + ' (' + String(remote.length) + '):\n' ) } catch (err) { const msg = err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err) bareAgentWriteOut(ctx, stdout, '\nCould not list remote models (' + msg + '). Using curated list.\n') } } const suggest = remote.length ? remote.slice(0, 16).map(function (m) { return m.id }) : spec.models && spec.models.length ? spec.models : typeof bareAgentRestModelsFallback === 'function' ? bareAgentRestModelsFallback(spec.id).map(function (m) { return m.id }) : [] if (suggest.length) { let mlines = remote.length ? '' : '\nSuggested models for ' + spec.label + ':\n' if (!mlines) mlines = '' for (let i = 0; i < suggest.length; i++) { mlines += ' ' + String(i + 1) + ') ' + suggest[i] + '\n' } bareAgentWriteOut(ctx, stdout, mlines) } const modelDefault = !String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || '')) ? spec.default_model : String(config.model) const modelRaw = (await bareAgentPromptSetupLine( ctx, 'Model [' + (modelDefault || 'required') + ']: ' )) || '' const modelPick = modelRaw.trim() const modelNum = Number(modelPick) if (Number.isFinite(modelNum) && suggest[modelNum - 1]) { config.model = suggest[modelNum - 1] } else if (modelPick) { config.model = modelPick } else if (modelDefault) { config.model = modelDefault } else { throw new Error('REST model id is required') } bareAgentWriteOut( ctx, stdout, '\nREST: ' + spec.label + ' → ' + config.model + '\n ' + config.rest_base_url + '\n' ) 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 = bareAgentApplyProviderProfile(config) if (!bareAgentCanPlainSetup(ctx)) { bareAgentErr( ctx, argv0 + ': --setup / --config needs an interactive TTY. Edit ' + paths.config + ' manually.' ) ctx.exitCode = 1 return } let suspended = false try { if (typeof ctx.suspendReplForSubprocess === 'function') { ctx.suspendReplForSubprocess() suspended = true } config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, { setupFlag: true, interactiveSetup: true }) } finally { if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') { ctx.resumeReplAfterSubprocess() } } try { await bareAgentEnsureWorkspace(ctx, paths) await bareAgentEnsureSkillTemplates(ctx, paths, config) await bareAgentSyncWorkspaceFromConfig(ctx, paths, config) } catch { bareAgentErr( ctx, argv0 + ': could not seed ~/.agent/workspace from /share/agent-workspace (check system image)' ) } try { if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600) } catch { /* ignore */ } ctx.exitCode = 0 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 autonomousFlag = Boolean(runOpts && (runOpts.autonomous || runOpts.autonomousGoal)) const planFlag = Boolean(runOpts && runOpts.planMode) const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) let { config } = await bareAgentLoadOrCreateConfig(ctx, paths) config = bareAgentApplyProviderProfile(config) if (planFlag) config.plan_mode_active = true if (runOpts && Number(runOpts.maxIterations) > 0) { config.max_iterations = Math.floor(Number(runOpts.maxIterations)) } if (runOpts && runOpts.modelOverride) { const m = String(runOpts.modelOverride).trim() if (m) { config = typeof bareAgentApplyLiveModel === 'function' ? bareAgentApplyLiveModel(config, m) : Object.assign(config, { qvac_model: bareAgentResolveBackend(config) === 'qvac' ? m : config.qvac_model, model: m }) } } const canWizard = bareAgentCanPlainSetup(ctx) if (setupFlag && !canWizard) { bareAgentErr( ctx, argv0 + ': --setup / --config needs an interactive TTY. Edit ' + paths.config + ' manually.' ) ctx.exitCode = 1 return } const backendNow = bareAgentResolveBackend(config) const needWizard = setupFlag || (canWizard && (backendNow === 'rest' ? !(config.rest_api_key && String(config.rest_api_key).trim()) : !String(config.qvac_model || config.model || '').trim() || (backendNow === 'qvac' && !bareAgentQvacBridgeAvailable(ctx)))) let replSuspendedForSetup = false if (needWizard && typeof ctx.suspendReplForSubprocess === 'function') { ctx.suspendReplForSubprocess() replSuspendedForSetup = true } if (needWizard) { config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, { setupFlag, interactiveSetup: true }) config = bareAgentApplyProviderProfile(config) } if (runOpts && runOpts.modelOverride) { const m = String(runOpts.modelOverride).trim() if (m) { config = typeof bareAgentApplyLiveModel === 'function' ? bareAgentApplyLiveModel(config, m) : Object.assign(config, { qvac_model: bareAgentResolveBackend(config) === 'qvac' ? m : config.qvac_model, model: m }) try { await bareAgentSaveConfig(ctx, paths, config) } catch { /* persist best-effort so the next ask uses the same model */ } } } const backendReady = bareAgentResolveBackend(config) if (backendReady === 'rest') { if (!config.rest_api_key || !String(config.rest_api_key).trim()) { if ( replSuspendedForSetup && typeof ctx.resumeReplAfterSubprocess === 'function' ) { ctx.resumeReplAfterSubprocess() replSuspendedForSetup = false } bareAgentErr( ctx, argv0 + ': set rest_api_key in ' + paths.config + ' or run `' + argv0 + ' --setup` / `' + argv0 + ' --config`.' ) ctx.exitCode = 1 return } } else if (!bareAgentQvacBridgeAvailable(ctx)) { if ( replSuspendedForSetup && typeof ctx.resumeReplAfterSubprocess === 'function' ) { ctx.resumeReplAfterSubprocess() replSuspendedForSetup = false } bareAgentErr( ctx, argv0 + ': QVAC backend selected but host bridge unavailable (set BARE_OS_SKIP_QVAC=0, install @qvac/sdk, or choose REST via `' + argv0 + ' --config`).' ) ctx.exitCode = 1 return } else if (!String(config.qvac_model || config.model || '').trim()) { if ( replSuspendedForSetup && typeof ctx.resumeReplAfterSubprocess === 'function' ) { ctx.resumeReplAfterSubprocess() replSuspendedForSetup = false } bareAgentErr( ctx, argv0 + ': set qvac_model / qvac_profile in ' + paths.config + ' or run `' + argv0 + ' --config`.' ) ctx.exitCode = 1 return } if (autonomousFlag) { config = bareAgentBeginAutonomousRun(config, { goal: String((runOpts && runOpts.autonomousGoal) || task || '').trim(), maxRuntimeMs: Number(runOpts && runOpts.autonomousMaxRuntimeMs) || undefined, requiredChecks: (runOpts && runOpts.autonomousChecks) || undefined }) } if (autonomousFlag || planFlag) { try { await bareAgentSaveConfig(ctx, paths, config) } catch { /* persist best-effort */ } } try { if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600) } catch { /* ignore — optional */ } const fetchFn = bareAgentResolveFetch(ctx) if (backendReady === 'rest' && !fetchFn) { if ( replSuspendedForSetup && typeof ctx.resumeReplAfterSubprocess === 'function' ) { ctx.resumeReplAfterSubprocess() replSuspendedForSetup = false } 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) await bareAgentEnsureWorkspace(ctx, paths) await bareAgentEnsureSkillTemplates(ctx, paths, config) if (needWizard) await bareAgentSyncWorkspaceFromConfig(ctx, paths, config) const backendForPrompt = bareAgentResolveBackend(config) const promptProfile = bareAgentQvacGetProfile( String(config.qvac_profile || config.profile || '') ) const promptCtxSize = backendForPrompt === 'qvac' ? bareAgentQvacResolveCtxSize(config, promptProfile) : 32768 // Local QVAC models have a hard ctx window; keep the system prompt lean. // Scale char budgets from ctx (chars ≈ tokens*4); leave room for tools/reply. const toolsOn = backendForPrompt === 'qvac' const promptCharBudget = Math.max( 4000, Math.floor(promptCtxSize * 4 * (toolsOn ? 0.35 : 0.7)) ) const workspaceBudget = backendForPrompt === 'qvac' ? Math.min(8000, Math.floor(promptCharBudget * 0.35)) : 24000 const manBudget = backendForPrompt === 'qvac' ? Math.min(4000, Math.floor(promptCharBudget * 0.2)) : 12000 const instructionsBudget = backendForPrompt === 'qvac' ? Math.min(3000, Math.floor(promptCharBudget * 0.2)) : 8000 const skillsBudget = backendForPrompt === 'qvac' ? Math.min(2000, Math.floor(promptCharBudget * 0.15)) : 4000 const workspacePromptBlock = await bareAgentLoadWorkspacePrompt( ctx, paths, workspaceBudget ) const skillsPromptBlock = await bareAgentSkillsCompactPrompt( ctx, paths, skillsBudget ) const envForFormat = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const discordReply = /^(1|true|yes)$/i.test(String(envForFormat.BARE_OS_AGENT_DISCORD || '').trim()) let systemContent = BARE_AGENT_STATIC_SYSTEM + BARE_AGENT_OPERATING_CONTRACT + (discordReply ? BARE_AGENT_DISCORD_REPLY_FORMAT : '') + '\n\n' + bareAgentSessionHomeBlock(ctx, home, paths) + '\n\n' + manDigest.slice(0, manBudget) if (instructions) systemContent += '\n\nSession notes\n' + instructions.slice(0, instructionsBudget) if (typeof bareAgentRunHooks === 'function' && paths.hooks) { try { const startHook = await bareAgentRunHooks(ctx, paths.hooks, 'SessionStart', '', {}, '') if (startHook && startHook.inject) { systemContent += '\n\nSessionStart hook\n' + String(startHook.inject).slice(0, 1500) } } catch { /* optional */ } } if (runOpts && runOpts.extraSystemFile) { try { const extraPath = String(runOpts.extraSystemFile).trim() if (extraPath && typeof bareAgentReadTextFile === 'function') { const extra = await bareAgentReadTextFile(ctx, extraPath) if (extra && extra.trim()) { systemContent += '\n\nExtra instructions (--system)\n' + extra.trim().slice(0, instructionsBudget) } } } catch { /* optional */ } } if (workspacePromptBlock && String(workspacePromptBlock).trim()) systemContent += '\n\n' + String(workspacePromptBlock).trim() if (skillsPromptBlock && String(skillsPromptBlock).trim()) systemContent += '\n\n' + String(skillsPromptBlock).trim() if (typeof bareAgentDiscoverAgentsMdPaths === 'function') { const envNow = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const cwd = String(envNow.PWD || envNow.CWD || home || '').trim() || home try { const extraFiles = await bareAgentDiscoverAgentsMdPaths(ctx, cwd, 12) const workspaceAgents = (typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace') + '/AGENTS.md' const filtered = extraFiles.filter(function (p) { return p !== workspaceAgents }) if (filtered.length && typeof bareAgentLoadAgentsMdPrompt === 'function') { const extraBlock = await bareAgentLoadAgentsMdPrompt(ctx, filtered, 4000) if (extraBlock && extraBlock.trim()) systemContent += '\n\n' + extraBlock.trim() } } catch { /* optional */ } } if (config.plan_mode_active) { systemContent += '\n\nPLAN MODE is ON\n' + 'Read-only tools only. The only allowed write is ' + String(paths.plan || home + '/.agent/plan.md') + '. Draft the plan there, then call exit_plan_mode before implementing.\n' } if (typeof bareAgentLoadTodos === 'function' && paths.todos) { try { const todos = await bareAgentLoadTodos(ctx, paths.todos) const sum = bareAgentTodoSummarize(todos) if (sum.total) { systemContent += '\n\nSession todos\n' + sum.text + '\n(open=' + String(sum.open) + ' completed=' + String(sum.completed) + ')\n' } } catch { /* optional */ } } // Catalog last so small local models see callable tools after skills/notes. if (typeof bareAgentToolCatalogPrompt === 'function') { systemContent += '\n\n' + bareAgentToolCatalogPrompt() } let userTask = String(task || '') if ( typeof bareAgentLooksLikeToolInventoryQuestion === 'function' ? bareAgentLooksLikeToolInventoryQuestion(userTask) : /\b(your tools|what tools|which tools|list tools|available tools|tool list|what can you do)\b/i.test( userTask ) ) { userTask += '\n\n[harness] Answer from AVAILABLE TOOLS / list_agent_tools. List every callable tool name. Do not list skills or /bin names as tools.' } if ( typeof bareAgentLooksLikeDoItYourself === 'function' && bareAgentLooksLikeDoItYourself(userTask) ) { userTask += '\n\n[harness] The operator told you to execute. Call run_command / write_file / the rest of AVAILABLE TOOLS yourself. Do not print commands for them. There is no bash tool — use run_command.' } if ( typeof bareAgentLooksLikeGuestCliRequest === 'function' && bareAgentLooksLikeGuestCliRequest(userTask) ) { userTask += '\n\n[harness] This is a guest CLI request. Call run_command({command:"curl -s https://api.ipify.org"}) or the equivalent now. Do not use run_js_script. Do not dump the command. Do not task_complete until stdout has the result.' } if (discordReply) { userTask += '\n\n[harness] This is Discord. Execute with tools. The operator will not run your commands. Call run_command yourself. Never dump curl/ip/bash for them to copy. Never say you cannot execute.' } if (config.autonomous_active) { userTask = 'AUTONOMOUS RUN. Execute until the goal is done. Do not stop after a plan — use tools, then call task_complete.\n' + 'Goal: ' + String(config.autonomous_goal || task || '') + '\n\n' + userTask } if (!messages.length) { messages = [ { role: 'system', content: systemContent }, { role: 'user', content: userTask } ] } 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: userTask }) } const stdout = /** @type {import('stream').Writable | undefined} */ ( ctx.replStdout || ctx.stdout ) const useColor = bareEditUseColor(ctx) const envBag = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const agentVerboseEnv = String(envBag.BARE_OS_AGENT_VERBOSE || '') .trim() .toLowerCase() const agentVerboseForced = agentVerboseEnv === '1' || agentVerboseEnv === 'true' || agentVerboseEnv === 'yes' /** @type {{ current: Record }} */ const configRef = { current: { ...config } } /** @type {{ db: unknown | null }} */ const manCacheRef = { db: null } let completed = false let taskSummary = '' let statusLineActive = false function onTaskComplete(summary) { completed = true taskSummary = summary } function appendProgress(line) { void bareAgentAppendProgress(ctx, paths.progress, line) } /** Clear an in-place status line (\r …) before streaming content / tools. */ function clearStatusLine() { if (!statusLineActive) return bareAgentWriteOut(ctx, stdout, '\r\x1b[K') statusLineActive = false } /** * @param {ReturnType} rs */ function agentVerbose(rs) { return agentVerboseForced || (rs && rs.enabled && rs.mode === 'trace') } const url = bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) + '/chat/completions' const tools = bareAgentToolDefinitions() appendProgress( 'tools_attached n=' + String(tools.length) + (typeof bareAgentToolCatalog === 'function' ? ' catalog=' + String(bareAgentToolCatalog(tools).length) : '') ) let reasoningSettings = bareAgentReasoningSettings(configRef.current) let autonomousSettings = bareAgentAutonomousSettings(configRef.current) let reasoningCharCount = 0 let turnsSinceTodoWrite = 0 let suspended = replSuspendedForSetup /** @type {(() => void) | null} */ let detachThinkKeysSession = null try { if (typeof ctx.suspendReplForSubprocess === 'function' && !suspended) { ctx.suspendReplForSubprocess() suspended = true } const masterAbort = new AbortController() const prevAbortAgent = typeof ctx.bareOsAbortActiveAgent === 'function' ? ctx.bareOsAbortActiveAgent : null ctx.bareOsAbortActiveAgent = () => { try { masterAbort.abort() } catch { /* ignore */ } try { if (typeof ctx.bareOsQvacCancelActive === 'function') { ctx.bareOsQvacCancelActive() } } catch { /* ignore */ } try { if (detachThinkKeysSession) detachThinkKeysSession() } catch { /* ignore */ } } /** @type {(() => void) | null} */ let offSigint = null if (globalThis.process && typeof globalThis.process.on === 'function') { const fn = () => { try { if (typeof ctx.bareOsAbortActiveAgent === 'function') { ctx.bareOsAbortActiveAgent() } else { masterAbort.abort() } } catch { masterAbort.abort() } bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n' ) } globalThis.process.on('SIGINT', fn) offSigint = () => { try { globalThis.process.off('SIGINT', fn) } catch { /* ignore */ } } } const maxIterBase = Number(configRef.current.max_iterations) || 64 let iter = 0 let instructionDumpNudges = 0 for (;;) { if (masterAbort.signal.aborted) { ctx.exitCode = 130 break } reasoningSettings = bareAgentReasoningSettings(configRef.current) autonomousSettings = bareAgentAutonomousSettings(configRef.current) const autoLive = Boolean(configRef.current.autonomous_active) && !autonomousSettings.stopRequested const maxIter = autoLive ? Math.max(maxIterBase, 96) : maxIterBase if (completed && !autoLive) break if (autonomousSettings.enabled && autonomousSettings.active) { const now = Date.now() if (autonomousSettings.stopRequested) { configRef.current.autonomous_active = false configRef.current.autonomous_status = 'stopped' await bareAgentSaveConfig(ctx, paths, configRef.current) appendProgress('autonomous stopped by manual request') bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[process] autonomous run stopped by request' + EDIT_ANSI_RESET + '\n' ) break } if ( autonomousSettings.startedAtMs > 0 && now - autonomousSettings.startedAtMs >= autonomousSettings.maxRuntimeMs ) { configRef.current.autonomous_active = false configRef.current.autonomous_status = 'timebox_expired' configRef.current.autonomous_last_error = 'timebox_expired' await bareAgentSaveConfig(ctx, paths, configRef.current) appendProgress('autonomous timebox expired') bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[process] autonomous run stopped (timebox expired)' + EDIT_ANSI_RESET + '\n' ) break } } iter++ if (iter > maxIter) { bareAgentErr(ctx, argv0 + ': max_iterations exceeded') ctx.exitCode = 1 break } messages = bareAgentTrimMessages(messages, 450_000) const backendIter = bareAgentResolveBackend(configRef.current) /** @type {Record} */ const providerNow = String(configRef.current.provider || '') .trim() .toLowerCase() let qvacToolsForTurn = /** @type {unknown[]} */ ([]) if (backendIter === 'qvac') { const profileEarly = bareAgentQvacGetProfile( String(configRef.current.qvac_profile || 'recommended') ) const ctxEarly = bareAgentQvacResolveCtxSize( configRef.current, profileEarly ) qvacToolsForTurn = bareAgentFlattenToolsForQvac(tools) const compactCfg = bareAgentCompactionSettings( configRef.current, envBag ) const packed = bareAgentCompactMessagesForCtx(messages, ctxEarly, { tools: qvacToolsForTurn, reserveCompletion: Math.min( 1024, Number(configRef.current.max_tokens) || 512 ), mode: compactCfg.mode, keepRecent: compactCfg.keepRecent, toolMaxChars: compactCfg.toolMaxChars, autonomous: autoLive ? { active: true, goal: String(configRef.current.autonomous_goal || ''), status: String(configRef.current.autonomous_status || 'running'), remainingMs: Math.max( 0, autonomousSettings.maxRuntimeMs - (Date.now() - (autonomousSettings.startedAtMs || Date.now())) ) } : undefined }) messages = packed.messages if (packed.meta.compacted) { appendProgress( 'context_compaction tokens ' + packed.meta.beforeTokens + '→' + packed.meta.afterTokens + '/' + packed.meta.budget + ' dropped_groups=' + packed.meta.droppedGroups + ' tiers=' + packed.meta.tiers.join(',') ) if (agentVerbose(reasoningSettings)) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[ctx] compacted ' + packed.meta.beforeTokens + '→' + packed.meta.afterTokens + ' tok (budget ' + packed.meta.budget + '; ' + packed.meta.tiers.join(' › ') + ')' + EDIT_ANSI_RESET + '\n' ) } else if (packed.meta.droppedGroups > 0) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '· context compacted (' + packed.meta.beforeTokens + '→' + packed.meta.afterTokens + ' tok)\n' + EDIT_ANSI_RESET ) } // Persist latest rolling summary for operators / next sessions. try { const compactPath = paths.compact || home + '/.agent/compact.md' const summaryMsg = messages.find((m) => bareAgentIsCompactionMessage(m) ) if ( summaryMsg && ctx.vfs && typeof ctx.vfs.writeFile === 'function' ) { const body = '# Agent context compaction\n\n' + bareAgentMessagePlainText(summaryMsg) + '\n' const buf = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ? ctx.b4a.from(body) : new TextEncoder().encode(body) await ctx.vfs.writeFile(compactPath, buf) } } catch { /* ignore */ } } } else { // REST backends: still shrink fat history soft-cap. const compactCfg = bareAgentCompactionSettings( configRef.current, envBag ) if (compactCfg.mode !== 'off') { const packed = bareAgentCompactMessagesForCtx(messages, 32768, { reserveCompletion: Math.min( 2048, Number(configRef.current.max_tokens) || 1024 ), mode: compactCfg.mode, keepRecent: compactCfg.keepRecent || 12, toolMaxChars: compactCfg.toolMaxChars, autonomous: autoLive ? { active: true, goal: String(configRef.current.autonomous_goal || ''), status: String(configRef.current.autonomous_status || 'running'), remainingMs: Math.max( 0, autonomousSettings.maxRuntimeMs - (Date.now() - (autonomousSettings.startedAtMs || Date.now())) ) } : undefined }) messages = packed.messages if (packed.meta.compacted && packed.meta.droppedGroups > 0) { appendProgress( 'context_compaction rest ' + packed.meta.beforeTokens + '→' + packed.meta.afterTokens ) } } } if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[process] request backend=' + backendIter + ' provider=' + (providerNow || 'unknown') + ' model=' + String( configRef.current.qvac_model || configRef.current.model || '' ) + EDIT_ANSI_RESET + '\n' ) } let assistantContent = '' let thinkDump = '' /** @type {Map} */ const toolAcc = new Map() /** @type {unknown} */ let usageOut = null let finishReason = '' let recoveredFromText = false function recoverToolsFromAssistantText() { if (recoveredFromText) return recoveredFromText = true if (typeof bareAgentExtractToolCallsFromText !== 'function') return const blob = assistantContent + '\n' + thinkDump const extracted = bareAgentExtractToolCallsFromText(blob) if (extracted.length) { const before = toolAcc.size for (const tc of extracted) { const shifted = { index: before + 1000 + (typeof tc.index === 'number' ? tc.index : 0), id: tc.id, function: tc.function } bareAgentMergeToolCallDelta(toolAcc, shifted) } appendProgress( (before ? 'recovered_additional_tool_calls_from_text n=' : 'recovered_tool_calls_from_text n=') + String(extracted.length) ) assistantContent = bareAgentStripToolCallsFromText(assistantContent) } if ( typeof bareAgentLooksLikeInstructionDump === 'function' && bareAgentLooksLikeInstructionDump(assistantContent) && (toolAcc.size || extracted.length) ) { assistantContent = '' } } const hideThinkEnv = String(envBag.BARE_OS_AGENT_HIDE_THINK || '') .trim() .toLowerCase() const hideThink = hideThinkEnv === '1' || hideThinkEnv === 'true' || hideThinkEnv === 'yes' const hideMdEnv = String(envBag.BARE_OS_AGENT_PLAIN || '') .trim() .toLowerCase() const discordEnv = String(envBag.BARE_OS_AGENT_DISCORD || '') .trim() .toLowerCase() const plainReply = hideMdEnv === '1' || hideMdEnv === 'true' || hideMdEnv === 'yes' || discordEnv === '1' || discordEnv === 'true' || discordEnv === 'yes' const thinkPanel = !hideThink && stdout && /** @type {{ isTTY?: boolean }} */ (stdout).isTTY ? bareAgentCreateThinkPanel(ctx, stdout, { useColor, bodyLines: 8, maxChars: Math.max(reasoningSettings.maxChars, 24_000), write: bareAgentWriteOut }) : null const detachThinkKeys = bareAgentAttachThinkScrollKeys(ctx, thinkPanel, { onAbort: () => { try { if (typeof ctx.bareOsAbortActiveAgent === 'function') { ctx.bareOsAbortActiveAgent() } else { masterAbort.abort() } } catch { masterAbort.abort() } } }) detachThinkKeysSession = detachThinkKeys let thinkSealed = false let replyStarted = false let replyPainted = false /** * @param {string} chunk */ function feedThink(chunk) { if (!chunk || !String(chunk).trim()) return thinkDump += chunk if (hideThink) return clearStatusLine() if (thinkPanel) thinkPanel.append(chunk) else { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + chunk + EDIT_ANSI_RESET ) } } function sealThinkIfNeeded() { if (thinkSealed) return thinkSealed = true if (thinkPanel && thinkPanel.hasContent()) thinkPanel.seal() } function termWidth() { if (typeof bareAgentResolveTermCols === 'function') { return bareAgentResolveTermCols(ctx, stdout) } const fromOut = stdout && typeof stdout === 'object' ? Number(/** @type {{ columns?: number }} */ (stdout).columns) : NaN const fromEnv = parseInt(String(envBag.COLUMNS || '80'), 10) const n = Number.isFinite(fromOut) && fromOut > 0 ? fromOut : fromEnv return Math.max(40, Number.isFinite(n) && n > 0 ? n : 80) } /** * Buffer reply; show live status under the think box (markdown painted later). * @param {string} chunk */ function feedReply(chunk) { if (!chunk) return sealThinkIfNeeded() if (!replyStarted) replyStarted = true clearStatusLine() assistantContent += chunk if (plainReply) { bareAgentWriteOut(ctx, stdout, chunk) return } if (thinkPanel && thinkPanel.hasContent()) { thinkPanel.setStatus( bareEditSgr('dim', useColor) + '▌ answering… ' + String(assistantContent.length) + ' chars · ↑↓ scroll thoughts' + EDIT_ANSI_RESET ) } else { // No think box — stream plain for responsiveness; paint markdown at end. bareAgentWriteOut(ctx, stdout, chunk) } } function paintReplyMarkdown() { if (replyPainted) return replyPainted = true thinkSplit.flush() sealThinkIfNeeded() if (plainReply) { if (thinkPanel) thinkPanel.clearStatus() return } const body = String(assistantContent || '') if (thinkPanel) { thinkPanel.clearStatus() thinkPanel.detachBelow() } if (!body.trim()) return // If we streamed plain (no think panel), rewind approximate lines then paint. if (!thinkPanel || !thinkPanel.hasContent()) { const roughLines = body.split('\n').length if ( roughLines > 0 && stdout && /** @type {{ isTTY?: boolean }} */ (stdout).isTTY ) { bareAgentWriteOut( ctx, stdout, '\x1b[' + String(Math.min(40, roughLines)) + 'A\r\x1b[J' ) } } const rendered = bareAgentRenderMarkdown(body, { useColor, width: termWidth() }) bareAgentWriteOut(ctx, stdout, '\n' + rendered) } const thinkSplit = bareAgentCreateThinkTagSplitter({ onThink: feedThink, onContent: feedReply }) /** * @param {Record} e */ function onCompletionEvent(e) { if (e.type === 'delta_content') { const chunk = typeof e.content === 'string' ? e.content : '' if (chunk) thinkSplit.push(chunk) } else if (e.type === 'delta_tool_calls') { sealThinkIfNeeded() thinkSplit.flush() const arr = e.tool_calls if (Array.isArray(arr)) { for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc) } } else if (e.type === 'delta_reasoning') { const chunk = typeof e.reasoning === 'string' ? e.reasoning : '' if (chunk) { if (reasoningSettings.enabled || !hideThink) { if (reasoningCharCount < reasoningSettings.maxChars) { const remain = reasoningSettings.maxChars - reasoningCharCount const out = chunk.slice(0, remain) reasoningCharCount += out.length if (out.length) feedThink(out) } } } } else if (e.type === 'response_shape_keys') { const keys = Array.isArray(e.keys) ? e.keys.map((k) => String(k)).join(',') : '' appendProgress('provider_shape_keys ' + keys.slice(0, 200)) } else if (e.type === 'usage') { usageOut = e.usage } else if (e.type === 'finish' || e.type === 'finish_reason') { thinkSplit.flush() sealThinkIfNeeded() finishReason = String(e.finish_reason || '') } } try { if (backendIter === 'qvac') { const profile = bareAgentQvacGetProfile( String(configRef.current.qvac_profile || 'recommended') ) const modelSrc = String( configRef.current.qvac_model || configRef.current.model || profile.chatModel ) const ctxSize = bareAgentQvacResolveCtxSize( configRef.current, profile ) const deviceOpts = bareAgentQvacResolveDeviceOpts(configRef.current) const verboseUi = agentVerbose(reasoningSettings) if (verboseUi) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[qvac] ensuring model ' + modelSrc + ' (ctx=' + ctxSize + (deviceOpts.device ? ', device=' + deviceOpts.device : ', auto-GPU') + ')…' + EDIT_ANSI_RESET + '\n' ) try { const st = typeof ctx.bareOsQvacStatus === 'function' ? ctx.bareOsQvacStatus() : null if (st && (st.cacheDir || st.hdmsPath || st.backendsDir)) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '[qvac] cache=' + String(st.cacheDir || '') + ' hdms=' + String(st.hdmsPath || '/mnt/models') + ' backends=' + String(st.backendsDir || '(unresolved)') + EDIT_ANSI_RESET + '\n' ) } } catch { /* ignore */ } } else { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\rLoading model…' + EDIT_ANSI_RESET ) statusLineActive = true } /** * @param {unknown} prog */ function onLoadProgress(prog) { const pct = prog && typeof prog === 'object' && 'percentage' in prog ? Number( /** @type {{ percentage?: unknown }} */ (prog).percentage ) : NaN if (!Number.isFinite(pct)) return const label = verboseUi ? '\r[qvac] download/load ' + Math.floor(pct) + '%' : '\rLoading model… ' + Math.floor(pct) + '%' bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + label + EDIT_ANSI_RESET ) statusLineActive = true } if (typeof ctx.bareOsQvacLoadModel === 'function') { const loaded = await ctx.bareOsQvacLoadModel({ modelSrc, tools: true, ctxSize, device: deviceOpts.device, mainGpu: deviceOpts.mainGpu, gpuLayers: deviceOpts.gpuLayers, onProgress: onLoadProgress }) clearStatusLine() if (verboseUi) { if (loaded && loaded.fellBackToCpu) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[qvac] GPU unavailable; using CPU' + EDIT_ANSI_RESET + '\n' ) } else if ( loaded && loaded.device === 'gpu' && loaded.mainGpu != null ) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[qvac] using main-gpu=' + String(loaded.mainGpu) + (loaded.probe ? ' (' + String(loaded.probe) + ')' : '') + EDIT_ANSI_RESET + '\n' ) } } else if (loaded && loaded.fellBackToCpu) { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + 'Using CPU (GPU unavailable)\n' + EDIT_ANSI_RESET ) } } const qvacHistory = typeof bareAgentSanitizeHistoryForQvac === 'function' ? bareAgentSanitizeHistoryForQvac(messages) : messages await ctx.bareOsQvacComplete({ history: qvacHistory, tools: qvacToolsForTurn, stream: Boolean(configRef.current.stream !== false), captureThinking: !hideThink || reasoningSettings.enabled, modelSrc, toolsEnabled: true, toolDialect: typeof bareAgentQvacDetectToolDialect === 'function' ? bareAgentQvacDetectToolDialect(modelSrc) : undefined, ctxSize, device: deviceOpts.device, mainGpu: deviceOpts.mainGpu, gpuLayers: deviceOpts.gpuLayers, signal: masterAbort.signal, onProgress: onLoadProgress, onEvent: onCompletionEvent }) clearStatusLine() thinkSplit.flush() sealThinkIfNeeded() recoverToolsFromAssistantText() paintReplyMarkdown() try { detachThinkKeys() } catch { /* ignore */ } } else { 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 } } 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 } if (providerNow === 'xai') { body.parallel_tool_calls = Number(configRef.current.tool_parallelism) > 1 ? true : false body.max_completion_tokens = Number(configRef.current.max_tokens) || 4096 } if (providerNow === 'groq') { body.parallel_tool_calls = Number(configRef.current.tool_parallelism) > 1 ? true : false body.max_completion_tokens = Number(configRef.current.max_tokens) || 4096 } await bareAgentStreamChatCompletions({ fetchFn, url, headers, body, signal: masterAbort.signal, onEvent: onCompletionEvent }) clearStatusLine() thinkSplit.flush() sealThinkIfNeeded() recoverToolsFromAssistantText() paintReplyMarkdown() try { detachThinkKeys() } catch { /* ignore */ } } } catch (e) { try { detachThinkKeys() } catch { /* ignore */ } 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 } let toolCallsArr = bareAgentFinalizeToolCalls(toolAcc) if (!toolCallsArr.length && typeof bareAgentExtractToolCallsFromText === 'function') { const extracted = bareAgentExtractToolCallsFromText( assistantContent + '\n' + thinkDump ) if (extracted.length) { for (const tc of extracted) bareAgentMergeToolCallDelta(toolAcc, tc) toolCallsArr = bareAgentFinalizeToolCalls(toolAcc) assistantContent = bareAgentStripToolCallsFromText(assistantContent) if ( typeof bareAgentLooksLikeInstructionDump === 'function' && bareAgentLooksLikeInstructionDump(assistantContent) ) { assistantContent = '' } appendProgress( 'recovered_tool_calls_from_text n=' + String(toolCallsArr.length) ) } } const hasTools = toolCallsArr.length > 0 if (!hasTools && finishReason === 'tool_calls') { appendProgress( 'warning tool_calls_finish_without_tool_deltas provider=' + providerNow ) } if ( hasTools && typeof bareAgentLooksLikeInstructionDump === 'function' && bareAgentLooksLikeInstructionDump(assistantContent) ) { assistantContent = '' } /** @type {Record} */ const assistantMsg = { role: 'assistant', content: assistantContent || '', tool_calls: hasTools ? toolCallsArr : undefined } messages.push(assistantMsg) if ( usageOut && typeof usageOut === 'object' && agentVerbose(reasoningSettings) ) { const u = /** @type {Record} */ (usageOut) const pt = u.prompt_tokens const ct = u.completion_tokens bareAgentWriteOut( ctx, stdout, '\n' + bareEditSgr('dim', useColor) + 'tokens: prompt=' + String(pt ?? '?') + ' completion=' + String(ct ?? '?') + EDIT_ANSI_RESET + '\n' ) } if (!hasTools) { if ( instructionDumpNudges < 2 && typeof bareAgentLooksLikeInstructionDump === 'function' && bareAgentLooksLikeInstructionDump(assistantContent) ) { instructionDumpNudges += 1 messages.push({ role: 'user', content: typeof bareAgentRefusalNudge === 'function' ? bareAgentRefusalNudge() : '[harness] Call run_command now. Do not tell the user to run it.' }) appendProgress( 'instruction_dump_nudge n=' + String(instructionDumpNudges) ) await bareAgentSaveHistory(ctx, paths.history, messages) continue } if ( bareAgentAutonomousShouldContinue(configRef.current, { completed: completed, hasTools: false, stopRequested: autonomousSettings.stopRequested }) ) { const remain = Math.max( 0, autonomousSettings.maxRuntimeMs - (Date.now() - (autonomousSettings.startedAtMs || Date.now())) ) messages.push({ role: 'user', content: bareAgentAutonomousContinuationPrompt(configRef.current, { remainingMs: remain, lastError: String(configRef.current.autonomous_last_error || '') }) }) appendProgress('autonomous continue (no tools this turn)') await bareAgentSaveHistory(ctx, paths.history, messages) continue } await bareAgentSaveHistory(ctx, paths.history, messages) bareAgentWriteOut(ctx, stdout, '\n') break } appendProgress( 'iteration ' + iter + ' tools ' + toolCallsArr .map( (x) => /** @type {{ function?: { name?: string } }} */ (x).function ?.name ) .join(',') ) if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') { bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\n[process] iteration ' + String(iter) + ' finish_reason=' + (finishReason || 'unknown') + EDIT_ANSI_RESET + '\n' ) } let sawTodoWrite = false 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 || '{}' if (name === 'todo_write') sawTodoWrite = true if ( reasoningSettings.enabled && reasoningSettings.mode === 'trace' && reasoningSettings.includeTools ) { const argsPreview = argsStr.length > 280 ? argsStr.slice(0, 280) + '…' : argsStr bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '[process] tool_call ' + name + ' args=' + argsPreview + EDIT_ANSI_RESET + '\n' ) } bareAgentWriteOut( ctx, stdout, '\n' + bareEditSgr('keyword', useColor) + '→ ' + name + EDIT_ANSI_RESET + '\n' ) const spinner = bareEditSgr('dim', useColor) + '…' + EDIT_ANSI_RESET bareAgentWriteOut(ctx, stdout, spinner + '\r') statusLineActive = true let resultStr = await bareAgentDispatchTool({ ctx, toolName: name, argsJson: argsStr, paths, signal: masterAbort.signal, appendProgress, home, configRef, manCacheRef, onTaskComplete }) if (typeof bareAgentRunHooks === 'function' && paths.hooks) { try { let parsedArgs = {} try { parsedArgs = JSON.parse(argsStr || '{}') } catch { parsedArgs = {} } const post = await bareAgentRunHooks( ctx, paths.hooks, 'PostToolUse', name, parsedArgs, resultStr ) if (post && post.inject) { resultStr = resultStr + '\n[hook PostToolUse] ' + String(post.inject).slice(0, 800) } } catch { /* optional */ } } clearStatusLine() messages.push({ role: 'tool', tool_call_id: id, content: resultStr }) if ( reasoningSettings.enabled && reasoningSettings.mode === 'trace' && reasoningSettings.includeTools ) { const resPreview = resultStr.length > 360 ? resultStr.slice(0, 360) + '…' : resultStr bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '[process] tool_result ' + name + ' ' + resPreview + EDIT_ANSI_RESET + '\n' ) } if (name === 'enter_plan_mode' || name === 'exit_plan_mode') { messages.push({ role: 'user', content: name === 'enter_plan_mode' ? '[harness] PLAN MODE is now ON. Only write ~/.agent/plan.md until exit_plan_mode.' : '[harness] PLAN MODE is now OFF. Mutating tools are allowed again.' }) } if (name === 'ask_user_question') { try { const parsed = JSON.parse(resultStr) if (parsed && parsed.text) { bareAgentWriteOut( ctx, stdout, '\n' + bareEditSgr('keyword', useColor) + 'Questions for you:\n' + EDIT_ANSI_RESET + String(parsed.text) + '\n' ) } } catch { /* ignore */ } } if (completed && !configRef.current.autonomous_active) break } if (sawTodoWrite) turnsSinceTodoWrite = 0 else turnsSinceTodoWrite++ if (typeof bareAgentTodoNudgeText === 'function' && typeof bareAgentLoadTodos === 'function') { try { const todosNow = paths.todos ? await bareAgentLoadTodos(ctx, paths.todos) : [] const sumNow = bareAgentTodoSummarize(todosNow) const nudge = bareAgentTodoNudgeText({ open: sumNow.open, turnsSinceTodoWrite, nudgeEnabled: configRef.current.todo_nudge_enabled !== false }) if (nudge) { messages.push({ role: 'user', content: '[harness reminder] ' + nudge }) } } catch { /* optional */ } } await bareAgentSaveHistory(ctx, paths.history, messages) if (completed) { if (configRef.current.autonomous_active) { const gate = await bareAgentRunAutonomousGates({ ctx, paths, signal: masterAbort.signal, appendProgress, home, configRef, manCacheRef, onTaskComplete, requiredChecks: autonomousSettings.requiredChecks }) if (!gate.ok) { completed = false configRef.current.autonomous_status = 'needs_fixups' configRef.current.autonomous_last_error = 'failed_checks:' + gate.failed.join(',') await bareAgentSaveConfig(ctx, paths, configRef.current) appendProgress('autonomous checks failed ' + gate.failed.join(',')) messages.push({ role: 'user', content: 'Autonomous completion gates failed for checks: ' + gate.failed.join(', ') + '. Fix the issues, rerun required checks, and only call task_complete when all pass.' }) continue } configRef.current.autonomous_active = false configRef.current.autonomous_status = 'completed' configRef.current.autonomous_last_error = '' await bareAgentSaveConfig(ctx, paths, configRef.current) appendProgress('autonomous completion gates passed') } bareAgentWriteOut( ctx, stdout, bareEditSgr('dim', useColor) + '\nDone: ' + taskSummary + EDIT_ANSI_RESET + '\n' ) break } } if (offSigint) offSigint() } finally { try { if (detachThinkKeysSession) detachThinkKeysSession() } catch { /* ignore */ } detachThinkKeysSession = null try { if (prevAbortAgent) ctx.bareOsAbortActiveAgent = prevAbortAgent else delete ctx.bareOsAbortActiveAgent } catch { try { delete ctx.bareOsAbortActiveAgent } catch { /* ignore */ } } try { if (typeof ctx.bareOsQvacCancelActive === 'function') { ctx.bareOsQvacCancelActive() } } catch { /* ignore */ } clearStatusLine() // Ensure cursor is on a fresh line so the shell prompt is visible. bareAgentWriteOut(ctx, stdout, '\n') if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') { try { ctx.resumeReplAfterSubprocess() } catch { /* ignore */ } } } } /** * Autonomous AI agent: QVAC (local) or 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(bareAgentCliUsage(argv0)) ctx.exitCode = wantHelp ? 0 : 1 return } let setupFlag = false let resetFlag = false let autoFlag = false let statusFlag = false let planFlag = false let newFlag = false let compactFlag = false let maxTurns = 0 let modelOverride = '' let systemFile = '' /** @type {string[]} */ const rest = [] for (let i = 0; i < args.length; i++) { const a = args[i] if (a === '--setup' || a === '--config') setupFlag = true else if (a === '--reset') resetFlag = true else if (a === '--auto' || a === '--autonomous') autoFlag = true else if (a === '--status') statusFlag = true else if (a === '--plan') planFlag = true else if (a === '--new') newFlag = true else if (a === '--compact') compactFlag = true else if (a === '--continue' || a === '-c') { /* default: history is always loaded unless --new/--reset */ } else if (a === '--max-turns' || a === '--max-iterations') { maxTurns = Number(args[++i]) || 0 } else if (a === '--model') { modelOverride = String(args[++i] || '').trim() } else if (a === '--system' || a === '--system-file') { systemFile = String(args[++i] || '').trim() } else rest.push(a) } const sub = rest[0] || '' if ( !setupFlag && !autoFlag && !statusFlag && !planFlag && !newFlag && !compactFlag && !resetFlag && ((rest.length === 1 && (sub === 'status' || sub === 'skills' || sub === 'todos' || sub === 'plan' || sub === 'compact' || sub === 'reset' || sub === 'undo' || sub === 'hooks' || sub === 'history' || sub === 'rewind' || sub === 'export' || sub === 'remember' || sub === 'recap' || sub === 'models')) || (sub === 'history' && rest[0] === 'history') || (sub === 'rewind' && rest[0] === 'rewind') || (sub === 'export' && rest[0] === 'export') || (sub === 'remember' && rest[0] === 'remember') || (sub === 'models' && rest[0] === 'models')) ) { if (sub === 'status') statusFlag = true else if (sub === 'reset') resetFlag = true else if (sub === 'compact') compactFlag = true else { await bareAgentRunInspectSubcommand(ctx, argv0, sub, rest.slice(1).join(' ')) return } } if (statusFlag) { await bareAgentPrintStatus(ctx) ctx.exitCode = 0 return } const wantReset = resetFlag || newFlag || (rest.length === 1 && rest[0] === 'reset') if (wantReset && !rest.filter((x) => x !== 'reset').length && !setupFlag && !autoFlag && !planFlag) { 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 } if (compactFlag && !rest.filter((x) => x !== 'compact').length && !autoFlag && !planFlag && !setupFlag) { await bareAgentRunCompactOnly(ctx, argv0) return } const task = rest .filter(function (x) { return x !== 'reset' && x !== 'compact' }) .join(' ') .trim() if (!task && !setupFlag) { ctx.console.error( argv0 + ': missing task (or use --setup / --config / --auto GOAL / skills / todos / plan)' ) ctx.exitCode = 1 return } if (setupFlag && !task) { await bareAgentRunSetupOnly(ctx, argv0) return } if (autoFlag && !task) { ctx.console.error(argv0 + ': --auto requires a goal string') ctx.exitCode = 1 return } if (newFlag || resetFlag) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) try { await bareAgentResetChatSession(ctx, paths, argv0) } 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 } } else if (compactFlag) { await bareAgentRunCompactOnly(ctx, argv0) if (ctx.exitCode && ctx.exitCode !== 0) return } await bareOsRunAgentSession(ctx, argv0, task, { setupFlag, autonomous: autoFlag, autonomousGoal: autoFlag ? task : '', planMode: planFlag, maxIterations: maxTurns > 0 ? maxTurns : undefined, modelOverride: modelOverride || undefined, extraSystemFile: systemFile || undefined }) } /** * @param {string} argv0 */ function bareAgentCliUsage(argv0) { return ( 'usage: ' + argv0 + ' [OPTION]... YOUR_REQUEST_HERE\n' + ' ' + argv0 + ' --setup | --config\n' + ' ' + argv0 + ' --status | status\n' + ' ' + argv0 + ' --reset | reset | --new\n' + ' ' + argv0 + ' --auto "GOAL"\n' + ' ' + argv0 + ' --plan "design the change"\n' + ' ' + argv0 + ' --compact\n' + ' ' + argv0 + ' skills | todos | plan | undo | hooks | history | rewind | export | remember | recap | models [ID]\n' + '\n' + 'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' + 'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' + '\n' + 'Options:\n' + ' --setup, --config Walk through QVAC or REST setup (only the chosen backend is stored)\n' + ' --auto, --autonomous Keep the tool loop going until task_complete, stop, or timebox\n' + ' --plan Start this turn in plan mode (read-only except ~/.agent/plan.md)\n' + ' --new, --reset Clear ~/.agent/history.json then run the request (or just reset)\n' + ' --continue, -c Resume history (default)\n' + ' --compact Compact history now (or compact then run if a task follows)\n' + ' --max-turns N Cap ReAct iterations for this run\n' + ' --model NAME Switch QVAC / REST model now and persist to ~/.agent/config.json\n' + ' --system FILE Append extra instructions from an absolute guest path\n' + ' --status Print backend, compaction, plan mode, and autonomous run state\n' + '\n' + 'Inspect (no model call):\n' + ' skills List discovered SKILL.md ids\n' + ' todos Print session todos\n' + ' plan Print ~/.agent/plan.md\n' + ' undo Restore the last file edit snapshot\n' + ' hooks List ~/.agent/hooks/*.json\n' + ' history [query] Search or tail chat history\n' + ' rewind [N] Drop the last N user turns from history (default 1)\n' + ' export [path] Write a Markdown transcript (default ~/.agent/export.md)\n' + ' remember TEXT Append a FACT to MEMORY.md\n' + ' recap Print the last user + assistant pair\n' + ' models [ID] List catalog / current, or persist a live model switch\n' + '\n' + 'Examples:\n' + ' ' + argv0 + ' "summarize ~/README and list five files in /bin"\n' + ' ' + argv0 + ' --plan "design a /bin/foo utility"\n' + ' ' + argv0 + ' --auto --max-turns 40 "add /agent tests and run them"\n' + ' ' + argv0 + ' --new --model QWEN3_1_7B_INST_Q4 "fresh session: inspect /proc"\n' + '\n' + 'See man agent.' ) } /** * @param {Record} ctx */ async function bareAgentPrintStatus(ctx) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) const loaded = await bareAgentLoadOrCreateConfig(ctx, paths) let cfg = loaded && loaded.config ? loaded.config : loaded if (typeof bareAgentSanitizeConfigForBackend === 'function') { cfg = bareAgentSanitizeConfigForBackend(cfg || {}) } const started = Number(cfg && cfg.autonomous_started_at_ms) || 0 const maxRt = Number(cfg && cfg.autonomous_max_runtime_ms) || 0 const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0 const backend = typeof bareAgentResolveBackend === 'function' ? bareAgentResolveBackend(cfg || {}) : String((cfg && (cfg.backend || cfg.provider)) || 'qvac') /** @type {string[]} */ const lines = ['agent status'] if (typeof bareAgentFormatConfigSummary === 'function') { String(bareAgentFormatConfigSummary(cfg || {})) .split('\n') .forEach(function (row) { lines.push(' ' + row) }) } else { lines.push(' backend: ' + backend) } lines.push( ' compaction: ' + String((cfg && cfg.context_compaction) || 'auto'), ' autonomous_enabled: ' + String(Boolean(cfg && cfg.autonomous_mode_enabled)), ' autonomous_active: ' + String(Boolean(cfg && cfg.autonomous_active)), ' status: ' + String((cfg && cfg.autonomous_status) || 'idle'), ' goal: ' + String((cfg && cfg.autonomous_goal) || ''), ' elapsed_s: ' + String(Math.round(elapsed / 1000)), ' remaining_s: ' + String(maxRt > 0 ? Math.max(0, Math.round((maxRt - elapsed) / 1000)) : 0), ' last_error: ' + String((cfg && cfg.autonomous_last_error) || ''), ' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active)), ' access_policy: ' + String((cfg && cfg.access_policy) || 'full'), ' allow_delete: ' + String(cfg && cfg.allow_delete !== false) ) ctx.console.log(lines.join('\n')) } /** * @param {Record} ctx * @param {string} argv0 * @param {string} sub */ async function bareAgentRunInspectSubcommand(ctx, argv0, sub, extra) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) try { if (sub === 'skills') { if (typeof bareAgentEnsureSkillTemplates === 'function') { const loaded = await bareAgentLoadOrCreateConfig(ctx, paths) const cfg = loaded && loaded.config ? loaded.config : loaded await bareAgentEnsureSkillTemplates(ctx, paths, cfg || {}) } const block = typeof bareAgentSkillsCompactPrompt === 'function' ? await bareAgentSkillsCompactPrompt(ctx, paths, 8000) : '' ctx.console.log(block && String(block).trim() ? String(block).trim() : argv0 + ': no skills') ctx.exitCode = 0 return } if (sub === 'todos') { const todos = typeof bareAgentLoadTodos === 'function' ? await bareAgentLoadTodos(ctx, paths.todos) : [] const sum = typeof bareAgentTodoSummarize === 'function' ? bareAgentTodoSummarize(todos) : { text: '', open: 0, total: 0 } ctx.console.log(sum.text || argv0 + ': no todos') ctx.exitCode = 0 return } if (sub === 'plan') { const text = typeof bareAgentReadTextFile === 'function' ? await bareAgentReadTextFile(ctx, paths.plan) : '' ctx.console.log(text && text.trim() ? text : argv0 + ': no plan (' + paths.plan + ')') ctx.exitCode = 0 return } if (sub === 'undo') { if (typeof bareAgentPopEdit !== 'function') { ctx.console.error(argv0 + ': undo unavailable') ctx.exitCode = 1 return } const rec = await bareAgentPopEdit(ctx, paths.edits) if (!rec || !rec.path) { ctx.console.log(argv0 + ': nothing to undo') ctx.exitCode = 0 return } await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || '')) ctx.console.log(argv0 + ': restored ' + rec.path) ctx.exitCode = 0 return } if (sub === 'hooks') { let names = [] try { names = ctx.vfs && typeof ctx.vfs.readdir === 'function' ? await ctx.vfs.readdir(paths.hooks) : [] } catch { names = [] } const json = (Array.isArray(names) ? names : []).filter(function (n) { return /\.json$/i.test(String(n || '')) }) ctx.console.log( json.length ? json.map(function (n) { return paths.hooks + '/' + n }).join('\n') : argv0 + ': no hooks in ' + paths.hooks ) ctx.exitCode = 0 return } if (sub === 'history') { const q = String(extra || '').trim() const messages = await bareAgentLoadHistory(ctx, paths.history) if (q && typeof bareAgentHistorySearch === 'function') { const hits = bareAgentHistorySearch(messages, q, 12) ctx.console.log( hits.length ? hits .map(function (h) { return '[' + h.role + '] ' + h.snippet }) .join('\n') : argv0 + ': no history matches' ) } else { ctx.console.log(argv0 + ': ' + String(messages.length) + ' messages in history') } ctx.exitCode = 0 return } if (sub === 'rewind') { const steps = Math.max(1, Math.floor(Number(extra) || 1)) const messages = await bareAgentLoadHistory(ctx, paths.history) if (typeof bareAgentRewindHistory !== 'function') { ctx.console.error(argv0 + ': rewind unavailable') ctx.exitCode = 1 return } const out = bareAgentRewindHistory(messages, { steps }) if (!out.ok) { ctx.console.log(argv0 + ': ' + (out.error || 'nothing to rewind')) ctx.exitCode = 0 return } await bareAgentSaveHistory(ctx, paths.history, out.messages) ctx.console.log( argv0 + ': rewound ' + String(out.dropped) + ' message(s), ' + String(out.messages.length) + ' remain' ) ctx.exitCode = 0 return } if (sub === 'export') { const dest = String(extra || '').trim() || paths.dir + '/export.md' const messages = await bareAgentLoadHistory(ctx, paths.history) const md = typeof bareAgentExportTranscript === 'function' ? bareAgentExportTranscript(messages) : '' await bareAgentWriteTextFile(ctx, dest, md) ctx.console.log(argv0 + ': exported ' + String(messages.length) + ' messages to ' + dest) ctx.exitCode = 0 return } if (sub === 'remember') { const text = String(extra || '').trim() if (!text) { ctx.console.error(argv0 + ': remember requires a note') ctx.exitCode = 1 return } const dest = paths.workspace + '/MEMORY.md' const line = '- FACT — ' + text.replace(/\s+/g, ' ') let prev = '' try { prev = await bareAgentReadTextFile(ctx, dest) } catch { prev = '' } await bareAgentWriteTextFile(ctx, dest, (prev ? prev.replace(/\s*$/, '') + '\n' : '') + line + '\n') ctx.console.log(argv0 + ': remembered in ' + dest) ctx.exitCode = 0 return } if (sub === 'recap') { const messages = await bareAgentLoadHistory(ctx, paths.history) let lastUser = '' let lastAsst = '' for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null if (!m) continue const role = String(m.role || '') if (!lastAsst && role === 'assistant') lastAsst = String(m.content || '') if (!lastUser && role === 'user') lastUser = String(m.content || '') if (lastUser && lastAsst) break } ctx.console.log( lastUser || lastAsst ? 'USER\n' + lastUser.slice(0, 1200) + '\n\nASSISTANT\n' + lastAsst.slice(0, 2000) : argv0 + ': no recap (empty history)' ) ctx.exitCode = 0 return } if (sub === 'models') { const loaded = await bareAgentLoadOrCreateConfig(ctx, paths) let cfg = loaded && loaded.config ? loaded.config : loaded || {} const want = String(extra || '').trim() if (want) { cfg = typeof bareAgentApplyLiveModel === 'function' ? bareAgentApplyLiveModel(cfg, want) : Object.assign(cfg, { qvac_model: want, model: want }) if (typeof bareAgentSanitizeConfigForBackend === 'function') { cfg = bareAgentSanitizeConfigForBackend(cfg) } await bareAgentSaveConfig(ctx, paths, cfg) const backend = typeof bareAgentResolveBackend === 'function' ? bareAgentResolveBackend(cfg) : String(cfg.backend || 'qvac') const used = backend === 'qvac' ? String(cfg.qvac_model || cfg.model || want) : String(cfg.model || want) ctx.console.log(argv0 + ': now using ' + backend + ' · ' + used) ctx.exitCode = 0 return } if (typeof bareAgentSanitizeConfigForBackend === 'function') { cfg = bareAgentSanitizeConfigForBackend(cfg) } const backend = typeof bareAgentResolveBackend === 'function' ? bareAgentResolveBackend(cfg) : String(cfg.backend || 'qvac') const current = backend === 'qvac' ? String(cfg.qvac_model || cfg.model || '') : String(cfg.model || '') const lines = [ 'backend: ' + backend, 'current: ' + (current || '(unset)') ] if (backend === 'qvac' && typeof BARE_AGENT_QVAC_CHAT_MODELS !== 'undefined') { lines.push('catalog:') for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) { const m = BARE_AGENT_QVAC_CHAT_MODELS[i] const mark = m.id === current ? '* ' : ' ' lines.push(mark + m.id + (m.label ? ' — ' + m.label : '')) } } else { lines.push('set with: ' + argv0 + ' models or ' + argv0 + ' --model ') } ctx.console.log(lines.join('\n')) ctx.exitCode = 0 return } } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) ctx.console.error(argv0 + ': ' + msg) ctx.exitCode = 1 } } /** * @param {Record} ctx * @param {string} argv0 */ async function bareAgentRunCompactOnly(ctx, argv0) { const home = bareAgentResolveHome(ctx) const paths = bareAgentPaths(home) try { const messages = await bareAgentLoadHistory(ctx, paths.history) if (typeof bareAgentCompactMessagesForCtx !== 'function') { ctx.console.error(argv0 + ': compact unavailable') ctx.exitCode = 1 return } const packed = bareAgentCompactMessagesForCtx(messages, 32768, { mode: 'aggressive', keepRecent: 6, toolMaxChars: 1200 }) await bareAgentSaveHistory(ctx, paths.history, packed.messages) ctx.console.log( argv0 + ': compacted ' + String(packed.meta.beforeTokens) + '→' + String(packed.meta.afterTokens) + ' tok (dropped_groups=' + String(packed.meta.droppedGroups) + ')' ) 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 } }