3505 lines
104 KiB
Plaintext
3505 lines
104 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.json',
|
||
'/proc/bare_os/swarm.json',
|
||
'/proc/bare_os/capabilities.json'
|
||
])
|
||
|
||
/**
|
||
* @param {unknown} statObj
|
||
* @param {string} path
|
||
*/
|
||
function bareAgentSerializeStat(statObj, path) {
|
||
if (!statObj || typeof statObj !== 'object')
|
||
return { path, error: 'no_stat' }
|
||
const s = /** @type {Record<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'
|
||
}
|
||
}
|
||
|
||
function bareAgentDefaultConfig() {
|
||
return {
|
||
rest_base_url: 'https://api.groq.com/openai/v1',
|
||
rest_api_key: '',
|
||
model: 'llama3-70b-8192',
|
||
max_tokens: 4096,
|
||
temperature: 0.7,
|
||
provider: 'groq',
|
||
max_iterations: 64,
|
||
stream: true,
|
||
tool_parallelism: 1,
|
||
request_timeout_ms: 120000,
|
||
extra_headers: /** @type {Record<string, string>} */ ({}),
|
||
allow_delete: false,
|
||
require_confirm_token: ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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([
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'extra_headers',
|
||
'allow_delete',
|
||
'require_confirm_token'
|
||
])
|
||
for (const k of Object.keys(src)) {
|
||
if (k.startsWith('x-')) continue
|
||
if (!known.has(k)) continue
|
||
const val = src[k]
|
||
if (k === 'extra_headers' && bareAgentIsPlainObject(val)) {
|
||
out.extra_headers = /** @type {Record<string, string>} */ ({ ...val })
|
||
continue
|
||
}
|
||
if (
|
||
k === 'rest_base_url' ||
|
||
k === 'rest_api_key' ||
|
||
k === 'model' ||
|
||
k === 'provider'
|
||
) {
|
||
out[k] = String(val ?? '')
|
||
continue
|
||
}
|
||
if (
|
||
k === 'max_tokens' ||
|
||
k === 'temperature' ||
|
||
k === 'max_iterations' ||
|
||
k === 'tool_parallelism' ||
|
||
k === 'request_timeout_ms'
|
||
) {
|
||
const n = Number(val)
|
||
out[k] = Number.isFinite(n) ? n : defaults[k]
|
||
continue
|
||
}
|
||
if (k === 'stream' || k === 'allow_delete') {
|
||
out[k] = Boolean(val)
|
||
continue
|
||
}
|
||
if (k === 'require_confirm_token') {
|
||
out.require_confirm_token = String(val ?? '')
|
||
continue
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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 = [
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'extra_headers',
|
||
'allow_delete',
|
||
'require_confirm_token'
|
||
]
|
||
if (!known.includes(k)) {
|
||
throw new Error('unknown config key: ' + k)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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 */
|
||
}
|
||
}
|
||
|
||
/** 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 = ''
|
||
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)
|
||
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
|
||
})
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentShellQuote(s) {
|
||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentJsonResult(v) {
|
||
try {
|
||
return JSON.stringify(v)
|
||
} catch {
|
||
return '{"error":"json_stringify_failed"}'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentToolDefinitions() {
|
||
return [
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_file',
|
||
description:
|
||
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute file path' },
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes to read (default 256000)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'write_file',
|
||
description:
|
||
'Create or overwrite a file. Parent directories are created as needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: { type: 'string', description: 'Full file contents' }
|
||
},
|
||
required: ['path', 'content']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_file',
|
||
description:
|
||
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: {
|
||
type: 'string',
|
||
description: 'If set (non-empty), full file replacement'
|
||
},
|
||
old_string: {
|
||
type: 'string',
|
||
description: 'Search string (used with new_string)'
|
||
},
|
||
new_string: { type: 'string', description: 'Replacement text' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_directory',
|
||
description: 'Create a directory (recursive).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_files',
|
||
description:
|
||
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string' },
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to search (default /home)'
|
||
},
|
||
max_lines: { type: 'integer', description: 'Default 200' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_command',
|
||
description:
|
||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<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. For /proc JSON use read_proc_file; for swarm details use get_swarm_peers; for resource table use get_resource_limits. want=capabilities|swarm still returns those blobs when needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
want: {
|
||
type: 'string',
|
||
enum: ['summary', 'capabilities', 'swarm'],
|
||
description: 'Optional focus (default summary)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_agent_config',
|
||
description:
|
||
'Merge keys into ~/.agent/config.json (shallow merge for known keys only).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
patch: {
|
||
type: 'object',
|
||
description:
|
||
'Partial config object (rest_base_url, model, temperature, …)'
|
||
}
|
||
},
|
||
required: ['patch']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_bin',
|
||
description: 'List Tier-1 utilities in /bin via VFS.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
limit: { type: 'integer', description: 'Max names (default 400)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_directory',
|
||
description:
|
||
'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute directory path' },
|
||
max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' },
|
||
include_stat: {
|
||
type: 'boolean',
|
||
description: 'If true, call stat on each entry (slower; default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'file_stat',
|
||
description:
|
||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
follow_symlinks: {
|
||
type: 'boolean',
|
||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'move_path',
|
||
description:
|
||
'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'delete_path',
|
||
description:
|
||
'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
recursive: {
|
||
type: 'boolean',
|
||
description: 'Remove directories recursively (default false)'
|
||
},
|
||
confirm_token: {
|
||
type: 'string',
|
||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_man_page',
|
||
description:
|
||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||
section: {
|
||
type: 'integer',
|
||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||
},
|
||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||
},
|
||
required: ['topic']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apropos_man',
|
||
description:
|
||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
keyword: { type: 'string' },
|
||
max_results: { type: 'integer', description: 'Default 40, max 200' }
|
||
},
|
||
required: ['keyword']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_proc_file',
|
||
description:
|
||
'Read a small allowlisted /proc/bare_os/*.json file (bounded). Use instead of shelling cat.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: {
|
||
type: 'string',
|
||
description:
|
||
'One of: /proc/bare_os/metrics_live.json, features.json, swarm.json, capabilities.json'
|
||
},
|
||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_swarm_peers',
|
||
description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_resource_limits',
|
||
description:
|
||
'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script_at_path',
|
||
description:
|
||
'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute path to .mjs file' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'task_complete',
|
||
description:
|
||
'Call when the user task is fully done. Provide a concise summary.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
summary: { type: 'string' }
|
||
},
|
||
required: ['summary']
|
||
}
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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 },
|
||
* 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 vfs = ctx.vfs
|
||
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 wrapped = captureExit
|
||
? '{ ' +
|
||
line +
|
||
' ; } > ' +
|
||
bareAgentShellQuote(outPath) +
|
||
' 2>&1; printf "\\nEXIT:%s\\n" $? >> ' +
|
||
bareAgentShellQuote(outPath)
|
||
: line +
|
||
' > ' +
|
||
bareAgentShellQuote(outPath) +
|
||
' 2>&1'
|
||
const opts =
|
||
signal || timeoutMs
|
||
? {
|
||
signal,
|
||
timeoutMs: timeoutMs || undefined
|
||
}
|
||
: undefined
|
||
try {
|
||
if (opts) await execLine(wrapped, opts)
|
||
else await execLine(wrapped)
|
||
} catch (e) {
|
||
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, exitNote: msg }
|
||
}
|
||
let captured = ''
|
||
try {
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
const buf = await vfs.readFile(outPath)
|
||
if (buf && buf.length) {
|
||
captured =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const max = 120_000
|
||
if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated'
|
||
return { ok: true, stdout_stderr: captured }
|
||
}
|
||
|
||
try {
|
||
if (toolName === 'task_complete') {
|
||
const summary = typeof args.summary === 'string' ? args.summary : ''
|
||
appendProgress('task_complete: ' + summary.slice(0, 200))
|
||
onTaskComplete(summary || '(done)')
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
completed: true,
|
||
summary
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 1_000_000)
|
||
: 256_000
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({ ok: true, path, content: t })
|
||
}
|
||
|
||
if (toolName === 'write_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const content = typeof args.content === 'string' ? args.content : ''
|
||
if (!path.startsWith('/') || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('write_file ' + path)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(content)
|
||
: new TextEncoder().encode(content)
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'edit_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!bareAgentPathAllowed(path) || !vfs?.readFile || !vfs?.writeFile) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
||
}
|
||
appendProgress('edit_file ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
let prev =
|
||
buf && buf.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
: ''
|
||
const full = typeof args.content === 'string' ? args.content : ''
|
||
const oldStr = typeof args.old_string === 'string' ? args.old_string : ''
|
||
const newStr = typeof args.new_string === 'string' ? args.new_string : ''
|
||
let next = prev
|
||
if (full.length > 0) next = full
|
||
else if (oldStr) {
|
||
if (!prev.includes(oldStr)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
|
||
}
|
||
next = prev.replace(oldStr, newStr)
|
||
} else {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'need content or old_string+new_string'
|
||
})
|
||
}
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(next)
|
||
: new TextEncoder().encode(next)
|
||
let dir = path.replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
||
}
|
||
|
||
if (toolName === 'create_directory') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!path.startsWith('/')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
||
}
|
||
if (!vfs || typeof vfs.mkdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('mkdir ' + path)
|
||
await vfs.mkdir(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true })
|
||
}
|
||
|
||
if (toolName === 'search_files') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||
const root = typeof args.root === 'string' ? args.root : '/home'
|
||
const maxLines =
|
||
typeof args.max_lines === 'number' && Number.isFinite(args.max_lines)
|
||
? Math.min(Math.floor(args.max_lines), 500)
|
||
: 200
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('search_files ' + pattern + ' @ ' + root)
|
||
const cmd =
|
||
'grep -Rnl -- ' +
|
||
bareAgentShellQuote(pattern) +
|
||
' ' +
|
||
bareAgentShellQuote(root) +
|
||
' 2>/dev/null | head -n ' +
|
||
maxLines
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_command') {
|
||
const command = typeof args.command === 'string' ? args.command : ''
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.floor(args.timeout_ms), 600000)
|
||
: 120000
|
||
const captureExit = Boolean(args.capture_exit)
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('run_command ' + command.slice(0, 160))
|
||
const r = await captureExec(command, timeoutMs, { captureExit })
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_js_script') {
|
||
const code = typeof args.code === 'string' ? args.code : ''
|
||
const scriptPath = paths.dir + '/_tmp_agent_run.mjs'
|
||
if (!vfs?.writeFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script (' + code.length + ' chars)')
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(code)
|
||
: new TextEncoder().encode(code)
|
||
await vfs.writeFile(scriptPath, body)
|
||
/** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'get_system_info') {
|
||
/** @type {Record<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 =
|
||
'Prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.'
|
||
}
|
||
if (want === 'capabilities' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/capabilities.json')
|
||
if (b && b.length) {
|
||
info.capabilities_json =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (want === 'swarm' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (b && b.length) {
|
||
info.swarm =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
ctx.b4a.toString
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1')
|
||
if (vfs?.readFile) {
|
||
const buf = await vfs.readFile(paths.cmdOut)
|
||
if (buf && buf.length) {
|
||
info.uname =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf).trim()
|
||
: String(new TextDecoder().decode(buf)).trim()
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
appendProgress('get_system_info ' + want)
|
||
return bareAgentJsonResult({ ok: true, want, info })
|
||
}
|
||
|
||
if (toolName === 'edit_agent_config') {
|
||
const patch = args.patch
|
||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
||
configRef.current = merged
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
appendProgress('edit_agent_config')
|
||
return bareAgentJsonResult({ ok: true, saved: true })
|
||
}
|
||
|
||
if (toolName === 'list_bin') {
|
||
const limit =
|
||
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
||
? Math.min(Math.floor(args.limit), 800)
|
||
: 400
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_bin')
|
||
try {
|
||
const names = await vfs.readdir('/bin')
|
||
const arr = Array.isArray(names) ? [...names].slice(0, limit) : []
|
||
arr.sort()
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: arr.length,
|
||
names: arr
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_directory') {
|
||
const dir = typeof args.path === 'string' ? args.path : ''
|
||
const maxEnt =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.floor(args.max_entries), 2000)
|
||
: 500
|
||
const includeStat = Boolean(args.include_stat)
|
||
if (!bareAgentPathAllowed(dir)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_directory ' + dir)
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const arr = Array.isArray(names) ? [...names] : []
|
||
arr.sort()
|
||
const slice = arr.slice(0, maxEnt)
|
||
const base = dir.replace(/\/+$/, '') || '/'
|
||
/** @type {{ name: string, stat?: Record<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 === 'get_swarm_peers') {
|
||
appendProgress('get_swarm_peers')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const buf = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (!buf || !buf.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
}
|
||
const txt =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
/** @type {unknown} */
|
||
let j = null
|
||
try {
|
||
j = JSON.parse(txt)
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: true, raw: txt.slice(0, 120_000) })
|
||
}
|
||
return bareAgentJsonResult({ ok: true, swarm: j })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_resource_limits') {
|
||
appendProgress('get_resource_limits')
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus !== 'function') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'bareOsGetResourceStatus unavailable'
|
||
})
|
||
}
|
||
const r = ctx.bareOsGetResourceStatus()
|
||
return bareAgentJsonResult({ ok: true, resources: r })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'run_js_script_at_path') {
|
||
const scriptPath = typeof args.path === 'string' ? args.path : ''
|
||
if (!bareAgentPathAllowed(scriptPath) || scriptPath.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs?.readFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script_at_path ' + scriptPath)
|
||
try {
|
||
await vfs.readFile(scriptPath)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: 'cannot_read_script', detail: msg })
|
||
}
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} base
|
||
* @param {Record<string, unknown>} patch
|
||
*/
|
||
function bareAgentMergeConfigPatch(base, patch) {
|
||
const out = { ...base }
|
||
const keys = [
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'allow_delete',
|
||
'require_confirm_token'
|
||
]
|
||
const numKeys = new Set([
|
||
'max_tokens',
|
||
'temperature',
|
||
'max_iterations',
|
||
'tool_parallelism',
|
||
'request_timeout_ms'
|
||
])
|
||
for (const k of keys) {
|
||
if (Object.prototype.hasOwnProperty.call(patch, k)) {
|
||
/** @type {unknown} */
|
||
const v = patch[k]
|
||
if (numKeys.has(k)) {
|
||
const n = Number(v)
|
||
if (Number.isFinite(n)) out[k] = n
|
||
} else if (k === 'stream' || k === 'allow_delete') {
|
||
out[k] = Boolean(v)
|
||
} else if (k === 'require_confirm_token') {
|
||
out[k] = String(v ?? '')
|
||
} else {
|
||
out[k] = String(v ?? '')
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
patch.extra_headers &&
|
||
typeof patch.extra_headers === 'object' &&
|
||
!Array.isArray(patch.extra_headers)
|
||
) {
|
||
out.extra_headers = { .../** @type {Record<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 */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {import('stream').Writable | undefined} out
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentWriteOut(out, s) {
|
||
if (out && typeof out.write === 'function') {
|
||
try {
|
||
out.write(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
try {
|
||
process.stdout.write(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS autonomous agent inside a JavaScript POSIX-like environment on Hyperdrive + Hyperswarm (Pear/Bare runtime).
|
||
|
||
JavaScript execution on this OS: **Node.js is not installed.** The \`node\`, \`npm\`, and \`npx\` commands **do not exist** and must never appear in plans or in run_command. To run JS as part of your agent work, **you must call the run_js_script tool** (writes under ~/.agent and executes via the Bare kernel). Optional: once a script exists on disk, run_command may invoke it by **absolute path** (e.g. \`/home/.../script.mjs\`)—same mechanism as \`/bin\` scripts—not via \`node\`.
|
||
|
||
Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/readdir/chmod. Paths under /home (personal Hyperdrive), /mnt, /tmp are writable where policy allows; /bin, /etc are system drive.
|
||
|
||
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
|
||
|
||
Discovery: man <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; use list_directory instead of \`ls\` in run_command when only listing.
|
||
|
||
Prefer tools over guessing for filesystem and shell facts.`
|
||
|
||
/**
|
||
* Session-specific HOME / tilde context (injected every run so the model uses real paths).
|
||
* @param {Record<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>} ctx
|
||
* @param {string} argv0
|
||
* @param {object} opts
|
||
* @param {boolean} opts.setupFlag
|
||
* @param {boolean} opts.interactiveSetup
|
||
*/
|
||
async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) {
|
||
bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json')
|
||
const readLine =
|
||
typeof ctx.readLine === 'function'
|
||
? /** @type {(p: string) => Promise<string | null>} */ (
|
||
ctx.readLine.bind(ctx)
|
||
)
|
||
: null
|
||
if (!readLine) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': interactive setup requires ctx.readLine (TTY session recommended). Edit ' +
|
||
paths.config +
|
||
' manually.'
|
||
)
|
||
return config
|
||
}
|
||
const url =
|
||
(await readLine(`REST base URL [${config.rest_base_url}]: `)) || ''
|
||
if (url.trim()) config.rest_base_url = url.trim()
|
||
const keyRaw =
|
||
(await readLine(`REST API key (paste; may echo) [leave empty to skip]: `)) ||
|
||
''
|
||
if (keyRaw.trim()) config.rest_api_key = keyRaw.trim()
|
||
const modelRaw =
|
||
(await readLine(`Model [${config.model}]: `)) || ''
|
||
if (modelRaw.trim()) config.model = modelRaw.trim()
|
||
const provRaw =
|
||
(await readLine(`Provider label [${config.provider}]: `)) || ''
|
||
if (provRaw.trim()) config.provider = provRaw.trim()
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
bareAgentLog(ctx, 'Configuration saved.')
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* Interactive setup only (`agent --setup`).
|
||
* @param {Record<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 = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag: true,
|
||
interactiveSetup: true
|
||
})
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
void config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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)
|
||
|
||
const readLine =
|
||
typeof ctx.readLine === 'function'
|
||
? /** @type {(p: string) => Promise<string | null>} */ (
|
||
ctx.readLine.bind(ctx)
|
||
)
|
||
: null
|
||
const stdin = /** @type {{ isTTY?: boolean } | undefined} */ (ctx.replStdin)
|
||
const isTTY = Boolean(stdin && stdin.isTTY)
|
||
if (
|
||
setupFlag ||
|
||
(!(config.rest_api_key && String(config.rest_api_key).trim()) &&
|
||
readLine &&
|
||
isTTY)
|
||
) {
|
||
config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag,
|
||
interactiveSetup: true
|
||
})
|
||
}
|
||
|
||
if (!config.rest_api_key || !String(config.rest_api_key).trim()) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': set rest_api_key in ' +
|
||
paths.config +
|
||
' or run `' +
|
||
argv0 +
|
||
' --setup`.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore — optional */
|
||
}
|
||
|
||
const fetchFn = bareAgentResolveFetch(ctx)
|
||
if (!fetchFn) {
|
||
bareAgentErr(ctx, argv0 + ': no fetch (ctx.httpFetch / bare.fetch / global fetch)')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
/** @type {unknown[]} */
|
||
let messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
const instructions = await bareAgentLoadInstructionFiles(ctx, paths)
|
||
const manDigest = await bareAgentManDigest(ctx)
|
||
|
||
let systemContent =
|
||
BARE_AGENT_STATIC_SYSTEM +
|
||
'\n\n' +
|
||
bareAgentSessionHomeBlock(ctx, home, paths) +
|
||
'\n\n' +
|
||
manDigest.slice(0, 12000)
|
||
if (instructions)
|
||
systemContent += '\n\n## Session notes\n' + instructions.slice(0, 8000)
|
||
|
||
if (!messages.length) {
|
||
messages = [
|
||
{ role: 'system', content: systemContent },
|
||
{ role: 'user', content: task }
|
||
]
|
||
} else {
|
||
const hasSystem =
|
||
messages[0] &&
|
||
typeof messages[0] === 'object' &&
|
||
/** @type {{ role?: string }} */ (messages[0]).role === 'system'
|
||
if (!hasSystem) {
|
||
messages = [{ role: 'system', content: systemContent }, ...messages]
|
||
} else {
|
||
messages[0] = { role: 'system', content: systemContent }
|
||
}
|
||
messages.push({ role: 'user', content: task })
|
||
}
|
||
|
||
const stdout =
|
||
/** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const useColor = bareEditUseColor(ctx)
|
||
|
||
/** @type {{ current: Record<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 suspended = false
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
|
||
const masterAbort = new AbortController()
|
||
/** @type {(() => void) | null} */
|
||
let offSigint = null
|
||
if (globalThis.process && typeof globalThis.process.on === 'function') {
|
||
const fn = () => {
|
||
masterAbort.abort()
|
||
bareAgentWriteOut(
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n'
|
||
)
|
||
}
|
||
globalThis.process.on('SIGINT', fn)
|
||
offSigint = () => {
|
||
try {
|
||
globalThis.process.off('SIGINT', fn)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
const maxIter = Number(configRef.current.max_iterations) || 64
|
||
let iter = 0
|
||
|
||
for (;;) {
|
||
if (completed) break
|
||
iter++
|
||
if (iter > maxIter) {
|
||
bareAgentErr(ctx, argv0 + ': max_iterations exceeded')
|
||
ctx.exitCode = 1
|
||
break
|
||
}
|
||
|
||
messages = bareAgentTrimMessages(messages, 450_000)
|
||
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
Authorization: 'Bearer ' + String(configRef.current.rest_api_key || '')
|
||
}
|
||
const eh = configRef.current.extra_headers
|
||
if (eh && typeof eh === 'object' && !Array.isArray(eh)) {
|
||
for (const [k, v] of Object.entries(eh)) {
|
||
if (typeof v === 'string') headers[k] = v
|
||
}
|
||
}
|
||
|
||
/** @type {Record<string, unknown>} */
|
||
const body = {
|
||
model: String(configRef.current.model || ''),
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
messages,
|
||
tools,
|
||
tool_choice: 'auto',
|
||
max_tokens: Number(configRef.current.max_tokens) || 4096,
|
||
temperature: Number(configRef.current.temperature) ?? 0.7
|
||
}
|
||
|
||
let assistantContent = ''
|
||
/** @type {Map<number, { id: string, name: string, args: string }>} */
|
||
const toolAcc = new Map()
|
||
/** @type {unknown} */
|
||
let usageOut = null
|
||
let finishReason = ''
|
||
|
||
try {
|
||
await bareAgentStreamChatCompletions({
|
||
fetchFn,
|
||
url,
|
||
headers,
|
||
body,
|
||
signal: masterAbort.signal,
|
||
onEvent: (ev) => {
|
||
const e = /** @type {Record<string, unknown>} */ (ev)
|
||
if (e.type === 'delta_content') {
|
||
const chunk = typeof e.content === 'string' ? e.content : ''
|
||
assistantContent += chunk
|
||
bareAgentWriteOut(
|
||
stdout,
|
||
bareEditSgr('string', useColor) + chunk + EDIT_ANSI_RESET
|
||
)
|
||
} else if (e.type === 'delta_tool_calls') {
|
||
const arr = e.tool_calls
|
||
if (Array.isArray(arr)) {
|
||
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||
}
|
||
} else if (e.type === 'usage') {
|
||
usageOut = e.usage
|
||
} else if (e.type === 'finish_reason') {
|
||
finishReason = String(e.finish_reason || '')
|
||
}
|
||
}
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'name' in e && e.name === 'AbortError'
|
||
? 'aborted'
|
||
: e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': ' + msg)
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
|
||
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||
const hasTools = toolCallsArr.length > 0
|
||
|
||
/** @type {Record<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(
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('dim', useColor) +
|
||
'tokens: prompt=' +
|
||
String(pt ?? '?') +
|
||
' completion=' +
|
||
String(ct ?? '?') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
if (!hasTools) {
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
bareAgentWriteOut(stdout, '\n')
|
||
break
|
||
}
|
||
|
||
appendProgress(
|
||
'iteration ' +
|
||
iter +
|
||
' tools ' +
|
||
toolCallsArr.map((x) =>
|
||
/** @type {{ function?: { name?: string } }} */ (x).function?.name
|
||
).join(',')
|
||
)
|
||
|
||
for (const tc of toolCallsArr) {
|
||
const fn = /** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ (
|
||
tc
|
||
).function
|
||
const id = /** @type {{ id?: string }} */ (tc).id || ''
|
||
const name = fn?.name || ''
|
||
const argsStr = fn?.arguments || '{}'
|
||
bareAgentWriteOut(
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('keyword', useColor) +
|
||
'[tool] ' +
|
||
name +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
|
||
const spinner = bareEditSgr('dim', useColor) + '… running ' + name + EDIT_ANSI_RESET
|
||
bareAgentWriteOut(stdout, spinner + '\r')
|
||
|
||
const resultStr = await bareAgentDispatchTool({
|
||
ctx,
|
||
toolName: name,
|
||
argsJson: argsStr,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
})
|
||
|
||
bareAgentWriteOut(stdout, '\x1b[K')
|
||
|
||
messages.push({
|
||
role: 'tool',
|
||
tool_call_id: id,
|
||
content: resultStr
|
||
})
|
||
|
||
if (completed) break
|
||
}
|
||
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
if (completed) {
|
||
bareAgentWriteOut(
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\nDone: ' +
|
||
taskSummary +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
|
||
if (offSigint) offSigint()
|
||
} finally {
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
ctx.resumeReplAfterSubprocess()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Autonomous AI agent: OpenAI-compatible REST, ReAct loop, ~/.agent/config.json.
|
||
*/
|
||
async function run(ctx, argv) {
|
||
const argv0 = argv[0] || 'agent'
|
||
const args = argv.slice(1)
|
||
const wantHelp = args.includes('-h') || args.includes('--help')
|
||
if (wantHelp || args.length === 0) {
|
||
ctx.console.log(
|
||
'usage: ' +
|
||
argv0 +
|
||
' [--setup | --reset] YOUR_REQUEST_HERE\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --setup\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --reset\n' +
|
||
' ' +
|
||
argv0 +
|
||
' reset\n' +
|
||
'\n' +
|
||
'Runs an autonomous coding/OS agent against any OpenAI-compatible HTTPS API.\n' +
|
||
'Configuration: ~/.agent/config.json on your personal drive (created on first run).\n' +
|
||
'Use --setup to interactively set API URL, key, model, and provider label.\n' +
|
||
'Use --reset or `reset` to clear ~/.agent/history.json and start a fresh chat session.\n' +
|
||
'\n' +
|
||
'Examples:\n' +
|
||
' ' +
|
||
argv0 +
|
||
' "summarize ~/README and list five files in /bin"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --setup\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --reset\n' +
|
||
'\n' +
|
||
'See man agent.'
|
||
)
|
||
ctx.exitCode = wantHelp ? 0 : 1
|
||
return
|
||
}
|
||
|
||
let setupFlag = false
|
||
let resetFlag = false
|
||
/** @type {string[]} */
|
||
const rest = []
|
||
for (let i = 0; i < args.length; i++) {
|
||
const a = args[i]
|
||
if (a === '--setup') setupFlag = true
|
||
else if (a === '--reset') resetFlag = true
|
||
else rest.push(a)
|
||
}
|
||
|
||
const wantReset =
|
||
resetFlag || (rest.length === 1 && rest[0] === 'reset')
|
||
if (wantReset) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
await bareAgentResetChatSession(ctx, paths, argv0)
|
||
ctx.exitCode = 0
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
}
|
||
return
|
||
}
|
||
|
||
const task = rest.join(' ').trim()
|
||
if (!task && !setupFlag) {
|
||
ctx.console.error(argv0 + ': missing task (or use --setup)')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (setupFlag && !task) {
|
||
await bareAgentRunSetupOnly(ctx, argv0)
|
||
return
|
||
}
|
||
|
||
await bareOsRunAgentSession(ctx, argv0, task, {
|
||
setupFlag
|
||
})
|
||
}
|