7526 lines
240 KiB
Plaintext
7526 lines
240 KiB
Plaintext
/* 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<string, unknown>} ctx
|
||
* @param {string | Uint8Array} chunk
|
||
* @returns {boolean}
|
||
*/
|
||
function bareOsEmitRaw(ctx, chunk) {
|
||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||
const b4 = ctx.b4a
|
||
const u8 =
|
||
typeof chunk === 'string'
|
||
? b4 && typeof b4.from === 'function'
|
||
? b4.from(chunk)
|
||
: new TextEncoder().encode(chunk)
|
||
: chunk
|
||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||
return true
|
||
}
|
||
const w = globalThis.process?.stdout?.write
|
||
if (typeof w === 'function') {
|
||
w.call(globalThis.process.stdout, chunk)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** ANSI helpers for /bin/edit (preamble; no import in src). */
|
||
|
||
const EDIT_ANSI_RESET = '\x1b[0m'
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [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<string, unknown>} [ctx]
|
||
*/
|
||
function bareEditUseColor(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (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<string, unknown>} 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<string, unknown> | 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<number|undefined>, 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<string, unknown>} */ ({})
|
||
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<string, unknown>} */ ({})
|
||
|
||
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<number>} */ (input))
|
||
} else if (input == null || input === undefined) {
|
||
return ''
|
||
} else {
|
||
return ''
|
||
}
|
||
const stream = !!(options && options.stream)
|
||
void stream
|
||
return bareAgentUtf8Decode(u8)
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextDecoder = BareTextDecoder
|
||
else g.TextDecoder = BareTextDecoder
|
||
}
|
||
|
||
if (typeof g.TextEncoder !== 'function') {
|
||
function BareTextEncoder() {
|
||
this.encoding = 'utf-8'
|
||
}
|
||
BareTextEncoder.prototype.encode = function (input) {
|
||
return bareAgentUtf8Encode(String(input == null ? '' : input))
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextEncoder = BareTextEncoder
|
||
else g.TextEncoder = BareTextEncoder
|
||
}
|
||
}
|
||
|
||
bareAgentEnsureTextCodecPolyfill()
|
||
|
||
/** Shared helpers for /bin/agent tools (preamble for agent bundle). */
|
||
|
||
/**
|
||
* Paths allowed for rename/delete via agent tools (stricter than general read paths).
|
||
* @param {string} absPath
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentPathAllowedMutate(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/') || p.includes('..')) return false
|
||
return (
|
||
p.startsWith('/home/') ||
|
||
p.startsWith('/tmp/') ||
|
||
p === '/tmp' ||
|
||
p.startsWith('/root/') ||
|
||
p.startsWith('/mnt/')
|
||
)
|
||
}
|
||
|
||
/** @type {readonly string[]} */
|
||
var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||
'/proc/bare_os/metrics_live.json',
|
||
'/proc/bare_os/features',
|
||
'/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 {unknown} statObj
|
||
* @param {string} path
|
||
*/
|
||
function bareAgentSerializeStat(statObj, path) {
|
||
if (!statObj || typeof statObj !== 'object')
|
||
return { path, error: 'no_stat' }
|
||
const s = /** @type {Record<string, unknown>} */ (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<string, unknown>} */
|
||
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<string, unknown>} 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<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} ctx
|
||
* @param {unknown} vfs
|
||
* @param {{ db: unknown | null }} cacheRef
|
||
* @returns {Promise<unknown | null>}
|
||
*/
|
||
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<string, unknown>} */ (db)
|
||
const index = d.index
|
||
if (!index || typeof index !== 'object') return { error: 'not_found' }
|
||
const idx = /** @type {Record<string, unknown>} */ (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<string, unknown>} */ (page) }
|
||
}
|
||
|
||
/**
|
||
* @param {string} absPath
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentProcReadPathAllowed(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/') || p.includes('..')) return false
|
||
for (let i = 0; i < BARE_AGENT_PROC_READ_ALLOWLIST.length; i++) {
|
||
if (p === BARE_AGENT_PROC_READ_ALLOWLIST[i]) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentResolveHome(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const vfsHome =
|
||
ctx &&
|
||
typeof ctx === 'object' &&
|
||
ctx.vfs &&
|
||
typeof ctx.vfs === 'object' &&
|
||
typeof /** @type {{ home?: string }} */ (ctx.vfs).home === 'string'
|
||
? String(/** @type {{ home?: string }} */ (ctx.vfs).home)
|
||
: ''
|
||
const h = env.HOME || vfsHome || '/home/guest'
|
||
return h.replace(/\/+$/, '') || '/home/guest'
|
||
}
|
||
|
||
/**
|
||
* @param {string} home
|
||
*/
|
||
function bareAgentPaths(home) {
|
||
const base = home + '/.agent'
|
||
return {
|
||
dir: base,
|
||
config: base + '/config.json',
|
||
history: base + '/history.json',
|
||
progress: base + '/progress.txt',
|
||
instructions: base + '/instructions.md',
|
||
context: base + '/context.md',
|
||
cmdOut: base + '/last_command_out.txt',
|
||
workspace: base + '/workspace',
|
||
workspaceMemory: base + '/workspace/memory',
|
||
workspaceSkills: base + '/workspace/skills',
|
||
skillsGlobal: base + '/skills'
|
||
}
|
||
}
|
||
|
||
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<string, string>} */ ({}),
|
||
allow_delete: false,
|
||
require_confirm_token: '',
|
||
owner_name: '',
|
||
agent_label: '',
|
||
show_reasoning: false,
|
||
reasoning_mode: 'off',
|
||
reasoning_max_chars: 4000,
|
||
reasoning_include_tools: true,
|
||
allow_bridge_mutations: false,
|
||
allow_host_notifications: false,
|
||
allow_host_actions: false,
|
||
emergency_stop_mutations: false,
|
||
autonomous_mode_enabled: false,
|
||
autonomous_max_runtime_ms: 1800000,
|
||
autonomous_completion_required_checks: [],
|
||
autonomous_allow_paths: ['*'],
|
||
autonomous_deny_ops: [
|
||
'delete_path',
|
||
'request_host_action',
|
||
'emit_host_notification',
|
||
'list_verification_scripts',
|
||
'run_maintenance_gate',
|
||
'run_contract_checks',
|
||
'summarize_build_drift'
|
||
],
|
||
autonomous_active: false,
|
||
autonomous_started_at_ms: 0,
|
||
autonomous_stop_requested: false,
|
||
autonomous_goal: '',
|
||
autonomous_status: 'idle',
|
||
autonomous_last_error: ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {v is Record<string, unknown>}
|
||
*/
|
||
function bareAgentIsPlainObject(v) {
|
||
return v != null && typeof v === 'object' && !Array.isArray(v)
|
||
}
|
||
|
||
/**
|
||
* Shallow merge known keys from src into defaults.
|
||
* @param {Record<string, unknown>} defaults
|
||
* @param {Record<string, unknown>} 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',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'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'
|
||
])
|
||
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<string, string>} */ ({ ...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'
|
||
) {
|
||
out[k] = String(val ?? '')
|
||
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'
|
||
) {
|
||
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'
|
||
) {
|
||
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'
|
||
) {
|
||
out[k] = Boolean(val)
|
||
continue
|
||
}
|
||
if (k === 'require_confirm_token') {
|
||
out.require_confirm_token = String(val ?? '')
|
||
continue
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'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'
|
||
]
|
||
if (!known.includes(k)) {
|
||
throw new Error('unknown config key: ' + k)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ dir: string, config: string }} paths
|
||
* @returns {Promise<{ config: ReturnType<typeof bareAgentDefaultConfig>, 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<string, unknown>} */
|
||
let raw = {}
|
||
try {
|
||
if (typeof vfs.readFile === 'function') {
|
||
const buf = await vfs.readFile(paths.config)
|
||
if (!buf || !buf.length) missingOrEmpty = true
|
||
else {
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
raw = JSON.parse(t)
|
||
}
|
||
} else missingOrEmpty = true
|
||
} catch {
|
||
missingOrEmpty = true
|
||
raw = {}
|
||
}
|
||
|
||
if (!bareAgentIsPlainObject(raw)) raw = {}
|
||
|
||
if (missingOrEmpty || Object.keys(raw).length === 0) {
|
||
raw = bareAgentDefaultConfig()
|
||
await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw))
|
||
return { config: /** @type {any} */ (raw), created: true }
|
||
}
|
||
|
||
bareAgentValidateConfigShape(raw)
|
||
const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw)
|
||
return { config: /** @type {any} */ (merged), created: false }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSaveConfig(ctx, paths, config) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.writeFile !== 'function')
|
||
throw new Error('agent: vfs.writeFile unavailable')
|
||
bareAgentValidateConfigShape(config)
|
||
const json = JSON.stringify(config, null, 2) + '\n'
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(json)
|
||
: new TextEncoder().encode(json)
|
||
await vfs.writeFile(paths.config, body)
|
||
}
|
||
|
||
/**
|
||
* Drop middle messages until JSON size fits (keep first system + recent tail).
|
||
* @param {unknown[]} msgs
|
||
* @param {number} maxBytes
|
||
*/
|
||
function bareAgentTrimMessages(msgs, maxBytes) {
|
||
if (!Array.isArray(msgs) || maxBytes <= 0) return []
|
||
/** @type {unknown[]} */
|
||
let out = msgs.slice()
|
||
while (JSON.stringify(out).length > maxBytes && out.length > 3) {
|
||
out.splice(2, 1)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Best-effort load instruction files.
|
||
* @param {Record<string, unknown>} 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<string, unknown>} 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 <name>` for full text). Sample entries:\n' + s
|
||
)
|
||
} catch {
|
||
return '(man digest unavailable)'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @param {unknown[]} messages
|
||
*/
|
||
async function bareAgentSaveHistory(ctx, path, messages) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.writeFile !== 'function') return
|
||
const trimmed = bareAgentTrimMessages(messages, 500_000)
|
||
const json = JSON.stringify(trimmed, null, 2) + '\n'
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(json)
|
||
: new TextEncoder().encode(json)
|
||
await vfs.writeFile(path, body)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @returns {Promise<unknown[]>}
|
||
*/
|
||
async function bareAgentLoadHistory(ctx, path) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return []
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf || !buf.length) return []
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
const j = JSON.parse(t)
|
||
return Array.isArray(j) ? j : []
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear persisted chat session (history + progress log; keeps config and instructions).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {ReturnType<typeof bareAgentPaths>} paths
|
||
* @param {string} argv0
|
||
*/
|
||
async function bareAgentResetChatSession(ctx, paths, argv0) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.writeFile !== 'function') {
|
||
throw new Error('agent reset: vfs unavailable')
|
||
}
|
||
await vfs.mkdir(paths.dir, { recursive: true })
|
||
const emptyHist =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from('[]\n')
|
||
: new TextEncoder().encode('[]\n')
|
||
await vfs.writeFile(paths.history, emptyHist)
|
||
const stamp = new Date().toISOString() + ' chat session reset\n'
|
||
const progBody =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(stamp)
|
||
: new TextEncoder().encode(stamp)
|
||
await vfs.writeFile(paths.progress, progBody)
|
||
try {
|
||
const z =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from('')
|
||
: new TextEncoder().encode('')
|
||
await vfs.writeFile(paths.cmdOut, z)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
ctx.console.log(
|
||
argv0 + ': chat session cleared (' + paths.history + ', ' + paths.progress + ')'
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* QVAC helpers for /bin/agent (no SDK import — host bridge via ctx.bareOsQvac*).
|
||
*/
|
||
|
||
/** @typedef {'lite'|'recommended'|'strong'|'tool-tiny'} BareAgentQvacProfileId */
|
||
|
||
/**
|
||
* @typedef {{
|
||
* id: BareAgentQvacProfileId,
|
||
* label: string,
|
||
* description: string,
|
||
* chatModel: string,
|
||
* tools: boolean,
|
||
* minRamGb: number,
|
||
* minDiskGb: number,
|
||
* approxDownloadGb: number,
|
||
* ctxSize: number
|
||
* }} BareAgentQvacProfile
|
||
*/
|
||
|
||
/** @type {Record<BareAgentQvacProfileId, BareAgentQvacProfile>} */
|
||
const BARE_AGENT_QVAC_PROFILES = {
|
||
lite: {
|
||
id: 'lite',
|
||
label: 'Lite',
|
||
description:
|
||
'Smallest download. Good for weak machines / ~4 GB VRAM; limited tool use.',
|
||
chatModel: 'QWEN3_600M_INST_Q4',
|
||
tools: false,
|
||
minRamGb: 4,
|
||
minDiskGb: 2,
|
||
approxDownloadGb: 0.5,
|
||
ctxSize: 4096
|
||
},
|
||
recommended: {
|
||
id: 'recommended',
|
||
label: 'Recommended',
|
||
description:
|
||
'Best balance for Bare OS agent tool calling (fits ~4–8 GB VRAM at ctx 8k).',
|
||
chatModel: 'QWEN3_1_7B_INST_Q4',
|
||
tools: true,
|
||
minRamGb: 8,
|
||
minDiskGb: 5,
|
||
approxDownloadGb: 2.5,
|
||
ctxSize: 8192
|
||
},
|
||
strong: {
|
||
id: 'strong',
|
||
label: 'Strong',
|
||
description: 'Better reasoning. Needs more RAM/VRAM/disk.',
|
||
chatModel: 'QWEN3_4B_INST_Q4_K_M',
|
||
tools: true,
|
||
minRamGb: 16,
|
||
minDiskGb: 8,
|
||
approxDownloadGb: 3.5,
|
||
ctxSize: 8192
|
||
},
|
||
'tool-tiny': {
|
||
id: 'tool-tiny',
|
||
label: 'Tool-tiny',
|
||
description: 'Llama tool-calling 1B fallback if Qwen tools misbehave.',
|
||
chatModel: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K',
|
||
tools: true,
|
||
minRamGb: 6,
|
||
minDiskGb: 3,
|
||
approxDownloadGb: 1,
|
||
ctxSize: 4096
|
||
}
|
||
}
|
||
|
||
/** @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
|
||
}
|
||
|
||
/**
|
||
* Flatten OpenAI nested tool defs → QVAC flat shape.
|
||
* @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<string, unknown>} */ (t)
|
||
if (o.function && typeof o.function === 'object') {
|
||
const fn = /** @type {Record<string, unknown>} */ (o.function)
|
||
out.push({
|
||
type: 'function',
|
||
name: typeof fn.name === 'string' ? fn.name : '',
|
||
description: typeof fn.description === 'string' ? fn.description : '',
|
||
parameters:
|
||
fn.parameters && typeof fn.parameters === 'object'
|
||
? fn.parameters
|
||
: { type: 'object', properties: {} }
|
||
})
|
||
continue
|
||
}
|
||
if (typeof o.name === 'string') {
|
||
out.push({
|
||
type: 'function',
|
||
name: o.name,
|
||
description: typeof o.description === 'string' ? o.description : '',
|
||
parameters:
|
||
o.parameters && typeof o.parameters === 'object'
|
||
? o.parameters
|
||
: { type: 'object', properties: {} }
|
||
})
|
||
}
|
||
}
|
||
return out.filter((x) => x && typeof x === 'object' && /** @type {any} */ (x).name)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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<string, unknown>} 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') 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'
|
||
}
|
||
|
||
/**
|
||
* Resolve context window for QVAC load (profile default or config override).
|
||
* @param {Record<string, unknown>} config
|
||
* @param {BareAgentQvacProfile} profile
|
||
*/
|
||
function bareAgentQvacResolveCtxSize(config, profile) {
|
||
const raw = Number(config && config.qvac_ctx_size)
|
||
if (Number.isFinite(raw) && raw >= 2048) {
|
||
// Cap high overrides; 32k+ KV often OOMs laptop GPUs (~4 GB VRAM).
|
||
return Math.min(32768, Math.floor(raw))
|
||
}
|
||
return Math.max(2048, Number(profile.ctxSize) || 8192)
|
||
}
|
||
|
||
/**
|
||
* Device / main-gpu / layers for QVAC load (config + profile defaults).
|
||
* @param {Record<string, unknown>} 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
|
||
}
|
||
|
||
/**
|
||
* 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
|
||
* @param {{ workspace: string, workspaceSkills: string, dir: string }} paths
|
||
* @param {Record<string, unknown>} [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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
|
||
* @param {{ workspace: string }} paths
|
||
* @param {Record<string, unknown>} 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<string, unknown>} 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<string, string>, 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<string, string>} */
|
||
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<string, unknown>} 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')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Compact Markdown block for system prompt (names + short descriptions only).
|
||
* @param {Record<string, unknown>} 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 =
|
||
'## Available skills (compact index)\n' +
|
||
'Each skill is a directory with **SKILL.md** (optional YAML frontmatter: `name`, `description`, …).\n' +
|
||
'**Workspace skills** (`~/.agent/workspace/skills/`) override **global** (`~/.agent/skills/`) when names match.\n' +
|
||
'To run one: call the **read_skill** tool with the skill id or frontmatter `name` before following its instructions.\n\n'
|
||
if (!skills.length) {
|
||
block += '(No skills discovered yet — add folders under `workspace/skills/<id>/SKILL.md`.)\n'
|
||
return block.length > cap ? block.slice(0, cap) + '\n…\n' : block
|
||
}
|
||
block += '| id | name | source | description |\n| --- | --- | --- | --- |\n'
|
||
for (const s of skills) {
|
||
const desc = s.description.replace(/\|/g, '/').replace(/\r?\n/g, ' ').slice(0, 160)
|
||
block +=
|
||
'| `' +
|
||
s.id.replace(/`/g, "'") +
|
||
'` | ' +
|
||
s.name.replace(/\|/g, '/').replace(/\r?\n/g, ' ') +
|
||
' | ' +
|
||
s.source +
|
||
' | ' +
|
||
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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} */ (cur)[p]
|
||
}
|
||
return cur
|
||
}
|
||
|
||
/**
|
||
* Stream chat/completions; invoke onEvent for each parsed chunk.
|
||
* @param {{
|
||
* fetchFn: typeof fetch,
|
||
* url: string,
|
||
* headers: Record<string, string>,
|
||
* body: Record<string, unknown>,
|
||
* signal?: AbortSignal | null,
|
||
* onEvent: (ev: Record<string, unknown>) => 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<string, unknown>} */ (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.function_call_arguments.delta' &&
|
||
typeof j.delta === 'string'
|
||
) {
|
||
onEvent({
|
||
type: 'delta_tool_calls',
|
||
tool_calls: [{ index: 0, function: { arguments: j.delta } }]
|
||
})
|
||
}
|
||
}
|
||
const choices = bareAgentDeepGet(j, 'choices')
|
||
const ch0 =
|
||
Array.isArray(choices) && choices[0] && typeof choices[0] === 'object'
|
||
? /** @type {Record<string, unknown>} */ (choices[0])
|
||
: null
|
||
const delta =
|
||
ch0 && typeof ch0.delta === 'object'
|
||
? /** @type {Record<string, unknown>} */ (ch0.delta)
|
||
: null
|
||
const finishReason =
|
||
typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : ''
|
||
const usage =
|
||
typeof j.usage === 'object' && j.usage ? j.usage : undefined
|
||
|
||
if (usage) {
|
||
onEvent({ type: 'usage', usage })
|
||
}
|
||
|
||
if (delta) {
|
||
const c = delta.content
|
||
if (typeof c === 'string' && c.length) {
|
||
onEvent({
|
||
type: 'delta_content',
|
||
content: c
|
||
})
|
||
}
|
||
const toolCalls = delta.tool_calls
|
||
if (toolCalls !== undefined)
|
||
onEvent({
|
||
type: 'delta_tool_calls',
|
||
tool_calls: toolCalls
|
||
})
|
||
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<string, unknown>} */ (chunk)
|
||
const tx =
|
||
typeof ro.text === 'string'
|
||
? ro.text
|
||
: typeof ro.content === 'string'
|
||
? ro.content
|
||
: ''
|
||
if (tx) {
|
||
onEvent({
|
||
type: 'delta_reasoning',
|
||
reasoning: tx
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} 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<string, unknown>} 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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||
s = s.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
||
s = s.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '')
|
||
s = s.replace(/<!--[\s\S]*?-->/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 =
|
||
/<a\b[^>]*\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 = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(h)
|
||
const title = titleM
|
||
? bareWebDecodeHtmlEntities(titleM[1].replace(/<[^>]+>/g, ' ')).trim()
|
||
: ''
|
||
let description = ''
|
||
const metaDescRe =
|
||
/<meta[^>]*\bname\s*=\s*["']description["'][^>]*>/i.exec(h)
|
||
if (metaDescRe) description = bareWebMetaContent(metaDescRe[0])
|
||
|
||
let og_title = ''
|
||
const ogT = /<meta[^>]*\bproperty\s*=\s*["']og:title["'][^>]*>/i.exec(h)
|
||
if (ogT) og_title = bareWebMetaContent(ogT[0])
|
||
|
||
let og_description = ''
|
||
const ogD =
|
||
/<meta[^>]*\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<string, unknown>,
|
||
* url: string,
|
||
* method?: string,
|
||
* headers?: Record<string, unknown>,
|
||
* 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<string, string>} */
|
||
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
|
||
|
||
if (method === 'HEAD') {
|
||
return {
|
||
ok: true,
|
||
url_final,
|
||
status: st,
|
||
content_type: ct,
|
||
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,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined,
|
||
truncated: bodyRead.truncated,
|
||
extract,
|
||
raw_preview: fmt === 'raw' || fmt === 'json' ? undefined : raw_preview
|
||
}
|
||
}
|
||
}
|
||
|
||
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* 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"}'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentToolDefinitions() {
|
||
return [
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_file',
|
||
description:
|
||
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute file path' },
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes to read (default 256000)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'write_file',
|
||
description:
|
||
'Create or overwrite a file. Parent directories are created as needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: { type: 'string', description: 'Full file contents' }
|
||
},
|
||
required: ['path', 'content']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_file',
|
||
description:
|
||
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: {
|
||
type: 'string',
|
||
description: 'If set (non-empty), full file replacement'
|
||
},
|
||
old_string: {
|
||
type: 'string',
|
||
description: 'Search string (used with new_string)'
|
||
},
|
||
new_string: { type: 'string', description: 'Replacement text' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_directory',
|
||
description: 'Create a directory (recursive).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_files',
|
||
description:
|
||
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string' },
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to search (default /home)'
|
||
},
|
||
max_lines: { type: 'integer', description: 'Default 200' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_command',
|
||
description:
|
||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<code> line.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: {
|
||
type: 'string',
|
||
description: 'Full command string (e.g. ls -la /bin)'
|
||
},
|
||
timeout_ms: { type: 'integer' },
|
||
capture_exit: {
|
||
type: 'boolean',
|
||
description: 'If true, append last line EXIT:<code> to capture (default false)'
|
||
}
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script',
|
||
description:
|
||
'REQUIRED to run agent-authored JavaScript: Node is not installed. Writes code to ~/.agent/_tmp_agent_run.mjs and runs it by absolute path (Bare kernel — same as /bin scripts). Do not use run_command with node/npm/npx. Prefer async function run(ctx, argv). stdout/stderr captured.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
code: { type: 'string', description: 'Full ESM/CommonJS script body' }
|
||
},
|
||
required: ['code']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_system_info',
|
||
description:
|
||
'Lightweight context: API version, uname, optional resource hook. 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)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'file_stat',
|
||
description:
|
||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
follow_symlinks: {
|
||
type: 'boolean',
|
||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'move_path',
|
||
description:
|
||
'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'delete_path',
|
||
description:
|
||
'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
recursive: {
|
||
type: 'boolean',
|
||
description: 'Remove directories recursively (default false)'
|
||
},
|
||
confirm_token: {
|
||
type: 'string',
|
||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_man_page',
|
||
description:
|
||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||
section: {
|
||
type: 'integer',
|
||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||
},
|
||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||
},
|
||
required: ['topic']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apropos_man',
|
||
description:
|
||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines. 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 a small allowlisted /proc/bare_os pseudo file (bounded). Canonical live kernel feature state: /proc/bare_os/features or /proc/bare_os/features.json (same content). Use instead of shelling cat.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: {
|
||
type: 'string',
|
||
description:
|
||
'Exact allowlisted path (metrics_live.json, features or features.json, capabilities.json, swarm*.json, swarm_*_status.json); bounded read.'
|
||
},
|
||
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/<unit>.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 allowlisted maintenance command with bounded capture for automation workflows.',
|
||
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: 'emit_host_notification',
|
||
description:
|
||
'Request an audited host notification via HRPC route (policy-gated; disabled by default).',
|
||
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 (policy-gated; disabled by default).',
|
||
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 goal, optional scope path, and runtime cap. This enables autonomous mode in config.',
|
||
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 allowlisted /proc/bare_os files that exist (features, swarm, metrics). Prefer over many separate read_proc_file calls.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'task_complete',
|
||
description:
|
||
'Call when the user task is fully done. Provide a concise summary.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
summary: { type: 'string' }
|
||
},
|
||
required: ['summary']
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} absPath
|
||
*/
|
||
function bareAgentPathAllowed(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/')) return false
|
||
const ok =
|
||
p.startsWith('/home/') ||
|
||
p.startsWith('/tmp/') ||
|
||
p === '/tmp' ||
|
||
p.startsWith('/root/') ||
|
||
p.startsWith('/mnt/') ||
|
||
p.startsWith('/bin/') ||
|
||
p === '/bin' ||
|
||
p.startsWith('/etc/') ||
|
||
p.startsWith('/share/') ||
|
||
p.startsWith('/usr/') ||
|
||
p.startsWith('/var/') ||
|
||
p.startsWith('/proc/') ||
|
||
p.startsWith('/boot/') ||
|
||
p.startsWith('/lib/') ||
|
||
p.startsWith('/dev/')
|
||
return ok
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* ctx: Record<string, unknown>,
|
||
* 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<string, unknown> },
|
||
* manCacheRef?: { db: unknown | null },
|
||
* onTaskComplete: (summary: string) => void
|
||
* }} o
|
||
*/
|
||
async function bareAgentDispatchTool(o) {
|
||
const {
|
||
ctx,
|
||
toolName,
|
||
argsJson,
|
||
paths,
|
||
signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
} = o
|
||
const manDbCache = manCacheRef || { db: null }
|
||
/** @type {Record<string, unknown>} */
|
||
let args = {}
|
||
try {
|
||
args = /** @type {Record<string, unknown>} */ (JSON.parse(argsJson || '{}'))
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
|
||
}
|
||
const cfgNow = configRef.current || {}
|
||
if (
|
||
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 })
|
||
}
|
||
|
||
const vfs = ctx.vfs
|
||
const AUTONOMOUS_DENY_PATH_PREFIXES = [
|
||
'/.git',
|
||
'/proc',
|
||
'/dev',
|
||
'/sys',
|
||
'/run',
|
||
'/boot',
|
||
'/lib',
|
||
'/usr/lib'
|
||
]
|
||
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<unknown>} */ (
|
||
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 <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(
|
||
/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi,
|
||
'$1=<redacted>'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @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
|
||
if (!s.startsWith('/')) return false
|
||
for (const pref of AUTONOMOUS_DENY_PATH_PREFIXES) {
|
||
if (s === pref || s.startsWith(pref + '/')) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {string} p
|
||
*/
|
||
function enforceAutonomousPath(p) {
|
||
const cfg = configRef.current || {}
|
||
if (!cfg.autonomous_active) return true
|
||
const path = String(p || '').trim()
|
||
if (!path) return true
|
||
if (!autonomousPathAllowed(path)) return false
|
||
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 === 'task_complete') {
|
||
const summary = typeof args.summary === 'string' ? args.summary : ''
|
||
appendProgress('task_complete: ' + summary.slice(0, 200))
|
||
onTaskComplete(summary || '(done)')
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
completed: true,
|
||
summary
|
||
})
|
||
}
|
||
|
||
if (toolName === '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 (!cfg.autonomous_mode_enabled) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_mode_disabled' })
|
||
}
|
||
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 maxRuntimeMs = Math.min(Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000), 7_200_000)
|
||
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 merged = bareAgentMergeConfigPatch(cfg, {
|
||
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
|
||
})
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
configRef.current = merged
|
||
appendProgress('autonomous_run start goal=' + goal.slice(0, 160))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
active: true,
|
||
goal,
|
||
scope_path: scopePath || null,
|
||
max_runtime_ms: maxRuntimeMs,
|
||
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 merged = bareAgentMergeConfigPatch(cfg, {
|
||
autonomous_stop_requested: true,
|
||
autonomous_status: 'stopped',
|
||
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
|
||
})
|
||
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 args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 1_000_000)
|
||
: 256_000
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({ ok: true, path, content: t })
|
||
}
|
||
|
||
if (toolName === 'write_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
const content = typeof args.content === 'string' ? args.content : ''
|
||
if (!path.startsWith('/') || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('write_file ' + path)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(content)
|
||
: new TextEncoder().encode(content)
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'edit_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
||
}
|
||
appendProgress('edit_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
let prev =
|
||
buf && buf.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
: ''
|
||
const full = typeof args.content === 'string' ? args.content : ''
|
||
const oldStr = typeof args.old_string === 'string' ? args.old_string : ''
|
||
const newStr = typeof args.new_string === 'string' ? args.new_string : ''
|
||
let next = prev
|
||
if (full.length > 0) next = full
|
||
else if (oldStr) {
|
||
if (!prev.includes(oldStr)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
|
||
}
|
||
next = prev.replace(oldStr, newStr)
|
||
} else {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'need content or old_string+new_string'
|
||
})
|
||
}
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(next)
|
||
: new TextEncoder().encode(next)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'create_directory') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_path_denied' })
|
||
}
|
||
if (!path.startsWith('/')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('mkdir ' + path)
|
||
await vfs.mkdir(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true })
|
||
}
|
||
|
||
if (toolName === 'search_files') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||
const root = typeof args.root === 'string' ? args.root : '/home'
|
||
const maxLines =
|
||
typeof args.max_lines === 'number' && Number.isFinite(args.max_lines)
|
||
? Math.min(Math.floor(args.max_lines), 500)
|
||
: 200
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('search_files ' + pattern + ' @ ' + root)
|
||
const cmd =
|
||
'grep -Rnl -- ' +
|
||
bareAgentShellQuote(pattern) +
|
||
' ' +
|
||
bareAgentShellQuote(root) +
|
||
' 2>/dev/null | head -n ' +
|
||
maxLines
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_command') {
|
||
const command = typeof args.command === 'string' ? args.command : ''
|
||
if ((configRef.current || {}).autonomous_active) {
|
||
const cmd = command.trim().toLowerCase()
|
||
if (
|
||
cmd.includes('rm ') ||
|
||
cmd.includes(' git reset') ||
|
||
cmd.includes(' git clean') ||
|
||
cmd.startsWith('git ') ||
|
||
cmd.includes(' git ')
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_command_denied' })
|
||
}
|
||
}
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.floor(args.timeout_ms), 600000)
|
||
: 120000
|
||
const captureExit = Boolean(args.capture_exit)
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('run_command ' + command.slice(0, 160))
|
||
const r = await captureExec(command, timeoutMs, { captureExit })
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_js_script') {
|
||
const code = typeof args.code === 'string' ? args.code : ''
|
||
const scriptPath = paths.dir + '/_tmp_agent_run.mjs'
|
||
if (!vfs?.writeFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script (' + code.length + ' chars)')
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(code)
|
||
: new TextEncoder().encode(code)
|
||
await vfs.writeFile(scriptPath, body)
|
||
/** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'get_system_info') {
|
||
/** @type {Record<string, unknown>} */
|
||
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' })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
||
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 : ''
|
||
const maxEnt =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.floor(args.max_entries), 2000)
|
||
: 500
|
||
const includeStat = Boolean(args.include_stat)
|
||
if (!bareAgentPathAllowed(dir)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_directory ' + dir)
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const arr = Array.isArray(names) ? [...names] : []
|
||
arr.sort()
|
||
const slice = arr.slice(0, maxEnt)
|
||
const base = dir.replace(/\/+$/, '') || '/'
|
||
/** @type {{ name: string, stat?: Record<string, unknown> }[]} */
|
||
const entries = []
|
||
for (const n of slice) {
|
||
const entry = { name: n }
|
||
if (includeStat && (vfs.lstat || vfs.stat)) {
|
||
try {
|
||
const full = base + '/' + n
|
||
const st =
|
||
typeof vfs.lstat === 'function'
|
||
? await vfs.lstat(full)
|
||
: await vfs.stat(full)
|
||
entry.stat = bareAgentSerializeStat(st, full)
|
||
} catch {
|
||
/* ignore per-entry stat errors */
|
||
}
|
||
}
|
||
entries.push(entry)
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dir,
|
||
count: entries.length,
|
||
truncated: arr.length > maxEnt,
|
||
entries
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'file_stat') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const follow = Boolean(args.follow_symlinks)
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('file_stat ' + path)
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (follow && typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
else if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
if (!st) return bareAgentJsonResult({ ok: false, error: 'stat unavailable' })
|
||
const serialized = bareAgentSerializeStat(st, path)
|
||
if (
|
||
serialized.kind === 'symlink' &&
|
||
typeof vfs.readlink === 'function'
|
||
) {
|
||
try {
|
||
serialized.target = await vfs.readlink(path)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult({ ok: true, stat: serialized })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'move_path') {
|
||
const from = typeof args.from_path === 'string' ? args.from_path : ''
|
||
const to = typeof args.to_path === 'string' ? args.to_path : ''
|
||
if (
|
||
!bareAgentPathAllowedMutate(from) ||
|
||
!bareAgentPathAllowedMutate(to) ||
|
||
from.includes('..') ||
|
||
to.includes('..')
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('move_path')
|
||
const cmd =
|
||
'mv -- ' + bareAgentShellQuote(from) + ' ' + bareAgentShellQuote(to)
|
||
const r = await captureExec(cmd, 120000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'delete_path') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const recursive = Boolean(args.recursive)
|
||
const token = typeof args.confirm_token === 'string' ? args.confirm_token : ''
|
||
const cfg = configRef.current
|
||
const allowDel = Boolean(cfg && cfg.allow_delete)
|
||
const reqTok =
|
||
cfg && typeof cfg.require_confirm_token === 'string'
|
||
? String(cfg.require_confirm_token)
|
||
: ''
|
||
if (!allowDel) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'delete_disabled',
|
||
hint: 'set allow_delete true in ~/.agent/config.json'
|
||
})
|
||
}
|
||
if (reqTok && token !== reqTok) {
|
||
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
||
}
|
||
if (!bareAgentPathAllowedMutate(path) || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('delete_path ' + path)
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
const isDir =
|
||
st &&
|
||
typeof st === 'object' &&
|
||
typeof /** @type {{ isDirectory?: () => boolean }} */ (st).isDirectory ===
|
||
'function' &&
|
||
st.isDirectory()
|
||
if (isDir && recursive && typeof vfs.rm === 'function') {
|
||
await vfs.rm(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true, removed: 'directory', recursive: true })
|
||
}
|
||
if (isDir && !recursive) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'is_directory',
|
||
hint: 'pass recursive true to remove a directory tree'
|
||
})
|
||
}
|
||
if (typeof vfs.unlink !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'unlink unavailable' })
|
||
}
|
||
await vfs.unlink(path)
|
||
return bareAgentJsonResult({ ok: true, removed: isDir ? 'directory' : 'file' })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'read_man_page') {
|
||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||
const maxC =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.floor(args.max_chars), 64_000)
|
||
: 12_000
|
||
let secExplicit = null
|
||
if (
|
||
typeof args.section === 'number' &&
|
||
Number.isFinite(args.section) &&
|
||
args.section >= 1 &&
|
||
args.section <= 8
|
||
) {
|
||
secExplicit = Math.floor(args.section)
|
||
}
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('read_man_page ' + topic)
|
||
const resolved = bareAgentManResolvePage(db, topic, secExplicit)
|
||
if ('error' in resolved && resolved.error === 'wrong_section') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'wrong_section',
|
||
foundSection: resolved.foundSection
|
||
})
|
||
}
|
||
if (!resolved.page) {
|
||
return bareAgentJsonResult({ ok: false, error: 'not_found' })
|
||
}
|
||
const slice = bareAgentManExtractPageSlice(resolved.page, maxC)
|
||
return bareAgentJsonResult({ ok: true, ...slice })
|
||
}
|
||
|
||
if (toolName === 'apropos_man') {
|
||
const kw = typeof args.keyword === 'string' ? args.keyword : ''
|
||
const maxRes =
|
||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||
? Math.floor(args.max_results)
|
||
: 40
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('apropos_man ' + kw)
|
||
const { lines, truncated } = bareAgentManAproposHits(db, kw, maxRes)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: lines.length,
|
||
truncated,
|
||
lines
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_proc_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 500_000)
|
||
: 256_000
|
||
if (!bareAgentProcReadPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', allowlist: [...BARE_AGENT_PROC_READ_ALLOWLIST] })
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_proc_file ' + path)
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
let parsed = null
|
||
try {
|
||
parsed = JSON.parse(t)
|
||
} catch {
|
||
parsed = null
|
||
}
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: t,
|
||
json: parsed
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_services') {
|
||
appendProgress('list_services')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
let readinessText = ''
|
||
/** @type {Record<string, unknown> | 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<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} */
|
||
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 <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=<redacted>')
|
||
}
|
||
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<string, unknown>} */
|
||
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<string, unknown>} */
|
||
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<string, unknown>} */
|
||
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<string, unknown>} */
|
||
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<string, string>} */ (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<string, string>} */ (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<string, unknown>} */
|
||
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 === '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)
|
||
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<string, unknown>} */
|
||
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<string, unknown>} */
|
||
const procParts = {}
|
||
const list = BARE_AGENT_PROC_READ_ALLOWLIST
|
||
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<string, unknown>} */ (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 })
|
||
}
|
||
}
|
||
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} base
|
||
* @param {Record<string, unknown>} 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',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'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'
|
||
]
|
||
const numKeys = new Set([
|
||
'max_tokens',
|
||
'temperature',
|
||
'max_iterations',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'reasoning_max_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'
|
||
) {
|
||
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'
|
||
) {
|
||
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 === 'require_confirm_token') {
|
||
out[k] = String(v ?? '')
|
||
} else {
|
||
out[k] = String(v ?? '')
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
patch.extra_headers &&
|
||
typeof patch.extra_headers === 'object' &&
|
||
!Array.isArray(patch.extra_headers)
|
||
) {
|
||
out.extra_headers = { .../** @type {Record<string, string>} */ (patch.extra_headers) }
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSaveConfigFromTools(ctx, paths, config) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs?.writeFile) return
|
||
const json = JSON.stringify(config, null, 2) + '\n'
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(json)
|
||
: new TextEncoder().encode(json)
|
||
await vfs.writeFile(paths.config, body)
|
||
}
|
||
|
||
/** Agent ReAct session: stream, tools, SIGINT, repl suspend (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentLog(ctx, s) {
|
||
try {
|
||
ctx.console.log(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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<string, unknown> | 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<string, unknown> | 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<string, unknown>} 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<string>}
|
||
*/
|
||
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<string, unknown>} ctx
|
||
* @param {import('stream').Readable} stdin
|
||
* @param {import('stream').Writable | undefined} stdout
|
||
* @returns {Promise<string>}
|
||
*/
|
||
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<string, unknown>} 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 autonomous agent inside a JavaScript POSIX-like environment on Hyperdrive + Hyperswarm (Pear/Bare runtime).
|
||
|
||
JavaScript execution on this OS: **Node.js is not installed.** The \`node\`, \`npm\`, and \`npx\` commands **do not exist** and must never appear in plans or in run_command. To run JS as part of your agent work, **you must call the run_js_script tool** (writes under ~/.agent and executes via the Bare kernel). Optional: once a script exists on disk, run_command may invoke it by **absolute path** (e.g. \`/home/.../script.mjs\`)—same mechanism as \`/bin\` scripts—not via \`node\`.
|
||
|
||
Bare OS Created by: Raven Scott (https://raven-scott.fyi)
|
||
|
||
Your Repo: https://git.ssh.surf/snxraven/bare-operating-system
|
||
|
||
Your booter address: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
|
||
|
||
Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/readdir/chmod. Paths under /home (personal Hyperdrive), /mnt, /tmp are writable where policy allows; /bin, /etc are system drive.
|
||
|
||
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
|
||
|
||
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory instead of \`ls\` in run_command when only listing.
|
||
|
||
Skills: modular instructions live under ~/.agent/workspace/skills/ (and optionally ~/.agent/skills/). The system message includes a compact skill index; use the read_skill tool to load full SKILL.md when a task matches a listed skill.
|
||
|
||
Kernel features / capabilities that are **actually enabled or disabled** in this runtime come from **read_proc_file** on \`/proc/bare_os/features\` (same payload as \`/proc/bare_os/features.json\`) and \`/proc/bare_os/capabilities.json\`. **Do not** infer current kernel state from \`apropos_man\` or man pages—that only searches documentation keywords.
|
||
|
||
Prefer tools over guessing for filesystem and shell facts.
|
||
|
||
Reply format (mandatory): Every message you stream to the user must be plain text only — readable in a terminal without a Markdown renderer. Do not use Markdown or similar markup: no emphasis/backtick/code-fence/link syntax, no # headings, no list markers used as markup. Structure with blank lines, short paragraphs, indentation, ALL CAPS or dashed lines for section breaks if needed, and bare URLs when linking. Paths and commands appear as normal text.`
|
||
|
||
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/npx — use run_js_script or run_command with /bin paths only.
|
||
|
||
DOC READING ORDER (complex tasks): Prefer developer-guide README, then 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: source files, canonical reference doc under docs/reference or developer-guide, generated artifact if any (run pretest generators), 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, 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 protocol package; /proc node -> developer-guide 15; ctx API -> bare-os-ctx-api.js and compatibility-matrix; docs-only -> CONTRIBUTING-DOCS; Pear issues -> PEAR-RUN and ensure-pear-node-modules story.
|
||
|
||
ASK FIRST: Destructive deletes, bridge mutations (emit_host_notification, request_host_action), vault/identity exfil patterns, or widening HTTP allowlists — confirm with the user unless config already allows.
|
||
|
||
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and read_proc allowlisted swarm files; closing order is swarm before drives when changing booter lifecycle code.
|
||
|
||
WORKFLOW: Plan briefly, execute tools, verify with read_proc or logs, then task_complete. Before large edits state blast radius (packages, contracts, docs). Before task_complete, self-review: docs updated? generated regen? wrong runtime assumption?
|
||
|
||
STOP CONDITIONS: If the same failing action repeats three times, stop and summarize evidence; do not loop blindly.
|
||
|
||
TOOLS: Use verification_hints for suggested repo-root npm checks when the user describes changed paths (host checkout). Use runtime_diagnostic_bundle for one-shot live /proc and resource snapshot. Use read_skill for workflow skills under workspace/skills/.`
|
||
|
||
/**
|
||
* Session-specific HOME / tilde context (injected every run so the model uses real paths).
|
||
* @param {Record<string, unknown>} 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<string, string>} */ (ctx.env)
|
||
: {}
|
||
const homeEnv = String(env.HOME || '').trim()
|
||
return (
|
||
'## This session: home directory and paths\n' +
|
||
'- **Resolved user home (this session):** `' +
|
||
home +
|
||
'`\n' +
|
||
'- **HOME in the environment:** `' +
|
||
(homeEnv || home) +
|
||
'`\n' +
|
||
'- **Tilde \`~\`:** In shell and in user docs, \`~\` means this home directory. Examples: \`~/.agent\` == `' +
|
||
paths.dir +
|
||
'`, agent config `' +
|
||
paths.config +
|
||
'`. Always expand \`~\` to `' +
|
||
home +
|
||
'\` when constructing absolute paths for tools.\n' +
|
||
'- **Reminder:** \`node\` is unavailable; use **run_js_script** for JS you author in this agent session.\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} tc
|
||
*/
|
||
function bareAgentMergeToolCallDelta(acc, tc) {
|
||
if (!tc || typeof tc !== 'object') return
|
||
const o = /** @type {Record<string, unknown>} */ (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<string, unknown>} */ (fn).name
|
||
const ar = /** @type {Record<string, unknown>} */ (fn).arguments
|
||
if (typeof nm === 'string') cur.name += nm
|
||
if (typeof ar === 'string') cur.args += ar
|
||
}
|
||
acc.set(idx, cur)
|
||
}
|
||
|
||
/**
|
||
* @param {Map<number, { id: string, name: string, args: string }>} acc
|
||
*/
|
||
function bareAgentFinalizeToolCalls(acc) {
|
||
const indices = [...acc.keys()].sort((a, b) => a - b)
|
||
/** @type {unknown[]} */
|
||
const arr = []
|
||
for (const i of indices) {
|
||
const c = acc.get(i)
|
||
if (!c || !c.name) continue
|
||
arr.push({
|
||
id:
|
||
c.id ||
|
||
'call_' +
|
||
i +
|
||
'_' +
|
||
String(Math.random()).slice(2, 10),
|
||
type: 'function',
|
||
function: {
|
||
name: c.name,
|
||
arguments: c.args || '{}'
|
||
}
|
||
})
|
||
}
|
||
return arr
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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<string, unknown>} cfg
|
||
*/
|
||
function bareAgentApplyProviderProfile(cfg) {
|
||
const out = { ...cfg }
|
||
const backend = bareAgentResolveBackend(out)
|
||
out.backend = backend
|
||
if (backend === 'qvac') {
|
||
const profile = bareAgentQvacGetProfile(String(out.qvac_profile || 'recommended'))
|
||
if (!String(out.qvac_model || '').trim()) out.qvac_model = profile.chatModel
|
||
if (!String(out.model || '').trim() || String(out.provider || '') === 'qvac') {
|
||
out.model = String(out.qvac_model || profile.chatModel)
|
||
}
|
||
if (!String(out.provider || '').trim()) out.provider = 'qvac'
|
||
return out
|
||
}
|
||
const provider = String(out.provider || '').trim().toLowerCase()
|
||
const model = String(out.model || '').trim().toLowerCase()
|
||
const defaultGroq = 'https://api.groq.com/openai/v1'
|
||
if (provider === 'groq') {
|
||
const base = String(out.rest_base_url || '').trim()
|
||
if (!base || base === 'https://api.x.ai/v1') out.rest_base_url = defaultGroq
|
||
const cur =
|
||
typeof out.request_timeout_ms === 'number' && Number.isFinite(out.request_timeout_ms)
|
||
? out.request_timeout_ms
|
||
: 120000
|
||
if (cur < 120000) out.request_timeout_ms = 120000
|
||
}
|
||
if (provider === 'xai') {
|
||
const base = String(out.rest_base_url || '').trim()
|
||
if (!base || base === defaultGroq) out.rest_base_url = 'https://api.x.ai/v1'
|
||
const isReasoningModel = model.includes('reasoning') || model.includes('grok-4.20')
|
||
if (isReasoningModel) {
|
||
const cur =
|
||
typeof out.request_timeout_ms === 'number' && Number.isFinite(out.request_timeout_ms)
|
||
? out.request_timeout_ms
|
||
: 120000
|
||
if (cur < 300000) out.request_timeout_ms = 300000
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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 }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {object} opts
|
||
* @param {boolean} opts.setupFlag
|
||
* @param {boolean} opts.interactiveSetup
|
||
*/
|
||
async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) {
|
||
void opts
|
||
bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json')
|
||
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' +
|
||
'Answer each question (Enter keeps the [default]). Typed input is echoed by the terminal.\n\n'
|
||
)
|
||
try {
|
||
const owner =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Owner / human name (who operates this agent) [' +
|
||
(String(config.owner_name || '').trim() || 'unset') +
|
||
']: '
|
||
)) || ''
|
||
if (owner.trim()) config.owner_name = owner.trim()
|
||
const label =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Agent display name / label (shown in notes and logs) [' +
|
||
(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 (QuantumVerse Automatic Computer) — local on-device' +
|
||
(qvacOk ? '' : ' [host bridge unavailable]') +
|
||
'\n' +
|
||
' 2) REST API — OpenAI-compatible (Groq, xAI, …)\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'
|
||
if (String(config.provider || '').trim().toLowerCase() === 'qvac') {
|
||
config.provider = 'groq'
|
||
}
|
||
} else if (v === '1' || v === 'qvac' || v === 'q') {
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
}
|
||
// empty → keep curBackend (defaults applied later)
|
||
}
|
||
if (!String(config.backend || '').trim()) {
|
||
config.backend = curBackend
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'qvac') {
|
||
if (!qvacOk) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nQVAC host bridge is unavailable (BARE_OS_SKIP_QVAC or missing @qvac/sdk).\n' +
|
||
'Switching to REST API setup.\n'
|
||
)
|
||
config.backend = 'rest'
|
||
if (String(config.provider || '').trim().toLowerCase() === 'qvac') {
|
||
config.provider = 'groq'
|
||
}
|
||
} else {
|
||
const profiles = bareAgentQvacProfileList()
|
||
bareAgentWriteOut(ctx, stdout, '\nQVAC model profiles (downloaded on first use):\n')
|
||
for (let i = 0; i < profiles.length; i++) {
|
||
const p = profiles[i]
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
' ' +
|
||
(i + 1) +
|
||
') ' +
|
||
p.label +
|
||
' — ' +
|
||
p.chatModel +
|
||
'\n ' +
|
||
p.description +
|
||
' (~' +
|
||
p.approxDownloadGb +
|
||
' GB; min ~' +
|
||
p.minRamGb +
|
||
' GB RAM)\n'
|
||
)
|
||
}
|
||
const curProf = String(config.qvac_profile || 'recommended')
|
||
let defIdx = profiles.findIndex((p) => p.id === curProf)
|
||
if (defIdx < 0) defIdx = 1
|
||
const profRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Model profile number [' + (defIdx + 1) + ']: '
|
||
)) || ''
|
||
let chosen = bareAgentQvacGetProfile(curProf)
|
||
const n = Number.parseInt(String(profRaw).trim(), 10)
|
||
if (Number.isFinite(n) && n >= 1 && n <= profiles.length) {
|
||
chosen = profiles[n - 1]
|
||
} else if (profRaw.trim()) {
|
||
chosen = bareAgentQvacGetProfile(profRaw.trim())
|
||
}
|
||
config.qvac_profile = chosen.id
|
||
config.qvac_model = chosen.chatModel
|
||
config.model = chosen.chatModel
|
||
config.provider = 'qvac'
|
||
config.backend = 'qvac'
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'Selected QVAC profile "' +
|
||
chosen.label +
|
||
'" → ' +
|
||
chosen.chatModel +
|
||
' (ctx ' +
|
||
chosen.ctxSize +
|
||
'; download on first agent run).\n' +
|
||
'Tip: GPU is auto-picked by VRAM (then CPU). Override with "qvac_main_gpu" / "qvac_device": "cpu".\n'
|
||
)
|
||
}
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'rest') {
|
||
const url =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'REST base URL [' + String(config.rest_base_url || '') + ']: '
|
||
)) || ''
|
||
if (url.trim()) config.rest_base_url = url.trim()
|
||
const keyRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'REST API key [leave empty to skip]: ',
|
||
{ mask: true }
|
||
)) || ''
|
||
if (keyRaw.trim()) config.rest_api_key = keyRaw.trim()
|
||
const modelRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Model [' + String(config.model || '') + ']: '
|
||
)) || ''
|
||
if (modelRaw.trim()) config.model = modelRaw.trim()
|
||
const provRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Provider label (groq/xai/…) [' +
|
||
(String(config.provider || '').trim() &&
|
||
String(config.provider || '').trim().toLowerCase() !== 'qvac'
|
||
? String(config.provider)
|
||
: 'groq') +
|
||
']: '
|
||
)) || ''
|
||
if (provRaw.trim()) config.provider = provRaw.trim()
|
||
else if (
|
||
!String(config.provider || '').trim() ||
|
||
String(config.provider).toLowerCase() === 'qvac'
|
||
) {
|
||
config.provider = 'groq'
|
||
}
|
||
config.backend = 'rest'
|
||
}
|
||
|
||
const autonomousModeRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Enable autonomous coding mode? (y/N) [' +
|
||
(config.autonomous_mode_enabled ? 'y' : 'n') +
|
||
']: '
|
||
)) || ''
|
||
{
|
||
const v = autonomousModeRaw.trim().toLowerCase()
|
||
if (v === 'y' || v === 'yes' || v === '1' || v === 'true') config.autonomous_mode_enabled = true
|
||
else if (v === 'n' || v === 'no' || v === '0' || v === 'false') config.autonomous_mode_enabled = false
|
||
}
|
||
const showReasoningRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Show thinking/process output? (y/N) [' +
|
||
(config.show_reasoning ? 'y' : 'n') +
|
||
']: '
|
||
)) || ''
|
||
{
|
||
const v = showReasoningRaw.trim().toLowerCase()
|
||
if (v === 'y' || v === 'yes' || v === '1' || v === 'true') config.show_reasoning = true
|
||
else if (v === 'n' || v === 'no' || v === '0' || v === 'false') config.show_reasoning = false
|
||
}
|
||
const modeRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Reasoning mode off|summary|trace [' +
|
||
String(config.reasoning_mode || 'off') +
|
||
']: '
|
||
)) || ''
|
||
if (modeRaw.trim()) {
|
||
const m = modeRaw.trim().toLowerCase()
|
||
if (m === 'off' || m === 'summary' || m === 'trace') config.reasoning_mode = m
|
||
}
|
||
const maxCharsRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Reasoning max chars [default ' +
|
||
String(config.reasoning_max_chars || 4000) +
|
||
']: '
|
||
)) || ''
|
||
if (maxCharsRaw.trim()) {
|
||
const n = Number(maxCharsRaw)
|
||
if (Number.isFinite(n) && n >= 200) config.reasoning_max_chars = Math.floor(n)
|
||
}
|
||
const includeToolsRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Include tool traces in process output? (Y/n) [' +
|
||
(config.reasoning_include_tools === false ? 'n' : 'y') +
|
||
']: '
|
||
)) || ''
|
||
{
|
||
const v = includeToolsRaw.trim().toLowerCase()
|
||
if (v === 'y' || v === 'yes' || v === '1' || v === 'true') config.reasoning_include_tools = true
|
||
else if (v === 'n' || v === 'no' || v === '0' || v === 'false') config.reasoning_include_tools = false
|
||
}
|
||
} 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.')
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* Interactive setup only (`agent --setup`).
|
||
* @param {Record<string, unknown>} 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<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {string} task
|
||
* @param {{ setupFlag?: boolean }} [runOpts]
|
||
*/
|
||
async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||
const setupFlag = Boolean(runOpts && runOpts.setupFlag)
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
config = bareAgentApplyProviderProfile(config)
|
||
|
||
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)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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)
|
||
// Local QVAC models have a hard ctx window; keep the system prompt lean.
|
||
const workspaceBudget = backendForPrompt === 'qvac' ? 8000 : 24000
|
||
const manBudget = backendForPrompt === 'qvac' ? 4000 : 12000
|
||
const instructionsBudget = backendForPrompt === 'qvac' ? 3000 : 8000
|
||
const skillsBudget = backendForPrompt === 'qvac' ? 2000 : 4000
|
||
|
||
const workspacePromptBlock = await bareAgentLoadWorkspacePrompt(
|
||
ctx,
|
||
paths,
|
||
workspaceBudget
|
||
)
|
||
const skillsPromptBlock = await bareAgentSkillsCompactPrompt(
|
||
ctx,
|
||
paths,
|
||
skillsBudget
|
||
)
|
||
|
||
let systemContent =
|
||
BARE_AGENT_STATIC_SYSTEM +
|
||
BARE_AGENT_OPERATING_CONTRACT +
|
||
'\n\n' +
|
||
bareAgentSessionHomeBlock(ctx, home, paths) +
|
||
'\n\n' +
|
||
manDigest.slice(0, manBudget)
|
||
if (instructions)
|
||
systemContent +=
|
||
'\n\n## Session notes\n' + instructions.slice(0, instructionsBudget)
|
||
if (workspacePromptBlock && String(workspacePromptBlock).trim())
|
||
systemContent += '\n\n' + String(workspacePromptBlock).trim()
|
||
if (skillsPromptBlock && String(skillsPromptBlock).trim())
|
||
systemContent += '\n\n' + String(skillsPromptBlock).trim()
|
||
|
||
if (!messages.length) {
|
||
messages = [
|
||
{ role: 'system', content: systemContent },
|
||
{ role: 'user', content: task }
|
||
]
|
||
} else {
|
||
const hasSystem =
|
||
messages[0] &&
|
||
typeof messages[0] === 'object' &&
|
||
/** @type {{ role?: string }} */ (messages[0]).role === 'system'
|
||
if (!hasSystem) {
|
||
messages = [{ role: 'system', content: systemContent }, ...messages]
|
||
} else {
|
||
messages[0] = { role: 'system', content: systemContent }
|
||
}
|
||
messages.push({ role: 'user', content: task })
|
||
}
|
||
|
||
const stdout =
|
||
/** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const useColor = bareEditUseColor(ctx)
|
||
|
||
/** @type {{ current: Record<string, unknown> }} */
|
||
const configRef = { current: { ...config } }
|
||
/** @type {{ db: unknown | null }} */
|
||
const manCacheRef = { db: null }
|
||
let completed = false
|
||
let taskSummary = ''
|
||
|
||
function onTaskComplete(summary) {
|
||
completed = true
|
||
taskSummary = summary
|
||
}
|
||
|
||
function appendProgress(line) {
|
||
void bareAgentAppendProgress(ctx, paths.progress, line)
|
||
}
|
||
|
||
const url =
|
||
bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) +
|
||
'/chat/completions'
|
||
const tools = bareAgentToolDefinitions()
|
||
let reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
let autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
let reasoningCharCount = 0
|
||
|
||
let suspended = replSuspendedForSetup
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function' && !suspended) {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
|
||
const masterAbort = new AbortController()
|
||
/** @type {(() => void) | null} */
|
||
let offSigint = null
|
||
if (globalThis.process && typeof globalThis.process.on === 'function') {
|
||
const fn = () => {
|
||
masterAbort.abort()
|
||
bareAgentWriteOut(
|
||
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 maxIter = Number(configRef.current.max_iterations) || 64
|
||
let iter = 0
|
||
|
||
for (;;) {
|
||
reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
if (completed) 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<string, unknown>} */
|
||
const providerNow = String(configRef.current.provider || '').trim().toLowerCase()
|
||
if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] request backend=' +
|
||
backendIter +
|
||
' provider=' +
|
||
(providerNow || 'unknown') +
|
||
' model=' +
|
||
String(configRef.current.model || configRef.current.qvac_model || '') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
let assistantContent = ''
|
||
/** @type {Map<number, { id: string, name: string, args: string }>} */
|
||
const toolAcc = new Map()
|
||
/** @type {unknown} */
|
||
let usageOut = null
|
||
let finishReason = ''
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} e
|
||
*/
|
||
function onCompletionEvent(e) {
|
||
if (e.type === 'delta_content') {
|
||
const chunk = typeof e.content === 'string' ? e.content : ''
|
||
assistantContent += chunk
|
||
bareAgentWriteOut(ctx, stdout, chunk)
|
||
} else if (e.type === 'delta_tool_calls') {
|
||
const arr = e.tool_calls
|
||
if (Array.isArray(arr)) {
|
||
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||
}
|
||
} else if (e.type === 'delta_reasoning' && reasoningSettings.enabled) {
|
||
const chunk = typeof e.reasoning === 'string' ? e.reasoning : ''
|
||
if (chunk && reasoningCharCount < reasoningSettings.maxChars) {
|
||
const remain = reasoningSettings.maxChars - reasoningCharCount
|
||
const out = chunk.slice(0, remain)
|
||
reasoningCharCount += out.length
|
||
if (out.length) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('dim', useColor) +
|
||
'[thinking] ' +
|
||
out +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
}
|
||
} 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') {
|
||
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)
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] ensuring model ' +
|
||
modelSrc +
|
||
' (ctx=' +
|
||
ctxSize +
|
||
(deviceOpts.device ? ', device=' + deviceOpts.device : ', auto-GPU') +
|
||
')…' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
if (typeof ctx.bareOsQvacLoadModel === 'function') {
|
||
const loaded = await ctx.bareOsQvacLoadModel({
|
||
modelSrc,
|
||
tools: profile.tools !== false,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
onProgress: (prog) => {
|
||
const pct =
|
||
prog && typeof prog === 'object' && 'percentage' in prog
|
||
? Number(/** @type {{ percentage?: unknown }} */ (prog).percentage)
|
||
: NaN
|
||
if (Number.isFinite(pct)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\r[qvac] download/load ' +
|
||
Math.floor(pct) +
|
||
'%' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
}
|
||
})
|
||
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) +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
}
|
||
await ctx.bareOsQvacComplete({
|
||
history: messages,
|
||
tools: bareAgentFlattenToolsForQvac(tools),
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
captureThinking: reasoningSettings.enabled,
|
||
modelSrc,
|
||
toolsEnabled: profile.tools !== false,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
signal: masterAbort.signal,
|
||
onProgress: (prog) => {
|
||
const pct =
|
||
prog && typeof prog === 'object' && 'percentage' in prog
|
||
? Number(/** @type {{ percentage?: unknown }} */ (prog).percentage)
|
||
: NaN
|
||
if (Number.isFinite(pct)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\r[qvac] download/load ' +
|
||
Math.floor(pct) +
|
||
'%' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
},
|
||
onEvent: onCompletionEvent
|
||
})
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
} 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
|
||
})
|
||
}
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'name' in e && e.name === 'AbortError'
|
||
? 'aborted'
|
||
: e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': ' + msg)
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
|
||
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||
const hasTools = toolCallsArr.length > 0
|
||
if (!hasTools && finishReason === 'tool_calls') {
|
||
appendProgress('warning tool_calls_finish_without_tool_deltas provider=' + providerNow)
|
||
}
|
||
|
||
/** @type {Record<string, unknown>} */
|
||
const assistantMsg = {
|
||
role: 'assistant',
|
||
content: assistantContent || null,
|
||
tool_calls: hasTools ? toolCallsArr : undefined
|
||
}
|
||
messages.push(assistantMsg)
|
||
|
||
if (usageOut && typeof usageOut === 'object') {
|
||
const u = /** @type {Record<string, unknown>} */ (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 (autonomousSettings.enabled && autonomousSettings.active && autonomousSettings.requiredChecks.length) {
|
||
/** @type {string[]} */
|
||
const failedChecks = []
|
||
for (const check of autonomousSettings.requiredChecks) {
|
||
const res = await bareAgentDispatchTool({
|
||
ctx,
|
||
toolName: 'run_maintenance_gate',
|
||
argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }),
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
})
|
||
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) failedChecks.push(check)
|
||
}
|
||
if (failedChecks.length) {
|
||
configRef.current.autonomous_status = 'needs_fixups'
|
||
configRef.current.autonomous_last_error = 'failed_checks:' + failedChecks.join(',')
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous checks failed ' + failedChecks.join(','))
|
||
messages.push({
|
||
role: 'user',
|
||
content:
|
||
'Autonomous completion gates failed for checks: ' +
|
||
failedChecks.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')
|
||
}
|
||
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'
|
||
)
|
||
}
|
||
|
||
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 (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) +
|
||
'[tool] ' +
|
||
name +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
|
||
const spinner = bareEditSgr('dim', useColor) + '… running ' + name + EDIT_ANSI_RESET
|
||
bareAgentWriteOut(ctx, stdout, spinner + '\r')
|
||
|
||
const resultStr = await bareAgentDispatchTool({
|
||
ctx,
|
||
toolName: name,
|
||
argsJson: argsStr,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
})
|
||
|
||
bareAgentWriteOut(ctx, stdout, '\x1b[K')
|
||
|
||
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 (completed) break
|
||
}
|
||
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
if (completed) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\nDone: ' +
|
||
taskSummary +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
|
||
if (offSigint) offSigint()
|
||
} finally {
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
ctx.resumeReplAfterSubprocess()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Autonomous AI agent: 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(
|
||
'usage: ' +
|
||
argv0 +
|
||
' [--setup | --config | --reset] YOUR_REQUEST_HERE\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --setup\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --config\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --reset\n' +
|
||
' ' +
|
||
argv0 +
|
||
' reset\n' +
|
||
'\n' +
|
||
'Runs an autonomous coding/OS agent. Default backend is QVAC (local on-device);\n' +
|
||
'or configure any OpenAI-compatible HTTPS REST API.\n' +
|
||
'Configuration: ~/.agent/config.json on your personal drive (created on first run).\n' +
|
||
'Use --setup or --config to choose QVAC vs REST, model profile / API URL+key (plain TTY prompts).\n' +
|
||
'Use --reset or `reset` to clear ~/.agent/history.json and start a fresh chat session.\n' +
|
||
'\n' +
|
||
'Examples:\n' +
|
||
' ' +
|
||
argv0 +
|
||
' "summarize ~/README and list five files in /bin"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --setup\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --config\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --reset\n' +
|
||
'\n' +
|
||
'See man agent.'
|
||
)
|
||
ctx.exitCode = wantHelp ? 0 : 1
|
||
return
|
||
}
|
||
|
||
let setupFlag = false
|
||
let resetFlag = false
|
||
/** @type {string[]} */
|
||
const rest = []
|
||
for (let i = 0; i < args.length; i++) {
|
||
const a = args[i]
|
||
if (a === '--setup' || a === '--config') setupFlag = true
|
||
else if (a === '--reset') resetFlag = true
|
||
else rest.push(a)
|
||
}
|
||
|
||
const wantReset =
|
||
resetFlag || (rest.length === 1 && rest[0] === 'reset')
|
||
if (wantReset) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
await bareAgentResetChatSession(ctx, paths, argv0)
|
||
ctx.exitCode = 0
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
}
|
||
return
|
||
}
|
||
|
||
const task = rest.join(' ').trim()
|
||
if (!task && !setupFlag) {
|
||
ctx.console.error(argv0 + ': missing task (or use --setup / --config)')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (setupFlag && !task) {
|
||
await bareAgentRunSetupOnly(ctx, argv0)
|
||
return
|
||
}
|
||
|
||
await bareOsRunAgentSession(ctx, argv0, task, {
|
||
setupFlag
|
||
})
|
||
}
|