4302 lines
128 KiB
Plaintext
4302 lines
128 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||
function bareStdin(ctx) {
|
||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||
}
|
||
|
||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||
function bareFormatModeString(mode, type) {
|
||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||
const perm = mode & 0o777
|
||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||
return (
|
||
typeChar +
|
||
r(0o400) +
|
||
w(0o200) +
|
||
x(0o100) +
|
||
r(0o040) +
|
||
w(0o020) +
|
||
x(0o010) +
|
||
r(0o004) +
|
||
w(0o002) +
|
||
x(0o001)
|
||
)
|
||
}
|
||
|
||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||
const now = nowMs != null ? nowMs : Date.now()
|
||
const d = new Date(mtimeMs)
|
||
const months = [
|
||
'Jan',
|
||
'Feb',
|
||
'Mar',
|
||
'Apr',
|
||
'May',
|
||
'Jun',
|
||
'Jul',
|
||
'Aug',
|
||
'Sep',
|
||
'Oct',
|
||
'Nov',
|
||
'Dec'
|
||
]
|
||
const mon = months[d.getMonth()]
|
||
const day = String(d.getDate()).padStart(2, ' ')
|
||
const sixMo = 180 * 24 * 3600 * 1000
|
||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||
return mon + ' ' + day + ' ' + yr
|
||
}
|
||
const hh = String(d.getHours()).padStart(2, '0')
|
||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||
}
|
||
|
||
/** @param {number} size */
|
||
function barePosixBlocks(size) {
|
||
return Math.ceil(Number(size) / 512) || 0
|
||
}
|
||
|
||
/**
|
||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string | Uint8Array} chunk
|
||
* @returns {boolean}
|
||
*/
|
||
function bareOsEmitRaw(ctx, chunk) {
|
||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||
const b4 = ctx.b4a
|
||
const u8 =
|
||
typeof chunk === 'string'
|
||
? b4 && typeof b4.from === 'function'
|
||
? b4.from(chunk)
|
||
: new TextEncoder().encode(chunk)
|
||
: chunk
|
||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||
return true
|
||
}
|
||
const w = globalThis.process?.stdout?.write
|
||
if (typeof w === 'function') {
|
||
w.call(globalThis.process.stdout, chunk)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** ANSI helpers for /bin/edit (preamble; no import in src). */
|
||
|
||
const EDIT_ANSI_RESET = '\x1b[0m'
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
* @returns {import('stream').Writable | undefined}
|
||
*/
|
||
function bareEditResolveStdout(ctx) {
|
||
if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout
|
||
const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx)
|
||
const out = c.replStdout || c.stdout || globalThis.process?.stdout
|
||
return /** @type {import('stream').Writable | undefined} */ (out)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
*/
|
||
function bareEditUseColor(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false
|
||
const out = bareEditResolveStdout(ctx)
|
||
return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY)
|
||
}
|
||
|
||
/**
|
||
* @param {'keyword'|'string'|'comment'|'number'|'status'|'inverse'|'dim'} cls
|
||
* @param {boolean} on
|
||
*/
|
||
function bareEditSgr(cls, on) {
|
||
if (!on) return ''
|
||
switch (cls) {
|
||
case 'keyword':
|
||
return '\x1b[36m'
|
||
case 'string':
|
||
return '\x1b[32m'
|
||
case 'comment':
|
||
return '\x1b[90m'
|
||
case 'number':
|
||
return '\x1b[33m'
|
||
case 'status':
|
||
return '\x1b[44m\x1b[97m'
|
||
case 'inverse':
|
||
return '\x1b[7m'
|
||
case 'dim':
|
||
return '\x1b[2m'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} fullLine
|
||
* @param {Array<{ start: number, end: number, cls: string }>} spans offsets into fullLine
|
||
* @param {number} visStart first column (0-based)
|
||
* @param {number} maxLen max code units to show
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareEditPaintLineWindow(fullLine, spans, visStart, maxLen, useColor) {
|
||
const slice = fullLine.slice(visStart, visStart + maxLen)
|
||
if (!useColor) return slice
|
||
const n = slice.length
|
||
const relSpans = spans
|
||
.map((s) => ({
|
||
start: Math.max(0, s.start - visStart),
|
||
end: Math.min(n, s.end - visStart),
|
||
cls: s.cls
|
||
}))
|
||
.filter((s) => s.end > 0 && s.start < n)
|
||
.sort((a, b) => a.start - b.start)
|
||
|
||
let out = ''
|
||
let pos = 0
|
||
for (const sp of relSpans) {
|
||
if (sp.start > pos) out += slice.slice(pos, sp.start)
|
||
out +=
|
||
bareEditSgr(/** @type {'keyword'} */ (sp.cls), true) +
|
||
slice.slice(sp.start, sp.end) +
|
||
EDIT_ANSI_RESET
|
||
pos = sp.end
|
||
}
|
||
if (pos < n) out += slice.slice(pos)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Move cursor (1-based row/col, DEC origin). Clamp to sane bounds for escape parsing.
|
||
* @param {number} row1
|
||
* @param {number} col1
|
||
*/
|
||
function bareEditCup(row1, col1) {
|
||
const r = Math.max(1, Math.min(Math.floor(row1), 9999))
|
||
const c = Math.max(1, Math.min(Math.floor(col1), 9999))
|
||
return '\x1b[' + r + ';' + c + 'H'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Writable} stdout
|
||
* @param {string} s
|
||
*/
|
||
function bareEditWrite(ctx, stdout, s) {
|
||
if (!stdout || typeof stdout.write !== 'function') return
|
||
try {
|
||
stdout.write(s)
|
||
} catch {
|
||
try {
|
||
ctx.console?.error?.('edit: stdout write failed')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/** TTY key parsing for /bin/edit: consume one logical key from a mutable byte queue. */
|
||
|
||
/**
|
||
* @param {number} b1
|
||
*/
|
||
function bareEditUtf8TrailCount(b1) {
|
||
if (b1 >= 0xc0 && b1 < 0xe0) return 1
|
||
if (b1 >= 0xe0 && b1 < 0xf0) return 2
|
||
if (b1 >= 0xf0) return 3
|
||
return 0
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditUtf8DecodeKey(bytes) {
|
||
try {
|
||
const u = new Uint8Array(bytes)
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(u)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} seq CSI payload after ESC [, including final byte (e.g. "A", "1;5A", "3~")
|
||
*/
|
||
function bareEditCsiToEvent(seq) {
|
||
if (seq === '3~') return { type: 'ctrl', code: 'delete' }
|
||
if (seq === '5~') return { type: 'nav', key: 'pageup' }
|
||
if (seq === '6~') return { type: 'nav', key: 'pagedown' }
|
||
const last = seq.charAt(seq.length - 1)
|
||
if (last === '~') {
|
||
if (seq === '1~' || seq === '7~') return { type: 'nav', key: 'home' }
|
||
if (seq === '4~' || seq === '8~') return { type: 'nav', key: 'end' }
|
||
const tildeNum = /^(\d+)~$/.exec(seq)
|
||
if (tildeNum) {
|
||
const n = parseInt(tildeNum[1], 10)
|
||
const fnMap = {
|
||
11: 1,
|
||
12: 2,
|
||
13: 3,
|
||
14: 4,
|
||
15: 5,
|
||
17: 6,
|
||
18: 7,
|
||
19: 8,
|
||
20: 9,
|
||
21: 10,
|
||
23: 11,
|
||
24: 12
|
||
}
|
||
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
|
||
}
|
||
return { type: 'unknown' }
|
||
}
|
||
if (last === 'Z') return { type: 'nav', key: 'stab' }
|
||
if (last === 'A' || last === 'B' || last === 'C' || last === 'D') {
|
||
const map = { A: 'up', B: 'down', C: 'right', D: 'left' }
|
||
return { type: 'nav', key: map[last] }
|
||
}
|
||
if (last === 'H') return { type: 'nav', key: 'home' }
|
||
if (last === 'F') return { type: 'nav', key: 'end' }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number} b3 byte after ESC O
|
||
*/
|
||
function bareEditSs3ToEvent(b3) {
|
||
if (b3 === 72) return { type: 'nav', key: 'home' }
|
||
if (b3 === 70) return { type: 'nav', key: 'end' }
|
||
if (b3 === 65) return { type: 'nav', key: 'up' }
|
||
if (b3 === 66) return { type: 'nav', key: 'down' }
|
||
if (b3 === 67) return { type: 'nav', key: 'right' }
|
||
if (b3 === 68) return { type: 'nav', key: 'left' }
|
||
if (b3 === 80) return { type: 'fn', n: 1 }
|
||
if (b3 === 81) return { type: 'fn', n: 2 }
|
||
if (b3 === 82) return { type: 'fn', n: 3 }
|
||
if (b3 === 83) return { type: 'fn', n: 4 }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/** ESC [ 200 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
|
||
/** ESC [ 201 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
|
||
|
||
/**
|
||
* @param {number[]} q
|
||
* @param {number[]} prefix
|
||
*/
|
||
function bareEditStartsWithBytes(q, prefix) {
|
||
if (q.length < prefix.length) return false
|
||
for (let i = 0; i < prefix.length; i++) {
|
||
if (q[i] !== prefix[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
|
||
* @param {number[]} q
|
||
*/
|
||
function bareEditCouldBeBracketPastePrefix(q) {
|
||
const pre = BARE_EDIT_BRACKET_PASTE_START
|
||
const n = Math.min(q.length, pre.length)
|
||
for (let i = 0; i < n; i++) {
|
||
if (q[i] !== pre[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditDecodePasteInner(bytes) {
|
||
try {
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(
|
||
new Uint8Array(bytes)
|
||
)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
|
||
* @param {number[]} q
|
||
* @returns {string|undefined}
|
||
*/
|
||
function bareEditFinalizeBracketedPasteOnEof(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
const inner = q.slice(SL)
|
||
q.length = 0
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
|
||
* @param {number[]} q mutable queue
|
||
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
|
||
*/
|
||
function bareEditTryConsumeBracketedPaste(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
const EL = BARE_EDIT_BRACKET_PASTE_END.length
|
||
if (!q.length) return undefined
|
||
if (q.length < SL) {
|
||
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
|
||
}
|
||
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
return undefined
|
||
}
|
||
for (let i = SL; i <= q.length - EL; i++) {
|
||
let ok = true
|
||
for (let j = 0; j < EL; j++) {
|
||
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
|
||
ok = false
|
||
break
|
||
}
|
||
}
|
||
if (ok) {
|
||
const inner = q.slice(SL, i)
|
||
q.splice(0, i + EL)
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q mutable queue (front = index 0)
|
||
* @returns {Record<string, unknown> | null} null if more bytes needed
|
||
*/
|
||
function bareEditTryConsumeKey(q) {
|
||
if (!q.length) return null
|
||
const b1 = q[0]
|
||
if (b1 === 3) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'interrupt' }
|
||
}
|
||
if (b1 === 8 || b1 === 127) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'backspace' }
|
||
}
|
||
if (b1 === 13 || b1 === 10) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\n' }
|
||
}
|
||
if (b1 === 9) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\t' }
|
||
}
|
||
if (b1 === 27) {
|
||
if (q.length < 2) return null
|
||
const b2 = q[1]
|
||
if (b2 === 91) {
|
||
let i = 2
|
||
while (i < q.length) {
|
||
const b = q[i]
|
||
if (b >= 0x40 && b <= 0x7e) {
|
||
const seq = String.fromCharCode.apply(null, q.slice(2, i + 1))
|
||
q.splice(0, i + 1)
|
||
return bareEditCsiToEvent(seq)
|
||
}
|
||
i++
|
||
}
|
||
return null
|
||
}
|
||
if (b2 === 79) {
|
||
if (q.length < 3) return null
|
||
const b3 = q[2]
|
||
q.splice(0, 3)
|
||
return bareEditSs3ToEvent(b3)
|
||
}
|
||
q.splice(0, 2)
|
||
return { type: 'key', ch: String.fromCharCode(b2) }
|
||
}
|
||
if (b1 < 0x20) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: b1 }
|
||
}
|
||
const need = bareEditUtf8TrailCount(b1)
|
||
if (q.length < 1 + need) return null
|
||
const chunk = q.splice(0, 1 + need)
|
||
return { type: 'key', ch: bareEditUtf8DecodeKey(chunk) }
|
||
}
|
||
|
||
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
|
||
|
||
/**
|
||
* @param {unknown} chunk
|
||
* @returns {number[]}
|
||
*/
|
||
function bareEditChunkBytes(chunk) {
|
||
if (chunk == null) return []
|
||
if (typeof chunk === 'string') {
|
||
const out = []
|
||
for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff)
|
||
return out
|
||
}
|
||
const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length
|
||
const out = []
|
||
for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
|
||
*/
|
||
async function bareEditReadKey(reader) {
|
||
reader._keyq = reader._keyq || []
|
||
const q = reader._keyq
|
||
for (;;) {
|
||
const pasteText = bareEditTryConsumeBracketedPaste(q)
|
||
if (pasteText !== undefined && pasteText !== null) {
|
||
return { type: 'paste', text: pasteText }
|
||
}
|
||
if (pasteText === null) {
|
||
const b = await reader.nextByte()
|
||
if (b === undefined) {
|
||
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
|
||
if (eofPaste !== undefined) {
|
||
return { type: 'paste', text: eofPaste }
|
||
}
|
||
if (!q.length) return { type: 'eof' }
|
||
if (q.length === 1 && q[0] === 27) {
|
||
q.length = 0
|
||
return { type: 'key', ch: '\x1b' }
|
||
}
|
||
const lone = q.shift()
|
||
if (lone !== undefined && lone < 0x20) {
|
||
return { type: 'ctrl', code: lone }
|
||
}
|
||
if (lone !== undefined) {
|
||
return { type: 'key', ch: String.fromCharCode(lone) }
|
||
}
|
||
return { type: 'eof' }
|
||
}
|
||
q.push(b)
|
||
continue
|
||
}
|
||
const ev = bareEditTryConsumeKey(q)
|
||
if (ev) return ev
|
||
const b = await reader.nextByte()
|
||
if (b === undefined) {
|
||
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
|
||
if (eofPaste !== undefined) {
|
||
return { type: 'paste', text: eofPaste }
|
||
}
|
||
if (!q.length) return { type: 'eof' }
|
||
if (q.length === 1 && q[0] === 27) {
|
||
q.length = 0
|
||
return { type: 'key', ch: '\x1b' }
|
||
}
|
||
const lone = q.shift()
|
||
if (lone !== undefined && lone < 0x20) {
|
||
return { type: 'ctrl', code: lone }
|
||
}
|
||
if (lone !== undefined) {
|
||
return { type: 'key', ch: String.fromCharCode(lone) }
|
||
}
|
||
return { type: 'eof' }
|
||
}
|
||
q.push(b)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {import('stream').Readable} stdin
|
||
*/
|
||
function bareEditCreateStdinReader(stdin) {
|
||
/** @type {number[]} */
|
||
const bytes = []
|
||
/** @type {(() => void)[]} */
|
||
const waiters = []
|
||
function drain() {
|
||
while (waiters.length && bytes.length) {
|
||
const w = waiters.shift()
|
||
if (w) w()
|
||
}
|
||
}
|
||
/** @param {unknown} chunk */
|
||
function onData(chunk) {
|
||
bytes.push(...bareEditChunkBytes(chunk))
|
||
drain()
|
||
}
|
||
stdin.on('data', onData)
|
||
return {
|
||
nextByte() {
|
||
if (bytes.length) return Promise.resolve(bytes.shift())
|
||
return new Promise((resolve) => {
|
||
waiters.push(() => resolve(bytes.shift()))
|
||
})
|
||
},
|
||
dispose() {
|
||
stdin.removeListener('data', onData)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Minimal AbortController for Bare/Pear when globalThis lacks it (drive-resident /bin preamble). */
|
||
|
||
function bareAgentEnsureAbortPolyfill() {
|
||
const g =
|
||
typeof globalThis !== 'undefined'
|
||
? globalThis
|
||
: typeof global !== 'undefined'
|
||
? global
|
||
: typeof self !== 'undefined'
|
||
? self
|
||
: /** @type {Record<string, unknown>} */ ({})
|
||
if (typeof g.AbortController === 'function') return
|
||
|
||
function BareAbortSignal() {
|
||
/** @type {boolean} */
|
||
this.aborted = false
|
||
/** @type {unknown} */
|
||
this.reason = undefined
|
||
/** @type {{ fn: () => void, once: boolean }[]} */
|
||
this._listeners = []
|
||
}
|
||
|
||
BareAbortSignal.prototype.addEventListener = function (type, fn, opts) {
|
||
if (type !== 'abort' || typeof fn !== 'function') return
|
||
const once = !!(opts && opts.once)
|
||
if (this.aborted) {
|
||
if (once) {
|
||
try {
|
||
fn.call(this)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return
|
||
}
|
||
this._listeners.push({ fn: /** @type {() => void} */ (fn), once })
|
||
}
|
||
|
||
BareAbortSignal.prototype.removeEventListener = function (type, fn) {
|
||
if (type !== 'abort' || typeof fn !== 'function') return
|
||
this._listeners = this._listeners.filter((x) => x.fn !== fn)
|
||
}
|
||
|
||
BareAbortSignal.prototype.throwIfAborted = function () {
|
||
if (!this.aborted) return
|
||
const DOMException = g.DOMException
|
||
if (typeof DOMException === 'function') {
|
||
throw new DOMException('Aborted', 'AbortError')
|
||
}
|
||
const e = new Error('Aborted')
|
||
e.name = 'AbortError'
|
||
throw e
|
||
}
|
||
|
||
function BareAbortController() {
|
||
this.signal = new BareAbortSignal()
|
||
}
|
||
|
||
BareAbortController.prototype.abort = function (reason) {
|
||
const s = this.signal
|
||
if (s.aborted) return
|
||
s.aborted = true
|
||
s.reason = reason
|
||
const list = s._listeners.slice()
|
||
s._listeners.length = 0
|
||
for (const x of list) {
|
||
try {
|
||
x.fn.call(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
if (typeof globalThis !== 'undefined') {
|
||
globalThis.AbortController = BareAbortController
|
||
globalThis.AbortSignal = BareAbortSignal
|
||
} else {
|
||
g.AbortController = BareAbortController
|
||
g.AbortSignal = BareAbortSignal
|
||
}
|
||
}
|
||
|
||
bareAgentEnsureAbortPolyfill()
|
||
|
||
/** TextEncoder/TextDecoder when missing on globalThis (Bare/Pear guest eval). */
|
||
|
||
/**
|
||
* @param {Uint8Array} bytes
|
||
*/
|
||
function bareAgentUtf8Decode(bytes) {
|
||
let out = ''
|
||
let i = 0
|
||
const len = bytes.length
|
||
while (i < len) {
|
||
const b0 = bytes[i++]
|
||
if (b0 < 0x80) {
|
||
out += String.fromCharCode(b0)
|
||
continue
|
||
}
|
||
if ((b0 & 0xe0) === 0xc0) {
|
||
if (i >= len || (bytes[i] & 0xc0) !== 0x80) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f)
|
||
if (cp < 0x80) out += '\ufffd'
|
||
else out += String.fromCharCode(cp)
|
||
continue
|
||
}
|
||
if ((b0 & 0xf0) === 0xe0) {
|
||
if (i + 1 >= len || (bytes[i] & 0xc0) !== 0x80 || (bytes[i + 1] & 0xc0) !== 0x80) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const b2 = bytes[i++]
|
||
let cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)
|
||
if (cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff)) out += '\ufffd'
|
||
else out += String.fromCharCode(cp)
|
||
continue
|
||
}
|
||
if ((b0 & 0xf8) === 0xf0) {
|
||
if (
|
||
i + 2 >= len ||
|
||
(bytes[i] & 0xc0) !== 0x80 ||
|
||
(bytes[i + 1] & 0xc0) !== 0x80 ||
|
||
(bytes[i + 2] & 0xc0) !== 0x80
|
||
) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const b2 = bytes[i++]
|
||
const b3 = bytes[i++]
|
||
let cp =
|
||
((b0 & 0x07) << 18) |
|
||
((b1 & 0x3f) << 12) |
|
||
((b2 & 0x3f) << 6) |
|
||
(b3 & 0x3f)
|
||
if (cp < 0x10000 || cp > 0x10ffff) out += '\ufffd'
|
||
else {
|
||
cp -= 0x10000
|
||
out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff))
|
||
}
|
||
continue
|
||
}
|
||
out += '\ufffd'
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} str
|
||
*/
|
||
function bareAgentUtf8Encode(str) {
|
||
const out = []
|
||
for (let i = 0; i < str.length; i++) {
|
||
let c = str.charCodeAt(i)
|
||
if (c < 0x80) {
|
||
out.push(c)
|
||
} else if (c < 0x800) {
|
||
out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
|
||
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
|
||
const c2 = str.charCodeAt(i + 1)
|
||
if ((c2 & 0xfc00) === 0xdc00) {
|
||
i++
|
||
const cp = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff)
|
||
out.push(
|
||
0xf0 | (cp >> 18),
|
||
0x80 | ((cp >> 12) & 0x3f),
|
||
0x80 | ((cp >> 6) & 0x3f),
|
||
0x80 | (cp & 0x3f)
|
||
)
|
||
} else {
|
||
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
|
||
}
|
||
} else if (c < 0xd800 || c >= 0xe000) {
|
||
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
|
||
}
|
||
}
|
||
return new Uint8Array(out)
|
||
}
|
||
|
||
function bareAgentEnsureTextCodecPolyfill() {
|
||
const g =
|
||
typeof globalThis !== 'undefined'
|
||
? globalThis
|
||
: typeof global !== 'undefined'
|
||
? global
|
||
: typeof self !== 'undefined'
|
||
? self
|
||
: /** @type {Record<string, unknown>} */ ({})
|
||
|
||
if (typeof g.TextDecoder !== 'function') {
|
||
function BareTextDecoder() {
|
||
/** @type {'utf-8'} */
|
||
this.encoding = 'utf-8'
|
||
}
|
||
BareTextDecoder.prototype.decode = function (input, options) {
|
||
let u8
|
||
if (input instanceof Uint8Array) u8 = input
|
||
else if (typeof ArrayBuffer !== 'undefined' && input instanceof ArrayBuffer)
|
||
u8 = new Uint8Array(input)
|
||
else if (input != null && typeof input === 'object' && 'length' in input) {
|
||
u8 = new Uint8Array(/** @type {ArrayLike<number>} */ (input))
|
||
} else if (input == null || input === undefined) {
|
||
return ''
|
||
} else {
|
||
return ''
|
||
}
|
||
const stream = !!(options && options.stream)
|
||
void stream
|
||
return bareAgentUtf8Decode(u8)
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextDecoder = BareTextDecoder
|
||
else g.TextDecoder = BareTextDecoder
|
||
}
|
||
|
||
if (typeof g.TextEncoder !== 'function') {
|
||
function BareTextEncoder() {
|
||
this.encoding = 'utf-8'
|
||
}
|
||
BareTextEncoder.prototype.encode = function (input) {
|
||
return bareAgentUtf8Encode(String(input == null ? '' : input))
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextEncoder = BareTextEncoder
|
||
else g.TextEncoder = BareTextEncoder
|
||
}
|
||
}
|
||
|
||
bareAgentEnsureTextCodecPolyfill()
|
||
|
||
/** Shared helpers for /bin/agent tools (preamble for agent bundle). */
|
||
|
||
/**
|
||
* Paths allowed for rename/delete via agent tools (stricter than general read paths).
|
||
* @param {string} absPath
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentPathAllowedMutate(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/') || p.includes('..')) return false
|
||
return (
|
||
p.startsWith('/home/') ||
|
||
p.startsWith('/tmp/') ||
|
||
p === '/tmp' ||
|
||
p.startsWith('/root/') ||
|
||
p.startsWith('/mnt/')
|
||
)
|
||
}
|
||
|
||
/** @type {readonly string[]} */
|
||
var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||
'/proc/bare_os/metrics_live.json',
|
||
'/proc/bare_os/features',
|
||
'/proc/bare_os/features.json',
|
||
'/proc/bare_os/swarm.json',
|
||
'/proc/bare_os/capabilities.json',
|
||
'/proc/bare_os/swarm_replication_status.json',
|
||
'/proc/bare_os/swarm_relay_status.json',
|
||
'/proc/bare_os/swarm_datagrams_status.json',
|
||
'/proc/bare_os/swarm_connection_manager_status.json',
|
||
'/proc/bare_os/swarm_key_broker_status.json',
|
||
'/proc/bare_os/swarm_holepunch_status.json',
|
||
'/proc/bare_os/swarm_datagram_replication_status.json',
|
||
'/proc/bare_os/swarm_status.json'
|
||
])
|
||
|
||
/**
|
||
* @param {unknown} statObj
|
||
* @param {string} path
|
||
*/
|
||
function bareAgentSerializeStat(statObj, path) {
|
||
if (!statObj || typeof statObj !== 'object')
|
||
return { path, error: 'no_stat' }
|
||
const s = /** @type {Record<string, unknown>} */ (statObj)
|
||
/** @type {'file' | 'dir' | 'symlink' | 'other'} */
|
||
let kind = 'other'
|
||
try {
|
||
if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir'
|
||
else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink'
|
||
else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file'
|
||
else if (typeof s.mode === 'number') {
|
||
const M = Number(s.mode)
|
||
if ((M & 0o170000) === 0o040000) kind = 'dir'
|
||
else if ((M & 0o170000) === 0o120000) kind = 'symlink'
|
||
else if ((M & 0o170000) === 0o100000) kind = 'file'
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const out = {
|
||
path,
|
||
kind,
|
||
size: typeof s.size === 'number' ? s.size : undefined,
|
||
mode: typeof s.mode === 'number' ? s.mode : undefined,
|
||
mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,
|
||
uid: typeof s.uid === 'number' ? s.uid : undefined,
|
||
gid: typeof s.gid === 'number' ? s.gid : undefined
|
||
}
|
||
if (typeof s.target === 'string') out.target = s.target
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareAgentTruncateChars(text, maxChars) {
|
||
const t = String(text || '')
|
||
const n = Math.floor(maxChars)
|
||
if (!Number.isFinite(n) || n <= 0) return ''
|
||
if (t.length <= n) return t
|
||
return t.slice(0, n) + '\n… truncated'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} page
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareAgentManExtractPageSlice(page, maxChars) {
|
||
if (!page || typeof page !== 'object')
|
||
return { error: 'bad_page' }
|
||
const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000)
|
||
const name = typeof page.name === 'string' ? page.name : ''
|
||
const section = typeof page.section === 'number' ? page.section : 0
|
||
const title = typeof page.title === 'string' ? page.title : ''
|
||
const synopsis = Array.isArray(page.synopsis)
|
||
? page.synopsis.map((x) => String(x)).join('\n')
|
||
: ''
|
||
const description = bareAgentTruncateChars(
|
||
typeof page.description === 'string' ? page.description : '',
|
||
Math.floor(m * 0.55)
|
||
)
|
||
let opts = ''
|
||
if (Array.isArray(page.options)) {
|
||
const lines = []
|
||
for (const o of page.options) {
|
||
if (!o || typeof o !== 'object') continue
|
||
const fl = typeof o.flag === 'string' ? o.flag : ''
|
||
const me = typeof o.meaning === 'string' ? o.meaning : ''
|
||
if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me)
|
||
}
|
||
opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35))
|
||
}
|
||
const blob =
|
||
name +
|
||
'(' +
|
||
section +
|
||
') — ' +
|
||
title +
|
||
'\n\nSYNOPSIS\n' +
|
||
synopsis +
|
||
'\n\nDESCRIPTION\n' +
|
||
description +
|
||
(opts ? '\n\nOPTIONS\n' + opts : '')
|
||
return {
|
||
name,
|
||
section,
|
||
title,
|
||
synopsis,
|
||
description,
|
||
options_text: opts || undefined,
|
||
text: bareAgentTruncateChars(blob, m)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Same semantics as `man -k`: substring match on indexed keywords (merged DB).
|
||
* @param {unknown} db
|
||
* @param {string} needle
|
||
* @param {number} maxHits
|
||
*/
|
||
function bareAgentManAproposHits(db, needle, maxHits) {
|
||
const n = String(needle || '').toLowerCase()
|
||
const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200)
|
||
if (!n || !db || typeof db !== 'object')
|
||
return /** @type {{ lines: string[], truncated: boolean }} */ ({
|
||
lines: [],
|
||
truncated: false
|
||
})
|
||
const d = /** @type {Record<string, unknown>} */ (db)
|
||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||
const apropos = Array.isArray(d.apropos) ? d.apropos : []
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
let truncated = false
|
||
for (const row of apropos) {
|
||
if (!row || typeof row !== 'object') continue
|
||
const kw = typeof row.kw === 'string' ? row.kw : ''
|
||
if (!kw.includes(n)) continue
|
||
const idx = row.pageRef
|
||
if (typeof idx !== 'number' || !pages[idx]) continue
|
||
if (seen.has(idx)) continue
|
||
seen.add(idx)
|
||
const p = /** @type {Record<string, unknown>} */ (pages[idx])
|
||
const name = typeof p.name === 'string' ? p.name : ''
|
||
const sec = typeof p.section === 'number' ? p.section : 0
|
||
const title = typeof p.title === 'string' ? p.title : ''
|
||
lines.push(name + '(' + sec + ') - ' + title)
|
||
if (lines.length >= max) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
lines.sort()
|
||
return { lines, truncated }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} vfs
|
||
* @param {{ db: unknown | null }} cacheRef
|
||
* @returns {Promise<unknown | null>}
|
||
*/
|
||
async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) {
|
||
if (cacheRef.db) return cacheRef.db
|
||
if (!vfs || typeof vfs.readFile !== 'function') return null
|
||
try {
|
||
const buf = await vfs.readFile('/share/man/man.json')
|
||
if (!buf || !buf.length) return null
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
const parsed = JSON.parse(t)
|
||
cacheRef.db = parsed
|
||
return parsed
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resolve one manual page from merged DB (like `man [[section] name]`).
|
||
* @param {unknown} db
|
||
* @param {string} topic
|
||
* @param {number | null} sectionExplicit
|
||
*/
|
||
function bareAgentManResolvePage(db, topic, sectionExplicit) {
|
||
const name = String(topic || '').toLowerCase()
|
||
if (!name || !db || typeof db !== 'object')
|
||
return { error: 'not_found' }
|
||
const d = /** @type {Record<string, unknown>} */ (db)
|
||
const index = d.index
|
||
if (!index || typeof index !== 'object') return { error: 'not_found' }
|
||
const idx = /** @type {Record<string, unknown>} */ (index)[name]
|
||
if (typeof idx !== 'number') return { error: 'not_found' }
|
||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||
const page = pages[idx]
|
||
if (!page || typeof page !== 'object') return { error: 'not_found' }
|
||
const sec = typeof page.section === 'number' ? page.section : 0
|
||
if (sectionExplicit !== null && sectionExplicit !== sec) {
|
||
return { error: 'wrong_section', foundSection: sec }
|
||
}
|
||
return { page: /** @type {Record<string, unknown>} */ (page) }
|
||
}
|
||
|
||
/**
|
||
* @param {string} absPath
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentProcReadPathAllowed(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/') || p.includes('..')) return false
|
||
for (let i = 0; i < BARE_AGENT_PROC_READ_ALLOWLIST.length; i++) {
|
||
if (p === BARE_AGENT_PROC_READ_ALLOWLIST[i]) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentResolveHome(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const vfsHome =
|
||
ctx &&
|
||
typeof ctx === 'object' &&
|
||
ctx.vfs &&
|
||
typeof ctx.vfs === 'object' &&
|
||
typeof /** @type {{ home?: string }} */ (ctx.vfs).home === 'string'
|
||
? String(/** @type {{ home?: string }} */ (ctx.vfs).home)
|
||
: ''
|
||
const h = env.HOME || vfsHome || '/home/guest'
|
||
return h.replace(/\/+$/, '') || '/home/guest'
|
||
}
|
||
|
||
/**
|
||
* @param {string} home
|
||
*/
|
||
function bareAgentPaths(home) {
|
||
const base = home + '/.agent'
|
||
return {
|
||
dir: base,
|
||
config: base + '/config.json',
|
||
history: base + '/history.json',
|
||
progress: base + '/progress.txt',
|
||
instructions: base + '/instructions.md',
|
||
context: base + '/context.md',
|
||
cmdOut: base + '/last_command_out.txt'
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/** HTTP fetch + HTML extract helpers for agent `web_fetch` tool (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {unknown} e
|
||
* @returns {string}
|
||
*/
|
||
function bareWebFmtErr(e) {
|
||
if (e === undefined)
|
||
return 'promise_rejected_with_undefined (no rejection reason)'
|
||
if (e === null) return 'promise_rejected_with_null'
|
||
if (typeof e === 'string') return e
|
||
if (typeof e !== 'object') return String(e)
|
||
const o = /** @type {Record<string, unknown>} */ (e)
|
||
const msg = o.message
|
||
if (typeof msg === 'string' && msg.trim()) return bareWebFmtErrAugment(o, msg.trim())
|
||
if (typeof msg === 'number' || typeof msg === 'boolean')
|
||
return bareWebFmtErrAugment(o, String(msg))
|
||
const nm = o.name
|
||
const code = o.code
|
||
const errno = o.errno
|
||
/** @type {string[]} */
|
||
const bits = []
|
||
if (typeof nm === 'string' && nm.trim()) bits.push(nm)
|
||
if (code !== undefined && code !== null && String(code) !== '')
|
||
bits.push('code=' + String(code))
|
||
if (errno !== undefined && errno !== null && String(errno) !== '')
|
||
bits.push('errno=' + String(errno))
|
||
const cause = o.cause
|
||
if (cause !== undefined && cause !== null && cause !== e) {
|
||
const cs = bareWebFmtErr(cause)
|
||
if (cs && cs !== 'unknown_error') bits.push('cause=(' + cs.slice(0, 280) + ')')
|
||
}
|
||
const errs = o.errors
|
||
if (Array.isArray(errs) && errs.length) {
|
||
errs.slice(0, 5).forEach((sub, i) => {
|
||
bits.push('agg' + i + '=' + bareWebFmtErr(sub).slice(0, 120))
|
||
})
|
||
}
|
||
if (bits.length) return bits.join(' ')
|
||
try {
|
||
const j = JSON.stringify(o)
|
||
if (j && j !== '{}' && j !== '[]') return j.slice(0, 400)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof (/** @type {{ toString?: () => string }} */ (o)).toString === 'function') {
|
||
const t = /** @type {{ toString: () => string }} */ (o).toString()
|
||
if (t && t !== '[object Object]') return t.slice(0, 400)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return 'unknown_error'
|
||
}
|
||
|
||
/**
|
||
* Append errno/syscall from Node-ish errors when message alone is vague.
|
||
* @param {Record<string, unknown>} o
|
||
* @param {string} base
|
||
*/
|
||
function bareWebFmtErrAugment(o, base) {
|
||
const syscall = o.syscall
|
||
const code = o.code
|
||
const errno = o.errno
|
||
/** @type {string[]} */
|
||
const tail = []
|
||
if (typeof syscall === 'string' && syscall.trim()) tail.push('syscall=' + syscall)
|
||
if (code !== undefined && code !== null && String(code) !== '') tail.push(String(code))
|
||
if (errno !== undefined && errno !== null && String(errno) !== '')
|
||
tail.push('errno=' + String(errno))
|
||
const cause = o.cause
|
||
if (cause !== undefined && cause !== null) {
|
||
const cs = bareWebFmtErr(cause)
|
||
if (cs && cs !== 'unknown_error') tail.push('cause=(' + cs.slice(0, 240) + ')')
|
||
}
|
||
return tail.length ? base + ' [' + tail.join(', ') + ']' : base
|
||
}
|
||
|
||
/**
|
||
* Tool args often omit numeric fields; `Number(undefined)` is NaN and `NaN ?? d` is still NaN.
|
||
* @param {unknown} n
|
||
* @param {number} def
|
||
*/
|
||
function bareWebFiniteOr(n, def) {
|
||
const x = Number(n)
|
||
return Number.isFinite(x) ? x : def
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {typeof fetch | null}
|
||
*/
|
||
function bareWebResolveFetch(ctx) {
|
||
if (typeof ctx.httpFetch === 'function')
|
||
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
|
||
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
|
||
let f =
|
||
bare && typeof bare.fetch === 'function'
|
||
? bare.fetch
|
||
: bare &&
|
||
bare.default &&
|
||
typeof bare.default === 'object' &&
|
||
typeof bare.default.fetch === 'function'
|
||
? bare.default.fetch
|
||
: null
|
||
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
|
||
if (typeof globalThis.fetch === 'function')
|
||
return globalThis.fetch.bind(globalThis)
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
|
||
* `controller.abort()` with no argument sets `reason === undefined`, so callers
|
||
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
|
||
* @param {number} timeoutMs
|
||
*/
|
||
function bareWebTimeoutAbortReason(timeoutMs) {
|
||
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
|
||
try {
|
||
if (typeof DOMException === 'function')
|
||
return new DOMException(msg, 'TimeoutError')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const e = new Error(msg)
|
||
e.name = 'TimeoutError'
|
||
return e
|
||
}
|
||
|
||
/**
|
||
* @param {AbortSignal} sig
|
||
*/
|
||
function bareWebSignalAbortReason(sig) {
|
||
try {
|
||
const r = /** @type {{ reason?: unknown }} */ (sig).reason
|
||
if (r !== undefined && r !== null) return r
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const e = new Error('web_fetch aborted (signal)')
|
||
e.name = 'AbortError'
|
||
return e
|
||
}
|
||
|
||
/**
|
||
* @param {AbortSignal | null | undefined} a
|
||
* @param {AbortSignal | null | undefined} b
|
||
*/
|
||
function bareWebUnionAbort(a, b) {
|
||
if (!a) return b || undefined
|
||
if (!b) return a
|
||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||
const c = new AbortController()
|
||
/**
|
||
* @param {AbortSignal} sig
|
||
*/
|
||
const forward = (sig) => {
|
||
try {
|
||
c.abort(bareWebSignalAbortReason(sig))
|
||
} catch {
|
||
/* ignore — second source may fire after controller already aborted */
|
||
}
|
||
}
|
||
try {
|
||
const as = /** @type {AbortSignal} */ (a)
|
||
const bs = /** @type {AbortSignal} */ (b)
|
||
if (as.aborted) forward(as)
|
||
else as.addEventListener('abort', () => forward(as), { once: true })
|
||
if (bs.aborted) forward(bs)
|
||
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return c.signal
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array[]} parts
|
||
*/
|
||
function bareWebConcatUint8(parts) {
|
||
let n = 0
|
||
for (const p of parts) n += p.length
|
||
const out = new Uint8Array(n)
|
||
let o = 0
|
||
for (const p of parts) {
|
||
out.set(p, o)
|
||
o += p.length
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Response} res
|
||
* @param {number} maxBytes
|
||
* @param {AbortSignal | undefined} signal
|
||
*/
|
||
async function bareWebReadBodyLimited(res, maxBytes, signal) {
|
||
if (!res.body || typeof res.body.getReader !== 'function') {
|
||
try {
|
||
const ab = await res.arrayBuffer()
|
||
const u8 = new Uint8Array(ab)
|
||
return {
|
||
bytes: u8.byteLength > maxBytes ? u8.slice(0, maxBytes) : u8,
|
||
truncated: u8.byteLength > maxBytes
|
||
}
|
||
} catch {
|
||
return { bytes: new Uint8Array(0), truncated: false }
|
||
}
|
||
}
|
||
const reader = res.body.getReader()
|
||
/** @type {Uint8Array[]} */
|
||
const chunks = []
|
||
let total = 0
|
||
try {
|
||
for (;;) {
|
||
if (signal && signal.aborted) {
|
||
try {
|
||
await reader.cancel()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
break
|
||
}
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
if (!value || !value.length) continue
|
||
total += value.length
|
||
if (total > maxBytes) {
|
||
const prev = total - value.length
|
||
const take = Math.max(0, maxBytes - prev)
|
||
if (take > 0) chunks.push(value.subarray(0, take))
|
||
try {
|
||
await reader.cancel()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return { bytes: bareWebConcatUint8(chunks), truncated: true }
|
||
}
|
||
chunks.push(value)
|
||
}
|
||
} finally {
|
||
try {
|
||
reader.releaseLock()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return { bytes: bareWebConcatUint8(chunks), truncated: false }
|
||
}
|
||
|
||
/**
|
||
* @param {string | null | undefined} ct
|
||
*/
|
||
function bareWebCharsetFromContentType(ct) {
|
||
const m = /charset\s*=\s*["']?([^"';\s]+)/i.exec(String(ct || ''))
|
||
return (m ? m[1] : 'utf-8').trim().toLowerCase()
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array} bytes
|
||
* @param {string} label
|
||
*/
|
||
function bareWebDecodeBytes(bytes, label) {
|
||
try {
|
||
const dec = new TextDecoder(label || 'utf-8', { fatal: false, ignoreBOM: true })
|
||
return dec.decode(bytes)
|
||
} catch {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareWebDecodeHtmlEntities(s) {
|
||
let t = String(s || '')
|
||
t = t.replace(/ /gi, ' ')
|
||
t = t.replace(/"/gi, '"')
|
||
t = t.replace(/'/g, "'")
|
||
t = t.replace(/'/gi, "'")
|
||
t = t.replace(/&/gi, '&')
|
||
t = t.replace(/</gi, '<')
|
||
t = t.replace(/>/gi, '>')
|
||
t = t.replace(/&#x([0-9a-f]+);/gi, (_, h) => {
|
||
const c = parseInt(h, 16)
|
||
return Number.isFinite(c) ? String.fromCodePoint(c) : _
|
||
})
|
||
t = t.replace(/&#(\d+);/g, (_, d) => {
|
||
const c = parseInt(d, 10)
|
||
return Number.isFinite(c) ? String.fromCodePoint(c) : _
|
||
})
|
||
return t
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
*/
|
||
function bareWebExtractHtmlText(html) {
|
||
let s = String(html || '')
|
||
s = s.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||
s = s.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
||
s = s.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '')
|
||
s = s.replace(/<!--[\s\S]*?-->/g, '')
|
||
s = s.replace(/<[^>]+>/g, ' ')
|
||
s = bareWebDecodeHtmlEntities(s)
|
||
s = s.replace(/\s+/g, ' ').trim()
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
* @param {string} baseUrl
|
||
* @param {number} maxLinks
|
||
*/
|
||
function bareWebExtractLinks(html, baseUrl, maxLinks) {
|
||
const cap = Math.min(Math.max(Number(maxLinks) || 200, 1), 500)
|
||
let uBase = null
|
||
try {
|
||
uBase = baseUrl ? new URL(String(baseUrl)) : null
|
||
} catch {
|
||
uBase = null
|
||
}
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const re =
|
||
/<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi
|
||
let m
|
||
const h = String(html || '')
|
||
while ((m = re.exec(h)) !== null) {
|
||
const href = (m[1] || m[2] || m[3] || '').trim()
|
||
if (!href || href.startsWith('javascript:') || href.startsWith('#')) continue
|
||
try {
|
||
const abs = uBase ? new URL(href, uBase).href : new URL(href).href
|
||
const proto = new URL(abs).protocol
|
||
if (proto !== 'http:' && proto !== 'https:') continue
|
||
if (!seen.has(abs)) {
|
||
seen.add(abs)
|
||
out.push(abs)
|
||
}
|
||
} catch {
|
||
/* skip */
|
||
}
|
||
if (out.length >= cap) break
|
||
}
|
||
return { links: out, links_truncated: out.length >= cap }
|
||
}
|
||
|
||
/**
|
||
* @param {string} tag
|
||
*/
|
||
function bareWebMetaContent(tag) {
|
||
const q =
|
||
/content\s*=\s*"([^"]*)"/i.exec(tag) ||
|
||
/content\s*=\s*'([^']*)'/i.exec(tag) ||
|
||
/content\s*=\s*([^\s>]+)/i.exec(tag)
|
||
return q ? bareWebDecodeHtmlEntities(q[1]).trim() : ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
*/
|
||
function bareWebExtractMeta(html) {
|
||
const h = String(html || '')
|
||
const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(h)
|
||
const title = titleM
|
||
? bareWebDecodeHtmlEntities(titleM[1].replace(/<[^>]+>/g, ' ')).trim()
|
||
: ''
|
||
let description = ''
|
||
const metaDescRe =
|
||
/<meta[^>]*\bname\s*=\s*["']description["'][^>]*>/i.exec(h)
|
||
if (metaDescRe) description = bareWebMetaContent(metaDescRe[0])
|
||
|
||
let og_title = ''
|
||
const ogT = /<meta[^>]*\bproperty\s*=\s*["']og:title["'][^>]*>/i.exec(h)
|
||
if (ogT) og_title = bareWebMetaContent(ogT[0])
|
||
|
||
let og_description = ''
|
||
const ogD =
|
||
/<meta[^>]*\bproperty\s*=\s*["']og:description["'][^>]*>/i.exec(h)
|
||
if (ogD) og_description = bareWebMetaContent(ogD[0])
|
||
|
||
return {
|
||
title,
|
||
description,
|
||
og_title,
|
||
og_description
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function bareWebMaybeParseJson(text) {
|
||
try {
|
||
return { ok: true, value: JSON.parse(String(text)) }
|
||
} catch {
|
||
return { ok: false }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} ct
|
||
*/
|
||
function bareWebLooksLikeHtml(ct) {
|
||
return /\btext\/html\b/i.test(String(ct || ''))
|
||
}
|
||
|
||
/**
|
||
* @param {string} ct
|
||
*/
|
||
function bareWebLooksLikeJson(ct) {
|
||
const s = String(ct || '').toLowerCase()
|
||
return (
|
||
/\bapplication\/json\b/.test(s) ||
|
||
/\bapplication\/.*\+json\b/.test(s) ||
|
||
/\btext\/json\b/.test(s)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* ctx: Record<string, unknown>,
|
||
* url: string,
|
||
* method?: string,
|
||
* headers?: Record<string, unknown>,
|
||
* body?: string,
|
||
* content_type?: string,
|
||
* max_response_bytes?: number,
|
||
* max_redirects?: number,
|
||
* timeout_ms?: number,
|
||
* format?: string,
|
||
* max_links?: number,
|
||
* signal?: AbortSignal | null
|
||
* }} o
|
||
*/
|
||
async function bareWebRunTool(o) {
|
||
const ctx = o.ctx
|
||
const fetchFn = bareWebResolveFetch(ctx)
|
||
if (!fetchFn) {
|
||
return {
|
||
ok: false,
|
||
error:
|
||
'web_fetch: no HTTP client (set ctx.httpFetch, bare.fetch, or global fetch)'
|
||
}
|
||
}
|
||
|
||
let startUrl = String(o.url || '').trim()
|
||
let method = String(o.method || 'GET').toUpperCase()
|
||
if (
|
||
!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(
|
||
method
|
||
)
|
||
) {
|
||
return { ok: false, error: 'web_fetch: unsupported method' }
|
||
}
|
||
|
||
let body =
|
||
o.body != null && method !== 'GET' && method !== 'HEAD'
|
||
? String(o.body)
|
||
: undefined
|
||
|
||
let u0
|
||
try {
|
||
u0 = new URL(startUrl)
|
||
} catch {
|
||
return { ok: false, error: 'web_fetch: invalid URL' }
|
||
}
|
||
if (u0.protocol !== 'http:' && u0.protocol !== 'https:') {
|
||
return { ok: false, error: 'web_fetch: only http(s) URLs are allowed' }
|
||
}
|
||
|
||
const maxRedirects = Math.min(
|
||
Math.max(bareWebFiniteOr(o.max_redirects, 5), 0),
|
||
20
|
||
)
|
||
const maxBytes = Math.min(
|
||
Math.max(bareWebFiniteOr(o.max_response_bytes, 524288), 1024),
|
||
2 * 1024 * 1024
|
||
)
|
||
const timeoutMs = Math.min(
|
||
Math.max(bareWebFiniteOr(o.timeout_ms, 30000), 500),
|
||
120000
|
||
)
|
||
const fmtRaw = String(o.format || 'auto').toLowerCase()
|
||
const maxLinks = Number(o.max_links) || 200
|
||
|
||
/** @type {string[]} */
|
||
const redirectChain = [startUrl]
|
||
let currentUrl = startUrl
|
||
let redirectsUsed = 0
|
||
|
||
for (;;) {
|
||
const controller = new AbortController()
|
||
const timer = setTimeout(() => {
|
||
try {
|
||
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}, timeoutMs)
|
||
const signal = bareWebUnionAbort(o.signal || undefined, controller.signal)
|
||
|
||
/** @type {Record<string, string>} */
|
||
const hdrObj = {}
|
||
const hin = o.headers
|
||
if (hin && typeof hin === 'object' && !Array.isArray(hin)) {
|
||
for (const [k, v] of Object.entries(hin)) {
|
||
if (typeof v === 'string' && k) hdrObj[k] = v
|
||
}
|
||
}
|
||
if (
|
||
body != null &&
|
||
method !== 'GET' &&
|
||
method !== 'HEAD' &&
|
||
!Object.keys(hdrObj).some((k) => k.toLowerCase() === 'content-type')
|
||
) {
|
||
hdrObj['Content-Type'] =
|
||
typeof o.content_type === 'string' && o.content_type.trim()
|
||
? o.content_type.trim()
|
||
: 'application/octet-stream'
|
||
}
|
||
|
||
/** @type {RequestInit} */
|
||
const init = {
|
||
method,
|
||
headers: hdrObj,
|
||
signal: signal || undefined,
|
||
redirect: 'manual'
|
||
}
|
||
if (body != null && method !== 'GET' && method !== 'HEAD') {
|
||
init.body = body
|
||
}
|
||
|
||
let res
|
||
try {
|
||
res = await fetchFn(currentUrl, init)
|
||
} catch (e) {
|
||
clearTimeout(timer)
|
||
const msg = bareWebFmtErr(
|
||
e === undefined
|
||
? new Error(
|
||
'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)'
|
||
)
|
||
: e
|
||
)
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
clearTimeout(timer)
|
||
|
||
const st = res.status
|
||
if (st >= 300 && st < 400) {
|
||
if (redirectsUsed >= maxRedirects) {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: too many redirects',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
const loc = res.headers.get('Location')
|
||
if (!loc) {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: redirect without Location',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
let nextUrl
|
||
try {
|
||
nextUrl = new URL(loc, currentUrl).href
|
||
} catch {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: bad redirect URL',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
redirectChain.push(nextUrl)
|
||
currentUrl = nextUrl
|
||
redirectsUsed++
|
||
if (st === 301 || st === 302 || st === 303) {
|
||
method = 'GET'
|
||
body = undefined
|
||
}
|
||
continue
|
||
}
|
||
|
||
const ct = res.headers.get('content-type') || ''
|
||
const url_final = currentUrl
|
||
|
||
if (method === 'HEAD') {
|
||
return {
|
||
ok: true,
|
||
url_final,
|
||
status: st,
|
||
content_type: ct,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined,
|
||
truncated: false,
|
||
extract: { note: 'HEAD — body omitted' }
|
||
}
|
||
}
|
||
|
||
let bodyRead
|
||
try {
|
||
bodyRead = await bareWebReadBodyLimited(res, maxBytes, signal || undefined)
|
||
} catch (e) {
|
||
const msg = bareWebFmtErr(e)
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: read body failed: ' + msg.slice(0, 400),
|
||
url_final,
|
||
status: st,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined
|
||
}
|
||
}
|
||
|
||
const charset = bareWebCharsetFromContentType(ct)
|
||
const text = bareWebDecodeBytes(bodyRead.bytes, charset)
|
||
|
||
const fmt =
|
||
fmtRaw === 'auto'
|
||
? bareWebLooksLikeJson(ct)
|
||
? 'json'
|
||
: bareWebLooksLikeHtml(ct)
|
||
? 'markdownish'
|
||
: 'raw'
|
||
: fmtRaw
|
||
|
||
/** @type {unknown} */
|
||
let extract
|
||
|
||
if (fmt === 'json') {
|
||
const p = bareWebMaybeParseJson(text)
|
||
extract = p.ok ? { json: p.value } : { parse_error: true, text_slice: text.slice(0, 8000) }
|
||
} else if (fmt === 'links') {
|
||
extract = bareWebExtractLinks(text, url_final, maxLinks)
|
||
} else if (fmt === 'meta') {
|
||
extract = bareWebExtractMeta(text)
|
||
} else if (fmt === 'markdownish' || fmt === 'text') {
|
||
const plain = bareWebExtractHtmlText(text)
|
||
extract = {
|
||
text: plain,
|
||
approx_chars: plain.length
|
||
}
|
||
} else if (fmt === 'raw') {
|
||
extract = {
|
||
raw_text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text,
|
||
char_count: text.length
|
||
}
|
||
} else {
|
||
extract = {
|
||
text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text,
|
||
char_count: text.length
|
||
}
|
||
}
|
||
|
||
const raw_preview = text.slice(0, 2000)
|
||
|
||
return {
|
||
ok: true,
|
||
url_final,
|
||
status: st,
|
||
content_type: ct,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined,
|
||
truncated: bodyRead.truncated,
|
||
extract,
|
||
raw_preview: fmt === 'raw' || fmt === 'json' ? undefined : raw_preview
|
||
}
|
||
}
|
||
}
|
||
|
||
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentShellQuote(s) {
|
||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentJsonResult(v) {
|
||
try {
|
||
return JSON.stringify(v)
|
||
} catch {
|
||
return '{"error":"json_stringify_failed"}'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentToolDefinitions() {
|
||
return [
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_file',
|
||
description:
|
||
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute file path' },
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes to read (default 256000)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'write_file',
|
||
description:
|
||
'Create or overwrite a file. Parent directories are created as needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: { type: 'string', description: 'Full file contents' }
|
||
},
|
||
required: ['path', 'content']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_file',
|
||
description:
|
||
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
content: {
|
||
type: 'string',
|
||
description: 'If set (non-empty), full file replacement'
|
||
},
|
||
old_string: {
|
||
type: 'string',
|
||
description: 'Search string (used with new_string)'
|
||
},
|
||
new_string: { type: 'string', description: 'Replacement text' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_directory',
|
||
description: 'Create a directory (recursive).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_files',
|
||
description:
|
||
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string' },
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to search (default /home)'
|
||
},
|
||
max_lines: { type: 'integer', description: 'Default 200' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_command',
|
||
description:
|
||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<code> line.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: {
|
||
type: 'string',
|
||
description: 'Full command string (e.g. ls -la /bin)'
|
||
},
|
||
timeout_ms: { type: 'integer' },
|
||
capture_exit: {
|
||
type: 'boolean',
|
||
description: 'If true, append last line EXIT:<code> to capture (default false)'
|
||
}
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script',
|
||
description:
|
||
'REQUIRED to run agent-authored JavaScript: Node is not installed. Writes code to ~/.agent/_tmp_agent_run.mjs and runs it by absolute path (Bare kernel — same as /bin scripts). Do not use run_command with node/npm/npx. Prefer async function run(ctx, argv). stdout/stderr captured.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
code: { type: 'string', description: 'Full ESM/CommonJS script body' }
|
||
},
|
||
required: ['code']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_system_info',
|
||
description:
|
||
'Lightweight context: API version, uname, optional resource hook. Questions about **which kernel features are on/off in this session** → read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json; not apropos_man. For other /proc JSON use read_proc_file; swarm → get_swarm_peers; resource table → get_resource_limits. want=capabilities|swarm still returns those blobs when needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
want: {
|
||
type: 'string',
|
||
enum: ['summary', 'capabilities', 'swarm'],
|
||
description: 'Optional focus (default summary)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_agent_config',
|
||
description:
|
||
'Merge keys into ~/.agent/config.json (shallow merge for known keys only).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
patch: {
|
||
type: 'object',
|
||
description:
|
||
'Partial config object (rest_base_url, model, temperature, …)'
|
||
}
|
||
},
|
||
required: ['patch']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_bin',
|
||
description: 'List Tier-1 utilities in /bin via VFS.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
limit: { type: 'integer', description: 'Max names (default 400)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_directory',
|
||
description:
|
||
'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute directory path' },
|
||
max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' },
|
||
include_stat: {
|
||
type: 'boolean',
|
||
description: 'If true, call stat on each entry (slower; default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'file_stat',
|
||
description:
|
||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
follow_symlinks: {
|
||
type: 'boolean',
|
||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'move_path',
|
||
description:
|
||
'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'delete_path',
|
||
description:
|
||
'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
recursive: {
|
||
type: 'boolean',
|
||
description: 'Remove directories recursively (default false)'
|
||
},
|
||
confirm_token: {
|
||
type: 'string',
|
||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_man_page',
|
||
description:
|
||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||
section: {
|
||
type: 'integer',
|
||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||
},
|
||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||
},
|
||
required: ['topic']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apropos_man',
|
||
description:
|
||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines. This is documentation search only—never use it to answer what kernel features are currently enabled or disabled (use read_proc_file on /proc/bare_os/features).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
keyword: { type: 'string' },
|
||
max_results: { type: 'integer', description: 'Default 40, max 200' }
|
||
},
|
||
required: ['keyword']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_proc_file',
|
||
description:
|
||
'Read a small allowlisted /proc/bare_os pseudo file (bounded). Canonical live kernel feature state: /proc/bare_os/features or /proc/bare_os/features.json (same content). Use instead of shelling cat.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: {
|
||
type: 'string',
|
||
description:
|
||
'Exact allowlisted path (metrics_live.json, features or features.json, capabilities.json, swarm*.json, swarm_*_status.json); bounded read.'
|
||
},
|
||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_swarm_peers',
|
||
description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_resource_limits',
|
||
description:
|
||
'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script_at_path',
|
||
description:
|
||
'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute path to .mjs file' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'web_fetch',
|
||
description:
|
||
'Fetch live HTTP(S) URLs and return structured content for the assistant. Uses ctx.httpFetch (same policy as wget/curl: BARE_OS_HTTP_ALLOWLIST / DENYLIST). For official docs index use read_man_page / apropos_man — they are not web pages. Supports GET/HEAD/POST and extract modes: auto (JSON vs HTML vs text), markdownish plain text from HTML, links (anchor hrefs), meta (title/og:), raw UTF-8 slice, or json parse.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
url: { type: 'string', description: 'Absolute http(s) URL' },
|
||
method: {
|
||
type: 'string',
|
||
description: 'HTTP method (default GET)',
|
||
enum: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
||
},
|
||
headers: {
|
||
type: 'object',
|
||
description: 'Optional header map (string values only)'
|
||
},
|
||
body: {
|
||
type: 'string',
|
||
description: 'Request body for non-GET (e.g. JSON string for APIs)'
|
||
},
|
||
content_type: {
|
||
type: 'string',
|
||
description: 'Content-Type when body is set (default application/octet-stream)'
|
||
},
|
||
format: {
|
||
type: 'string',
|
||
enum: ['auto', 'json', 'markdownish', 'text', 'links', 'meta', 'raw'],
|
||
description:
|
||
'auto: sniff Content-Type; json: parse JSON; markdownish/text: strip HTML to readable text; links: absolute http(s) links; meta: title/description/og tags; raw: bounded UTF-8 text'
|
||
},
|
||
max_response_bytes: {
|
||
type: 'integer',
|
||
description: 'Cap downloaded bytes (default 524288, max 2MiB)'
|
||
},
|
||
max_redirects: {
|
||
type: 'integer',
|
||
description: 'Max redirects to follow (default 5)'
|
||
},
|
||
timeout_ms: {
|
||
type: 'integer',
|
||
description: 'Per-request timeout ms (default 30000, max 120000)'
|
||
},
|
||
max_links: {
|
||
type: 'integer',
|
||
description: 'Max links when format=links (default 200)'
|
||
}
|
||
},
|
||
required: ['url']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: '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 =
|
||
'For live kernel feature flags use read_proc_file on /proc/bare_os/features (or features.json); apropos_man only searches man-page text, not runtime state. Otherwise prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.'
|
||
}
|
||
if (want === 'capabilities' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/capabilities.json')
|
||
if (b && b.length) {
|
||
info.capabilities_json =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (want === 'swarm' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (b && b.length) {
|
||
info.swarm =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
ctx.b4a.toString
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1')
|
||
if (vfs?.readFile) {
|
||
const buf = await vfs.readFile(paths.cmdOut)
|
||
if (buf && buf.length) {
|
||
info.uname =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf).trim()
|
||
: String(new TextDecoder().decode(buf)).trim()
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
appendProgress('get_system_info ' + want)
|
||
return bareAgentJsonResult({ ok: true, want, info })
|
||
}
|
||
|
||
if (toolName === 'edit_agent_config') {
|
||
const patch = args.patch
|
||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
||
configRef.current = merged
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
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 }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'web_fetch') {
|
||
const url = typeof args.url === 'string' ? args.url : ''
|
||
let hostHint = ''
|
||
try {
|
||
hostHint = new URL(url).hostname
|
||
} catch {
|
||
hostHint = ''
|
||
}
|
||
appendProgress('web_fetch ' + (hostHint || url.slice(0, 80)))
|
||
try {
|
||
const out = await bareWebRunTool({
|
||
ctx,
|
||
url,
|
||
method: typeof args.method === 'string' ? args.method : undefined,
|
||
headers:
|
||
args.headers &&
|
||
typeof args.headers === 'object' &&
|
||
!Array.isArray(args.headers)
|
||
? /** @type {Record<string, unknown>} */ (args.headers)
|
||
: undefined,
|
||
body: typeof args.body === 'string' ? args.body : undefined,
|
||
content_type:
|
||
typeof args.content_type === 'string' ? args.content_type : undefined,
|
||
max_response_bytes:
|
||
typeof args.max_response_bytes === 'number'
|
||
? args.max_response_bytes
|
||
: undefined,
|
||
max_redirects:
|
||
typeof args.max_redirects === 'number' ? args.max_redirects : undefined,
|
||
timeout_ms:
|
||
typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
||
format: typeof args.format === 'string' ? args.format : undefined,
|
||
max_links:
|
||
typeof args.max_links === 'number' ? args.max_links : undefined,
|
||
signal
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
} catch (e) {
|
||
const msg = bareWebFmtErr(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} base
|
||
* @param {Record<string, unknown>} patch
|
||
*/
|
||
function bareAgentMergeConfigPatch(base, patch) {
|
||
const out = { ...base }
|
||
const keys = [
|
||
'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\`.
|
||
|
||
Bare OS Created by: Raven Scott (https://raven-scott.fyi)
|
||
|
||
Your Repo: https://git.ssh.surf/snxraven/bare-operating-system
|
||
|
||
Your booter address: pear://xc5xw4odfpd7txd1b3xr4u37rfpbjoqtb6zp3xxutgbbmrbtogio
|
||
|
||
Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/readdir/chmod. Paths under /home (personal Hyperdrive), /mnt, /tmp are writable where policy allows; /bin, /etc are system drive.
|
||
|
||
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
|
||
|
||
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits, web_fetch (live http(s) pages and APIs; same host allowlist as wget); use list_directory instead of \`ls\` in run_command when only listing.
|
||
|
||
Kernel features / capabilities that are **actually enabled or disabled** in this runtime come from **read_proc_file** on \`/proc/bare_os/features\` (same payload as \`/proc/bare_os/features.json\`) and \`/proc/bare_os/capabilities.json\`. **Do not** infer current kernel state from \`apropos_man\` or man pages—that only searches documentation keywords.
|
||
|
||
Prefer tools over guessing for filesystem and shell facts.
|
||
|
||
Reply format (mandatory): Every message you stream to the user must be plain text only — readable in a terminal without a Markdown renderer. Do not use Markdown or similar markup: no emphasis/backtick/code-fence/link syntax, no # headings, no list markers used as markup. Structure with blank lines, short paragraphs, indentation, ALL CAPS or dashed lines for section breaks if needed, and bare URLs when linking. Paths and commands appear as normal text.`
|
||
|
||
/**
|
||
* 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, chunk)
|
||
} else if (e.type === 'delta_tool_calls') {
|
||
const arr = e.tool_calls
|
||
if (Array.isArray(arr)) {
|
||
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||
}
|
||
} else if (e.type === '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
|
||
})
|
||
}
|