15587 lines
490 KiB
Plaintext
15587 lines
490 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
|
||
}
|
||
|
||
/** Session env map (`vfs.env`, then `ctx.env`). Never throws. */
|
||
function bareOsEnv(ctx) {
|
||
const v = ctx && ctx.vfs && ctx.vfs.env
|
||
if (v && typeof v === 'object') return v
|
||
const e = ctx && ctx.env
|
||
if (e && typeof e === 'object') return e
|
||
return {}
|
||
}
|
||
|
||
/**
|
||
* Strict POSIX-ish decimal integer (no octal, no exponent, no empty).
|
||
* @param {unknown} s
|
||
* @returns {number}
|
||
*/
|
||
function bareOsParseDecInt(s) {
|
||
const t = String(s == null ? '' : s).trim()
|
||
if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN
|
||
const n = Number.parseInt(t, 10)
|
||
return Number.isSafeInteger(n) ? n : NaN
|
||
}
|
||
|
||
/** @param {unknown} s */
|
||
function bareOsParseNonNegInt(s) {
|
||
const n = bareOsParseDecInt(s)
|
||
return n >= 0 ? n : NaN
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} name
|
||
* @param {number} fallback
|
||
* @param {number} [min]
|
||
* @param {number} [max]
|
||
*/
|
||
function bareOsEnvInt(ctx, name, fallback, min, max) {
|
||
const raw = bareOsEnv(ctx)[name]
|
||
if (raw == null || raw === '') return fallback
|
||
const n = Number.parseInt(String(raw), 10)
|
||
if (!Number.isFinite(n)) return fallback
|
||
let v = n
|
||
if (min != null && v < min) v = min
|
||
if (max != null && v > max) v = max
|
||
return v
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} msg
|
||
* @param {number} [code]
|
||
*/
|
||
function bareOsFail(ctx, msg, code) {
|
||
if (msg) ctx.console.error(msg)
|
||
ctx.exitCode = code == null ? 1 : code
|
||
}
|
||
|
||
/** @param {unknown} e */
|
||
function bareOsIsNotFoundErr(e) {
|
||
const code = e && typeof e === 'object' ? e.code : ''
|
||
if (code === 'ENOENT') return true
|
||
const msg = String((e && e.message) || e || '')
|
||
return /ENOENT|No such file|not found/i.test(msg)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} buf
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsToU8(ctx, buf) {
|
||
if (!buf) return new Uint8Array(0)
|
||
if (buf instanceof Uint8Array) return buf
|
||
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf)
|
||
return new Uint8Array(buf)
|
||
}
|
||
|
||
/** @param {string} dir @param {string} name */
|
||
function bareOsJoinPath(dir, name) {
|
||
const d = String(dir || '').replace(/\/+$/, '')
|
||
const n = String(name || '').replace(/^\/+/, '')
|
||
if (!d || d === '/') return '/' + n
|
||
return d + '/' + n
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsBaseName(p) {
|
||
const t = String(p || '').replace(/\/+$/, '')
|
||
if (!t || t === '/') return t === '/' ? '/' : ''
|
||
const i = t.lastIndexOf('/')
|
||
return i < 0 ? t : t.slice(i + 1) || t
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsParentDir(p) {
|
||
const t = String(p || '').replace(/\/+$/, '') || '/'
|
||
if (t === '/') return '/'
|
||
const i = t.lastIndexOf('/')
|
||
return i <= 0 ? '/' : t.slice(0, i) || '/'
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsNormPath(p) {
|
||
return String(p || '').replace(/\/+$/, '') || '/'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} p
|
||
*/
|
||
function bareOsResolvePath(ctx, p) {
|
||
if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') {
|
||
try {
|
||
return String(ctx.vfs.resolveLogical(p) || p)
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
}
|
||
return String(p || '')
|
||
}
|
||
|
||
/**
|
||
* True when dest is src or lives under src (self-copy / self-move).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} src
|
||
* @param {string} dest
|
||
*/
|
||
function bareOsDestInsideSrc(ctx, src, dest) {
|
||
const s = bareOsNormPath(bareOsResolvePath(ctx, src))
|
||
const d = bareOsNormPath(bareOsResolvePath(ctx, dest))
|
||
if (s === d) return true
|
||
if (s === '/') return d !== '/'
|
||
return d === s || d.startsWith(s + '/')
|
||
}
|
||
|
||
const BARE_OS_B64_ALPH =
|
||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||
|
||
/** @param {Uint8Array} u8 */
|
||
function bareOsB64Encode(u8) {
|
||
let out = ''
|
||
let i = 0
|
||
for (; i + 2 < u8.length; i += 3) {
|
||
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
|
||
out +=
|
||
BARE_OS_B64_ALPH[(n >> 18) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 12) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 6) & 63] +
|
||
BARE_OS_B64_ALPH[n & 63]
|
||
}
|
||
const rest = u8.length - i
|
||
if (rest === 1) {
|
||
const n = u8[i] << 16
|
||
out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '=='
|
||
} else if (rest === 2) {
|
||
const n = (u8[i] << 16) | (u8[i + 1] << 8)
|
||
out +=
|
||
BARE_OS_B64_ALPH[(n >> 18) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 12) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 6) & 63] +
|
||
'='
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk.
|
||
* @param {string} s
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsB64Decode(s) {
|
||
const t = String(s).replace(/\s+/g, '')
|
||
if (!t) return new Uint8Array(0)
|
||
if (t.length % 4 === 1) throw new Error('invalid base64 length')
|
||
let pad = 0
|
||
if (t.endsWith('==')) pad = 2
|
||
else if (t.endsWith('=')) pad = 1
|
||
const body = pad ? t.slice(0, t.length - pad) : t
|
||
const bytes = []
|
||
let buf = 0
|
||
let bits = 0
|
||
for (let i = 0; i < body.length; i++) {
|
||
const c = body[i]
|
||
let v = BARE_OS_B64_ALPH.indexOf(c)
|
||
if (v < 0) {
|
||
if (c === '-') v = 62
|
||
else if (c === '_') v = 63
|
||
else throw new Error('invalid base64 character')
|
||
}
|
||
buf = (buf << 6) | v
|
||
bits += 6
|
||
if (bits >= 8) {
|
||
bits -= 8
|
||
bytes.push((buf >> bits) & 255)
|
||
}
|
||
}
|
||
if (pad) {
|
||
const want = Math.floor((body.length * 6) / 8)
|
||
if (bytes.length > want) bytes.length = want
|
||
}
|
||
return new Uint8Array(bytes)
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsHexDecode(s) {
|
||
const t = String(s).replace(/\s+/g, '')
|
||
if (t.length % 2 !== 0) throw new Error('odd hex length')
|
||
const out = new Uint8Array(t.length / 2)
|
||
for (let i = 0; i < out.length; i++) {
|
||
const pair = t.slice(i * 2, i * 2 + 2)
|
||
if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex')
|
||
out[i] = Number.parseInt(pair, 16)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** @param {Uint8Array} u8 */
|
||
function bareOsHexEncode(u8) {
|
||
let s = ''
|
||
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
|
||
return s
|
||
}
|
||
|
||
/** ANSI helpers for /bin/edit (preamble; no import in src). */
|
||
|
||
const EDIT_ANSI_RESET = '\x1b[0m'
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
* @returns {import('stream').Writable | undefined}
|
||
*/
|
||
function bareEditResolveStdout(ctx) {
|
||
if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout
|
||
const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx)
|
||
const out = c.replStdout || c.stdout || globalThis.process?.stdout
|
||
return /** @type {import('stream').Writable | undefined} */ (out)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
*/
|
||
function bareEditUseColor(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false
|
||
const out = bareEditResolveStdout(ctx)
|
||
return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY)
|
||
}
|
||
|
||
/**
|
||
* @param {'keyword'|'string'|'comment'|'number'|'status'|'inverse'|'dim'} cls
|
||
* @param {boolean} on
|
||
*/
|
||
function bareEditSgr(cls, on) {
|
||
if (!on) return ''
|
||
switch (cls) {
|
||
case 'keyword':
|
||
return '\x1b[36m'
|
||
case 'string':
|
||
return '\x1b[32m'
|
||
case 'comment':
|
||
return '\x1b[90m'
|
||
case 'number':
|
||
return '\x1b[33m'
|
||
case 'status':
|
||
return '\x1b[44m\x1b[97m'
|
||
case 'inverse':
|
||
return '\x1b[7m'
|
||
case 'dim':
|
||
return '\x1b[2m'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} fullLine
|
||
* @param {Array<{ start: number, end: number, cls: string }>} spans offsets into fullLine
|
||
* @param {number} visStart first column (0-based)
|
||
* @param {number} maxLen max code units to show
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareEditPaintLineWindow(fullLine, spans, visStart, maxLen, useColor) {
|
||
const slice = fullLine.slice(visStart, visStart + maxLen)
|
||
if (!useColor) return slice
|
||
const n = slice.length
|
||
const relSpans = spans
|
||
.map((s) => ({
|
||
start: Math.max(0, s.start - visStart),
|
||
end: Math.min(n, s.end - visStart),
|
||
cls: s.cls
|
||
}))
|
||
.filter((s) => s.end > 0 && s.start < n)
|
||
.sort((a, b) => a.start - b.start)
|
||
|
||
let out = ''
|
||
let pos = 0
|
||
for (const sp of relSpans) {
|
||
if (sp.start > pos) out += slice.slice(pos, sp.start)
|
||
out +=
|
||
bareEditSgr(/** @type {'keyword'} */ (sp.cls), true) +
|
||
slice.slice(sp.start, sp.end) +
|
||
EDIT_ANSI_RESET
|
||
pos = sp.end
|
||
}
|
||
if (pos < n) out += slice.slice(pos)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Move cursor (1-based row/col, DEC origin). Clamp to sane bounds for escape parsing.
|
||
* @param {number} row1
|
||
* @param {number} col1
|
||
*/
|
||
function bareEditCup(row1, col1) {
|
||
const r = Math.max(1, Math.min(Math.floor(row1), 9999))
|
||
const c = Math.max(1, Math.min(Math.floor(col1), 9999))
|
||
return '\x1b[' + r + ';' + c + 'H'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Writable} stdout
|
||
* @param {string} s
|
||
*/
|
||
function bareEditWrite(ctx, stdout, s) {
|
||
if (!stdout || typeof stdout.write !== 'function') return
|
||
try {
|
||
stdout.write(s)
|
||
} catch {
|
||
try {
|
||
ctx.console?.error?.('edit: stdout write failed')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/** TTY key parsing for /bin/edit: consume one logical key from a mutable byte queue. */
|
||
|
||
/**
|
||
* @param {number} b1
|
||
*/
|
||
function bareEditUtf8TrailCount(b1) {
|
||
if (b1 >= 0xc0 && b1 < 0xe0) return 1
|
||
if (b1 >= 0xe0 && b1 < 0xf0) return 2
|
||
if (b1 >= 0xf0) return 3
|
||
return 0
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditUtf8DecodeKey(bytes) {
|
||
try {
|
||
const u = new Uint8Array(bytes)
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(u)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} seq CSI payload after ESC [, including final byte (e.g. "A", "1;5A", "3~")
|
||
*/
|
||
function bareEditCsiToEvent(seq) {
|
||
if (seq === '3~') return { type: 'ctrl', code: 'delete' }
|
||
if (seq === '5~') return { type: 'nav', key: 'pageup' }
|
||
if (seq === '6~') return { type: 'nav', key: 'pagedown' }
|
||
const last = seq.charAt(seq.length - 1)
|
||
if (last === '~') {
|
||
if (seq === '1~' || seq === '7~') return { type: 'nav', key: 'home' }
|
||
if (seq === '4~' || seq === '8~') return { type: 'nav', key: 'end' }
|
||
const tildeNum = /^(\d+)~$/.exec(seq)
|
||
if (tildeNum) {
|
||
const n = parseInt(tildeNum[1], 10)
|
||
const fnMap = {
|
||
11: 1,
|
||
12: 2,
|
||
13: 3,
|
||
14: 4,
|
||
15: 5,
|
||
17: 6,
|
||
18: 7,
|
||
19: 8,
|
||
20: 9,
|
||
21: 10,
|
||
23: 11,
|
||
24: 12
|
||
}
|
||
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
|
||
}
|
||
return { type: 'unknown' }
|
||
}
|
||
if (last === 'Z') return { type: 'nav', key: 'stab' }
|
||
if (last === 'A' || last === 'B' || last === 'C' || last === 'D') {
|
||
const map = { A: 'up', B: 'down', C: 'right', D: 'left' }
|
||
return { type: 'nav', key: map[last] }
|
||
}
|
||
if (last === 'H') return { type: 'nav', key: 'home' }
|
||
if (last === 'F') return { type: 'nav', key: 'end' }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number} b3 byte after ESC O
|
||
*/
|
||
function bareEditSs3ToEvent(b3) {
|
||
if (b3 === 72) return { type: 'nav', key: 'home' }
|
||
if (b3 === 70) return { type: 'nav', key: 'end' }
|
||
if (b3 === 65) return { type: 'nav', key: 'up' }
|
||
if (b3 === 66) return { type: 'nav', key: 'down' }
|
||
if (b3 === 67) return { type: 'nav', key: 'right' }
|
||
if (b3 === 68) return { type: 'nav', key: 'left' }
|
||
if (b3 === 80) return { type: 'fn', n: 1 }
|
||
if (b3 === 81) return { type: 'fn', n: 2 }
|
||
if (b3 === 82) return { type: 'fn', n: 3 }
|
||
if (b3 === 83) return { type: 'fn', n: 4 }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/** ESC [ 200 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
|
||
/** ESC [ 201 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
|
||
|
||
/**
|
||
* @param {number[]} q
|
||
* @param {number[]} prefix
|
||
*/
|
||
function bareEditStartsWithBytes(q, prefix) {
|
||
if (q.length < prefix.length) return false
|
||
for (let i = 0; i < prefix.length; i++) {
|
||
if (q[i] !== prefix[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
|
||
* @param {number[]} q
|
||
*/
|
||
function bareEditCouldBeBracketPastePrefix(q) {
|
||
const pre = BARE_EDIT_BRACKET_PASTE_START
|
||
const n = Math.min(q.length, pre.length)
|
||
for (let i = 0; i < n; i++) {
|
||
if (q[i] !== pre[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditDecodePasteInner(bytes) {
|
||
try {
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(
|
||
new Uint8Array(bytes)
|
||
)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
|
||
* @param {number[]} q
|
||
* @returns {string|undefined}
|
||
*/
|
||
function bareEditFinalizeBracketedPasteOnEof(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
const inner = q.slice(SL)
|
||
q.length = 0
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
|
||
* @param {number[]} q mutable queue
|
||
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
|
||
*/
|
||
function bareEditTryConsumeBracketedPaste(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
const EL = BARE_EDIT_BRACKET_PASTE_END.length
|
||
if (!q.length) return undefined
|
||
if (q.length < SL) {
|
||
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
|
||
}
|
||
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
return undefined
|
||
}
|
||
for (let i = SL; i <= q.length - EL; i++) {
|
||
let ok = true
|
||
for (let j = 0; j < EL; j++) {
|
||
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
|
||
ok = false
|
||
break
|
||
}
|
||
}
|
||
if (ok) {
|
||
const inner = q.slice(SL, i)
|
||
q.splice(0, i + EL)
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q mutable queue (front = index 0)
|
||
* @returns {Record<string, unknown> | null} null if more bytes needed
|
||
*/
|
||
function bareEditTryConsumeKey(q) {
|
||
if (!q.length) return null
|
||
const b1 = q[0]
|
||
if (b1 === 3) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'interrupt' }
|
||
}
|
||
if (b1 === 8 || b1 === 127) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'backspace' }
|
||
}
|
||
if (b1 === 13 || b1 === 10) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\n' }
|
||
}
|
||
if (b1 === 9) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\t' }
|
||
}
|
||
if (b1 === 27) {
|
||
if (q.length < 2) return null
|
||
const b2 = q[1]
|
||
if (b2 === 91) {
|
||
let i = 2
|
||
while (i < q.length) {
|
||
const b = q[i]
|
||
if (b >= 0x40 && b <= 0x7e) {
|
||
const seq = String.fromCharCode.apply(null, q.slice(2, i + 1))
|
||
q.splice(0, i + 1)
|
||
return bareEditCsiToEvent(seq)
|
||
}
|
||
i++
|
||
}
|
||
return null
|
||
}
|
||
if (b2 === 79) {
|
||
if (q.length < 3) return null
|
||
const b3 = q[2]
|
||
q.splice(0, 3)
|
||
return bareEditSs3ToEvent(b3)
|
||
}
|
||
q.splice(0, 2)
|
||
return { type: 'key', ch: String.fromCharCode(b2) }
|
||
}
|
||
if (b1 < 0x20) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: b1 }
|
||
}
|
||
const need = bareEditUtf8TrailCount(b1)
|
||
if (q.length < 1 + need) return null
|
||
const chunk = q.splice(0, 1 + need)
|
||
return { type: 'key', ch: bareEditUtf8DecodeKey(chunk) }
|
||
}
|
||
|
||
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
|
||
|
||
/**
|
||
* @param {unknown} chunk
|
||
* @returns {number[]}
|
||
*/
|
||
function bareEditChunkBytes(chunk) {
|
||
if (chunk == null) return []
|
||
if (typeof chunk === 'string') {
|
||
const out = []
|
||
for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff)
|
||
return out
|
||
}
|
||
const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length
|
||
const out = []
|
||
for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
|
||
*/
|
||
async function bareEditReadKey(reader) {
|
||
reader._keyq = reader._keyq || []
|
||
const q = reader._keyq
|
||
for (;;) {
|
||
const pasteText = bareEditTryConsumeBracketedPaste(q)
|
||
if (pasteText !== undefined && pasteText !== null) {
|
||
return { type: 'paste', text: pasteText }
|
||
}
|
||
if (pasteText === null) {
|
||
const b = await reader.nextByte()
|
||
if (b === undefined) {
|
||
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
|
||
if (eofPaste !== undefined) {
|
||
return { type: 'paste', text: eofPaste }
|
||
}
|
||
if (!q.length) return { type: 'eof' }
|
||
if (q.length === 1 && q[0] === 27) {
|
||
q.length = 0
|
||
return { type: 'key', ch: '\x1b' }
|
||
}
|
||
const lone = q.shift()
|
||
if (lone !== undefined && lone < 0x20) {
|
||
return { type: 'ctrl', code: lone }
|
||
}
|
||
if (lone !== undefined) {
|
||
return { type: 'key', ch: String.fromCharCode(lone) }
|
||
}
|
||
return { type: 'eof' }
|
||
}
|
||
q.push(b)
|
||
continue
|
||
}
|
||
const ev = bareEditTryConsumeKey(q)
|
||
if (ev) return ev
|
||
const b = await reader.nextByte()
|
||
if (b === undefined) {
|
||
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
|
||
if (eofPaste !== undefined) {
|
||
return { type: 'paste', text: eofPaste }
|
||
}
|
||
if (!q.length) return { type: 'eof' }
|
||
if (q.length === 1 && q[0] === 27) {
|
||
q.length = 0
|
||
return { type: 'key', ch: '\x1b' }
|
||
}
|
||
const lone = q.shift()
|
||
if (lone !== undefined && lone < 0x20) {
|
||
return { type: 'ctrl', code: lone }
|
||
}
|
||
if (lone !== undefined) {
|
||
return { type: 'key', ch: String.fromCharCode(lone) }
|
||
}
|
||
return { type: 'eof' }
|
||
}
|
||
q.push(b)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {import('stream').Readable} stdin
|
||
*/
|
||
function bareEditCreateStdinReader(stdin) {
|
||
/** @type {number[]} */
|
||
const bytes = []
|
||
/** @type {(() => void)[]} */
|
||
const waiters = []
|
||
function drain() {
|
||
while (waiters.length && bytes.length) {
|
||
const w = waiters.shift()
|
||
if (w) w()
|
||
}
|
||
}
|
||
/** @param {unknown} chunk */
|
||
function onData(chunk) {
|
||
bytes.push(...bareEditChunkBytes(chunk))
|
||
drain()
|
||
}
|
||
stdin.on('data', onData)
|
||
return {
|
||
nextByte() {
|
||
if (bytes.length) return Promise.resolve(bytes.shift())
|
||
return new Promise((resolve) => {
|
||
waiters.push(() => resolve(bytes.shift()))
|
||
})
|
||
},
|
||
dispose() {
|
||
stdin.removeListener('data', onData)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Minimal AbortController for Bare/Pear when globalThis lacks it (drive-resident /bin preamble). */
|
||
|
||
function bareAgentEnsureAbortPolyfill() {
|
||
const g =
|
||
typeof globalThis !== 'undefined'
|
||
? globalThis
|
||
: typeof global !== 'undefined'
|
||
? global
|
||
: typeof self !== 'undefined'
|
||
? self
|
||
: /** @type {Record<string, unknown>} */ ({})
|
||
if (typeof g.AbortController === 'function') return
|
||
|
||
function BareAbortSignal() {
|
||
/** @type {boolean} */
|
||
this.aborted = false
|
||
/** @type {unknown} */
|
||
this.reason = undefined
|
||
/** @type {{ fn: () => void, once: boolean }[]} */
|
||
this._listeners = []
|
||
}
|
||
|
||
BareAbortSignal.prototype.addEventListener = function (type, fn, opts) {
|
||
if (type !== 'abort' || typeof fn !== 'function') return
|
||
const once = !!(opts && opts.once)
|
||
if (this.aborted) {
|
||
if (once) {
|
||
try {
|
||
fn.call(this)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return
|
||
}
|
||
this._listeners.push({ fn: /** @type {() => void} */ (fn), once })
|
||
}
|
||
|
||
BareAbortSignal.prototype.removeEventListener = function (type, fn) {
|
||
if (type !== 'abort' || typeof fn !== 'function') return
|
||
this._listeners = this._listeners.filter((x) => x.fn !== fn)
|
||
}
|
||
|
||
BareAbortSignal.prototype.throwIfAborted = function () {
|
||
if (!this.aborted) return
|
||
const DOMException = g.DOMException
|
||
if (typeof DOMException === 'function') {
|
||
throw new DOMException('Aborted', 'AbortError')
|
||
}
|
||
const e = new Error('Aborted')
|
||
e.name = 'AbortError'
|
||
throw e
|
||
}
|
||
|
||
function BareAbortController() {
|
||
this.signal = new BareAbortSignal()
|
||
}
|
||
|
||
BareAbortController.prototype.abort = function (reason) {
|
||
const s = this.signal
|
||
if (s.aborted) return
|
||
s.aborted = true
|
||
s.reason = reason
|
||
const list = s._listeners.slice()
|
||
s._listeners.length = 0
|
||
for (const x of list) {
|
||
try {
|
||
x.fn.call(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
if (typeof globalThis !== 'undefined') {
|
||
globalThis.AbortController = BareAbortController
|
||
globalThis.AbortSignal = BareAbortSignal
|
||
} else {
|
||
g.AbortController = BareAbortController
|
||
g.AbortSignal = BareAbortSignal
|
||
}
|
||
}
|
||
|
||
bareAgentEnsureAbortPolyfill()
|
||
|
||
/** TextEncoder/TextDecoder when missing on globalThis (Bare/Pear guest eval). */
|
||
|
||
/**
|
||
* @param {Uint8Array} bytes
|
||
*/
|
||
function bareAgentUtf8Decode(bytes) {
|
||
let out = ''
|
||
let i = 0
|
||
const len = bytes.length
|
||
while (i < len) {
|
||
const b0 = bytes[i++]
|
||
if (b0 < 0x80) {
|
||
out += String.fromCharCode(b0)
|
||
continue
|
||
}
|
||
if ((b0 & 0xe0) === 0xc0) {
|
||
if (i >= len || (bytes[i] & 0xc0) !== 0x80) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f)
|
||
if (cp < 0x80) out += '\ufffd'
|
||
else out += String.fromCharCode(cp)
|
||
continue
|
||
}
|
||
if ((b0 & 0xf0) === 0xe0) {
|
||
if (i + 1 >= len || (bytes[i] & 0xc0) !== 0x80 || (bytes[i + 1] & 0xc0) !== 0x80) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const b2 = bytes[i++]
|
||
let cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)
|
||
if (cp < 0x800 || (cp >= 0xd800 && cp <= 0xdfff)) out += '\ufffd'
|
||
else out += String.fromCharCode(cp)
|
||
continue
|
||
}
|
||
if ((b0 & 0xf8) === 0xf0) {
|
||
if (
|
||
i + 2 >= len ||
|
||
(bytes[i] & 0xc0) !== 0x80 ||
|
||
(bytes[i + 1] & 0xc0) !== 0x80 ||
|
||
(bytes[i + 2] & 0xc0) !== 0x80
|
||
) {
|
||
out += '\ufffd'
|
||
continue
|
||
}
|
||
const b1 = bytes[i++]
|
||
const b2 = bytes[i++]
|
||
const b3 = bytes[i++]
|
||
let cp =
|
||
((b0 & 0x07) << 18) |
|
||
((b1 & 0x3f) << 12) |
|
||
((b2 & 0x3f) << 6) |
|
||
(b3 & 0x3f)
|
||
if (cp < 0x10000 || cp > 0x10ffff) out += '\ufffd'
|
||
else {
|
||
cp -= 0x10000
|
||
out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff))
|
||
}
|
||
continue
|
||
}
|
||
out += '\ufffd'
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} str
|
||
*/
|
||
function bareAgentUtf8Encode(str) {
|
||
const out = []
|
||
for (let i = 0; i < str.length; i++) {
|
||
let c = str.charCodeAt(i)
|
||
if (c < 0x80) {
|
||
out.push(c)
|
||
} else if (c < 0x800) {
|
||
out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
|
||
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
|
||
const c2 = str.charCodeAt(i + 1)
|
||
if ((c2 & 0xfc00) === 0xdc00) {
|
||
i++
|
||
const cp = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff)
|
||
out.push(
|
||
0xf0 | (cp >> 18),
|
||
0x80 | ((cp >> 12) & 0x3f),
|
||
0x80 | ((cp >> 6) & 0x3f),
|
||
0x80 | (cp & 0x3f)
|
||
)
|
||
} else {
|
||
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
|
||
}
|
||
} else if (c < 0xd800 || c >= 0xe000) {
|
||
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
|
||
}
|
||
}
|
||
return new Uint8Array(out)
|
||
}
|
||
|
||
function bareAgentEnsureTextCodecPolyfill() {
|
||
const g =
|
||
typeof globalThis !== 'undefined'
|
||
? globalThis
|
||
: typeof global !== 'undefined'
|
||
? global
|
||
: typeof self !== 'undefined'
|
||
? self
|
||
: /** @type {Record<string, unknown>} */ ({})
|
||
|
||
if (typeof g.TextDecoder !== 'function') {
|
||
function BareTextDecoder() {
|
||
/** @type {'utf-8'} */
|
||
this.encoding = 'utf-8'
|
||
}
|
||
BareTextDecoder.prototype.decode = function (input, options) {
|
||
let u8
|
||
if (input instanceof Uint8Array) u8 = input
|
||
else if (typeof ArrayBuffer !== 'undefined' && input instanceof ArrayBuffer)
|
||
u8 = new Uint8Array(input)
|
||
else if (input != null && typeof input === 'object' && 'length' in input) {
|
||
u8 = new Uint8Array(/** @type {ArrayLike<number>} */ (input))
|
||
} else if (input == null || input === undefined) {
|
||
return ''
|
||
} else {
|
||
return ''
|
||
}
|
||
const stream = !!(options && options.stream)
|
||
void stream
|
||
return bareAgentUtf8Decode(u8)
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextDecoder = BareTextDecoder
|
||
else g.TextDecoder = BareTextDecoder
|
||
}
|
||
|
||
if (typeof g.TextEncoder !== 'function') {
|
||
function BareTextEncoder() {
|
||
this.encoding = 'utf-8'
|
||
}
|
||
BareTextEncoder.prototype.encode = function (input) {
|
||
return bareAgentUtf8Encode(String(input == null ? '' : input))
|
||
}
|
||
if (typeof globalThis !== 'undefined') globalThis.TextEncoder = BareTextEncoder
|
||
else g.TextEncoder = BareTextEncoder
|
||
}
|
||
}
|
||
|
||
bareAgentEnsureTextCodecPolyfill()
|
||
|
||
/** Shared helpers for /bin/agent tools (preamble for agent bundle). */
|
||
|
||
/**
|
||
* Read-only base system (kernel / system Hyperdrive + virtual fs).
|
||
* Everything else is writable by default (denylist, not allowlist).
|
||
* @type {readonly string[]}
|
||
*/
|
||
var BARE_AGENT_MUTATE_DENY_PREFIXES = Object.freeze([
|
||
'/bin',
|
||
'/etc',
|
||
'/boot',
|
||
'/lib',
|
||
'/usr',
|
||
'/share',
|
||
'/proc',
|
||
'/dev',
|
||
'/sys',
|
||
'/run'
|
||
])
|
||
|
||
/**
|
||
* Seed list for diagnostic snapshots. Live reads allow any /proc path.
|
||
* @type {readonly string[]}
|
||
*/
|
||
var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||
'/proc/bare_os/metrics_live.json',
|
||
'/proc/bare_os/features',
|
||
'/proc/bare_os/features.json',
|
||
'/proc/bare_os/swarm.json',
|
||
'/proc/bare_os/capabilities.json',
|
||
'/proc/bare_os/swarm_replication_status.json',
|
||
'/proc/bare_os/swarm_relay_status.json',
|
||
'/proc/bare_os/swarm_datagrams_status.json',
|
||
'/proc/bare_os/swarm_connection_manager_status.json',
|
||
'/proc/bare_os/swarm_key_broker_status.json',
|
||
'/proc/bare_os/swarm_holepunch_status.json',
|
||
'/proc/bare_os/swarm_datagram_replication_status.json',
|
||
'/proc/bare_os/swarm_status.json'
|
||
])
|
||
|
||
/**
|
||
* @param {string} absPath
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentNormalizeAbsPath(absPath) {
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
if (!p.startsWith('/') || p.includes('..')) return ''
|
||
if (p.length > 1) return p.replace(/\/+$/, '')
|
||
return p
|
||
}
|
||
|
||
/**
|
||
* @param {string} absPath
|
||
* @param {unknown} prefixes
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentPrefixDenied(absPath, prefixes) {
|
||
const p = bareAgentNormalizeAbsPath(absPath)
|
||
if (!p) return true
|
||
const list =
|
||
Array.isArray(prefixes) && prefixes.length
|
||
? prefixes
|
||
: BARE_AGENT_MUTATE_DENY_PREFIXES
|
||
for (let i = 0; i < list.length; i++) {
|
||
const pref = String(list[i] || '').replace(/\/+$/, '')
|
||
if (!pref) continue
|
||
if (p === pref || p.startsWith(pref + '/')) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Any absolute guest path is readable (denylist-empty).
|
||
* @param {string} absPath
|
||
*/
|
||
function bareAgentPathAllowedRead(absPath) {
|
||
return Boolean(bareAgentNormalizeAbsPath(absPath))
|
||
}
|
||
|
||
/**
|
||
* Writes/renames/deletes: whole VFS except the read-only base system.
|
||
* @param {string} absPath
|
||
* @param {unknown} [prefixes]
|
||
*/
|
||
function bareAgentPathAllowedMutate(absPath, prefixes) {
|
||
const p = bareAgentNormalizeAbsPath(absPath)
|
||
if (!p || p === '/') return false
|
||
return !bareAgentPrefixDenied(p, prefixes)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} statObj
|
||
* @param {string} path
|
||
*/
|
||
function bareAgentSerializeStat(statObj, path) {
|
||
if (!statObj || typeof statObj !== 'object')
|
||
return { path, error: 'no_stat' }
|
||
const s = /** @type {Record<string, unknown>} */ (statObj)
|
||
/** @type {'file' | 'dir' | 'symlink' | 'other'} */
|
||
let kind = 'other'
|
||
try {
|
||
if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir'
|
||
else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink'
|
||
else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file'
|
||
else if (typeof s.mode === 'number') {
|
||
const M = Number(s.mode)
|
||
if ((M & 0o170000) === 0o040000) kind = 'dir'
|
||
else if ((M & 0o170000) === 0o120000) kind = 'symlink'
|
||
else if ((M & 0o170000) === 0o100000) kind = 'file'
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const out = {
|
||
path,
|
||
kind,
|
||
size: typeof s.size === 'number' ? s.size : undefined,
|
||
mode: typeof s.mode === 'number' ? s.mode : undefined,
|
||
mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,
|
||
uid: typeof s.uid === 'number' ? s.uid : undefined,
|
||
gid: typeof s.gid === 'number' ? s.gid : undefined
|
||
}
|
||
if (typeof s.target === 'string') out.target = s.target
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareAgentTruncateChars(text, maxChars) {
|
||
const t = String(text || '')
|
||
const n = Math.floor(maxChars)
|
||
if (!Number.isFinite(n) || n <= 0) return ''
|
||
if (t.length <= n) return t
|
||
return t.slice(0, n) + '\n… truncated'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} page
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareAgentManExtractPageSlice(page, maxChars) {
|
||
if (!page || typeof page !== 'object')
|
||
return { error: 'bad_page' }
|
||
const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000)
|
||
const name = typeof page.name === 'string' ? page.name : ''
|
||
const section = typeof page.section === 'number' ? page.section : 0
|
||
const title = typeof page.title === 'string' ? page.title : ''
|
||
const synopsis = Array.isArray(page.synopsis)
|
||
? page.synopsis.map((x) => String(x)).join('\n')
|
||
: ''
|
||
const description = bareAgentTruncateChars(
|
||
typeof page.description === 'string' ? page.description : '',
|
||
Math.floor(m * 0.55)
|
||
)
|
||
let opts = ''
|
||
if (Array.isArray(page.options)) {
|
||
const lines = []
|
||
for (const o of page.options) {
|
||
if (!o || typeof o !== 'object') continue
|
||
const fl = typeof o.flag === 'string' ? o.flag : ''
|
||
const me = typeof o.meaning === 'string' ? o.meaning : ''
|
||
if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me)
|
||
}
|
||
opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35))
|
||
}
|
||
const blob =
|
||
name +
|
||
'(' +
|
||
section +
|
||
') — ' +
|
||
title +
|
||
'\n\nSYNOPSIS\n' +
|
||
synopsis +
|
||
'\n\nDESCRIPTION\n' +
|
||
description +
|
||
(opts ? '\n\nOPTIONS\n' + opts : '')
|
||
return {
|
||
name,
|
||
section,
|
||
title,
|
||
synopsis,
|
||
description,
|
||
options_text: opts || undefined,
|
||
text: bareAgentTruncateChars(blob, m)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Same semantics as `man -k`: substring match on indexed keywords (merged DB).
|
||
* @param {unknown} db
|
||
* @param {string} needle
|
||
* @param {number} maxHits
|
||
*/
|
||
function bareAgentManAproposHits(db, needle, maxHits) {
|
||
const n = String(needle || '').toLowerCase()
|
||
const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200)
|
||
if (!n || !db || typeof db !== 'object')
|
||
return /** @type {{ lines: string[], truncated: boolean }} */ ({
|
||
lines: [],
|
||
truncated: false
|
||
})
|
||
const d = /** @type {Record<string, unknown>} */ (db)
|
||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||
const apropos = Array.isArray(d.apropos) ? d.apropos : []
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
let truncated = false
|
||
for (const row of apropos) {
|
||
if (!row || typeof row !== 'object') continue
|
||
const kw = typeof row.kw === 'string' ? row.kw : ''
|
||
if (!kw.includes(n)) continue
|
||
const idx = row.pageRef
|
||
if (typeof idx !== 'number' || !pages[idx]) continue
|
||
if (seen.has(idx)) continue
|
||
seen.add(idx)
|
||
const p = /** @type {Record<string, unknown>} */ (pages[idx])
|
||
const name = typeof p.name === 'string' ? p.name : ''
|
||
const sec = typeof p.section === 'number' ? p.section : 0
|
||
const title = typeof p.title === 'string' ? p.title : ''
|
||
lines.push(name + '(' + sec + ') - ' + title)
|
||
if (lines.length >= max) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
lines.sort()
|
||
return { lines, truncated }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} vfs
|
||
* @param {{ db: unknown | null }} cacheRef
|
||
* @returns {Promise<unknown | null>}
|
||
*/
|
||
async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) {
|
||
if (cacheRef.db) return cacheRef.db
|
||
if (!vfs || typeof vfs.readFile !== 'function') return null
|
||
try {
|
||
const buf = await vfs.readFile('/share/man/man.json')
|
||
if (!buf || !buf.length) return null
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
const parsed = JSON.parse(t)
|
||
cacheRef.db = parsed
|
||
return parsed
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resolve one manual page from merged DB (like `man [[section] name]`).
|
||
* @param {unknown} db
|
||
* @param {string} topic
|
||
* @param {number | null} sectionExplicit
|
||
*/
|
||
function bareAgentManResolvePage(db, topic, sectionExplicit) {
|
||
const name = String(topic || '').toLowerCase()
|
||
if (!name || !db || typeof db !== 'object')
|
||
return { error: 'not_found' }
|
||
const d = /** @type {Record<string, unknown>} */ (db)
|
||
const index = d.index
|
||
if (!index || typeof index !== 'object') return { error: 'not_found' }
|
||
const idx = /** @type {Record<string, unknown>} */ (index)[name]
|
||
if (typeof idx !== 'number') return { error: 'not_found' }
|
||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||
const page = pages[idx]
|
||
if (!page || typeof page !== 'object') return { error: 'not_found' }
|
||
const sec = typeof page.section === 'number' ? page.section : 0
|
||
if (sectionExplicit !== null && sectionExplicit !== sec) {
|
||
return { error: 'wrong_section', foundSection: sec }
|
||
}
|
||
return { page: /** @type {Record<string, unknown>} */ (page) }
|
||
}
|
||
|
||
/**
|
||
* @param {string} absPath
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentProcReadPathAllowed(absPath) {
|
||
const p = bareAgentNormalizeAbsPath(absPath)
|
||
return Boolean(p && (p === '/proc' || p.startsWith('/proc/')))
|
||
}
|
||
|
||
/** ~/.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',
|
||
compact: base + '/compact.md',
|
||
cmdOut: base + '/last_command_out.txt',
|
||
workspace: base + '/workspace',
|
||
workspaceMemory: base + '/workspace/memory',
|
||
workspaceSkills: base + '/workspace/skills',
|
||
skillsGlobal: base + '/skills',
|
||
todos: base + '/todos.json',
|
||
plan: base + '/plan.md',
|
||
hooks: base + '/hooks',
|
||
ask: base + '/ask.json',
|
||
edits: base + '/edits.json',
|
||
lastCwd: base + '/last_cwd'
|
||
}
|
||
}
|
||
|
||
function bareAgentDefaultConfig() {
|
||
return {
|
||
backend: 'qvac',
|
||
rest_base_url: 'https://api.groq.com/openai/v1',
|
||
rest_api_key: '',
|
||
model: 'QWEN3_1_7B_INST_Q4',
|
||
qvac_model: 'QWEN3_1_7B_INST_Q4',
|
||
qvac_profile: 'recommended',
|
||
qvac_ctx_size: 0,
|
||
qvac_device: '',
|
||
qvac_main_gpu: 'auto',
|
||
qvac_gpu_layers: -1,
|
||
max_tokens: 4096,
|
||
temperature: 0.7,
|
||
provider: 'qvac',
|
||
max_iterations: 64,
|
||
stream: true,
|
||
tool_parallelism: 1,
|
||
request_timeout_ms: 120000,
|
||
extra_headers: /** @type {Record<string, string>} */ ({}),
|
||
access_policy: 'full',
|
||
allow_delete: true,
|
||
require_confirm_token: '',
|
||
owner_name: '',
|
||
agent_label: '',
|
||
show_reasoning: false,
|
||
reasoning_mode: 'off',
|
||
reasoning_max_chars: 4000,
|
||
reasoning_include_tools: true,
|
||
allow_bridge_mutations: true,
|
||
allow_host_notifications: true,
|
||
allow_host_actions: true,
|
||
emergency_stop_mutations: false,
|
||
autonomous_mode_enabled: true,
|
||
autonomous_max_runtime_ms: 1800000,
|
||
autonomous_completion_required_checks: [],
|
||
autonomous_allow_paths: ['*'],
|
||
autonomous_deny_ops: [],
|
||
command_deny: [],
|
||
mutate_deny_prefixes: [
|
||
'/bin',
|
||
'/etc',
|
||
'/boot',
|
||
'/lib',
|
||
'/usr',
|
||
'/share',
|
||
'/proc',
|
||
'/dev',
|
||
'/sys',
|
||
'/run'
|
||
],
|
||
autonomous_active: false,
|
||
autonomous_started_at_ms: 0,
|
||
autonomous_stop_requested: false,
|
||
autonomous_goal: '',
|
||
autonomous_status: 'idle',
|
||
autonomous_last_error: '',
|
||
context_compaction: 'auto',
|
||
compaction_keep_recent: 8,
|
||
compaction_tool_chars: 1600,
|
||
plan_mode_active: false,
|
||
todo_nudge_enabled: true
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {v is Record<string, unknown>}
|
||
*/
|
||
function bareAgentIsPlainObject(v) {
|
||
return v != null && typeof v === 'object' && !Array.isArray(v)
|
||
}
|
||
|
||
/**
|
||
* Shallow merge known keys from src into defaults.
|
||
* @param {Record<string, unknown>} defaults
|
||
* @param {Record<string, unknown>} src
|
||
*/
|
||
function bareAgentMergeConfig(defaults, src) {
|
||
const out = { ...defaults }
|
||
const known = new Set([
|
||
'backend',
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'qvac_model',
|
||
'qvac_profile',
|
||
'qvac_ctx_size',
|
||
'qvac_device',
|
||
'qvac_main_gpu',
|
||
'qvac_gpu_layers',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'extra_headers',
|
||
'access_policy',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'command_deny',
|
||
'mutate_deny_prefixes',
|
||
'owner_name',
|
||
'agent_label',
|
||
'show_reasoning',
|
||
'reasoning_mode',
|
||
'reasoning_max_chars',
|
||
'reasoning_include_tools',
|
||
'allow_bridge_mutations',
|
||
'allow_host_notifications',
|
||
'allow_host_actions',
|
||
'emergency_stop_mutations',
|
||
'autonomous_mode_enabled',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_completion_required_checks',
|
||
'autonomous_allow_paths',
|
||
'autonomous_deny_ops',
|
||
'autonomous_active',
|
||
'autonomous_started_at_ms',
|
||
'autonomous_stop_requested',
|
||
'autonomous_goal',
|
||
'autonomous_status',
|
||
'autonomous_last_error',
|
||
'context_compaction',
|
||
'compaction_keep_recent',
|
||
'compaction_tool_chars',
|
||
'plan_mode_active',
|
||
'todo_nudge_enabled'
|
||
])
|
||
for (const k of Object.keys(src)) {
|
||
if (k.startsWith('x-')) continue
|
||
if (!known.has(k)) continue
|
||
const val = src[k]
|
||
if (k === 'extra_headers' && bareAgentIsPlainObject(val)) {
|
||
out.extra_headers = /** @type {Record<string, string>} */ ({ ...val })
|
||
continue
|
||
}
|
||
if (k === 'backend') {
|
||
const b = String(val ?? '').trim().toLowerCase()
|
||
out.backend = b === 'rest' || b === 'openai' || b === 'http' ? 'rest' : 'qvac'
|
||
continue
|
||
}
|
||
if (
|
||
k === 'rest_base_url' ||
|
||
k === 'rest_api_key' ||
|
||
k === 'model' ||
|
||
k === 'qvac_model' ||
|
||
k === 'qvac_profile' ||
|
||
k === 'provider' ||
|
||
k === 'owner_name' ||
|
||
k === 'agent_label' ||
|
||
k === 'autonomous_goal' ||
|
||
k === 'autonomous_status' ||
|
||
k === 'autonomous_last_error' ||
|
||
k === 'qvac_device' ||
|
||
k === 'qvac_main_gpu' ||
|
||
k === 'require_confirm_token'
|
||
) {
|
||
out[k] = String(val ?? '')
|
||
continue
|
||
}
|
||
if (k === 'context_compaction') {
|
||
const mode = String(val ?? '').trim().toLowerCase()
|
||
out.context_compaction =
|
||
mode === 'off' || mode === 'aggressive' ? mode : 'auto'
|
||
continue
|
||
}
|
||
if (k === 'reasoning_mode') {
|
||
const mode = String(val ?? '').trim().toLowerCase()
|
||
out.reasoning_mode =
|
||
mode === 'summary' || mode === 'trace' ? mode : 'off'
|
||
continue
|
||
}
|
||
if (
|
||
k === 'max_tokens' ||
|
||
k === 'temperature' ||
|
||
k === 'max_iterations' ||
|
||
k === 'tool_parallelism' ||
|
||
k === 'request_timeout_ms' ||
|
||
k === 'reasoning_max_chars' ||
|
||
k === 'qvac_ctx_size' ||
|
||
k === 'qvac_gpu_layers' ||
|
||
k === 'autonomous_max_runtime_ms' ||
|
||
k === 'autonomous_started_at_ms' ||
|
||
k === 'compaction_keep_recent' ||
|
||
k === 'compaction_tool_chars'
|
||
) {
|
||
const n = Number(val)
|
||
out[k] = Number.isFinite(n) ? n : defaults[k]
|
||
continue
|
||
}
|
||
if (
|
||
k === 'autonomous_completion_required_checks' ||
|
||
k === 'autonomous_allow_paths' ||
|
||
k === 'autonomous_deny_ops' ||
|
||
k === 'command_deny' ||
|
||
k === 'mutate_deny_prefixes'
|
||
) {
|
||
out[k] = Array.isArray(val) ? val.map((x) => String(x ?? '')).filter(Boolean) : defaults[k]
|
||
continue
|
||
}
|
||
if (
|
||
k === 'stream' ||
|
||
k === 'allow_delete' ||
|
||
k === 'show_reasoning' ||
|
||
k === 'reasoning_include_tools' ||
|
||
k === 'allow_bridge_mutations' ||
|
||
k === 'allow_host_notifications' ||
|
||
k === 'allow_host_actions' ||
|
||
k === 'emergency_stop_mutations' ||
|
||
k === 'autonomous_mode_enabled' ||
|
||
k === 'autonomous_active' ||
|
||
k === 'autonomous_stop_requested' ||
|
||
k === 'plan_mode_active' ||
|
||
k === 'todo_nudge_enabled'
|
||
) {
|
||
out[k] = Boolean(val)
|
||
continue
|
||
}
|
||
if (k === 'require_confirm_token') {
|
||
out.require_confirm_token = String(val ?? '')
|
||
continue
|
||
}
|
||
if (k === 'access_policy') {
|
||
const pol = String(val ?? '').trim().toLowerCase()
|
||
out.access_policy = pol === 'restricted' ? 'restricted' : 'full'
|
||
continue
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Old configs persisted restrictive defaults. Missing access_policy means
|
||
* upgrade onto full guest admin (denylist-only) so existing homes match.
|
||
* @param {Record<string, unknown>} raw
|
||
* @param {Record<string, unknown>} merged
|
||
*/
|
||
function bareAgentApplyAccessPolicyUpgrade(raw, merged) {
|
||
const src = bareAgentIsPlainObject(raw) ? raw : {}
|
||
if (Object.prototype.hasOwnProperty.call(src, 'access_policy')) {
|
||
return { config: merged, upgraded: false }
|
||
}
|
||
const next = { ...merged }
|
||
next.access_policy = 'full'
|
||
next.allow_delete = true
|
||
next.require_confirm_token = ''
|
||
next.allow_bridge_mutations = true
|
||
next.allow_host_notifications = true
|
||
next.allow_host_actions = true
|
||
next.emergency_stop_mutations = false
|
||
next.autonomous_deny_ops = []
|
||
next.autonomous_allow_paths = ['*']
|
||
next.command_deny = Array.isArray(next.command_deny) ? next.command_deny : []
|
||
next.autonomous_mode_enabled = true
|
||
if (!Array.isArray(next.mutate_deny_prefixes) || !next.mutate_deny_prefixes.length) {
|
||
next.mutate_deny_prefixes = bareAgentDefaultConfig().mutate_deny_prefixes
|
||
}
|
||
return { config: next, upgraded: true }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} raw
|
||
*/
|
||
function bareAgentValidateConfigShape(raw) {
|
||
if (!bareAgentIsPlainObject(raw)) throw new Error('config must be a JSON object')
|
||
for (const k of Object.keys(raw)) {
|
||
if (k.startsWith('x-')) continue
|
||
const known = [
|
||
'backend',
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'qvac_model',
|
||
'qvac_profile',
|
||
'qvac_ctx_size',
|
||
'qvac_device',
|
||
'qvac_main_gpu',
|
||
'qvac_gpu_layers',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'extra_headers',
|
||
'access_policy',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'command_deny',
|
||
'mutate_deny_prefixes',
|
||
'owner_name',
|
||
'agent_label',
|
||
'show_reasoning',
|
||
'reasoning_mode',
|
||
'reasoning_max_chars',
|
||
'reasoning_include_tools',
|
||
'allow_bridge_mutations',
|
||
'allow_host_notifications',
|
||
'allow_host_actions',
|
||
'emergency_stop_mutations',
|
||
'autonomous_mode_enabled',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_completion_required_checks',
|
||
'autonomous_allow_paths',
|
||
'autonomous_deny_ops',
|
||
'autonomous_active',
|
||
'autonomous_started_at_ms',
|
||
'autonomous_stop_requested',
|
||
'autonomous_goal',
|
||
'autonomous_status',
|
||
'autonomous_last_error',
|
||
'context_compaction',
|
||
'compaction_keep_recent',
|
||
'compaction_tool_chars',
|
||
'plan_mode_active',
|
||
'todo_nudge_enabled'
|
||
]
|
||
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()
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
raw = bareAgentSanitizeConfigForBackend(raw)
|
||
}
|
||
await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (raw))
|
||
return { config: /** @type {any} */ (raw), created: true }
|
||
}
|
||
|
||
bareAgentValidateConfigShape(raw)
|
||
const merged = bareAgentMergeConfig(bareAgentDefaultConfig(), raw)
|
||
const applied = bareAgentApplyAccessPolicyUpgrade(raw, merged)
|
||
let next = applied.config
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
next = bareAgentSanitizeConfigForBackend(next)
|
||
}
|
||
const dirty =
|
||
applied.upgraded ||
|
||
JSON.stringify(next) !== JSON.stringify(merged)
|
||
if (dirty) {
|
||
try {
|
||
await bareAgentSaveConfig(ctx, paths, /** @type {any} */ (next))
|
||
} catch {
|
||
/* keep upgraded in-memory even if persist fails */
|
||
}
|
||
}
|
||
return { config: /** @type {any} */ (next), 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')
|
||
let next = config
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
next = bareAgentSanitizeConfigForBackend(config)
|
||
if (config && typeof config === 'object' && config !== next) {
|
||
for (const k of Object.keys(config)) {
|
||
if (!Object.prototype.hasOwnProperty.call(next, k)) delete config[k]
|
||
}
|
||
Object.assign(config, next)
|
||
}
|
||
}
|
||
bareAgentValidateConfigShape(next)
|
||
const json = JSON.stringify(next, 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
|
||
}
|
||
|
||
/**
|
||
* Rough token estimate (chars/4). Good enough for local ctx budgeting.
|
||
* @param {unknown} value
|
||
*/
|
||
function bareAgentEstimateTokens(value) {
|
||
try {
|
||
const n = JSON.stringify(value == null ? '' : value).length
|
||
return Math.max(0, Math.ceil(n / 4))
|
||
} catch {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Trim history + shrink system content so prompt fits a QVAC ctx window.
|
||
* Reserves room for completion and optional tools JSON.
|
||
* @param {unknown[]} msgs
|
||
* @param {number} ctxSize
|
||
* @param {{ tools?: unknown[], reserveCompletion?: number }} [opts]
|
||
*/
|
||
function bareAgentTrimMessagesForCtx(msgs, ctxSize, opts) {
|
||
const ctx = Math.max(2048, Math.floor(Number(ctxSize) || 4096))
|
||
const reserve =
|
||
opts && Number.isFinite(Number(opts.reserveCompletion))
|
||
? Math.max(256, Math.floor(Number(opts.reserveCompletion)))
|
||
: Math.min(1024, Math.max(256, Math.floor(ctx * 0.15)))
|
||
const toolsTok = bareAgentEstimateTokens(
|
||
opts && Array.isArray(opts.tools) && opts.tools.length ? opts.tools : []
|
||
)
|
||
const budget = Math.max(512, ctx - reserve - toolsTok)
|
||
|
||
/** @type {unknown[]} */
|
||
let out = Array.isArray(msgs) ? msgs.slice() : []
|
||
// Drop middle turns first (keep system + recent).
|
||
while (bareAgentEstimateTokens(out) > budget && out.length > 3) {
|
||
out.splice(2, 1)
|
||
}
|
||
// Shrink system blob if still over (workspace/man/skills dominate).
|
||
if (bareAgentEstimateTokens(out) > budget && out[0] && typeof out[0] === 'object') {
|
||
const sys = /** @type {Record<string, unknown>} */ (out[0])
|
||
if (sys.role === 'system' && typeof sys.content === 'string') {
|
||
let content = sys.content
|
||
let guard = 0
|
||
while (
|
||
bareAgentEstimateTokens(out) > budget &&
|
||
content.length > 800 &&
|
||
guard < 24
|
||
) {
|
||
content =
|
||
content.slice(0, Math.floor(content.length * 0.82)) + '\n… truncated'
|
||
out[0] = { ...sys, content }
|
||
guard++
|
||
}
|
||
}
|
||
}
|
||
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 compacted =
|
||
typeof bareAgentCompactMessagesForDisk === 'function'
|
||
? bareAgentCompactMessagesForDisk(messages, 500_000, { keepRecent: 16 })
|
||
: bareAgentTrimMessages(messages, 500_000)
|
||
const trimmed =
|
||
typeof bareAgentTrimMessages === 'function'
|
||
? bareAgentTrimMessages(compacted, 500_000)
|
||
: compacted
|
||
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 []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Start an autonomous run on a config object (does not persist).
|
||
* @param {Record<string, unknown>} cfg
|
||
* @param {{ goal: string, maxRuntimeMs?: number, requiredChecks?: string[], scopePath?: string }} opts
|
||
*/
|
||
function bareAgentBeginAutonomousRun(cfg, opts) {
|
||
const goal = String((opts && opts.goal) || '').trim()
|
||
const maxRuntimeMs = Math.min(
|
||
Math.max(Math.floor(Number((opts && opts.maxRuntimeMs) || cfg.autonomous_max_runtime_ms) || 0), 60000),
|
||
7_200_000
|
||
)
|
||
const requiredChecks = Array.isArray(opts && opts.requiredChecks)
|
||
? opts.requiredChecks.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: Array.isArray(cfg.autonomous_completion_required_checks)
|
||
? cfg.autonomous_completion_required_checks.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: []
|
||
return {
|
||
...cfg,
|
||
autonomous_mode_enabled: true,
|
||
autonomous_active: true,
|
||
autonomous_stop_requested: false,
|
||
autonomous_started_at_ms: Date.now(),
|
||
autonomous_goal: goal,
|
||
autonomous_status: 'running',
|
||
autonomous_last_error: '',
|
||
autonomous_max_runtime_ms: maxRuntimeMs,
|
||
autonomous_completion_required_checks: requiredChecks
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
* @param {string} [reason]
|
||
*/
|
||
function bareAgentStopAutonomousRun(cfg, reason) {
|
||
return {
|
||
...cfg,
|
||
autonomous_stop_requested: true,
|
||
autonomous_status: 'stopping',
|
||
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
* @param {{ remainingMs?: number, lastError?: string }} [extra]
|
||
*/
|
||
function bareAgentAutonomousContinuationPrompt(cfg, extra) {
|
||
const goal = String(cfg.autonomous_goal || '').trim() || '(goal unset)'
|
||
const remain =
|
||
extra && typeof extra.remainingMs === 'number'
|
||
? Math.max(0, Math.round(extra.remainingMs / 1000))
|
||
: 0
|
||
return (
|
||
'AUTONOMOUS RUN still active. Do not stop with a plan — take the next concrete tool action.\n' +
|
||
'Goal: ' +
|
||
goal +
|
||
'\n' +
|
||
(remain > 0 ? 'Time remaining: ' + String(remain) + 's.\n' : '') +
|
||
(extra && extra.lastError ? 'Last gate error: ' + extra.lastError + '\n' : '') +
|
||
'Call task_complete(summary) only when the goal is actually finished and verified.'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
* @param {{ completed?: boolean, hasTools?: boolean, stopRequested?: boolean }} state
|
||
*/
|
||
function bareAgentAutonomousShouldContinue(cfg, state) {
|
||
if (!cfg || !cfg.autonomous_active) return false
|
||
if (state && state.stopRequested) return false
|
||
if (state && state.completed) return false
|
||
if (state && state.hasTools) return false
|
||
return true
|
||
}
|
||
|
||
async function bareAgentResetChatSession(ctx, paths, argv0) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.writeFile !== 'function') {
|
||
throw new Error('agent reset: vfs unavailable')
|
||
}
|
||
await vfs.mkdir(paths.dir, { recursive: true })
|
||
const emptyHist =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from('[]\n')
|
||
: new TextEncoder().encode('[]\n')
|
||
await vfs.writeFile(paths.history, emptyHist)
|
||
const stamp = new Date().toISOString() + ' chat session reset\n'
|
||
const progBody =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(stamp)
|
||
: new TextEncoder().encode(stamp)
|
||
await vfs.writeFile(paths.progress, progBody)
|
||
try {
|
||
const z =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from('')
|
||
: new TextEncoder().encode('')
|
||
await vfs.writeFile(paths.cmdOut, z)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
ctx.console.log(
|
||
argv0 + ': chat session cleared (' + paths.history + ', ' + paths.progress + ')'
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* QVAC helpers for /bin/agent (no SDK import — host bridge via ctx.bareOsQvac*).
|
||
*/
|
||
|
||
/** @typedef {'lite'|'recommended'|'strong'|'tool-tiny'} BareAgentQvacProfileId */
|
||
|
||
/**
|
||
* Native context windows from upstream model cards (not conservative laptop defaults).
|
||
* Qwen3 dense (0.6B/1.7B/4B): https://huggingface.co/Qwen/Qwen3-0.6B — 32,768
|
||
* Llama 3.2 1B (tool-calling finetune base): Meta Llama 3.2 — 128,000
|
||
* Absolute ceiling for overrides / host clamp.
|
||
*/
|
||
const BARE_AGENT_QVAC_CTX_QWEN3 = 32768
|
||
const BARE_AGENT_QVAC_CTX_LLAMA32_1B = 131072
|
||
const BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX = 131072
|
||
|
||
/**
|
||
* @typedef {{
|
||
* id: BareAgentQvacProfileId,
|
||
* label: string,
|
||
* description: string,
|
||
* chatModel: string,
|
||
* tools: boolean,
|
||
* minRamGb: number,
|
||
* minDiskGb: number,
|
||
* approxDownloadGb: number,
|
||
* ctxSize: number
|
||
* }} BareAgentQvacProfile
|
||
*/
|
||
|
||
/** @type {Record<BareAgentQvacProfileId, BareAgentQvacProfile>} */
|
||
const BARE_AGENT_QVAC_PROFILES = {
|
||
lite: {
|
||
id: 'lite',
|
||
label: 'Lite',
|
||
description:
|
||
'Smallest download (Qwen3-0.6B). Model-card context 32k; tools enabled.',
|
||
chatModel: 'QWEN3_600M_INST_Q4',
|
||
tools: true,
|
||
minRamGb: 4,
|
||
minDiskGb: 2,
|
||
approxDownloadGb: 0.5,
|
||
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
||
},
|
||
recommended: {
|
||
id: 'recommended',
|
||
label: 'Recommended',
|
||
description:
|
||
'Best balance for Bare OS agent tool calling (Qwen3-1.7B, model-card context 32k).',
|
||
chatModel: 'QWEN3_1_7B_INST_Q4',
|
||
tools: true,
|
||
minRamGb: 8,
|
||
minDiskGb: 5,
|
||
approxDownloadGb: 2.5,
|
||
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
||
},
|
||
strong: {
|
||
id: 'strong',
|
||
label: 'Strong',
|
||
description:
|
||
'Better reasoning (Qwen3-4B). Model-card context 32k (131k with YaRN not enabled).',
|
||
chatModel: 'QWEN3_4B_INST_Q4_K_M',
|
||
tools: true,
|
||
minRamGb: 16,
|
||
minDiskGb: 8,
|
||
approxDownloadGb: 3.5,
|
||
ctxSize: BARE_AGENT_QVAC_CTX_QWEN3
|
||
},
|
||
'tool-tiny': {
|
||
id: 'tool-tiny',
|
||
label: 'Tool-tiny',
|
||
description:
|
||
'Llama 3.2 1B tool-calling fallback (model-card context 128k).',
|
||
chatModel: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K',
|
||
tools: true,
|
||
minRamGb: 6,
|
||
minDiskGb: 3,
|
||
approxDownloadGb: 1,
|
||
ctxSize: BARE_AGENT_QVAC_CTX_LLAMA32_1B
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Model-card / train context for a QVAC registry id (fallback 32k).
|
||
* @param {string} [modelId]
|
||
* @returns {number}
|
||
*/
|
||
function bareAgentQvacModelCardCtxSize(modelId) {
|
||
const id = String(modelId || '')
|
||
.trim()
|
||
.toUpperCase()
|
||
if (!id) return BARE_AGENT_QVAC_CTX_QWEN3
|
||
if (id.includes('LLAMA') || id.includes('LLAMA_TOOL')) {
|
||
return BARE_AGENT_QVAC_CTX_LLAMA32_1B
|
||
}
|
||
if (id.includes('QWEN3')) return BARE_AGENT_QVAC_CTX_QWEN3
|
||
return BARE_AGENT_QVAC_CTX_QWEN3
|
||
}
|
||
|
||
/** @returns {BareAgentQvacProfile[]} */
|
||
function bareAgentQvacProfileList() {
|
||
return Object.values(BARE_AGENT_QVAC_PROFILES)
|
||
}
|
||
|
||
/**
|
||
* @param {string} [id]
|
||
* @returns {BareAgentQvacProfile}
|
||
*/
|
||
function bareAgentQvacGetProfile(id) {
|
||
const key = String(id || '').trim().toLowerCase()
|
||
return BARE_AGENT_QVAC_PROFILES[key] || BARE_AGENT_QVAC_PROFILES.recommended
|
||
}
|
||
|
||
/**
|
||
* Flatten OpenAI nested tool defs → QVAC flat shape.
|
||
* @param {unknown[]} tools
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentFlattenToolsForQvac(tools) {
|
||
if (!Array.isArray(tools)) return []
|
||
/** @type {unknown[]} */
|
||
const out = []
|
||
for (const t of tools) {
|
||
if (!t || typeof t !== 'object') continue
|
||
const o = /** @type {Record<string, unknown>} */ (t)
|
||
if (o.function && typeof o.function === 'object') {
|
||
const fn = /** @type {Record<string, unknown>} */ (o.function)
|
||
out.push({
|
||
type: 'function',
|
||
name: typeof fn.name === 'string' ? fn.name : '',
|
||
description: typeof fn.description === 'string' ? fn.description : '',
|
||
parameters:
|
||
fn.parameters && typeof fn.parameters === 'object'
|
||
? fn.parameters
|
||
: { type: 'object', properties: {} }
|
||
})
|
||
continue
|
||
}
|
||
if (typeof o.name === 'string') {
|
||
out.push({
|
||
type: 'function',
|
||
name: o.name,
|
||
description: typeof o.description === 'string' ? o.description : '',
|
||
parameters:
|
||
o.parameters && typeof o.parameters === 'object'
|
||
? o.parameters
|
||
: { type: 'object', properties: {} }
|
||
})
|
||
}
|
||
}
|
||
return out.filter((x) => x && typeof x === 'object' && /** @type {any} */ (x).name)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {boolean}
|
||
*/
|
||
function bareAgentQvacBridgeAvailable(ctx) {
|
||
if (!ctx || typeof ctx !== 'object') return false
|
||
if (typeof ctx.bareOsQvacAvailable === 'function') {
|
||
try {
|
||
return Boolean(ctx.bareOsQvacAvailable())
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
return typeof ctx.bareOsQvacComplete === 'function'
|
||
}
|
||
|
||
/**
|
||
* Normalize backend from config (qvac | rest).
|
||
* @param {Record<string, unknown>} config
|
||
* @returns {'qvac'|'rest'}
|
||
*/
|
||
function bareAgentResolveBackend(config) {
|
||
const b = String(config?.backend || '').trim().toLowerCase()
|
||
if (b === 'rest' || b === 'openai' || b === 'http') return 'rest'
|
||
if (b === 'qvac') return 'qvac'
|
||
const p = String(config?.provider || '').trim().toLowerCase()
|
||
if (p === 'qvac') return 'qvac'
|
||
if (p === 'groq' || p === 'xai' || p === 'openai' || p === 'custom') return 'rest'
|
||
// Default: qvac when key unset; rest when key present (legacy configs)
|
||
if (config?.rest_api_key && String(config.rest_api_key).trim()) return 'rest'
|
||
return 'qvac'
|
||
}
|
||
|
||
/** @type {readonly string[]} */
|
||
var BARE_AGENT_QVAC_ONLY_KEYS = Object.freeze([
|
||
'qvac_model',
|
||
'qvac_profile',
|
||
'qvac_ctx_size',
|
||
'qvac_device',
|
||
'qvac_main_gpu',
|
||
'qvac_gpu_layers'
|
||
])
|
||
|
||
/** @type {readonly string[]} */
|
||
var BARE_AGENT_REST_ONLY_KEYS = Object.freeze(['rest_base_url', 'rest_api_key'])
|
||
|
||
/**
|
||
* @typedef {{
|
||
* id: string,
|
||
* label: string,
|
||
* rest_base_url: string,
|
||
* default_model: string,
|
||
* models: string[]
|
||
* }} BareAgentRestProvider
|
||
*/
|
||
|
||
/** @type {Record<string, BareAgentRestProvider>} */
|
||
const BARE_AGENT_REST_PROVIDERS = {
|
||
groq: {
|
||
id: 'groq',
|
||
label: 'Groq',
|
||
rest_base_url: 'https://api.groq.com/openai/v1',
|
||
default_model: 'llama-3.3-70b-versatile',
|
||
models: [
|
||
'llama-3.3-70b-versatile',
|
||
'llama-3.1-8b-instant',
|
||
'openai/gpt-oss-120b',
|
||
'openai/gpt-oss-20b',
|
||
'qwen/qwen3-32b',
|
||
'moonshotai/kimi-k2-instruct'
|
||
]
|
||
},
|
||
xai: {
|
||
id: 'xai',
|
||
label: 'xAI (Grok)',
|
||
rest_base_url: 'https://api.x.ai/v1',
|
||
default_model: 'grok-4',
|
||
models: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212']
|
||
},
|
||
openai: {
|
||
id: 'openai',
|
||
label: 'OpenAI',
|
||
rest_base_url: 'https://api.openai.com/v1',
|
||
default_model: 'gpt-4.1',
|
||
models: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'o4-mini']
|
||
},
|
||
custom: {
|
||
id: 'custom',
|
||
label: 'Custom OpenAI-compatible',
|
||
rest_base_url: '',
|
||
default_model: '',
|
||
models: []
|
||
}
|
||
}
|
||
|
||
/** @returns {BareAgentRestProvider[]} */
|
||
function bareAgentRestProviderList() {
|
||
return [BARE_AGENT_REST_PROVIDERS.groq, BARE_AGENT_REST_PROVIDERS.xai, BARE_AGENT_REST_PROVIDERS.openai, BARE_AGENT_REST_PROVIDERS.custom]
|
||
}
|
||
|
||
/**
|
||
* @param {string} [id]
|
||
* @returns {BareAgentRestProvider}
|
||
*/
|
||
function bareAgentRestGetProvider(id) {
|
||
const key = String(id || '').trim().toLowerCase()
|
||
if (key === 'http' || key === 'rest') return BARE_AGENT_REST_PROVIDERS.custom
|
||
return BARE_AGENT_REST_PROVIDERS[key] || BARE_AGENT_REST_PROVIDERS.groq
|
||
}
|
||
|
||
/**
|
||
* @param {string} [model]
|
||
*/
|
||
function bareAgentIsQvacModelId(model) {
|
||
const m = String(model || '').trim()
|
||
if (!m) return false
|
||
return (
|
||
/^QWEN/i.test(m) ||
|
||
/^LLAMA_TOOL/i.test(m) ||
|
||
/QWEN3/i.test(m) ||
|
||
/LLAMA_TOOL_CALLING/i.test(m)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Drop keys that belong to the other backend so config.json matches the choice.
|
||
* @param {Record<string, unknown>} config
|
||
* @returns {Record<string, unknown>}
|
||
*/
|
||
function bareAgentSanitizeConfigForBackend(config) {
|
||
const out = { ...(config && typeof config === 'object' ? config : {}) }
|
||
const backend = bareAgentResolveBackend(out)
|
||
out.backend = backend
|
||
if (backend === 'rest') {
|
||
for (let i = 0; i < BARE_AGENT_QVAC_ONLY_KEYS.length; i++) {
|
||
delete out[BARE_AGENT_QVAC_ONLY_KEYS[i]]
|
||
}
|
||
const prov = String(out.provider || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!prov || prov === 'qvac') out.provider = 'groq'
|
||
if (bareAgentIsQvacModelId(String(out.model || ''))) {
|
||
const spec = bareAgentRestGetProvider(String(out.provider || 'groq'))
|
||
if (spec.default_model) out.model = spec.default_model
|
||
}
|
||
} else {
|
||
for (let i = 0; i < BARE_AGENT_REST_ONLY_KEYS.length; i++) {
|
||
delete out[BARE_AGENT_REST_ONLY_KEYS[i]]
|
||
}
|
||
out.provider = 'qvac'
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} [secret]
|
||
*/
|
||
function bareAgentMaskSecretPreview(secret) {
|
||
const t = String(secret || '')
|
||
if (!t.trim()) return '(not set)'
|
||
if (t.length <= 8) return '********'
|
||
return t.slice(0, 3) + '…' + t.slice(-4)
|
||
}
|
||
|
||
/**
|
||
* Resolve context window for QVAC load from the model card / profile.
|
||
* Legacy undersized `qvac_ctx_size` values (e.g. 4096/8192) are ignored so
|
||
* setup stays automatic; only overrides ≥ the profile default apply.
|
||
* Host may still cap via `BARE_OS_QVAC_MAX_CTX`.
|
||
* @param {Record<string, unknown>} config
|
||
* @param {BareAgentQvacProfile} profile
|
||
*/
|
||
function bareAgentQvacResolveCtxSize(config, profile) {
|
||
const modelId = String(
|
||
(config && (config.qvac_model || config.model)) ||
|
||
(profile && profile.chatModel) ||
|
||
''
|
||
)
|
||
const cardCtx = bareAgentQvacModelCardCtxSize(modelId)
|
||
const profileCtx = Math.max(
|
||
2048,
|
||
Number(profile && profile.ctxSize) || cardCtx
|
||
)
|
||
let ctx = Math.min(
|
||
BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX,
|
||
Math.max(profileCtx, cardCtx)
|
||
)
|
||
const raw = Number(config && config.qvac_ctx_size)
|
||
if (Number.isFinite(raw) && raw >= profileCtx) {
|
||
ctx = Math.min(BARE_AGENT_QVAC_CTX_ABSOLUTE_MAX, Math.floor(raw))
|
||
}
|
||
// Full Bare OS tool list ≈ 4.5k tokens; leave room for system + reply.
|
||
// Tools are always on for agent sessions — keep enough ctx for schemas.
|
||
if (ctx < 8192) ctx = 8192
|
||
return ctx
|
||
}
|
||
|
||
/**
|
||
* Device / main-gpu / layers for QVAC load (config + profile defaults).
|
||
* @param {Record<string, unknown>} config
|
||
* @returns {{ device?: string, mainGpu?: string | number, gpuLayers?: number }}
|
||
*/
|
||
function bareAgentQvacResolveDeviceOpts(config) {
|
||
/** @type {{ device?: string, mainGpu?: string | number, gpuLayers?: number }} */
|
||
const out = {}
|
||
const device = String(config && config.qvac_device ? config.qvac_device : '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (device === 'cpu' || device === 'gpu') out.device = device
|
||
|
||
const mainRaw = config && config.qvac_main_gpu
|
||
if (mainRaw !== undefined && mainRaw !== null && String(mainRaw).trim() !== '') {
|
||
const s = String(mainRaw).trim().toLowerCase()
|
||
if (s === 'auto' || s === 'dedicated' || s === 'integrated') out.mainGpu = s
|
||
else if (/^\d+$/.test(s)) out.mainGpu = Number.parseInt(s, 10)
|
||
} else {
|
||
out.mainGpu = 'auto'
|
||
}
|
||
|
||
const layers = Number(config && config.qvac_gpu_layers)
|
||
if (Number.isFinite(layers) && layers >= 0) out.gpuLayers = Math.floor(layers)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Shared QVAC chat catalog + REST /models helpers (preamble for agent and discord-bot).
|
||
*/
|
||
|
||
/** @type {{ id: string, family: string, label: string, tools: boolean, ramGb: number, profile?: string }[]} */
|
||
var BARE_AGENT_QVAC_CHAT_MODELS = [
|
||
{ id: 'QWEN3_600M_INST_Q4', family: 'qwen3', label: 'Qwen3 0.6B Instruct Q4', tools: true, ramGb: 4, profile: 'lite' },
|
||
{ id: 'QWEN3_1_7B_INST_Q4', family: 'qwen3', label: 'Qwen3 1.7B Instruct Q4', tools: true, ramGb: 8, profile: 'recommended' },
|
||
{ id: 'QWEN3_4B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Instruct Q4_K_M', tools: true, ramGb: 16, profile: 'strong' },
|
||
{ id: 'QWEN3_4B_Q4_K_M', family: 'qwen3', label: 'Qwen3 4B Q4_K_M', tools: true, ramGb: 16 },
|
||
{ id: 'QWEN3_8B_INST_Q4_K_M', family: 'qwen3', label: 'Qwen3 8B Instruct Q4_K_M', tools: true, ramGb: 24 },
|
||
{ id: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K', family: 'llama', label: 'Llama 3.2 1B tool-calling', tools: true, ramGb: 6, profile: 'tool-tiny' },
|
||
{ id: 'LLAMA_3_2_1B_INST_Q4_0', family: 'llama', label: 'Llama 3.2 1B Instruct Q4_0', tools: true, ramGb: 6 },
|
||
{ id: 'SMOLLM2_360M_INST_Q8', family: 'smol', label: 'SmolLM2 360M Instruct Q8', tools: false, ramGb: 3 },
|
||
{ id: 'GPT_OSS_20B_INST_Q4_K_M', family: 'gpt-oss', label: 'GPT-OSS 20B Instruct Q4_K_M', tools: true, ramGb: 24 },
|
||
{ id: 'GEMMA4_2B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 2B multimodal Q4', tools: true, ramGb: 8 },
|
||
{ id: 'GEMMA4_2B_MULTIMODAL_Q6_K', family: 'gemma', label: 'Gemma 4 2B multimodal Q6', tools: true, ramGb: 10 },
|
||
{ id: 'GEMMA4_4B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 4B multimodal Q4', tools: true, ramGb: 16 },
|
||
{ id: 'GEMMA4_31B_MULTIMODAL_Q4_K_M', family: 'gemma', label: 'Gemma 4 31B multimodal Q4', tools: true, ramGb: 48 },
|
||
{ id: 'QWEN3VL_2B_MULTIMODAL_Q4_K', family: 'qwen3', label: 'Qwen3-VL 2B multimodal Q4', tools: true, ramGb: 10 },
|
||
{ id: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 2B multimodal Q4', tools: true, ramGb: 10 },
|
||
{ id: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 4B multimodal Q4', tools: true, ramGb: 16 },
|
||
{ id: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 8B multimodal Q4', tools: true, ramGb: 24 },
|
||
{ id: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M', family: 'qwen3.5', label: 'Qwen3.5 9B multimodal Q4', tools: true, ramGb: 28 },
|
||
{ id: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL', family: 'large', label: 'Qwen3.6 27B multimodal Q4', tools: true, ramGb: 48 }
|
||
]
|
||
|
||
/** @type {Record<string, string[]>} */
|
||
var BARE_AGENT_REST_MODEL_FALLBACKS = {
|
||
groq: [
|
||
'llama-3.3-70b-versatile',
|
||
'llama-3.1-8b-instant',
|
||
'openai/gpt-oss-120b',
|
||
'openai/gpt-oss-20b',
|
||
'qwen/qwen3-32b',
|
||
'moonshotai/kimi-k2-instruct',
|
||
'meta-llama/llama-4-scout-17b-16e-instruct',
|
||
'meta-llama/llama-4-maverick-17b-128e-instruct',
|
||
'groq/compound'
|
||
],
|
||
xai: ['grok-4', 'grok-3', 'grok-3-mini', 'grok-3-fast', 'grok-2-1212', 'grok-2-vision-1212'],
|
||
openai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3'],
|
||
custom: []
|
||
}
|
||
|
||
/**
|
||
* @param {string} [profileId]
|
||
*/
|
||
function bareAgentQvacModelForProfile(profileId) {
|
||
const id = String(profileId || '').trim().toLowerCase()
|
||
for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) {
|
||
if (BARE_AGENT_QVAC_CHAT_MODELS[i].profile === id) return BARE_AGENT_QVAC_CHAT_MODELS[i]
|
||
}
|
||
return BARE_AGENT_QVAC_CHAT_MODELS[1] || BARE_AGENT_QVAC_CHAT_MODELS[0]
|
||
}
|
||
|
||
/**
|
||
* @param {string} [modelId]
|
||
*/
|
||
function bareAgentQvacFindChatModel(modelId) {
|
||
const id = String(modelId || '').trim()
|
||
for (let i = 0; i < BARE_AGENT_QVAC_CHAT_MODELS.length; i++) {
|
||
if (BARE_AGENT_QVAC_CHAT_MODELS[i].id === id) return BARE_AGENT_QVAC_CHAT_MODELS[i]
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {{ id: string, label?: string, family?: string }[]} models
|
||
* @param {{ family?: string, query?: string }} [opts]
|
||
*/
|
||
function bareAgentFilterModelList(models, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const fam = String(o.family || 'all').trim().toLowerCase()
|
||
const q = String(o.query || '').trim().toLowerCase()
|
||
const rows = Array.isArray(models) ? models : []
|
||
/** @type {{ id: string, label?: string, family?: string }[]} */
|
||
const out = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const m = rows[i]
|
||
if (!m || !m.id) continue
|
||
if (fam && fam !== 'all' && String(m.family || '').toLowerCase() !== fam) continue
|
||
if (q) {
|
||
const blob = (String(m.id) + ' ' + String(m.label || '')).toLowerCase()
|
||
if (blob.indexOf(q) === -1) continue
|
||
}
|
||
out.push(m)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} json
|
||
* @returns {{ id: string, label: string, family: string, owned_by: string }[]}
|
||
*/
|
||
function bareAgentParseOpenAiModels(json) {
|
||
const raw =
|
||
json && typeof json === 'object' && Array.isArray(/** @type {{ data?: unknown }} */ (json).data)
|
||
? /** @type {{ data: unknown[] }} */ (json).data
|
||
: Array.isArray(json)
|
||
? json
|
||
: []
|
||
const skip = /embed|whisper|tts|dall-e|davinci|babbage|audio|moderation|realtime|image|sora/i
|
||
/** @type {{ id: string, label: string, family: string, owned_by: string }[]} */
|
||
const out = []
|
||
const seen = Object.create(null)
|
||
for (let i = 0; i < raw.length; i++) {
|
||
const row = raw[i] && typeof raw[i] === 'object' ? /** @type {Record<string, unknown>} */ (raw[i]) : null
|
||
if (!row) continue
|
||
const id = String(row.id || row.name || '').trim()
|
||
if (!id || seen[id] || skip.test(id)) continue
|
||
seen[id] = 1
|
||
const owned = String(row.owned_by || row.ownedBy || '')
|
||
out.push({
|
||
id: id,
|
||
label: id,
|
||
family: owned || 'api',
|
||
owned_by: owned
|
||
})
|
||
}
|
||
out.sort(function (a, b) {
|
||
return a.id.localeCompare(b.id)
|
||
})
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} [provider]
|
||
*/
|
||
function bareAgentRestModelsFallback(provider) {
|
||
const key = String(provider || 'groq').trim().toLowerCase()
|
||
const ids = BARE_AGENT_REST_MODEL_FALLBACKS[key] || BARE_AGENT_REST_MODEL_FALLBACKS.groq
|
||
return (ids || []).map(function (id) {
|
||
return { id: id, label: id, family: key, owned_by: key }
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param {(url: string, init?: object) => Promise<{ ok?: boolean, status?: number, json?: () => Promise<unknown>, text?: () => Promise<string> }>} fetchFn
|
||
* @param {{ baseUrl?: string, apiKey?: string }} opts
|
||
*/
|
||
async function bareAgentFetchRestModels(fetchFn, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const base = String(o.baseUrl || '').trim().replace(/\/+$/, '')
|
||
if (!base) throw new Error('rest_base_url required')
|
||
if (typeof fetchFn !== 'function') throw new Error('fetch unavailable')
|
||
const url = base + '/models'
|
||
/** @type {Record<string, string>} */
|
||
const headers = { Accept: 'application/json' }
|
||
const key = String(o.apiKey || '').trim()
|
||
if (key) headers.Authorization = 'Bearer ' + key
|
||
const res = await fetchFn(url, { method: 'GET', headers: headers })
|
||
if (!res || res.ok === false) {
|
||
const st = res && res.status != null ? String(res.status) : 'fetch_failed'
|
||
throw new Error('models_http_' + st)
|
||
}
|
||
let json
|
||
if (typeof res.json === 'function') json = await res.json()
|
||
else if (typeof res.text === 'function') json = JSON.parse(await res.text())
|
||
else throw new Error('models_unreadable')
|
||
const list = bareAgentParseOpenAiModels(json)
|
||
if (!list.length) throw new Error('models_empty')
|
||
return list
|
||
}
|
||
|
||
/**
|
||
* Agent Markdown workspace at ~/.agent/workspace — soul files + optional daily memory.
|
||
* Defaults ship on the system image at /share/agent-workspace/ and are seeded
|
||
* into the personal drive when SOUL.md is missing (portable across peers via Hyperdrive).
|
||
*/
|
||
|
||
/** @type {readonly string[]} Canonical Markdown load order */
|
||
var BARE_AGENT_WORKSPACE_FILES = Object.freeze([
|
||
'SOUL.md',
|
||
'AGENTS.md',
|
||
'IDENTITY.md',
|
||
'USER.md',
|
||
'TOOLS.md',
|
||
'MEMORY.md',
|
||
'BOOTSTRAP.md',
|
||
'HEARTBEAT.md',
|
||
'PROMPT.md'
|
||
])
|
||
|
||
/** System-drive templates (kernel share) */
|
||
var BARE_AGENT_WORKSPACE_SHARE = '/share/agent-workspace'
|
||
|
||
/** Relative paths under workspace/ and share root for skill templates */
|
||
var BARE_AGENT_SKILL_SEED_REL = Object.freeze([
|
||
'skills/.gitkeep',
|
||
'skills/p2p-os-status/SKILL.md',
|
||
'skills/bare-os-kernel-proc/SKILL.md',
|
||
'skills/bare-os-super-developer/SKILL.md',
|
||
'skills/agent-ops/SKILL.md',
|
||
'skills/xai-compat/SKILL.md',
|
||
'skills/holesail/SKILL.md',
|
||
'skills/hdms/SKILL.md',
|
||
'skills/bareos-code-change/SKILL.md',
|
||
'skills/hyperdrive-replication/SKILL.md',
|
||
'skills/protomux-channel/SKILL.md',
|
||
'skills/ctx-api-change/SKILL.md',
|
||
'skills/proc-node-change/SKILL.md',
|
||
'skills/seed-rpc-change/SKILL.md',
|
||
'skills/coreutils-command-change/SKILL.md',
|
||
'skills/shell-grammar-change/SKILL.md',
|
||
'skills/docs-contract-update/SKILL.md',
|
||
'skills/kernel-program-extension/SKILL.md',
|
||
'skills/appstore/SKILL.md',
|
||
'skills/pear-dev/SKILL.md',
|
||
'skills/pear-runtime-debug/SKILL.md',
|
||
'skills/holepunch-local-mirror/SKILL.md'
|
||
])
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Uint8Array} buf
|
||
*/
|
||
function bareAgentWorkspaceDecode(ctx, buf) {
|
||
if (!buf || !buf.length) return ''
|
||
if (
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
)
|
||
return ctx.b4a.toString(buf)
|
||
return String(new TextDecoder().decode(buf))
|
||
}
|
||
|
||
/**
|
||
* @returns {string} UTC YYYY-MM-DD
|
||
*/
|
||
function bareAgentWorkspaceUtcYmd() {
|
||
const d = new Date()
|
||
const y = d.getUTCFullYear()
|
||
const m = d.getUTCMonth() + 1
|
||
const day = d.getUTCDate()
|
||
const pad = (n) => (n < 10 ? '0' : '') + n
|
||
return y + '-' + pad(m) + '-' + pad(day)
|
||
}
|
||
|
||
/**
|
||
* Seed ~/.agent/workspace from /share/agent-workspace when SOUL.md is absent.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ dir: string, workspace: string, workspaceMemory: string, workspaceSkills: string }} paths
|
||
*/
|
||
async function bareAgentEnsureWorkspace(ctx, paths) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.mkdir !== 'function' || typeof vfs.readFile !== 'function')
|
||
return
|
||
if (typeof vfs.writeFile !== 'function') return
|
||
try {
|
||
const b = await vfs.readFile(paths.workspace + '/SOUL.md')
|
||
if (b && b.length) return
|
||
} catch {
|
||
/* missing — seed */
|
||
}
|
||
try {
|
||
await vfs.mkdir(paths.workspace, { recursive: true })
|
||
await vfs.mkdir(paths.workspaceMemory, { recursive: true })
|
||
} catch {
|
||
return
|
||
}
|
||
const share = BARE_AGENT_WORKSPACE_SHARE
|
||
for (const name of BARE_AGENT_WORKSPACE_FILES) {
|
||
try {
|
||
const buf = await vfs.readFile(share + '/' + name)
|
||
await vfs.writeFile(paths.workspace + '/' + name, buf)
|
||
} catch {
|
||
/* template missing on image — skip */
|
||
}
|
||
}
|
||
try {
|
||
const gk = await vfs.readFile(share + '/memory/.gitkeep')
|
||
await vfs.writeFile(paths.workspaceMemory + '/.gitkeep', gk)
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
/** @type {[string, string][]} */
|
||
const stubs = [
|
||
['loader.stub.js', paths.dir + '/loader.js'],
|
||
['index.stub.js', paths.dir + '/index.js'],
|
||
['skill-loader.stub.js', paths.dir + '/skill-loader.js'],
|
||
['README-agent.md', paths.dir + '/README-agent.md']
|
||
]
|
||
for (const [srcName, dest] of stubs) {
|
||
try {
|
||
const buf = await vfs.readFile(share + '/' + srcName)
|
||
await vfs.writeFile(dest, buf)
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ensure workspace/skills templates and ~/.agent/skill-loader.js exist (idempotent; for upgrades).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspace: string, workspaceSkills: string, dir: string }} paths
|
||
* @param {Record<string, unknown>} [config]
|
||
*/
|
||
async function bareAgentEnsureSkillTemplates(ctx, paths, config) {
|
||
const vfs = ctx.vfs
|
||
if (
|
||
!vfs ||
|
||
typeof vfs.readFile !== 'function' ||
|
||
typeof vfs.writeFile !== 'function' ||
|
||
typeof vfs.mkdir !== 'function'
|
||
)
|
||
return
|
||
const provider =
|
||
config && typeof config === 'object' ? String(config.provider || '').trim().toLowerCase() : ''
|
||
try {
|
||
await vfs.mkdir(paths.workspaceSkills, { recursive: true })
|
||
} catch {
|
||
return
|
||
}
|
||
const share = BARE_AGENT_WORKSPACE_SHARE
|
||
for (const rel of BARE_AGENT_SKILL_SEED_REL) {
|
||
if (rel === 'skills/xai-compat/SKILL.md' && provider !== 'xai') {
|
||
if (typeof vfs.unlink === 'function') {
|
||
try {
|
||
await vfs.unlink(paths.workspace + '/' + rel)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
const dest = paths.workspace + '/' + rel
|
||
try {
|
||
const b = await vfs.readFile(dest)
|
||
if (b && b.length) continue
|
||
} catch {
|
||
/* missing — copy */
|
||
}
|
||
try {
|
||
const buf = await vfs.readFile(share + '/' + rel)
|
||
const parent = dest.replace(/\/[^/]+$/, '')
|
||
await vfs.mkdir(parent, { recursive: true })
|
||
await vfs.writeFile(dest, buf)
|
||
} catch {
|
||
/* template missing on image */
|
||
}
|
||
}
|
||
try {
|
||
await vfs.readFile(paths.dir + '/skill-loader.js')
|
||
} catch {
|
||
try {
|
||
const buf = await vfs.readFile(share + '/skill-loader.stub.js')
|
||
await vfs.writeFile(paths.dir + '/skill-loader.js', buf)
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} text
|
||
*/
|
||
function bareAgentWorkspaceEncode(ctx, text) {
|
||
const s = String(text)
|
||
if (
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.from === 'function'
|
||
)
|
||
return ctx.b4a.from(s)
|
||
return new TextEncoder().encode(s)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
function bareAgentIdentityMarkdownForConfig(config) {
|
||
const label = String(config.agent_label || '').trim() || 'BareAgent'
|
||
const owner = String(config.owner_name || '').trim()
|
||
const role = owner
|
||
? 'Decentralized OS intelligence for **' +
|
||
owner +
|
||
'** · upstream [bare-operating-system](https://git.ssh.surf/snxraven/bare-operating-system).'
|
||
: 'Decentralized OS Intelligence for snxraven\'s bare-operating-system'
|
||
return (
|
||
'# IDENTITY.md\n\n' +
|
||
'**Name:** ' +
|
||
label +
|
||
'\n' +
|
||
'**Role:** ' +
|
||
role +
|
||
'\n' +
|
||
'**Emoji:** 🦾\n' +
|
||
'**Version:** 0.1\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
function bareAgentUserMarkdownForConfig(config) {
|
||
const owner = String(config.owner_name || '').trim()
|
||
const opLine = owner
|
||
? '- **Operator (this Hyperdrive):** ' + owner + '\n'
|
||
: '- **Operator:** (set `owner_name` via `agent --config` or `edit_agent_config`)\n'
|
||
return (
|
||
'# USER.md - About the Owner\n\n' +
|
||
opLine +
|
||
'- **Upstream maintainer (repo):** snxraven\n' +
|
||
'- **Location:** Atlanta, Georgia, US\n' +
|
||
'- **Expertise:** P2P systems, Bare runtime, Hyperdrive, decentralized identity, POSIX-in-JS\n' +
|
||
'- **Preferences:** Concise technical answers, bullet points, no corporate speak, direct honesty\n' +
|
||
'- **Permissions:** Full access to system drive and personal Hyperdrive within tool policy\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Rewrite IDENTITY.md / USER.md from ~/.agent/config.json (owner_name, agent_label).
|
||
* Call after seeding workspace or when those keys change.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspace: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSyncWorkspaceFromConfig(ctx, paths, config) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function')
|
||
return
|
||
const label = String(config.agent_label || '').trim()
|
||
const owner = String(config.owner_name || '').trim()
|
||
if (!label && !owner) return
|
||
try {
|
||
await vfs.mkdir(paths.workspace, { recursive: true })
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
const idMd = bareAgentIdentityMarkdownForConfig(config)
|
||
await vfs.writeFile(
|
||
paths.workspace + '/IDENTITY.md',
|
||
bareAgentWorkspaceEncode(ctx, idMd)
|
||
)
|
||
const userMd = bareAgentUserMarkdownForConfig(config)
|
||
await vfs.writeFile(paths.workspace + '/USER.md', bareAgentWorkspaceEncode(ctx, userMd))
|
||
} catch {
|
||
/* ignore — best-effort */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Build concatenated system prompt block (Markdown) from workspace files.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspace: string, workspaceMemory: string }} paths
|
||
* @param {number} [maxChars]
|
||
*/
|
||
async function bareAgentLoadWorkspacePrompt(ctx, paths, maxChars) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return ''
|
||
const cap = Math.min(Math.max(Number(maxChars) || 24000, 4000), 64000)
|
||
let out = '# Agent workspace (~/.agent/workspace)\n\n'
|
||
for (const name of BARE_AGENT_WORKSPACE_FILES) {
|
||
try {
|
||
const buf = await vfs.readFile(paths.workspace + '/' + name)
|
||
const t = bareAgentWorkspaceDecode(ctx, buf).trim()
|
||
out += '=== ' + name + ' ===\n' + (t || '(empty)') + '\n\n'
|
||
} catch {
|
||
out += '=== ' + name + ' ===\n(File not found)\n\n'
|
||
}
|
||
}
|
||
const day = bareAgentWorkspaceUtcYmd()
|
||
try {
|
||
const buf = await vfs.readFile(paths.workspaceMemory + '/' + day + '.md')
|
||
const t = bareAgentWorkspaceDecode(ctx, buf).trim()
|
||
if (t) out += '=== memory/' + day + '.md ===\n' + t + '\n\n'
|
||
} catch {
|
||
/* no daily log */
|
||
}
|
||
if (out.length > cap) out = out.slice(0, cap) + '\n… truncated\n'
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Agent skills: discover SKILL.md under workspace/skills/ (highest precedence) then ~/.agent/skills/.
|
||
* Depends on bareAgentWorkspaceDecode from agent-workspace.js (same preamble order).
|
||
*/
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @returns {{ front: Record<string, string>, body: string }}
|
||
*/
|
||
function bareAgentParseSkillFrontmatter(text) {
|
||
const t = String(text || '')
|
||
if (!t.startsWith('---')) return { front: {}, body: t.trim() }
|
||
const nl = t.indexOf('\n')
|
||
const afterFirst = nl === -1 ? '' : t.slice(nl + 1)
|
||
const end = afterFirst.search(/\n---\s*(?:\n|$)/)
|
||
if (end === -1) return { front: {}, body: t.trim() }
|
||
const yamlBlock = afterFirst.slice(0, end)
|
||
const body = afterFirst.slice(end + 1).replace(/^---\s*/, '').replace(/^\r?\n/, '')
|
||
/** @type {Record<string, string>} */
|
||
const front = {}
|
||
for (const line of yamlBlock.split(/\r?\n/)) {
|
||
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
|
||
if (m) front[m[1]] = m[2].trim()
|
||
}
|
||
return { front, body: body.trim() }
|
||
}
|
||
|
||
/**
|
||
* @param {string} full SKILL.md text
|
||
* @param {string} folderName directory basename
|
||
*/
|
||
function bareAgentSkillMetaFromMarkdown(full, folderName) {
|
||
const { front } = bareAgentParseSkillFrontmatter(full)
|
||
const name = (front.name || folderName || 'unnamed').trim() || folderName
|
||
const descFromFront =
|
||
front.description && String(front.description).trim()
|
||
? String(front.description).trim()
|
||
: ''
|
||
const descFromBody =
|
||
full
|
||
.split(/\r?\n/)
|
||
.find((l) => {
|
||
const x = l.trim()
|
||
return x && !x.startsWith('---') && !x.startsWith('#')
|
||
})
|
||
?.trim() || ''
|
||
const description = (descFromFront || descFromBody || 'Skill').slice(0, 400)
|
||
return { name, description }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
|
||
* @returns {Promise<{ id: string, name: string, description: string, path: string, source: string }[]>}
|
||
*/
|
||
async function bareAgentDiscoverSkills(ctx, paths) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function')
|
||
return []
|
||
/** @type {{ id: string, name: string, description: string, path: string, source: string }[]} */
|
||
const out = []
|
||
const seen = new Set()
|
||
/**
|
||
* @param {string} root
|
||
* @param {string} source
|
||
*/
|
||
async function scanRoot(root, source) {
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(root)
|
||
} catch {
|
||
return
|
||
}
|
||
if (!Array.isArray(names)) return
|
||
for (const raw of names) {
|
||
const entry = String(raw)
|
||
if (!entry || entry.startsWith('.')) continue
|
||
const skillMd = root.replace(/\/+$/, '') + '/' + entry + '/SKILL.md'
|
||
try {
|
||
const buf = await vfs.readFile(skillMd)
|
||
if (!buf || !buf.length) continue
|
||
const full = bareAgentWorkspaceDecode(ctx, buf)
|
||
const meta = bareAgentSkillMetaFromMarkdown(full, entry)
|
||
const keys = [entry.toLowerCase(), meta.name.toLowerCase()]
|
||
let dup = false
|
||
for (const k of keys) {
|
||
if (seen.has(k)) dup = true
|
||
}
|
||
if (dup) continue
|
||
for (const k of keys) seen.add(k)
|
||
out.push({
|
||
id: entry,
|
||
name: meta.name,
|
||
description: meta.description,
|
||
path: skillMd,
|
||
source
|
||
})
|
||
} catch {
|
||
/* not a skill dir */
|
||
}
|
||
}
|
||
}
|
||
await scanRoot(paths.workspaceSkills, 'workspace')
|
||
await scanRoot(paths.skillsGlobal, 'global')
|
||
let extras = Array.isArray(paths.extraSkillRoots) ? paths.extraSkillRoots : []
|
||
if (!extras.length && typeof bareAgentDiscoverProjectSkillRoots === 'function') {
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const start = String(env.PWD || env.CWD || env.HOME || '').trim()
|
||
if (start) extras = await bareAgentDiscoverProjectSkillRoots(ctx, start)
|
||
}
|
||
for (let i = 0; i < extras.length; i++) {
|
||
const item = extras[i]
|
||
const root = typeof item === 'string' ? item : String((item && item.path) || '')
|
||
const source =
|
||
typeof item === 'object' && item && item.source ? String(item.source) : 'project'
|
||
if (root) await scanRoot(root, source)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Compact Markdown block for system prompt (names + short descriptions only).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
|
||
* @param {number} [maxChars]
|
||
*/
|
||
async function bareAgentSkillsCompactPrompt(ctx, paths, maxChars) {
|
||
const cap = Math.min(Math.max(Number(maxChars) || 4000, 500), 12000)
|
||
const skills = await bareAgentDiscoverSkills(ctx, paths)
|
||
let block =
|
||
'## Available skills (compact index)\n' +
|
||
'Each skill is a directory with **SKILL.md** (optional YAML frontmatter: `name`, `description`, …).\n' +
|
||
'**Workspace skills** (`~/.agent/workspace/skills/`) override **global** (`~/.agent/skills/`) and walk-up `.grok/skills` / `.agents/skills` when names match.\n' +
|
||
'To run one: call the **read_skill** tool with the skill id or frontmatter `name` before following its instructions.\n\n'
|
||
if (!skills.length) {
|
||
block += '(No skills discovered yet — add folders under `workspace/skills/<id>/SKILL.md`.)\n'
|
||
return block.length > cap ? block.slice(0, cap) + '\n…\n' : block
|
||
}
|
||
block += '| id | name | source | description |\n| --- | --- | --- | --- |\n'
|
||
for (const s of skills) {
|
||
const desc = s.description.replace(/\|/g, '/').replace(/\r?\n/g, ' ').slice(0, 160)
|
||
block +=
|
||
'| `' +
|
||
s.id.replace(/`/g, "'") +
|
||
'` | ' +
|
||
s.name.replace(/\|/g, '/').replace(/\r?\n/g, ' ') +
|
||
' | ' +
|
||
s.source +
|
||
' | ' +
|
||
desc +
|
||
' |\n'
|
||
}
|
||
if (block.length > cap) block = block.slice(0, cap) + '\n… truncated\n'
|
||
return block
|
||
}
|
||
|
||
/**
|
||
* Load full SKILL.md for a skill matched by folder id or frontmatter name (case-insensitive).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{ workspaceSkills: string, skillsGlobal: string }} paths
|
||
* @param {string} skillQuery
|
||
*/
|
||
async function bareAgentLoadSkillMarkdown(ctx, paths, skillQuery) {
|
||
const q = String(skillQuery || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!q) return { ok: false, error: 'empty_skill', content: '', path: '' }
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function')
|
||
return { ok: false, error: 'vfs unavailable', content: '', path: '' }
|
||
const skills = await bareAgentDiscoverSkills(ctx, paths)
|
||
const hit =
|
||
skills.find((s) => s.id.toLowerCase() === q) ||
|
||
skills.find((s) => s.name.toLowerCase() === q)
|
||
if (!hit) return { ok: false, error: 'skill_not_found', content: '', path: '' }
|
||
try {
|
||
const buf = await vfs.readFile(hit.path)
|
||
if (!buf || !buf.length)
|
||
return { ok: false, error: 'empty_file', content: '', path: hit.path }
|
||
const t = bareAgentWorkspaceDecode(ctx, buf)
|
||
return { ok: true, skill: hit.name, id: hit.id, path: hit.path, source: hit.source, content: t }
|
||
} catch (e) {
|
||
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, error: msg, content: '', path: hit.path }
|
||
}
|
||
}
|
||
|
||
/** SSE line split + data payload parse (shared by agent-openai + tests). */
|
||
|
||
/**
|
||
* Parse one SSE `data:` JSON line after the `data: ` prefix.
|
||
* @param {string} dataLine content after `data: ` prefix
|
||
*/
|
||
function bareAgentParseSseDataPayload(dataLine) {
|
||
const t = String(dataLine).trim()
|
||
if (t === '[DONE]') return { kind: 'done' }
|
||
try {
|
||
const j = JSON.parse(t)
|
||
return { kind: 'json', value: j }
|
||
} catch {
|
||
return { kind: 'raw', value: t }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Split SSE buffer into lines (keep incomplete tail).
|
||
* @param {string} buf
|
||
* @returns {{ lines: string[], rest: string }}
|
||
*/
|
||
function bareAgentSplitSseLines(buf) {
|
||
const lines = []
|
||
let start = 0
|
||
for (let i = 0; i < buf.length; i++) {
|
||
if (buf.charCodeAt(i) === 10) {
|
||
lines.push(buf.slice(start, i))
|
||
start = i + 1
|
||
}
|
||
}
|
||
return { lines, rest: buf.slice(start) }
|
||
}
|
||
|
||
/** OpenAI-compatible chat/completions HTTP + SSE (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {typeof fetch | null}
|
||
*/
|
||
function bareAgentResolveFetch(ctx) {
|
||
if (typeof ctx.httpFetch === 'function')
|
||
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
|
||
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
|
||
let f =
|
||
bare && typeof bare.fetch === 'function'
|
||
? bare.fetch
|
||
: bare &&
|
||
bare.default &&
|
||
typeof bare.default === 'object' &&
|
||
typeof bare.default.fetch === 'function'
|
||
? bare.default.fetch
|
||
: null
|
||
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
|
||
if (typeof globalThis.fetch === 'function')
|
||
return globalThis.fetch.bind(globalThis)
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {string} base
|
||
*/
|
||
function bareAgentNormalizeBaseUrl(base) {
|
||
let s = String(base || '').trim()
|
||
while (s.endsWith('/')) s = s.slice(0, -1)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} obj
|
||
* @param {string} key
|
||
*/
|
||
function bareAgentDeepGet(obj, key) {
|
||
const parts = key.split('.')
|
||
let cur = obj
|
||
for (const p of parts) {
|
||
if (cur == null || typeof cur !== 'object') return undefined
|
||
cur = /** @type {Record<string, unknown>} */ (cur)[p]
|
||
}
|
||
return cur
|
||
}
|
||
|
||
/**
|
||
* Stream chat/completions; invoke onEvent for each parsed chunk.
|
||
* @param {{
|
||
* fetchFn: typeof fetch,
|
||
* url: string,
|
||
* headers: Record<string, string>,
|
||
* body: Record<string, unknown>,
|
||
* signal?: AbortSignal | null,
|
||
* onEvent: (ev: Record<string, unknown>) => void
|
||
* }} opts
|
||
*/
|
||
async function bareAgentStreamChatCompletions(opts) {
|
||
const { fetchFn, url, headers, body, signal, onEvent } = opts
|
||
const res = await fetchFn(url, {
|
||
method: 'POST',
|
||
headers,
|
||
body: JSON.stringify(body),
|
||
signal: signal || undefined
|
||
})
|
||
if (!res.ok) {
|
||
let errText = ''
|
||
try {
|
||
errText = await res.text()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
||
}
|
||
const stream = res.body
|
||
if (!stream || typeof stream.getReader !== 'function') {
|
||
throw new Error('agent: response body is not a readable stream')
|
||
}
|
||
const reader = stream.getReader()
|
||
const dec = new TextDecoder()
|
||
let buf = ''
|
||
let emittedShape = false
|
||
try {
|
||
for (;;) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
buf += dec.decode(value, { stream: true })
|
||
const sp = bareAgentSplitSseLines(buf)
|
||
buf = sp.rest
|
||
for (const line of sp.lines) {
|
||
if (!line.trim()) continue
|
||
if (line.startsWith(':')) continue
|
||
if (!line.startsWith('data:')) continue
|
||
const payload = line.slice(5).replace(/^\s/, '')
|
||
const parsed = bareAgentParseSseDataPayload(payload)
|
||
if (parsed.kind === 'done') {
|
||
onEvent({ type: 'sse_done' })
|
||
continue
|
||
}
|
||
if (parsed.kind !== 'json' || !parsed.value || typeof parsed.value !== 'object')
|
||
continue
|
||
const j = /** @type {Record<string, unknown>} */ (parsed.value)
|
||
if (!emittedShape) {
|
||
emittedShape = true
|
||
onEvent({
|
||
type: 'response_shape_keys',
|
||
keys: Object.keys(j).slice(0, 24)
|
||
})
|
||
}
|
||
if (typeof j.type === 'string') {
|
||
if (j.type === 'response.reasoning_summary_text.delta' && typeof j.delta === 'string') {
|
||
onEvent({ type: 'delta_reasoning', reasoning: j.delta })
|
||
}
|
||
if (j.type === 'response.output_text.delta' && typeof j.delta === 'string') {
|
||
onEvent({ type: 'delta_content', content: j.delta })
|
||
}
|
||
if (
|
||
j.type === 'response.function_call_arguments.delta' &&
|
||
typeof j.delta === 'string'
|
||
) {
|
||
onEvent({
|
||
type: 'delta_tool_calls',
|
||
tool_calls: [{ index: 0, function: { arguments: j.delta } }]
|
||
})
|
||
}
|
||
}
|
||
const choices = bareAgentDeepGet(j, 'choices')
|
||
const ch0 =
|
||
Array.isArray(choices) && choices[0] && typeof choices[0] === 'object'
|
||
? /** @type {Record<string, unknown>} */ (choices[0])
|
||
: null
|
||
const delta =
|
||
ch0 && typeof ch0.delta === 'object'
|
||
? /** @type {Record<string, unknown>} */ (ch0.delta)
|
||
: null
|
||
const finishReason =
|
||
typeof ch0?.finish_reason === 'string' ? ch0.finish_reason : ''
|
||
const usage =
|
||
typeof j.usage === 'object' && j.usage ? j.usage : undefined
|
||
|
||
if (usage) {
|
||
onEvent({ type: 'usage', usage })
|
||
}
|
||
|
||
if (delta) {
|
||
const c = delta.content
|
||
if (typeof c === 'string' && c.length) {
|
||
onEvent({
|
||
type: 'delta_content',
|
||
content: c
|
||
})
|
||
}
|
||
const toolCalls = delta.tool_calls
|
||
if (toolCalls !== undefined)
|
||
onEvent({
|
||
type: 'delta_tool_calls',
|
||
tool_calls: toolCalls
|
||
})
|
||
const rc = delta.reasoning_content
|
||
if (typeof rc === 'string' && rc.length) {
|
||
onEvent({
|
||
type: 'delta_reasoning',
|
||
reasoning: rc
|
||
})
|
||
}
|
||
const r = delta.reasoning
|
||
if (typeof r === 'string' && r.length) {
|
||
onEvent({
|
||
type: 'delta_reasoning',
|
||
reasoning: r
|
||
})
|
||
} else if (Array.isArray(r)) {
|
||
for (const chunk of r) {
|
||
if (!chunk || typeof chunk !== 'object') continue
|
||
const ro = /** @type {Record<string, unknown>} */ (chunk)
|
||
const tx =
|
||
typeof ro.text === 'string'
|
||
? ro.text
|
||
: typeof ro.content === 'string'
|
||
? ro.content
|
||
: ''
|
||
if (tx) {
|
||
onEvent({
|
||
type: 'delta_reasoning',
|
||
reasoning: tx
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (finishReason)
|
||
onEvent({
|
||
type: 'finish_reason',
|
||
finish_reason: finishReason
|
||
})
|
||
}
|
||
}
|
||
} finally {
|
||
try {
|
||
reader.releaseLock()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Non-streaming completion (same endpoint, stream:false).
|
||
*/
|
||
async function bareAgentCompleteOnce(opts) {
|
||
const { fetchFn, url, headers, body, signal } = opts
|
||
const res = await fetchFn(url, {
|
||
method: 'POST',
|
||
headers,
|
||
body: JSON.stringify({ ...body, stream: false }),
|
||
signal: signal || undefined
|
||
})
|
||
if (!res.ok) {
|
||
let errText = ''
|
||
try {
|
||
errText = await res.text()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
throw new Error('HTTP ' + res.status + ' ' + errText.slice(0, 800))
|
||
}
|
||
const j = /** @type {Record<string, unknown>} */ (await res.json())
|
||
return j
|
||
}
|
||
|
||
/** HTTP fetch + HTML extract helpers for agent `web_fetch` tool (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {unknown} e
|
||
* @returns {string}
|
||
*/
|
||
function bareWebFmtErr(e) {
|
||
if (e === undefined)
|
||
return 'promise_rejected_with_undefined (no rejection reason)'
|
||
if (e === null) return 'promise_rejected_with_null'
|
||
if (typeof e === 'string') return e
|
||
if (typeof e !== 'object') return String(e)
|
||
const o = /** @type {Record<string, unknown>} */ (e)
|
||
const msg = o.message
|
||
if (typeof msg === 'string' && msg.trim())
|
||
return bareWebFmtErrAugment(o, msg.trim())
|
||
if (typeof msg === 'number' || typeof msg === 'boolean')
|
||
return bareWebFmtErrAugment(o, String(msg))
|
||
const nm = o.name
|
||
const code = o.code
|
||
const errno = o.errno
|
||
/** @type {string[]} */
|
||
const bits = []
|
||
if (typeof nm === 'string' && nm.trim()) bits.push(nm)
|
||
if (code !== undefined && code !== null && String(code) !== '')
|
||
bits.push('code=' + String(code))
|
||
if (errno !== undefined && errno !== null && String(errno) !== '')
|
||
bits.push('errno=' + String(errno))
|
||
const cause = o.cause
|
||
if (cause !== undefined && cause !== null && cause !== e) {
|
||
const cs = bareWebFmtErr(cause)
|
||
if (cs && cs !== 'unknown_error')
|
||
bits.push('cause=(' + cs.slice(0, 280) + ')')
|
||
}
|
||
const errs = o.errors
|
||
if (Array.isArray(errs) && errs.length) {
|
||
errs.slice(0, 5).forEach((sub, i) => {
|
||
bits.push('agg' + i + '=' + bareWebFmtErr(sub).slice(0, 120))
|
||
})
|
||
}
|
||
if (bits.length) return bits.join(' ')
|
||
try {
|
||
const j = JSON.stringify(o)
|
||
if (j && j !== '{}' && j !== '[]') return j.slice(0, 400)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (
|
||
typeof /** @type {{ toString?: () => string }} */ (o).toString ===
|
||
'function'
|
||
) {
|
||
const t = /** @type {{ toString: () => string }} */ (o).toString()
|
||
if (t && t !== '[object Object]') return t.slice(0, 400)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return 'unknown_error'
|
||
}
|
||
|
||
/**
|
||
* Append errno/syscall from Node-ish errors when message alone is vague.
|
||
* @param {Record<string, unknown>} o
|
||
* @param {string} base
|
||
*/
|
||
function bareWebFmtErrAugment(o, base) {
|
||
const syscall = o.syscall
|
||
const code = o.code
|
||
const errno = o.errno
|
||
/** @type {string[]} */
|
||
const tail = []
|
||
if (typeof syscall === 'string' && syscall.trim())
|
||
tail.push('syscall=' + syscall)
|
||
if (code !== undefined && code !== null && String(code) !== '')
|
||
tail.push(String(code))
|
||
if (errno !== undefined && errno !== null && String(errno) !== '')
|
||
tail.push('errno=' + String(errno))
|
||
const cause = o.cause
|
||
if (cause !== undefined && cause !== null) {
|
||
const cs = bareWebFmtErr(cause)
|
||
if (cs && cs !== 'unknown_error')
|
||
tail.push('cause=(' + cs.slice(0, 240) + ')')
|
||
}
|
||
return tail.length ? base + ' [' + tail.join(', ') + ']' : base
|
||
}
|
||
|
||
/**
|
||
* Tool args often omit numeric fields; `Number(undefined)` is NaN and `NaN ?? d` is still NaN.
|
||
* @param {unknown} n
|
||
* @param {number} def
|
||
*/
|
||
function bareWebFiniteOr(n, def) {
|
||
const x = Number(n)
|
||
return Number.isFinite(x) ? x : def
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {typeof fetch | null}
|
||
*/
|
||
function bareWebResolveFetch(ctx) {
|
||
if (typeof ctx.httpFetch === 'function')
|
||
return /** @type {typeof fetch} */ (ctx.httpFetch.bind(ctx))
|
||
const bare = ctx.bare && typeof ctx.bare === 'object' ? ctx.bare : null
|
||
let f =
|
||
bare && typeof bare.fetch === 'function'
|
||
? bare.fetch
|
||
: bare &&
|
||
bare.default &&
|
||
typeof bare.default === 'object' &&
|
||
typeof bare.default.fetch === 'function'
|
||
? bare.default.fetch
|
||
: null
|
||
if (typeof f === 'function') return /** @type {typeof fetch} */ (f.bind(bare))
|
||
if (typeof globalThis.fetch === 'function')
|
||
return globalThis.fetch.bind(globalThis)
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
|
||
* `controller.abort()` with no argument sets `reason === undefined`, so callers
|
||
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
|
||
* @param {number} timeoutMs
|
||
*/
|
||
function bareWebTimeoutAbortReason(timeoutMs) {
|
||
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
|
||
try {
|
||
if (typeof DOMException === 'function')
|
||
return new DOMException(msg, 'TimeoutError')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const e = new Error(msg)
|
||
e.name = 'TimeoutError'
|
||
return e
|
||
}
|
||
|
||
/**
|
||
* @param {AbortSignal} sig
|
||
*/
|
||
function bareWebSignalAbortReason(sig) {
|
||
try {
|
||
const r = /** @type {{ reason?: unknown }} */ (sig).reason
|
||
if (r !== undefined && r !== null) return r
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const e = new Error('web_fetch aborted (signal)')
|
||
e.name = 'AbortError'
|
||
return e
|
||
}
|
||
|
||
/**
|
||
* @param {AbortSignal | null | undefined} a
|
||
* @param {AbortSignal | null | undefined} b
|
||
*/
|
||
function bareWebUnionAbort(a, b) {
|
||
if (!a) return b || undefined
|
||
if (!b) return a
|
||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||
const c = new AbortController()
|
||
/**
|
||
* @param {AbortSignal} sig
|
||
*/
|
||
const forward = (sig) => {
|
||
try {
|
||
c.abort(bareWebSignalAbortReason(sig))
|
||
} catch {
|
||
/* ignore — second source may fire after controller already aborted */
|
||
}
|
||
}
|
||
try {
|
||
const as = /** @type {AbortSignal} */ (a)
|
||
const bs = /** @type {AbortSignal} */ (b)
|
||
if (as.aborted) forward(as)
|
||
else as.addEventListener('abort', () => forward(as), { once: true })
|
||
if (bs.aborted) forward(bs)
|
||
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return c.signal
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array[]} parts
|
||
*/
|
||
function bareWebConcatUint8(parts) {
|
||
let n = 0
|
||
for (const p of parts) n += p.length
|
||
const out = new Uint8Array(n)
|
||
let o = 0
|
||
for (const p of parts) {
|
||
out.set(p, o)
|
||
o += p.length
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Response} res
|
||
* @param {number} maxBytes
|
||
* @param {AbortSignal | undefined} signal
|
||
*/
|
||
async function bareWebReadBodyLimited(res, maxBytes, signal) {
|
||
if (!res.body || typeof res.body.getReader !== 'function') {
|
||
try {
|
||
const ab = await res.arrayBuffer()
|
||
const u8 = new Uint8Array(ab)
|
||
return {
|
||
bytes: u8.byteLength > maxBytes ? u8.slice(0, maxBytes) : u8,
|
||
truncated: u8.byteLength > maxBytes
|
||
}
|
||
} catch {
|
||
return { bytes: new Uint8Array(0), truncated: false }
|
||
}
|
||
}
|
||
const reader = res.body.getReader()
|
||
/** @type {Uint8Array[]} */
|
||
const chunks = []
|
||
let total = 0
|
||
try {
|
||
for (;;) {
|
||
if (signal && signal.aborted) {
|
||
try {
|
||
await reader.cancel()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
break
|
||
}
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
if (!value || !value.length) continue
|
||
total += value.length
|
||
if (total > maxBytes) {
|
||
const prev = total - value.length
|
||
const take = Math.max(0, maxBytes - prev)
|
||
if (take > 0) chunks.push(value.subarray(0, take))
|
||
try {
|
||
await reader.cancel()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return { bytes: bareWebConcatUint8(chunks), truncated: true }
|
||
}
|
||
chunks.push(value)
|
||
}
|
||
} finally {
|
||
try {
|
||
reader.releaseLock()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return { bytes: bareWebConcatUint8(chunks), truncated: false }
|
||
}
|
||
|
||
/**
|
||
* @param {string | null | undefined} ct
|
||
*/
|
||
function bareWebCharsetFromContentType(ct) {
|
||
const m = /charset\s*=\s*["']?([^"';\s]+)/i.exec(String(ct || ''))
|
||
return (m ? m[1] : 'utf-8').trim().toLowerCase()
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array} bytes
|
||
* @param {string} label
|
||
*/
|
||
function bareWebDecodeBytes(bytes, label) {
|
||
try {
|
||
const dec = new TextDecoder(label || 'utf-8', {
|
||
fatal: false,
|
||
ignoreBOM: true
|
||
})
|
||
return dec.decode(bytes)
|
||
} catch {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareWebDecodeHtmlEntities(s) {
|
||
let t = String(s || '')
|
||
t = t.replace(/ /gi, ' ')
|
||
t = t.replace(/"/gi, '"')
|
||
t = t.replace(/'/g, "'")
|
||
t = t.replace(/'/gi, "'")
|
||
t = t.replace(/&/gi, '&')
|
||
t = t.replace(/</gi, '<')
|
||
t = t.replace(/>/gi, '>')
|
||
t = t.replace(/&#x([0-9a-f]+);/gi, (_, h) => {
|
||
const c = parseInt(h, 16)
|
||
return Number.isFinite(c) ? String.fromCodePoint(c) : _
|
||
})
|
||
t = t.replace(/&#(\d+);/g, (_, d) => {
|
||
const c = parseInt(d, 10)
|
||
return Number.isFinite(c) ? String.fromCodePoint(c) : _
|
||
})
|
||
return t
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
*/
|
||
function bareWebExtractHtmlText(html) {
|
||
let s = String(html || '')
|
||
s = s.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||
s = s.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
||
s = s.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '')
|
||
s = s.replace(/<!--[\s\S]*?-->/g, '')
|
||
s = s.replace(/<[^>]+>/g, ' ')
|
||
s = bareWebDecodeHtmlEntities(s)
|
||
s = s.replace(/\s+/g, ' ').trim()
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
* @param {string} baseUrl
|
||
* @param {number} maxLinks
|
||
*/
|
||
function bareWebExtractLinks(html, baseUrl, maxLinks) {
|
||
const cap = Math.min(Math.max(Number(maxLinks) || 200, 1), 500)
|
||
let uBase = null
|
||
try {
|
||
uBase = baseUrl ? new URL(String(baseUrl)) : null
|
||
} catch {
|
||
uBase = null
|
||
}
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const re = /<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi
|
||
let m
|
||
const h = String(html || '')
|
||
while ((m = re.exec(h)) !== null) {
|
||
const href = (m[1] || m[2] || m[3] || '').trim()
|
||
if (!href || href.startsWith('javascript:') || href.startsWith('#'))
|
||
continue
|
||
try {
|
||
const abs = uBase ? new URL(href, uBase).href : new URL(href).href
|
||
const proto = new URL(abs).protocol
|
||
if (proto !== 'http:' && proto !== 'https:') continue
|
||
if (!seen.has(abs)) {
|
||
seen.add(abs)
|
||
out.push(abs)
|
||
}
|
||
} catch {
|
||
/* skip */
|
||
}
|
||
if (out.length >= cap) break
|
||
}
|
||
return { links: out, links_truncated: out.length >= cap }
|
||
}
|
||
|
||
/**
|
||
* @param {string} tag
|
||
*/
|
||
function bareWebMetaContent(tag) {
|
||
const q =
|
||
/content\s*=\s*"([^"]*)"/i.exec(tag) ||
|
||
/content\s*=\s*'([^']*)'/i.exec(tag) ||
|
||
/content\s*=\s*([^\s>]+)/i.exec(tag)
|
||
return q ? bareWebDecodeHtmlEntities(q[1]).trim() : ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} html
|
||
*/
|
||
function bareWebExtractMeta(html) {
|
||
const h = String(html || '')
|
||
const titleM = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(h)
|
||
const title = titleM
|
||
? bareWebDecodeHtmlEntities(titleM[1].replace(/<[^>]+>/g, ' ')).trim()
|
||
: ''
|
||
let description = ''
|
||
const metaDescRe = /<meta[^>]*\bname\s*=\s*["']description["'][^>]*>/i.exec(h)
|
||
if (metaDescRe) description = bareWebMetaContent(metaDescRe[0])
|
||
|
||
let og_title = ''
|
||
const ogT = /<meta[^>]*\bproperty\s*=\s*["']og:title["'][^>]*>/i.exec(h)
|
||
if (ogT) og_title = bareWebMetaContent(ogT[0])
|
||
|
||
let og_description = ''
|
||
const ogD = /<meta[^>]*\bproperty\s*=\s*["']og:description["'][^>]*>/i.exec(h)
|
||
if (ogD) og_description = bareWebMetaContent(ogD[0])
|
||
|
||
return {
|
||
title,
|
||
description,
|
||
og_title,
|
||
og_description
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function bareWebMaybeParseJson(text) {
|
||
try {
|
||
return { ok: true, value: JSON.parse(String(text)) }
|
||
} catch {
|
||
return { ok: false }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} ct
|
||
*/
|
||
function bareWebLooksLikeHtml(ct) {
|
||
return /\btext\/html\b/i.test(String(ct || ''))
|
||
}
|
||
|
||
/**
|
||
* @param {string} ct
|
||
*/
|
||
function bareWebLooksLikeJson(ct) {
|
||
const s = String(ct || '').toLowerCase()
|
||
return (
|
||
/\bapplication\/json\b/.test(s) ||
|
||
/\bapplication\/.*\+json\b/.test(s) ||
|
||
/\btext\/json\b/.test(s)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* ctx: Record<string, unknown>,
|
||
* url: string,
|
||
* method?: string,
|
||
* headers?: Record<string, unknown>,
|
||
* body?: string,
|
||
* content_type?: string,
|
||
* max_response_bytes?: number,
|
||
* max_redirects?: number,
|
||
* timeout_ms?: number,
|
||
* format?: string,
|
||
* max_links?: number,
|
||
* signal?: AbortSignal | null
|
||
* }} o
|
||
*/
|
||
async function bareWebRunTool(o) {
|
||
const ctx = o.ctx
|
||
const fetchFn = bareWebResolveFetch(ctx)
|
||
if (!fetchFn) {
|
||
return {
|
||
ok: false,
|
||
error:
|
||
'web_fetch: no HTTP client (set ctx.httpFetch, bare.fetch, or global fetch)'
|
||
}
|
||
}
|
||
|
||
let startUrl = String(o.url || '').trim()
|
||
let method = String(o.method || 'GET').toUpperCase()
|
||
if (
|
||
!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(
|
||
method
|
||
)
|
||
) {
|
||
return { ok: false, error: 'web_fetch: unsupported method' }
|
||
}
|
||
|
||
let body =
|
||
o.body != null && method !== 'GET' && method !== 'HEAD'
|
||
? String(o.body)
|
||
: undefined
|
||
|
||
let u0
|
||
try {
|
||
u0 = new URL(startUrl)
|
||
} catch {
|
||
return { ok: false, error: 'web_fetch: invalid URL' }
|
||
}
|
||
if (u0.protocol !== 'http:' && u0.protocol !== 'https:') {
|
||
return { ok: false, error: 'web_fetch: only http(s) URLs are allowed' }
|
||
}
|
||
|
||
const maxRedirects = Math.min(
|
||
Math.max(bareWebFiniteOr(o.max_redirects, 5), 0),
|
||
20
|
||
)
|
||
const maxBytes = Math.min(
|
||
Math.max(bareWebFiniteOr(o.max_response_bytes, 524288), 1024),
|
||
2 * 1024 * 1024
|
||
)
|
||
const timeoutMs = Math.min(
|
||
Math.max(bareWebFiniteOr(o.timeout_ms, 30000), 500),
|
||
120000
|
||
)
|
||
const fmtRaw = String(o.format || 'auto').toLowerCase()
|
||
const maxLinks = Number(o.max_links) || 200
|
||
|
||
/** @type {string[]} */
|
||
const redirectChain = [startUrl]
|
||
let currentUrl = startUrl
|
||
let redirectsUsed = 0
|
||
|
||
for (;;) {
|
||
const controller = new AbortController()
|
||
const timer = setTimeout(() => {
|
||
try {
|
||
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}, timeoutMs)
|
||
const signal = bareWebUnionAbort(o.signal || undefined, controller.signal)
|
||
|
||
/** @type {Record<string, string>} */
|
||
const hdrObj = {}
|
||
const hin = o.headers
|
||
if (hin && typeof hin === 'object' && !Array.isArray(hin)) {
|
||
for (const [k, v] of Object.entries(hin)) {
|
||
if (typeof v === 'string' && k) hdrObj[k] = v
|
||
}
|
||
}
|
||
if (
|
||
body != null &&
|
||
method !== 'GET' &&
|
||
method !== 'HEAD' &&
|
||
!Object.keys(hdrObj).some((k) => k.toLowerCase() === 'content-type')
|
||
) {
|
||
hdrObj['Content-Type'] =
|
||
typeof o.content_type === 'string' && o.content_type.trim()
|
||
? o.content_type.trim()
|
||
: 'application/octet-stream'
|
||
}
|
||
|
||
/** @type {RequestInit} */
|
||
const init = {
|
||
method,
|
||
headers: hdrObj,
|
||
signal: signal || undefined,
|
||
redirect: 'manual'
|
||
}
|
||
if (body != null && method !== 'GET' && method !== 'HEAD') {
|
||
init.body = body
|
||
}
|
||
|
||
let res
|
||
try {
|
||
res = await fetchFn(currentUrl, init)
|
||
} catch (e) {
|
||
clearTimeout(timer)
|
||
const msg = bareWebFmtErr(
|
||
e === undefined
|
||
? new Error(
|
||
'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)'
|
||
)
|
||
: e
|
||
)
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
clearTimeout(timer)
|
||
|
||
const st = res.status
|
||
if (st >= 300 && st < 400) {
|
||
if (redirectsUsed >= maxRedirects) {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: too many redirects',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
const loc = res.headers.get('Location')
|
||
if (!loc) {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: redirect without Location',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
let nextUrl
|
||
try {
|
||
nextUrl = new URL(loc, currentUrl).href
|
||
} catch {
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: bad redirect URL',
|
||
status: st,
|
||
url_final: currentUrl,
|
||
redirect_chain: redirectChain
|
||
}
|
||
}
|
||
redirectChain.push(nextUrl)
|
||
currentUrl = nextUrl
|
||
redirectsUsed++
|
||
if (st === 301 || st === 302 || st === 303) {
|
||
method = 'GET'
|
||
body = undefined
|
||
}
|
||
continue
|
||
}
|
||
|
||
const ct = res.headers.get('content-type') || ''
|
||
const url_final = currentUrl
|
||
const responseType = typeof res.type === 'string' ? res.type : undefined
|
||
let setCookie
|
||
try {
|
||
const hdrs = res.headers
|
||
if (hdrs && typeof hdrs.getSetCookie === 'function') {
|
||
const sc = hdrs.getSetCookie()
|
||
if (Array.isArray(sc) && sc.length) setCookie = sc.slice(0, 32)
|
||
}
|
||
} catch {
|
||
setCookie = undefined
|
||
}
|
||
|
||
if (method === 'HEAD') {
|
||
return {
|
||
ok: true,
|
||
url_final,
|
||
status: st,
|
||
content_type: ct,
|
||
response_type: responseType,
|
||
set_cookie: setCookie,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined,
|
||
truncated: false,
|
||
extract: { note: 'HEAD — body omitted' }
|
||
}
|
||
}
|
||
|
||
let bodyRead
|
||
try {
|
||
bodyRead = await bareWebReadBodyLimited(
|
||
res,
|
||
maxBytes,
|
||
signal || undefined
|
||
)
|
||
} catch (e) {
|
||
const msg = bareWebFmtErr(e)
|
||
return {
|
||
ok: false,
|
||
error: 'web_fetch: read body failed: ' + msg.slice(0, 400),
|
||
url_final,
|
||
status: st,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined
|
||
}
|
||
}
|
||
|
||
const charset = bareWebCharsetFromContentType(ct)
|
||
const text = bareWebDecodeBytes(bodyRead.bytes, charset)
|
||
|
||
const fmt =
|
||
fmtRaw === 'auto'
|
||
? bareWebLooksLikeJson(ct)
|
||
? 'json'
|
||
: bareWebLooksLikeHtml(ct)
|
||
? 'markdownish'
|
||
: 'raw'
|
||
: fmtRaw
|
||
|
||
/** @type {unknown} */
|
||
let extract
|
||
|
||
if (fmt === 'json') {
|
||
const p = bareWebMaybeParseJson(text)
|
||
extract = p.ok
|
||
? { json: p.value }
|
||
: { parse_error: true, text_slice: text.slice(0, 8000) }
|
||
} else if (fmt === 'links') {
|
||
extract = bareWebExtractLinks(text, url_final, maxLinks)
|
||
} else if (fmt === 'meta') {
|
||
extract = bareWebExtractMeta(text)
|
||
} else if (fmt === 'markdownish' || fmt === 'text') {
|
||
const plain = bareWebExtractHtmlText(text)
|
||
extract = {
|
||
text: plain,
|
||
approx_chars: plain.length
|
||
}
|
||
} else if (fmt === 'raw') {
|
||
extract = {
|
||
raw_text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text,
|
||
char_count: text.length
|
||
}
|
||
} else {
|
||
extract = {
|
||
text: text.length > 12000 ? text.slice(0, 12000) + '\n…' : text,
|
||
char_count: text.length
|
||
}
|
||
}
|
||
|
||
const raw_preview = text.slice(0, 2000)
|
||
|
||
return {
|
||
ok: true,
|
||
url_final,
|
||
status: st,
|
||
content_type: ct,
|
||
response_type: responseType,
|
||
set_cookie: setCookie,
|
||
redirect_chain: redirectChain.length > 1 ? redirectChain : undefined,
|
||
truncated: bodyRead.truncated,
|
||
extract,
|
||
raw_preview: fmt === 'raw' || fmt === 'json' ? undefined : raw_preview
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Guest-safe Grok Build harness ports: todos, plan mode, memory, glob,
|
||
* AGENTS.md walk-up, JSON hooks, and search_replace uniqueness.
|
||
*/
|
||
|
||
var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||
read_file: 1,
|
||
list_directory: 1,
|
||
list_dir: 1,
|
||
file_stat: 1,
|
||
search_files: 1,
|
||
grep: 1,
|
||
glob_files: 1,
|
||
glob: 1,
|
||
get_system_info: 1,
|
||
list_bin: 1,
|
||
read_man_page: 1,
|
||
apropos_man: 1,
|
||
read_proc_file: 1,
|
||
get_swarm_peers: 1,
|
||
get_resource_limits: 1,
|
||
web_fetch: 1,
|
||
web_search: 1,
|
||
git_status: 1,
|
||
read_skill: 1,
|
||
list_services: 1,
|
||
service_status: 1,
|
||
list_timers: 1,
|
||
read_cron_log: 1,
|
||
read_audit_log: 1,
|
||
read_boot_policy: 1,
|
||
read_kernel_extension_resolution: 1,
|
||
get_initd_graph: 1,
|
||
read_unit_journal: 1,
|
||
inspect_ipc_backpressure: 1,
|
||
get_network_summary: 1,
|
||
tail_telemetry_streams: 1,
|
||
pkg_index_lookup: 1,
|
||
list_verification_scripts: 1,
|
||
verification_hints: 1,
|
||
runtime_diagnostic_bundle: 1,
|
||
memory_search: 1,
|
||
memory_get: 1,
|
||
memory_append: 1,
|
||
list_skills: 1,
|
||
fuzzy_find: 1,
|
||
wait_for: 1,
|
||
read_many: 1,
|
||
git_diff: 1,
|
||
git_log: 1,
|
||
git_show: 1,
|
||
git_blame: 1,
|
||
history_search: 1,
|
||
list_scheduled: 1,
|
||
find_symbol: 1,
|
||
diff_files: 1,
|
||
export_session: 1,
|
||
remember: 1,
|
||
todo_write: 1,
|
||
enter_plan_mode: 1,
|
||
exit_plan_mode: 1,
|
||
ask_user_question: 1,
|
||
update_goal: 1,
|
||
autonomous_run_status: 1,
|
||
get_hrpc_bridge_health: 1,
|
||
get_hrpc_allowlist_status: 1
|
||
})
|
||
|
||
var BARE_AGENT_PLAN_WRITE_TOOLS = Object.freeze({
|
||
write_file: 1,
|
||
edit_file: 1,
|
||
search_replace: 1,
|
||
apply_patch: 1
|
||
})
|
||
|
||
/**
|
||
* @param {string} pattern
|
||
* @returns {RegExp | null}
|
||
*/
|
||
function bareAgentGlobToRegExp(pattern) {
|
||
const src = String(pattern || '').trim()
|
||
if (!src) return null
|
||
let out = '^'
|
||
for (let i = 0; i < src.length; i++) {
|
||
const c = src.charAt(i)
|
||
if (c === '*' && src.charAt(i + 1) === '*') {
|
||
const next = src.charAt(i + 2)
|
||
if (next === '/' || next === '') {
|
||
out += '.*'
|
||
i += next === '/' ? 2 : 1
|
||
continue
|
||
}
|
||
}
|
||
if (c === '*') {
|
||
out += '[^/]*'
|
||
continue
|
||
}
|
||
if (c === '?') {
|
||
out += '[^/]'
|
||
continue
|
||
}
|
||
if ('\\^$+()[]{}|.'.indexOf(c) !== -1) out += '\\' + c
|
||
else out += c
|
||
}
|
||
out += '$'
|
||
try {
|
||
return new RegExp(out)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} rel
|
||
* @param {string} pattern
|
||
*/
|
||
function bareAgentGlobMatch(rel, pattern) {
|
||
const re = bareAgentGlobToRegExp(pattern)
|
||
if (!re) return false
|
||
const n = String(rel || '').replace(/^\/+/, '')
|
||
if (re.test(n)) return true
|
||
const base = n.split('/').pop() || n
|
||
return re.test(base)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} updates
|
||
* @param {{ merge?: boolean }} [opts]
|
||
* @param {{ id: string, content: string, status: string }[]} [prev]
|
||
*/
|
||
function bareAgentTodoApply(updates, opts, prev) {
|
||
const merge = Boolean(opts && opts.merge)
|
||
/** @type {{ id: string, content: string, status: string }[]} */
|
||
const list = merge && Array.isArray(prev) ? prev.slice() : []
|
||
const byId = Object.create(null)
|
||
for (let i = 0; i < list.length; i++) byId[list[i].id] = i
|
||
const rows = Array.isArray(updates) ? updates : []
|
||
const seen = Object.create(null)
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
|
||
const id = String(row.id || '').trim()
|
||
if (!id) throw new Error('todo_id_required')
|
||
if (seen[id]) throw new Error('duplicate_todo_id:' + id)
|
||
seen[id] = 1
|
||
const statusRaw = String(row.status || 'pending').trim().toLowerCase()
|
||
const status =
|
||
statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled'
|
||
? statusRaw
|
||
: 'pending'
|
||
const content = String(row.content == null ? '' : row.content).trim()
|
||
if (byId[id] != null) {
|
||
const cur = list[byId[id]]
|
||
if (content) cur.content = content
|
||
cur.status = status
|
||
} else {
|
||
list.push({ id, content: content || id, status })
|
||
byId[id] = list.length - 1
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|
||
/**
|
||
* @param {{ id: string, content: string, status: string }[]} todos
|
||
*/
|
||
function bareAgentTodoSummarize(todos) {
|
||
const rows = Array.isArray(todos) ? todos : []
|
||
let pending = 0
|
||
let inProgress = 0
|
||
let completed = 0
|
||
const lines = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const t = rows[i]
|
||
const st = String(t.status || 'pending')
|
||
if (st === 'completed' || st === 'cancelled') completed++
|
||
else if (st === 'in_progress') inProgress++
|
||
else pending++
|
||
lines.push('- [' + st + '] ' + t.id + ': ' + String(t.content || '').slice(0, 160))
|
||
}
|
||
return {
|
||
total: rows.length,
|
||
pending,
|
||
in_progress: inProgress,
|
||
completed,
|
||
open: pending + inProgress,
|
||
text: lines.join('\n')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {{ open?: number, turnsSinceTodoWrite?: number, nudgeEnabled?: boolean }} opts
|
||
*/
|
||
function bareAgentTodoNudgeText(opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
if (o.nudgeEnabled === false) return ''
|
||
const turns = Number(o.turnsSinceTodoWrite) || 0
|
||
const open = Number(o.open) || 0
|
||
if (open > 0 && turns >= 3) {
|
||
return (
|
||
'Open todos remain (' +
|
||
String(open) +
|
||
'). Use todo_write to mark progress or complete items before stopping.'
|
||
)
|
||
}
|
||
if (open === 0 && turns >= 5) {
|
||
return 'Multi-step work: call todo_write to track remaining steps (merge=true to update one id).'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} toolName
|
||
* @param {Record<string, unknown>} args
|
||
* @param {{ plan?: string }} paths
|
||
*/
|
||
function bareAgentPlanModeToolAllowed(toolName, args, paths) {
|
||
const name = String(toolName || '')
|
||
if (BARE_AGENT_PLAN_READONLY_TOOLS[name]) return true
|
||
if (!BARE_AGENT_PLAN_WRITE_TOOLS[name]) return false
|
||
const plan = String((paths && paths.plan) || '')
|
||
const p = String(args.path || args.file_path || '')
|
||
return Boolean(plan && p && p === plan)
|
||
}
|
||
|
||
/**
|
||
* @param {string} hay
|
||
* @param {string} needle
|
||
*/
|
||
function bareAgentCountOccurrences(hay, needle) {
|
||
if (!needle) return 0
|
||
let n = 0
|
||
let from = 0
|
||
while (from <= hay.length) {
|
||
const i = hay.indexOf(needle, from)
|
||
if (i === -1) break
|
||
n++
|
||
from = i + Math.max(needle.length, 1)
|
||
}
|
||
return n
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {string} oldStr
|
||
* @param {string} newStr
|
||
* @param {boolean} replaceAll
|
||
*/
|
||
function bareAgentSearchReplaceApply(text, oldStr, newStr, replaceAll) {
|
||
const prev = String(text == null ? '' : text)
|
||
if (!oldStr) {
|
||
if (prev.trim()) {
|
||
return { ok: false, error: 'empty_old_string_cannot_overwrite' }
|
||
}
|
||
return { ok: true, next: String(newStr == null ? '' : newStr), replacements: 1 }
|
||
}
|
||
const count = bareAgentCountOccurrences(prev, oldStr)
|
||
if (count === 0) return { ok: false, error: 'old_string not found', count: 0 }
|
||
if (count > 1 && !replaceAll) {
|
||
return {
|
||
ok: false,
|
||
error: 'old_string not unique',
|
||
count,
|
||
hint: 'add surrounding lines to make the match unique, or set replace_all true'
|
||
}
|
||
}
|
||
const next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr)
|
||
return { ok: true, next, replacements: replaceAll ? count : 1 }
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {{ offset?: number, limit?: number, numbered?: boolean }} [opts]
|
||
*/
|
||
function bareAgentSliceFileLines(text, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const raw = String(text == null ? '' : text)
|
||
const lines = raw.split('\n')
|
||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||
const offset =
|
||
typeof o.offset === 'number' && o.offset >= 1 ? Math.floor(o.offset) : 1
|
||
const limit =
|
||
typeof o.limit === 'number' && o.limit >= 1
|
||
? Math.floor(o.limit)
|
||
: lines.length
|
||
const start = Math.min(lines.length, offset - 1)
|
||
const end = Math.min(lines.length, start + limit)
|
||
const slice = lines.slice(start, end)
|
||
const numbered = o.numbered !== false
|
||
const body = numbered
|
||
? slice
|
||
.map(function (line, i) {
|
||
return String(start + i + 1) + '→' + line
|
||
})
|
||
.join('\n')
|
||
: slice.join('\n')
|
||
return {
|
||
content: body,
|
||
start_line: start + 1,
|
||
end_line: end,
|
||
total_lines: lines.length,
|
||
truncated: start > 0 || end < lines.length
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} query
|
||
*/
|
||
function bareAgentMemoryTokens(query) {
|
||
return String(query || '')
|
||
.toLowerCase()
|
||
.split(/[^a-z0-9_./-]+/)
|
||
.filter(function (t) {
|
||
return t.length > 1
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {string[]} tokens
|
||
*/
|
||
function bareAgentMemoryScore(text, tokens) {
|
||
const low = String(text || '').toLowerCase()
|
||
if (!tokens.length || !low) return 0
|
||
let hits = 0
|
||
for (let i = 0; i < tokens.length; i++) {
|
||
if (low.indexOf(tokens[i]) !== -1) hits++
|
||
}
|
||
return hits / tokens.length
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} startDir
|
||
* @param {number} [maxHops]
|
||
* @returns {Promise<string[]>}
|
||
*/
|
||
async function bareAgentDiscoverAgentsMdPaths(ctx, startDir, maxHops) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return []
|
||
/** @type {string[]} */
|
||
const found = []
|
||
const seen = Object.create(null)
|
||
let dir = String(startDir || '').replace(/\/+$/, '') || '/'
|
||
const hops = Math.min(Math.max(Number(maxHops) || 12, 1), 24)
|
||
const names = ['AGENTS.md', 'Claude.md', 'CLAUDE.md']
|
||
for (let n = 0; n < hops; n++) {
|
||
for (let i = 0; i < names.length; i++) {
|
||
const p = (dir === '/' ? '' : dir) + '/' + names[i]
|
||
if (seen[p]) continue
|
||
seen[p] = 1
|
||
try {
|
||
const buf = await vfs.readFile(p)
|
||
if (buf && buf.length) found.push(p)
|
||
} catch {
|
||
/* missing */
|
||
}
|
||
}
|
||
const rulesDirs = [dir + '/.grok/rules', dir + '/.agent/rules']
|
||
for (let r = 0; r < rulesDirs.length; r++) {
|
||
if (typeof vfs.readdir !== 'function') continue
|
||
let entries = []
|
||
try {
|
||
entries = await vfs.readdir(rulesDirs[r])
|
||
} catch {
|
||
continue
|
||
}
|
||
if (!Array.isArray(entries)) continue
|
||
entries = entries.slice().sort()
|
||
for (let e = 0; e < entries.length; e++) {
|
||
const name = String(entries[e] || '')
|
||
if (!/\.md$/i.test(name)) continue
|
||
const p = rulesDirs[r] + '/' + name
|
||
if (seen[p]) continue
|
||
seen[p] = 1
|
||
try {
|
||
const buf = await vfs.readFile(p)
|
||
if (buf && buf.length) found.push(p)
|
||
} catch {
|
||
/* skip */
|
||
}
|
||
}
|
||
}
|
||
if (dir === '/' || dir === '') break
|
||
const slash = dir.lastIndexOf('/')
|
||
dir = slash <= 0 ? '/' : dir.slice(0, slash)
|
||
}
|
||
return found
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string[]} files
|
||
* @param {number} [maxChars]
|
||
*/
|
||
async function bareAgentLoadAgentsMdPrompt(ctx, files, maxChars) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return ''
|
||
const cap = Math.min(Math.max(Number(maxChars) || 6000, 500), 16000)
|
||
const parts = []
|
||
let used = 0
|
||
for (let i = 0; i < files.length; i++) {
|
||
if (used >= cap) break
|
||
try {
|
||
const buf = await vfs.readFile(files[i])
|
||
if (!buf || !buf.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
const body = String(t || '').trim()
|
||
if (!body) continue
|
||
const block = '### ' + files[i] + '\n' + body
|
||
const room = cap - used
|
||
parts.push(block.slice(0, room))
|
||
used += Math.min(block.length, room)
|
||
} catch {
|
||
/* skip */
|
||
}
|
||
}
|
||
if (!parts.length) return ''
|
||
return (
|
||
'## Discovered project agent files (cwd walk-up, Grok-style)\n' +
|
||
parts.join('\n\n')
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} hook
|
||
* @param {string} toolName
|
||
* @param {Record<string, unknown>} args
|
||
*/
|
||
function bareAgentHookDenies(hook, toolName, args) {
|
||
if (!hook || typeof hook !== 'object') return ''
|
||
const event = String(hook.event || hook.type || 'PreToolUse')
|
||
if (event !== 'PreToolUse' && event !== 'pre') return ''
|
||
const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : []
|
||
if (tools.length && tools.indexOf(toolName) === -1) return ''
|
||
const reSrc = String(hook.deny_regex || hook.denyRegex || '').trim()
|
||
if (!reSrc) return ''
|
||
let re
|
||
try {
|
||
re = new RegExp(reSrc, 'i')
|
||
} catch {
|
||
return ''
|
||
}
|
||
const blob =
|
||
toolName +
|
||
' ' +
|
||
String(args.command || args.path || args.file_path || args.pattern || '')
|
||
if (!re.test(blob)) return ''
|
||
return String(hook.reason || hook.message || 'blocked by ~/.agent/hooks').slice(0, 240)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} hooksDir
|
||
* @param {string} toolName
|
||
* @param {Record<string, unknown>} args
|
||
*/
|
||
async function bareAgentRunPreToolHooks(ctx, hooksDir, toolName, args) {
|
||
if (typeof bareAgentRunHooks === 'function') {
|
||
const out = await bareAgentRunHooks(ctx, hooksDir, 'PreToolUse', toolName, args, '')
|
||
return out.deny || ''
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} buf
|
||
*/
|
||
function bareAgentPortDecode(ctx, buf) {
|
||
if (!buf || !buf.length) return ''
|
||
if (
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
) {
|
||
return ctx.b4a.toString(buf)
|
||
}
|
||
return String(new TextDecoder().decode(buf))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} text
|
||
*/
|
||
function bareAgentPortEncode(ctx, text) {
|
||
if (
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.from === 'function'
|
||
) {
|
||
return ctx.b4a.from(String(text == null ? '' : text))
|
||
}
|
||
return new TextEncoder().encode(String(text == null ? '' : text))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
*/
|
||
async function bareAgentReadTextFile(ctx, path) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return ''
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
return bareAgentPortDecode(ctx, buf)
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @param {string} text
|
||
*/
|
||
async function bareAgentWriteTextFile(ctx, path, text) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.writeFile !== 'function') {
|
||
throw new Error('vfs.writeFile unavailable')
|
||
}
|
||
if (typeof vfs.mkdir === 'function') {
|
||
const dir = String(path || '').replace(/\/[^/]+$/, '')
|
||
if (dir && dir !== path) {
|
||
try {
|
||
await vfs.mkdir(dir, { recursive: true })
|
||
} catch {
|
||
/* parent may already exist */
|
||
}
|
||
}
|
||
}
|
||
await vfs.writeFile(path, bareAgentPortEncode(ctx, text))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @param {unknown} fallback
|
||
*/
|
||
async function bareAgentReadJsonFile(ctx, path, fallback) {
|
||
const t = await bareAgentReadTextFile(ctx, path)
|
||
if (!t.trim()) return fallback
|
||
try {
|
||
return JSON.parse(t)
|
||
} catch {
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @param {unknown} value
|
||
*/
|
||
async function bareAgentWriteJsonFile(ctx, path, value) {
|
||
await bareAgentWriteTextFile(ctx, path, JSON.stringify(value, null, 2) + '\n')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
*/
|
||
async function bareAgentVfsIsDir(ctx, path) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs) return false
|
||
if (typeof vfs.lstat === 'function' || typeof vfs.stat === 'function') {
|
||
try {
|
||
const st =
|
||
typeof vfs.lstat === 'function' ? await vfs.lstat(path) : await vfs.stat(path)
|
||
if (st && typeof st.isDirectory === 'function') return Boolean(st.isDirectory())
|
||
if (st && typeof st === 'object' && 'isDirectory' in st) {
|
||
return Boolean(/** @type {{ isDirectory?: unknown }} */ (st).isDirectory)
|
||
}
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
if (typeof vfs.readdir === 'function') {
|
||
try {
|
||
await vfs.readdir(path)
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
* @param {{ maxFiles?: number, maxDepth?: number }} [opts]
|
||
* @returns {Promise<string[]>}
|
||
*/
|
||
async function bareAgentVfsWalkFiles(ctx, root, opts) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readdir !== 'function') return []
|
||
const maxFiles = Math.min(Math.max(Number(opts && opts.maxFiles) || 400, 1), 4000)
|
||
const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 8, 1), 16)
|
||
const skip = Object.create(null)
|
||
skip['.git'] = 1
|
||
skip['node_modules'] = 1
|
||
skip['.bare-os'] = 1
|
||
const rootN = String(root || '/').replace(/\/+$/, '') || '/'
|
||
const rules =
|
||
opts && Array.isArray(opts.ignore)
|
||
? opts.ignore
|
||
: typeof bareAgentLoadIgnoreRules === 'function'
|
||
? await bareAgentLoadIgnoreRules(ctx, rootN)
|
||
: []
|
||
/** @type {string[]} */
|
||
const out = []
|
||
/** @type {{ dir: string, depth: number }[]} */
|
||
const queue = [{ dir: rootN, depth: 0 }]
|
||
while (queue.length && out.length < maxFiles) {
|
||
const cur = queue.shift()
|
||
if (!cur) break
|
||
let names
|
||
try {
|
||
names = await vfs.readdir(cur.dir)
|
||
} catch {
|
||
continue
|
||
}
|
||
if (!Array.isArray(names)) continue
|
||
for (let i = 0; i < names.length && out.length < maxFiles; i++) {
|
||
const name = String(names[i] || '')
|
||
if (!name || name === '.' || name === '..' || skip[name]) continue
|
||
const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name
|
||
let rel = full
|
||
if (rootN !== '/' && full.indexOf(rootN + '/') === 0) rel = full.slice(rootN.length + 1)
|
||
else if (full.charAt(0) === '/') rel = full.slice(1)
|
||
const isDir = await bareAgentVfsIsDir(ctx, full)
|
||
if (rules.length && bareAgentIgnoreMatch(rel, isDir, rules)) continue
|
||
if (isDir) {
|
||
if (cur.depth + 1 < maxDepth) queue.push({ dir: full, depth: cur.depth + 1 })
|
||
} else {
|
||
out.push(full)
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
* @param {string} pattern
|
||
* @param {{ maxFiles?: number, maxDepth?: number }} [opts]
|
||
*/
|
||
async function bareAgentGlobFiles(ctx, root, pattern, opts) {
|
||
const files = await bareAgentVfsWalkFiles(ctx, root, opts)
|
||
const rootN = String(root || '/').replace(/\/+$/, '') || '/'
|
||
/** @type {string[]} */
|
||
const hits = []
|
||
for (let i = 0; i < files.length; i++) {
|
||
const abs = files[i]
|
||
let rel = abs
|
||
if (rootN !== '/' && abs.indexOf(rootN + '/') === 0) rel = abs.slice(rootN.length + 1)
|
||
else if (abs.charAt(0) === '/') rel = abs.slice(1)
|
||
if (bareAgentGlobMatch(rel, pattern) || bareAgentGlobMatch(abs, pattern)) {
|
||
hits.push(abs)
|
||
}
|
||
}
|
||
return hits
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string[]} files
|
||
* @param {string} query
|
||
* @param {{ maxHits?: number, snippet?: number }} [opts]
|
||
*/
|
||
async function bareAgentMemorySearchFiles(ctx, files, query, opts) {
|
||
const tokens = bareAgentMemoryTokens(query)
|
||
const maxHits = Math.min(Math.max(Number(opts && opts.maxHits) || 8, 1), 24)
|
||
const snippet = Math.min(Math.max(Number(opts && opts.snippet) || 280, 80), 1200)
|
||
/** @type {{ path: string, score: number, snippet: string }[]} */
|
||
const scored = []
|
||
for (let i = 0; i < files.length; i++) {
|
||
const path = files[i]
|
||
const text = await bareAgentReadTextFile(ctx, path)
|
||
if (!text) continue
|
||
const score = tokens.length ? bareAgentMemoryScore(text, tokens) : 0.15
|
||
if (score <= 0) continue
|
||
let snip = text.trim().replace(/\s+/g, ' ').slice(0, snippet)
|
||
if (tokens.length) {
|
||
const low = text.toLowerCase()
|
||
let at = -1
|
||
for (let t = 0; t < tokens.length; t++) {
|
||
const j = low.indexOf(tokens[t])
|
||
if (j !== -1 && (at === -1 || j < at)) at = j
|
||
}
|
||
if (at >= 0) {
|
||
const start = Math.max(0, at - 40)
|
||
snip =
|
||
(start > 0 ? '…' : '') +
|
||
text.slice(start, start + snippet).replace(/\s+/g, ' ')
|
||
}
|
||
}
|
||
scored.push({ path, score: Math.round(score * 1000) / 1000, snippet: snip })
|
||
}
|
||
scored.sort(function (a, b) {
|
||
return b.score - a.score
|
||
})
|
||
return scored.slice(0, maxHits)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
*/
|
||
async function bareAgentLoadTodos(ctx, path) {
|
||
const raw = await bareAgentReadJsonFile(ctx, path, [])
|
||
if (!Array.isArray(raw)) return []
|
||
/** @type {{ id: string, content: string, status: string }[]} */
|
||
const out = []
|
||
for (let i = 0; i < raw.length; i++) {
|
||
const row = raw[i] && typeof raw[i] === 'object' ? raw[i] : null
|
||
if (!row) continue
|
||
const id = String(row.id || '').trim()
|
||
if (!id) continue
|
||
const statusRaw = String(row.status || 'pending').trim().toLowerCase()
|
||
const status =
|
||
statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled'
|
||
? statusRaw
|
||
: 'pending'
|
||
out.push({
|
||
id,
|
||
content: String(row.content == null ? id : row.content),
|
||
status
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @param {{ id: string, content: string, status: string }[]} todos
|
||
*/
|
||
async function bareAgentSaveTodos(ctx, path, todos) {
|
||
await bareAgentWriteJsonFile(ctx, path, Array.isArray(todos) ? todos : [])
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} args
|
||
*/
|
||
function bareAgentToolPathArg(args) {
|
||
if (!args || typeof args !== 'object') return ''
|
||
if (typeof args.path === 'string' && args.path.trim()) return args.path.trim()
|
||
if (typeof args.file_path === 'string' && args.file_path.trim()) {
|
||
return args.file_path.trim()
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function bareAgentLooksBinaryText(text) {
|
||
const s = String(text || '')
|
||
if (!s) return false
|
||
if (s.indexOf('\0') !== -1) return true
|
||
let bad = 0
|
||
const n = Math.min(s.length, 800)
|
||
for (let i = 0; i < n; i++) {
|
||
const c = s.charCodeAt(i)
|
||
if (c === 9 || c === 10 || c === 13) continue
|
||
if (c < 32) bad++
|
||
}
|
||
return bad > 8
|
||
}
|
||
|
||
/**
|
||
* Guest-native grep (Grok-style). VFS walk + JS regex — no host rg/node.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {{
|
||
* pattern: string,
|
||
* root?: string,
|
||
* glob?: string,
|
||
* ignore_case?: boolean,
|
||
* before?: number,
|
||
* after?: number,
|
||
* context?: number,
|
||
* max_matches?: number,
|
||
* files_with_matches?: boolean
|
||
* }} opts
|
||
*/
|
||
async function bareAgentGrepFiles(ctx, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const pattern = String(o.pattern || '')
|
||
if (!pattern) return { ok: false, error: 'pattern_required' }
|
||
let re
|
||
try {
|
||
re = new RegExp(pattern, o.ignore_case ? 'i' : '')
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, error: 'bad_regex', detail: msg }
|
||
}
|
||
const root = String(o.root || '/').replace(/\/+$/, '') || '/'
|
||
const before = Math.min(
|
||
8,
|
||
Math.max(0, Math.floor(Number(o.before != null ? o.before : o.context) || 0))
|
||
)
|
||
const after = Math.min(
|
||
8,
|
||
Math.max(0, Math.floor(Number(o.after != null ? o.after : o.context) || 0))
|
||
)
|
||
const maxMatches = Math.min(
|
||
200,
|
||
Math.max(1, Math.floor(Number(o.max_matches) || 50))
|
||
)
|
||
const mode = String(o.output_mode || '').toLowerCase()
|
||
const filesOnly = Boolean(o.files_with_matches) || mode === 'files_with_matches'
|
||
const countOnly = Boolean(o.count) || mode === 'count'
|
||
const glob = typeof o.glob === 'string' && o.glob.trim() ? o.glob.trim() : ''
|
||
const files = glob
|
||
? await bareAgentGlobFiles(ctx, root, glob, { maxFiles: 800, maxDepth: 12 })
|
||
: await bareAgentVfsWalkFiles(ctx, root, { maxFiles: 800, maxDepth: 12 })
|
||
/** @type {{ path: string, line?: number, text?: string }[]} */
|
||
const matches = []
|
||
/** @type {string[]} */
|
||
const filesHit = []
|
||
/** @type {{ path: string, count: number }[]} */
|
||
const counts = []
|
||
let truncated = false
|
||
for (let f = 0; f < files.length; f++) {
|
||
const path = files[f]
|
||
const text = await bareAgentReadTextFile(ctx, path)
|
||
if (!text || bareAgentLooksBinaryText(text)) continue
|
||
const lines = text.split('\n')
|
||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||
let fileHit = false
|
||
let fileCount = 0
|
||
for (let i = 0; i < lines.length; i++) {
|
||
re.lastIndex = 0
|
||
if (!re.test(lines[i])) continue
|
||
fileHit = true
|
||
fileCount++
|
||
if (filesOnly || countOnly) continue
|
||
const start = Math.max(0, i - before)
|
||
const end = Math.min(lines.length, i + 1 + after)
|
||
const slice = lines.slice(start, end)
|
||
const body =
|
||
before || after
|
||
? slice
|
||
.map(function (ln, j) {
|
||
const n = start + j + 1
|
||
const mark = n === i + 1 ? ':' : '-'
|
||
return String(n) + mark + ln
|
||
})
|
||
.join('\n')
|
||
: String(i + 1) + ':' + lines[i]
|
||
matches.push({ path, line: i + 1, text: body })
|
||
if (matches.length >= maxMatches) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
if (fileHit && filesOnly) {
|
||
filesHit.push(path)
|
||
if (filesHit.length >= maxMatches) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
if (fileHit && countOnly) {
|
||
counts.push({ path, count: fileCount })
|
||
if (counts.length >= maxMatches) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
if (truncated) break
|
||
}
|
||
if (filesOnly) {
|
||
return {
|
||
ok: true,
|
||
pattern,
|
||
root,
|
||
glob: glob || null,
|
||
output_mode: 'files_with_matches',
|
||
files_with_matches: filesHit,
|
||
count: filesHit.length,
|
||
truncated
|
||
}
|
||
}
|
||
if (countOnly) {
|
||
let total = 0
|
||
for (let i = 0; i < counts.length; i++) total += counts[i].count
|
||
return {
|
||
ok: true,
|
||
pattern,
|
||
root,
|
||
glob: glob || null,
|
||
output_mode: 'count',
|
||
files: counts,
|
||
count: total,
|
||
truncated
|
||
}
|
||
}
|
||
return {
|
||
ok: true,
|
||
pattern,
|
||
root,
|
||
glob: glob || null,
|
||
output_mode: 'content',
|
||
matches,
|
||
count: matches.length,
|
||
truncated
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Codex / Grok apply_patch parser (Begin Patch … End Patch).
|
||
* @param {string} text
|
||
* @returns {{ ok: boolean, error?: string, ops?: object[] }}
|
||
*/
|
||
function bareAgentParseApplyPatch(text) {
|
||
const raw = String(text || '').replace(/\r\n/g, '\n')
|
||
if (!raw.trim()) return { ok: false, error: 'empty_patch' }
|
||
const all = raw.split('\n')
|
||
let start = 0
|
||
let end = all.length
|
||
for (let i = 0; i < all.length; i++) {
|
||
if (/^\s*\*\*\*\s*Begin Patch\s*$/i.test(all[i])) {
|
||
start = i + 1
|
||
break
|
||
}
|
||
}
|
||
for (let i = all.length - 1; i >= 0; i--) {
|
||
if (/^\s*\*\*\*\s*End Patch\s*$/i.test(all[i])) {
|
||
end = i
|
||
break
|
||
}
|
||
}
|
||
const body = all.slice(start, end)
|
||
/** @type {object[]} */
|
||
const ops = []
|
||
/** @type {Record<string, unknown> | null} */
|
||
let cur = null
|
||
/** @type {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean } | null} */
|
||
let chunk = null
|
||
|
||
function flushChunk() {
|
||
if (cur && cur.type === 'update' && chunk) {
|
||
const chunks = Array.isArray(cur.chunks) ? cur.chunks : []
|
||
chunks.push(chunk)
|
||
cur.chunks = chunks
|
||
chunk = null
|
||
}
|
||
}
|
||
function flushOp() {
|
||
flushChunk()
|
||
if (cur) ops.push(cur)
|
||
cur = null
|
||
}
|
||
|
||
for (let i = 0; i < body.length; i++) {
|
||
const line = body[i]
|
||
if (/^\s*\*\*\*\s*Add File:\s*/i.test(line)) {
|
||
flushOp()
|
||
cur = {
|
||
type: 'add',
|
||
path: line.replace(/^\s*\*\*\*\s*Add File:\s*/i, '').trim(),
|
||
content: ''
|
||
}
|
||
continue
|
||
}
|
||
if (/^\s*\*\*\*\s*Delete File:\s*/i.test(line)) {
|
||
flushOp()
|
||
cur = {
|
||
type: 'delete',
|
||
path: line.replace(/^\s*\*\*\*\s*Delete File:\s*/i, '').trim()
|
||
}
|
||
continue
|
||
}
|
||
if (/^\s*\*\*\*\s*Update File:\s*/i.test(line)) {
|
||
flushOp()
|
||
cur = {
|
||
type: 'update',
|
||
path: line.replace(/^\s*\*\*\*\s*Update File:\s*/i, '').trim(),
|
||
move_to: '',
|
||
chunks: []
|
||
}
|
||
continue
|
||
}
|
||
if (/^\s*\*\*\*\s*Move to:\s*/i.test(line) && cur && cur.type === 'update') {
|
||
cur.move_to = line.replace(/^\s*\*\*\*\s*Move to:\s*/i, '').trim()
|
||
continue
|
||
}
|
||
if (/^\s*\*\*\*\s*End of File\s*$/i.test(line)) {
|
||
if (chunk) chunk.is_end_of_file = true
|
||
flushChunk()
|
||
continue
|
||
}
|
||
if (line.indexOf('@@') === 0 && cur && cur.type === 'update') {
|
||
flushChunk()
|
||
chunk = {
|
||
context: line.replace(/^@@\s?/, '').trim(),
|
||
old_lines: [],
|
||
new_lines: []
|
||
}
|
||
continue
|
||
}
|
||
if (cur && cur.type === 'add') {
|
||
const bodyLine = line.charAt(0) === '+' ? line.slice(1) : line
|
||
cur.content = cur.content ? String(cur.content) + '\n' + bodyLine : bodyLine
|
||
continue
|
||
}
|
||
if (cur && cur.type === 'update') {
|
||
if (!chunk) {
|
||
chunk = { context: '', old_lines: [], new_lines: [] }
|
||
}
|
||
const tag = line.charAt(0)
|
||
const rest = line.length ? line.slice(1) : ''
|
||
if (tag === '-') chunk.old_lines.push(rest)
|
||
else if (tag === '+') chunk.new_lines.push(rest)
|
||
else {
|
||
const ctxLine = tag === ' ' ? rest : line
|
||
chunk.old_lines.push(ctxLine)
|
||
chunk.new_lines.push(ctxLine)
|
||
}
|
||
}
|
||
}
|
||
flushOp()
|
||
if (!ops.length) return { ok: false, error: 'no_patch_ops' }
|
||
return { ok: true, ops }
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean }} chunk
|
||
*/
|
||
function bareAgentApplyPatchChunk(text, chunk) {
|
||
const src = String(text == null ? '' : text)
|
||
const oldBlock = (chunk.old_lines || []).join('\n')
|
||
const newBlock = (chunk.new_lines || []).join('\n')
|
||
if (!oldBlock && !newBlock) {
|
||
return { ok: false, error: 'empty_chunk' }
|
||
}
|
||
if (!oldBlock) {
|
||
const next = src
|
||
? src.replace(/\s*$/, '') + (src.endsWith('\n') ? '' : '\n') + newBlock + '\n'
|
||
: newBlock + (newBlock.endsWith('\n') ? '' : '\n')
|
||
return { ok: true, next }
|
||
}
|
||
let from = 0
|
||
if (chunk.context) {
|
||
const at = src.indexOf(chunk.context)
|
||
if (at === -1) {
|
||
return { ok: false, error: 'chunk_context_not_found', context: chunk.context }
|
||
}
|
||
from = at
|
||
}
|
||
const hay = src.slice(from)
|
||
const count = bareAgentCountOccurrences(hay, oldBlock)
|
||
if (count === 0) return { ok: false, error: 'chunk_old_not_found' }
|
||
if (count > 1 && !chunk.context) {
|
||
return {
|
||
ok: false,
|
||
error: 'chunk_old_not_unique',
|
||
count,
|
||
hint: 'add @@ context or more surrounding lines'
|
||
}
|
||
}
|
||
const next = src.slice(0, from) + hay.replace(oldBlock, newBlock)
|
||
return { ok: true, next }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {object[]} ops
|
||
* @param {{ home?: string, denyPrefixes?: unknown }} [opts]
|
||
*/
|
||
async function bareAgentApplyPatchOps(ctx, ops, opts) {
|
||
const home = String((opts && opts.home) || '')
|
||
const deny = opts && opts.denyPrefixes
|
||
/** @type {object[]} */
|
||
const results = []
|
||
const rows = Array.isArray(ops) ? ops : []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const op = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
|
||
let path = String(op.path || '').trim()
|
||
if (path && path.charAt(0) !== '/' && home) {
|
||
path = home.replace(/\/+$/, '') + '/' + path.replace(/^\.\//, '')
|
||
}
|
||
if (!path) {
|
||
results.push({ ok: false, error: 'path_required' })
|
||
continue
|
||
}
|
||
if (
|
||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||
!bareAgentPathAllowedMutate(path, deny)
|
||
) {
|
||
results.push({ ok: false, path, error: 'path_not_allowed' })
|
||
continue
|
||
}
|
||
if (op.type === 'add') {
|
||
const content = String(op.content == null ? '' : op.content)
|
||
await bareAgentWriteTextFile(
|
||
ctx,
|
||
path,
|
||
content.endsWith('\n') ? content : content + '\n'
|
||
)
|
||
results.push({ ok: true, op: 'add', path })
|
||
continue
|
||
}
|
||
if (op.type === 'delete') {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.unlink !== 'function') {
|
||
results.push({ ok: false, path, error: 'vfs.unlink unavailable' })
|
||
continue
|
||
}
|
||
try {
|
||
await vfs.unlink(path)
|
||
results.push({ ok: true, op: 'delete', path })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
results.push({ ok: false, path, error: msg })
|
||
}
|
||
continue
|
||
}
|
||
if (op.type === 'update') {
|
||
let text = await bareAgentReadTextFile(ctx, path)
|
||
if (!text && text !== '') {
|
||
results.push({ ok: false, path, error: 'missing_file' })
|
||
continue
|
||
}
|
||
const chunks = Array.isArray(op.chunks) ? op.chunks : []
|
||
let failed = null
|
||
for (let c = 0; c < chunks.length; c++) {
|
||
const applied = bareAgentApplyPatchChunk(text, chunks[c])
|
||
if (!applied.ok) {
|
||
failed = applied
|
||
break
|
||
}
|
||
text = applied.next
|
||
}
|
||
if (failed) {
|
||
results.push({ ok: false, path, error: failed.error, hint: failed.hint || null })
|
||
continue
|
||
}
|
||
const dest = String(op.move_to || '').trim() || path
|
||
const destAbs =
|
||
dest.charAt(0) === '/'
|
||
? dest
|
||
: home
|
||
? home.replace(/\/+$/, '') + '/' + dest.replace(/^\.\//, '')
|
||
: dest
|
||
if (
|
||
destAbs !== path &&
|
||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||
!bareAgentPathAllowedMutate(destAbs, deny)
|
||
) {
|
||
results.push({ ok: false, path, error: 'move_path_not_allowed', dest: destAbs })
|
||
continue
|
||
}
|
||
await bareAgentWriteTextFile(ctx, destAbs, text)
|
||
if (destAbs !== path && ctx.vfs && typeof ctx.vfs.unlink === 'function') {
|
||
try {
|
||
await ctx.vfs.unlink(path)
|
||
} catch {
|
||
/* keep original if unlink fails */
|
||
}
|
||
}
|
||
results.push({
|
||
ok: true,
|
||
op: destAbs !== path ? 'move' : 'update',
|
||
path,
|
||
dest: destAbs !== path ? destAbs : undefined,
|
||
chunks: chunks.length
|
||
})
|
||
continue
|
||
}
|
||
results.push({ ok: false, path, error: 'unknown_op' })
|
||
}
|
||
const failed = results.filter(function (r) {
|
||
return !r.ok
|
||
})
|
||
return {
|
||
ok: failed.length === 0,
|
||
applied: results.length - failed.length,
|
||
failed: failed.length,
|
||
results
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse DuckDuckGo instant-answer JSON into a compact result list.
|
||
* @param {unknown} payload
|
||
* @param {number} [max]
|
||
*/
|
||
function bareAgentParseSearchResults(payload, max) {
|
||
const cap = Math.min(Math.max(Number(max) || 8, 1), 16)
|
||
/** @type {{ title: string, url: string, snippet: string }[]} */
|
||
const out = []
|
||
const seen = Object.create(null)
|
||
function add(title, url, snippet) {
|
||
const u = String(url || '').trim()
|
||
if (!u || seen[u] || out.length >= cap) return
|
||
if (!/^https?:\/\//i.test(u)) return
|
||
seen[u] = 1
|
||
out.push({
|
||
title: String(title || u).slice(0, 160),
|
||
url: u,
|
||
snippet: String(snippet || '').replace(/\s+/g, ' ').trim().slice(0, 280)
|
||
})
|
||
}
|
||
const obj = payload && typeof payload === 'object' ? payload : {}
|
||
const rec = /** @type {Record<string, unknown>} */ (obj)
|
||
if (rec.AbstractURL || rec.Abstract) {
|
||
add(
|
||
String(rec.Heading || rec.AbstractSource || 'Abstract'),
|
||
String(rec.AbstractURL || ''),
|
||
String(rec.AbstractText || rec.Abstract || '')
|
||
)
|
||
}
|
||
const results = Array.isArray(rec.Results) ? rec.Results : []
|
||
for (let i = 0; i < results.length; i++) {
|
||
const row = results[i] && typeof results[i] === 'object' ? results[i] : {}
|
||
add(row.Text || row.Name, row.FirstURL, row.Text)
|
||
}
|
||
const related = Array.isArray(rec.RelatedTopics) ? rec.RelatedTopics : []
|
||
function walk(list) {
|
||
for (let i = 0; i < list.length && out.length < cap; i++) {
|
||
const row = list[i] && typeof list[i] === 'object' ? list[i] : {}
|
||
if (Array.isArray(row.Topics)) walk(row.Topics)
|
||
else add(row.Text, row.FirstURL, row.Text)
|
||
}
|
||
}
|
||
walk(related)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Parse Grok-style intervals (5m / 2h / 1d) or a five-field cron line.
|
||
* @param {string} raw
|
||
* @returns {{ kind: 'everyMs', everyMs: number } | { kind: 'calendar', onCalendar: string } | { error: string }}
|
||
*/
|
||
function bareAgentParseScheduleInterval(raw) {
|
||
const s = String(raw || '').trim()
|
||
if (!s) return { error: 'interval_required' }
|
||
const compact = s.replace(/\s+/g, '')
|
||
const m = /^(\d+)(ms|s|m|h|d)$/i.exec(compact)
|
||
if (m) {
|
||
const n = Number(m[1])
|
||
const unit = m[2].toLowerCase()
|
||
let ms = 0
|
||
if (unit === 'ms') ms = n
|
||
else if (unit === 's') ms = n * 1000
|
||
else if (unit === 'm') ms = n * 60 * 1000
|
||
else if (unit === 'h') ms = n * 60 * 60 * 1000
|
||
else ms = n * 24 * 60 * 60 * 1000
|
||
if (ms < 1000) return { error: 'interval_too_short', min_ms: 1000 }
|
||
if (ms > 86400000) return { error: 'interval_too_long', max_ms: 86400000 }
|
||
return { kind: 'everyMs', everyMs: ms }
|
||
}
|
||
const fields = s.split(/\s+/)
|
||
if (fields.length === 5) return { kind: 'calendar', onCalendar: s }
|
||
return { error: 'bad_interval', hint: 'use 5m, 2h, 1d, or five cron fields' }
|
||
}
|
||
|
||
/**
|
||
* @param {string} id
|
||
*/
|
||
function bareAgentParseIgnoreRules(text) {
|
||
const lines = String(text || '').split(/\r?\n/)
|
||
/** @type {{ pattern: string, negate: boolean, dirOnly: boolean }[]} */
|
||
const rules = []
|
||
for (let i = 0; i < lines.length; i++) {
|
||
let line = String(lines[i] || '').trim()
|
||
if (!line || line.charAt(0) === '#') continue
|
||
let negate = false
|
||
if (line.charAt(0) === '!') {
|
||
negate = true
|
||
line = line.slice(1)
|
||
}
|
||
let dirOnly = false
|
||
if (line.charAt(line.length - 1) === '/') {
|
||
dirOnly = true
|
||
line = line.slice(0, -1)
|
||
}
|
||
if (line.charAt(0) === '/') line = line.slice(1)
|
||
if (!line) continue
|
||
rules.push({ pattern: line, negate, dirOnly })
|
||
}
|
||
return rules
|
||
}
|
||
|
||
/**
|
||
* Last matching gitignore-style rule wins.
|
||
* @param {string} rel
|
||
* @param {boolean} isDir
|
||
* @param {{ pattern: string, negate: boolean, dirOnly: boolean }[]} rules
|
||
*/
|
||
function bareAgentIgnoreMatch(rel, isDir, rules) {
|
||
const n = String(rel || '').replace(/^\/+/, '')
|
||
if (!n || !Array.isArray(rules) || !rules.length) return false
|
||
let ignored = false
|
||
for (let i = 0; i < rules.length; i++) {
|
||
const rule = rules[i]
|
||
if (rule.dirOnly && !isDir) continue
|
||
let hit = false
|
||
if (rule.pattern.indexOf('/') === -1) {
|
||
const parts = n.split('/')
|
||
for (let p = 0; p < parts.length; p++) {
|
||
if (bareAgentGlobMatch(parts[p], rule.pattern)) {
|
||
hit = true
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
hit = bareAgentGlobMatch(n, rule.pattern) || bareAgentGlobMatch(n, '**/' + rule.pattern)
|
||
}
|
||
if (!hit) continue
|
||
ignored = !rule.negate
|
||
}
|
||
return ignored
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
*/
|
||
async function bareAgentLoadIgnoreRules(ctx, root) {
|
||
const names = ['.gitignore', '.agentignore', '.grokignore']
|
||
/** @type {string[]} */
|
||
const chunks = []
|
||
for (let i = 0; i < names.length; i++) {
|
||
const t = await bareAgentReadTextFile(ctx, String(root || '').replace(/\/+$/, '') + '/' + names[i])
|
||
if (t && t.trim()) chunks.push(t)
|
||
}
|
||
return bareAgentParseIgnoreRules(chunks.join('\n'))
|
||
}
|
||
|
||
/**
|
||
* Simple Grok-style fuzzy score (basename + path subsequence). Higher is better.
|
||
* @param {string} query
|
||
* @param {string} path
|
||
*/
|
||
function bareAgentFuzzyScore(query, path) {
|
||
const q = String(query || '').toLowerCase().trim()
|
||
const p = String(path || '')
|
||
if (!q || !p) return 0
|
||
const low = p.toLowerCase()
|
||
const base = (p.split('/').pop() || p).toLowerCase()
|
||
if (base === q) return 1000
|
||
if (base.indexOf(q) === 0) return 800 - Math.min(base.length, 80)
|
||
if (base.indexOf(q) !== -1) return 600 - base.indexOf(q)
|
||
if (low.indexOf(q) !== -1) return 400
|
||
let qi = 0
|
||
let score = 0
|
||
let streak = 0
|
||
for (let i = 0; i < low.length && qi < q.length; i++) {
|
||
if (low.charAt(i) === q.charAt(qi)) {
|
||
qi++
|
||
streak++
|
||
score += 8 + streak * 4
|
||
} else streak = 0
|
||
}
|
||
if (qi < q.length) return 0
|
||
if (base.length && q.length / base.length > 0.5) score += 40
|
||
return score
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
* @param {string} query
|
||
* @param {{ max?: number, maxFiles?: number }} [opts]
|
||
*/
|
||
async function bareAgentFuzzyFind(ctx, root, query, opts) {
|
||
const files = await bareAgentVfsWalkFiles(ctx, root, {
|
||
maxFiles: Math.min(Math.max(Number(opts && opts.maxFiles) || 600, 40), 2000),
|
||
maxDepth: 12
|
||
})
|
||
const cap = Math.min(Math.max(Number(opts && opts.max) || 20, 1), 80)
|
||
/** @type {{ path: string, score: number }[]} */
|
||
const scored = []
|
||
for (let i = 0; i < files.length; i++) {
|
||
const score = bareAgentFuzzyScore(query, files[i])
|
||
if (score <= 0) continue
|
||
scored.push({ path: files[i], score })
|
||
}
|
||
scored.sort(function (a, b) {
|
||
return b.score - a.score
|
||
})
|
||
return scored.slice(0, cap)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} hook
|
||
* @param {string} event
|
||
* @param {string} toolName
|
||
* @param {Record<string, unknown>} args
|
||
* @param {string} [result]
|
||
*/
|
||
function bareAgentHookMatch(hook, event, toolName, args, result) {
|
||
if (!hook || typeof hook !== 'object') return null
|
||
const ev = String(hook.event || hook.type || 'PreToolUse')
|
||
const want = String(event || '')
|
||
if (ev !== want && ev.toLowerCase() !== want.toLowerCase()) return null
|
||
const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : []
|
||
if (tools.length && toolName && tools.indexOf(toolName) === -1) return null
|
||
const reSrc = String(hook.deny_regex || hook.match_regex || hook.denyRegex || '').trim()
|
||
if (reSrc) {
|
||
let re
|
||
try {
|
||
re = new RegExp(reSrc, 'i')
|
||
} catch {
|
||
return null
|
||
}
|
||
const blob =
|
||
toolName +
|
||
' ' +
|
||
String((args && (args.command || args.path || args.file_path || args.pattern)) || '') +
|
||
' ' +
|
||
String(result || '').slice(0, 400)
|
||
if (!re.test(blob)) return null
|
||
}
|
||
return hook
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} hooksDir
|
||
* @param {string} event
|
||
* @param {string} [toolName]
|
||
* @param {Record<string, unknown>} [args]
|
||
* @param {string} [result]
|
||
*/
|
||
async function bareAgentRunHooks(ctx, hooksDir, event, toolName, args, result) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
||
return { deny: '', inject: '' }
|
||
}
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(hooksDir)
|
||
} catch {
|
||
return { deny: '', inject: '' }
|
||
}
|
||
if (!Array.isArray(names)) return { deny: '', inject: '' }
|
||
names = names.filter(function (n) {
|
||
return /\.json$/i.test(String(n || ''))
|
||
})
|
||
names.sort()
|
||
/** @type {string[]} */
|
||
const injects = []
|
||
for (let i = 0; i < names.length; i++) {
|
||
try {
|
||
const t = await bareAgentReadTextFile(ctx, hooksDir + '/' + names[i])
|
||
if (!t.trim()) continue
|
||
const hook = JSON.parse(t)
|
||
const matched = bareAgentHookMatch(hook, event, toolName || '', args || {}, result || '')
|
||
if (!matched) continue
|
||
if (event === 'PreToolUse' || event === 'pre') {
|
||
const reason = String(matched.reason || matched.message || '').trim()
|
||
if (reason && (matched.deny === true || matched.deny_regex || matched.denyRegex)) {
|
||
return { deny: reason.slice(0, 240), inject: '' }
|
||
}
|
||
if (typeof bareAgentHookDenies === 'function') {
|
||
const d = bareAgentHookDenies(matched, toolName || '', args || {})
|
||
if (d) return { deny: d, inject: '' }
|
||
}
|
||
}
|
||
const inj = String(matched.inject || matched.append || '').trim()
|
||
if (inj) injects.push(inj.slice(0, 800))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return { deny: '', inject: injects.join('\n') }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} editsPath
|
||
* @param {{ path: string, prev: string, tool?: string }} rec
|
||
*/
|
||
async function bareAgentPushEdit(ctx, editsPath, rec) {
|
||
const prev = await bareAgentReadJsonFile(ctx, editsPath, [])
|
||
const list = Array.isArray(prev) ? prev : []
|
||
list.push({
|
||
path: String(rec.path || ''),
|
||
prev: String(rec.prev == null ? '' : rec.prev),
|
||
tool: String(rec.tool || ''),
|
||
ts: new Date().toISOString()
|
||
})
|
||
while (list.length > 20) list.shift()
|
||
await bareAgentWriteJsonFile(ctx, editsPath, list)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} editsPath
|
||
*/
|
||
async function bareAgentPopEdit(ctx, editsPath) {
|
||
const prev = await bareAgentReadJsonFile(ctx, editsPath, [])
|
||
const list = Array.isArray(prev) ? prev : []
|
||
const last = list.pop()
|
||
await bareAgentWriteJsonFile(ctx, editsPath, list)
|
||
return last && typeof last === 'object' ? last : null
|
||
}
|
||
|
||
/**
|
||
* @param {unknown[]} messages
|
||
* @param {string} query
|
||
* @param {number} [max]
|
||
*/
|
||
function bareAgentHistorySearch(messages, query, max) {
|
||
const tokens = bareAgentMemoryTokens(query)
|
||
const cap = Math.min(Math.max(Number(max) || 8, 1), 24)
|
||
if (!Array.isArray(messages) || !tokens.length) return []
|
||
/** @type {{ role: string, score: number, snippet: string }[]} */
|
||
const hits = []
|
||
for (let i = 0; i < messages.length; i++) {
|
||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||
if (!m) continue
|
||
const role = String(m.role || '')
|
||
if (role !== 'user' && role !== 'assistant') continue
|
||
const text = String(m.content || '')
|
||
const score = bareAgentMemoryScore(text, tokens)
|
||
if (score <= 0) continue
|
||
hits.push({
|
||
role,
|
||
score: Math.round(score * 1000) / 1000,
|
||
snippet: text.replace(/\s+/g, ' ').trim().slice(0, 280)
|
||
})
|
||
}
|
||
hits.sort(function (a, b) {
|
||
return b.score - a.score
|
||
})
|
||
return hits.slice(0, cap)
|
||
}
|
||
|
||
function bareAgentScheduleId(id) {
|
||
let s = String(id || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9-]+/g, '-')
|
||
.replace(/^-+|-+$/g, '')
|
||
if (!s) s = 'task'
|
||
if (s.indexOf('agent-') !== 0) s = 'agent-' + s
|
||
return s.slice(0, 40)
|
||
}
|
||
|
||
/**
|
||
* User-turn rewind points (Grok /rewind). Index 0 is the first user message.
|
||
* @param {unknown[]} messages
|
||
*/
|
||
function bareAgentRewindPoints(messages) {
|
||
/** @type {{ userIndex: number, messageIndex: number, preview: string }[]} */
|
||
const points = []
|
||
if (!Array.isArray(messages)) return points
|
||
for (let i = 0; i < messages.length; i++) {
|
||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||
if (!m || String(m.role || '') !== 'user') continue
|
||
points.push({
|
||
userIndex: points.length,
|
||
messageIndex: i,
|
||
preview: String(m.content || '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 140)
|
||
})
|
||
}
|
||
return points
|
||
}
|
||
|
||
/**
|
||
* Drop the last N user turns (and everything after that user message).
|
||
* userIndex rewinds to that user turn (keeps messages before it).
|
||
* keep_user keeps the target user message so the prompt can be retried.
|
||
* @param {unknown[]} messages
|
||
* @param {{ steps?: number, userIndex?: number, keep_user?: boolean }} [opts]
|
||
*/
|
||
function bareAgentRewindHistory(messages, opts) {
|
||
const list = Array.isArray(messages) ? messages.slice() : []
|
||
const points = bareAgentRewindPoints(list)
|
||
if (!points.length) return { ok: false, error: 'nothing_to_rewind', messages: list, dropped: 0 }
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
let target
|
||
if (o.userIndex != null && Number.isFinite(Number(o.userIndex))) {
|
||
target = points[Math.floor(Number(o.userIndex))]
|
||
if (!target) return { ok: false, error: 'bad_user_index', messages: list, dropped: 0 }
|
||
} else {
|
||
const steps = Math.min(points.length, Math.max(1, Math.floor(Number(o.steps) || 1)))
|
||
target = points[points.length - steps]
|
||
}
|
||
const keepUser = Boolean(o.keep_user)
|
||
const cut = keepUser ? target.messageIndex : target.messageIndex - 1
|
||
const next = cut < 0 ? [] : list.slice(0, cut + 1)
|
||
return {
|
||
ok: true,
|
||
messages: next,
|
||
dropped: list.length - next.length,
|
||
target: target,
|
||
keep_user: keepUser
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Markdown transcript (Grok /export).
|
||
* @param {unknown[]} messages
|
||
*/
|
||
function bareAgentExportTranscript(messages) {
|
||
const lines = [
|
||
'# Agent session export',
|
||
'',
|
||
'Exported: ' + new Date().toISOString(),
|
||
''
|
||
]
|
||
const list = Array.isArray(messages) ? messages : []
|
||
for (let i = 0; i < list.length; i++) {
|
||
const m = list[i] && typeof list[i] === 'object' ? list[i] : {}
|
||
const role = String(m.role || 'unknown')
|
||
let content = String(m.content || '')
|
||
if (role === 'tool' && content.length > 1200) content = content.slice(0, 1200) + '\n… truncated'
|
||
const calls = Array.isArray(m.tool_calls) ? m.tool_calls : []
|
||
const names = []
|
||
for (let c = 0; c < calls.length; c++) {
|
||
const fn = calls[c] && calls[c].function ? calls[c].function : {}
|
||
if (fn && fn.name) names.push(String(fn.name))
|
||
}
|
||
lines.push('## ' + String(i + 1) + '. ' + role)
|
||
lines.push('')
|
||
if (names.length) lines.push('tools: ' + names.join(', '))
|
||
lines.push(content || '(empty)')
|
||
lines.push('')
|
||
}
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* Line-based unified diff (Grok-style file compare, no git required).
|
||
* @param {string} oldText
|
||
* @param {string} newText
|
||
* @param {{ from?: string, to?: string, context?: number }} [opts]
|
||
*/
|
||
function bareAgentUnifiedDiff(oldText, newText, opts) {
|
||
const pathA = String((opts && opts.from) || 'a')
|
||
const pathB = String((opts && opts.to) || 'b')
|
||
const ctxN = Math.min(8, Math.max(0, Math.floor(Number(opts && opts.context) || 3)))
|
||
const a = String(oldText || '')
|
||
.replace(/\r\n/g, '\n')
|
||
.split('\n')
|
||
const b = String(newText || '')
|
||
.replace(/\r\n/g, '\n')
|
||
.split('\n')
|
||
if (a.length && a[a.length - 1] === '') a.pop()
|
||
if (b.length && b[b.length - 1] === '') b.pop()
|
||
if (a.join('\n') === b.join('\n')) {
|
||
return { ok: true, identical: true, text: '', added: 0, removed: 0 }
|
||
}
|
||
const maxLines = 800
|
||
if (a.length > maxLines || b.length > maxLines) {
|
||
return {
|
||
ok: true,
|
||
truncated: true,
|
||
identical: false,
|
||
text:
|
||
'--- ' +
|
||
pathA +
|
||
'\n+++ ' +
|
||
pathB +
|
||
'\n@@ files too large for inline LCS diff (' +
|
||
a.length +
|
||
'/' +
|
||
b.length +
|
||
' lines) @@\n',
|
||
added: 0,
|
||
removed: 0
|
||
}
|
||
}
|
||
const n = a.length
|
||
const m = b.length
|
||
/** @type {number[][]} */
|
||
const dp = new Array(n + 1)
|
||
for (let i = 0; i <= n; i++) {
|
||
dp[i] = new Array(m + 1)
|
||
for (let j = 0; j <= m; j++) dp[i][j] = 0
|
||
}
|
||
for (let i = 1; i <= n; i++) {
|
||
for (let j = 1; j <= m; j++) {
|
||
dp[i][j] =
|
||
a[i - 1] === b[j - 1]
|
||
? dp[i - 1][j - 1] + 1
|
||
: dp[i - 1][j] >= dp[i][j - 1]
|
||
? dp[i - 1][j]
|
||
: dp[i][j - 1]
|
||
}
|
||
}
|
||
/** @type {{ op: string, line: string }[]} */
|
||
const ops = []
|
||
let i = n
|
||
let j = m
|
||
while (i > 0 || j > 0) {
|
||
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
||
ops.push({ op: ' ', line: a[i - 1] })
|
||
i--
|
||
j--
|
||
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||
ops.push({ op: '+', line: b[j - 1] })
|
||
j--
|
||
} else {
|
||
ops.push({ op: '-', line: a[i - 1] })
|
||
i--
|
||
}
|
||
}
|
||
ops.reverse()
|
||
let added = 0
|
||
let removed = 0
|
||
for (let k = 0; k < ops.length; k++) {
|
||
if (ops[k].op === '+') added++
|
||
else if (ops[k].op === '-') removed++
|
||
}
|
||
const lines = ['--- ' + pathA, '+++ ' + pathB]
|
||
let idx = 0
|
||
while (idx < ops.length) {
|
||
while (idx < ops.length && ops[idx].op === ' ') idx++
|
||
if (idx >= ops.length) break
|
||
let start = Math.max(0, idx - ctxN)
|
||
let end = idx
|
||
while (end < ops.length) {
|
||
if (ops[end].op !== ' ') {
|
||
end++
|
||
continue
|
||
}
|
||
let run = 0
|
||
let p = end
|
||
while (p < ops.length && ops[p].op === ' ') {
|
||
run++
|
||
p++
|
||
}
|
||
if (run > ctxN * 2) {
|
||
end += ctxN
|
||
break
|
||
}
|
||
end = p
|
||
}
|
||
end = Math.min(ops.length, end)
|
||
let oldLine = 1
|
||
let newLine = 1
|
||
for (let k = 0; k < start; k++) {
|
||
if (ops[k].op !== '+') oldLine++
|
||
if (ops[k].op !== '-') newLine++
|
||
}
|
||
let oldCount = 0
|
||
let newCount = 0
|
||
for (let k = start; k < end; k++) {
|
||
if (ops[k].op !== '+') oldCount++
|
||
if (ops[k].op !== '-') newCount++
|
||
}
|
||
lines.push(
|
||
'@@ -' +
|
||
String(oldLine) +
|
||
',' +
|
||
String(oldCount) +
|
||
' +' +
|
||
String(newLine) +
|
||
',' +
|
||
String(newCount) +
|
||
' @@'
|
||
)
|
||
for (let k = start; k < end; k++) {
|
||
lines.push(ops[k].op + ops[k].line)
|
||
}
|
||
idx = end
|
||
}
|
||
return {
|
||
ok: true,
|
||
identical: false,
|
||
truncated: false,
|
||
text: lines.join('\n') + '\n',
|
||
added,
|
||
removed
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Definition-oriented regex for a symbol name (JS / Python / Rust / C-like).
|
||
* @param {string} name
|
||
*/
|
||
function bareAgentSymbolRegex(name) {
|
||
const esc = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||
if (!esc) return ''
|
||
return (
|
||
'(?:(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+' +
|
||
esc +
|
||
'\\b|(?:export\\s+)?(?:default\\s+)?class\\s+' +
|
||
esc +
|
||
'\\b|(?:export\\s+)?(?:const|let|var)\\s+' +
|
||
esc +
|
||
'\\b|def\\s+' +
|
||
esc +
|
||
'\\s*\\(|fn\\s+' +
|
||
esc +
|
||
'\\b|' +
|
||
esc +
|
||
'\\s*=\\s*(?:async\\s+)?(?:function|\\(|class))'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
* @param {string} name
|
||
* @param {{ glob?: string, max?: number }} [opts]
|
||
*/
|
||
async function bareAgentFindSymbol(ctx, root, name, opts) {
|
||
const pattern = bareAgentSymbolRegex(name)
|
||
if (!pattern) return { ok: false, error: 'name_required' }
|
||
const out = await bareAgentGrepFiles(ctx, {
|
||
pattern,
|
||
root,
|
||
glob: opts && opts.glob,
|
||
max_matches: opts && opts.max ? opts.max : 40
|
||
})
|
||
if (!out || out.ok === false) return out
|
||
return {
|
||
ok: true,
|
||
name,
|
||
root,
|
||
matches: out.matches || [],
|
||
count: out.count || 0,
|
||
truncated: Boolean(out.truncated)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Grok list_dir-style BFS tree (bounded).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} root
|
||
* @param {{ max?: number, maxDepth?: number }} [opts]
|
||
*/
|
||
async function bareAgentRenderTree(ctx, root, opts) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return { ok: false, error: 'readdir unavailable' }
|
||
}
|
||
const base = String(root || '/').replace(/\/+$/, '') || '/'
|
||
const maxItems = Math.min(Math.max(Number(opts && opts.max) || 200, 20), 800)
|
||
const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 6, 1), 12)
|
||
const skip = Object.create(null)
|
||
skip['.git'] = 1
|
||
skip['node_modules'] = 1
|
||
skip['.bare-os'] = 1
|
||
/** @type {string[]} */
|
||
const lines = [base + '/']
|
||
/** @type {{ dir: string, prefix: string, depth: number }[]} */
|
||
const queue = [{ dir: base, prefix: '', depth: 0 }]
|
||
let count = 1
|
||
let truncated = false
|
||
while (queue.length && count < maxItems) {
|
||
const cur = queue.shift()
|
||
if (!cur) break
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(cur.dir)
|
||
} catch {
|
||
continue
|
||
}
|
||
if (!Array.isArray(names)) continue
|
||
names = names
|
||
.map(function (n) {
|
||
return String(n || '')
|
||
})
|
||
.filter(function (n) {
|
||
return n && n !== '.' && n !== '..' && !skip[n]
|
||
})
|
||
.sort()
|
||
for (let i = 0; i < names.length && count < maxItems; i++) {
|
||
const name = names[i]
|
||
const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name
|
||
const isDir = await bareAgentVfsIsDir(ctx, full)
|
||
const last = i === names.length - 1
|
||
const branch = last ? '`-- ' : '|-- '
|
||
lines.push(cur.prefix + branch + name + (isDir ? '/' : ''))
|
||
count++
|
||
if (isDir && cur.depth + 1 < maxDepth) {
|
||
queue.push({
|
||
dir: full,
|
||
prefix: cur.prefix + (last ? ' ' : '| '),
|
||
depth: cur.depth + 1
|
||
})
|
||
}
|
||
}
|
||
if (count >= maxItems) {
|
||
truncated = true
|
||
break
|
||
}
|
||
}
|
||
return { ok: true, path: base, tree: lines.join('\n'), count, truncated }
|
||
}
|
||
|
||
/**
|
||
* VFS copy (file or directory). Dest parents are created.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} from
|
||
* @param {string} to
|
||
*/
|
||
async function bareAgentCopyPath(ctx, from, to) {
|
||
const src = String(from || '').replace(/\/+$/, '')
|
||
let dest = String(to || '')
|
||
if (!src || !dest) return { ok: false, error: 'from_and_to_required' }
|
||
const srcIsDir = await bareAgentVfsIsDir(ctx, src)
|
||
const destIsDir = await bareAgentVfsIsDir(ctx, dest.replace(/\/+$/, ''))
|
||
if (destIsDir) {
|
||
const base = src.split('/').pop() || 'copy'
|
||
dest = dest.replace(/\/+$/, '') + '/' + base
|
||
}
|
||
if (!srcIsDir) {
|
||
const text = await bareAgentReadTextFile(ctx, src)
|
||
await bareAgentWriteTextFile(ctx, dest, text)
|
||
return { ok: true, from: src, to: dest, kind: 'file' }
|
||
}
|
||
const files = await bareAgentVfsWalkFiles(ctx, src, { maxFiles: 400, maxDepth: 12 })
|
||
for (let i = 0; i < files.length; i++) {
|
||
const rel = files[i].slice(src.length)
|
||
const text = await bareAgentReadTextFile(ctx, files[i])
|
||
await bareAgentWriteTextFile(ctx, dest + rel, text)
|
||
}
|
||
return { ok: true, from: src, to: dest, kind: 'directory', files: files.length }
|
||
}
|
||
|
||
/**
|
||
* SKILL.md body with YAML frontmatter (Grok skill format).
|
||
* @param {{ name: string, description: string, body?: string }} spec
|
||
*/
|
||
function bareAgentSkillMarkdown(spec) {
|
||
const name = String((spec && spec.name) || '').trim() || 'skill'
|
||
const description = String((spec && spec.description) || '').trim() || name
|
||
const body = String((spec && spec.body) || '').trim() || '# ' + name + '\n'
|
||
return (
|
||
'---\nname: ' +
|
||
name.replace(/\n/g, ' ') +
|
||
'\ndescription: ' +
|
||
description.replace(/\n/g, ' ') +
|
||
'\n---\n\n' +
|
||
body +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Walk-up Grok/Claude/Cursor skill roots (project-local).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} startDir
|
||
*/
|
||
async function bareAgentDiscoverProjectSkillRoots(ctx, startDir) {
|
||
const vfs = ctx && ctx.vfs
|
||
if (!vfs || typeof vfs.readdir !== 'function') return []
|
||
/** @type {{ path: string, source: string }[]} */
|
||
const roots = []
|
||
const seen = Object.create(null)
|
||
let dir = String(startDir || '').replace(/\/+$/, '') || '/'
|
||
const suffixes = ['.grok/skills', '.agents/skills', '.claude/skills', '.cursor/skills']
|
||
for (let hop = 0; hop < 12; hop++) {
|
||
for (let i = 0; i < suffixes.length; i++) {
|
||
const p = (dir === '/' ? '' : dir) + '/' + suffixes[i]
|
||
if (seen[p]) continue
|
||
seen[p] = 1
|
||
try {
|
||
const names = await vfs.readdir(p)
|
||
if (Array.isArray(names) && names.length) roots.push({ path: p, source: 'project' })
|
||
} catch {
|
||
/* missing */
|
||
}
|
||
}
|
||
if (dir === '/') break
|
||
const parent = dir.replace(/\/[^/]+$/, '') || '/'
|
||
if (parent === dir) break
|
||
dir = parent
|
||
}
|
||
return roots
|
||
}
|
||
|
||
/** OpenAI-style tool schemas + dispatch (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* Host-checkout verification hints (keep loosely aligned with scripts/lib/agent-check-hints-data.mjs).
|
||
* @param {string} combined paths + topic
|
||
* @returns {string[]}
|
||
*/
|
||
function bareAgentVerificationHintsList(combined) {
|
||
const c = String(combined || '').toLowerCase()
|
||
/** @type {string[]} */
|
||
const hints = []
|
||
if (/(^|\/)kernel\/|kernel\\|\/boot\/init|lib\/init|lib\\init/.test(c)) {
|
||
hints.push('npm run bundle:kernel', 'node scripts/verify-kernel-seeder-parity.mjs')
|
||
}
|
||
if (/bare-os-coreutils|kernel\/bin|kernel\\bin/.test(c)) {
|
||
hints.push('npm run build -w bare-os-coreutils', 'node scripts/verify-man-coverage.mjs')
|
||
}
|
||
if (/bare-os-booter|bare-os-ctx-api/.test(c)) {
|
||
hints.push(
|
||
'npm run test -w bare-os-booter',
|
||
'node scripts/verify-ctx-api-feature-bits.mjs'
|
||
)
|
||
}
|
||
if (/bare-os-protocol|seed-rpc|channel\.js/.test(c)) {
|
||
hints.push('npm run test -w bare-os-protocol')
|
||
}
|
||
if (/bare-os-seeder/.test(c)) {
|
||
hints.push('node scripts/verify-kernel-seeder-parity.mjs')
|
||
}
|
||
if (/bare-os-bare-libs|kernel\/lib\/bare|kernel\\lib\\bare/.test(c)) {
|
||
hints.push('npm run build -w bare-os-bare-libs', 'node scripts/verify-bundle-health.mjs')
|
||
}
|
||
if (/shell|sh\.js|test\.js/.test(c) && /booter/.test(c)) {
|
||
hints.push('npm run test:shell-fast')
|
||
}
|
||
if (/docs\/|handbook\/|developer-guide\//.test(c)) {
|
||
hints.push('npm run pretest', 'node scripts/verify-doc-links.mjs')
|
||
}
|
||
if (!hints.length) hints.push('npm run pretest', 'npm test')
|
||
return [...new Set(hints)]
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentShellQuote(s) {
|
||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentJsonResult(v) {
|
||
try {
|
||
return JSON.stringify(v)
|
||
} catch {
|
||
return '{"error":"json_stringify_failed"}'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentToolDefinitions() {
|
||
return [
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_file',
|
||
description:
|
||
'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...). Optional offset/limit return numbered line slices (Grok-style).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute file path' },
|
||
file_path: { type: 'string', description: 'Alias for path' },
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes to read (default 256000)'
|
||
},
|
||
offset: {
|
||
type: 'integer',
|
||
description: '1-based start line for a numbered slice'
|
||
},
|
||
limit: {
|
||
type: 'integer',
|
||
description: 'Max lines to return from offset'
|
||
},
|
||
numbered: {
|
||
type: 'boolean',
|
||
description: 'Prefix N→ on each line when slicing (default true if offset/limit set)'
|
||
}
|
||
},
|
||
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: full replacement via content, or search/replace. old_string must be unique unless replace_all is true.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
file_path: { type: 'string', description: 'Alias for path' },
|
||
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' },
|
||
replace_all: {
|
||
type: 'boolean',
|
||
description: 'Replace every occurrence of old_string (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_directory',
|
||
description: 'Create a directory (recursive).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_files',
|
||
description:
|
||
'Run grep -R to list paths matching a pattern (bounded). Uses the shell.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string' },
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to search (default /home)'
|
||
},
|
||
max_lines: { type: 'integer', description: 'Default 200' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_command',
|
||
description:
|
||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<code> line.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: {
|
||
type: 'string',
|
||
description: 'Full command string (e.g. ls -la /bin)'
|
||
},
|
||
cwd: {
|
||
type: 'string',
|
||
description: 'Working directory (cd there first). Persists as ~/.agent/last_cwd.'
|
||
},
|
||
timeout_ms: { type: 'integer' },
|
||
capture_exit: {
|
||
type: 'boolean',
|
||
description: 'If true, append last line EXIT:<code> to capture (default false)'
|
||
}
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script',
|
||
description:
|
||
'REQUIRED to run agent-authored JavaScript: Node is not installed. Writes code to ~/.agent/_tmp_agent_run.mjs and runs it by absolute path (Bare kernel — same as /bin scripts). Do not use run_command with node/npm/npx. Prefer async function run(ctx, argv). stdout/stderr captured.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
code: { type: 'string', description: 'Full ESM/CommonJS script body' }
|
||
},
|
||
required: ['code']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_system_info',
|
||
description:
|
||
'Lightweight context: API version, uname, optional resource hook. Questions about **which kernel features are on/off in this session** → read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json; not apropos_man. For other /proc JSON use read_proc_file; swarm → get_swarm_peers; resource table → get_resource_limits. want=capabilities|swarm still returns those blobs when needed.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
want: {
|
||
type: 'string',
|
||
enum: ['summary', 'capabilities', 'swarm'],
|
||
description: 'Optional focus (default summary)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'edit_agent_config',
|
||
description:
|
||
'Merge keys into ~/.agent/config.json (shallow merge for known keys only).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
patch: {
|
||
type: 'object',
|
||
description:
|
||
'Partial config object (rest_base_url, model, temperature, owner_name, agent_label, …)'
|
||
}
|
||
},
|
||
required: ['patch']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_bin',
|
||
description: 'List Tier-1 utilities in /bin via VFS.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
limit: { type: 'integer', description: 'Max names (default 400)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_directory',
|
||
description:
|
||
'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute directory path' },
|
||
max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' },
|
||
include_stat: {
|
||
type: 'boolean',
|
||
description: 'If true, call stat on each entry (slower; default false)'
|
||
},
|
||
tree: {
|
||
type: 'boolean',
|
||
description: 'Grok-style bounded BFS tree instead of a flat listing'
|
||
},
|
||
max_depth: { type: 'integer', description: 'Tree depth (default 6)' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'file_stat',
|
||
description:
|
||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
follow_symlinks: {
|
||
type: 'boolean',
|
||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'move_path',
|
||
description:
|
||
'Rename or move a file or directory via shell mv. Any writable VFS path is allowed; the read-only base system (/bin, /etc, /boot, /lib, /usr, /share, /proc, /dev) is denied.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'delete_path',
|
||
description:
|
||
'Delete a file or directory (recursive optional). Allowed by default. Optional confirm_token only if require_confirm_token is set. Cannot delete the read-only base system.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
recursive: {
|
||
type: 'boolean',
|
||
description: 'Remove directories recursively (default false)'
|
||
},
|
||
confirm_token: {
|
||
type: 'string',
|
||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||
}
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_man_page',
|
||
description:
|
||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||
section: {
|
||
type: 'integer',
|
||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||
},
|
||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||
},
|
||
required: ['topic']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apropos_man',
|
||
description:
|
||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines. This is documentation search only—never use it to answer what kernel features are currently enabled or disabled (use read_proc_file on /proc/bare_os/features).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
keyword: { type: 'string' },
|
||
max_results: { type: 'integer', description: 'Default 40, max 200' }
|
||
},
|
||
required: ['keyword']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_proc_file',
|
||
description:
|
||
'Read any /proc path (bounded), including the full /proc/bare_os kernel surface. Canonical live feature state: /proc/bare_os/features or features.json. Prefer this over shelling cat.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: {
|
||
type: 'string',
|
||
description:
|
||
'Absolute /proc path (any kernel node). Examples: /proc/bare_os/features.json, /proc/bare_os/capabilities.json, /proc/bare_os/swarm.json.'
|
||
},
|
||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_swarm_peers',
|
||
description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_resource_limits',
|
||
description:
|
||
'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_js_script_at_path',
|
||
description:
|
||
'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute path to .mjs file' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'web_fetch',
|
||
description:
|
||
'Fetch live HTTP(S) URLs and return structured content for the assistant. Uses ctx.httpFetch (same policy as wget/curl: BARE_OS_HTTP_ALLOWLIST / DENYLIST). For official docs index use read_man_page / apropos_man — they are not web pages. Supports GET/HEAD/POST and extract modes: auto (JSON vs HTML vs text), markdownish plain text from HTML, links (anchor hrefs), meta (title/og:), raw UTF-8 slice, or json parse.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
url: { type: 'string', description: 'Absolute http(s) URL' },
|
||
method: {
|
||
type: 'string',
|
||
description: 'HTTP method (default GET)',
|
||
enum: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
||
},
|
||
headers: {
|
||
type: 'object',
|
||
description: 'Optional header map (string values only)'
|
||
},
|
||
body: {
|
||
type: 'string',
|
||
description: 'Request body for non-GET (e.g. JSON string for APIs)'
|
||
},
|
||
content_type: {
|
||
type: 'string',
|
||
description: 'Content-Type when body is set (default application/octet-stream)'
|
||
},
|
||
format: {
|
||
type: 'string',
|
||
enum: ['auto', 'json', 'markdownish', 'text', 'links', 'meta', 'raw'],
|
||
description:
|
||
'auto: sniff Content-Type; json: parse JSON; markdownish/text: strip HTML to readable text; links: absolute http(s) links; meta: title/description/og tags; raw: bounded UTF-8 text'
|
||
},
|
||
max_response_bytes: {
|
||
type: 'integer',
|
||
description: 'Cap downloaded bytes (default 524288, max 2MiB)'
|
||
},
|
||
max_redirects: {
|
||
type: 'integer',
|
||
description: 'Max redirects to follow (default 5)'
|
||
},
|
||
timeout_ms: {
|
||
type: 'integer',
|
||
description: 'Per-request timeout ms (default 30000, max 120000)'
|
||
},
|
||
max_links: {
|
||
type: 'integer',
|
||
description: 'Max links when format=links (default 200)'
|
||
}
|
||
},
|
||
required: ['url']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_skill',
|
||
description:
|
||
'Load the full SKILL.md for a modular agent skill (folder id or frontmatter name, case-insensitive). Workspace ~/.agent/workspace/skills/ overrides ~/.agent/skills/. Use after checking the compact skills index in the system prompt.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
skill: {
|
||
type: 'string',
|
||
description: 'Skill folder name (e.g. p2p-os-status) or YAML frontmatter name'
|
||
},
|
||
max_bytes: {
|
||
type: 'integer',
|
||
description: 'Max bytes of SKILL.md (default 256000)'
|
||
}
|
||
},
|
||
required: ['skill']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_services',
|
||
description:
|
||
'List initd service definitions and current runtime phases from /proc/bare_os/initd_readiness.json when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'service_status',
|
||
description:
|
||
'Read one initd unit status by name from /proc/bare_os/initd_readiness.json and include journal hint paths when present.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string', description: 'Unit name, e.g. bare-cron' }
|
||
},
|
||
required: ['name']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_timers',
|
||
description:
|
||
'List user timer drop-ins from ~/.config/bare-os/timers and optionally include short file previews.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
include_preview: {
|
||
type: 'boolean',
|
||
description: 'Include bounded timer file text previews (default false)'
|
||
},
|
||
max_entries: {
|
||
type: 'integer',
|
||
description: 'Maximum timer files to return (default 128, max 512)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_cron_log',
|
||
description:
|
||
'Read /var/log/bare-os/cron.log with bounded output and optional tail mode.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 12000)'
|
||
},
|
||
tail_only: {
|
||
type: 'boolean',
|
||
description: 'When true, return only the trailing max_chars slice'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_audit_log',
|
||
description:
|
||
'Read /var/log/bare-os/audit.log with bounded output and best-effort secret redaction.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 12000)'
|
||
},
|
||
tail_only: {
|
||
type: 'boolean',
|
||
description: 'When true, return only the trailing max_chars slice'
|
||
},
|
||
redact: {
|
||
type: 'boolean',
|
||
description: 'Apply lightweight token redaction (default true)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_boot_policy',
|
||
description:
|
||
'Read /etc/bare-os/boot.policy.json and return text plus parsed JSON when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 20000)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_kernel_extension_resolution',
|
||
description:
|
||
'Read /run/bare-os/kernel-ext-resolution.json for extension ordering, conflicts, and pin outcomes.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: {
|
||
type: 'integer',
|
||
description: 'Maximum returned characters (default 20000)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_initd_graph',
|
||
description:
|
||
'Read initd dependency DAG/readiness graph from /proc/bare_os/initd_dag.json and /proc/bare_os/initd_readiness.json.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_unit_journal',
|
||
description:
|
||
'Read a bounded/redacted tail of /run/bare-os/unit-journal/<unit>.ndjson.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
unit: { type: 'string', description: 'Initd unit name, e.g. bare-cron' },
|
||
max_chars: { type: 'integer', description: 'Maximum output chars (default 12000)' },
|
||
tail_only: { type: 'boolean', description: 'Return trailing max_chars only' }
|
||
},
|
||
required: ['unit']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'inspect_ipc_backpressure',
|
||
description:
|
||
'Inspect IPC/backpressure operator snapshots from /proc/bare_os JSON surfaces.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_network_summary',
|
||
description:
|
||
'Read a typed network/swarm summary from /proc/bare_os surfaces with best-effort fallback paths.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'tail_telemetry_streams',
|
||
description:
|
||
'Read bounded/redacted tails from telemetry logs such as /var/log/bare-os/audit.log, logger.jsonl, and initd logs.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max_chars: { type: 'integer', description: 'Maximum chars per stream (default 8000)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'pkg_index_lookup',
|
||
description:
|
||
'Run pkg-swarm-index lookup and return parsed output for one package key.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
key: { type: 'string', description: 'Package key to lookup' }
|
||
},
|
||
required: ['key']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_verification_scripts',
|
||
description:
|
||
'List known verification scripts from /scripts and summarize likely check families.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_maintenance_gate',
|
||
description:
|
||
'Run one named maintenance check with bounded capture (id maps to a known verifier). Use run_command for arbitrary guest commands.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
command: { type: 'string', description: 'Allowlisted command id' },
|
||
cwd: { type: 'string', description: 'Optional working directory' },
|
||
timeout_ms: { type: 'integer', description: 'Timeout in milliseconds' }
|
||
},
|
||
required: ['command']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_contract_checks',
|
||
description:
|
||
'Run a grouped set of contract checks by profile id (allowlisted) with bounded output.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
profile: { type: 'string', description: 'Check profile id, e.g. core, docs, parity' }
|
||
},
|
||
required: ['profile']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'summarize_build_drift',
|
||
description:
|
||
'Summarize build/generated drift by comparing git status and key generated artifacts.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_hrpc_bridge_health',
|
||
description:
|
||
'Read HRPC bridge/operator health from /proc/bare_os surfaces and include host capability hints when available.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'get_hrpc_allowlist_status',
|
||
description:
|
||
'Inspect effective HRPC allowlist/probe status using hrpc probe and operator snapshots.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'emit_host_notification',
|
||
description:
|
||
'Request an audited host notification via HRPC route. Enabled by default; emergency_stop_mutations or a denylist can still block.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
title: { type: 'string' },
|
||
message: { type: 'string' },
|
||
level: { type: 'string', enum: ['info', 'warn', 'error'] }
|
||
},
|
||
required: ['title', 'message']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'request_host_action',
|
||
description:
|
||
'Request a schema-validated host action through HRPC. Enabled by default; emergency_stop_mutations or a denylist can still block.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
action: { type: 'string', description: 'Host action id' },
|
||
payload: { type: 'object', description: 'Action payload object' }
|
||
},
|
||
required: ['action']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run',
|
||
description:
|
||
'Start an autonomous coding run with a goal. Enables autonomous mode, keeps the ReAct loop going until task_complete, stop, or timebox. Optional scope path, runtime cap, and quality-gate ids.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
goal: { type: 'string', description: 'Task goal the agent should complete autonomously' },
|
||
scope_path: { type: 'string', description: 'Preferred working scope path (optional)' },
|
||
max_runtime_ms: { type: 'integer', description: 'Optional runtime cap override' },
|
||
required_checks: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description: 'Optional quality gates (allowlisted check ids)'
|
||
}
|
||
},
|
||
required: ['goal']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run_status',
|
||
description:
|
||
'Return current autonomous run state, elapsed/runtime budget, configured checks, and latest status.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'autonomous_run_stop',
|
||
description:
|
||
'Request manual stop for an autonomous run; loop will stop safely on next control checkpoint.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
reason: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'verification_hints',
|
||
description:
|
||
'Suggest npm/node verification commands for a developer working at the Bare OS git checkout on the host (paths or topic keywords). Does not run commands.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
topic: {
|
||
type: 'string',
|
||
description: 'Free-text task or area (e.g. kernel init, seed RPC, shell)'
|
||
},
|
||
paths_touched: {
|
||
type: 'string',
|
||
description:
|
||
'Optional comma-separated path-like strings from the repo (forward slashes ok)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'runtime_diagnostic_bundle',
|
||
description:
|
||
'Non-secret snapshot: ctx API version, optional resource status, and every readable /proc/bare_os file. Prefer over many separate read_proc_file calls.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'search_replace',
|
||
description:
|
||
'Replace old_string with new_string in a file. Match must be unique unless replace_all is true. Same uniqueness rules as Grok Build edit_file.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' },
|
||
file_path: { type: 'string', description: 'Alias for path' },
|
||
old_string: { type: 'string' },
|
||
new_string: { type: 'string' },
|
||
replace_all: { type: 'boolean' }
|
||
},
|
||
required: ['old_string', 'new_string']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'glob_files',
|
||
description:
|
||
'Walk the VFS from root and return paths matching a glob (supports ** and *). Prefer this over run_command find/ls.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: {
|
||
type: 'string',
|
||
description: 'Glob such as **/*.js or src/**/foo.md'
|
||
},
|
||
root: {
|
||
type: 'string',
|
||
description: 'Directory to walk (default session home)'
|
||
},
|
||
max_results: { type: 'integer', description: 'Default 100, cap 400' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'todo_write',
|
||
description:
|
||
'Create or update the session todo list (Grok-style). merge=true updates by id; merge=false replaces the list.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
todos: {
|
||
type: 'array',
|
||
description: 'Items with id, optional content, status pending|in_progress|completed|cancelled',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string' },
|
||
content: { type: 'string' },
|
||
status: { type: 'string' }
|
||
},
|
||
required: ['id']
|
||
}
|
||
},
|
||
merge: {
|
||
type: 'boolean',
|
||
description: 'Default true: update matching ids, keep others'
|
||
}
|
||
},
|
||
required: ['todos']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'memory_search',
|
||
description:
|
||
'Search ~/.agent/workspace/memory, MEMORY.md, and compact.md by keyword (guest-safe, no embeddings).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
query: { type: 'string' },
|
||
max_hits: { type: 'integer', description: 'Default 8' }
|
||
},
|
||
required: ['query']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'memory_get',
|
||
description:
|
||
'Read one memory file. Path must be under ~/.agent/workspace/memory or a named file there.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string', description: 'Absolute path or basename under workspace/memory' },
|
||
max_bytes: { type: 'integer' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'enter_plan_mode',
|
||
description:
|
||
'Switch to read-only plan mode. Writes are denied except ~/.agent/plan.md until exit_plan_mode.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
note: { type: 'string', description: 'Optional starter text appended to plan.md' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'exit_plan_mode',
|
||
description: 'Leave plan mode and allow mutating tools again.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
summary: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'ask_user_question',
|
||
description:
|
||
'Pose one or more multiple-choice questions to the user (Grok-style). Persists to ~/.agent/ask.json.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
questions: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
question: { type: 'string' },
|
||
options: {
|
||
type: 'array',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
label: { type: 'string' },
|
||
description: { type: 'string' }
|
||
}
|
||
}
|
||
},
|
||
multi_select: { type: 'boolean' }
|
||
},
|
||
required: ['question']
|
||
}
|
||
}
|
||
},
|
||
required: ['questions']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'grep',
|
||
description:
|
||
'Search file contents with a JS regular expression (Grok-style, VFS-native). Prefer this over run_command grep. Supports glob, ignore_case, context lines, and files_with_matches.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: {
|
||
type: 'string',
|
||
description: 'JavaScript regular expression (no surrounding slashes)'
|
||
},
|
||
path: {
|
||
type: 'string',
|
||
description: 'Directory or file to search (default session home)'
|
||
},
|
||
root: { type: 'string', description: 'Alias for path' },
|
||
glob: {
|
||
type: 'string',
|
||
description: 'Optional file glob such as **/*.js'
|
||
},
|
||
ignore_case: { type: 'boolean' },
|
||
before: { type: 'integer', description: 'Context lines before each match' },
|
||
after: { type: 'integer', description: 'Context lines after each match' },
|
||
context: { type: 'integer', description: 'Context lines before and after' },
|
||
max_matches: { type: 'integer', description: 'Default 50, cap 200' },
|
||
files_with_matches: {
|
||
type: 'boolean',
|
||
description: 'Return matching paths only'
|
||
},
|
||
output_mode: {
|
||
type: 'string',
|
||
description: 'content (default) | files_with_matches | count'
|
||
}
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'apply_patch',
|
||
description:
|
||
'Apply a Codex/Grok multi-file patch (*** Begin Patch … *** End Patch). Supports *** Add File, *** Delete File, *** Update File, optional *** Move to, and @@ hunks with + / - / context lines. Prefer this for multi-hunk or multi-file edits.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
patch: { type: 'string', description: 'Full apply_patch document' },
|
||
input: { type: 'string', description: 'Alias for patch' }
|
||
},
|
||
required: ['patch']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'update_goal',
|
||
description:
|
||
'Report progress on the current autonomous/session goal. completed=true ends the goal. blocked_reason is a failure signal after 3+ failed attempts — never use it for success.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
completed: { type: 'boolean' },
|
||
message: { type: 'string', description: 'Short progress or completion note' },
|
||
blocked_reason: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'web_search',
|
||
description:
|
||
'Search the public web (DuckDuckGo instant answers via the same HTTP policy as web_fetch / wget). Use for current facts, docs, and error lookup. Then web_fetch promising URLs.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
query: { type: 'string' },
|
||
max_results: { type: 'integer', description: 'Default 8, cap 16' }
|
||
},
|
||
required: ['query']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'git_status',
|
||
description:
|
||
'Guest git status --short plus diff --stat (isomorphic-git /bin/git). Prefer this over parsing git via run_command when you only need a snapshot.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
cwd: {
|
||
type: 'string',
|
||
description: 'Repo directory (default PWD or home)'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'memory_append',
|
||
description:
|
||
'Append a durable FACT / CHECK / RISK / HANDOFF line to MEMORY.md or today\'s daily log. Prefer this over write_file for memory.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
text: { type: 'string', description: 'What to remember' },
|
||
kind: {
|
||
type: 'string',
|
||
enum: ['FACT', 'CHECK', 'RISK', 'HANDOFF', 'NOTE'],
|
||
description: 'Ledger tag (default NOTE)'
|
||
},
|
||
daily: {
|
||
type: 'boolean',
|
||
description: 'Append to memory/YYYY-MM-DD.md instead of MEMORY.md'
|
||
}
|
||
},
|
||
required: ['text']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_skills',
|
||
description:
|
||
'List discovered SKILL.md ids from workspace/skills and ~/.agent/skills. Use read_skill to load one.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
max: { type: 'integer', description: 'Max entries (default 40)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'schedule_task',
|
||
description:
|
||
'Create or replace a guest timer that re-runs the agent (Grok scheduler, mapped to ~/.config/bare-os/timers). Interval like 5m, 2h, 1d, or five cron fields. Max 8 timers on the guest.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string', description: 'Timer id (stored as agent-<id>.timer)' },
|
||
interval: { type: 'string', description: '5m, 2h, 1d, or cron (min hour day month dow)' },
|
||
prompt: { type: 'string', description: 'Task the agent should run on each fire' },
|
||
auto: {
|
||
type: 'boolean',
|
||
description: 'Pass --auto (default true)'
|
||
}
|
||
},
|
||
required: ['interval', 'prompt']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'unschedule_task',
|
||
description: 'Delete a guest agent timer by id (agent-<id>.timer).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string' }
|
||
},
|
||
required: ['id']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'list_scheduled',
|
||
description:
|
||
'List guest agent timers (agent-*.timer) under ~/.config/bare-os/timers, with previews.',
|
||
parameters: { type: 'object', properties: {} }
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'fuzzy_find',
|
||
description:
|
||
'Fuzzy-search file names under a root (Grok-style). Prefer this when you remember part of a filename.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
query: { type: 'string' },
|
||
root: { type: 'string', description: 'Directory to walk (default home)' },
|
||
max: { type: 'integer', description: 'Default 20, cap 80' }
|
||
},
|
||
required: ['query']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'read_many',
|
||
description:
|
||
'Read several UTF-8 files in one call (bounded). Prefer over many read_file calls for a small set.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
paths: { type: 'array', items: { type: 'string' } },
|
||
max_bytes_each: { type: 'integer', description: 'Default 32000' }
|
||
},
|
||
required: ['paths']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'wait_for',
|
||
description:
|
||
'Poll a file or guest command until a regex matches, or timeout (Grok monitor lite; sync).',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
pattern: { type: 'string', description: 'JS regex' },
|
||
path: { type: 'string', description: 'File to poll' },
|
||
command: { type: 'string', description: 'Guest command whose output is polled' },
|
||
timeout_ms: { type: 'integer', description: 'Default 15000, cap 120000' },
|
||
interval_ms: { type: 'integer', description: 'Default 400, min 100' },
|
||
ignore_case: { type: 'boolean' }
|
||
},
|
||
required: ['pattern']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'undo_last_edit',
|
||
description:
|
||
'Restore the last file snapshot from ~/.agent/edits.json (write_file / edit / apply_patch / delete).',
|
||
parameters: { type: 'object', properties: {} }
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'git_diff',
|
||
description: 'git diff (optional path / --stat) via /bin/git. Prefer over raw run_command.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
cwd: { type: 'string' },
|
||
path: { type: 'string' },
|
||
stat: { type: 'boolean', description: 'Only --stat (default false)' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'history_search',
|
||
description: 'Search this session\'s ~/.agent/history.json by keyword.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
query: { type: 'string' },
|
||
max: { type: 'integer' }
|
||
},
|
||
required: ['query']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'git_log',
|
||
description:
|
||
'git log --oneline (optional -N / path) via /bin/git. Prefer over raw run_command.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
cwd: { type: 'string' },
|
||
max: { type: 'integer', description: 'Default 20, cap 80' },
|
||
path: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'git_show',
|
||
description: 'git show <rev> (optional path / --stat) via /bin/git.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
cwd: { type: 'string' },
|
||
rev: { type: 'string', description: 'Commit-ish (default HEAD)' },
|
||
path: { type: 'string' },
|
||
stat: { type: 'boolean' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'git_blame',
|
||
description: 'git blame a file via /bin/git.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
cwd: { type: 'string' },
|
||
path: { type: 'string' }
|
||
},
|
||
required: ['path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'copy_path',
|
||
description:
|
||
'Copy a file or directory on the VFS (parents created). Prefer this over run_command cp.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'diff_files',
|
||
description:
|
||
'Unified diff of two UTF-8 files (no git). Prefer this when comparing two paths that are not a git hunk.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
from_path: { type: 'string' },
|
||
to_path: { type: 'string' },
|
||
context: { type: 'integer', description: 'Context lines (default 3)' }
|
||
},
|
||
required: ['from_path', 'to_path']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'find_symbol',
|
||
description:
|
||
'Find likely definitions of a symbol (function/class/const/def/fn) under a root. Prefer over ad-hoc grep for go-to-definition.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string' },
|
||
root: { type: 'string' },
|
||
glob: { type: 'string' },
|
||
max: { type: 'integer' }
|
||
},
|
||
required: ['name']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'create_skill',
|
||
description:
|
||
'Write a Grok-style SKILL.md (YAML frontmatter + body) under ~/.agent/workspace/skills/<id>/.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string', description: 'Folder name (kebab-case)' },
|
||
name: { type: 'string' },
|
||
description: { type: 'string' },
|
||
body: { type: 'string', description: 'Markdown instructions after frontmatter' }
|
||
},
|
||
required: ['id', 'description']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'remember',
|
||
description:
|
||
'Save a durable FACT to MEMORY.md now (Grok /remember). Same store as memory_append.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
text: { type: 'string' },
|
||
daily: { type: 'boolean' }
|
||
},
|
||
required: ['text']
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'rewind_session',
|
||
description:
|
||
'Grok /rewind: drop the last N user turns from ~/.agent/history.json (and everything after). Default 1 turn.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
steps: { type: 'integer', description: 'How many user turns to drop (default 1)' },
|
||
user_index: { type: 'integer', description: 'Rewind to this 0-based user turn instead' },
|
||
keep_user: { type: 'boolean', description: 'Keep the target user prompt' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'export_session',
|
||
description:
|
||
'Grok /export: write the current chat history as Markdown. Default ~/.agent/export.md.',
|
||
parameters: {
|
||
type: 'object',
|
||
properties: {
|
||
path: { type: 'string' }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
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) {
|
||
if (typeof bareAgentPathAllowedRead === 'function') {
|
||
return bareAgentPathAllowedRead(absPath)
|
||
}
|
||
const p = String(absPath || '').replace(/\\/g, '/')
|
||
return Boolean(p.startsWith('/') && !p.includes('..'))
|
||
}
|
||
|
||
/**
|
||
* @param {string} command
|
||
* @param {unknown} denyList
|
||
*/
|
||
function bareAgentCommandDeniedByList(command, denyList) {
|
||
const cmd = String(command || '').toLowerCase()
|
||
const list = Array.isArray(denyList) ? denyList : []
|
||
for (let i = 0; i < list.length; i++) {
|
||
const needle = String(list[i] || '').trim().toLowerCase()
|
||
if (needle && cmd.includes(needle)) return needle
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* ctx: Record<string, unknown>,
|
||
* toolName: string,
|
||
* argsJson: string,
|
||
* paths: { dir: string, config: string, cmdOut: string, workspace?: string, workspaceSkills?: string, skillsGlobal?: string },
|
||
* signal?: AbortSignal,
|
||
* appendProgress: (line: string) => void,
|
||
* home: string,
|
||
* configRef: { current: Record<string, unknown> },
|
||
* manCacheRef?: { db: unknown | null },
|
||
* onTaskComplete: (summary: string) => void
|
||
* }} o
|
||
*/
|
||
async function bareAgentDispatchTool(o) {
|
||
const {
|
||
ctx,
|
||
toolName,
|
||
argsJson,
|
||
paths,
|
||
signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
} = o
|
||
const internalGate = Boolean(o.internalGate)
|
||
const manDbCache = manCacheRef || { db: null }
|
||
/** @type {Record<string, unknown>} */
|
||
let args = {}
|
||
try {
|
||
args = /** @type {Record<string, unknown>} */ (JSON.parse(argsJson || '{}'))
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
|
||
}
|
||
const cfgNow = configRef.current || {}
|
||
if (
|
||
!internalGate &&
|
||
cfgNow.autonomous_active &&
|
||
Array.isArray(cfgNow.autonomous_deny_ops) &&
|
||
cfgNow.autonomous_deny_ops.map((x) => String(x)).includes(toolName)
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'autonomous_op_denied', tool: toolName })
|
||
}
|
||
if (
|
||
!internalGate &&
|
||
cfgNow.plan_mode_active &&
|
||
typeof bareAgentPlanModeToolAllowed === 'function' &&
|
||
!bareAgentPlanModeToolAllowed(toolName, args, paths)
|
||
) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'plan_mode_readonly',
|
||
tool: toolName,
|
||
hint: 'exit_plan_mode before mutating, or write only ' + String(paths.plan || '~/.agent/plan.md')
|
||
})
|
||
}
|
||
if (!internalGate && typeof bareAgentRunPreToolHooks === 'function' && paths.hooks) {
|
||
const hookReason = await bareAgentRunPreToolHooks(ctx, paths.hooks, toolName, args)
|
||
if (hookReason) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'hook_denied',
|
||
tool: toolName,
|
||
reason: hookReason
|
||
})
|
||
}
|
||
}
|
||
if (toolName === 'list_dir') {
|
||
return bareAgentDispatchTool({ ...o, toolName: 'list_directory' })
|
||
}
|
||
if (toolName === 'glob') {
|
||
return bareAgentDispatchTool({ ...o, toolName: 'glob_files' })
|
||
}
|
||
if (toolName === 'ripgrep') {
|
||
return bareAgentDispatchTool({ ...o, toolName: 'grep' })
|
||
}
|
||
if (toolName === 'remember') {
|
||
return bareAgentDispatchTool({
|
||
...o,
|
||
toolName: 'memory_append',
|
||
argsJson: JSON.stringify({ ...args, kind: 'FACT' })
|
||
})
|
||
}
|
||
|
||
const vfs = ctx.vfs
|
||
const mutateDenyPrefixes = (function () {
|
||
const cfg = configRef.current || {}
|
||
if (Array.isArray(cfg.mutate_deny_prefixes) && cfg.mutate_deny_prefixes.length) {
|
||
return cfg.mutate_deny_prefixes.map((x) => String(x || '')).filter(Boolean)
|
||
}
|
||
return typeof BARE_AGENT_MUTATE_DENY_PREFIXES !== 'undefined'
|
||
? BARE_AGENT_MUTATE_DENY_PREFIXES
|
||
: ['/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run']
|
||
})()
|
||
const AUTONOMOUS_CHECK_ALLOW = {
|
||
'coreutils-test': 'npm test -w bare-os-coreutils',
|
||
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
||
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
||
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs'
|
||
}
|
||
const execLine =
|
||
typeof ctx.execLine === 'function'
|
||
? /** @type {(s: string, opts?: unknown) => Promise<unknown>} */ (
|
||
ctx.execLine.bind(ctx)
|
||
)
|
||
: null
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @param {number | undefined} timeoutMs
|
||
* @param {{ captureExit?: boolean }} [captureOpts]
|
||
*/
|
||
async function captureExec(line, timeoutMs, captureOpts) {
|
||
const outPath = paths.cmdOut
|
||
const captureExit = Boolean(captureOpts && captureOpts.captureExit)
|
||
const trimmed = String(line || '').trim()
|
||
const compoundShell =
|
||
/\n/.test(trimmed) ||
|
||
/(^|[;\s])(for|if|while|until|case|function)\b/.test(trimmed) ||
|
||
/\b(do|done|then|else|fi|esac)\b/.test(trimmed) ||
|
||
/&&|\|\||\(\(|\{|\}/.test(trimmed)
|
||
/**
|
||
* Do not wrap with `{ cmd ; }` — Bare OS `splitTokensBySemicolon` splits on every `;`
|
||
* at depth 0 and does not treat `{ … }` as a compound, so `{` became argv[0]
|
||
* (`unknown command: {`). Redirect only; read exit from env after `execLine`.
|
||
*/
|
||
const wrapped =
|
||
(compoundShell
|
||
? 'sh -c ' + bareAgentShellQuote(trimmed)
|
||
: line) +
|
||
' > ' +
|
||
bareAgentShellQuote(outPath) +
|
||
' 2>&1'
|
||
const opts =
|
||
signal || timeoutMs
|
||
? {
|
||
signal,
|
||
timeoutMs: timeoutMs || undefined
|
||
}
|
||
: undefined
|
||
try {
|
||
if (opts) await execLine(wrapped, opts)
|
||
else await execLine(wrapped)
|
||
} catch (e) {
|
||
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, exitNote: msg }
|
||
}
|
||
let captured = ''
|
||
try {
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
const buf = await vfs.readFile(outPath)
|
||
if (buf && buf.length) {
|
||
captured =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (captureExit) {
|
||
const env = ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
||
const rawEc =
|
||
env && env.BARE_OS_EXIT_STATUS != null && env.BARE_OS_EXIT_STATUS !== ''
|
||
? env.BARE_OS_EXIT_STATUS
|
||
: ctx.exitCode
|
||
const n = Number(rawEc)
|
||
const codeStr = String(Number.isFinite(n) ? n : 0)
|
||
const exitLine = '\nEXIT:' + codeStr + '\n'
|
||
captured += exitLine
|
||
try {
|
||
if (vfs?.readFile && vfs?.writeFile && ctx.b4a && typeof ctx.b4a.concat === 'function') {
|
||
let prev = await vfs.readFile(outPath)
|
||
const prevBytes =
|
||
prev && prev.length
|
||
? prev instanceof Uint8Array
|
||
? prev
|
||
: ctx.b4a.from(prev)
|
||
: ctx.b4a.from('')
|
||
await vfs.writeFile(
|
||
outPath,
|
||
ctx.b4a.concat([prevBytes, ctx.b4a.from(exitLine)])
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
const max = 120_000
|
||
if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated'
|
||
return { ok: true, stdout_stderr: captured }
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function redactSensitiveText(text) {
|
||
return String(text || '')
|
||
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(
|
||
/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi,
|
||
'$1=<redacted>'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {string} path
|
||
* @param {number} maxChars
|
||
* @param {boolean} tailOnly
|
||
* @param {boolean} redact
|
||
*/
|
||
async function readBoundedText(path, maxChars, tailOnly, redact) {
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return { ok: false, error: 'vfs unavailable' }
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return { ok: false, error: 'empty_or_missing' }
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
if (redact) t = redactSensitiveText(t)
|
||
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
||
return { ok: true, path, text: out, truncated: t.length > out.length }
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return { ok: false, error: msg }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} p
|
||
*/
|
||
function autonomousPathAllowed(p) {
|
||
const s = String(p || '').trim()
|
||
if (!s) return true
|
||
return bareAgentPathAllowed(s)
|
||
}
|
||
|
||
/**
|
||
* @param {string} p
|
||
* @param {'read' | 'mutate'} [kind]
|
||
*/
|
||
function enforceAutonomousPath(p, kind) {
|
||
const cfg = configRef.current || {}
|
||
const path = String(p || '').trim()
|
||
if (!path) return true
|
||
if (kind === 'mutate') {
|
||
if (
|
||
typeof bareAgentPathAllowedMutate === 'function' &&
|
||
!bareAgentPathAllowedMutate(path, mutateDenyPrefixes)
|
||
) {
|
||
return false
|
||
}
|
||
} else if (!bareAgentPathAllowed(path)) {
|
||
return false
|
||
}
|
||
if (!cfg.autonomous_active) return true
|
||
if (kind !== 'mutate') return true
|
||
const allowList = Array.isArray(cfg.autonomous_allow_paths)
|
||
? cfg.autonomous_allow_paths.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: []
|
||
if (!allowList.length || allowList.includes('*')) return true
|
||
for (const pref of allowList) {
|
||
if (path === pref || path.startsWith(pref.endsWith('/') ? pref : pref + '/')) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
try {
|
||
if (toolName === 'read_skill') {
|
||
const skill = typeof args.skill === 'string' ? args.skill.trim() : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 512_000)
|
||
: 256_000
|
||
if (!skill) {
|
||
return bareAgentJsonResult({ ok: false, error: 'skill_required' })
|
||
}
|
||
const skillPaths = {
|
||
workspaceSkills:
|
||
typeof paths.workspaceSkills === 'string'
|
||
? paths.workspaceSkills
|
||
: paths.dir + '/workspace/skills',
|
||
skillsGlobal:
|
||
typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills'
|
||
}
|
||
appendProgress('read_skill ' + skill)
|
||
const loaded = await bareAgentLoadSkillMarkdown(ctx, skillPaths, skill)
|
||
if (!loaded.ok) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: loaded.error || 'load_failed',
|
||
skill
|
||
})
|
||
}
|
||
let content = loaded.content
|
||
if (content.length > maxB) content = content.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
id: loaded.id,
|
||
name: loaded.skill,
|
||
path: loaded.path,
|
||
source: loaded.source,
|
||
content
|
||
})
|
||
}
|
||
|
||
if (toolName === 'glob_files') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern.trim() : ''
|
||
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
||
const root =
|
||
typeof args.root === 'string' && args.root.trim()
|
||
? args.root.trim()
|
||
: home || '/home'
|
||
if (!bareAgentPathAllowed(root)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
const maxResults =
|
||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||
? Math.min(Math.max(Math.floor(args.max_results), 1), 400)
|
||
: 100
|
||
if (typeof bareAgentGlobFiles !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'glob_unavailable' })
|
||
}
|
||
appendProgress('glob_files ' + pattern + ' @ ' + root)
|
||
const files = await bareAgentGlobFiles(ctx, root, pattern, {
|
||
maxFiles: Math.max(maxResults * 4, 200),
|
||
maxDepth: 10
|
||
})
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
root,
|
||
pattern,
|
||
count: Math.min(files.length, maxResults),
|
||
truncated: files.length > maxResults,
|
||
files: files.slice(0, maxResults)
|
||
})
|
||
}
|
||
|
||
if (toolName === 'todo_write') {
|
||
const todosPath = paths.todos || paths.dir + '/todos.json'
|
||
const merge = args.merge !== false
|
||
const prev =
|
||
typeof bareAgentLoadTodos === 'function'
|
||
? await bareAgentLoadTodos(ctx, todosPath)
|
||
: []
|
||
let next
|
||
try {
|
||
next = bareAgentTodoApply(args.todos, { merge }, prev)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
if (typeof bareAgentSaveTodos === 'function') {
|
||
await bareAgentSaveTodos(ctx, todosPath, next)
|
||
}
|
||
const summary = bareAgentTodoSummarize(next)
|
||
appendProgress('todo_write open=' + String(summary.open) + '/' + String(summary.total))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
merge,
|
||
todos: next,
|
||
summary
|
||
})
|
||
}
|
||
|
||
if (toolName === 'memory_search') {
|
||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||
const maxHits =
|
||
typeof args.max_hits === 'number' && Number.isFinite(args.max_hits)
|
||
? Math.min(Math.max(Math.floor(args.max_hits), 1), 24)
|
||
: 8
|
||
const memDir =
|
||
typeof paths.workspaceMemory === 'string'
|
||
? paths.workspaceMemory
|
||
: paths.dir + '/workspace/memory'
|
||
const workspace =
|
||
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
||
const seeds = []
|
||
if (typeof bareAgentVfsWalkFiles === 'function') {
|
||
const walked = await bareAgentVfsWalkFiles(ctx, memDir, {
|
||
maxFiles: 80,
|
||
maxDepth: 4
|
||
})
|
||
for (let i = 0; i < walked.length; i++) seeds.push(walked[i])
|
||
}
|
||
seeds.push(workspace + '/MEMORY.md')
|
||
if (paths.compact) seeds.push(paths.compact)
|
||
else seeds.push(paths.dir + '/compact.md')
|
||
const uniq = []
|
||
const seenMem = Object.create(null)
|
||
for (let i = 0; i < seeds.length; i++) {
|
||
if (seenMem[seeds[i]]) continue
|
||
seenMem[seeds[i]] = 1
|
||
uniq.push(seeds[i])
|
||
}
|
||
appendProgress('memory_search ' + query.slice(0, 80))
|
||
const hits =
|
||
typeof bareAgentMemorySearchFiles === 'function'
|
||
? await bareAgentMemorySearchFiles(ctx, uniq, query, { maxHits })
|
||
: []
|
||
return bareAgentJsonResult({ ok: true, query, hits })
|
||
}
|
||
|
||
if (toolName === 'memory_get') {
|
||
const rawPath = typeof args.path === 'string' ? args.path.trim() : ''
|
||
if (!rawPath) return bareAgentJsonResult({ ok: false, error: 'path_required' })
|
||
const memDir =
|
||
typeof paths.workspaceMemory === 'string'
|
||
? paths.workspaceMemory
|
||
: paths.dir + '/workspace/memory'
|
||
const workspace =
|
||
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
||
let path = rawPath
|
||
if (!path.startsWith('/')) path = memDir + '/' + path.replace(/^\/+/, '')
|
||
const allowed =
|
||
path === workspace + '/MEMORY.md' ||
|
||
path === (paths.compact || paths.dir + '/compact.md') ||
|
||
path === memDir ||
|
||
path.indexOf(memDir + '/') === 0
|
||
if (!allowed) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'memory_path_denied',
|
||
hint: 'path must be under ' + memDir
|
||
})
|
||
}
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 256_000)
|
||
: 64_000
|
||
appendProgress('memory_get ' + path)
|
||
let text = await bareAgentReadTextFile(ctx, path)
|
||
if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing', path })
|
||
const truncated = text.length > maxB
|
||
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({ ok: true, path, content: text, truncated })
|
||
}
|
||
|
||
if (toolName === 'enter_plan_mode') {
|
||
const planPath = paths.plan || paths.dir + '/plan.md'
|
||
configRef.current = { ...(configRef.current || {}), plan_mode_active: true }
|
||
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
||
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
|
||
}
|
||
const note = typeof args.note === 'string' ? args.note.trim() : ''
|
||
let prev = ''
|
||
try {
|
||
prev = await bareAgentReadTextFile(ctx, planPath)
|
||
} catch {
|
||
prev = ''
|
||
}
|
||
if (!prev.trim()) {
|
||
const starter =
|
||
'# Plan\n\n' +
|
||
(note ? note + '\n' : '- Investigate with read-only tools.\n- Write the plan here.\n- Call exit_plan_mode when ready to implement.\n')
|
||
await bareAgentWriteTextFile(ctx, planPath, starter)
|
||
} else if (note) {
|
||
await bareAgentWriteTextFile(ctx, planPath, prev.replace(/\s*$/, '') + '\n\n' + note + '\n')
|
||
}
|
||
appendProgress('enter_plan_mode ' + planPath)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
plan_mode_active: true,
|
||
plan: planPath
|
||
})
|
||
}
|
||
|
||
if (toolName === 'exit_plan_mode') {
|
||
const summary = typeof args.summary === 'string' ? args.summary.trim() : ''
|
||
configRef.current = { ...(configRef.current || {}), plan_mode_active: false }
|
||
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
||
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
|
||
}
|
||
if (summary && paths.plan) {
|
||
const prev = await bareAgentReadTextFile(ctx, paths.plan)
|
||
await bareAgentWriteTextFile(
|
||
ctx,
|
||
paths.plan,
|
||
(prev ? prev.replace(/\s*$/, '') + '\n\n' : '') + '## Exit\n' + summary + '\n'
|
||
)
|
||
}
|
||
appendProgress('exit_plan_mode')
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
plan_mode_active: false,
|
||
summary: summary || null
|
||
})
|
||
}
|
||
|
||
if (toolName === 'ask_user_question') {
|
||
const rows = Array.isArray(args.questions) ? args.questions : []
|
||
if (!rows.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
|
||
}
|
||
/** @type {Record<string, unknown>[]} */
|
||
const questions = []
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
|
||
const question = String(row.question || '').trim()
|
||
if (!question) continue
|
||
const opts = Array.isArray(row.options) ? row.options : []
|
||
questions.push({
|
||
question,
|
||
multi_select: Boolean(row.multi_select),
|
||
options: opts.map(function (opt) {
|
||
if (opt && typeof opt === 'object') {
|
||
return {
|
||
label: String(opt.label || ''),
|
||
description: String(opt.description || '')
|
||
}
|
||
}
|
||
return { label: String(opt || ''), description: '' }
|
||
})
|
||
})
|
||
}
|
||
if (!questions.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
|
||
}
|
||
const askPath = paths.ask || paths.dir + '/ask.json'
|
||
const payload = {
|
||
asked_at: new Date().toISOString(),
|
||
questions
|
||
}
|
||
if (typeof bareAgentWriteJsonFile === 'function') {
|
||
await bareAgentWriteJsonFile(ctx, askPath, payload)
|
||
}
|
||
const text = questions
|
||
.map(function (q, i) {
|
||
const opts = Array.isArray(q.options)
|
||
? q.options
|
||
.map(function (o, j) {
|
||
return (
|
||
' ' +
|
||
String(j + 1) +
|
||
') ' +
|
||
String(o.label || '') +
|
||
(o.description ? ' — ' + String(o.description) : '')
|
||
)
|
||
})
|
||
.join('\n')
|
||
: ''
|
||
return (
|
||
String(i + 1) +
|
||
'. ' +
|
||
q.question +
|
||
(q.multi_select ? ' (multi-select)' : '') +
|
||
(opts ? '\n' + opts : '')
|
||
)
|
||
})
|
||
.join('\n')
|
||
appendProgress('ask_user_question n=' + String(questions.length))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
ask: askPath,
|
||
questions,
|
||
text,
|
||
hint: 'Wait for the user to answer these questions on the next turn.'
|
||
})
|
||
}
|
||
|
||
if (toolName === 'task_complete') {
|
||
const summary = typeof args.summary === 'string' ? args.summary : ''
|
||
appendProgress('task_complete: ' + summary.slice(0, 200))
|
||
onTaskComplete(summary || '(done)')
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
completed: true,
|
||
summary
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run') {
|
||
const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
|
||
const scopePath = typeof args.scope_path === 'string' ? args.scope_path.trim() : ''
|
||
const cfg = configRef.current || {}
|
||
if (!goal) return bareAgentJsonResult({ ok: false, error: 'goal_required' })
|
||
if (scopePath && !autonomousPathAllowed(scopePath)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'scope_path_denied' })
|
||
}
|
||
const maxRuntimeMsRaw =
|
||
typeof args.max_runtime_ms === 'number' && Number.isFinite(args.max_runtime_ms)
|
||
? args.max_runtime_ms
|
||
: cfg.autonomous_max_runtime_ms
|
||
const requiredChecks = Array.isArray(args.required_checks)
|
||
? args.required_checks.map((x) => String(x || '').trim()).filter(Boolean)
|
||
: []
|
||
const unknown = requiredChecks.filter((x) => !Object.prototype.hasOwnProperty.call(AUTONOMOUS_CHECK_ALLOW, x))
|
||
if (unknown.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_required_checks', unknown, allowlist: Object.keys(AUTONOMOUS_CHECK_ALLOW) })
|
||
}
|
||
const started =
|
||
typeof bareAgentBeginAutonomousRun === 'function'
|
||
? bareAgentBeginAutonomousRun(cfg, {
|
||
goal,
|
||
maxRuntimeMs: maxRuntimeMsRaw,
|
||
requiredChecks
|
||
})
|
||
: {
|
||
...cfg,
|
||
autonomous_mode_enabled: true,
|
||
autonomous_active: true,
|
||
autonomous_stop_requested: false,
|
||
autonomous_started_at_ms: Date.now(),
|
||
autonomous_goal: goal,
|
||
autonomous_status: 'running',
|
||
autonomous_last_error: '',
|
||
autonomous_max_runtime_ms: Math.min(
|
||
Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000),
|
||
7_200_000
|
||
),
|
||
autonomous_completion_required_checks: requiredChecks
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(cfg, started)
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
configRef.current = merged
|
||
appendProgress('autonomous_run start goal=' + goal.slice(0, 160))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
active: true,
|
||
enabled: true,
|
||
goal,
|
||
scope_path: scopePath || null,
|
||
max_runtime_ms: merged.autonomous_max_runtime_ms,
|
||
required_checks: requiredChecks,
|
||
status: 'running'
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run_status') {
|
||
const cfg = configRef.current || {}
|
||
const started = Number(cfg.autonomous_started_at_ms) || 0
|
||
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
||
const maxRuntime = Number(cfg.autonomous_max_runtime_ms) || 0
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
autonomous_mode_enabled: Boolean(cfg.autonomous_mode_enabled),
|
||
active: Boolean(cfg.autonomous_active),
|
||
stop_requested: Boolean(cfg.autonomous_stop_requested),
|
||
goal: String(cfg.autonomous_goal || ''),
|
||
status: String(cfg.autonomous_status || 'idle'),
|
||
last_error: String(cfg.autonomous_last_error || ''),
|
||
started_at_ms: started,
|
||
elapsed_ms: elapsed,
|
||
max_runtime_ms: maxRuntime,
|
||
remaining_ms: maxRuntime > 0 ? Math.max(0, maxRuntime - elapsed) : 0,
|
||
required_checks: Array.isArray(cfg.autonomous_completion_required_checks)
|
||
? cfg.autonomous_completion_required_checks
|
||
: []
|
||
})
|
||
}
|
||
|
||
if (toolName === 'autonomous_run_stop') {
|
||
const cfg = configRef.current || {}
|
||
const reason = typeof args.reason === 'string' ? args.reason.trim() : ''
|
||
const stopped =
|
||
typeof bareAgentStopAutonomousRun === 'function'
|
||
? bareAgentStopAutonomousRun(cfg, reason)
|
||
: {
|
||
...cfg,
|
||
autonomous_stop_requested: true,
|
||
autonomous_status: 'stopping',
|
||
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(cfg, stopped)
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
configRef.current = merged
|
||
appendProgress('autonomous_run_stop ' + (reason || 'requested'))
|
||
return bareAgentJsonResult({ ok: true, stop_requested: true, reason: reason || null })
|
||
}
|
||
|
||
if (toolName === 'read_file') {
|
||
const path =
|
||
typeof bareAgentToolPathArg === 'function'
|
||
? bareAgentToolPathArg(args)
|
||
: typeof args.path === 'string'
|
||
? args.path
|
||
: ''
|
||
if (!enforceAutonomousPath(path, 'read')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
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'
|
||
const hasSlice =
|
||
(typeof args.offset === 'number' && Number.isFinite(args.offset)) ||
|
||
(typeof args.limit === 'number' && Number.isFinite(args.limit))
|
||
if (hasSlice && typeof bareAgentSliceFileLines === 'function') {
|
||
const sliced = bareAgentSliceFileLines(t, {
|
||
offset:
|
||
typeof args.offset === 'number' && Number.isFinite(args.offset)
|
||
? args.offset
|
||
: 1,
|
||
limit:
|
||
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
||
? args.limit
|
||
: undefined,
|
||
numbered: args.numbered !== false
|
||
})
|
||
return bareAgentJsonResult({ ok: true, path, ...sliced })
|
||
}
|
||
return bareAgentJsonResult({ ok: true, path, content: t })
|
||
}
|
||
|
||
if (toolName === 'write_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
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)
|
||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||
try {
|
||
const prev = await bareAgentReadTextFile(ctx, path)
|
||
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'write_file' })
|
||
} catch {
|
||
/* new file */
|
||
}
|
||
}
|
||
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' || toolName === 'search_replace') {
|
||
const path =
|
||
typeof bareAgentToolPathArg === 'function'
|
||
? bareAgentToolPathArg(args)
|
||
: typeof args.path === 'string'
|
||
? args.path
|
||
: ''
|
||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs?.readFile || !vfs?.writeFile) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
||
}
|
||
appendProgress(toolName + ' ' + path)
|
||
const buf = await vfs.readFile(path)
|
||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||
try {
|
||
const snap =
|
||
buf && buf.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
: ''
|
||
await bareAgentPushEdit(ctx, paths.edits, { path, prev: snap, tool: toolName })
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
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 =
|
||
toolName === 'search_replace'
|
||
? ''
|
||
: 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 : ''
|
||
const replaceAll = Boolean(args.replace_all)
|
||
let next = prev
|
||
let replacements = 0
|
||
if (full.length > 0) {
|
||
next = full
|
||
replacements = 1
|
||
} else if (typeof bareAgentSearchReplaceApply === 'function') {
|
||
const applied = bareAgentSearchReplaceApply(prev, oldStr, newStr, replaceAll)
|
||
if (!applied.ok) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: applied.error,
|
||
count: applied.count,
|
||
hint: applied.hint
|
||
})
|
||
}
|
||
next = applied.next
|
||
replacements = applied.replacements || 0
|
||
} else if (oldStr) {
|
||
if (!prev.includes(oldStr)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
|
||
}
|
||
next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr)
|
||
replacements = 1
|
||
} 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 && typeof vfs.mkdir === 'function') {
|
||
await vfs.mkdir(dir, { recursive: true })
|
||
}
|
||
await vfs.writeFile(path, body)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
bytes: body.length,
|
||
replacements,
|
||
replace_all: replaceAll
|
||
})
|
||
}
|
||
|
||
if (toolName === 'create_directory') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
if (!enforceAutonomousPath(path, 'mutate')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
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 deniedBy = bareAgentCommandDeniedByList(
|
||
command,
|
||
(configRef.current || {}).command_deny
|
||
)
|
||
if (deniedBy) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'command_denied',
|
||
matched: deniedBy
|
||
})
|
||
}
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.floor(args.timeout_ms), 600000)
|
||
: 120000
|
||
const captureExit = Boolean(args.capture_exit)
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
let cwd = typeof args.cwd === 'string' ? args.cwd.trim() : ''
|
||
if (!cwd && paths.lastCwd) {
|
||
try {
|
||
cwd = (await bareAgentReadTextFile(ctx, paths.lastCwd)).trim()
|
||
} catch {
|
||
cwd = ''
|
||
}
|
||
}
|
||
let line = command
|
||
if (cwd) {
|
||
if (!bareAgentPathAllowed(cwd)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'cwd_not_allowed' })
|
||
}
|
||
line = 'cd ' + bareAgentShellQuote(cwd) + ' && ' + command
|
||
try {
|
||
if (paths.lastCwd) await bareAgentWriteTextFile(ctx, paths.lastCwd, cwd + '\n')
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
appendProgress('run_command ' + line.slice(0, 160))
|
||
const r = await captureExec(line, timeoutMs, { captureExit })
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, cwd: cwd || null, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'run_js_script') {
|
||
const code = typeof args.code === 'string' ? args.code : ''
|
||
const scriptPath = paths.dir + '/_tmp_agent_run.mjs'
|
||
if (!vfs?.writeFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script (' + code.length + ' chars)')
|
||
const body =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(code)
|
||
: new TextEncoder().encode(code)
|
||
await vfs.writeFile(scriptPath, body)
|
||
/** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'get_system_info') {
|
||
/** @type {Record<string, unknown>} */
|
||
const info = {}
|
||
try {
|
||
info.ctxApiVersion =
|
||
typeof ctx.ctxApiVersion === 'string'
|
||
? ctx.ctxApiVersion
|
||
: typeof ctx.ctxApiVersion === 'number'
|
||
? String(ctx.ctxApiVersion)
|
||
: undefined
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const want =
|
||
typeof args.want === 'string' ? args.want : 'summary'
|
||
if (want === 'summary') {
|
||
info.discovery_hint =
|
||
'For live kernel feature flags use read_proc_file on /proc/bare_os/features (or features.json); apropos_man only searches man-page text, not runtime state. Otherwise prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.'
|
||
}
|
||
if (want === 'capabilities' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/capabilities.json')
|
||
if (b && b.length) {
|
||
info.capabilities_json =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (want === 'swarm' && vfs?.readFile) {
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (b && b.length) {
|
||
info.swarm =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
ctx.b4a.toString
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1')
|
||
if (vfs?.readFile) {
|
||
const buf = await vfs.readFile(paths.cmdOut)
|
||
if (buf && buf.length) {
|
||
info.uname =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf).trim()
|
||
: String(new TextDecoder().decode(buf)).trim()
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
appendProgress('get_system_info ' + want)
|
||
return bareAgentJsonResult({ ok: true, want, info })
|
||
}
|
||
|
||
if (toolName === 'edit_agent_config') {
|
||
const patch = args.patch
|
||
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
|
||
}
|
||
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
||
configRef.current = merged
|
||
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
||
if (
|
||
Object.prototype.hasOwnProperty.call(patch, 'owner_name') ||
|
||
Object.prototype.hasOwnProperty.call(patch, 'agent_label')
|
||
) {
|
||
const workspace =
|
||
typeof paths.workspace === 'string'
|
||
? paths.workspace
|
||
: paths.dir + '/workspace'
|
||
await bareAgentSyncWorkspaceFromConfig(ctx, { workspace }, merged)
|
||
}
|
||
appendProgress('edit_agent_config')
|
||
return bareAgentJsonResult({ ok: true, saved: true })
|
||
}
|
||
|
||
if (toolName === 'list_bin') {
|
||
const limit =
|
||
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
||
? Math.min(Math.floor(args.limit), 800)
|
||
: 400
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_bin')
|
||
try {
|
||
const names = await vfs.readdir('/bin')
|
||
const arr = Array.isArray(names) ? [...names].slice(0, limit) : []
|
||
arr.sort()
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: arr.length,
|
||
names: arr
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_directory') {
|
||
const dir = typeof args.path === 'string' ? args.path : ''
|
||
const maxEnt =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.floor(args.max_entries), 2000)
|
||
: 500
|
||
const includeStat = Boolean(args.include_stat)
|
||
if (!bareAgentPathAllowed(dir)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (Boolean(args.tree) && typeof bareAgentRenderTree === 'function') {
|
||
appendProgress('list_directory tree ' + dir)
|
||
const tree = await bareAgentRenderTree(ctx, dir, {
|
||
max: maxEnt,
|
||
maxDepth: args.max_depth
|
||
})
|
||
return bareAgentJsonResult(tree)
|
||
}
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||
}
|
||
appendProgress('list_directory ' + dir)
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const arr = Array.isArray(names) ? [...names] : []
|
||
arr.sort()
|
||
const slice = arr.slice(0, maxEnt)
|
||
const base = dir.replace(/\/+$/, '') || '/'
|
||
/** @type {{ name: string, stat?: Record<string, unknown> }[]} */
|
||
const entries = []
|
||
for (const n of slice) {
|
||
const entry = { name: n }
|
||
if (includeStat && (vfs.lstat || vfs.stat)) {
|
||
try {
|
||
const full = base + '/' + n
|
||
const st =
|
||
typeof vfs.lstat === 'function'
|
||
? await vfs.lstat(full)
|
||
: await vfs.stat(full)
|
||
entry.stat = bareAgentSerializeStat(st, full)
|
||
} catch {
|
||
/* ignore per-entry stat errors */
|
||
}
|
||
}
|
||
entries.push(entry)
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dir,
|
||
count: entries.length,
|
||
truncated: arr.length > maxEnt,
|
||
entries
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'file_stat') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const follow = Boolean(args.follow_symlinks)
|
||
if (!bareAgentPathAllowed(path)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('file_stat ' + path)
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (follow && typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
else if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
if (!st) return bareAgentJsonResult({ ok: false, error: 'stat unavailable' })
|
||
const serialized = bareAgentSerializeStat(st, path)
|
||
if (
|
||
serialized.kind === 'symlink' &&
|
||
typeof vfs.readlink === 'function'
|
||
) {
|
||
try {
|
||
serialized.target = await vfs.readlink(path)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult({ ok: true, stat: serialized })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'move_path') {
|
||
const from = typeof args.from_path === 'string' ? args.from_path : ''
|
||
const to = typeof args.to_path === 'string' ? args.to_path : ''
|
||
if (
|
||
!bareAgentPathAllowedMutate(from, mutateDenyPrefixes) ||
|
||
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes) ||
|
||
from.includes('..') ||
|
||
to.includes('..')
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('move_path')
|
||
const cmd =
|
||
'mv -- ' + bareAgentShellQuote(from) + ' ' + bareAgentShellQuote(to)
|
||
const r = await captureExec(cmd, 120000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'delete_path') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const recursive = Boolean(args.recursive)
|
||
const token = typeof args.confirm_token === 'string' ? args.confirm_token : ''
|
||
const cfg = configRef.current
|
||
const allowDel = Boolean(cfg && cfg.allow_delete)
|
||
const reqTok =
|
||
cfg && typeof cfg.require_confirm_token === 'string'
|
||
? String(cfg.require_confirm_token)
|
||
: ''
|
||
if (!allowDel) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'delete_disabled',
|
||
hint: 'allow_delete is on by default; this home set it false'
|
||
})
|
||
}
|
||
if (reqTok && token !== reqTok) {
|
||
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
||
}
|
||
if (!bareAgentPathAllowedMutate(path, mutateDenyPrefixes) || path.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('delete_path ' + path)
|
||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||
try {
|
||
const prev = await bareAgentReadTextFile(ctx, path)
|
||
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'delete_path' })
|
||
} catch {
|
||
/* missing */
|
||
}
|
||
}
|
||
try {
|
||
/** @type {unknown} */
|
||
let st = null
|
||
if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||
const isDir =
|
||
st &&
|
||
typeof st === 'object' &&
|
||
typeof /** @type {{ isDirectory?: () => boolean }} */ (st).isDirectory ===
|
||
'function' &&
|
||
st.isDirectory()
|
||
if (isDir && recursive && typeof vfs.rm === 'function') {
|
||
await vfs.rm(path, { recursive: true })
|
||
return bareAgentJsonResult({ ok: true, removed: 'directory', recursive: true })
|
||
}
|
||
if (isDir && !recursive) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'is_directory',
|
||
hint: 'pass recursive true to remove a directory tree'
|
||
})
|
||
}
|
||
if (typeof vfs.unlink !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'unlink unavailable' })
|
||
}
|
||
await vfs.unlink(path)
|
||
return bareAgentJsonResult({ ok: true, removed: isDir ? 'directory' : 'file' })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'read_man_page') {
|
||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||
const maxC =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.floor(args.max_chars), 64_000)
|
||
: 12_000
|
||
let secExplicit = null
|
||
if (
|
||
typeof args.section === 'number' &&
|
||
Number.isFinite(args.section) &&
|
||
args.section >= 1 &&
|
||
args.section <= 8
|
||
) {
|
||
secExplicit = Math.floor(args.section)
|
||
}
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('read_man_page ' + topic)
|
||
const resolved = bareAgentManResolvePage(db, topic, secExplicit)
|
||
if ('error' in resolved && resolved.error === 'wrong_section') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'wrong_section',
|
||
foundSection: resolved.foundSection
|
||
})
|
||
}
|
||
if (!resolved.page) {
|
||
return bareAgentJsonResult({ ok: false, error: 'not_found' })
|
||
}
|
||
const slice = bareAgentManExtractPageSlice(resolved.page, maxC)
|
||
return bareAgentJsonResult({ ok: true, ...slice })
|
||
}
|
||
|
||
if (toolName === 'apropos_man') {
|
||
const kw = typeof args.keyword === 'string' ? args.keyword : ''
|
||
const maxRes =
|
||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||
? Math.floor(args.max_results)
|
||
: 40
|
||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||
if (!db) {
|
||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||
}
|
||
appendProgress('apropos_man ' + kw)
|
||
const { lines, truncated } = bareAgentManAproposHits(db, kw, maxRes)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: lines.length,
|
||
truncated,
|
||
lines
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_proc_file') {
|
||
const path = typeof args.path === 'string' ? args.path : ''
|
||
const maxB =
|
||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||
? Math.min(Math.floor(args.max_bytes), 500_000)
|
||
: 256_000
|
||
if (!bareAgentProcReadPathAllowed(path)) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'path_not_allowed',
|
||
hint: 'read_proc_file accepts any /proc path'
|
||
})
|
||
}
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
appendProgress('read_proc_file ' + path)
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
let parsed = null
|
||
try {
|
||
parsed = JSON.parse(t)
|
||
} catch {
|
||
parsed = null
|
||
}
|
||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: t,
|
||
json: parsed
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_services') {
|
||
appendProgress('list_services')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
let readinessText = ''
|
||
/** @type {Record<string, unknown> | null} */
|
||
let readinessJson = null
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
||
if (b && b.length) {
|
||
readinessText =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
const parsed = JSON.parse(readinessText)
|
||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
||
readinessJson = /** @type {Record<string, unknown>} */ (parsed)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const units = Array.isArray(readinessJson?.units)
|
||
? /** @type {unknown[]} */ (readinessJson.units)
|
||
: []
|
||
const rows = units
|
||
.filter((u) => u && typeof u === 'object')
|
||
.map((u) => {
|
||
const o = /** @type {Record<string, unknown>} */ (u)
|
||
return {
|
||
name: String(o.name || ''),
|
||
phase: String(o.phase || ''),
|
||
startedAtMs:
|
||
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
||
error: typeof o.error === 'string' ? o.error : undefined
|
||
}
|
||
})
|
||
.filter((r) => r.name)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
source: '/proc/bare_os/initd_readiness.json',
|
||
count: rows.length,
|
||
units: rows,
|
||
note:
|
||
rows.length > 0
|
||
? 'Runtime units from initd readiness snapshot.'
|
||
: 'No parsed readiness units available.'
|
||
})
|
||
}
|
||
|
||
if (toolName === 'service_status') {
|
||
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
||
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
||
appendProgress('service_status ' + name)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
||
const t =
|
||
b && b.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
: ''
|
||
const parsed = JSON.parse(t)
|
||
const units = Array.isArray(parsed?.units) ? parsed.units : []
|
||
const hit =
|
||
units.find((u) => u && typeof u === 'object' && String(u.name || '') === name) ||
|
||
null
|
||
if (!hit) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'not_found',
|
||
source: '/proc/bare_os/initd_readiness.json'
|
||
})
|
||
}
|
||
const o = /** @type {Record<string, unknown>} */ (hit)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
status: {
|
||
name: String(o.name || ''),
|
||
phase: String(o.phase || ''),
|
||
startedAtMs:
|
||
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
||
error: typeof o.error === 'string' ? o.error : undefined
|
||
},
|
||
journal_hint: '/run/bare-os/unit-journal/' + name + '.ndjson'
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'list_timers') {
|
||
const includePreview = Boolean(args.include_preview)
|
||
const maxEntries =
|
||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||
? Math.min(Math.max(Math.floor(args.max_entries), 1), 512)
|
||
: 128
|
||
appendProgress('list_timers')
|
||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const dir = home + '/.config/bare-os/timers'
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(dir)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg, path: dir })
|
||
}
|
||
const timerNames = names
|
||
.filter((n) => typeof n === 'string' && n.endsWith('.timer'))
|
||
.sort()
|
||
.slice(0, maxEntries)
|
||
/** @type {unknown[]} */
|
||
const timers = []
|
||
for (const name of timerNames) {
|
||
const path = dir + '/' + name
|
||
/** @type {Record<string, unknown>} */
|
||
const row = { name, path }
|
||
if (includePreview) {
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
const txt =
|
||
b && b.length
|
||
? typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
: ''
|
||
row.preview = bareAgentTruncateChars(txt, 1200)
|
||
} catch (e) {
|
||
row.preview_error =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
}
|
||
}
|
||
timers.push(row)
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dir,
|
||
count: timers.length,
|
||
timers
|
||
})
|
||
}
|
||
|
||
if (toolName === 'read_cron_log' || toolName === 'read_audit_log') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 80_000)
|
||
: 12_000
|
||
const tailOnly = Boolean(args.tail_only)
|
||
const redact = toolName === 'read_audit_log' ? args.redact !== false : false
|
||
const path =
|
||
toolName === 'read_audit_log'
|
||
? '/var/log/bare-os/audit.log'
|
||
: '/var/log/bare-os/cron.log'
|
||
appendProgress(toolName + ' ' + path)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
if (redact) {
|
||
t = t
|
||
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
||
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
||
.replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=<redacted>')
|
||
}
|
||
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: out,
|
||
truncated: t.length > out.length
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'read_boot_policy' || toolName === 'read_kernel_extension_resolution') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
||
: 20_000
|
||
const path =
|
||
toolName === 'read_boot_policy'
|
||
? '/etc/bare-os/boot.policy.json'
|
||
: '/run/bare-os/kernel-ext-resolution.json'
|
||
appendProgress(toolName + ' ' + path)
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const b = await vfs.readFile(path)
|
||
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
/** @type {unknown} */
|
||
let json = null
|
||
try {
|
||
json = JSON.parse(t)
|
||
} catch {
|
||
json = null
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path,
|
||
text: bareAgentTruncateChars(t, maxChars),
|
||
json
|
||
})
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_initd_graph') {
|
||
appendProgress('get_initd_graph')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true }
|
||
for (const p of ['/proc/bare_os/initd_dag.json', '/proc/bare_os/initd_readiness.json']) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 8000)
|
||
}
|
||
} catch {
|
||
/* ignore missing */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'read_unit_journal') {
|
||
const unit = typeof args.unit === 'string' ? args.unit.trim() : ''
|
||
if (!/^[a-zA-Z0-9._-]{1,96}$/.test(unit)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_unit' })
|
||
}
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
||
: 12_000
|
||
const tailOnly = Boolean(args.tail_only)
|
||
const p = '/run/bare-os/unit-journal/' + unit + '.ndjson'
|
||
appendProgress('read_unit_journal ' + unit)
|
||
return bareAgentJsonResult(await readBoundedText(p, maxChars, tailOnly, true))
|
||
}
|
||
|
||
if (toolName === 'inspect_ipc_backpressure') {
|
||
appendProgress('inspect_ipc_backpressure')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const candidates = [
|
||
'/proc/bare_os/ipc_backpressure.json',
|
||
'/proc/bare_os/replication_operator_sketch.json',
|
||
'/proc/bare_os/metrics_live.json'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, sources: [] }
|
||
for (const p of candidates) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
out.sources.push(p)
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'get_network_summary') {
|
||
appendProgress('get_network_summary')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const candidates = [
|
||
'/proc/bare_os/net_summary.json',
|
||
'/proc/bare_os/swarm.json',
|
||
'/proc/bare_os/swarm_status.json',
|
||
'/proc/bare_os/swarm_connection_manager_status.json'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, sources: [] }
|
||
for (const p of candidates) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
out.sources.push(p)
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'tail_telemetry_streams') {
|
||
const maxChars =
|
||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||
? Math.min(Math.max(Math.floor(args.max_chars), 200), 60_000)
|
||
: 8000
|
||
appendProgress('tail_telemetry_streams')
|
||
const pathsToRead = [
|
||
'/var/log/bare-os/audit.log',
|
||
'/var/log/bare-os/logger.jsonl',
|
||
'/var/log/bare-os/initd.log',
|
||
'/var/log/bare-os/cron.log'
|
||
]
|
||
/** @type {Record<string, unknown>} */
|
||
const out = { ok: true, streams: {} }
|
||
for (const p of pathsToRead) {
|
||
out.streams[p] = await readBoundedText(p, maxChars, true, true)
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'pkg_index_lookup') {
|
||
const key = typeof args.key === 'string' ? args.key.trim() : ''
|
||
if (!key) return bareAgentJsonResult({ ok: false, error: 'key_required' })
|
||
appendProgress('pkg_index_lookup ' + key.slice(0, 80))
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = 'pkg-swarm-index get --key ' + bareAgentShellQuote(key)
|
||
const r = await captureExec(cmd, 90000)
|
||
if (r.ok === false) return bareAgentJsonResult(r)
|
||
const txt = typeof r.stdout_stderr === 'string' ? r.stdout_stderr : ''
|
||
let json = null
|
||
try {
|
||
json = JSON.parse(txt)
|
||
} catch {
|
||
json = null
|
||
}
|
||
return bareAgentJsonResult({ ok: true, key, json, text: bareAgentTruncateChars(txt, 12000) })
|
||
}
|
||
|
||
if (toolName === 'list_verification_scripts') {
|
||
appendProgress('list_verification_scripts')
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
const out = []
|
||
for (const dir of ['/scripts', '/home/guest/scripts']) {
|
||
try {
|
||
const names = await vfs.readdir(dir)
|
||
const rows = names
|
||
.filter((n) => typeof n === 'string' && (n.endsWith('.mjs') || n.endsWith('.js')))
|
||
.sort()
|
||
.slice(0, 400)
|
||
.map((n) => dir + '/' + n)
|
||
out.push(...rows)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return bareAgentJsonResult({ ok: true, scripts: out })
|
||
}
|
||
|
||
if (toolName === 'run_maintenance_gate') {
|
||
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
||
const timeoutMs =
|
||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||
? Math.min(Math.max(Math.floor(args.timeout_ms), 1000), 900000)
|
||
: 180000
|
||
appendProgress('run_maintenance_gate ' + command)
|
||
const allow = {
|
||
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
||
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
||
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs',
|
||
'coreutils-test': 'npm test -w bare-os-coreutils'
|
||
}
|
||
if (!Object.prototype.hasOwnProperty.call(allow, command)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'command_not_allowlisted', allowlist: Object.keys(allow) })
|
||
}
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = /** @type {Record<string, string>} */ (allow)[command]
|
||
const r = await captureExec(cmd, timeoutMs, { captureExit: true })
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'run_contract_checks') {
|
||
const profile = typeof args.profile === 'string' ? args.profile.trim() : ''
|
||
appendProgress('run_contract_checks ' + profile)
|
||
const mapping = {
|
||
core: 'node scripts/verify-kernel-seeder-parity.mjs && node scripts/verify-man-coverage.mjs',
|
||
docs: 'node scripts/verify-doc-links.mjs && node scripts/verify-doc-contracts.mjs',
|
||
parity: 'node scripts/verify-kernel-seeder-parity.mjs'
|
||
}
|
||
if (!Object.prototype.hasOwnProperty.call(mapping, profile)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_profile', profiles: Object.keys(mapping) })
|
||
}
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const cmd = /** @type {Record<string, string>} */ (mapping)[profile]
|
||
const r = await captureExec(cmd, 300000, { captureExit: true })
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'summarize_build_drift') {
|
||
appendProgress('summarize_build_drift')
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const r = await captureExec('git status --short', 60000)
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'get_hrpc_bridge_health') {
|
||
appendProgress('get_hrpc_bridge_health')
|
||
/** @type {Record<string, unknown>} */
|
||
const out = {
|
||
ok: true,
|
||
hostCapabilities: {
|
||
hrpcBridge:
|
||
typeof ctx.bareOsHostCapability === 'function'
|
||
? Boolean(ctx.bareOsHostCapability('hrpcBridge'))
|
||
: false
|
||
}
|
||
}
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
for (const p of ['/proc/bare_os/hrpc_route_table.json', '/proc/bare_os/hrpc_health.json']) {
|
||
try {
|
||
const b = await vfs.readFile(p)
|
||
if (!b || !b.length) continue
|
||
const t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(b)
|
||
: String(new TextDecoder().decode(b))
|
||
try {
|
||
out[p] = JSON.parse(t)
|
||
} catch {
|
||
out[p] = bareAgentTruncateChars(t, 6000)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'get_hrpc_allowlist_status') {
|
||
appendProgress('get_hrpc_allowlist_status')
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const r = await captureExec('hrpc probe', 60000)
|
||
return bareAgentJsonResult(r)
|
||
}
|
||
|
||
if (toolName === 'emit_host_notification' || toolName === 'request_host_action') {
|
||
const cfg = configRef.current || {}
|
||
if (cfg && cfg.emergency_stop_mutations) {
|
||
return bareAgentJsonResult({ ok: false, error: 'emergency_stop_mutations_enabled' })
|
||
}
|
||
if (toolName === 'emit_host_notification' && !cfg.allow_host_notifications) {
|
||
return bareAgentJsonResult({ ok: false, error: 'host_notifications_disabled' })
|
||
}
|
||
if (toolName === 'request_host_action' && !cfg.allow_host_actions) {
|
||
return bareAgentJsonResult({ ok: false, error: 'host_actions_disabled' })
|
||
}
|
||
if (!cfg.allow_bridge_mutations) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bridge_mutations_disabled' })
|
||
}
|
||
if (typeof ctx.bareOsHrpcRequest !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'bareOsHrpcRequest unavailable' })
|
||
}
|
||
try {
|
||
if (toolName === 'emit_host_notification') {
|
||
const payload = {
|
||
title: String(args.title || '').slice(0, 200),
|
||
message: String(args.message || '').slice(0, 2000),
|
||
level: typeof args.level === 'string' ? args.level : 'info'
|
||
}
|
||
appendProgress('emit_host_notification ' + payload.title)
|
||
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_notify', payload)
|
||
return bareAgentJsonResult({ ok: true, result: res })
|
||
}
|
||
const action = String(args.action || '').trim()
|
||
if (!/^[a-zA-Z0-9._-]{1,64}$/.test(action)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'invalid_action' })
|
||
}
|
||
const payload =
|
||
args.payload && typeof args.payload === 'object' && !Array.isArray(args.payload)
|
||
? args.payload
|
||
: {}
|
||
appendProgress('request_host_action ' + action)
|
||
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_action', {
|
||
action,
|
||
payload
|
||
})
|
||
return bareAgentJsonResult({ ok: true, result: res })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_swarm_peers') {
|
||
appendProgress('get_swarm_peers')
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
const buf = await vfs.readFile('/proc/bare_os/swarm.json')
|
||
if (!buf || !buf.length) {
|
||
return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||
}
|
||
const txt =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
/** @type {unknown} */
|
||
let j = null
|
||
try {
|
||
j = JSON.parse(txt)
|
||
} catch {
|
||
return bareAgentJsonResult({ ok: true, raw: txt.slice(0, 120_000) })
|
||
}
|
||
return bareAgentJsonResult({ ok: true, swarm: j })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'get_resource_limits') {
|
||
appendProgress('get_resource_limits')
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus !== 'function') {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'bareOsGetResourceStatus unavailable'
|
||
})
|
||
}
|
||
const r = ctx.bareOsGetResourceStatus()
|
||
return bareAgentJsonResult({ ok: true, resources: r })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'run_js_script_at_path') {
|
||
const scriptPath = typeof args.path === 'string' ? args.path : ''
|
||
if (!bareAgentPathAllowed(scriptPath) || scriptPath.includes('..')) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (!vfs?.readFile || !execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||
}
|
||
appendProgress('run_js_script_at_path ' + scriptPath)
|
||
try {
|
||
await vfs.readFile(scriptPath)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: 'cannot_read_script', detail: msg })
|
||
}
|
||
const cmd = bareAgentShellQuote(scriptPath)
|
||
const r = await captureExec(cmd, 60000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'verification_hints') {
|
||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||
const pathsTouch =
|
||
typeof args.paths_touched === 'string' ? args.paths_touched : ''
|
||
appendProgress('verification_hints')
|
||
const combined = topic + ' ' + pathsTouch.replace(/,/g, ' ')
|
||
const hints = bareAgentVerificationHintsList(combined)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
scope_note:
|
||
'Suggested commands apply to the Bare OS git checkout on the host (npm/node at repo root). They do not run automatically.',
|
||
suggested_commands: hints
|
||
})
|
||
}
|
||
|
||
if (toolName === 'runtime_diagnostic_bundle') {
|
||
appendProgress('runtime_diagnostic_bundle')
|
||
/** @type {Record<string, unknown>} */
|
||
const bundle = {}
|
||
bundle.bareOsCtxApiVersion =
|
||
typeof ctx.bareOsCtxApiVersion !== 'undefined' ? ctx.bareOsCtxApiVersion : null
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus === 'function')
|
||
bundle.resources = ctx.bareOsGetResourceStatus()
|
||
} catch {
|
||
bundle.resources_error = true
|
||
}
|
||
/** @type {Record<string, unknown>} */
|
||
const procParts = {}
|
||
/** @type {string[]} */
|
||
let list = [...BARE_AGENT_PROC_READ_ALLOWLIST]
|
||
if (vfs && typeof vfs.readdir === 'function') {
|
||
try {
|
||
const names = await vfs.readdir('/proc/bare_os')
|
||
if (Array.isArray(names)) {
|
||
for (let i = 0; i < names.length; i++) {
|
||
const n = String(names[i] || '')
|
||
if (!n || n === '.' || n === '..') continue
|
||
const full = '/proc/bare_os/' + n
|
||
if (list.indexOf(full) === -1) list.push(full)
|
||
}
|
||
}
|
||
} catch {
|
||
/* keep seed list */
|
||
}
|
||
}
|
||
if (vfs && typeof vfs.readFile === 'function') {
|
||
for (let i = 0; i < list.length; i++) {
|
||
const procPath = list[i]
|
||
try {
|
||
const buf = await vfs.readFile(procPath)
|
||
if (!buf || !buf.length) continue
|
||
let t =
|
||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(new TextDecoder().decode(buf))
|
||
if (t.length > 80_000) t = t.slice(0, 80_000) + '\n… truncated'
|
||
try {
|
||
procParts[procPath] = JSON.parse(t)
|
||
} catch {
|
||
procParts[procPath] = t
|
||
}
|
||
} catch {
|
||
/* missing path */
|
||
}
|
||
}
|
||
}
|
||
bundle.proc = procParts
|
||
return bareAgentJsonResult({ ok: true, bundle })
|
||
}
|
||
|
||
if (toolName === 'web_fetch') {
|
||
const url = typeof args.url === 'string' ? args.url : ''
|
||
let hostHint = ''
|
||
try {
|
||
hostHint = new URL(url).hostname
|
||
} catch {
|
||
hostHint = ''
|
||
}
|
||
appendProgress('web_fetch ' + (hostHint || url.slice(0, 80)))
|
||
try {
|
||
const out = await bareWebRunTool({
|
||
ctx,
|
||
url,
|
||
method: typeof args.method === 'string' ? args.method : undefined,
|
||
headers:
|
||
args.headers &&
|
||
typeof args.headers === 'object' &&
|
||
!Array.isArray(args.headers)
|
||
? /** @type {Record<string, unknown>} */ (args.headers)
|
||
: undefined,
|
||
body: typeof args.body === 'string' ? args.body : undefined,
|
||
content_type:
|
||
typeof args.content_type === 'string' ? args.content_type : undefined,
|
||
max_response_bytes:
|
||
typeof args.max_response_bytes === 'number'
|
||
? args.max_response_bytes
|
||
: undefined,
|
||
max_redirects:
|
||
typeof args.max_redirects === 'number' ? args.max_redirects : undefined,
|
||
timeout_ms:
|
||
typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
||
format: typeof args.format === 'string' ? args.format : undefined,
|
||
max_links:
|
||
typeof args.max_links === 'number' ? args.max_links : undefined,
|
||
signal
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
} catch (e) {
|
||
const msg = bareWebFmtErr(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'grep') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
||
if (typeof bareAgentGrepFiles !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'grep_unavailable' })
|
||
}
|
||
const root =
|
||
(typeof args.path === 'string' && args.path.trim()) ||
|
||
(typeof args.root === 'string' && args.root.trim()) ||
|
||
home ||
|
||
'/home'
|
||
if (!bareAgentPathAllowed(root)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
appendProgress('grep ' + pattern + ' @ ' + root)
|
||
const out = await bareAgentGrepFiles(ctx, {
|
||
pattern,
|
||
root,
|
||
glob: typeof args.glob === 'string' ? args.glob : '',
|
||
ignore_case: Boolean(args.ignore_case),
|
||
before: args.before,
|
||
after: args.after,
|
||
context: args.context,
|
||
max_matches: args.max_matches,
|
||
files_with_matches: Boolean(args.files_with_matches),
|
||
output_mode: typeof args.output_mode === 'string' ? args.output_mode : ''
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'apply_patch') {
|
||
const patch =
|
||
(typeof args.patch === 'string' && args.patch) ||
|
||
(typeof args.input === 'string' && args.input) ||
|
||
''
|
||
if (!patch.trim()) return bareAgentJsonResult({ ok: false, error: 'patch_required' })
|
||
if (typeof bareAgentParseApplyPatch !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'apply_patch_unavailable' })
|
||
}
|
||
const parsed = bareAgentParseApplyPatch(patch)
|
||
if (!parsed.ok) return bareAgentJsonResult(parsed)
|
||
appendProgress('apply_patch ops=' + String((parsed.ops || []).length))
|
||
if (typeof bareAgentPushEdit === 'function' && paths.edits && parsed.ops) {
|
||
for (let i = 0; i < parsed.ops.length; i++) {
|
||
const op = parsed.ops[i]
|
||
const p = String((op && op.path) || '')
|
||
if (!p) continue
|
||
try {
|
||
const prev = await bareAgentReadTextFile(ctx, p)
|
||
await bareAgentPushEdit(ctx, paths.edits, { path: p, prev, tool: 'apply_patch' })
|
||
} catch {
|
||
/* new file */
|
||
}
|
||
}
|
||
}
|
||
const out = await bareAgentApplyPatchOps(ctx, parsed.ops, {
|
||
home,
|
||
denyPrefixes: mutateDenyPrefixes
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'update_goal') {
|
||
const message = typeof args.message === 'string' ? args.message.trim() : ''
|
||
const blocked =
|
||
typeof args.blocked_reason === 'string' ? args.blocked_reason.trim() : ''
|
||
const completed = Boolean(args.completed)
|
||
const next = { ...(configRef.current || {}) }
|
||
if (message) next.autonomous_last_error = ''
|
||
if (blocked) {
|
||
next.autonomous_status = 'blocked'
|
||
next.autonomous_last_error = blocked
|
||
next.autonomous_active = false
|
||
} else if (completed) {
|
||
next.autonomous_status = 'completed'
|
||
next.autonomous_active = false
|
||
if (message) next.autonomous_goal = String(next.autonomous_goal || '')
|
||
} else {
|
||
next.autonomous_status = 'running'
|
||
}
|
||
configRef.current = next
|
||
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
||
await bareAgentSaveConfigFromTools(ctx, paths, next)
|
||
}
|
||
appendProgress(
|
||
'update_goal ' +
|
||
(completed ? 'completed' : blocked ? 'blocked' : 'progress')
|
||
)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
completed,
|
||
blocked: Boolean(blocked),
|
||
message: message || null,
|
||
blocked_reason: blocked || null,
|
||
status: next.autonomous_status
|
||
})
|
||
}
|
||
|
||
if (toolName === 'web_search') {
|
||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||
const maxResults =
|
||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||
? Math.min(Math.max(Math.floor(args.max_results), 1), 16)
|
||
: 8
|
||
const url =
|
||
'https://api.duckduckgo.com/?q=' +
|
||
encodeURIComponent(query) +
|
||
'&format=json&no_html=1&skip_disambig=1'
|
||
appendProgress('web_search ' + query.slice(0, 80))
|
||
try {
|
||
const raw = await bareWebRunTool({
|
||
ctx,
|
||
url,
|
||
format: 'json',
|
||
timeout_ms: 20000,
|
||
max_response_bytes: 200000,
|
||
signal
|
||
})
|
||
if (!raw || raw.ok === false) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: (raw && raw.error) || 'web_search_failed',
|
||
hint: 'HTTP policy may block api.duckduckgo.com; try web_fetch on a known URL'
|
||
})
|
||
}
|
||
let payload = raw.extract && raw.extract.json
|
||
if (payload == null && raw.extract && typeof raw.extract.text_slice === 'string') {
|
||
try {
|
||
payload = JSON.parse(raw.extract.text_slice)
|
||
} catch {
|
||
payload = null
|
||
}
|
||
}
|
||
const results =
|
||
typeof bareAgentParseSearchResults === 'function'
|
||
? bareAgentParseSearchResults(payload, maxResults)
|
||
: []
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
query,
|
||
results,
|
||
abstract: payload && payload.Abstract ? String(payload.Abstract) : '',
|
||
count: results.length
|
||
})
|
||
} catch (e) {
|
||
const msg = typeof bareWebFmtErr === 'function' ? bareWebFmtErr(e) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
if (toolName === 'git_status') {
|
||
const cwd =
|
||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||
(ctx.env && typeof ctx.env === 'object'
|
||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||
: '') ||
|
||
home ||
|
||
'/home'
|
||
if (!execLine) {
|
||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
}
|
||
appendProgress('git_status ' + cwd)
|
||
const cmd =
|
||
'git -C ' +
|
||
bareAgentShellQuote(cwd) +
|
||
' status --short && echo --- && git -C ' +
|
||
bareAgentShellQuote(cwd) +
|
||
' diff --stat && echo --- && git -C ' +
|
||
bareAgentShellQuote(cwd) +
|
||
' log --oneline -8'
|
||
const r = await captureExec(cmd, 30000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false
|
||
? r
|
||
: { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'memory_append') {
|
||
const text = typeof args.text === 'string' ? args.text.trim() : ''
|
||
if (!text) return bareAgentJsonResult({ ok: false, error: 'text_required' })
|
||
const kindRaw = String(args.kind || 'NOTE').trim().toUpperCase()
|
||
const kind =
|
||
kindRaw === 'FACT' ||
|
||
kindRaw === 'CHECK' ||
|
||
kindRaw === 'RISK' ||
|
||
kindRaw === 'HANDOFF' ||
|
||
kindRaw === 'NOTE'
|
||
? kindRaw
|
||
: 'NOTE'
|
||
const workspace =
|
||
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
||
const memDir =
|
||
typeof paths.workspaceMemory === 'string'
|
||
? paths.workspaceMemory
|
||
: workspace + '/memory'
|
||
const dest = args.daily
|
||
? memDir +
|
||
'/' +
|
||
(typeof bareAgentWorkspaceUtcYmd === 'function'
|
||
? bareAgentWorkspaceUtcYmd()
|
||
: new Date().toISOString().slice(0, 10)) +
|
||
'.md'
|
||
: workspace + '/MEMORY.md'
|
||
const stamp = new Date().toISOString()
|
||
const line = '- ' + kind + ' — ' + text.replace(/\s+/g, ' ').trim()
|
||
let prev = ''
|
||
try {
|
||
prev = await bareAgentReadTextFile(ctx, dest)
|
||
} catch {
|
||
prev = ''
|
||
}
|
||
const next =
|
||
(prev ? prev.replace(/\s*$/, '') + '\n' : '# Memory\n\n') +
|
||
line +
|
||
' \n _' +
|
||
stamp +
|
||
'_\n'
|
||
await bareAgentWriteTextFile(ctx, dest, next)
|
||
appendProgress('memory_append ' + dest)
|
||
return bareAgentJsonResult({ ok: true, path: dest, kind, appended: line })
|
||
}
|
||
|
||
if (toolName === 'list_skills') {
|
||
const max =
|
||
typeof args.max === 'number' && Number.isFinite(args.max)
|
||
? Math.min(Math.max(Math.floor(args.max), 1), 80)
|
||
: 40
|
||
if (typeof bareAgentDiscoverSkills !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'skills_unavailable' })
|
||
}
|
||
const skillPaths = {
|
||
workspaceSkills:
|
||
typeof paths.workspaceSkills === 'string'
|
||
? paths.workspaceSkills
|
||
: paths.dir + '/workspace/skills',
|
||
skillsGlobal:
|
||
typeof paths.skillsGlobal === 'string'
|
||
? paths.skillsGlobal
|
||
: paths.dir + '/skills'
|
||
}
|
||
const skills = await bareAgentDiscoverSkills(ctx, skillPaths)
|
||
appendProgress('list_skills ' + String(skills.length))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
count: Math.min(skills.length, max),
|
||
truncated: skills.length > max,
|
||
skills: skills.slice(0, max)
|
||
})
|
||
}
|
||
|
||
if (toolName === 'schedule_task') {
|
||
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : ''
|
||
if (!prompt) return bareAgentJsonResult({ ok: false, error: 'prompt_required' })
|
||
const parsed =
|
||
typeof bareAgentParseScheduleInterval === 'function'
|
||
? bareAgentParseScheduleInterval(args.interval)
|
||
: { error: 'schedule_unavailable' }
|
||
if (parsed.error) return bareAgentJsonResult({ ok: false, error: parsed.error, hint: parsed.hint })
|
||
const id =
|
||
typeof bareAgentScheduleId === 'function'
|
||
? bareAgentScheduleId(args.id || prompt.slice(0, 24))
|
||
: 'agent-task'
|
||
const dir = (home || '/home') + '/.config/bare-os/timers'
|
||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||
}
|
||
try {
|
||
if (typeof vfs.mkdir === 'function') await vfs.mkdir(dir, { recursive: true })
|
||
} catch {
|
||
/* exists */
|
||
}
|
||
let names = []
|
||
try {
|
||
names = await vfs.readdir(dir)
|
||
} catch {
|
||
names = []
|
||
}
|
||
const existing = Array.isArray(names)
|
||
? names.filter(function (n) {
|
||
return typeof n === 'string' && n.endsWith('.timer')
|
||
})
|
||
: []
|
||
const dest = dir + '/' + id + '.timer'
|
||
const already = existing.indexOf(id + '.timer') >= 0
|
||
if (!already && existing.length >= 8) {
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'timer_limit',
|
||
max: 8,
|
||
hint: 'unschedule_task an existing id first'
|
||
})
|
||
}
|
||
const quoted = bareAgentShellQuote(prompt)
|
||
const line = args.auto === false ? 'agent ' + quoted : 'agent --auto ' + quoted
|
||
const body =
|
||
parsed.kind === 'everyMs'
|
||
? '[Timer]\nEveryMs=' + String(parsed.everyMs) + '\nExecLine=' + line + '\n'
|
||
: '[Timer]\nOnCalendar=' + parsed.onCalendar + '\nExecLine=' + line + '\n'
|
||
await bareAgentWriteTextFile(ctx, dest, body)
|
||
appendProgress('schedule_task ' + id)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
id,
|
||
path: dest,
|
||
interval: parsed,
|
||
exec: line
|
||
})
|
||
}
|
||
|
||
if (toolName === 'unschedule_task') {
|
||
const id =
|
||
typeof bareAgentScheduleId === 'function'
|
||
? bareAgentScheduleId(args.id)
|
||
: String(args.id || '')
|
||
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
|
||
const dest = (home || '/home') + '/.config/bare-os/timers/' + id + '.timer'
|
||
if (!vfs || typeof vfs.unlink !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'vfs.unlink unavailable' })
|
||
}
|
||
try {
|
||
await vfs.unlink(dest)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
return bareAgentJsonResult({ ok: false, error: msg, path: dest })
|
||
}
|
||
appendProgress('unschedule_task ' + id)
|
||
return bareAgentJsonResult({ ok: true, id, path: dest })
|
||
}
|
||
|
||
if (toolName === 'list_scheduled') {
|
||
const dir = (home || '/home') + '/.config/bare-os/timers'
|
||
let names = []
|
||
try {
|
||
names = vfs && typeof vfs.readdir === 'function' ? await vfs.readdir(dir) : []
|
||
} catch {
|
||
names = []
|
||
}
|
||
const rows = []
|
||
const list = Array.isArray(names) ? names : []
|
||
for (let i = 0; i < list.length; i++) {
|
||
const name = String(list[i] || '')
|
||
if (!name.endsWith('.timer') || name.indexOf('agent-') !== 0) continue
|
||
const path = dir + '/' + name
|
||
const preview = await bareAgentReadTextFile(ctx, path)
|
||
rows.push({ id: name.replace(/\.timer$/, ''), path, preview: preview.slice(0, 400) })
|
||
}
|
||
appendProgress('list_scheduled ' + String(rows.length))
|
||
return bareAgentJsonResult({ ok: true, count: rows.length, timers: rows })
|
||
}
|
||
|
||
if (toolName === 'fuzzy_find') {
|
||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||
const root =
|
||
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
||
if (typeof bareAgentFuzzyFind !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'fuzzy_unavailable' })
|
||
}
|
||
appendProgress('fuzzy_find ' + query)
|
||
const hits = await bareAgentFuzzyFind(ctx, root, query, { max: args.max })
|
||
return bareAgentJsonResult({ ok: true, query, root, hits })
|
||
}
|
||
|
||
if (toolName === 'read_many') {
|
||
const pathsIn = Array.isArray(args.paths) ? args.paths : []
|
||
if (!pathsIn.length) return bareAgentJsonResult({ ok: false, error: 'paths_required' })
|
||
const maxB =
|
||
typeof args.max_bytes_each === 'number' && Number.isFinite(args.max_bytes_each)
|
||
? Math.min(Math.max(Math.floor(args.max_bytes_each), 200), 200000)
|
||
: 32000
|
||
const files = []
|
||
for (let i = 0; i < pathsIn.length && i < 16; i++) {
|
||
const path = String(pathsIn[i] || '').trim()
|
||
if (!path || !bareAgentPathAllowed(path)) {
|
||
files.push({ path, ok: false, error: 'path_not_allowed' })
|
||
continue
|
||
}
|
||
let text = await bareAgentReadTextFile(ctx, path)
|
||
const truncated = text.length > maxB
|
||
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
|
||
files.push({ path, ok: true, content: text, truncated })
|
||
}
|
||
appendProgress('read_many ' + String(files.length))
|
||
return bareAgentJsonResult({ ok: true, files })
|
||
}
|
||
|
||
if (toolName === 'wait_for') {
|
||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
||
let re
|
||
try {
|
||
re = new RegExp(pattern, args.ignore_case ? 'i' : '')
|
||
} catch (e) {
|
||
return bareAgentJsonResult({ ok: false, error: 'bad_regex' })
|
||
}
|
||
const timeoutMs = Math.min(
|
||
120000,
|
||
Math.max(200, Math.floor(Number(args.timeout_ms) || 15000))
|
||
)
|
||
const intervalMs = Math.min(
|
||
5000,
|
||
Math.max(100, Math.floor(Number(args.interval_ms) || 400))
|
||
)
|
||
const path = typeof args.path === 'string' ? args.path.trim() : ''
|
||
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
||
if (!path && !command) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_or_command_required' })
|
||
}
|
||
appendProgress('wait_for ' + pattern)
|
||
const deadline = Date.now() + timeoutMs
|
||
let last = ''
|
||
while (Date.now() <= deadline) {
|
||
if (path) last = await bareAgentReadTextFile(ctx, path)
|
||
else if (command && execLine) {
|
||
const r = await captureExec(command, Math.min(intervalMs + 2000, 15000))
|
||
last = r && r.stdout_stderr ? String(r.stdout_stderr) : ''
|
||
}
|
||
re.lastIndex = 0
|
||
if (re.test(last)) {
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
matched: true,
|
||
elapsed_ms: timeoutMs - Math.max(0, deadline - Date.now()),
|
||
sample: last.slice(0, 800)
|
||
})
|
||
}
|
||
if (Date.now() + intervalMs > deadline) break
|
||
await new Promise(function (resolve) {
|
||
setTimeout(resolve, intervalMs)
|
||
})
|
||
}
|
||
return bareAgentJsonResult({
|
||
ok: false,
|
||
error: 'wait_timeout',
|
||
sample: last.slice(0, 400)
|
||
})
|
||
}
|
||
|
||
if (toolName === 'undo_last_edit') {
|
||
const editsPath = paths.edits || paths.dir + '/edits.json'
|
||
if (typeof bareAgentPopEdit !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'undo_unavailable' })
|
||
}
|
||
const rec = await bareAgentPopEdit(ctx, editsPath)
|
||
if (!rec || !rec.path) {
|
||
return bareAgentJsonResult({ ok: false, error: 'nothing_to_undo' })
|
||
}
|
||
await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || ''))
|
||
appendProgress('undo_last_edit ' + rec.path)
|
||
return bareAgentJsonResult({ ok: true, path: rec.path, tool: rec.tool || null })
|
||
}
|
||
|
||
if (toolName === 'git_diff') {
|
||
const cwd =
|
||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||
(ctx.env && typeof ctx.env === 'object'
|
||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||
: '') ||
|
||
home ||
|
||
'/home'
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||
const stat = Boolean(args.stat)
|
||
const cmd =
|
||
'git -C ' +
|
||
bareAgentShellQuote(cwd) +
|
||
' diff' +
|
||
(stat ? ' --stat' : '') +
|
||
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||
appendProgress('git_diff ' + cwd)
|
||
const r = await captureExec(cmd, 30000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'history_search') {
|
||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||
const histPath = paths.history || paths.dir + '/history.json'
|
||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||
const hits =
|
||
typeof bareAgentHistorySearch === 'function'
|
||
? bareAgentHistorySearch(Array.isArray(raw) ? raw : [], query, args.max)
|
||
: []
|
||
appendProgress('history_search ' + query)
|
||
return bareAgentJsonResult({ ok: true, query, hits })
|
||
}
|
||
|
||
if (toolName === 'git_log' || toolName === 'git_show' || toolName === 'git_blame') {
|
||
const cwd =
|
||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||
(ctx.env && typeof ctx.env === 'object'
|
||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||
: '') ||
|
||
home ||
|
||
'/home'
|
||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||
const quoted = bareAgentShellQuote(cwd)
|
||
let cmd = 'git -C ' + quoted + ' '
|
||
if (toolName === 'git_log') {
|
||
const max = Math.min(80, Math.max(1, Math.floor(Number(args.max) || 20)))
|
||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||
cmd += 'log --oneline -' + String(max) + (p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||
} else if (toolName === 'git_show') {
|
||
const rev = typeof args.rev === 'string' && args.rev.trim() ? args.rev.trim() : 'HEAD'
|
||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||
cmd +=
|
||
'show ' +
|
||
(args.stat ? '--stat ' : '') +
|
||
bareAgentShellQuote(rev) +
|
||
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||
} else {
|
||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||
if (!p) return bareAgentJsonResult({ ok: false, error: 'path_required' })
|
||
cmd += 'blame -- ' + bareAgentShellQuote(p)
|
||
}
|
||
appendProgress(toolName + ' ' + cwd)
|
||
const r = await captureExec(cmd, 30000)
|
||
return bareAgentJsonResult(
|
||
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||
)
|
||
}
|
||
|
||
if (toolName === 'copy_path') {
|
||
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
||
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
||
if (
|
||
!from ||
|
||
!to ||
|
||
from.includes('..') ||
|
||
to.includes('..') ||
|
||
!bareAgentPathAllowed(from) ||
|
||
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes)
|
||
) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (typeof bareAgentCopyPath !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'copy_unavailable' })
|
||
}
|
||
appendProgress('copy_path ' + from)
|
||
const out = await bareAgentCopyPath(ctx, from, to)
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'diff_files') {
|
||
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
||
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
||
if (!from || !to || !bareAgentPathAllowed(from) || !bareAgentPathAllowed(to)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
if (typeof bareAgentUnifiedDiff !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'diff_unavailable' })
|
||
}
|
||
const a = await bareAgentReadTextFile(ctx, from)
|
||
const b = await bareAgentReadTextFile(ctx, to)
|
||
appendProgress('diff_files ' + from)
|
||
const out = bareAgentUnifiedDiff(a, b, {
|
||
from,
|
||
to,
|
||
context: args.context
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'find_symbol') {
|
||
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
||
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
||
const root =
|
||
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
||
if (!bareAgentPathAllowed(root) || typeof bareAgentFindSymbol !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'find_symbol_unavailable' })
|
||
}
|
||
appendProgress('find_symbol ' + name)
|
||
const out = await bareAgentFindSymbol(ctx, root, name, {
|
||
glob: typeof args.glob === 'string' ? args.glob : '',
|
||
max: args.max
|
||
})
|
||
return bareAgentJsonResult(out)
|
||
}
|
||
|
||
if (toolName === 'create_skill') {
|
||
const id = String(args.id || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9-]+/g, '-')
|
||
.replace(/^-+|-+$/g, '')
|
||
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
|
||
const destRoot =
|
||
(typeof paths.workspaceSkills === 'string' && paths.workspaceSkills) ||
|
||
(typeof paths.workspace === 'string' ? paths.workspace + '/skills' : paths.dir + '/workspace/skills')
|
||
const dest = destRoot + '/' + id + '/SKILL.md'
|
||
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
const md =
|
||
typeof bareAgentSkillMarkdown === 'function'
|
||
? bareAgentSkillMarkdown({
|
||
name: typeof args.name === 'string' ? args.name : id,
|
||
description: typeof args.description === 'string' ? args.description : id,
|
||
body: typeof args.body === 'string' ? args.body : ''
|
||
})
|
||
: String(args.body || '')
|
||
await bareAgentWriteTextFile(ctx, dest, md)
|
||
appendProgress('create_skill ' + id)
|
||
return bareAgentJsonResult({ ok: true, id, path: dest })
|
||
}
|
||
|
||
if (toolName === 'rewind_session') {
|
||
const histPath = paths.history || paths.dir + '/history.json'
|
||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||
if (typeof bareAgentRewindHistory !== 'function') {
|
||
return bareAgentJsonResult({ ok: false, error: 'rewind_unavailable' })
|
||
}
|
||
const out = bareAgentRewindHistory(Array.isArray(raw) ? raw : [], {
|
||
steps: args.steps,
|
||
userIndex: args.user_index,
|
||
keep_user: args.keep_user
|
||
})
|
||
if (!out.ok) return bareAgentJsonResult(out)
|
||
await bareAgentSaveHistory(ctx, histPath, out.messages)
|
||
appendProgress('rewind_session dropped=' + String(out.dropped))
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
dropped: out.dropped,
|
||
remaining: out.messages.length,
|
||
target: out.target || null,
|
||
keep_user: Boolean(out.keep_user)
|
||
})
|
||
}
|
||
|
||
if (toolName === 'export_session') {
|
||
const histPath = paths.history || paths.dir + '/history.json'
|
||
const dest =
|
||
(typeof args.path === 'string' && args.path.trim()) ||
|
||
(paths.dir ? paths.dir + '/export.md' : '/tmp/agent-export.md')
|
||
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||
}
|
||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||
const md =
|
||
typeof bareAgentExportTranscript === 'function'
|
||
? bareAgentExportTranscript(Array.isArray(raw) ? raw : [])
|
||
: ''
|
||
await bareAgentWriteTextFile(ctx, dest, md)
|
||
appendProgress('export_session ' + dest)
|
||
return bareAgentJsonResult({
|
||
ok: true,
|
||
path: dest,
|
||
messages: Array.isArray(raw) ? raw.length : 0
|
||
})
|
||
}
|
||
|
||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
||
return bareAgentJsonResult({ ok: false, error: msg })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} base
|
||
* @param {Record<string, unknown>} patch
|
||
*/
|
||
function bareAgentMergeConfigPatch(base, patch) {
|
||
const out = { ...base }
|
||
const keys = [
|
||
'backend',
|
||
'rest_base_url',
|
||
'rest_api_key',
|
||
'model',
|
||
'qvac_model',
|
||
'qvac_profile',
|
||
'qvac_ctx_size',
|
||
'qvac_device',
|
||
'qvac_main_gpu',
|
||
'qvac_gpu_layers',
|
||
'max_tokens',
|
||
'temperature',
|
||
'provider',
|
||
'max_iterations',
|
||
'stream',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'access_policy',
|
||
'allow_delete',
|
||
'require_confirm_token',
|
||
'command_deny',
|
||
'mutate_deny_prefixes',
|
||
'owner_name',
|
||
'agent_label',
|
||
'show_reasoning',
|
||
'reasoning_mode',
|
||
'reasoning_max_chars',
|
||
'reasoning_include_tools',
|
||
'allow_bridge_mutations',
|
||
'allow_host_notifications',
|
||
'allow_host_actions',
|
||
'emergency_stop_mutations',
|
||
'autonomous_mode_enabled',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_completion_required_checks',
|
||
'autonomous_allow_paths',
|
||
'autonomous_deny_ops',
|
||
'autonomous_active',
|
||
'autonomous_started_at_ms',
|
||
'autonomous_stop_requested',
|
||
'autonomous_goal',
|
||
'autonomous_status',
|
||
'autonomous_last_error',
|
||
'context_compaction',
|
||
'compaction_keep_recent',
|
||
'compaction_tool_chars',
|
||
'plan_mode_active',
|
||
'todo_nudge_enabled'
|
||
]
|
||
const numKeys = new Set([
|
||
'max_tokens',
|
||
'temperature',
|
||
'max_iterations',
|
||
'tool_parallelism',
|
||
'request_timeout_ms',
|
||
'reasoning_max_chars',
|
||
'compaction_keep_recent',
|
||
'compaction_tool_chars',
|
||
'qvac_ctx_size',
|
||
'qvac_gpu_layers',
|
||
'autonomous_max_runtime_ms',
|
||
'autonomous_started_at_ms'
|
||
])
|
||
for (const k of keys) {
|
||
if (Object.prototype.hasOwnProperty.call(patch, k)) {
|
||
/** @type {unknown} */
|
||
const v = patch[k]
|
||
if (numKeys.has(k)) {
|
||
const n = Number(v)
|
||
if (Number.isFinite(n)) out[k] = n
|
||
} else if (
|
||
k === 'autonomous_completion_required_checks' ||
|
||
k === 'autonomous_allow_paths' ||
|
||
k === 'autonomous_deny_ops' ||
|
||
k === 'command_deny' ||
|
||
k === 'mutate_deny_prefixes'
|
||
) {
|
||
out[k] = Array.isArray(v) ? v.map((x) => String(x ?? '')).filter(Boolean) : out[k]
|
||
} else if (
|
||
k === 'stream' ||
|
||
k === 'allow_delete' ||
|
||
k === 'show_reasoning' ||
|
||
k === 'reasoning_include_tools' ||
|
||
k === 'allow_bridge_mutations' ||
|
||
k === 'allow_host_notifications' ||
|
||
k === 'allow_host_actions' ||
|
||
k === 'emergency_stop_mutations' ||
|
||
k === 'autonomous_mode_enabled' ||
|
||
k === 'autonomous_active' ||
|
||
k === 'autonomous_stop_requested' ||
|
||
k === 'plan_mode_active' ||
|
||
k === 'todo_nudge_enabled'
|
||
) {
|
||
out[k] = Boolean(v)
|
||
} else if (k === 'reasoning_mode') {
|
||
const mode = String(v ?? '').trim().toLowerCase()
|
||
out[k] = mode === 'summary' || mode === 'trace' ? mode : 'off'
|
||
} else if (k === 'context_compaction') {
|
||
const mode = String(v ?? '').trim().toLowerCase()
|
||
out[k] = mode === 'off' || mode === 'aggressive' ? mode : 'auto'
|
||
} else if (k === 'require_confirm_token') {
|
||
out[k] = String(v ?? '')
|
||
} else if (k === 'access_policy') {
|
||
const pol = String(v ?? '').trim().toLowerCase()
|
||
out[k] = pol === 'restricted' ? 'restricted' : 'full'
|
||
} 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) {
|
||
if (typeof bareAgentSaveConfig === 'function') {
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
return
|
||
}
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* Markdown → ANSI for /bin/agent replies (TTY-friendly, no deps).
|
||
*/
|
||
|
||
/**
|
||
* @param {boolean} useColor
|
||
* @param {'bold'|'dim'|'italic'|'code'|'heading'|'quote'|'hr'|'bullet'|'link'} kind
|
||
*/
|
||
function bareAgentMdSgr(useColor, kind) {
|
||
if (!useColor) return ''
|
||
switch (kind) {
|
||
case 'bold':
|
||
return '\x1b[1m'
|
||
case 'dim':
|
||
return '\x1b[2m'
|
||
case 'italic':
|
||
return '\x1b[3m'
|
||
case 'code':
|
||
return '\x1b[36m'
|
||
case 'heading':
|
||
return '\x1b[1;36m'
|
||
case 'quote':
|
||
return '\x1b[2;37m'
|
||
case 'hr':
|
||
return '\x1b[2m'
|
||
case 'bullet':
|
||
return '\x1b[33m'
|
||
case 'link':
|
||
return '\x1b[4;36m'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inline markdown on a single line (no nested fences).
|
||
* @param {string} line
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareAgentMdInline(line, useColor) {
|
||
const reset = useColor ? '\x1b[0m' : ''
|
||
let s = String(line || '')
|
||
// escape-ish: leave raw backslashes mostly alone
|
||
/** @type {string[]} */
|
||
const out = []
|
||
let i = 0
|
||
while (i < s.length) {
|
||
// inline code
|
||
if (s[i] === '`') {
|
||
const end = s.indexOf('`', i + 1)
|
||
if (end > i) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'code') + s.slice(i + 1, end) + reset
|
||
)
|
||
i = end + 1
|
||
continue
|
||
}
|
||
}
|
||
// bold ** or __
|
||
if (
|
||
(s[i] === '*' && s[i + 1] === '*') ||
|
||
(s[i] === '_' && s[i + 1] === '_')
|
||
) {
|
||
const mark = s[i]
|
||
const end = s.indexOf(mark + mark, i + 2)
|
||
if (end > i) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'bold') +
|
||
bareAgentMdInline(s.slice(i + 2, end), useColor) +
|
||
reset
|
||
)
|
||
i = end + 2
|
||
continue
|
||
}
|
||
}
|
||
// italic * or _
|
||
if (s[i] === '*' || s[i] === '_') {
|
||
const mark = s[i]
|
||
// avoid matching list markers at start handled elsewhere
|
||
const end = s.indexOf(mark, i + 1)
|
||
if (end > i + 1) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'italic') +
|
||
s.slice(i + 1, end) +
|
||
reset
|
||
)
|
||
i = end + 1
|
||
continue
|
||
}
|
||
}
|
||
// strike ~~
|
||
if (s[i] === '~' && s[i + 1] === '~') {
|
||
const end = s.indexOf('~~', i + 2)
|
||
if (end > i) {
|
||
out.push('\x1b[9m' + s.slice(i + 2, end) + reset)
|
||
i = end + 2
|
||
continue
|
||
}
|
||
}
|
||
// links [text](url)
|
||
if (s[i] === '[') {
|
||
const mid = s.indexOf('](', i + 1)
|
||
const end = mid >= 0 ? s.indexOf(')', mid + 2) : -1
|
||
if (mid > i && end > mid) {
|
||
const label = s.slice(i + 1, mid)
|
||
const url = s.slice(mid + 2, end)
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'link') +
|
||
label +
|
||
reset +
|
||
bareAgentMdSgr(useColor, 'dim') +
|
||
' (' +
|
||
url +
|
||
')' +
|
||
reset
|
||
)
|
||
i = end + 1
|
||
continue
|
||
}
|
||
}
|
||
out.push(s[i])
|
||
i++
|
||
}
|
||
return out.join('')
|
||
}
|
||
|
||
/**
|
||
* Visible width ignoring ANSI CSI sequences.
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentMdVisibleWidth(s) {
|
||
return String(s || '')
|
||
.replace(/\x1b\[[0-9;]*m/g, '')
|
||
.length
|
||
}
|
||
|
||
/**
|
||
* Wrap a string to width by visible columns (ANSI-aware, crude).
|
||
* @param {string} s
|
||
* @param {number} width
|
||
* @returns {string[]}
|
||
*/
|
||
function bareAgentMdWrapAnsi(s, width) {
|
||
const w = Math.max(8, width | 0)
|
||
const raw = String(s || '')
|
||
if (!raw) return ['']
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
let cur = ''
|
||
let vis = 0
|
||
let i = 0
|
||
while (i < raw.length) {
|
||
if (raw[i] === '\x1b' && raw[i + 1] === '[') {
|
||
let j = i + 2
|
||
while (j < raw.length && raw[j] !== 'm') j++
|
||
cur += raw.slice(i, Math.min(j + 1, raw.length))
|
||
i = Math.min(j + 1, raw.length)
|
||
continue
|
||
}
|
||
if (raw[i] === '\n') {
|
||
lines.push(cur)
|
||
cur = ''
|
||
vis = 0
|
||
i++
|
||
continue
|
||
}
|
||
if (vis >= w) {
|
||
lines.push(cur)
|
||
cur = ''
|
||
vis = 0
|
||
}
|
||
cur += raw[i]
|
||
vis++
|
||
i++
|
||
}
|
||
if (cur || !lines.length) lines.push(cur)
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* Render markdown source to ANSI lines (no trailing newline join).
|
||
* @param {string} md
|
||
* @param {{ useColor?: boolean, width?: number }} [opts]
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentRenderMarkdown(md, opts) {
|
||
const useColor = !opts || opts.useColor !== false
|
||
const width = Math.max(
|
||
40,
|
||
Math.min(500, (opts && opts.width) || 80)
|
||
)
|
||
const reset = useColor ? '\x1b[0m' : ''
|
||
const src = String(md || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||
const rows = src.split('\n')
|
||
/** @type {string[]} */
|
||
const out = []
|
||
let i = 0
|
||
let inFence = false
|
||
let fenceLang = ''
|
||
|
||
while (i < rows.length) {
|
||
const line = rows[i]
|
||
|
||
// fenced code
|
||
const fenceOpen = /^```\s*(\S*)\s*$/.exec(line)
|
||
if (fenceOpen && !inFence) {
|
||
inFence = true
|
||
fenceLang = fenceOpen[1] || ''
|
||
const label = fenceLang
|
||
? bareAgentMdSgr(useColor, 'dim') + '┌─ ' + fenceLang + ' ' + reset
|
||
: bareAgentMdSgr(useColor, 'dim') + '┌──' + reset
|
||
out.push(label)
|
||
i++
|
||
continue
|
||
}
|
||
if (inFence) {
|
||
if (/^```\s*$/.test(line)) {
|
||
inFence = false
|
||
out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset)
|
||
i++
|
||
continue
|
||
}
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'dim') +
|
||
'│ ' +
|
||
reset +
|
||
bareAgentMdSgr(useColor, 'code') +
|
||
line +
|
||
reset
|
||
)
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// hr
|
||
if (/^\s*(?:---+|\*\*\*+|___+)\s*$/.test(line)) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'hr') + '─'.repeat(Math.min(width, 40)) + reset
|
||
)
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// headings
|
||
const hm = /^(#{1,6})\s+(.*)$/.exec(line)
|
||
if (hm) {
|
||
const level = hm[1].length
|
||
const text = hm[2]
|
||
const prefix = level <= 2 ? '' : ' '.repeat(level - 2)
|
||
out.push(
|
||
prefix +
|
||
bareAgentMdSgr(useColor, 'heading') +
|
||
bareAgentMdInline(text, useColor) +
|
||
reset
|
||
)
|
||
if (level === 1) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'dim') +
|
||
'━'.repeat(Math.min(width, Math.max(8, text.length))) +
|
||
reset
|
||
)
|
||
}
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// blockquote
|
||
const qm = /^>\s?(.*)$/.exec(line)
|
||
if (qm) {
|
||
out.push(
|
||
bareAgentMdSgr(useColor, 'quote') +
|
||
'┃ ' +
|
||
reset +
|
||
bareAgentMdInline(qm[1], useColor)
|
||
)
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// unordered list
|
||
const ul = /^(\s*)([-*+])\s+(.*)$/.exec(line)
|
||
if (ul) {
|
||
const indent = Math.min(6, Math.floor(ul[1].length / 2))
|
||
const pad = ' '.repeat(indent)
|
||
out.push(
|
||
pad +
|
||
bareAgentMdSgr(useColor, 'bullet') +
|
||
'• ' +
|
||
reset +
|
||
bareAgentMdInline(ul[3], useColor)
|
||
)
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// ordered list
|
||
const ol = /^(\s*)(\d+)\.\s+(.*)$/.exec(line)
|
||
if (ol) {
|
||
const indent = Math.min(6, Math.floor(ol[1].length / 2))
|
||
const pad = ' '.repeat(indent)
|
||
out.push(
|
||
pad +
|
||
bareAgentMdSgr(useColor, 'bullet') +
|
||
ol[2] +
|
||
'. ' +
|
||
reset +
|
||
bareAgentMdInline(ol[3], useColor)
|
||
)
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// empty
|
||
if (!line.trim()) {
|
||
out.push('')
|
||
i++
|
||
continue
|
||
}
|
||
|
||
// paragraph — wrap
|
||
const rendered = bareAgentMdInline(line, useColor)
|
||
for (const wline of bareAgentMdWrapAnsi(rendered, width)) out.push(wline)
|
||
i++
|
||
}
|
||
|
||
// Ensure fence close if stream was truncated
|
||
if (inFence) out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset)
|
||
|
||
return out.join('\n') + (out.length ? '\n' : '')
|
||
}
|
||
|
||
/**
|
||
* Live thinking viewport for /bin/agent — fixed-height auto-follow box with
|
||
* scrollbar + keyboard review, separated from the assistant reply.
|
||
* Also splits Qwen-style <think> tags out of content deltas.
|
||
*/
|
||
|
||
/**
|
||
* Resolve usable terminal width (full width; no artificial 80/100/120 cap).
|
||
* @param {Record<string, unknown>} [ctx]
|
||
* @param {import('stream').Writable | undefined} [stdout]
|
||
*/
|
||
function bareAgentResolveTermCols(ctx, stdout) {
|
||
/** @type {number[]} */
|
||
const cands = []
|
||
const push = (v) => {
|
||
const n = Number(v)
|
||
if (Number.isFinite(n) && n >= 20) cands.push(Math.floor(n))
|
||
}
|
||
if (stdout && typeof stdout === 'object') {
|
||
push(/** @type {{ columns?: number }} */ (stdout).columns)
|
||
}
|
||
if (ctx && typeof ctx === 'object') {
|
||
const rs = ctx.replStdout
|
||
if (rs && typeof rs === 'object') {
|
||
push(/** @type {{ columns?: number }} */ (rs).columns)
|
||
}
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: null
|
||
if (env) push(env.COLUMNS)
|
||
}
|
||
try {
|
||
if (globalThis.process && globalThis.process.stdout) {
|
||
push(globalThis.process.stdout.columns)
|
||
}
|
||
if (globalThis.process && globalThis.process.env) {
|
||
push(globalThis.process.env.COLUMNS)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (!cands.length) return 80
|
||
// Prefer the largest reported size (stale COLUMNS=80 is common on wide TTYs).
|
||
return Math.min(500, Math.max(40, Math.max(...cands)))
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @param {number} width
|
||
* @returns {string[]}
|
||
*/
|
||
function bareAgentThinkWrapLines(s, width) {
|
||
const w = Math.max(8, width | 0)
|
||
const raw = String(s || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||
/** @type {string[]} */
|
||
const out = []
|
||
for (const para of raw.split('\n')) {
|
||
if (!para) {
|
||
out.push('')
|
||
continue
|
||
}
|
||
let rest = para
|
||
while (rest.length > w) {
|
||
let cut = rest.lastIndexOf(' ', w)
|
||
if (cut < Math.floor(w * 0.5)) cut = w
|
||
out.push(rest.slice(0, cut).trimEnd())
|
||
rest = rest.slice(cut).trimStart()
|
||
}
|
||
if (rest.length || !out.length) out.push(rest)
|
||
}
|
||
return out.length ? out : ['']
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @param {number} width
|
||
*/
|
||
function bareAgentThinkPad(s, width) {
|
||
const t = String(s || '')
|
||
if (t.length >= width) return t.slice(0, width)
|
||
return t + ' '.repeat(width - t.length)
|
||
}
|
||
|
||
/**
|
||
* @param {string} title
|
||
* @param {number} inner
|
||
* @param {boolean} fancy
|
||
*/
|
||
function bareAgentThinkTitleBar(title, inner, fancy) {
|
||
const label = String(title || ' thinking ')
|
||
const fill = Math.max(0, inner - label.length)
|
||
const left = Math.floor(fill / 2)
|
||
const right = fill - left
|
||
if (fancy) {
|
||
return '─'.repeat(left) + label + '─'.repeat(right)
|
||
}
|
||
return '-'.repeat(left) + label + '-'.repeat(right)
|
||
}
|
||
|
||
/**
|
||
* Scrollbar column chars for a viewport.
|
||
* @param {number} bodyLines
|
||
* @param {number} totalLines
|
||
* @param {number} viewStart
|
||
* @param {boolean} fancy
|
||
* @returns {string[]} length === bodyLines
|
||
*/
|
||
function bareAgentThinkScrollbar(bodyLines, totalLines, viewStart, fancy) {
|
||
/** @type {string[]} */
|
||
const col = []
|
||
const track = fancy ? '│' : '|'
|
||
const thumb = fancy ? '█' : '#'
|
||
const gap = fancy ? '░' : ':'
|
||
if (totalLines <= bodyLines) {
|
||
for (let i = 0; i < bodyLines; i++) col.push(track)
|
||
return col
|
||
}
|
||
const maxStart = totalLines - bodyLines
|
||
const thumbSize = Math.max(
|
||
1,
|
||
Math.round((bodyLines / totalLines) * bodyLines)
|
||
)
|
||
const thumbStart =
|
||
maxStart <= 0
|
||
? 0
|
||
: Math.round((viewStart / maxStart) * (bodyLines - thumbSize))
|
||
for (let i = 0; i < bodyLines; i++) {
|
||
col.push(i >= thumbStart && i < thumbStart + thumbSize ? thumb : gap)
|
||
}
|
||
return col
|
||
}
|
||
|
||
/**
|
||
* Fixed-height thinking panel with auto-follow + manual scroll review.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Writable | undefined} stdout
|
||
* @param {{
|
||
* useColor?: boolean,
|
||
* bodyLines?: number,
|
||
* maxChars?: number,
|
||
* write?: (ctx: Record<string, unknown>, out: unknown, s: string) => void
|
||
* }} [opts]
|
||
*/
|
||
function bareAgentCreateThinkPanel(ctx, stdout, opts) {
|
||
const useColor = opts && opts.useColor !== undefined ? Boolean(opts.useColor) : true
|
||
const bodyLines = Math.min(
|
||
18,
|
||
Math.max(4, (opts && opts.bodyLines) || 8)
|
||
)
|
||
const maxChars = Math.min(
|
||
80_000,
|
||
Math.max(400, (opts && opts.maxChars) || 24_000)
|
||
)
|
||
const write =
|
||
opts && typeof opts.write === 'function'
|
||
? opts.write
|
||
: typeof bareAgentWriteOut === 'function'
|
||
? bareAgentWriteOut
|
||
: (c, o, s) => {
|
||
try {
|
||
if (o && typeof o.write === 'function') o.write(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
const fancy =
|
||
useColor &&
|
||
!(
|
||
ctx.env &&
|
||
typeof ctx.env === 'object' &&
|
||
String(/** @type {Record<string, string>} */ (ctx.env).BARE_OS_AGENT_ASCII || '') ===
|
||
'1'
|
||
)
|
||
|
||
let text = ''
|
||
let drawn = false
|
||
let sealed = false
|
||
let height = 0
|
||
let startedAt = 0
|
||
/** Lines below the panel (status / reply) that must be preserved on redraw. */
|
||
let belowLines = 0
|
||
/** 0 = pinned to bottom (auto-follow); >0 = scrolled up from bottom. */
|
||
let scrollFromBottom = 0
|
||
let followTail = true
|
||
/** Optional status line under the box (e.g. answering…). */
|
||
let statusLine = ''
|
||
|
||
function cols() {
|
||
return bareAgentResolveTermCols(ctx, stdout)
|
||
}
|
||
|
||
function dim(s) {
|
||
if (!useColor) return s
|
||
const paint =
|
||
typeof bareEditSgr === 'function' ? bareEditSgr('dim', true) : '\x1b[90m'
|
||
const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m'
|
||
return paint + s + reset
|
||
}
|
||
|
||
function accent(s) {
|
||
if (!useColor) return s
|
||
const paint =
|
||
typeof bareEditSgr === 'function' ? bareEditSgr('comment', true) : '\x1b[90m'
|
||
const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m'
|
||
return paint + s + reset
|
||
}
|
||
|
||
function keyword(s) {
|
||
if (!useColor) return s
|
||
const paint =
|
||
typeof bareEditSgr === 'function' ? bareEditSgr('keyword', true) : '\x1b[36m'
|
||
const reset = typeof EDIT_ANSI_RESET === 'string' ? EDIT_ANSI_RESET : '\x1b[0m'
|
||
return paint + s + reset
|
||
}
|
||
|
||
function contentWidth(inner) {
|
||
// border + space + text + space + scrollbar + border
|
||
return Math.max(12, inner - 4)
|
||
}
|
||
|
||
/**
|
||
* @returns {{ lines: string[], wrappedCount: number, viewStart: number }}
|
||
*/
|
||
function frameLines() {
|
||
const c = cols()
|
||
const inner = Math.max(28, c - 2)
|
||
const textW = contentWidth(inner)
|
||
const wrapped = bareAgentThinkWrapLines(text, textW)
|
||
const maxStart = Math.max(0, wrapped.length - bodyLines)
|
||
if (followTail) scrollFromBottom = 0
|
||
const viewStart = Math.max(
|
||
0,
|
||
Math.min(maxStart, maxStart - scrollFromBottom)
|
||
)
|
||
const view = wrapped.slice(viewStart, viewStart + bodyLines)
|
||
while (view.length < bodyLines) view.push('')
|
||
const sb = bareAgentThinkScrollbar(
|
||
bodyLines,
|
||
wrapped.length,
|
||
viewStart,
|
||
fancy
|
||
)
|
||
|
||
const elapsed =
|
||
startedAt > 0 ? ((Date.now() - startedAt) / 1000).toFixed(1) + 's' : ''
|
||
const pos =
|
||
wrapped.length > bodyLines
|
||
? ' · ' +
|
||
String(viewStart + 1) +
|
||
'–' +
|
||
String(Math.min(wrapped.length, viewStart + bodyLines)) +
|
||
'/' +
|
||
String(wrapped.length)
|
||
: ''
|
||
const scrollHint =
|
||
wrapped.length > bodyLines ? ' · ↑↓/PgUp/PgDn' : ''
|
||
const title = sealed
|
||
? ' thinking · done' +
|
||
(elapsed ? ' · ' + elapsed : '') +
|
||
pos +
|
||
scrollHint +
|
||
' '
|
||
: ' thinking' +
|
||
(elapsed ? ' · ' + elapsed : '') +
|
||
(followTail ? ' · live' : ' · paused') +
|
||
pos +
|
||
scrollHint +
|
||
' '
|
||
const bar = bareAgentThinkTitleBar(title, inner, fancy)
|
||
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
if (fancy) {
|
||
lines.push(accent('┌' + bar + '┐'))
|
||
for (let r = 0; r < bodyLines; r++) {
|
||
lines.push(
|
||
accent('│') +
|
||
' ' +
|
||
dim(bareAgentThinkPad(view[r], textW)) +
|
||
' ' +
|
||
keyword(sb[r]) +
|
||
accent('│')
|
||
)
|
||
}
|
||
const footLabel = followTail
|
||
? wrapped.length > bodyLines
|
||
? ' follow · ' + String(wrapped.length) + ' lines '
|
||
: ''
|
||
: ' scrolled · end to resume '
|
||
const foot = footLabel
|
||
? bareAgentThinkTitleBar(footLabel, inner, true)
|
||
: '─'.repeat(inner)
|
||
lines.push(accent('└' + foot + '┘'))
|
||
} else {
|
||
lines.push('+' + bar.replace(/─/g, '-') + '+')
|
||
for (let r = 0; r < bodyLines; r++) {
|
||
lines.push(
|
||
'| ' + bareAgentThinkPad(view[r], textW) + ' ' + sb[r] + '|'
|
||
)
|
||
}
|
||
lines.push('+' + '-'.repeat(inner) + '+')
|
||
}
|
||
return { lines, wrappedCount: wrapped.length, viewStart }
|
||
}
|
||
|
||
function totalDrawnHeight() {
|
||
return height + belowLines
|
||
}
|
||
|
||
function redraw() {
|
||
// Never paint an empty think frame.
|
||
if (!text.trim() && !statusLine) return
|
||
if (!text.trim()) return
|
||
const { lines } = frameLines()
|
||
/** @type {string[]} */
|
||
const block = lines.slice()
|
||
if (statusLine) block.push(statusLine)
|
||
|
||
let out = ''
|
||
if (drawn && totalDrawnHeight() > 0) {
|
||
out += '\x1b[' + String(totalDrawnHeight()) + 'A\r'
|
||
} else {
|
||
out += '\n'
|
||
}
|
||
for (let i = 0; i < block.length; i++) {
|
||
out += '\x1b[K' + block[i] + '\n'
|
||
}
|
||
const prev = totalDrawnHeight()
|
||
if (drawn && prev > block.length) {
|
||
for (let i = block.length; i < prev; i++) out += '\x1b[K\n'
|
||
out += '\x1b[' + String(prev - block.length) + 'A\r'
|
||
}
|
||
height = lines.length
|
||
belowLines = statusLine ? 1 : 0
|
||
drawn = true
|
||
write(ctx, stdout, out)
|
||
}
|
||
|
||
/**
|
||
* @param {number} delta positive = scroll up into history
|
||
*/
|
||
function scrollBy(delta) {
|
||
if (!drawn || !text.trim()) return false
|
||
const c = cols()
|
||
const inner = Math.max(28, c - 2)
|
||
const wrapped = bareAgentThinkWrapLines(text, contentWidth(inner))
|
||
const maxFromBottom = Math.max(0, wrapped.length - bodyLines)
|
||
if (maxFromBottom <= 0) return false
|
||
followTail = false
|
||
scrollFromBottom = Math.max(
|
||
0,
|
||
Math.min(maxFromBottom, scrollFromBottom + delta)
|
||
)
|
||
if (scrollFromBottom === 0) followTail = true
|
||
redraw()
|
||
return true
|
||
}
|
||
|
||
return {
|
||
/**
|
||
* @param {string} chunk
|
||
*/
|
||
append(chunk) {
|
||
const add = String(chunk || '')
|
||
if (!add || sealed) return
|
||
// Ignore whitespace-only until we have real thinking text (no empty box).
|
||
if (!text.trim() && !add.trim()) return
|
||
if (!startedAt) startedAt = Date.now()
|
||
text += add
|
||
if (text.length > maxChars) text = text.slice(text.length - maxChars)
|
||
if (!text.trim()) return
|
||
if (followTail) scrollFromBottom = 0
|
||
redraw()
|
||
},
|
||
seal() {
|
||
// Empty box: stay undrawn and unlocked so late reasoning can still appear.
|
||
if (!text.trim()) return
|
||
if (sealed) {
|
||
redraw()
|
||
return
|
||
}
|
||
sealed = true
|
||
redraw()
|
||
},
|
||
/**
|
||
* @param {string} s
|
||
*/
|
||
setStatus(s) {
|
||
if (!text.trim()) return
|
||
statusLine = String(s || '')
|
||
redraw()
|
||
},
|
||
clearStatus() {
|
||
if (!statusLine) return
|
||
statusLine = ''
|
||
if (drawn) redraw()
|
||
},
|
||
/**
|
||
* After reply is painted below, stop managing below-region.
|
||
*/
|
||
detachBelow() {
|
||
belowLines = 0
|
||
statusLine = ''
|
||
},
|
||
scrollUp(n) {
|
||
return scrollBy(Math.max(1, n || 1))
|
||
},
|
||
scrollDown(n) {
|
||
return scrollBy(-Math.max(1, n || 1))
|
||
},
|
||
pageUp() {
|
||
return scrollBy(Math.max(1, bodyLines - 1))
|
||
},
|
||
pageDown() {
|
||
return scrollBy(-Math.max(1, bodyLines - 1))
|
||
},
|
||
scrollHome() {
|
||
if (!drawn || !text.trim()) return false
|
||
const c = cols()
|
||
const inner = Math.max(28, c - 2)
|
||
const wrapped = bareAgentThinkWrapLines(text, contentWidth(inner))
|
||
const maxFromBottom = Math.max(0, wrapped.length - bodyLines)
|
||
followTail = false
|
||
scrollFromBottom = maxFromBottom
|
||
redraw()
|
||
return true
|
||
},
|
||
scrollEnd() {
|
||
if (!drawn || !text.trim()) return false
|
||
followTail = true
|
||
scrollFromBottom = 0
|
||
redraw()
|
||
return true
|
||
},
|
||
/**
|
||
* @param {{ type?: string, key?: string, ch?: string, code?: number | string }} ev
|
||
*/
|
||
handleKey(ev) {
|
||
if (!ev || !drawn) return false
|
||
if (ev.type === 'nav') {
|
||
if (ev.key === 'up') return this.scrollUp(1)
|
||
if (ev.key === 'down') return this.scrollDown(1)
|
||
if (ev.key === 'pageup') return this.pageUp()
|
||
if (ev.key === 'pagedown') return this.pageDown()
|
||
if (ev.key === 'home') return this.scrollHome()
|
||
if (ev.key === 'end') return this.scrollEnd()
|
||
}
|
||
if (ev.type === 'key') {
|
||
if (ev.ch === 'k') return this.scrollUp(1)
|
||
if (ev.ch === 'j') return this.scrollDown(1)
|
||
if (ev.ch === 'g') return this.scrollHome()
|
||
if (ev.ch === 'G') return this.scrollEnd()
|
||
}
|
||
return false
|
||
},
|
||
panelHeight() {
|
||
return height
|
||
},
|
||
isOpen() {
|
||
return drawn && !sealed
|
||
},
|
||
isDrawn() {
|
||
return drawn
|
||
},
|
||
hasContent() {
|
||
return text.trim().length > 0
|
||
},
|
||
getText() {
|
||
return text
|
||
},
|
||
isFollowing() {
|
||
return followTail
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Attach arrow/page keys to a think panel for the duration of a turn.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {ReturnType<typeof bareAgentCreateThinkPanel> | null} panel
|
||
* @param {{ onAbort?: () => void }} [opts]
|
||
* @returns {() => void} dispose
|
||
*/
|
||
function bareAgentAttachThinkScrollKeys(ctx, panel, opts) {
|
||
if (!panel) return () => {}
|
||
const stdin =
|
||
/** @type {{ isTTY?: boolean, setRawMode?: (v: boolean) => void, on?: Function, off?: Function, removeListener?: Function, resume?: Function }} */ (
|
||
ctx.replStdin || ctx.stdin
|
||
)
|
||
if (!stdin || !stdin.isTTY || typeof stdin.on !== 'function') return () => {}
|
||
|
||
let rawSet = false
|
||
let disposed = false
|
||
try {
|
||
if (typeof stdin.setRawMode === 'function') {
|
||
stdin.setRawMode(true)
|
||
rawSet = true
|
||
}
|
||
} catch {
|
||
rawSet = false
|
||
}
|
||
try {
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
/** @type {number[]} */
|
||
const q = []
|
||
const onAbort =
|
||
opts && typeof opts.onAbort === 'function' ? opts.onAbort : null
|
||
|
||
/**
|
||
* @param {string | Uint8Array | Buffer} chunk
|
||
*/
|
||
function onData(chunk) {
|
||
if (disposed) return
|
||
const bytes =
|
||
typeof bareEditChunkBytes === 'function'
|
||
? bareEditChunkBytes(chunk)
|
||
: typeof chunk === 'string'
|
||
? [...chunk].map((c) => c.charCodeAt(0) & 0xff)
|
||
: Array.from(/** @type {Uint8Array} */ (chunk))
|
||
for (const b of bytes) q.push(b)
|
||
for (;;) {
|
||
if (!q.length) break
|
||
// Ctrl+C — abort agent turn
|
||
if (q[0] === 3) {
|
||
q.shift()
|
||
try {
|
||
if (onAbort) onAbort()
|
||
else if (globalThis.process && typeof globalThis.process.emit === 'function') {
|
||
globalThis.process.emit('SIGINT')
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
continue
|
||
}
|
||
// Ctrl+D / Ctrl+X — treat as abort so shell exit is not wedged under raw mode
|
||
if (q[0] === 4 || q[0] === 24) {
|
||
q.shift()
|
||
try {
|
||
if (onAbort) onAbort()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
continue
|
||
}
|
||
const ev =
|
||
typeof bareEditTryConsumeKey === 'function'
|
||
? bareEditTryConsumeKey(q)
|
||
: null
|
||
if (!ev) {
|
||
if (q.length && q[0] === 27 && q.length < 6) break
|
||
if (q.length && q[0] !== 27) {
|
||
const ch = String.fromCharCode(/** @type {number} */ (q.shift()))
|
||
panel.handleKey({ type: 'key', ch })
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
panel.handleKey(ev)
|
||
}
|
||
}
|
||
|
||
stdin.on('data', onData)
|
||
return () => {
|
||
if (disposed) return
|
||
disposed = true
|
||
try {
|
||
if (typeof stdin.off === 'function') stdin.off('data', onData)
|
||
else if (typeof stdin.removeListener === 'function') {
|
||
stdin.removeListener('data', onData)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (rawSet) {
|
||
try {
|
||
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Split streamed assistant text into thinking vs visible content.
|
||
* Handles Qwen3 `<think>…</think>` (and `<thinking>`) across chunk boundaries.
|
||
* @param {{
|
||
* onThink: (s: string) => void,
|
||
* onContent: (s: string) => void
|
||
* }} handlers
|
||
*/
|
||
function bareAgentCreateThinkTagSplitter(handlers) {
|
||
const onThink = handlers.onThink
|
||
const onContent = handlers.onContent
|
||
/** @type {'content' | 'think'} */
|
||
let mode = 'content'
|
||
let buf = ''
|
||
|
||
const OPEN = '<'
|
||
/** @type {RegExp} */
|
||
const OPEN_TAG = /^<(?:think|thinking|redacted_thinking)\s*>/i
|
||
/** @type {RegExp} */
|
||
const CLOSE_TAG = /^<\/(?:think|thinking|redacted_thinking)\s*>/i
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @param {'content' | 'think'} m
|
||
*/
|
||
function partialTagLen(s, m) {
|
||
const samples =
|
||
m === 'content'
|
||
? ['<think>', '<thinking>', '<redacted_thinking>']
|
||
: ['</think>', '</thinking>', '</redacted_thinking>']
|
||
let best = 0
|
||
for (const sample of samples) {
|
||
for (let n = 1; n < sample.length; n++) {
|
||
if (s.endsWith(sample.slice(0, n))) best = Math.max(best, n)
|
||
}
|
||
}
|
||
if (s.endsWith('<')) best = Math.max(best, 1)
|
||
if (s.endsWith('</')) best = Math.max(best, 2)
|
||
return best
|
||
}
|
||
|
||
/**
|
||
* @param {string} chunk
|
||
*/
|
||
function push(chunk) {
|
||
if (!chunk) return
|
||
buf += chunk
|
||
for (;;) {
|
||
if (mode === 'content') {
|
||
const lt = buf.indexOf(OPEN)
|
||
if (lt < 0) {
|
||
if (buf) onContent(buf)
|
||
buf = ''
|
||
return
|
||
}
|
||
if (lt > 0) {
|
||
onContent(buf.slice(0, lt))
|
||
buf = buf.slice(lt)
|
||
}
|
||
const om = OPEN_TAG.exec(buf)
|
||
if (om) {
|
||
buf = buf.slice(om[0].length)
|
||
mode = 'think'
|
||
continue
|
||
}
|
||
if (partialTagLen(buf, 'content') === buf.length) return
|
||
onContent(buf.slice(0, 1))
|
||
buf = buf.slice(1)
|
||
continue
|
||
}
|
||
const lt = buf.indexOf('<')
|
||
if (lt < 0) {
|
||
if (buf) onThink(buf)
|
||
buf = ''
|
||
return
|
||
}
|
||
if (lt > 0) {
|
||
onThink(buf.slice(0, lt))
|
||
buf = buf.slice(lt)
|
||
}
|
||
const cm = CLOSE_TAG.exec(buf)
|
||
if (cm) {
|
||
buf = buf.slice(cm[0].length)
|
||
mode = 'content'
|
||
continue
|
||
}
|
||
if (partialTagLen(buf, 'think') === buf.length) return
|
||
onThink(buf.slice(0, 1))
|
||
buf = buf.slice(1)
|
||
}
|
||
}
|
||
|
||
function flush() {
|
||
if (!buf) return
|
||
if (mode === 'think') onThink(buf)
|
||
else onContent(buf)
|
||
buf = ''
|
||
}
|
||
|
||
return { push, flush, inThink: () => mode === 'think' }
|
||
}
|
||
|
||
/**
|
||
* Advanced context-window compaction for /bin/agent.
|
||
* Tiered rollover: shrink fat tool payloads → digest older turns →
|
||
* rolling summary message → hard trim fallback.
|
||
*/
|
||
|
||
/**
|
||
* @param {unknown} value
|
||
*/
|
||
function bareAgentCompactEstimateTokens(value) {
|
||
if (typeof bareAgentEstimateTokens === 'function') {
|
||
return bareAgentEstimateTokens(value)
|
||
}
|
||
try {
|
||
return Math.max(0, Math.ceil(JSON.stringify(value == null ? '' : value).length / 4))
|
||
} catch {
|
||
return 0
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} msg
|
||
* @returns {string}
|
||
*/
|
||
function bareAgentMessagePlainText(msg) {
|
||
if (!msg || typeof msg !== 'object') return ''
|
||
const m = /** @type {Record<string, unknown>} */ (msg)
|
||
const c = m.content
|
||
if (typeof c === 'string') return c
|
||
if (Array.isArray(c)) {
|
||
/** @type {string[]} */
|
||
const parts = []
|
||
for (const part of c) {
|
||
if (typeof part === 'string') parts.push(part)
|
||
else if (part && typeof part === 'object') {
|
||
const p = /** @type {Record<string, unknown>} */ (part)
|
||
if (typeof p.text === 'string') parts.push(p.text)
|
||
else if (typeof p.content === 'string') parts.push(p.content)
|
||
}
|
||
}
|
||
return parts.join('\n')
|
||
}
|
||
if (m.tool_calls && Array.isArray(m.tool_calls)) {
|
||
/** @type {string[]} */
|
||
const names = []
|
||
for (const tc of m.tool_calls) {
|
||
if (!tc || typeof tc !== 'object') continue
|
||
const fn = /** @type {Record<string, unknown>} */ (tc).function
|
||
if (fn && typeof fn === 'object') {
|
||
const n = /** @type {Record<string, unknown>} */ (fn).name
|
||
if (typeof n === 'string' && n) names.push(n)
|
||
}
|
||
}
|
||
if (names.length) return 'tool_calls: ' + names.join(', ')
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @param {number} max
|
||
*/
|
||
function bareAgentClipText(s, max) {
|
||
const t = String(s || '').replace(/\s+/g, ' ').trim()
|
||
if (t.length <= max) return t
|
||
if (max < 24) return t.slice(0, max)
|
||
const head = Math.floor(max * 0.62)
|
||
const tail = Math.max(8, max - head - 5)
|
||
return t.slice(0, head) + ' … ' + t.slice(-tail)
|
||
}
|
||
|
||
/**
|
||
* Shrink oversized tool / assistant payloads in-place (clone).
|
||
* @param {unknown[]} msgs
|
||
* @param {number} toolMaxChars
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentShrinkFatMessages(msgs, toolMaxChars) {
|
||
const max = Math.max(240, Math.floor(toolMaxChars || 1600))
|
||
/** @type {unknown[]} */
|
||
const out = []
|
||
for (const msg of msgs) {
|
||
if (!msg || typeof msg !== 'object') {
|
||
out.push(msg)
|
||
continue
|
||
}
|
||
const m = /** @type {Record<string, unknown>} */ (msg)
|
||
const role = String(m.role || '')
|
||
if (role === 'tool' && typeof m.content === 'string' && m.content.length > max) {
|
||
out.push({
|
||
...m,
|
||
content:
|
||
bareAgentClipText(m.content, max) +
|
||
'\n[tool output compacted ' +
|
||
String(m.content.length) +
|
||
'→' +
|
||
String(max) +
|
||
' chars]'
|
||
})
|
||
continue
|
||
}
|
||
if (
|
||
(role === 'assistant' || role === 'user') &&
|
||
typeof m.content === 'string' &&
|
||
m.content.length > max * 4
|
||
) {
|
||
out.push({
|
||
...m,
|
||
content:
|
||
bareAgentClipText(m.content, max * 4) +
|
||
'\n[message compacted]'
|
||
})
|
||
continue
|
||
}
|
||
out.push(msg)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Group chat into [system?] + turn groups (user | assistant+tools*).
|
||
* @param {unknown[]} msgs
|
||
* @returns {{ system: unknown | null, groups: unknown[][] }}
|
||
*/
|
||
function bareAgentGroupMessageTurns(msgs) {
|
||
/** @type {unknown | null} */
|
||
let system = null
|
||
/** @type {unknown[][]} */
|
||
const groups = []
|
||
/** @type {unknown[]} */
|
||
let cur = []
|
||
|
||
function flush() {
|
||
if (cur.length) {
|
||
groups.push(cur)
|
||
cur = []
|
||
}
|
||
}
|
||
|
||
for (const msg of Array.isArray(msgs) ? msgs : []) {
|
||
if (!msg || typeof msg !== 'object') continue
|
||
const role = String(/** @type {Record<string, unknown>} */ (msg).role || '')
|
||
if (role === 'system' && system == null && groups.length === 0 && !cur.length) {
|
||
system = msg
|
||
continue
|
||
}
|
||
if (role === 'user') {
|
||
flush()
|
||
cur = [msg]
|
||
continue
|
||
}
|
||
if (role === 'assistant' || role === 'tool') {
|
||
if (!cur.length) cur = [msg]
|
||
else cur.push(msg)
|
||
continue
|
||
}
|
||
// unknown roles — attach to current or solo group
|
||
if (!cur.length) cur = [msg]
|
||
else cur.push(msg)
|
||
}
|
||
flush()
|
||
return { system, groups }
|
||
}
|
||
|
||
/**
|
||
* True if message is a prior compaction summary we injected.
|
||
* @param {unknown} msg
|
||
*/
|
||
function bareAgentIsCompactionMessage(msg) {
|
||
if (!msg || typeof msg !== 'object') return false
|
||
const m = /** @type {Record<string, unknown>} */ (msg)
|
||
if (m.bare_os_compaction === true) return true
|
||
const c = typeof m.content === 'string' ? m.content : ''
|
||
const t = c.trim()
|
||
return (
|
||
String(m.role || '') === 'user' &&
|
||
(/^\[context compaction\]/i.test(t) ||
|
||
/^This session is being continued from a previous conversation/i.test(t) ||
|
||
m.bare_os_reminder === true)
|
||
)
|
||
}
|
||
|
||
/** Grok-style continuation carrier minimum (extractive; slightly below LLM 500). */
|
||
var BARE_AGENT_MIN_SUMMARY_SEED_CHARS = 160
|
||
|
||
/**
|
||
* Strip drafting tags and neutralize leftover <summary>/<analysis> tokens
|
||
* so they cannot prime the next turn (Grok format_compact_summary).
|
||
* @param {string} raw
|
||
*/
|
||
function bareAgentFormatCompactSummary(raw) {
|
||
let result = String(raw || '')
|
||
result = result.replace(/<analysis>[\s\S]*?<\/analysis>/gi, '')
|
||
result = result.replace(/<\/?summary>/gi, '')
|
||
result = result
|
||
.replace(/<\/summary>/g, '<\u200b/summary>')
|
||
.replace(/<summary>/g, '<\u200bsummary>')
|
||
.replace(/<\/analysis>/g, '<\u200b/analysis>')
|
||
.replace(/<analysis>/g, '<\u200banalysis>')
|
||
while (result.indexOf('\n\n\n') >= 0) result = result.replace(/\n\n\n/g, '\n\n')
|
||
return result.trim()
|
||
}
|
||
|
||
/**
|
||
* @param {string} raw
|
||
*/
|
||
function bareAgentFormatCompactSummaryContent(raw) {
|
||
const cleaned = bareAgentFormatCompactSummary(raw)
|
||
return (
|
||
'This session is being continued from a previous conversation that ran out of context. ' +
|
||
'The summary below covers the earlier portion of the conversation.\n\n' +
|
||
cleaned
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
*/
|
||
function bareAgentWrapUserQuery(text) {
|
||
const t = String(text || '').trim()
|
||
if (!t) return ''
|
||
if (/^<user_query>/.test(t)) return t
|
||
return '<user_query>\n' + t + '\n</user_query>'
|
||
}
|
||
|
||
/**
|
||
* @param {string} raw
|
||
*/
|
||
function bareAgentIsDegenerateSummary(raw) {
|
||
return bareAgentFormatCompactSummary(raw).length < BARE_AGENT_MIN_SUMMARY_SEED_CHARS
|
||
}
|
||
|
||
/**
|
||
* Collect unique path-like and command tokens for the artifacts section.
|
||
* @param {string} text
|
||
* @param {number} max
|
||
*/
|
||
function bareAgentExtractPathTokens(text, max) {
|
||
const s = String(text || '')
|
||
const found = []
|
||
const re = /(?:~|\/)[A-Za-z0-9._+\-@/]+/g
|
||
let m
|
||
while ((m = re.exec(s)) && found.length < max) {
|
||
const p = m[0]
|
||
if (p.length < 3 || found.indexOf(p) >= 0) continue
|
||
found.push(p)
|
||
}
|
||
return found
|
||
}
|
||
|
||
/**
|
||
* One-line digest for a message (extractive).
|
||
* @param {unknown} msg
|
||
* @param {number} max
|
||
*/
|
||
function bareAgentDigestMessage(msg, max) {
|
||
if (!msg || typeof msg !== 'object') return ''
|
||
const m = /** @type {Record<string, unknown>} */ (msg)
|
||
const role = String(m.role || '?')
|
||
const text = bareAgentMessagePlainText(m)
|
||
if (role === 'tool') {
|
||
const name =
|
||
typeof m.name === 'string'
|
||
? m.name
|
||
: typeof m.tool_call_id === 'string'
|
||
? m.tool_call_id.slice(0, 12)
|
||
: 'tool'
|
||
return '- tool:' + name + ' ' + bareAgentClipText(text, Math.max(40, max - 24))
|
||
}
|
||
if (role === 'assistant') {
|
||
return '- assistant ' + bareAgentClipText(text || '(tools only)', max)
|
||
}
|
||
if (role === 'user') {
|
||
if (bareAgentIsCompactionMessage(m)) {
|
||
return '- prior-compaction ' + bareAgentClipText(text.replace(/^\[context compaction\][^\n]*\n?/i, ''), max)
|
||
}
|
||
return '- user ' + bareAgentClipText(text, max)
|
||
}
|
||
return '- ' + role + ' ' + bareAgentClipText(text, max)
|
||
}
|
||
|
||
/**
|
||
* Grok-style 7-section extractive summary of older turn groups.
|
||
* @param {unknown[][]} groups
|
||
* @param {{ maxChars?: number, perMsg?: number }} [opts]
|
||
*/
|
||
function bareAgentBuildStructuredSummary(groups, opts) {
|
||
const maxChars = Math.max(400, Math.floor((opts && opts.maxChars) || 3500))
|
||
const perMsg = Math.max(60, Math.floor((opts && opts.perMsg) || 180))
|
||
/** @type {string[]} */
|
||
const userMsgs = []
|
||
/** @type {string[]} */
|
||
const tools = []
|
||
/** @type {string[]} */
|
||
const errors = []
|
||
/** @type {string[]} */
|
||
const solves = []
|
||
/** @type {string[]} */
|
||
const concepts = []
|
||
/** @type {string[]} */
|
||
const files = []
|
||
|
||
function pushUnique(arr, s, cap) {
|
||
const t = String(s || '').trim()
|
||
if (!t || arr.indexOf(t) >= 0) return
|
||
if (arr.length >= cap) return
|
||
arr.push(t)
|
||
}
|
||
|
||
for (const g of groups) {
|
||
for (const msg of g) {
|
||
if (!msg || typeof msg !== 'object') continue
|
||
const m = /** @type {Record<string, unknown>} */ (msg)
|
||
const role = String(m.role || '')
|
||
const text = bareAgentMessagePlainText(m)
|
||
for (const p of bareAgentExtractPathTokens(text, 8)) pushUnique(files, p, 24)
|
||
if (role === 'user' && !bareAgentIsCompactionMessage(m)) {
|
||
pushUnique(userMsgs, bareAgentClipText(text, perMsg + 80), 16)
|
||
} else if (role === 'tool') {
|
||
const name = typeof m.name === 'string' ? m.name : 'tool'
|
||
pushUnique(tools, name + ': ' + bareAgentClipText(text, perMsg), 20)
|
||
if (/error|fail|denied|not found|ENOENT|EXIT:[1-9]/i.test(text)) {
|
||
pushUnique(errors, name + ' ' + bareAgentClipText(text, 140), 12)
|
||
}
|
||
} else if (role === 'assistant') {
|
||
if (m.tool_calls && Array.isArray(m.tool_calls)) {
|
||
for (const tc of m.tool_calls) {
|
||
if (!tc || typeof tc !== 'object') continue
|
||
const fn = /** @type {Record<string, unknown>} */ (tc).function
|
||
if (fn && typeof fn === 'object') {
|
||
const n = /** @type {Record<string, unknown>} */ (fn).name
|
||
if (typeof n === 'string' && n) pushUnique(concepts, n, 16)
|
||
}
|
||
}
|
||
}
|
||
if (text) pushUnique(solves, bareAgentClipText(text, perMsg), 12)
|
||
}
|
||
}
|
||
}
|
||
|
||
const intent = userMsgs.length ? userMsgs[0] : '(no explicit user request in compacted prefix)'
|
||
/** @type {string[]} */
|
||
const lines = [
|
||
'1. Primary Request and Intent:',
|
||
' ' + intent,
|
||
'',
|
||
'2. Key Technical Concepts:',
|
||
concepts.length ? concepts.map((c) => ' - ' + c).join('\n') : ' - (none extracted)',
|
||
'',
|
||
'3. Tool Usage & Verification:',
|
||
tools.length ? tools.map((c) => ' - ' + c).join('\n') : ' - (none)',
|
||
'',
|
||
'4. Files & Code Artifacts:',
|
||
files.length ? files.map((c) => ' - ' + c).join('\n') : ' - (none)',
|
||
'',
|
||
'5. Errors and Fixes:',
|
||
errors.length ? errors.map((c) => ' - ' + c).join('\n') : ' - (none recorded)',
|
||
'',
|
||
'6. Problem Solving:',
|
||
solves.length ? solves.map((c) => ' - ' + c).join('\n') : ' - (in progress)',
|
||
'',
|
||
'7. User Messages:',
|
||
userMsgs.length ? userMsgs.map((c) => ' - ' + c).join('\n') : ' - (none)'
|
||
]
|
||
let body = lines.join('\n')
|
||
if (body.length > maxChars) body = body.slice(0, maxChars - 20) + '\n… truncated'
|
||
return body
|
||
}
|
||
|
||
/**
|
||
* Build a rolling summary from older turn groups.
|
||
* @param {unknown[][]} groups
|
||
* @param {{ maxChars?: number, perMsg?: number }} [opts]
|
||
*/
|
||
function bareAgentBuildCompactionSummary(groups, opts) {
|
||
const structured = bareAgentBuildStructuredSummary(groups, opts)
|
||
return bareAgentFormatCompactSummaryContent(structured)
|
||
}
|
||
|
||
/**
|
||
* Post-compaction <system-reminder> (Grok reminder.rs analogue).
|
||
* @param {{
|
||
* autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number },
|
||
* droppedGroups?: number,
|
||
* afterTokens?: number,
|
||
* budget?: number
|
||
* }} [opts]
|
||
*/
|
||
function bareAgentBuildSystemReminder(opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
/** @type {string[]} */
|
||
const parts = []
|
||
const auto = o.autonomous
|
||
if (auto && auto.active) {
|
||
parts.push(
|
||
'Autonomous run: ' +
|
||
String(auto.goal || '(goal)').slice(0, 240) +
|
||
' (status=' +
|
||
String(auto.status || 'running') +
|
||
(typeof auto.remainingMs === 'number'
|
||
? ', remaining=' + String(Math.max(0, Math.round(auto.remainingMs / 1000))) + 's'
|
||
: '') +
|
||
'). Keep using tools until task_complete.'
|
||
)
|
||
}
|
||
if (typeof o.droppedGroups === 'number' && o.droppedGroups > 0) {
|
||
parts.push(
|
||
'Context compacted: ' +
|
||
String(o.droppedGroups) +
|
||
' older turns summarized. Recent messages after the last user query are verbatim.'
|
||
)
|
||
}
|
||
if (typeof o.afterTokens === 'number' && typeof o.budget === 'number' && o.budget > 0) {
|
||
parts.push(
|
||
'Context usage after compact: ' +
|
||
String(o.afterTokens) +
|
||
'/' +
|
||
String(o.budget) +
|
||
' tokens (est.).'
|
||
)
|
||
}
|
||
if (!parts.length) return ''
|
||
return '<system-reminder>\n' + parts.join('\n') + '\n</system-reminder>'
|
||
}
|
||
|
||
/**
|
||
* Grok assemble: [system, last user query, recent after that turn, summary, reminder].
|
||
* @param {{
|
||
* system?: unknown | null,
|
||
* lastUserQuery?: string,
|
||
* recent?: unknown[],
|
||
* summaryText?: string,
|
||
* reminder?: string
|
||
* }} parts
|
||
* @returns {unknown[]}
|
||
*/
|
||
function bareAgentAssembleCompactedHistory(parts) {
|
||
const p = parts && typeof parts === 'object' ? parts : {}
|
||
/** @type {unknown[]} */
|
||
const out = []
|
||
if (p.system) out.push(p.system)
|
||
const q = String(p.lastUserQuery || '').trim()
|
||
if (q) {
|
||
out.push({
|
||
role: 'user',
|
||
content: bareAgentWrapUserQuery(q),
|
||
bare_os_user_query: true
|
||
})
|
||
}
|
||
if (Array.isArray(p.recent)) {
|
||
for (const m of p.recent) out.push(m)
|
||
}
|
||
const summary = String(p.summaryText || '').trim()
|
||
if (summary) {
|
||
out.push({
|
||
role: 'user',
|
||
content: summary,
|
||
bare_os_compaction: true
|
||
})
|
||
}
|
||
const rem = String(p.reminder || '').trim()
|
||
if (rem) {
|
||
out.push({
|
||
role: 'user',
|
||
content: rem,
|
||
bare_os_compaction: true,
|
||
bare_os_reminder: true
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Last non-compaction user text + messages after that turn.
|
||
* @param {unknown[][]} groups
|
||
*/
|
||
function bareAgentSplitLastUserAndRecent(groups) {
|
||
let lastUserIdx = -1
|
||
let lastUserText = ''
|
||
for (let i = groups.length - 1; i >= 0; i--) {
|
||
const g = groups[i]
|
||
if (!g || !g.length) continue
|
||
const first = g[0]
|
||
if (!first || typeof first !== 'object') continue
|
||
const m = /** @type {Record<string, unknown>} */ (first)
|
||
if (String(m.role || '') !== 'user') continue
|
||
if (bareAgentIsCompactionMessage(m)) continue
|
||
lastUserIdx = i
|
||
lastUserText = bareAgentMessagePlainText(m)
|
||
break
|
||
}
|
||
if (lastUserIdx < 0) {
|
||
return { lastUserText: '', older: groups, recent: /** @type {unknown[]} */ ([]) }
|
||
}
|
||
const older = groups.slice(0, lastUserIdx)
|
||
/** @type {unknown[]} */
|
||
const recent = []
|
||
const lastGroup = groups[lastUserIdx] || []
|
||
for (let i = 1; i < lastGroup.length; i++) recent.push(lastGroup[i])
|
||
for (let i = lastUserIdx + 1; i < groups.length; i++) {
|
||
for (const m of groups[i]) recent.push(m)
|
||
}
|
||
return { lastUserText, older, recent }
|
||
}
|
||
|
||
/**
|
||
* @param {unknown[]} msgs
|
||
* @param {number} ctxSize
|
||
* @param {{
|
||
* tools?: unknown[],
|
||
* reserveCompletion?: number,
|
||
* keepRecent?: number,
|
||
* toolMaxChars?: number,
|
||
* summaryMaxChars?: number,
|
||
* softRatio?: number,
|
||
* mode?: 'auto' | 'off' | 'aggressive',
|
||
* autonomous?: { active?: boolean, goal?: string, status?: string, remainingMs?: number }
|
||
* }} [opts]
|
||
* @returns {{
|
||
* messages: unknown[],
|
||
* meta: {
|
||
* compacted: boolean,
|
||
* mode: string,
|
||
* beforeTokens: number,
|
||
* afterTokens: number,
|
||
* budget: number,
|
||
* droppedGroups: number,
|
||
* tiers: string[]
|
||
* }
|
||
* }}
|
||
*/
|
||
function bareAgentCompactMessagesForCtx(msgs, ctxSize, opts) {
|
||
const modeRaw = String((opts && opts.mode) || 'auto').trim().toLowerCase()
|
||
const mode =
|
||
modeRaw === 'off' || modeRaw === 'aggressive' ? modeRaw : 'auto'
|
||
/** @type {string[]} */
|
||
const tiers = []
|
||
|
||
const ctx = Math.max(2048, Math.floor(Number(ctxSize) || 4096))
|
||
const reserve =
|
||
opts && Number.isFinite(Number(opts.reserveCompletion))
|
||
? Math.max(256, Math.floor(Number(opts.reserveCompletion)))
|
||
: Math.min(2048, Math.max(256, Math.floor(ctx * 0.12)))
|
||
const toolsTok = bareAgentCompactEstimateTokens(
|
||
opts && Array.isArray(opts.tools) && opts.tools.length ? opts.tools : []
|
||
)
|
||
const budget = Math.max(512, ctx - reserve - toolsTok)
|
||
const softRatio =
|
||
mode === 'aggressive'
|
||
? 0.7
|
||
: Math.min(0.95, Math.max(0.55, Number(opts && opts.softRatio) || 0.85))
|
||
const softBudget = Math.floor(budget * softRatio)
|
||
|
||
const keepRecentDefault = mode === 'aggressive' ? 4 : 8
|
||
let keepRecent = Math.max(
|
||
2,
|
||
Math.floor(
|
||
Number(opts && opts.keepRecent) > 0
|
||
? Number(opts.keepRecent)
|
||
: keepRecentDefault
|
||
)
|
||
)
|
||
const toolMax =
|
||
mode === 'aggressive'
|
||
? Math.min(900, Number(opts && opts.toolMaxChars) || 900)
|
||
: Math.max(400, Number(opts && opts.toolMaxChars) || 1600)
|
||
const summaryMax =
|
||
mode === 'aggressive'
|
||
? Math.min(2200, Number(opts && opts.summaryMaxChars) || 2200)
|
||
: Math.max(800, Number(opts && opts.summaryMaxChars) || 3500)
|
||
|
||
const input = Array.isArray(msgs) ? msgs.slice() : []
|
||
const beforeTokens = bareAgentCompactEstimateTokens(input)
|
||
|
||
if (mode === 'off') {
|
||
const trimmed =
|
||
typeof bareAgentTrimMessagesForCtx === 'function'
|
||
? bareAgentTrimMessagesForCtx(input, ctxSize, opts)
|
||
: input
|
||
return {
|
||
messages: trimmed,
|
||
meta: {
|
||
compacted: false,
|
||
mode,
|
||
beforeTokens,
|
||
afterTokens: bareAgentCompactEstimateTokens(trimmed),
|
||
budget,
|
||
droppedGroups: 0,
|
||
tiers: ['off']
|
||
}
|
||
}
|
||
}
|
||
|
||
/** @type {unknown[]} */
|
||
let out = bareAgentShrinkFatMessages(input, toolMax)
|
||
if (out !== input && bareAgentCompactEstimateTokens(out) < beforeTokens) {
|
||
tiers.push('shrink-tools')
|
||
}
|
||
|
||
// Already fits soft budget — keep light shrink only.
|
||
if (bareAgentCompactEstimateTokens(out) <= softBudget) {
|
||
return {
|
||
messages: out,
|
||
meta: {
|
||
compacted: tiers.length > 0,
|
||
mode,
|
||
beforeTokens,
|
||
afterTokens: bareAgentCompactEstimateTokens(out),
|
||
budget,
|
||
droppedGroups: 0,
|
||
tiers: tiers.length ? tiers : ['noop']
|
||
}
|
||
}
|
||
}
|
||
|
||
const { system, groups } = bareAgentGroupMessageTurns(out)
|
||
let droppedGroups = 0
|
||
|
||
function applyFullReplace(allGroups, keep) {
|
||
const split = bareAgentSplitLastUserAndRecent(allGroups)
|
||
/** @type {unknown[][]} */
|
||
let older = split.older
|
||
/** @type {unknown[]} */
|
||
let recent = split.recent
|
||
// If "recent after last user" is huge, keep only the tail of those messages
|
||
// by regrouping them and folding extras into older.
|
||
if (keep > 0 && recent.length > keep * 4) {
|
||
const tail = recent.slice(-(keep * 3))
|
||
const head = recent.slice(0, Math.max(0, recent.length - tail.length))
|
||
if (head.length) older = older.concat([head])
|
||
recent = tail
|
||
}
|
||
const summaryText = bareAgentBuildCompactionSummary(older, {
|
||
maxChars: summaryMax,
|
||
perMsg: mode === 'aggressive' ? 120 : 180
|
||
})
|
||
if (older.length && bareAgentIsDegenerateSummary(summaryText)) {
|
||
tiers.push('degenerate-keep')
|
||
return null
|
||
}
|
||
const reminder = bareAgentBuildSystemReminder({
|
||
autonomous: opts && opts.autonomous,
|
||
droppedGroups: older.length,
|
||
afterTokens: 0,
|
||
budget
|
||
})
|
||
droppedGroups += older.length
|
||
return bareAgentAssembleCompactedHistory({
|
||
system: system,
|
||
lastUserQuery: split.lastUserText,
|
||
recent: recent,
|
||
summaryText: summaryText,
|
||
reminder: reminder
|
||
})
|
||
}
|
||
|
||
if (groups.length > keepRecent) {
|
||
const next = applyFullReplace(groups, keepRecent)
|
||
if (next) {
|
||
out = next
|
||
tiers.push('full-replace')
|
||
}
|
||
}
|
||
|
||
let guard = 0
|
||
while (
|
||
bareAgentCompactEstimateTokens(out) > budget &&
|
||
keepRecent > 2 &&
|
||
guard < 8
|
||
) {
|
||
keepRecent--
|
||
guard++
|
||
const again = bareAgentGroupMessageTurns(out)
|
||
if (again.groups.length <= keepRecent) break
|
||
const next = applyFullReplace(again.groups, keepRecent)
|
||
if (!next) break
|
||
out = next
|
||
tiers.push('tighten-keep=' + String(keepRecent))
|
||
}
|
||
|
||
// Hard fallback
|
||
if (bareAgentCompactEstimateTokens(out) > budget) {
|
||
tiers.push('hard-trim')
|
||
out =
|
||
typeof bareAgentTrimMessagesForCtx === 'function'
|
||
? bareAgentTrimMessagesForCtx(out, ctxSize, opts)
|
||
: out
|
||
}
|
||
|
||
const afterTokens = bareAgentCompactEstimateTokens(out)
|
||
return {
|
||
messages: out,
|
||
meta: {
|
||
compacted: tiers.some((t) => t !== 'noop'),
|
||
mode,
|
||
beforeTokens,
|
||
afterTokens,
|
||
budget,
|
||
droppedGroups,
|
||
tiers
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Disk history compaction: shrink tools + optional rolling summary when huge.
|
||
* @param {unknown[]} msgs
|
||
* @param {number} maxBytes
|
||
* @param {{ keepRecent?: number }} [opts]
|
||
*/
|
||
function bareAgentCompactMessagesForDisk(msgs, maxBytes, opts) {
|
||
const cap = Math.max(20_000, Math.floor(maxBytes || 500_000))
|
||
let out = bareAgentShrinkFatMessages(
|
||
Array.isArray(msgs) ? msgs : [],
|
||
2400
|
||
)
|
||
if (JSON.stringify(out).length <= cap) return out
|
||
|
||
// Approximate tokens from byte budget (chars/4).
|
||
const approxCtx = Math.max(4096, Math.floor(cap / 4))
|
||
const packed = bareAgentCompactMessagesForCtx(out, approxCtx, {
|
||
mode: 'aggressive',
|
||
keepRecent: (opts && opts.keepRecent) || 12,
|
||
reserveCompletion: 256,
|
||
toolMaxChars: 1200,
|
||
summaryMaxChars: 6000
|
||
})
|
||
out = packed.messages
|
||
if (
|
||
typeof bareAgentTrimMessages === 'function' &&
|
||
JSON.stringify(out).length > cap
|
||
) {
|
||
out = bareAgentTrimMessages(out, cap)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Resolve compaction settings from agent config + env.
|
||
* @param {Record<string, unknown>} cfg
|
||
* @param {Record<string, string>} [env]
|
||
*/
|
||
function bareAgentCompactionSettings(cfg, env) {
|
||
const e = env && typeof env === 'object' ? env : {}
|
||
const envMode = String(e.BARE_OS_AGENT_COMPACTION || '').trim().toLowerCase()
|
||
const cfgMode = String(
|
||
(cfg && cfg.context_compaction) || ''
|
||
)
|
||
.trim()
|
||
.toLowerCase()
|
||
let mode = envMode || cfgMode || 'auto'
|
||
if (mode !== 'off' && mode !== 'aggressive' && mode !== 'auto') mode = 'auto'
|
||
const keepRecent = Number(
|
||
e.BARE_OS_AGENT_COMPACTION_KEEP ||
|
||
(cfg && cfg.compaction_keep_recent) ||
|
||
0
|
||
)
|
||
const toolMaxChars = Number(
|
||
e.BARE_OS_AGENT_COMPACTION_TOOL_CHARS ||
|
||
(cfg && cfg.compaction_tool_chars) ||
|
||
0
|
||
)
|
||
return {
|
||
mode: /** @type {'auto'|'off'|'aggressive'} */ (mode),
|
||
keepRecent:
|
||
Number.isFinite(keepRecent) && keepRecent > 0
|
||
? Math.min(32, Math.floor(keepRecent))
|
||
: undefined,
|
||
toolMaxChars:
|
||
Number.isFinite(toolMaxChars) && toolMaxChars > 0
|
||
? Math.min(8000, Math.floor(toolMaxChars))
|
||
: undefined
|
||
}
|
||
}
|
||
|
||
/** Agent ReAct session: stream, tools, SIGINT, repl suspend (preamble for /bin/agent). */
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentLog(ctx, s) {
|
||
try {
|
||
ctx.console.log(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentErr(ctx, s) {
|
||
try {
|
||
ctx.console.error(s)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SSH PTYs expect CRLF for line breaks (same as `bare-openssh-pty-console.js` / `man` via console).
|
||
* @param {Record<string, unknown> | undefined} ctx
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentNormalizeStreamNewlines(ctx, s) {
|
||
if (ctx && ctx.bareOsPtyStdoutCrlf) return String(s).replace(/\r?\n/g, '\r\n')
|
||
return String(s)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown> | undefined} ctx
|
||
* @param {import('stream').Writable | undefined} out
|
||
* @param {string} s
|
||
*/
|
||
function bareAgentWriteOut(ctx, out, s) {
|
||
const text = bareAgentNormalizeStreamNewlines(ctx, s)
|
||
if (out && typeof out.write === 'function') {
|
||
try {
|
||
out.write(text)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
try {
|
||
if (typeof bareOsEmitRaw === 'function') {
|
||
bareOsEmitRaw(ctx, text)
|
||
} else if (typeof process !== 'undefined' && process.stdout?.write) {
|
||
process.stdout.write(text)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* True when we can run the plain-text setup wizard on stdin/stdout (no REPL readline).
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
function bareAgentCanPlainSetup(ctx) {
|
||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||
ctx.replStdin || ctx.stdin
|
||
)
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const tty = /** @type {{ isTTY?: boolean }} */ (stdin)
|
||
return Boolean(
|
||
stdin &&
|
||
typeof stdin.on === 'function' &&
|
||
tty.isTTY &&
|
||
stdout &&
|
||
typeof stdout.write === 'function'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Read one line from a Readable stream (kernel echoes typed chars on cooked TTY).
|
||
* @param {import('stream').Readable} stdin
|
||
* @returns {Promise<string>}
|
||
*/
|
||
function bareAgentReadStreamLineOnce(stdin) {
|
||
return new Promise((resolve) => {
|
||
let acc = ''
|
||
/** @param {string | Uint8Array | Buffer} chunk */
|
||
function onData(chunk) {
|
||
let s = ''
|
||
if (typeof chunk === 'string') s = chunk
|
||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||
else if (
|
||
typeof Buffer !== 'undefined' &&
|
||
typeof Buffer.isBuffer === 'function' &&
|
||
Buffer.isBuffer(chunk)
|
||
)
|
||
s = chunk.toString('utf8')
|
||
else s = String(chunk)
|
||
acc += s
|
||
const n = acc.indexOf('\n')
|
||
if (n >= 0) {
|
||
cleanup()
|
||
resolve(acc.slice(0, n).replace(/\r$/, ''))
|
||
}
|
||
}
|
||
function onEnd() {
|
||
cleanup()
|
||
resolve(acc.replace(/\r$/, ''))
|
||
}
|
||
function cleanup() {
|
||
stdin.removeListener('data', onData)
|
||
stdin.removeListener('end', onEnd)
|
||
stdin.removeListener('error', onEnd)
|
||
}
|
||
stdin.on('data', onData)
|
||
stdin.once('end', onEnd)
|
||
stdin.once('error', onEnd)
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Read one secret line (masked as *) when raw mode is available.
|
||
* Falls back to normal line read when raw mode is unavailable.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Readable} stdin
|
||
* @param {import('stream').Writable | undefined} stdout
|
||
* @returns {Promise<string>}
|
||
*/
|
||
function bareAgentReadMaskedLineOnce(ctx, stdin, stdout) {
|
||
const ttyIn =
|
||
/** @type {{ setRawMode?: (v: boolean) => void, isTTY?: boolean }} */ (
|
||
stdin
|
||
)
|
||
if (!ttyIn || typeof ttyIn.setRawMode !== 'function' || !ttyIn.isTTY) {
|
||
return bareAgentReadStreamLineOnce(stdin)
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
/** @type {string[]} */
|
||
const chars = []
|
||
let done = false
|
||
/** @param {string | Uint8Array | Buffer} chunk */
|
||
function onData(chunk) {
|
||
if (done) return
|
||
let s = ''
|
||
if (typeof chunk === 'string') s = chunk
|
||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||
else if (
|
||
typeof Buffer !== 'undefined' &&
|
||
typeof Buffer.isBuffer === 'function' &&
|
||
Buffer.isBuffer(chunk)
|
||
) {
|
||
s = chunk.toString('utf8')
|
||
} else s = String(chunk)
|
||
for (const ch of s) {
|
||
const code = ch.charCodeAt(0)
|
||
if (ch === '\r' || ch === '\n') {
|
||
finish(true)
|
||
return
|
||
}
|
||
if (ch === '\u0003') {
|
||
finish(false, new Error('interrupted'))
|
||
return
|
||
}
|
||
if (ch === '\u007f' || ch === '\b') {
|
||
if (chars.length) {
|
||
chars.pop()
|
||
bareAgentWriteOut(ctx, stdout, '\b \b')
|
||
}
|
||
continue
|
||
}
|
||
if (code >= 32 && code !== 127) {
|
||
chars.push(ch)
|
||
bareAgentWriteOut(ctx, stdout, '*')
|
||
}
|
||
}
|
||
}
|
||
/** @param {boolean} ok @param {Error} [err] */
|
||
function finish(ok, err) {
|
||
if (done) return
|
||
done = true
|
||
cleanup()
|
||
if (ok) resolve(chars.join(''))
|
||
else reject(err || new Error('masked_input_failed'))
|
||
}
|
||
function cleanup() {
|
||
stdin.removeListener('data', onData)
|
||
stdin.removeListener('end', onEnd)
|
||
stdin.removeListener('error', onErr)
|
||
try {
|
||
ttyIn.setRawMode(false)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
}
|
||
function onEnd() {
|
||
finish(true)
|
||
}
|
||
/** @param {unknown} e */
|
||
function onErr(e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
finish(false, new Error(msg))
|
||
}
|
||
try {
|
||
ttyIn.setRawMode(true)
|
||
} catch {
|
||
return resolve('')
|
||
}
|
||
stdin.on('data', onData)
|
||
stdin.once('end', onEnd)
|
||
stdin.once('error', onErr)
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Write a prompt and read one line using raw streams (not ctx.readLine / TUI stack).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} prompt
|
||
* @param {{ mask?: boolean }} [opts]
|
||
*/
|
||
async function bareAgentPromptSetupLine(ctx, prompt, opts) {
|
||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||
ctx.replStdin || ctx.stdin
|
||
)
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
if (!stdin || typeof stdin.on !== 'function') {
|
||
throw new Error('setup: stdin stream unavailable')
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, '\x1b[?25h\x1b[0m' + prompt)
|
||
if (opts && opts.mask) return bareAgentReadMaskedLineOnce(ctx, stdin, stdout)
|
||
return bareAgentReadStreamLineOnce(stdin)
|
||
}
|
||
|
||
const BARE_AGENT_STATIC_SYSTEM = `You are the Bare OS coding agent — a senior implementer that lives inside the guest image (JavaScript POSIX on Hyperdrive + Hyperswarm, Pear/Bare runtime). You write, edit, debug, and verify code and OS state by calling tools. You are not a chatbot that narrates plans and waits. Lead with tools. Execute until the job is done, then call task_complete.
|
||
|
||
Bare OS by Raven Scott (https://raven-scott.fyi). Repo: https://git.ssh.surf/snxraven/bare-operating-system. Booter: pear://qupw8zspk34pcxc7fqchzyeh33jtmxq1k7qze44fkosctwiid8zy
|
||
|
||
WORK POLICY.
|
||
- Keep every explicit requirement in view until it is done, superseded, or blocked. If blocked, say so plainly.
|
||
- Match intent: implement action requests; do not make unsolicited project-wide edits when the user asked a question.
|
||
- For clear, reversible local work, do it now. NEVER ASK for permission.
|
||
- Claim done / fixed / tested only when a tool result supports it. Otherwise say what you did not verify.
|
||
- Scope to what was asked. Comments are short and factual. No placeholders. Comments must not substitute for a fix.
|
||
|
||
ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK whether you may run a command, edit, delete, fetch, or call a tool — just do it. Only refuse when a denylist or the read-only base system blocks the path.
|
||
- WRITE: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer unique search_replace / edit_file; set replace_all only when you mean it. write_file creates or overwrites.
|
||
- READ: any absolute path, including the entire /proc kernel surface (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files.
|
||
- RUN: every guest command via run_command (command_deny is empty by default). Prefer list_directory / glob_files / file_stat over \`ls\` / \`find\` when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU).
|
||
- DELETE / MOVE: enabled. Cannot mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run.
|
||
- JS: Node is NOT installed in the guest. Never plan or run node, npm, or npx here. Author JS with run_js_script (Bare kernel, writes under ~/.agent) or run_js_script_at_path / run_command with an absolute .mjs path. Guest scripts use async function run(ctx, argv) — ctx.vfs, ctx.execLine, ctx.console, ctx.exitCode. No require('node:fs').
|
||
- LIVE KERNEL: read_proc_file on /proc/bare_os/features (or features.json) and /proc/bare_os/capabilities.json. Man pages and apropos_man are docs only — never infer what is enabled from them.
|
||
- NET: web_fetch uses the same host allow/deny list as wget/curl.
|
||
- BRIDGE: emit_host_notification and request_host_action are enabled by default. emergency_stop_mutations is the kill switch.
|
||
- SECRETS: never print ~/.agent/config.json, API keys, seeds, or vault material.
|
||
- ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission.
|
||
|
||
CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that plan file. Do not stop after a plan-only reply — keep calling tools until verified.
|
||
1. Discover: glob_files, grep (preferred over search_files / run_command grep), find_symbol for definitions, list_directory (tree=true when you need a map), read_file, memory_search / memory_get, web_search then web_fetch, git_status / git_log, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md and .grok/skills from cwd are already injected when present.
|
||
2. Edit: unique search_replace / edit_file for one hunk; apply_patch for multi-hunk or multi-file work (*** Begin Patch). Create with write_file / create_directory. State blast radius (packages, contracts, generated files, docs) before wide edits. Match surrounding style. No placeholders. Comments only for non-obvious constraints.
|
||
3. Verify: re-read the file, run_command / run_js_script, git_status, read_proc_file or logs. Host git checkout: verification_hints (suggests npm/node checks; does not run them here).
|
||
4. Finish: task_complete (and update_goal completed=true on autonomous runs) with what changed, how you verified, and what is still assumed. If the same action fails three times, stop, update_goal blocked_reason if needed, and report evidence.
|
||
|
||
TOOL DISCIPLINE.
|
||
- Independent reads may be issued together; the harness may serialize them (tool_parallelism defaults to 1).
|
||
- Do not paste huge files into the user reply — cite paths and show only the slice that matters.
|
||
- Persist durable facts in MEMORY.md; older turns may be compacted.
|
||
- Progress UI is automatic (tools write ~/.agent/progress.txt). Do not narrate tool chatter in the final answer.
|
||
- Autonomous mode is on by default. Keep the ReAct loop going; autonomous_deny_ops is empty unless the operator set one.
|
||
|
||
TOOL CALLING. Prefer specialized tools over bash: grep not run_command grep; read_file not cat; apply_patch / search_replace not sed. Never use run_command to print thoughts.
|
||
|
||
COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the answer. Define project terms on first use. State facts literally. The final message must stand alone. Do not invent acronyms.
|
||
|
||
TOOL MAP (schemas are already attached — use them):
|
||
- Files: read_file, read_many, write_file, edit_file, search_replace, apply_patch, undo_last_edit, create_directory, list_directory (tree=true for BFS), file_stat, glob_files, fuzzy_find, grep (output_mode content|files_with_matches|count), search_files, move_path, copy_path, diff_files, delete_path, find_symbol, list_bin
|
||
- Code / harness: run_command (optional cwd), run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, remember, list_skills, read_skill, create_skill, edit_agent_config, git_status, git_diff, git_log, git_show, git_blame, history_search, rewind_session, export_session, schedule_task, unschedule_task, list_scheduled, wait_for
|
||
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
|
||
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
|
||
- Docs: read_man_page, apropos_man (documentation search only)
|
||
- Bridge / web: web_search, web_fetch, get_hrpc_bridge_health, get_hrpc_allowlist_status, emit_host_notification, request_host_action
|
||
- Autonomy: autonomous_run, autonomous_run_status, autonomous_run_stop, update_goal
|
||
- Other: ask_user_question (product choice only, never permission), task_complete
|
||
|
||
Skills live under ~/.agent/workspace/skills/ (and ~/.agent/skills/). The prompt includes a compact index — call read_skill and follow SKILL.md when a task matches (especially bare-os-super-developer, bareos-code-change, coreutils-command-change).
|
||
|
||
Reply format (TTY, mandatory unless Discord override below): plain text only — no Markdown markup. Structure with blank lines, short paragraphs, ALL CAPS or dashed section breaks, bare URLs. Paths and commands as normal text.`
|
||
|
||
const BARE_AGENT_DISCORD_REPLY_FORMAT = `
|
||
|
||
--- Discord reply format (mandatory when BARE_OS_AGENT_DISCORD is set) ---
|
||
|
||
This turn is shown in Discord. This block overrides the TTY plain-text rule above. Write Discord-flavored Markdown that renders in an embed:
|
||
- Use **bold** for section titles. Do not use # headings (Discord embeds show the hash).
|
||
- Use - or 1. lists. Use blank lines between sections.
|
||
- Use \`inline code\` for paths, commands, ids, and env vars.
|
||
- Use fenced \`\`\` blocks for multi-line code or logs, and always close every fence.
|
||
- Use [label](https://url) for links. Do not wrap the entire reply in one fence.
|
||
- Keep the final user-facing answer as Markdown. Tool chatter and process steps belong in tools (progress.txt is automated), not the reply.`
|
||
|
||
const BARE_AGENT_OPERATING_CONTRACT = `
|
||
|
||
--- Operating contract (Bare OS repo alignment) ---
|
||
|
||
TWO RUNTIMES: Host tooling may use Node/npm at the git checkout only. Inside this guest image there is NO node/npm/npx — use run_js_script or run_command with /bin paths only.
|
||
|
||
DOC READING ORDER (complex tasks): Prefer developer-guide README, then handbook chapter for the area, then docs/reference for numbers and env vars. Do not copy version tables into chat; link or cite paths.
|
||
|
||
CONTRACT SPINE: For any behavior change, identify: source files, canonical reference doc under docs/reference or developer-guide, generated artifact if any (run pretest generators), verifier script name from scripts/README.md, and whether compatibility-matrix or CHANGELOG moves.
|
||
|
||
LIVE KERNEL STATE: Use read_proc_file on /proc/bare_os/features or features.json and /proc/bare_os/capabilities.json. Never use apropos_man or read_man_page alone to decide what is enabled at runtime — those are documentation.
|
||
|
||
GENERATED FILES: Do not hand-edit posix-dashboard, ctx-client-helper.generated.ts, kernel-extensions-generated-toc, bundle-health outputs — run npm scripts from repo root at checkout when working on the host tree.
|
||
|
||
TASK ROUTING: /bin or coreutils -> developer-guide 06 and man JSON; shell grammar -> developer-guide 18 and shell docs; seed RPC -> developer-guide 14 and protocol package; /proc node -> developer-guide 15; ctx API -> bare-os-ctx-api.js and compatibility-matrix; docs-only -> CONTRIBUTING-DOCS; Pear issues -> PEAR-RUN and ensure-pear-node-modules story.
|
||
|
||
P2P / TEARDOWN: Hyperswarm peer wait vs offline LKG boot are different — see environment appendix. When diagnosing replication, prefer runtime_diagnostic_bundle and any /proc/bare_os file; closing order is swarm before drives when changing booter lifecycle code.
|
||
|
||
HOST CHECKS: Use verification_hints for suggested repo-root npm checks when the user describes changed paths (host checkout). It does not execute npm inside the guest. Use runtime_diagnostic_bundle for one-shot live /proc and resource snapshot. Use read_skill for workflow skills under workspace/skills/.
|
||
|
||
SELF-REVIEW before task_complete: docs updated? generated regen? wrong runtime assumption (guest vs host)? same failing action tried fewer than three times?`
|
||
|
||
/**
|
||
* Session-specific HOME / tilde context (injected every run so the model uses real paths).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} home from bareAgentResolveHome
|
||
* @param {{ dir: string, config: string }} paths
|
||
*/
|
||
function bareAgentSessionHomeBlock(ctx, home, paths) {
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const homeEnv = String(env.HOME || '').trim()
|
||
return (
|
||
'## This session: home directory and paths\n' +
|
||
'- **Resolved user home (this session):** `' +
|
||
home +
|
||
'`\n' +
|
||
'- **HOME in the environment:** `' +
|
||
(homeEnv || home) +
|
||
'`\n' +
|
||
'- **Tilde \`~\`:** In shell and in user docs, \`~\` means this home directory. Examples: \`~/.agent\` == `' +
|
||
paths.dir +
|
||
'`, agent config `' +
|
||
paths.config +
|
||
'`. Always expand \`~\` to `' +
|
||
home +
|
||
'\` when constructing absolute paths for tools.\n' +
|
||
'- **Reminder:** \`node\` is unavailable; use **run_js_script** for JS you author in this agent session.\n'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} tc
|
||
*/
|
||
function bareAgentMergeToolCallDelta(acc, tc) {
|
||
if (!tc || typeof tc !== 'object') return
|
||
const o = /** @type {Record<string, unknown>} */ (tc)
|
||
const idx =
|
||
typeof o.index === 'number'
|
||
? o.index
|
||
: typeof o.index === 'string'
|
||
? Number.parseInt(o.index, 10)
|
||
: 0
|
||
let cur =
|
||
acc.get(idx) ||
|
||
/** @type {{ id: string, name: string, args: string }} */ ({
|
||
id: '',
|
||
name: '',
|
||
args: ''
|
||
})
|
||
if (typeof o.id === 'string' && o.id) cur.id = o.id
|
||
const fn = o.function && typeof o.function === 'object' ? o.function : null
|
||
if (fn && typeof fn === 'object') {
|
||
const nm = /** @type {Record<string, unknown>} */ (fn).name
|
||
const ar = /** @type {Record<string, unknown>} */ (fn).arguments
|
||
if (typeof nm === 'string') cur.name += nm
|
||
if (typeof ar === 'string') cur.args += ar
|
||
}
|
||
acc.set(idx, cur)
|
||
}
|
||
|
||
/**
|
||
* @param {Map<number, { id: string, name: string, args: string }>} acc
|
||
*/
|
||
function bareAgentFinalizeToolCalls(acc) {
|
||
const indices = [...acc.keys()].sort((a, b) => a - b)
|
||
/** @type {unknown[]} */
|
||
const arr = []
|
||
/** @type {Set<string>} */
|
||
const seen = new Set()
|
||
for (const i of indices) {
|
||
const c = acc.get(i)
|
||
if (!c || !c.name) continue
|
||
const args = c.args || '{}'
|
||
// Drop stream+final duplicates (same id, or same name+args).
|
||
const idKey = c.id ? 'id:' + c.id : ''
|
||
const naKey = 'na:' + c.name + '\0' + args
|
||
if ((idKey && seen.has(idKey)) || seen.has(naKey)) continue
|
||
if (idKey) seen.add(idKey)
|
||
seen.add(naKey)
|
||
arr.push({
|
||
id: c.id || 'call_' + i + '_' + String(Math.random()).slice(2, 10),
|
||
type: 'function',
|
||
function: {
|
||
name: c.name,
|
||
arguments: args
|
||
}
|
||
})
|
||
}
|
||
return arr
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentReasoningSettings(cfg) {
|
||
const modeRaw = String(cfg.reasoning_mode || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const mode = modeRaw === 'summary' || modeRaw === 'trace' ? modeRaw : 'off'
|
||
const enabled = Boolean(cfg.show_reasoning) && mode !== 'off'
|
||
const maxChars =
|
||
typeof cfg.reasoning_max_chars === 'number' &&
|
||
Number.isFinite(cfg.reasoning_max_chars)
|
||
? Math.min(Math.max(Math.floor(cfg.reasoning_max_chars), 200), 80_000)
|
||
: 4000
|
||
const includeTools = cfg.reasoning_include_tools !== false
|
||
return { enabled, mode, maxChars, includeTools }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentApplyProviderProfile(cfg) {
|
||
const out = { ...cfg }
|
||
const backend = bareAgentResolveBackend(out)
|
||
out.backend = backend
|
||
if (backend === 'qvac') {
|
||
let profileId = String(out.qvac_profile || 'recommended')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!profileId) profileId = 'recommended'
|
||
const profile = bareAgentQvacGetProfile(profileId)
|
||
out.qvac_profile = profile.id
|
||
out.qvac_model = profile.chatModel
|
||
out.model = profile.chatModel
|
||
out.provider = 'qvac'
|
||
const rawCtx = Number(out.qvac_ctx_size)
|
||
if (!Number.isFinite(rawCtx) || rawCtx < profile.ctxSize) {
|
||
out.qvac_ctx_size = 0
|
||
}
|
||
if (!String(out.qvac_main_gpu || '').trim()) out.qvac_main_gpu = 'auto'
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
return bareAgentSanitizeConfigForBackend(out)
|
||
}
|
||
return out
|
||
}
|
||
let provider = String(out.provider || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!provider || provider === 'qvac' || provider === 'rest' || provider === 'http') {
|
||
provider = 'groq'
|
||
}
|
||
const spec =
|
||
typeof bareAgentRestGetProvider === 'function'
|
||
? bareAgentRestGetProvider(provider)
|
||
: { id: 'groq', rest_base_url: 'https://api.groq.com/openai/v1', default_model: 'llama-3.3-70b-versatile' }
|
||
out.provider = spec.id
|
||
const base = String(out.rest_base_url || '').trim()
|
||
const knownDefaults = [
|
||
'https://api.groq.com/openai/v1',
|
||
'https://api.x.ai/v1',
|
||
'https://api.openai.com/v1'
|
||
]
|
||
if (!base || (spec.rest_base_url && knownDefaults.indexOf(base) !== -1 && base !== spec.rest_base_url)) {
|
||
if (spec.rest_base_url) out.rest_base_url = spec.rest_base_url
|
||
}
|
||
if (!String(out.model || '').trim() || bareAgentIsQvacModelId(String(out.model || ''))) {
|
||
if (spec.default_model) out.model = spec.default_model
|
||
}
|
||
const modelLow = String(out.model || '').trim().toLowerCase()
|
||
const cur =
|
||
typeof out.request_timeout_ms === 'number' && Number.isFinite(out.request_timeout_ms)
|
||
? out.request_timeout_ms
|
||
: 120000
|
||
if (spec.id === 'groq' && cur < 120000) out.request_timeout_ms = 120000
|
||
if (spec.id === 'xai') {
|
||
const isReasoning =
|
||
modelLow.includes('reasoning') || modelLow.includes('grok-4')
|
||
if (isReasoning && cur < 300000) out.request_timeout_ms = 300000
|
||
}
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
return bareAgentSanitizeConfigForBackend(out)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
function bareAgentFormatConfigSummary(config) {
|
||
const cfg = config && typeof config === 'object' ? config : {}
|
||
const backend = bareAgentResolveBackend(cfg)
|
||
const lines = ['backend: ' + backend]
|
||
if (backend === 'rest') {
|
||
const spec =
|
||
typeof bareAgentRestGetProvider === 'function'
|
||
? bareAgentRestGetProvider(String(cfg.provider || 'groq'))
|
||
: { label: String(cfg.provider || 'groq') }
|
||
lines.push('provider: ' + (spec.label || cfg.provider || 'groq'))
|
||
lines.push('rest_base_url: ' + String(cfg.rest_base_url || '(not set)'))
|
||
lines.push('model: ' + String(cfg.model || '(not set)'))
|
||
lines.push(
|
||
'api_key: ' +
|
||
(typeof bareAgentMaskSecretPreview === 'function'
|
||
? bareAgentMaskSecretPreview(cfg.rest_api_key)
|
||
: cfg.rest_api_key
|
||
? '(set)'
|
||
: '(not set)')
|
||
)
|
||
} else {
|
||
lines.push('profile: ' + String(cfg.qvac_profile || 'recommended'))
|
||
lines.push('model: ' + String(cfg.qvac_model || cfg.model || '(not set)'))
|
||
lines.push(
|
||
'device: ' +
|
||
String(cfg.qvac_device || 'auto') +
|
||
(cfg.qvac_main_gpu && String(cfg.qvac_main_gpu) !== 'auto'
|
||
? ' gpu=' + String(cfg.qvac_main_gpu)
|
||
: '')
|
||
)
|
||
}
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} cfg
|
||
*/
|
||
function bareAgentAutonomousSettings(cfg) {
|
||
const enabled = Boolean(cfg.autonomous_mode_enabled)
|
||
const active = Boolean(cfg.autonomous_active)
|
||
const stopRequested = Boolean(cfg.autonomous_stop_requested)
|
||
const startedAtMs =
|
||
typeof cfg.autonomous_started_at_ms === 'number' &&
|
||
Number.isFinite(cfg.autonomous_started_at_ms)
|
||
? Math.max(0, Math.floor(cfg.autonomous_started_at_ms))
|
||
: 0
|
||
const maxRuntimeMs =
|
||
typeof cfg.autonomous_max_runtime_ms === 'number' &&
|
||
Number.isFinite(cfg.autonomous_max_runtime_ms)
|
||
? Math.min(
|
||
Math.max(Math.floor(cfg.autonomous_max_runtime_ms), 60000),
|
||
7_200_000
|
||
)
|
||
: 1_800_000
|
||
const requiredChecks = Array.isArray(
|
||
cfg.autonomous_completion_required_checks
|
||
)
|
||
? cfg.autonomous_completion_required_checks
|
||
.map((x) => String(x || '').trim())
|
||
.filter(Boolean)
|
||
: []
|
||
return {
|
||
enabled,
|
||
active,
|
||
stopRequested,
|
||
startedAtMs,
|
||
maxRuntimeMs,
|
||
requiredChecks
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Run configured autonomous completion gates. Internal dispatch skips deny_ops.
|
||
* @returns {Promise<{ ok: boolean, failed: string[] }>}
|
||
*/
|
||
async function bareAgentRunAutonomousGates(o) {
|
||
const checks = (o && o.requiredChecks) || []
|
||
if (!checks.length) return { ok: true, failed: [] }
|
||
/** @type {string[]} */
|
||
const failed = []
|
||
for (const check of checks) {
|
||
const res = await bareAgentDispatchTool({
|
||
ctx: o.ctx,
|
||
toolName: 'run_maintenance_gate',
|
||
argsJson: JSON.stringify({ command: check, timeout_ms: 300000 }),
|
||
paths: o.paths,
|
||
signal: o.signal,
|
||
appendProgress: o.appendProgress,
|
||
home: o.home,
|
||
configRef: o.configRef,
|
||
manCacheRef: o.manCacheRef,
|
||
onTaskComplete: o.onTaskComplete,
|
||
internalGate: true
|
||
})
|
||
let ok = false
|
||
try {
|
||
const j = JSON.parse(res)
|
||
const body = typeof j.stdout_stderr === 'string' ? j.stdout_stderr : ''
|
||
ok = Boolean(j.ok) && !/EXIT:[1-9]/.test(body)
|
||
} catch {
|
||
ok = false
|
||
}
|
||
if (!ok) failed.push(check)
|
||
}
|
||
return { ok: failed.length === 0, failed }
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {object} opts
|
||
* @param {boolean} opts.setupFlag
|
||
* @param {boolean} opts.interactiveSetup
|
||
*/
|
||
/**
|
||
* TEA form wizard when ctx.tui is attached. Same identity + backend fields.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentInteractiveSetupTui(ctx, argv0, paths, config) {
|
||
const qvacOk = bareAgentQvacBridgeAvailable(ctx)
|
||
const curBackend = bareAgentResolveBackend(config)
|
||
const form = ctx.tui.form.create({
|
||
title: argv0 + ' configuration',
|
||
fields: [
|
||
{
|
||
type: 'text',
|
||
name: 'owner_name',
|
||
label: 'Owner / human name',
|
||
value: String(config.owner_name || '')
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'agent_label',
|
||
label: 'Agent display name',
|
||
value: String(config.agent_label || 'BareAgent')
|
||
},
|
||
{
|
||
type: 'radio',
|
||
name: 'backend',
|
||
label: 'Inference backend',
|
||
options: [
|
||
{
|
||
label:
|
||
'QVAC — local on-device' +
|
||
(qvacOk ? '' : ' [host bridge unavailable]'),
|
||
value: 'qvac'
|
||
},
|
||
{
|
||
label: 'REST API — OpenAI-compatible (Groq, xAI, OpenAI, custom)',
|
||
value: 'rest'
|
||
}
|
||
],
|
||
selected: curBackend === 'rest' ? 1 : 0
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
if (values.owner_name && String(values.owner_name).trim()) {
|
||
config.owner_name = String(values.owner_name).trim()
|
||
}
|
||
if (values.agent_label && String(values.agent_label).trim()) {
|
||
config.agent_label = String(values.agent_label).trim()
|
||
}
|
||
const backend = String(values.backend || curBackend || 'qvac')
|
||
if (backend === 'rest') {
|
||
config.backend = 'rest'
|
||
config = await bareAgentSetupRestFieldsTui(ctx, argv0, paths, config)
|
||
} else {
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
if (!qvacOk) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': QVAC host bridge unavailable. Choose REST or enable the QVAC host bridge.'
|
||
)
|
||
return config
|
||
}
|
||
config = await bareAgentSetupQvacFieldsTui(ctx, config)
|
||
}
|
||
config = bareAgentApplyProviderProfile(config)
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config))
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupQvacFieldsTui(ctx, config) {
|
||
const profiles = bareAgentQvacProfileList()
|
||
const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase()
|
||
let selected = 0
|
||
for (let i = 0; i < profiles.length; i++) {
|
||
if (profiles[i].id === cur) selected = i
|
||
}
|
||
const deviceCur = String(config.qvac_device || 'auto').trim().toLowerCase() || 'auto'
|
||
const form = ctx.tui.form.create({
|
||
title: 'QVAC (local on-device)',
|
||
fields: [
|
||
{
|
||
type: 'radio',
|
||
name: 'qvac_profile',
|
||
label: 'Model profile',
|
||
options: profiles.map(function (p) {
|
||
return {
|
||
label: p.label + ' — ' + p.chatModel + ' (' + p.description + ')',
|
||
value: p.id
|
||
}
|
||
}),
|
||
selected: selected
|
||
},
|
||
{
|
||
type: 'radio',
|
||
name: 'qvac_device',
|
||
label: 'Device',
|
||
options: [
|
||
{ label: 'Auto (detect GPU)', value: 'auto' },
|
||
{ label: 'CPU', value: 'cpu' },
|
||
{ label: 'GPU', value: 'gpu' }
|
||
],
|
||
selected: deviceCur === 'cpu' ? 1 : deviceCur === 'gpu' ? 2 : 0
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
const chosen = bareAgentQvacGetProfile(String(values.qvac_profile || 'recommended'))
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
config.qvac_profile = chosen.id
|
||
config.qvac_model = chosen.chatModel
|
||
config.model = chosen.chatModel
|
||
config.qvac_ctx_size = 0
|
||
config.qvac_device = String(values.qvac_device || 'auto').trim() || 'auto'
|
||
config.qvac_main_gpu = 'auto'
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {{ config: string }} paths
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupRestFieldsTui(ctx, argv0, paths, config) {
|
||
const providers = bareAgentRestProviderList()
|
||
const curProv = String(config.provider || 'groq').trim().toLowerCase()
|
||
let selected = 0
|
||
for (let i = 0; i < providers.length; i++) {
|
||
if (providers[i].id === curProv) selected = i
|
||
}
|
||
const form = ctx.tui.form.create({
|
||
title: 'REST API (OpenAI-compatible)',
|
||
fields: [
|
||
{
|
||
type: 'radio',
|
||
name: 'provider',
|
||
label: 'Provider',
|
||
options: providers.map(function (p) {
|
||
return { label: p.label, value: p.id }
|
||
}),
|
||
selected: selected
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'rest_base_url',
|
||
label: 'Base URL (blank = provider default)',
|
||
value: String(config.rest_base_url || '')
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'rest_api_key',
|
||
label:
|
||
'API key' +
|
||
(String(config.rest_api_key || '').trim()
|
||
? ' [leave blank to keep ' +
|
||
bareAgentMaskSecretPreview(config.rest_api_key) +
|
||
']'
|
||
: ' (required)'),
|
||
value: ''
|
||
},
|
||
{
|
||
type: 'text',
|
||
name: 'model',
|
||
label: 'Model id (blank = provider default)',
|
||
value: bareAgentIsQvacModelId(String(config.model || ''))
|
||
? ''
|
||
: String(config.model || '')
|
||
}
|
||
]
|
||
})
|
||
const values = await ctx.tui.form.run(form)
|
||
if (!values) return config
|
||
const spec = bareAgentRestGetProvider(String(values.provider || 'groq'))
|
||
config.backend = 'rest'
|
||
config.provider = spec.id
|
||
const url = String(values.rest_base_url || '').trim()
|
||
config.rest_base_url = url || spec.rest_base_url || String(config.rest_base_url || '')
|
||
const typedKey = String(values.rest_api_key || '').trim()
|
||
if (typedKey) config.rest_api_key = typedKey
|
||
const model = String(values.model || '').trim()
|
||
if (model) config.model = model
|
||
else if (!String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || ''))) {
|
||
config.model = spec.default_model || config.model
|
||
}
|
||
if (!String(config.rest_api_key || '').trim() && bareAgentCanPlainSetup(ctx)) {
|
||
const key =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'API key (required, input hidden): ',
|
||
{ mask: true }
|
||
)) || ''
|
||
if (key.trim()) config.rest_api_key = key.trim()
|
||
}
|
||
if (spec.id === 'custom' && !String(config.rest_base_url || '').trim()) {
|
||
bareAgentErr(ctx, argv0 + ': custom REST provider needs a base URL (e.g. https://host/v1).')
|
||
}
|
||
void paths
|
||
return config
|
||
}
|
||
|
||
async function bareAgentInteractiveSetup(ctx, argv0, paths, config, opts) {
|
||
void opts
|
||
bareAgentLog(ctx, argv0 + ': configuring ~/.agent/config.json')
|
||
if (
|
||
ctx.tui &&
|
||
ctx.tui.form &&
|
||
typeof ctx.tui.form.run === 'function' &&
|
||
typeof ctx.tui.isTTY === 'function' &&
|
||
ctx.tui.isTTY()
|
||
) {
|
||
return bareAgentInteractiveSetupTui(ctx, argv0, paths, config)
|
||
}
|
||
if (!bareAgentCanPlainSetup(ctx)) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': interactive setup needs a TTY with stdin/stdout. Edit ' +
|
||
paths.config +
|
||
' manually or run from an interactive shell.'
|
||
)
|
||
return config
|
||
}
|
||
const stdout = ctx.replStdout || ctx.stdout
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n=== ' +
|
||
argv0 +
|
||
' configuration ===\n' +
|
||
'Pick a backend, then only that backend is stored in ~/.agent/config.json.\n' +
|
||
'Enter keeps the [default].\n\n'
|
||
)
|
||
try {
|
||
const owner =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Owner / human name [' +
|
||
(String(config.owner_name || '').trim() || 'unset') +
|
||
']: '
|
||
)) || ''
|
||
if (owner.trim()) config.owner_name = owner.trim()
|
||
const label =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Agent display name [' +
|
||
(String(config.agent_label || '').trim() || 'BareAgent') +
|
||
']: '
|
||
)) || ''
|
||
if (label.trim()) config.agent_label = label.trim()
|
||
|
||
const curBackend = bareAgentResolveBackend(config)
|
||
const qvacOk = bareAgentQvacBridgeAvailable(ctx)
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nInference backend:\n' +
|
||
' 1) QVAC — local on-device models' +
|
||
(qvacOk ? '' : ' [host bridge unavailable]') +
|
||
'\n' +
|
||
' 2) REST API — Groq, xAI, OpenAI, or any OpenAI-compatible URL\n'
|
||
)
|
||
const backendRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Backend [1=QVAC, 2=REST] [' +
|
||
(curBackend === 'rest' ? '2' : '1') +
|
||
']: '
|
||
)) || ''
|
||
{
|
||
const v = backendRaw.trim().toLowerCase()
|
||
if (v === '2' || v === 'rest' || v === 'r') config.backend = 'rest'
|
||
else if (v === '1' || v === 'qvac' || v === 'q') config.backend = 'qvac'
|
||
else if (!String(config.backend || '').trim()) config.backend = curBackend
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'qvac') {
|
||
if (!qvacOk) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nQVAC host bridge is unavailable on this boot.\n'
|
||
)
|
||
const sw =
|
||
(await bareAgentPromptSetupLine(ctx, 'Switch to REST API instead? [Y/n]: ')) ||
|
||
''
|
||
const ans = sw.trim().toLowerCase()
|
||
if (ans === 'n' || ans === 'no') {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 + ': QVAC selected but the host bridge is unavailable.'
|
||
)
|
||
return config
|
||
}
|
||
config.backend = 'rest'
|
||
} else {
|
||
config = await bareAgentSetupQvacFieldsPlain(ctx, stdout, config)
|
||
}
|
||
}
|
||
|
||
if (bareAgentResolveBackend(config) === 'rest') {
|
||
config = await bareAgentSetupRestFieldsPlain(ctx, stdout, config)
|
||
}
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': setup input failed: ' + msg)
|
||
return config
|
||
}
|
||
config = bareAgentApplyProviderProfile(config)
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
bareAgentLog(ctx, 'Configuration saved.\n' + bareAgentFormatConfigSummary(config))
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} stdout
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupQvacFieldsPlain(ctx, stdout, config) {
|
||
const profiles = bareAgentQvacProfileList()
|
||
const cur = String(config.qvac_profile || 'recommended').trim().toLowerCase()
|
||
let lines = '\nQVAC model profile:\n'
|
||
for (let i = 0; i < profiles.length; i++) {
|
||
const p = profiles[i]
|
||
lines +=
|
||
' ' +
|
||
String(i + 1) +
|
||
') ' +
|
||
p.label +
|
||
' — ' +
|
||
p.chatModel +
|
||
(p.id === cur ? ' [current]' : '') +
|
||
'\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, lines)
|
||
const raw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Profile [1-' + String(profiles.length) + '] [recommended]: '
|
||
)) || ''
|
||
let chosen = bareAgentQvacGetProfile(cur || 'recommended')
|
||
const n = Number(raw.trim())
|
||
if (Number.isFinite(n) && n >= 1 && n <= profiles.length) {
|
||
chosen = profiles[n - 1]
|
||
} else if (raw.trim()) {
|
||
chosen = bareAgentQvacGetProfile(raw.trim())
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nDevice:\n 1) auto (detect GPU)\n 2) cpu\n 3) gpu\n'
|
||
)
|
||
const devRaw = (await bareAgentPromptSetupLine(ctx, 'Device [1=auto, 2=cpu, 3=gpu] [1]: ')) || ''
|
||
const dv = devRaw.trim().toLowerCase()
|
||
const device = dv === '2' || dv === 'cpu' ? 'cpu' : dv === '3' || dv === 'gpu' ? 'gpu' : 'auto'
|
||
config.backend = 'qvac'
|
||
config.provider = 'qvac'
|
||
config.qvac_profile = chosen.id
|
||
config.qvac_model = chosen.chatModel
|
||
config.model = chosen.chatModel
|
||
config.qvac_ctx_size = 0
|
||
config.qvac_device = device
|
||
config.qvac_main_gpu = 'auto'
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nQVAC: ' + chosen.label + ' → ' + chosen.chatModel + ' (device ' + device + ').\n'
|
||
)
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} stdout
|
||
* @param {Record<string, unknown>} config
|
||
*/
|
||
async function bareAgentSetupRestFieldsPlain(ctx, stdout, config) {
|
||
const providers = bareAgentRestProviderList()
|
||
let lines = '\nREST provider:\n'
|
||
for (let i = 0; i < providers.length; i++) {
|
||
lines += ' ' + String(i + 1) + ') ' + providers[i].label + '\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, lines)
|
||
const curProv = String(config.provider || 'groq').trim().toLowerCase()
|
||
const defIdx =
|
||
providers.findIndex(function (p) {
|
||
return p.id === curProv
|
||
}) + 1
|
||
const raw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Provider [1=Groq, 2=xAI, 3=OpenAI, 4=Custom] [' +
|
||
String(defIdx > 0 ? defIdx : 1) +
|
||
']: '
|
||
)) || ''
|
||
let spec = bareAgentRestGetProvider(curProv === 'qvac' ? 'groq' : curProv)
|
||
const n = Number(raw.trim())
|
||
if (Number.isFinite(n) && n >= 1 && n <= providers.length) spec = providers[n - 1]
|
||
else if (raw.trim()) spec = bareAgentRestGetProvider(raw.trim())
|
||
config.backend = 'rest'
|
||
config.provider = spec.id
|
||
|
||
const urlDefault = spec.rest_base_url || String(config.rest_base_url || '')
|
||
const urlRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Base URL [' + (urlDefault || 'required for custom') + ']: '
|
||
)) || ''
|
||
const url = urlRaw.trim() || urlDefault
|
||
if (!url) {
|
||
throw new Error('REST base URL is required for provider ' + spec.id)
|
||
}
|
||
config.rest_base_url = url.replace(/\/+$/, '')
|
||
|
||
const haveKey = Boolean(String(config.rest_api_key || '').trim())
|
||
const keyPrompt = haveKey
|
||
? 'API key [Enter keeps ' + bareAgentMaskSecretPreview(config.rest_api_key) + ']: '
|
||
: 'API key (required, input hidden): '
|
||
const keyRaw = (await bareAgentPromptSetupLine(ctx, keyPrompt, { mask: true })) || ''
|
||
if (keyRaw.trim()) config.rest_api_key = keyRaw.trim()
|
||
if (!String(config.rest_api_key || '').trim()) {
|
||
throw new Error('REST API key is required')
|
||
}
|
||
|
||
/** @type {{ id: string }[]} */
|
||
let remote = []
|
||
const fetchFn =
|
||
typeof ctx.httpFetch === 'function'
|
||
? ctx.httpFetch.bind(ctx)
|
||
: typeof fetch === 'function'
|
||
? fetch
|
||
: null
|
||
if (fetchFn && typeof bareAgentFetchRestModels === 'function') {
|
||
try {
|
||
remote = await bareAgentFetchRestModels(fetchFn, {
|
||
baseUrl: config.rest_base_url,
|
||
apiKey: config.rest_api_key
|
||
})
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nLive models from ' + config.rest_base_url + ' (' + String(remote.length) + '):\n'
|
||
)
|
||
} catch (err) {
|
||
const msg =
|
||
err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err)
|
||
bareAgentWriteOut(ctx, stdout, '\nCould not list remote models (' + msg + '). Using curated list.\n')
|
||
}
|
||
}
|
||
const suggest = remote.length
|
||
? remote.slice(0, 16).map(function (m) {
|
||
return m.id
|
||
})
|
||
: spec.models && spec.models.length
|
||
? spec.models
|
||
: typeof bareAgentRestModelsFallback === 'function'
|
||
? bareAgentRestModelsFallback(spec.id).map(function (m) {
|
||
return m.id
|
||
})
|
||
: []
|
||
if (suggest.length) {
|
||
let mlines = remote.length ? '' : '\nSuggested models for ' + spec.label + ':\n'
|
||
if (!mlines) mlines = ''
|
||
for (let i = 0; i < suggest.length; i++) {
|
||
mlines += ' ' + String(i + 1) + ') ' + suggest[i] + '\n'
|
||
}
|
||
bareAgentWriteOut(ctx, stdout, mlines)
|
||
}
|
||
const modelDefault =
|
||
!String(config.model || '').trim() || bareAgentIsQvacModelId(String(config.model || ''))
|
||
? spec.default_model
|
||
: String(config.model)
|
||
const modelRaw =
|
||
(await bareAgentPromptSetupLine(
|
||
ctx,
|
||
'Model [' + (modelDefault || 'required') + ']: '
|
||
)) || ''
|
||
const modelPick = modelRaw.trim()
|
||
const modelNum = Number(modelPick)
|
||
if (Number.isFinite(modelNum) && suggest[modelNum - 1]) {
|
||
config.model = suggest[modelNum - 1]
|
||
} else if (modelPick) {
|
||
config.model = modelPick
|
||
} else if (modelDefault) {
|
||
config.model = modelDefault
|
||
} else {
|
||
throw new Error('REST model id is required')
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\nREST: ' +
|
||
spec.label +
|
||
' → ' +
|
||
config.model +
|
||
'\n ' +
|
||
config.rest_base_url +
|
||
'\n'
|
||
)
|
||
return config
|
||
}
|
||
|
||
/**
|
||
* Interactive setup only (`agent --setup`).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
*/
|
||
async function bareAgentRunSetupOnly(ctx, argv0) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
config = bareAgentApplyProviderProfile(config)
|
||
if (!bareAgentCanPlainSetup(ctx)) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': --setup / --config needs an interactive TTY. Edit ' +
|
||
paths.config +
|
||
' manually.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
let suspended = false
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag: true,
|
||
interactiveSetup: true
|
||
})
|
||
} finally {
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
ctx.resumeReplAfterSubprocess()
|
||
}
|
||
}
|
||
try {
|
||
await bareAgentEnsureWorkspace(ctx, paths)
|
||
await bareAgentEnsureSkillTemplates(ctx, paths, config)
|
||
await bareAgentSyncWorkspaceFromConfig(ctx, paths, config)
|
||
} catch {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': could not seed ~/.agent/workspace from /share/agent-workspace (check system image)'
|
||
)
|
||
}
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
ctx.exitCode = 0
|
||
void config
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {string} task
|
||
* @param {{ setupFlag?: boolean }} [runOpts]
|
||
*/
|
||
async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||
const setupFlag = Boolean(runOpts && runOpts.setupFlag)
|
||
const autonomousFlag = Boolean(runOpts && (runOpts.autonomous || runOpts.autonomousGoal))
|
||
const planFlag = Boolean(runOpts && runOpts.planMode)
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
let { config } = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
config = bareAgentApplyProviderProfile(config)
|
||
if (planFlag) config.plan_mode_active = true
|
||
if (runOpts && Number(runOpts.maxIterations) > 0) {
|
||
config.max_iterations = Math.floor(Number(runOpts.maxIterations))
|
||
}
|
||
if (runOpts && runOpts.modelOverride) {
|
||
const m = String(runOpts.modelOverride).trim()
|
||
if (m) {
|
||
const backendNow = bareAgentResolveBackend(config)
|
||
if (backendNow === 'qvac') config.qvac_model = m
|
||
else config.model = m
|
||
}
|
||
}
|
||
|
||
const canWizard = bareAgentCanPlainSetup(ctx)
|
||
if (setupFlag && !canWizard) {
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': --setup / --config needs an interactive TTY. Edit ' +
|
||
paths.config +
|
||
' manually.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
const backendNow = bareAgentResolveBackend(config)
|
||
const needWizard =
|
||
setupFlag ||
|
||
(canWizard &&
|
||
(backendNow === 'rest'
|
||
? !(config.rest_api_key && String(config.rest_api_key).trim())
|
||
: !String(config.qvac_model || config.model || '').trim() ||
|
||
(backendNow === 'qvac' && !bareAgentQvacBridgeAvailable(ctx))))
|
||
|
||
let replSuspendedForSetup = false
|
||
if (needWizard && typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
replSuspendedForSetup = true
|
||
}
|
||
if (needWizard) {
|
||
config = await bareAgentInteractiveSetup(ctx, argv0, paths, config, {
|
||
setupFlag,
|
||
interactiveSetup: true
|
||
})
|
||
config = bareAgentApplyProviderProfile(config)
|
||
}
|
||
|
||
const backendReady = bareAgentResolveBackend(config)
|
||
if (backendReady === 'rest') {
|
||
if (!config.rest_api_key || !String(config.rest_api_key).trim()) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': set rest_api_key in ' +
|
||
paths.config +
|
||
' or run `' +
|
||
argv0 +
|
||
' --setup` / `' +
|
||
argv0 +
|
||
' --config`.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
} else if (!bareAgentQvacBridgeAvailable(ctx)) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': QVAC backend selected but host bridge unavailable (set BARE_OS_SKIP_QVAC=0, install @qvac/sdk, or choose REST via `' +
|
||
argv0 +
|
||
' --config`).'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
} else if (!String(config.qvac_model || config.model || '').trim()) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 +
|
||
': set qvac_model / qvac_profile in ' +
|
||
paths.config +
|
||
' or run `' +
|
||
argv0 +
|
||
' --config`.'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (autonomousFlag) {
|
||
config = bareAgentBeginAutonomousRun(config, {
|
||
goal: String((runOpts && runOpts.autonomousGoal) || task || '').trim(),
|
||
maxRuntimeMs: Number(runOpts && runOpts.autonomousMaxRuntimeMs) || undefined,
|
||
requiredChecks: (runOpts && runOpts.autonomousChecks) || undefined
|
||
})
|
||
}
|
||
if (autonomousFlag || planFlag) {
|
||
try {
|
||
await bareAgentSaveConfig(ctx, paths, config)
|
||
} catch {
|
||
/* persist best-effort */
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (ctx.vfs?.chmod) await ctx.vfs.chmod(paths.config, 0o600)
|
||
} catch {
|
||
/* ignore — optional */
|
||
}
|
||
|
||
const fetchFn = bareAgentResolveFetch(ctx)
|
||
if (backendReady === 'rest' && !fetchFn) {
|
||
if (
|
||
replSuspendedForSetup &&
|
||
typeof ctx.resumeReplAfterSubprocess === 'function'
|
||
) {
|
||
ctx.resumeReplAfterSubprocess()
|
||
replSuspendedForSetup = false
|
||
}
|
||
bareAgentErr(
|
||
ctx,
|
||
argv0 + ': no fetch (ctx.httpFetch / bare.fetch / global fetch)'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
/** @type {unknown[]} */
|
||
let messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
const instructions = await bareAgentLoadInstructionFiles(ctx, paths)
|
||
const manDigest = await bareAgentManDigest(ctx)
|
||
|
||
await bareAgentEnsureWorkspace(ctx, paths)
|
||
await bareAgentEnsureSkillTemplates(ctx, paths, config)
|
||
if (needWizard) await bareAgentSyncWorkspaceFromConfig(ctx, paths, config)
|
||
|
||
const backendForPrompt = bareAgentResolveBackend(config)
|
||
const promptProfile = bareAgentQvacGetProfile(
|
||
String(config.qvac_profile || config.profile || '')
|
||
)
|
||
const promptCtxSize =
|
||
backendForPrompt === 'qvac'
|
||
? bareAgentQvacResolveCtxSize(config, promptProfile)
|
||
: 32768
|
||
// Local QVAC models have a hard ctx window; keep the system prompt lean.
|
||
// Scale char budgets from ctx (chars ≈ tokens*4); leave room for tools/reply.
|
||
const toolsOn = backendForPrompt === 'qvac'
|
||
const promptCharBudget = Math.max(
|
||
4000,
|
||
Math.floor(promptCtxSize * 4 * (toolsOn ? 0.35 : 0.7))
|
||
)
|
||
const workspaceBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(8000, Math.floor(promptCharBudget * 0.35))
|
||
: 24000
|
||
const manBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(4000, Math.floor(promptCharBudget * 0.2))
|
||
: 12000
|
||
const instructionsBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(3000, Math.floor(promptCharBudget * 0.2))
|
||
: 8000
|
||
const skillsBudget =
|
||
backendForPrompt === 'qvac'
|
||
? Math.min(2000, Math.floor(promptCharBudget * 0.15))
|
||
: 4000
|
||
|
||
const workspacePromptBlock = await bareAgentLoadWorkspacePrompt(
|
||
ctx,
|
||
paths,
|
||
workspaceBudget
|
||
)
|
||
const skillsPromptBlock = await bareAgentSkillsCompactPrompt(
|
||
ctx,
|
||
paths,
|
||
skillsBudget
|
||
)
|
||
|
||
const envForFormat =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const discordReply =
|
||
/^(1|true|yes)$/i.test(String(envForFormat.BARE_OS_AGENT_DISCORD || '').trim())
|
||
|
||
let systemContent =
|
||
BARE_AGENT_STATIC_SYSTEM +
|
||
BARE_AGENT_OPERATING_CONTRACT +
|
||
(discordReply ? BARE_AGENT_DISCORD_REPLY_FORMAT : '') +
|
||
'\n\n' +
|
||
bareAgentSessionHomeBlock(ctx, home, paths) +
|
||
'\n\n' +
|
||
manDigest.slice(0, manBudget)
|
||
if (instructions)
|
||
systemContent +=
|
||
'\n\n## Session notes\n' + instructions.slice(0, instructionsBudget)
|
||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||
try {
|
||
const startHook = await bareAgentRunHooks(ctx, paths.hooks, 'SessionStart', '', {}, '')
|
||
if (startHook && startHook.inject) {
|
||
systemContent +=
|
||
'\n\n## SessionStart hook\n' + String(startHook.inject).slice(0, 1500)
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
if (runOpts && runOpts.extraSystemFile) {
|
||
try {
|
||
const extraPath = String(runOpts.extraSystemFile).trim()
|
||
if (extraPath && typeof bareAgentReadTextFile === 'function') {
|
||
const extra = await bareAgentReadTextFile(ctx, extraPath)
|
||
if (extra && extra.trim()) {
|
||
systemContent +=
|
||
'\n\n## Extra instructions (--system)\n' +
|
||
extra.trim().slice(0, instructionsBudget)
|
||
}
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
if (workspacePromptBlock && String(workspacePromptBlock).trim())
|
||
systemContent += '\n\n' + String(workspacePromptBlock).trim()
|
||
if (skillsPromptBlock && String(skillsPromptBlock).trim())
|
||
systemContent += '\n\n' + String(skillsPromptBlock).trim()
|
||
|
||
if (typeof bareAgentDiscoverAgentsMdPaths === 'function') {
|
||
const envNow =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const cwd = String(envNow.PWD || envNow.CWD || home || '').trim() || home
|
||
try {
|
||
const extraFiles = await bareAgentDiscoverAgentsMdPaths(ctx, cwd, 12)
|
||
const workspaceAgents =
|
||
(typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace') +
|
||
'/AGENTS.md'
|
||
const filtered = extraFiles.filter(function (p) {
|
||
return p !== workspaceAgents
|
||
})
|
||
if (filtered.length && typeof bareAgentLoadAgentsMdPrompt === 'function') {
|
||
const extraBlock = await bareAgentLoadAgentsMdPrompt(ctx, filtered, 4000)
|
||
if (extraBlock && extraBlock.trim()) systemContent += '\n\n' + extraBlock.trim()
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
if (config.plan_mode_active) {
|
||
systemContent +=
|
||
'\n\n## PLAN MODE is ON\n' +
|
||
'Read-only tools only. The only allowed write is ' +
|
||
String(paths.plan || home + '/.agent/plan.md') +
|
||
'. Draft the plan there, then call exit_plan_mode before implementing.\n'
|
||
}
|
||
|
||
if (typeof bareAgentLoadTodos === 'function' && paths.todos) {
|
||
try {
|
||
const todos = await bareAgentLoadTodos(ctx, paths.todos)
|
||
const sum = bareAgentTodoSummarize(todos)
|
||
if (sum.total) {
|
||
systemContent +=
|
||
'\n\n## Session todos\n' +
|
||
sum.text +
|
||
'\n(open=' +
|
||
String(sum.open) +
|
||
' completed=' +
|
||
String(sum.completed) +
|
||
')\n'
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
let userTask = String(task || '')
|
||
if (config.autonomous_active) {
|
||
userTask =
|
||
'AUTONOMOUS RUN. Execute until the goal is done. Do not stop after a plan — use tools, then call task_complete.\n' +
|
||
'Goal: ' +
|
||
String(config.autonomous_goal || task || '') +
|
||
'\n\n' +
|
||
userTask
|
||
}
|
||
|
||
if (!messages.length) {
|
||
messages = [
|
||
{ role: 'system', content: systemContent },
|
||
{ role: 'user', content: userTask }
|
||
]
|
||
} 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: userTask })
|
||
}
|
||
|
||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||
ctx.replStdout || ctx.stdout
|
||
)
|
||
const useColor = bareEditUseColor(ctx)
|
||
const envBag =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const agentVerboseEnv = String(envBag.BARE_OS_AGENT_VERBOSE || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const agentVerboseForced =
|
||
agentVerboseEnv === '1' ||
|
||
agentVerboseEnv === 'true' ||
|
||
agentVerboseEnv === 'yes'
|
||
|
||
/** @type {{ current: Record<string, unknown> }} */
|
||
const configRef = { current: { ...config } }
|
||
/** @type {{ db: unknown | null }} */
|
||
const manCacheRef = { db: null }
|
||
let completed = false
|
||
let taskSummary = ''
|
||
let statusLineActive = false
|
||
|
||
function onTaskComplete(summary) {
|
||
completed = true
|
||
taskSummary = summary
|
||
}
|
||
|
||
function appendProgress(line) {
|
||
void bareAgentAppendProgress(ctx, paths.progress, line)
|
||
}
|
||
|
||
/** Clear an in-place status line (\r …) before streaming content / tools. */
|
||
function clearStatusLine() {
|
||
if (!statusLineActive) return
|
||
bareAgentWriteOut(ctx, stdout, '\r\x1b[K')
|
||
statusLineActive = false
|
||
}
|
||
|
||
/**
|
||
* @param {ReturnType<typeof bareAgentReasoningSettings>} rs
|
||
*/
|
||
function agentVerbose(rs) {
|
||
return agentVerboseForced || (rs && rs.enabled && rs.mode === 'trace')
|
||
}
|
||
|
||
const url =
|
||
bareAgentNormalizeBaseUrl(String(configRef.current.rest_base_url || '')) +
|
||
'/chat/completions'
|
||
const tools = bareAgentToolDefinitions()
|
||
let reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
let autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
let reasoningCharCount = 0
|
||
let turnsSinceTodoWrite = 0
|
||
|
||
let suspended = replSuspendedForSetup
|
||
/** @type {(() => void) | null} */
|
||
let detachThinkKeysSession = null
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function' && !suspended) {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
|
||
const masterAbort = new AbortController()
|
||
const prevAbortAgent =
|
||
typeof ctx.bareOsAbortActiveAgent === 'function'
|
||
? ctx.bareOsAbortActiveAgent
|
||
: null
|
||
ctx.bareOsAbortActiveAgent = () => {
|
||
try {
|
||
masterAbort.abort()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof ctx.bareOsQvacCancelActive === 'function') {
|
||
ctx.bareOsQvacCancelActive()
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (detachThinkKeysSession) detachThinkKeysSession()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/** @type {(() => void) | null} */
|
||
let offSigint = null
|
||
if (globalThis.process && typeof globalThis.process.on === 'function') {
|
||
const fn = () => {
|
||
try {
|
||
if (typeof ctx.bareOsAbortActiveAgent === 'function') {
|
||
ctx.bareOsAbortActiveAgent()
|
||
} else {
|
||
masterAbort.abort()
|
||
}
|
||
} catch {
|
||
masterAbort.abort()
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + '^C' + EDIT_ANSI_RESET + '\n'
|
||
)
|
||
}
|
||
globalThis.process.on('SIGINT', fn)
|
||
offSigint = () => {
|
||
try {
|
||
globalThis.process.off('SIGINT', fn)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
const maxIterBase = Number(configRef.current.max_iterations) || 64
|
||
let iter = 0
|
||
|
||
for (;;) {
|
||
if (masterAbort.signal.aborted) {
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
reasoningSettings = bareAgentReasoningSettings(configRef.current)
|
||
autonomousSettings = bareAgentAutonomousSettings(configRef.current)
|
||
const autoLive =
|
||
Boolean(configRef.current.autonomous_active) &&
|
||
!autonomousSettings.stopRequested
|
||
const maxIter = autoLive ? Math.max(maxIterBase, 96) : maxIterBase
|
||
if (completed && !autoLive) break
|
||
if (autonomousSettings.enabled && autonomousSettings.active) {
|
||
const now = Date.now()
|
||
if (autonomousSettings.stopRequested) {
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'stopped'
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous stopped by manual request')
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] autonomous run stopped by request' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
if (
|
||
autonomousSettings.startedAtMs > 0 &&
|
||
now - autonomousSettings.startedAtMs >=
|
||
autonomousSettings.maxRuntimeMs
|
||
) {
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'timebox_expired'
|
||
configRef.current.autonomous_last_error = 'timebox_expired'
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous timebox expired')
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] autonomous run stopped (timebox expired)' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
iter++
|
||
if (iter > maxIter) {
|
||
bareAgentErr(ctx, argv0 + ': max_iterations exceeded')
|
||
ctx.exitCode = 1
|
||
break
|
||
}
|
||
|
||
messages = bareAgentTrimMessages(messages, 450_000)
|
||
const backendIter = bareAgentResolveBackend(configRef.current)
|
||
/** @type {Record<string, unknown>} */
|
||
const providerNow = String(configRef.current.provider || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
let qvacToolsForTurn = /** @type {unknown[]} */ ([])
|
||
if (backendIter === 'qvac') {
|
||
const profileEarly = bareAgentQvacGetProfile(
|
||
String(configRef.current.qvac_profile || 'recommended')
|
||
)
|
||
const ctxEarly = bareAgentQvacResolveCtxSize(
|
||
configRef.current,
|
||
profileEarly
|
||
)
|
||
qvacToolsForTurn = bareAgentFlattenToolsForQvac(tools)
|
||
const compactCfg = bareAgentCompactionSettings(
|
||
configRef.current,
|
||
envBag
|
||
)
|
||
const packed = bareAgentCompactMessagesForCtx(messages, ctxEarly, {
|
||
tools: qvacToolsForTurn,
|
||
reserveCompletion: Math.min(
|
||
1024,
|
||
Number(configRef.current.max_tokens) || 512
|
||
),
|
||
mode: compactCfg.mode,
|
||
keepRecent: compactCfg.keepRecent,
|
||
toolMaxChars: compactCfg.toolMaxChars,
|
||
autonomous: autoLive
|
||
? {
|
||
active: true,
|
||
goal: String(configRef.current.autonomous_goal || ''),
|
||
status: String(configRef.current.autonomous_status || 'running'),
|
||
remainingMs: Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
}
|
||
: undefined
|
||
})
|
||
messages = packed.messages
|
||
if (packed.meta.compacted) {
|
||
appendProgress(
|
||
'context_compaction tokens ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
'/' +
|
||
packed.meta.budget +
|
||
' dropped_groups=' +
|
||
packed.meta.droppedGroups +
|
||
' tiers=' +
|
||
packed.meta.tiers.join(',')
|
||
)
|
||
if (agentVerbose(reasoningSettings)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[ctx] compacted ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
' tok (budget ' +
|
||
packed.meta.budget +
|
||
'; ' +
|
||
packed.meta.tiers.join(' › ') +
|
||
')' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
} else if (packed.meta.droppedGroups > 0) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'· context compacted (' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens +
|
||
' tok)\n' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
// Persist latest rolling summary for operators / next sessions.
|
||
try {
|
||
const compactPath = paths.compact || home + '/.agent/compact.md'
|
||
const summaryMsg = messages.find((m) =>
|
||
bareAgentIsCompactionMessage(m)
|
||
)
|
||
if (
|
||
summaryMsg &&
|
||
ctx.vfs &&
|
||
typeof ctx.vfs.writeFile === 'function'
|
||
) {
|
||
const body =
|
||
'# Agent context compaction\n\n' +
|
||
bareAgentMessagePlainText(summaryMsg) +
|
||
'\n'
|
||
const buf =
|
||
typeof ctx.b4a !== 'undefined' &&
|
||
ctx.b4a &&
|
||
typeof ctx.b4a.from === 'function'
|
||
? ctx.b4a.from(body)
|
||
: new TextEncoder().encode(body)
|
||
await ctx.vfs.writeFile(compactPath, buf)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} else {
|
||
// REST backends: still shrink fat history soft-cap.
|
||
const compactCfg = bareAgentCompactionSettings(
|
||
configRef.current,
|
||
envBag
|
||
)
|
||
if (compactCfg.mode !== 'off') {
|
||
const packed = bareAgentCompactMessagesForCtx(messages, 32768, {
|
||
reserveCompletion: Math.min(
|
||
2048,
|
||
Number(configRef.current.max_tokens) || 1024
|
||
),
|
||
mode: compactCfg.mode,
|
||
keepRecent: compactCfg.keepRecent || 12,
|
||
toolMaxChars: compactCfg.toolMaxChars,
|
||
autonomous: autoLive
|
||
? {
|
||
active: true,
|
||
goal: String(configRef.current.autonomous_goal || ''),
|
||
status: String(configRef.current.autonomous_status || 'running'),
|
||
remainingMs: Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() -
|
||
(autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
}
|
||
: undefined
|
||
})
|
||
messages = packed.messages
|
||
if (packed.meta.compacted && packed.meta.droppedGroups > 0) {
|
||
appendProgress(
|
||
'context_compaction rest ' +
|
||
packed.meta.beforeTokens +
|
||
'→' +
|
||
packed.meta.afterTokens
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] request backend=' +
|
||
backendIter +
|
||
' provider=' +
|
||
(providerNow || 'unknown') +
|
||
' model=' +
|
||
String(
|
||
configRef.current.model || configRef.current.qvac_model || ''
|
||
) +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
let assistantContent = ''
|
||
/** @type {Map<number, { id: string, name: string, args: string }>} */
|
||
const toolAcc = new Map()
|
||
/** @type {unknown} */
|
||
let usageOut = null
|
||
let finishReason = ''
|
||
const hideThinkEnv = String(envBag.BARE_OS_AGENT_HIDE_THINK || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const hideThink =
|
||
hideThinkEnv === '1' ||
|
||
hideThinkEnv === 'true' ||
|
||
hideThinkEnv === 'yes'
|
||
const hideMdEnv = String(envBag.BARE_OS_AGENT_PLAIN || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const discordEnv = String(envBag.BARE_OS_AGENT_DISCORD || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const plainReply =
|
||
hideMdEnv === '1' ||
|
||
hideMdEnv === 'true' ||
|
||
hideMdEnv === 'yes' ||
|
||
discordEnv === '1' ||
|
||
discordEnv === 'true' ||
|
||
discordEnv === 'yes'
|
||
const thinkPanel =
|
||
!hideThink &&
|
||
stdout &&
|
||
/** @type {{ isTTY?: boolean }} */ (stdout).isTTY
|
||
? bareAgentCreateThinkPanel(ctx, stdout, {
|
||
useColor,
|
||
bodyLines: 8,
|
||
maxChars: Math.max(reasoningSettings.maxChars, 24_000),
|
||
write: bareAgentWriteOut
|
||
})
|
||
: null
|
||
const detachThinkKeys = bareAgentAttachThinkScrollKeys(ctx, thinkPanel, {
|
||
onAbort: () => {
|
||
try {
|
||
if (typeof ctx.bareOsAbortActiveAgent === 'function') {
|
||
ctx.bareOsAbortActiveAgent()
|
||
} else {
|
||
masterAbort.abort()
|
||
}
|
||
} catch {
|
||
masterAbort.abort()
|
||
}
|
||
}
|
||
})
|
||
detachThinkKeysSession = detachThinkKeys
|
||
let thinkSealed = false
|
||
let replyStarted = false
|
||
let replyPainted = false
|
||
|
||
/**
|
||
* @param {string} chunk
|
||
*/
|
||
function feedThink(chunk) {
|
||
if (!chunk || !String(chunk).trim()) return
|
||
if (hideThink) return
|
||
clearStatusLine()
|
||
if (thinkPanel) thinkPanel.append(chunk)
|
||
else {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + chunk + EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
}
|
||
|
||
function sealThinkIfNeeded() {
|
||
if (thinkSealed) return
|
||
thinkSealed = true
|
||
if (thinkPanel && thinkPanel.hasContent()) thinkPanel.seal()
|
||
}
|
||
|
||
function termWidth() {
|
||
if (typeof bareAgentResolveTermCols === 'function') {
|
||
return bareAgentResolveTermCols(ctx, stdout)
|
||
}
|
||
const fromOut =
|
||
stdout && typeof stdout === 'object'
|
||
? Number(/** @type {{ columns?: number }} */ (stdout).columns)
|
||
: NaN
|
||
const fromEnv = parseInt(String(envBag.COLUMNS || '80'), 10)
|
||
const n = Number.isFinite(fromOut) && fromOut > 0 ? fromOut : fromEnv
|
||
return Math.max(40, Number.isFinite(n) && n > 0 ? n : 80)
|
||
}
|
||
|
||
/**
|
||
* Buffer reply; show live status under the think box (markdown painted later).
|
||
* @param {string} chunk
|
||
*/
|
||
function feedReply(chunk) {
|
||
if (!chunk) return
|
||
sealThinkIfNeeded()
|
||
if (!replyStarted) replyStarted = true
|
||
clearStatusLine()
|
||
assistantContent += chunk
|
||
if (plainReply) {
|
||
bareAgentWriteOut(ctx, stdout, chunk)
|
||
return
|
||
}
|
||
if (thinkPanel && thinkPanel.hasContent()) {
|
||
thinkPanel.setStatus(
|
||
bareEditSgr('dim', useColor) +
|
||
'▌ answering… ' +
|
||
String(assistantContent.length) +
|
||
' chars · ↑↓ scroll thoughts' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
} else {
|
||
// No think box — stream plain for responsiveness; paint markdown at end.
|
||
bareAgentWriteOut(ctx, stdout, chunk)
|
||
}
|
||
}
|
||
|
||
function paintReplyMarkdown() {
|
||
if (replyPainted) return
|
||
replyPainted = true
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
if (plainReply) {
|
||
if (thinkPanel) thinkPanel.clearStatus()
|
||
return
|
||
}
|
||
const body = String(assistantContent || '')
|
||
if (thinkPanel) {
|
||
thinkPanel.clearStatus()
|
||
thinkPanel.detachBelow()
|
||
}
|
||
if (!body.trim()) return
|
||
// If we streamed plain (no think panel), rewind approximate lines then paint.
|
||
if (!thinkPanel || !thinkPanel.hasContent()) {
|
||
const roughLines = body.split('\n').length
|
||
if (
|
||
roughLines > 0 &&
|
||
stdout &&
|
||
/** @type {{ isTTY?: boolean }} */ (stdout).isTTY
|
||
) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\x1b[' + String(Math.min(40, roughLines)) + 'A\r\x1b[J'
|
||
)
|
||
}
|
||
}
|
||
const rendered = bareAgentRenderMarkdown(body, {
|
||
useColor,
|
||
width: termWidth()
|
||
})
|
||
bareAgentWriteOut(ctx, stdout, '\n' + rendered)
|
||
}
|
||
|
||
const thinkSplit = bareAgentCreateThinkTagSplitter({
|
||
onThink: feedThink,
|
||
onContent: feedReply
|
||
})
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} e
|
||
*/
|
||
function onCompletionEvent(e) {
|
||
if (e.type === 'delta_content') {
|
||
const chunk = typeof e.content === 'string' ? e.content : ''
|
||
if (chunk) thinkSplit.push(chunk)
|
||
} else if (e.type === 'delta_tool_calls') {
|
||
sealThinkIfNeeded()
|
||
thinkSplit.flush()
|
||
const arr = e.tool_calls
|
||
if (Array.isArray(arr)) {
|
||
for (const tc of arr) bareAgentMergeToolCallDelta(toolAcc, tc)
|
||
}
|
||
} else if (e.type === 'delta_reasoning') {
|
||
const chunk = typeof e.reasoning === 'string' ? e.reasoning : ''
|
||
if (chunk) {
|
||
if (reasoningSettings.enabled || !hideThink) {
|
||
if (reasoningCharCount < reasoningSettings.maxChars) {
|
||
const remain = reasoningSettings.maxChars - reasoningCharCount
|
||
const out = chunk.slice(0, remain)
|
||
reasoningCharCount += out.length
|
||
if (out.length) feedThink(out)
|
||
}
|
||
}
|
||
}
|
||
} else if (e.type === 'response_shape_keys') {
|
||
const keys = Array.isArray(e.keys)
|
||
? e.keys.map((k) => String(k)).join(',')
|
||
: ''
|
||
appendProgress('provider_shape_keys ' + keys.slice(0, 200))
|
||
} else if (e.type === 'usage') {
|
||
usageOut = e.usage
|
||
} else if (e.type === 'finish' || e.type === 'finish_reason') {
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
finishReason = String(e.finish_reason || '')
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (backendIter === 'qvac') {
|
||
const profile = bareAgentQvacGetProfile(
|
||
String(configRef.current.qvac_profile || 'recommended')
|
||
)
|
||
const modelSrc = String(
|
||
configRef.current.qvac_model ||
|
||
configRef.current.model ||
|
||
profile.chatModel
|
||
)
|
||
const ctxSize = bareAgentQvacResolveCtxSize(
|
||
configRef.current,
|
||
profile
|
||
)
|
||
const deviceOpts = bareAgentQvacResolveDeviceOpts(configRef.current)
|
||
const verboseUi = agentVerbose(reasoningSettings)
|
||
if (verboseUi) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] ensuring model ' +
|
||
modelSrc +
|
||
' (ctx=' +
|
||
ctxSize +
|
||
(deviceOpts.device
|
||
? ', device=' + deviceOpts.device
|
||
: ', auto-GPU') +
|
||
')…' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
try {
|
||
const st =
|
||
typeof ctx.bareOsQvacStatus === 'function'
|
||
? ctx.bareOsQvacStatus()
|
||
: null
|
||
if (st && (st.cacheDir || st.hdmsPath || st.backendsDir)) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[qvac] cache=' +
|
||
String(st.cacheDir || '') +
|
||
' hdms=' +
|
||
String(st.hdmsPath || '/mnt/models') +
|
||
' backends=' +
|
||
String(st.backendsDir || '(unresolved)') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\rLoading model…' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
statusLineActive = true
|
||
}
|
||
/**
|
||
* @param {unknown} prog
|
||
*/
|
||
function onLoadProgress(prog) {
|
||
const pct =
|
||
prog && typeof prog === 'object' && 'percentage' in prog
|
||
? Number(
|
||
/** @type {{ percentage?: unknown }} */ (prog).percentage
|
||
)
|
||
: NaN
|
||
if (!Number.isFinite(pct)) return
|
||
const label = verboseUi
|
||
? '\r[qvac] download/load ' + Math.floor(pct) + '%'
|
||
: '\rLoading model… ' + Math.floor(pct) + '%'
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) + label + EDIT_ANSI_RESET
|
||
)
|
||
statusLineActive = true
|
||
}
|
||
if (typeof ctx.bareOsQvacLoadModel === 'function') {
|
||
const loaded = await ctx.bareOsQvacLoadModel({
|
||
modelSrc,
|
||
tools: true,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
onProgress: onLoadProgress
|
||
})
|
||
clearStatusLine()
|
||
if (verboseUi) {
|
||
if (loaded && loaded.fellBackToCpu) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] GPU unavailable; using CPU' +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
} else if (
|
||
loaded &&
|
||
loaded.device === 'gpu' &&
|
||
loaded.mainGpu != null
|
||
) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[qvac] using main-gpu=' +
|
||
String(loaded.mainGpu) +
|
||
(loaded.probe ? ' (' + String(loaded.probe) + ')' : '') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
} else if (loaded && loaded.fellBackToCpu) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'Using CPU (GPU unavailable)\n' +
|
||
EDIT_ANSI_RESET
|
||
)
|
||
}
|
||
}
|
||
await ctx.bareOsQvacComplete({
|
||
history: messages,
|
||
tools: qvacToolsForTurn,
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
captureThinking: !hideThink || reasoningSettings.enabled,
|
||
modelSrc,
|
||
toolsEnabled: true,
|
||
ctxSize,
|
||
device: deviceOpts.device,
|
||
mainGpu: deviceOpts.mainGpu,
|
||
gpuLayers: deviceOpts.gpuLayers,
|
||
signal: masterAbort.signal,
|
||
onProgress: onLoadProgress,
|
||
onEvent: onCompletionEvent
|
||
})
|
||
clearStatusLine()
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
paintReplyMarkdown()
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
} else {
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
Authorization:
|
||
'Bearer ' + String(configRef.current.rest_api_key || '')
|
||
}
|
||
const eh = configRef.current.extra_headers
|
||
if (eh && typeof eh === 'object' && !Array.isArray(eh)) {
|
||
for (const [k, v] of Object.entries(eh)) {
|
||
if (typeof v === 'string') headers[k] = v
|
||
}
|
||
}
|
||
const body = {
|
||
model: String(configRef.current.model || ''),
|
||
stream: Boolean(configRef.current.stream !== false),
|
||
messages,
|
||
tools,
|
||
tool_choice: 'auto',
|
||
max_tokens: Number(configRef.current.max_tokens) || 4096,
|
||
temperature: Number(configRef.current.temperature) ?? 0.7
|
||
}
|
||
if (providerNow === 'xai') {
|
||
body.parallel_tool_calls =
|
||
Number(configRef.current.tool_parallelism) > 1 ? true : false
|
||
body.max_completion_tokens =
|
||
Number(configRef.current.max_tokens) || 4096
|
||
}
|
||
if (providerNow === 'groq') {
|
||
body.parallel_tool_calls =
|
||
Number(configRef.current.tool_parallelism) > 1 ? true : false
|
||
body.max_completion_tokens =
|
||
Number(configRef.current.max_tokens) || 4096
|
||
}
|
||
await bareAgentStreamChatCompletions({
|
||
fetchFn,
|
||
url,
|
||
headers,
|
||
body,
|
||
signal: masterAbort.signal,
|
||
onEvent: onCompletionEvent
|
||
})
|
||
clearStatusLine()
|
||
thinkSplit.flush()
|
||
sealThinkIfNeeded()
|
||
paintReplyMarkdown()
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
detachThinkKeys()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const msg =
|
||
e && typeof e === 'object' && 'name' in e && e.name === 'AbortError'
|
||
? 'aborted'
|
||
: e && typeof e === 'object' && 'message' in e
|
||
? String(e.message)
|
||
: String(e)
|
||
bareAgentErr(ctx, argv0 + ': ' + msg)
|
||
ctx.exitCode = 130
|
||
break
|
||
}
|
||
|
||
const toolCallsArr = bareAgentFinalizeToolCalls(toolAcc)
|
||
const hasTools = toolCallsArr.length > 0
|
||
if (!hasTools && finishReason === 'tool_calls') {
|
||
appendProgress(
|
||
'warning tool_calls_finish_without_tool_deltas provider=' +
|
||
providerNow
|
||
)
|
||
}
|
||
|
||
/** @type {Record<string, unknown>} */
|
||
const assistantMsg = {
|
||
role: 'assistant',
|
||
content: assistantContent || null,
|
||
tool_calls: hasTools ? toolCallsArr : undefined
|
||
}
|
||
messages.push(assistantMsg)
|
||
|
||
if (
|
||
usageOut &&
|
||
typeof usageOut === 'object' &&
|
||
agentVerbose(reasoningSettings)
|
||
) {
|
||
const u = /** @type {Record<string, unknown>} */ (usageOut)
|
||
const pt = u.prompt_tokens
|
||
const ct = u.completion_tokens
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('dim', useColor) +
|
||
'tokens: prompt=' +
|
||
String(pt ?? '?') +
|
||
' completion=' +
|
||
String(ct ?? '?') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
if (!hasTools) {
|
||
if (
|
||
bareAgentAutonomousShouldContinue(configRef.current, {
|
||
completed: completed,
|
||
hasTools: false,
|
||
stopRequested: autonomousSettings.stopRequested
|
||
})
|
||
) {
|
||
const remain = Math.max(
|
||
0,
|
||
autonomousSettings.maxRuntimeMs -
|
||
(Date.now() - (autonomousSettings.startedAtMs || Date.now()))
|
||
)
|
||
messages.push({
|
||
role: 'user',
|
||
content: bareAgentAutonomousContinuationPrompt(configRef.current, {
|
||
remainingMs: remain,
|
||
lastError: String(configRef.current.autonomous_last_error || '')
|
||
})
|
||
})
|
||
appendProgress('autonomous continue (no tools this turn)')
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
continue
|
||
}
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
break
|
||
}
|
||
|
||
appendProgress(
|
||
'iteration ' +
|
||
iter +
|
||
' tools ' +
|
||
toolCallsArr
|
||
.map(
|
||
(x) =>
|
||
/** @type {{ function?: { name?: string } }} */ (x).function
|
||
?.name
|
||
)
|
||
.join(',')
|
||
)
|
||
if (reasoningSettings.enabled && reasoningSettings.mode === 'trace') {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\n[process] iteration ' +
|
||
String(iter) +
|
||
' finish_reason=' +
|
||
(finishReason || 'unknown') +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
let sawTodoWrite = false
|
||
for (const tc of toolCallsArr) {
|
||
const fn =
|
||
/** @type {{ id?: string, function?: { name?: string, arguments?: string } }} */ (
|
||
tc
|
||
).function
|
||
const id = /** @type {{ id?: string }} */ (tc).id || ''
|
||
const name = fn?.name || ''
|
||
const argsStr = fn?.arguments || '{}'
|
||
if (name === 'todo_write') sawTodoWrite = true
|
||
if (
|
||
reasoningSettings.enabled &&
|
||
reasoningSettings.mode === 'trace' &&
|
||
reasoningSettings.includeTools
|
||
) {
|
||
const argsPreview =
|
||
argsStr.length > 280 ? argsStr.slice(0, 280) + '…' : argsStr
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[process] tool_call ' +
|
||
name +
|
||
' args=' +
|
||
argsPreview +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('keyword', useColor) +
|
||
'→ ' +
|
||
name +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
|
||
const spinner = bareEditSgr('dim', useColor) + '…' + EDIT_ANSI_RESET
|
||
bareAgentWriteOut(ctx, stdout, spinner + '\r')
|
||
statusLineActive = true
|
||
|
||
let resultStr = await bareAgentDispatchTool({
|
||
ctx,
|
||
toolName: name,
|
||
argsJson: argsStr,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete
|
||
})
|
||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||
try {
|
||
let parsedArgs = {}
|
||
try {
|
||
parsedArgs = JSON.parse(argsStr || '{}')
|
||
} catch {
|
||
parsedArgs = {}
|
||
}
|
||
const post = await bareAgentRunHooks(
|
||
ctx,
|
||
paths.hooks,
|
||
'PostToolUse',
|
||
name,
|
||
parsedArgs,
|
||
resultStr
|
||
)
|
||
if (post && post.inject) {
|
||
resultStr =
|
||
resultStr +
|
||
'\n[hook PostToolUse] ' +
|
||
String(post.inject).slice(0, 800)
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
clearStatusLine()
|
||
|
||
messages.push({
|
||
role: 'tool',
|
||
tool_call_id: id,
|
||
content: resultStr
|
||
})
|
||
if (
|
||
reasoningSettings.enabled &&
|
||
reasoningSettings.mode === 'trace' &&
|
||
reasoningSettings.includeTools
|
||
) {
|
||
const resPreview =
|
||
resultStr.length > 360 ? resultStr.slice(0, 360) + '…' : resultStr
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'[process] tool_result ' +
|
||
name +
|
||
' ' +
|
||
resPreview +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
}
|
||
|
||
if (name === 'enter_plan_mode' || name === 'exit_plan_mode') {
|
||
messages.push({
|
||
role: 'user',
|
||
content:
|
||
name === 'enter_plan_mode'
|
||
? '[harness] PLAN MODE is now ON. Only write ~/.agent/plan.md until exit_plan_mode.'
|
||
: '[harness] PLAN MODE is now OFF. Mutating tools are allowed again.'
|
||
})
|
||
}
|
||
if (name === 'ask_user_question') {
|
||
try {
|
||
const parsed = JSON.parse(resultStr)
|
||
if (parsed && parsed.text) {
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
'\n' +
|
||
bareEditSgr('keyword', useColor) +
|
||
'Questions for you:\n' +
|
||
EDIT_ANSI_RESET +
|
||
String(parsed.text) +
|
||
'\n'
|
||
)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
if (completed && !configRef.current.autonomous_active) break
|
||
}
|
||
|
||
if (sawTodoWrite) turnsSinceTodoWrite = 0
|
||
else turnsSinceTodoWrite++
|
||
if (typeof bareAgentTodoNudgeText === 'function' && typeof bareAgentLoadTodos === 'function') {
|
||
try {
|
||
const todosNow = paths.todos
|
||
? await bareAgentLoadTodos(ctx, paths.todos)
|
||
: []
|
||
const sumNow = bareAgentTodoSummarize(todosNow)
|
||
const nudge = bareAgentTodoNudgeText({
|
||
open: sumNow.open,
|
||
turnsSinceTodoWrite,
|
||
nudgeEnabled: configRef.current.todo_nudge_enabled !== false
|
||
})
|
||
if (nudge) {
|
||
messages.push({
|
||
role: 'user',
|
||
content: '[harness reminder] ' + nudge
|
||
})
|
||
}
|
||
} catch {
|
||
/* optional */
|
||
}
|
||
}
|
||
|
||
await bareAgentSaveHistory(ctx, paths.history, messages)
|
||
if (completed) {
|
||
if (configRef.current.autonomous_active) {
|
||
const gate = await bareAgentRunAutonomousGates({
|
||
ctx,
|
||
paths,
|
||
signal: masterAbort.signal,
|
||
appendProgress,
|
||
home,
|
||
configRef,
|
||
manCacheRef,
|
||
onTaskComplete,
|
||
requiredChecks: autonomousSettings.requiredChecks
|
||
})
|
||
if (!gate.ok) {
|
||
completed = false
|
||
configRef.current.autonomous_status = 'needs_fixups'
|
||
configRef.current.autonomous_last_error =
|
||
'failed_checks:' + gate.failed.join(',')
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous checks failed ' + gate.failed.join(','))
|
||
messages.push({
|
||
role: 'user',
|
||
content:
|
||
'Autonomous completion gates failed for checks: ' +
|
||
gate.failed.join(', ') +
|
||
'. Fix the issues, rerun required checks, and only call task_complete when all pass.'
|
||
})
|
||
continue
|
||
}
|
||
configRef.current.autonomous_active = false
|
||
configRef.current.autonomous_status = 'completed'
|
||
configRef.current.autonomous_last_error = ''
|
||
await bareAgentSaveConfig(ctx, paths, configRef.current)
|
||
appendProgress('autonomous completion gates passed')
|
||
}
|
||
bareAgentWriteOut(
|
||
ctx,
|
||
stdout,
|
||
bareEditSgr('dim', useColor) +
|
||
'\nDone: ' +
|
||
taskSummary +
|
||
EDIT_ANSI_RESET +
|
||
'\n'
|
||
)
|
||
break
|
||
}
|
||
}
|
||
|
||
if (offSigint) offSigint()
|
||
} finally {
|
||
try {
|
||
if (detachThinkKeysSession) detachThinkKeysSession()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
detachThinkKeysSession = null
|
||
try {
|
||
if (prevAbortAgent) ctx.bareOsAbortActiveAgent = prevAbortAgent
|
||
else delete ctx.bareOsAbortActiveAgent
|
||
} catch {
|
||
try {
|
||
delete ctx.bareOsAbortActiveAgent
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (typeof ctx.bareOsQvacCancelActive === 'function') {
|
||
ctx.bareOsQvacCancelActive()
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
clearStatusLine()
|
||
// Ensure cursor is on a fresh line so the shell prompt is visible.
|
||
bareAgentWriteOut(ctx, stdout, '\n')
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
try {
|
||
ctx.resumeReplAfterSubprocess()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Autonomous AI agent: QVAC (local) or OpenAI-compatible REST, ReAct loop, ~/.agent/config.json.
|
||
*/
|
||
async function run(ctx, argv) {
|
||
const argv0 = argv[0] || 'agent'
|
||
const args = argv.slice(1)
|
||
const wantHelp = args.includes('-h') || args.includes('--help')
|
||
if (wantHelp || args.length === 0) {
|
||
ctx.console.log(bareAgentCliUsage(argv0))
|
||
ctx.exitCode = wantHelp ? 0 : 1
|
||
return
|
||
}
|
||
|
||
let setupFlag = false
|
||
let resetFlag = false
|
||
let autoFlag = false
|
||
let statusFlag = false
|
||
let planFlag = false
|
||
let newFlag = false
|
||
let compactFlag = false
|
||
let maxTurns = 0
|
||
let modelOverride = ''
|
||
let systemFile = ''
|
||
/** @type {string[]} */
|
||
const rest = []
|
||
for (let i = 0; i < args.length; i++) {
|
||
const a = args[i]
|
||
if (a === '--setup' || a === '--config') setupFlag = true
|
||
else if (a === '--reset') resetFlag = true
|
||
else if (a === '--auto' || a === '--autonomous') autoFlag = true
|
||
else if (a === '--status') statusFlag = true
|
||
else if (a === '--plan') planFlag = true
|
||
else if (a === '--new') newFlag = true
|
||
else if (a === '--compact') compactFlag = true
|
||
else if (a === '--continue' || a === '-c') {
|
||
/* default: history is always loaded unless --new/--reset */
|
||
} else if (a === '--max-turns' || a === '--max-iterations') {
|
||
maxTurns = Number(args[++i]) || 0
|
||
} else if (a === '--model') {
|
||
modelOverride = String(args[++i] || '').trim()
|
||
} else if (a === '--system' || a === '--system-file') {
|
||
systemFile = String(args[++i] || '').trim()
|
||
} else rest.push(a)
|
||
}
|
||
|
||
const sub = rest[0] || ''
|
||
if (
|
||
!setupFlag &&
|
||
!autoFlag &&
|
||
!statusFlag &&
|
||
!planFlag &&
|
||
!newFlag &&
|
||
!compactFlag &&
|
||
!resetFlag &&
|
||
((rest.length === 1 &&
|
||
(sub === 'status' ||
|
||
sub === 'skills' ||
|
||
sub === 'todos' ||
|
||
sub === 'plan' ||
|
||
sub === 'compact' ||
|
||
sub === 'reset' ||
|
||
sub === 'undo' ||
|
||
sub === 'hooks' ||
|
||
sub === 'history' ||
|
||
sub === 'rewind' ||
|
||
sub === 'export' ||
|
||
sub === 'remember' ||
|
||
sub === 'recap')) ||
|
||
(sub === 'history' && rest[0] === 'history') ||
|
||
(sub === 'rewind' && rest[0] === 'rewind') ||
|
||
(sub === 'export' && rest[0] === 'export') ||
|
||
(sub === 'remember' && rest[0] === 'remember'))
|
||
) {
|
||
if (sub === 'status') statusFlag = true
|
||
else if (sub === 'reset') resetFlag = true
|
||
else if (sub === 'compact') compactFlag = true
|
||
else {
|
||
await bareAgentRunInspectSubcommand(ctx, argv0, sub, rest.slice(1).join(' '))
|
||
return
|
||
}
|
||
}
|
||
|
||
if (statusFlag) {
|
||
await bareAgentPrintStatus(ctx)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
|
||
const wantReset =
|
||
resetFlag || newFlag || (rest.length === 1 && rest[0] === 'reset')
|
||
if (wantReset && !rest.filter((x) => x !== 'reset').length && !setupFlag && !autoFlag && !planFlag) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
await bareAgentResetChatSession(ctx, paths, argv0)
|
||
ctx.exitCode = 0
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
}
|
||
return
|
||
}
|
||
|
||
if (compactFlag && !rest.filter((x) => x !== 'compact').length && !autoFlag && !planFlag && !setupFlag) {
|
||
await bareAgentRunCompactOnly(ctx, argv0)
|
||
return
|
||
}
|
||
|
||
const task = rest
|
||
.filter(function (x) {
|
||
return x !== 'reset' && x !== 'compact'
|
||
})
|
||
.join(' ')
|
||
.trim()
|
||
if (!task && !setupFlag) {
|
||
ctx.console.error(
|
||
argv0 + ': missing task (or use --setup / --config / --auto GOAL / skills / todos / plan)'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (setupFlag && !task) {
|
||
await bareAgentRunSetupOnly(ctx, argv0)
|
||
return
|
||
}
|
||
|
||
if (autoFlag && !task) {
|
||
ctx.console.error(argv0 + ': --auto requires a goal string')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
if (newFlag || resetFlag) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
await bareAgentResetChatSession(ctx, paths, argv0)
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
} else if (compactFlag) {
|
||
await bareAgentRunCompactOnly(ctx, argv0)
|
||
if (ctx.exitCode && ctx.exitCode !== 0) return
|
||
}
|
||
|
||
await bareOsRunAgentSession(ctx, argv0, task, {
|
||
setupFlag,
|
||
autonomous: autoFlag,
|
||
autonomousGoal: autoFlag ? task : '',
|
||
planMode: planFlag,
|
||
maxIterations: maxTurns > 0 ? maxTurns : undefined,
|
||
modelOverride: modelOverride || undefined,
|
||
extraSystemFile: systemFile || undefined
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param {string} argv0
|
||
*/
|
||
function bareAgentCliUsage(argv0) {
|
||
return (
|
||
'usage: ' +
|
||
argv0 +
|
||
' [OPTION]... YOUR_REQUEST_HERE\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --setup | --config\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --status | status\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --reset | reset | --new\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --auto "GOAL"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --plan "design the change"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --compact\n' +
|
||
' ' +
|
||
argv0 +
|
||
' skills | todos | plan | undo | hooks | history | rewind | export | remember | recap\n' +
|
||
'\n' +
|
||
'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' +
|
||
'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' +
|
||
'\n' +
|
||
'Options:\n' +
|
||
' --setup, --config Walk through QVAC or REST setup (only the chosen backend is stored)\n' +
|
||
' --auto, --autonomous Keep the tool loop going until task_complete, stop, or timebox\n' +
|
||
' --plan Start this turn in plan mode (read-only except ~/.agent/plan.md)\n' +
|
||
' --new, --reset Clear ~/.agent/history.json then run the request (or just reset)\n' +
|
||
' --continue, -c Resume history (default)\n' +
|
||
' --compact Compact history now (or compact then run if a task follows)\n' +
|
||
' --max-turns N Cap ReAct iterations for this run\n' +
|
||
' --model NAME Override qvac_model / REST model for this run\n' +
|
||
' --system FILE Append extra instructions from an absolute guest path\n' +
|
||
' --status Print backend, compaction, plan mode, and autonomous run state\n' +
|
||
'\n' +
|
||
'Inspect (no model call):\n' +
|
||
' skills List discovered SKILL.md ids\n' +
|
||
' todos Print session todos\n' +
|
||
' plan Print ~/.agent/plan.md\n' +
|
||
' undo Restore the last file edit snapshot\n' +
|
||
' hooks List ~/.agent/hooks/*.json\n' +
|
||
' history [query] Search or tail chat history\n' +
|
||
' rewind [N] Drop the last N user turns from history (default 1)\n' +
|
||
' export [path] Write a Markdown transcript (default ~/.agent/export.md)\n' +
|
||
' remember TEXT Append a FACT to MEMORY.md\n' +
|
||
' recap Print the last user + assistant pair\n' +
|
||
'\n' +
|
||
'Examples:\n' +
|
||
' ' +
|
||
argv0 +
|
||
' "summarize ~/README and list five files in /bin"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --plan "design a /bin/foo utility"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --auto --max-turns 40 "add /agent tests and run them"\n' +
|
||
' ' +
|
||
argv0 +
|
||
' --new --model QWEN3_1_7B_INST_Q4 "fresh session: inspect /proc"\n' +
|
||
'\n' +
|
||
'See man agent.'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
async function bareAgentPrintStatus(ctx) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
let cfg = loaded && loaded.config ? loaded.config : loaded
|
||
if (typeof bareAgentSanitizeConfigForBackend === 'function') {
|
||
cfg = bareAgentSanitizeConfigForBackend(cfg || {})
|
||
}
|
||
const started = Number(cfg && cfg.autonomous_started_at_ms) || 0
|
||
const maxRt = Number(cfg && cfg.autonomous_max_runtime_ms) || 0
|
||
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
||
const backend =
|
||
typeof bareAgentResolveBackend === 'function'
|
||
? bareAgentResolveBackend(cfg || {})
|
||
: String((cfg && (cfg.backend || cfg.provider)) || 'qvac')
|
||
/** @type {string[]} */
|
||
const lines = ['agent status']
|
||
if (typeof bareAgentFormatConfigSummary === 'function') {
|
||
String(bareAgentFormatConfigSummary(cfg || {}))
|
||
.split('\n')
|
||
.forEach(function (row) {
|
||
lines.push(' ' + row)
|
||
})
|
||
} else {
|
||
lines.push(' backend: ' + backend)
|
||
}
|
||
lines.push(
|
||
' compaction: ' + String((cfg && cfg.context_compaction) || 'auto'),
|
||
' autonomous_enabled: ' + String(Boolean(cfg && cfg.autonomous_mode_enabled)),
|
||
' autonomous_active: ' + String(Boolean(cfg && cfg.autonomous_active)),
|
||
' status: ' + String((cfg && cfg.autonomous_status) || 'idle'),
|
||
' goal: ' + String((cfg && cfg.autonomous_goal) || ''),
|
||
' elapsed_s: ' + String(Math.round(elapsed / 1000)),
|
||
' remaining_s: ' +
|
||
String(maxRt > 0 ? Math.max(0, Math.round((maxRt - elapsed) / 1000)) : 0),
|
||
' last_error: ' + String((cfg && cfg.autonomous_last_error) || ''),
|
||
' plan_mode: ' + String(Boolean(cfg && cfg.plan_mode_active)),
|
||
' access_policy: ' + String((cfg && cfg.access_policy) || 'full'),
|
||
' allow_delete: ' + String(cfg && cfg.allow_delete !== false)
|
||
)
|
||
ctx.console.log(lines.join('\n'))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
* @param {string} sub
|
||
*/
|
||
async function bareAgentRunInspectSubcommand(ctx, argv0, sub, extra) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
if (sub === 'skills') {
|
||
if (typeof bareAgentEnsureSkillTemplates === 'function') {
|
||
const loaded = await bareAgentLoadOrCreateConfig(ctx, paths)
|
||
const cfg = loaded && loaded.config ? loaded.config : loaded
|
||
await bareAgentEnsureSkillTemplates(ctx, paths, cfg || {})
|
||
}
|
||
const block =
|
||
typeof bareAgentSkillsCompactPrompt === 'function'
|
||
? await bareAgentSkillsCompactPrompt(ctx, paths, 8000)
|
||
: ''
|
||
ctx.console.log(block && String(block).trim() ? String(block).trim() : argv0 + ': no skills')
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'todos') {
|
||
const todos =
|
||
typeof bareAgentLoadTodos === 'function'
|
||
? await bareAgentLoadTodos(ctx, paths.todos)
|
||
: []
|
||
const sum =
|
||
typeof bareAgentTodoSummarize === 'function'
|
||
? bareAgentTodoSummarize(todos)
|
||
: { text: '', open: 0, total: 0 }
|
||
ctx.console.log(sum.text || argv0 + ': no todos')
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'plan') {
|
||
const text =
|
||
typeof bareAgentReadTextFile === 'function'
|
||
? await bareAgentReadTextFile(ctx, paths.plan)
|
||
: ''
|
||
ctx.console.log(text && text.trim() ? text : argv0 + ': no plan (' + paths.plan + ')')
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'undo') {
|
||
if (typeof bareAgentPopEdit !== 'function') {
|
||
ctx.console.error(argv0 + ': undo unavailable')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
const rec = await bareAgentPopEdit(ctx, paths.edits)
|
||
if (!rec || !rec.path) {
|
||
ctx.console.log(argv0 + ': nothing to undo')
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || ''))
|
||
ctx.console.log(argv0 + ': restored ' + rec.path)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'hooks') {
|
||
let names = []
|
||
try {
|
||
names = ctx.vfs && typeof ctx.vfs.readdir === 'function'
|
||
? await ctx.vfs.readdir(paths.hooks)
|
||
: []
|
||
} catch {
|
||
names = []
|
||
}
|
||
const json = (Array.isArray(names) ? names : []).filter(function (n) {
|
||
return /\.json$/i.test(String(n || ''))
|
||
})
|
||
ctx.console.log(
|
||
json.length
|
||
? json.map(function (n) {
|
||
return paths.hooks + '/' + n
|
||
}).join('\n')
|
||
: argv0 + ': no hooks in ' + paths.hooks
|
||
)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'history') {
|
||
const q = String(extra || '').trim()
|
||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
if (q && typeof bareAgentHistorySearch === 'function') {
|
||
const hits = bareAgentHistorySearch(messages, q, 12)
|
||
ctx.console.log(
|
||
hits.length
|
||
? hits
|
||
.map(function (h) {
|
||
return '[' + h.role + '] ' + h.snippet
|
||
})
|
||
.join('\n')
|
||
: argv0 + ': no history matches'
|
||
)
|
||
} else {
|
||
ctx.console.log(argv0 + ': ' + String(messages.length) + ' messages in history')
|
||
}
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'rewind') {
|
||
const steps = Math.max(1, Math.floor(Number(extra) || 1))
|
||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
if (typeof bareAgentRewindHistory !== 'function') {
|
||
ctx.console.error(argv0 + ': rewind unavailable')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
const out = bareAgentRewindHistory(messages, { steps })
|
||
if (!out.ok) {
|
||
ctx.console.log(argv0 + ': ' + (out.error || 'nothing to rewind'))
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
await bareAgentSaveHistory(ctx, paths.history, out.messages)
|
||
ctx.console.log(
|
||
argv0 +
|
||
': rewound ' +
|
||
String(out.dropped) +
|
||
' message(s), ' +
|
||
String(out.messages.length) +
|
||
' remain'
|
||
)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'export') {
|
||
const dest = String(extra || '').trim() || paths.dir + '/export.md'
|
||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
const md =
|
||
typeof bareAgentExportTranscript === 'function'
|
||
? bareAgentExportTranscript(messages)
|
||
: ''
|
||
await bareAgentWriteTextFile(ctx, dest, md)
|
||
ctx.console.log(argv0 + ': exported ' + String(messages.length) + ' messages to ' + dest)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'remember') {
|
||
const text = String(extra || '').trim()
|
||
if (!text) {
|
||
ctx.console.error(argv0 + ': remember requires a note')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
const dest = paths.workspace + '/MEMORY.md'
|
||
const line = '- FACT — ' + text.replace(/\s+/g, ' ')
|
||
let prev = ''
|
||
try {
|
||
prev = await bareAgentReadTextFile(ctx, dest)
|
||
} catch {
|
||
prev = ''
|
||
}
|
||
await bareAgentWriteTextFile(ctx, dest, (prev ? prev.replace(/\s*$/, '') + '\n' : '') + line + '\n')
|
||
ctx.console.log(argv0 + ': remembered in ' + dest)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
if (sub === 'recap') {
|
||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
let lastUser = ''
|
||
let lastAsst = ''
|
||
for (let i = messages.length - 1; i >= 0; i--) {
|
||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||
if (!m) continue
|
||
const role = String(m.role || '')
|
||
if (!lastAsst && role === 'assistant') lastAsst = String(m.content || '')
|
||
if (!lastUser && role === 'user') lastUser = String(m.content || '')
|
||
if (lastUser && lastAsst) break
|
||
}
|
||
ctx.console.log(
|
||
lastUser || lastAsst
|
||
? 'USER\n' +
|
||
lastUser.slice(0, 1200) +
|
||
'\n\nASSISTANT\n' +
|
||
lastAsst.slice(0, 2000)
|
||
: argv0 + ': no recap (empty history)'
|
||
)
|
||
ctx.exitCode = 0
|
||
return
|
||
}
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} argv0
|
||
*/
|
||
async function bareAgentRunCompactOnly(ctx, argv0) {
|
||
const home = bareAgentResolveHome(ctx)
|
||
const paths = bareAgentPaths(home)
|
||
try {
|
||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||
if (typeof bareAgentCompactMessagesForCtx !== 'function') {
|
||
ctx.console.error(argv0 + ': compact unavailable')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
const packed = bareAgentCompactMessagesForCtx(messages, 32768, {
|
||
mode: 'aggressive',
|
||
keepRecent: 6,
|
||
toolMaxChars: 1200
|
||
})
|
||
await bareAgentSaveHistory(ctx, paths.history, packed.messages)
|
||
ctx.console.log(
|
||
argv0 +
|
||
': compacted ' +
|
||
String(packed.meta.beforeTokens) +
|
||
'→' +
|
||
String(packed.meta.afterTokens) +
|
||
' tok (dropped_groups=' +
|
||
String(packed.meta.droppedGroups) +
|
||
')'
|
||
)
|
||
ctx.exitCode = 0
|
||
} catch (e) {
|
||
const msg =
|
||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||
ctx.console.error(argv0 + ': ' + msg)
|
||
ctx.exitCode = 1
|
||
}
|
||
}
|