Files
bare-operating-system/packages/bare-os-seeder/kernel/bin/agent
T

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
})
}