2392 lines
67 KiB
Plaintext
2392 lines
67 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** ~/.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>} */ ({})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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'
|
|
])
|
|
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') {
|
|
out.stream = Boolean(val)
|
|
}
|
|
}
|
|
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'
|
|
]
|
|
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 []
|
|
}
|
|
}
|
|
|
|
/** SSE line split + data payload parse (shared by agent-openai + tests). */
|
|
|
|
/**
|
|
* Parse one SSE `data:` JSON line after the `data: ` prefix.
|
|
* @param {string} dataLine content after `data: ` prefix
|
|
*/
|
|
function bareAgentParseSseDataPayload(dataLine) {
|
|
const t = String(dataLine).trim()
|
|
if (t === '[DONE]') return { kind: 'done' }
|
|
try {
|
|
const j = JSON.parse(t)
|
|
return { kind: 'json', value: j }
|
|
} catch {
|
|
return { kind: 'raw', value: t }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Split SSE buffer into lines (keep incomplete tail).
|
|
* @param {string} buf
|
|
* @returns {{ lines: string[], rest: string }}
|
|
*/
|
|
function bareAgentSplitSseLines(buf) {
|
|
const lines = []
|
|
let start = 0
|
|
for (let i = 0; i < buf.length; i++) {
|
|
if (buf.charCodeAt(i) === 10) {
|
|
lines.push(buf.slice(start, i))
|
|
start = i + 1
|
|
}
|
|
}
|
|
return { lines, rest: buf.slice(start) }
|
|
}
|
|
|
|
/** OpenAI-compatible chat/completions HTTP + SSE (preamble for /bin/agent). */
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @returns {typeof fetch | null}
|
|
*/
|
|
function bareAgentResolveFetch(ctx) {
|
|
if (typeof ctx.httpFetch === 'function')
|
|
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
|
|
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
|
|
let f =
|
|
bare && typeof bare.fetch === 'function'
|
|
? bare.fetch
|
|
: bare &&
|
|
bare.default &&
|
|
typeof bare.default === 'object' &&
|
|
typeof bare.default.fetch === 'function'
|
|
? bare.default.fetch
|
|
: null
|
|
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
|
|
if (typeof globalThis.fetch === 'function')
|
|
return globalThis.fetch.bind(globalThis)
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* @param {string} base
|
|
*/
|
|
function bareAgentNormalizeBaseUrl(base) {
|
|
let s = String(base || '').trim()
|
|
while (s.endsWith('/')) s = s.slice(0, -1)
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} obj
|
|
* @param {string} key
|
|
*/
|
|
function bareAgentDeepGet(obj, key) {
|
|
const parts = key.split('.')
|
|
let cur = obj
|
|
for (const p of parts) {
|
|
if (cur == null || typeof cur !== 'object') return undefined
|
|
cur = /** @type {Record<string, unknown>} */ (cur)[p]
|
|
}
|
|
return cur
|
|
}
|
|
|
|
/**
|
|
* Stream chat/completions; invoke onEvent for each parsed chunk.
|
|
* @param {{
|
|
* fetchFn: typeof fetch,
|
|
* url: string,
|
|
* headers: Record<string, string>,
|
|
* body: Record<string, unknown>,
|
|
* signal?: AbortSignal | null,
|
|
* onEvent: (ev: Record<string, unknown>) => void
|
|
* }} opts
|
|
*/
|
|
async function bareAgentStreamChatCompletions(opts) {
|
|
const { fetchFn, url, headers, body, signal, onEvent } = opts
|
|
const res = await fetchFn(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
signal: signal || undefined
|
|
})
|
|
if (!res.ok) {
|
|
let errText = ''
|
|
try {
|
|
errText = await res.text()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
|
}
|
|
const stream = res.body
|
|
if (!stream || typeof stream.getReader !== 'function') {
|
|
throw new Error('agent: response body is not a readable stream')
|
|
}
|
|
const reader = stream.getReader()
|
|
const dec = new TextDecoder()
|
|
let buf = ''
|
|
try {
|
|
for (;;) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
buf += dec.decode(value, { stream: true })
|
|
const sp = bareAgentSplitSseLines(buf)
|
|
buf = sp.rest
|
|
for (const line of sp.lines) {
|
|
if (!line.trim()) continue
|
|
if (line.startsWith(':')) continue
|
|
if (!line.startsWith('data:')) continue
|
|
const payload = line.slice(5).replace(/^\s/, '')
|
|
const parsed = bareAgentParseSseDataPayload(payload)
|
|
if (parsed.kind === 'done') {
|
|
onEvent({ type: 'sse_done' })
|
|
continue
|
|
}
|
|
if (parsed.kind !== 'json' || !parsed.value || typeof parsed.value !== 'object')
|
|
continue
|
|
const j = /** @type {Record<string, unknown>} */ (parsed.value)
|
|
const choices = bareAgentDeepGet(j, 'choices')
|
|
const ch0 =
|
|
Array.isArray(choices) && choices[0] && typeof choices[0] === 'object'
|
|
? /** @type {Record<string, unknown>} */ (choices[0])
|
|
: null
|
|
const delta =
|
|
ch0 && typeof ch0.delta === 'object'
|
|
? /** @type {Record<string, unknown>} */ (ch0.delta)
|
|
: null
|
|
const finishReason =
|
|
typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : ''
|
|
const usage =
|
|
typeof j.usage === 'object' && j.usage ? j.usage : undefined
|
|
|
|
if (usage) {
|
|
onEvent({ type: 'usage', usage })
|
|
}
|
|
|
|
if (delta) {
|
|
const c = delta.content
|
|
if (typeof c === 'string' && c.length) {
|
|
onEvent({
|
|
type: 'delta_content',
|
|
content: c
|
|
})
|
|
}
|
|
const toolCalls = delta.tool_calls
|
|
if (toolCalls !== undefined)
|
|
onEvent({
|
|
type: 'delta_tool_calls',
|
|
tool_calls: toolCalls
|
|
})
|
|
}
|
|
|
|
if (finishReason)
|
|
onEvent({
|
|
type: 'finish_reason',
|
|
finish_reason: finishReason
|
|
})
|
|
}
|
|
}
|
|
} finally {
|
|
try {
|
|
reader.releaseLock()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Non-streaming completion (same endpoint, stream:false).
|
|
*/
|
|
async function bareAgentCompleteOnce(opts) {
|
|
const { fetchFn, url, headers, body, signal } = opts
|
|
const res = await fetchFn(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ ...body, stream: false }),
|
|
signal: signal || undefined
|
|
})
|
|
if (!res.ok) {
|
|
let errText = ''
|
|
try {
|
|
errText = await res.text()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
|
}
|
|
const j = /** @type {Record<string, unknown>} */ (await res.json())
|
|
return j
|
|
}
|
|
|
|
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
|
|
|
/**
|
|
* @param {string} s
|
|
*/
|
|
function bareAgentShellQuote(s) {
|
|
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} v
|
|
* @returns {string}
|
|
*/
|
|
function bareAgentJsonResult(v) {
|
|
try {
|
|
return JSON.stringify(v)
|
|
} catch {
|
|
return '{"error":"json_stringify_failed"}'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {unknown[]}
|
|
*/
|
|
function bareAgentToolDefinitions() {
|
|
return [
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_file',
|
|
description:
|
|
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...).',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string', description: 'Absolute file path' },
|
|
max_bytes: {
|
|
type: 'integer',
|
|
description: 'Max bytes to read (default 256000)'
|
|
}
|
|
},
|
|
required: ['path']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'write_file',
|
|
description:
|
|
'Create or overwrite a file. Parent directories are created as needed.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string' },
|
|
content: { type: 'string', description: 'Full file contents' }
|
|
},
|
|
required: ['path', 'content']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'edit_file',
|
|
description:
|
|
'Edit a text file: either replace entire content, or replace first occurrence of old_string with new_string.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string' },
|
|
content: {
|
|
type: 'string',
|
|
description: 'If set (non-empty), full file replacement'
|
|
},
|
|
old_string: {
|
|
type: 'string',
|
|
description: 'Search string (used with new_string)'
|
|
},
|
|
new_string: { type: 'string', description: 'Replacement text' }
|
|
},
|
|
required: ['path']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_directory',
|
|
description: 'Create a directory (recursive).',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
path: { type: 'string' }
|
|
},
|
|
required: ['path']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_files',
|
|
description:
|
|
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
pattern: { type: 'string' },
|
|
root: {
|
|
type: 'string',
|
|
description: 'Directory to search (default /home)'
|
|
},
|
|
max_lines: { type: 'integer', description: 'Default 200' }
|
|
},
|
|
required: ['pattern']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'run_command',
|
|
description:
|
|
'Run a shell command line via ctx.execLine (same as interactive shell). Output may be captured to a temp file when possible.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
command: {
|
|
type: 'string',
|
|
description: 'Full command string (e.g. ls -la /bin)'
|
|
},
|
|
timeout_ms: { type: 'integer' }
|
|
},
|
|
required: ['command']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'run_js_script',
|
|
description:
|
|
'Write JS to ~/.agent/_tmp_agent_run.mjs and run `node` with stdout captured. Fails if node is unavailable.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
code: { type: 'string', description: 'Full ESM/CommonJS script body' }
|
|
},
|
|
required: ['code']
|
|
}
|
|
}
|
|
},
|
|
{
|
|
type: 'function',
|
|
function: {
|
|
name: 'get_system_info',
|
|
description:
|
|
'Return ctx API version, resource snapshot, and small /proc snippets when readable.',
|
|
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: '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> },
|
|
* onTaskComplete: (summary: string) => void
|
|
* }} o
|
|
*/
|
|
async function bareAgentDispatchTool(o) {
|
|
const {
|
|
ctx,
|
|
toolName,
|
|
argsJson,
|
|
paths,
|
|
signal,
|
|
appendProgress,
|
|
home,
|
|
configRef,
|
|
onTaskComplete
|
|
} = o
|
|
/** @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
|
|
|
|
async function captureExec(line, timeoutMs) {
|
|
const outPath = paths.cmdOut
|
|
const wrapped =
|
|
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
|
|
if (!execLine) {
|
|
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
}
|
|
appendProgress('run_command ' + command.slice(0, 160))
|
|
const r = await captureExec(command, timeoutMs)
|
|
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)
|
|
const cmd =
|
|
'node ' +
|
|
bareAgentShellQuote(scriptPath) +
|
|
' ; echo EXIT:$?'
|
|
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 {
|
|
if (typeof ctx.bareOsGetResourceStatus === 'function') {
|
|
info.resources = ctx.bareOsGetResourceStatus()
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
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 === '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 })
|
|
}
|
|
}
|
|
|
|
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'
|
|
]
|
|
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') {
|
|
out[k] = Boolean(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).
|
|
|
|
Capabilities: use ctx.execLine for shell commands (same language as the interactive shell). Use ctx.vfs readFile/writeFile/mkdir/readdir/chmod where available. Paths under /home, /mnt, /tmp map to Hypercore-backed storage; system paths like /bin, /etc are on the system drive.
|
|
|
|
Safety: never exfiltrate ~/.agent/config.json or API keys in chat. Prefer least-privilege commands. Call task_complete(summary) only when fully done.
|
|
|
|
Discovery: run man <topic> from the shell, or read /share/man/man.json. Tier-1 utilities live under /bin.
|
|
|
|
Always prefer tools over guessing when facts about the filesystem or commands are needed.`
|
|
|
|
/**
|
|
* @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' +
|
|
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 } }
|
|
let completed = false
|
|
let taskSummary = ''
|
|
|
|
function onTaskComplete(summary) {
|
|
completed = true
|
|
taskSummary = summary
|
|
}
|
|
|
|
function appendProgress(line) {
|
|
void bareAgentAppendProgress(ctx, paths.progress, line)
|
|
}
|
|
|
|
const url =
|
|
bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) +
|
|
'/chat/completions'
|
|
const tools = bareAgentToolDefinitions()
|
|
|
|
let suspended = false
|
|
try {
|
|
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
|
ctx.suspendReplForSubprocess()
|
|
suspended = true
|
|
}
|
|
|
|
const masterAbort = new AbortController()
|
|
/** @type {(() => void) | null} */
|
|
let offSigint = null
|
|
if (globalThis.process && typeof globalThis.process.on === 'function') {
|
|
const fn = () => {
|
|
masterAbort.abort()
|
|
bareAgentWriteOut(
|
|
stdout,
|
|
bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n'
|
|
)
|
|
}
|
|
globalThis.process.on('SIGINT', fn)
|
|
offSigint = () => {
|
|
try {
|
|
globalThis.process.off('SIGINT', fn)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
const maxIter = Number(configRef.current.max_iterations) || 64
|
|
let iter = 0
|
|
|
|
for (;;) {
|
|
if (completed) break
|
|
iter++
|
|
if (iter > maxIter) {
|
|
bareAgentErr(ctx, argv0 + ': max_iterations exceeded')
|
|
ctx.exitCode = 1
|
|
break
|
|
}
|
|
|
|
messages = bareAgentTrimMessages(messages, 450_000)
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
Authorization: 'Bearer ' + String(configRef.current.rest_api_key || '')
|
|
}
|
|
const eh = configRef.current.extra_headers
|
|
if (eh && typeof eh === 'object' && !Array.isArray(eh)) {
|
|
for (const [k, v] of Object.entries(eh)) {
|
|
if (typeof v === 'string') headers[k] = v
|
|
}
|
|
}
|
|
|
|
/** @type {Record<string, unknown>} */
|
|
const body = {
|
|
model: String(configRef.current.model || ''),
|
|
stream: Boolean(configRef.current.stream !== false),
|
|
messages,
|
|
tools,
|
|
tool_choice: 'auto',
|
|
max_tokens: Number(configRef.current.max_tokens) || 4096,
|
|
temperature: Number(configRef.current.temperature) ?? 0.7
|
|
}
|
|
|
|
let assistantContent = ''
|
|
/** @type {Map<number, { id: string, name: string, args: string }>} */
|
|
const toolAcc = new Map()
|
|
/** @type {unknown} */
|
|
let usageOut = null
|
|
let finishReason = ''
|
|
|
|
try {
|
|
await bareAgentStreamChatCompletions({
|
|
fetchFn,
|
|
url,
|
|
headers,
|
|
body,
|
|
signal: masterAbort.signal,
|
|
onEvent: (ev) => {
|
|
const e = /** @type {Record<string, unknown>} */ (ev)
|
|
if (e.type === 'delta_content') {
|
|
const chunk = typeof e.content === 'string' ? e.content : ''
|
|
assistantContent += chunk
|
|
bareAgentWriteOut(
|
|
stdout,
|
|
bareEditSgr('string', useColor) + chunk + EDIT_ANSI_RESET
|
|
)
|
|
} else if (e.type === 'delta_tool_calls') {
|
|
const arr = e.tool_calls
|
|
if (Array.isArray(arr)) {
|
|
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
|
}
|
|
} else if (e.type === 'usage') {
|
|
usageOut = e.usage
|
|
} else if (e.type === 'finish_reason') {
|
|
finishReason = String(e.finish_reason || '')
|
|
}
|
|
}
|
|
})
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'name' in e && e.name === 'AbortError'
|
|
? 'aborted'
|
|
: e && typeof e === 'object' && 'message' in e
|
|
? String(e.message)
|
|
: String(e)
|
|
bareAgentErr(ctx, argv0 + ': ' + msg)
|
|
ctx.exitCode = 130
|
|
break
|
|
}
|
|
|
|
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
|
const hasTools = toolCallsArr.length > 0
|
|
|
|
/** @type {Record<string, unknown>} */
|
|
const assistantMsg = {
|
|
role: 'assistant',
|
|
content: assistantContent || null,
|
|
tool_calls: hasTools ? toolCallsArr : undefined
|
|
}
|
|
messages.push(assistantMsg)
|
|
|
|
if (usageOut && typeof usageOut === 'object') {
|
|
const u = /** @type {Record<string, unknown>} */ (usageOut)
|
|
const pt = u.prompt_tokens
|
|
const ct = u.completion_tokens
|
|
bareAgentWriteOut(
|
|
stdout,
|
|
'\n' +
|
|
bareEditSgr('dim', useColor) +
|
|
'tokens: prompt=' +
|
|
String(pt ?? '?') +
|
|
' completion=' +
|
|
String(ct ?? '?') +
|
|
EDIT_ANSI_RESET +
|
|
'\n'
|
|
)
|
|
}
|
|
|
|
if (!hasTools) {
|
|
await bareAgentSaveHistory(ctx, paths.history, messages)
|
|
bareAgentWriteOut(stdout, '\n')
|
|
break
|
|
}
|
|
|
|
appendProgress(
|
|
'iteration ' +
|
|
iter +
|
|
' tools ' +
|
|
toolCallsArr.map((x) =>
|
|
/** @type {{ function?: { name?: string } }} */ (x).function?.name
|
|
).join(',')
|
|
)
|
|
|
|
for (const tc of toolCallsArr) {
|
|
const fn = /** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ (
|
|
tc
|
|
).function
|
|
const id = /** @type {{ id?: string }} */ (tc).id || ''
|
|
const name = fn?.name || ''
|
|
const argsStr = fn?.arguments || '{}'
|
|
bareAgentWriteOut(
|
|
stdout,
|
|
'\n' +
|
|
bareEditSgr('keyword', useColor) +
|
|
'[tool] ' +
|
|
name +
|
|
EDIT_ANSI_RESET +
|
|
'\n'
|
|
)
|
|
|
|
const spinner = bareEditSgr('dim', useColor) + '… running ' + name + EDIT_ANSI_RESET
|
|
bareAgentWriteOut(stdout, spinner + '\r')
|
|
|
|
const resultStr = await bareAgentDispatchTool({
|
|
ctx,
|
|
toolName: name,
|
|
argsJson: argsStr,
|
|
paths,
|
|
signal: masterAbort.signal,
|
|
appendProgress,
|
|
home,
|
|
configRef,
|
|
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] YOUR_REQUEST_HERE\n' +
|
|
' ' +
|
|
argv0 +
|
|
' --setup\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' +
|
|
'\n' +
|
|
'Examples:\n' +
|
|
' ' +
|
|
argv0 +
|
|
' "summarize ~/README and list five files in /bin"\n' +
|
|
' ' +
|
|
argv0 +
|
|
' --setup\n' +
|
|
'\n' +
|
|
'See man agent.'
|
|
)
|
|
ctx.exitCode = wantHelp ? 0 : 1
|
|
return
|
|
}
|
|
|
|
let setupFlag = false
|
|
/** @type {string[]} */
|
|
const rest = []
|
|
for (let i = 0; i < args.length; i++) {
|
|
const a = args[i]
|
|
if (a === '--setup') setupFlag = true
|
|
else rest.push(a)
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|