Orginize
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/** 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()
|
||||
@@ -0,0 +1,774 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
||||
/** 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/')))
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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' : '')
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/** 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
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/** 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) }
|
||||
}
|
||||
@@ -0,0 +1,803 @@
|
||||
/** ~/.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 */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/** 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()
|
||||
@@ -0,0 +1,691 @@
|
||||
/**
|
||||
* 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' }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
/** Shared agent tool helpers (preamble for /bin/agent; schemas/dispatch are sibling files). */
|
||||
|
||||
/**
|
||||
* 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"}'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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 {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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,713 @@
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user