Updates to Agent Harness
This commit is contained in:
@@ -43,6 +43,12 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||||
memory_get: 1,
|
||||
memory_append: 1,
|
||||
list_skills: 1,
|
||||
fuzzy_find: 1,
|
||||
wait_for: 1,
|
||||
read_many: 1,
|
||||
git_diff: 1,
|
||||
history_search: 1,
|
||||
list_scheduled: 1,
|
||||
todo_write: 1,
|
||||
enter_plan_mode: 1,
|
||||
exit_plan_mode: 1,
|
||||
@@ -447,37 +453,9 @@ function bareAgentHookDenies(hook, toolName, args) {
|
||||
* @param {Record<string, unknown>} args
|
||||
*/
|
||||
async function bareAgentRunPreToolHooks(ctx, hooksDir, toolName, args) {
|
||||
const vfs = ctx && ctx.vfs
|
||||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
||||
return ''
|
||||
}
|
||||
let names = []
|
||||
try {
|
||||
names = await vfs.readdir(hooksDir)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
if (!Array.isArray(names)) return ''
|
||||
names = names.filter(function (n) {
|
||||
return /\.json$/i.test(String(n || ''))
|
||||
})
|
||||
names.sort()
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
try {
|
||||
const buf = await vfs.readFile(hooksDir + '/' + names[i])
|
||||
if (!buf || !buf.length) continue
|
||||
const t =
|
||||
typeof ctx.b4a !== 'undefined' &&
|
||||
ctx.b4a &&
|
||||
typeof ctx.b4a.toString === 'function'
|
||||
? ctx.b4a.toString(buf)
|
||||
: String(new TextDecoder().decode(buf))
|
||||
const hook = JSON.parse(t)
|
||||
const reason = bareAgentHookDenies(hook, toolName, args)
|
||||
if (reason) return reason
|
||||
} catch {
|
||||
/* ignore bad hook files */
|
||||
}
|
||||
if (typeof bareAgentRunHooks === 'function') {
|
||||
const out = await bareAgentRunHooks(ctx, hooksDir, 'PreToolUse', toolName, args, '')
|
||||
return out.deny || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -620,12 +598,17 @@ async function bareAgentVfsWalkFiles(ctx, root, opts) {
|
||||
skip['.git'] = 1
|
||||
skip['node_modules'] = 1
|
||||
skip['.bare-os'] = 1
|
||||
const rootN = String(root || '/').replace(/\/+$/, '') || '/'
|
||||
const rules =
|
||||
opts && Array.isArray(opts.ignore)
|
||||
? opts.ignore
|
||||
: typeof bareAgentLoadIgnoreRules === 'function'
|
||||
? await bareAgentLoadIgnoreRules(ctx, rootN)
|
||||
: []
|
||||
/** @type {string[]} */
|
||||
const out = []
|
||||
/** @type {{ dir: string, depth: number }[]} */
|
||||
const queue = [
|
||||
{ dir: String(root || '/').replace(/\/+$/, '') || '/', depth: 0 }
|
||||
]
|
||||
const queue = [{ dir: rootN, depth: 0 }]
|
||||
while (queue.length && out.length < maxFiles) {
|
||||
const cur = queue.shift()
|
||||
if (!cur) break
|
||||
@@ -640,7 +623,11 @@ async function bareAgentVfsWalkFiles(ctx, root, opts) {
|
||||
const name = String(names[i] || '')
|
||||
if (!name || name === '.' || name === '..' || skip[name]) continue
|
||||
const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name
|
||||
let rel = full
|
||||
if (rootN !== '/' && full.indexOf(rootN + '/') === 0) rel = full.slice(rootN.length + 1)
|
||||
else if (full.charAt(0) === '/') rel = full.slice(1)
|
||||
const isDir = await bareAgentVfsIsDir(ctx, full)
|
||||
if (rules.length && bareAgentIgnoreMatch(rel, isDir, rules)) continue
|
||||
if (isDir) {
|
||||
if (cur.depth + 1 < maxDepth) queue.push({ dir: full, depth: cur.depth + 1 })
|
||||
} else {
|
||||
@@ -1247,6 +1234,277 @@ function bareAgentParseScheduleInterval(raw) {
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function bareAgentParseIgnoreRules(text) {
|
||||
const lines = String(text || '').split(/\r?\n/)
|
||||
/** @type {{ pattern: string, negate: boolean, dirOnly: boolean }[]} */
|
||||
const rules = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = String(lines[i] || '').trim()
|
||||
if (!line || line.charAt(0) === '#') continue
|
||||
let negate = false
|
||||
if (line.charAt(0) === '!') {
|
||||
negate = true
|
||||
line = line.slice(1)
|
||||
}
|
||||
let dirOnly = false
|
||||
if (line.charAt(line.length - 1) === '/') {
|
||||
dirOnly = true
|
||||
line = line.slice(0, -1)
|
||||
}
|
||||
if (line.charAt(0) === '/') line = line.slice(1)
|
||||
if (!line) continue
|
||||
rules.push({ pattern: line, negate, dirOnly })
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* Last matching gitignore-style rule wins.
|
||||
* @param {string} rel
|
||||
* @param {boolean} isDir
|
||||
* @param {{ pattern: string, negate: boolean, dirOnly: boolean }[]} rules
|
||||
*/
|
||||
function bareAgentIgnoreMatch(rel, isDir, rules) {
|
||||
const n = String(rel || '').replace(/^\/+/, '')
|
||||
if (!n || !Array.isArray(rules) || !rules.length) return false
|
||||
let ignored = false
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i]
|
||||
if (rule.dirOnly && !isDir) continue
|
||||
let hit = false
|
||||
if (rule.pattern.indexOf('/') === -1) {
|
||||
const parts = n.split('/')
|
||||
for (let p = 0; p < parts.length; p++) {
|
||||
if (bareAgentGlobMatch(parts[p], rule.pattern)) {
|
||||
hit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hit = bareAgentGlobMatch(n, rule.pattern) || bareAgentGlobMatch(n, '**/' + rule.pattern)
|
||||
}
|
||||
if (!hit) continue
|
||||
ignored = !rule.negate
|
||||
}
|
||||
return ignored
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} root
|
||||
*/
|
||||
async function bareAgentLoadIgnoreRules(ctx, root) {
|
||||
const names = ['.gitignore', '.agentignore', '.grokignore']
|
||||
/** @type {string[]} */
|
||||
const chunks = []
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const t = await bareAgentReadTextFile(ctx, String(root || '').replace(/\/+$/, '') + '/' + names[i])
|
||||
if (t && t.trim()) chunks.push(t)
|
||||
}
|
||||
return bareAgentParseIgnoreRules(chunks.join('\n'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Grok-style fuzzy score (basename + path subsequence). Higher is better.
|
||||
* @param {string} query
|
||||
* @param {string} path
|
||||
*/
|
||||
function bareAgentFuzzyScore(query, path) {
|
||||
const q = String(query || '').toLowerCase().trim()
|
||||
const p = String(path || '')
|
||||
if (!q || !p) return 0
|
||||
const low = p.toLowerCase()
|
||||
const base = (p.split('/').pop() || p).toLowerCase()
|
||||
if (base === q) return 1000
|
||||
if (base.indexOf(q) === 0) return 800 - Math.min(base.length, 80)
|
||||
if (base.indexOf(q) !== -1) return 600 - base.indexOf(q)
|
||||
if (low.indexOf(q) !== -1) return 400
|
||||
let qi = 0
|
||||
let score = 0
|
||||
let streak = 0
|
||||
for (let i = 0; i < low.length && qi < q.length; i++) {
|
||||
if (low.charAt(i) === q.charAt(qi)) {
|
||||
qi++
|
||||
streak++
|
||||
score += 8 + streak * 4
|
||||
} else streak = 0
|
||||
}
|
||||
if (qi < q.length) return 0
|
||||
if (base.length && q.length / base.length > 0.5) score += 40
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} root
|
||||
* @param {string} query
|
||||
* @param {{ max?: number, maxFiles?: number }} [opts]
|
||||
*/
|
||||
async function bareAgentFuzzyFind(ctx, root, query, opts) {
|
||||
const files = await bareAgentVfsWalkFiles(ctx, root, {
|
||||
maxFiles: Math.min(Math.max(Number(opts && opts.maxFiles) || 600, 40), 2000),
|
||||
maxDepth: 12
|
||||
})
|
||||
const cap = Math.min(Math.max(Number(opts && opts.max) || 20, 1), 80)
|
||||
/** @type {{ path: string, score: number }[]} */
|
||||
const scored = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const score = bareAgentFuzzyScore(query, files[i])
|
||||
if (score <= 0) continue
|
||||
scored.push({ path: files[i], score })
|
||||
}
|
||||
scored.sort(function (a, b) {
|
||||
return b.score - a.score
|
||||
})
|
||||
return scored.slice(0, cap)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} hook
|
||||
* @param {string} event
|
||||
* @param {string} toolName
|
||||
* @param {Record<string, unknown>} args
|
||||
* @param {string} [result]
|
||||
*/
|
||||
function bareAgentHookMatch(hook, event, toolName, args, result) {
|
||||
if (!hook || typeof hook !== 'object') return null
|
||||
const ev = String(hook.event || hook.type || 'PreToolUse')
|
||||
const want = String(event || '')
|
||||
if (ev !== want && ev.toLowerCase() !== want.toLowerCase()) return null
|
||||
const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : []
|
||||
if (tools.length && toolName && tools.indexOf(toolName) === -1) return null
|
||||
const reSrc = String(hook.deny_regex || hook.match_regex || hook.denyRegex || '').trim()
|
||||
if (reSrc) {
|
||||
let re
|
||||
try {
|
||||
re = new RegExp(reSrc, 'i')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const blob =
|
||||
toolName +
|
||||
' ' +
|
||||
String((args && (args.command || args.path || args.file_path || args.pattern)) || '') +
|
||||
' ' +
|
||||
String(result || '').slice(0, 400)
|
||||
if (!re.test(blob)) return null
|
||||
}
|
||||
return hook
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} hooksDir
|
||||
* @param {string} event
|
||||
* @param {string} [toolName]
|
||||
* @param {Record<string, unknown>} [args]
|
||||
* @param {string} [result]
|
||||
*/
|
||||
async function bareAgentRunHooks(ctx, hooksDir, event, toolName, args, result) {
|
||||
const vfs = ctx && ctx.vfs
|
||||
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
||||
return { deny: '', inject: '' }
|
||||
}
|
||||
let names = []
|
||||
try {
|
||||
names = await vfs.readdir(hooksDir)
|
||||
} catch {
|
||||
return { deny: '', inject: '' }
|
||||
}
|
||||
if (!Array.isArray(names)) return { deny: '', inject: '' }
|
||||
names = names.filter(function (n) {
|
||||
return /\.json$/i.test(String(n || ''))
|
||||
})
|
||||
names.sort()
|
||||
/** @type {string[]} */
|
||||
const injects = []
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
try {
|
||||
const t = await bareAgentReadTextFile(ctx, hooksDir + '/' + names[i])
|
||||
if (!t.trim()) continue
|
||||
const hook = JSON.parse(t)
|
||||
const matched = bareAgentHookMatch(hook, event, toolName || '', args || {}, result || '')
|
||||
if (!matched) continue
|
||||
if (event === 'PreToolUse' || event === 'pre') {
|
||||
const reason = String(matched.reason || matched.message || '').trim()
|
||||
if (reason && (matched.deny === true || matched.deny_regex || matched.denyRegex)) {
|
||||
return { deny: reason.slice(0, 240), inject: '' }
|
||||
}
|
||||
if (typeof bareAgentHookDenies === 'function') {
|
||||
const d = bareAgentHookDenies(matched, toolName || '', args || {})
|
||||
if (d) return { deny: d, inject: '' }
|
||||
}
|
||||
}
|
||||
const inj = String(matched.inject || matched.append || '').trim()
|
||||
if (inj) injects.push(inj.slice(0, 800))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return { deny: '', inject: injects.join('\n') }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} editsPath
|
||||
* @param {{ path: string, prev: string, tool?: string }} rec
|
||||
*/
|
||||
async function bareAgentPushEdit(ctx, editsPath, rec) {
|
||||
const prev = await bareAgentReadJsonFile(ctx, editsPath, [])
|
||||
const list = Array.isArray(prev) ? prev : []
|
||||
list.push({
|
||||
path: String(rec.path || ''),
|
||||
prev: String(rec.prev == null ? '' : rec.prev),
|
||||
tool: String(rec.tool || ''),
|
||||
ts: new Date().toISOString()
|
||||
})
|
||||
while (list.length > 20) list.shift()
|
||||
await bareAgentWriteJsonFile(ctx, editsPath, list)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} editsPath
|
||||
*/
|
||||
async function bareAgentPopEdit(ctx, editsPath) {
|
||||
const prev = await bareAgentReadJsonFile(ctx, editsPath, [])
|
||||
const list = Array.isArray(prev) ? prev : []
|
||||
const last = list.pop()
|
||||
await bareAgentWriteJsonFile(ctx, editsPath, list)
|
||||
return last && typeof last === 'object' ? last : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown[]} messages
|
||||
* @param {string} query
|
||||
* @param {number} [max]
|
||||
*/
|
||||
function bareAgentHistorySearch(messages, query, max) {
|
||||
const tokens = bareAgentMemoryTokens(query)
|
||||
const cap = Math.min(Math.max(Number(max) || 8, 1), 24)
|
||||
if (!Array.isArray(messages) || !tokens.length) return []
|
||||
/** @type {{ role: string, score: number, snippet: string }[]} */
|
||||
const hits = []
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||||
if (!m) continue
|
||||
const role = String(m.role || '')
|
||||
if (role !== 'user' && role !== 'assistant') continue
|
||||
const text = String(m.content || '')
|
||||
const score = bareAgentMemoryScore(text, tokens)
|
||||
if (score <= 0) continue
|
||||
hits.push({
|
||||
role,
|
||||
score: Math.round(score * 1000) / 1000,
|
||||
snippet: text.replace(/\s+/g, ' ').trim().slice(0, 280)
|
||||
})
|
||||
}
|
||||
hits.sort(function (a, b) {
|
||||
return b.score - a.score
|
||||
})
|
||||
return hits.slice(0, cap)
|
||||
}
|
||||
|
||||
function bareAgentScheduleId(id) {
|
||||
let s = String(id || '')
|
||||
.trim()
|
||||
|
||||
@@ -42,7 +42,9 @@ function bareAgentPaths(home) {
|
||||
todos: base + '/todos.json',
|
||||
plan: base + '/plan.md',
|
||||
hooks: base + '/hooks',
|
||||
ask: base + '/ask.json'
|
||||
ask: base + '/ask.json',
|
||||
edits: base + '/edits.json',
|
||||
lastCwd: base + '/last_cwd'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,10 @@ function bareAgentToolDefinitions() {
|
||||
type: 'string',
|
||||
description: 'Full command string (e.g. ls -la /bin)'
|
||||
},
|
||||
cwd: {
|
||||
type: 'string',
|
||||
description: 'Working directory (cd there first). Persists as ~/.agent/last_cwd.'
|
||||
},
|
||||
timeout_ms: { type: 'integer' },
|
||||
capture_exit: {
|
||||
type: 'boolean',
|
||||
@@ -1257,6 +1261,107 @@ function bareAgentToolDefinitions() {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'list_scheduled',
|
||||
description:
|
||||
'List guest agent timers (agent-*.timer) under ~/.config/bare-os/timers, with previews.',
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'fuzzy_find',
|
||||
description:
|
||||
'Fuzzy-search file names under a root (Grok-style). Prefer this when you remember part of a filename.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
root: { type: 'string', description: 'Directory to walk (default home)' },
|
||||
max: { type: 'integer', description: 'Default 20, cap 80' }
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'read_many',
|
||||
description:
|
||||
'Read several UTF-8 files in one call (bounded). Prefer over many read_file calls for a small set.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paths: { type: 'array', items: { type: 'string' } },
|
||||
max_bytes_each: { type: 'integer', description: 'Default 32000' }
|
||||
},
|
||||
required: ['paths']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'wait_for',
|
||||
description:
|
||||
'Poll a file or guest command until a regex matches, or timeout (Grok monitor lite; sync).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: { type: 'string', description: 'JS regex' },
|
||||
path: { type: 'string', description: 'File to poll' },
|
||||
command: { type: 'string', description: 'Guest command whose output is polled' },
|
||||
timeout_ms: { type: 'integer', description: 'Default 15000, cap 120000' },
|
||||
interval_ms: { type: 'integer', description: 'Default 400, min 100' },
|
||||
ignore_case: { type: 'boolean' }
|
||||
},
|
||||
required: ['pattern']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'undo_last_edit',
|
||||
description:
|
||||
'Restore the last file snapshot from ~/.agent/edits.json (write_file / edit / apply_patch / delete).',
|
||||
parameters: { type: 'object', properties: {} }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_diff',
|
||||
description: 'git diff (optional path / --stat) via /bin/git. Prefer over raw run_command.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cwd: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
stat: { type: 'boolean', description: 'Only --stat (default false)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'history_search',
|
||||
description: 'Search this session\'s ~/.agent/history.json by keyword.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
max: { type: 'integer' }
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -2037,6 +2142,14 @@ async function bareAgentDispatchTool(o) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
appendProgress('write_file ' + path)
|
||||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||||
try {
|
||||
const prev = await bareAgentReadTextFile(ctx, path)
|
||||
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'write_file' })
|
||||
} catch {
|
||||
/* new file */
|
||||
}
|
||||
}
|
||||
let dir = path.replace(/\/[^/]+$/, '')
|
||||
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
||||
const body =
|
||||
@@ -2062,6 +2175,21 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
appendProgress(toolName + ' ' + path)
|
||||
const buf = await vfs.readFile(path)
|
||||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||||
try {
|
||||
const snap =
|
||||
buf && buf.length
|
||||
? typeof ctx.b4a !== 'undefined' &&
|
||||
ctx.b4a &&
|
||||
typeof ctx.b4a.toString === 'function'
|
||||
? ctx.b4a.toString(buf)
|
||||
: String(new TextDecoder().decode(buf))
|
||||
: ''
|
||||
await bareAgentPushEdit(ctx, paths.edits, { path, prev: snap, tool: toolName })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
let prev =
|
||||
buf && buf.length
|
||||
? typeof ctx.b4a !== 'undefined' &&
|
||||
@@ -2186,10 +2314,30 @@ async function bareAgentDispatchTool(o) {
|
||||
if (!execLine) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
}
|
||||
appendProgress('run_command ' + command.slice(0, 160))
|
||||
const r = await captureExec(command, timeoutMs, { captureExit })
|
||||
let cwd = typeof args.cwd === 'string' ? args.cwd.trim() : ''
|
||||
if (!cwd && paths.lastCwd) {
|
||||
try {
|
||||
cwd = (await bareAgentReadTextFile(ctx, paths.lastCwd)).trim()
|
||||
} catch {
|
||||
cwd = ''
|
||||
}
|
||||
}
|
||||
let line = command
|
||||
if (cwd) {
|
||||
if (!bareAgentPathAllowed(cwd)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'cwd_not_allowed' })
|
||||
}
|
||||
line = 'cd ' + bareAgentShellQuote(cwd) + ' && ' + command
|
||||
try {
|
||||
if (paths.lastCwd) await bareAgentWriteTextFile(ctx, paths.lastCwd, cwd + '\n')
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
appendProgress('run_command ' + line.slice(0, 160))
|
||||
const r = await captureExec(line, timeoutMs, { captureExit })
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||||
r.ok === false ? r : { ok: true, cwd: cwd || null, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2467,6 +2615,14 @@ async function bareAgentDispatchTool(o) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
appendProgress('delete_path ' + path)
|
||||
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
||||
try {
|
||||
const prev = await bareAgentReadTextFile(ctx, path)
|
||||
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'delete_path' })
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
}
|
||||
try {
|
||||
/** @type {unknown} */
|
||||
let st = null
|
||||
@@ -3370,6 +3526,19 @@ async function bareAgentDispatchTool(o) {
|
||||
const parsed = bareAgentParseApplyPatch(patch)
|
||||
if (!parsed.ok) return bareAgentJsonResult(parsed)
|
||||
appendProgress('apply_patch ops=' + String((parsed.ops || []).length))
|
||||
if (typeof bareAgentPushEdit === 'function' && paths.edits && parsed.ops) {
|
||||
for (let i = 0; i < parsed.ops.length; i++) {
|
||||
const op = parsed.ops[i]
|
||||
const p = String((op && op.path) || '')
|
||||
if (!p) continue
|
||||
try {
|
||||
const prev = await bareAgentReadTextFile(ctx, p)
|
||||
await bareAgentPushEdit(ctx, paths.edits, { path: p, prev, tool: 'apply_patch' })
|
||||
} catch {
|
||||
/* new file */
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = await bareAgentApplyPatchOps(ctx, parsed.ops, {
|
||||
home,
|
||||
denyPrefixes: mutateDenyPrefixes
|
||||
@@ -3647,6 +3816,166 @@ async function bareAgentDispatchTool(o) {
|
||||
return bareAgentJsonResult({ ok: true, id, path: dest })
|
||||
}
|
||||
|
||||
if (toolName === 'list_scheduled') {
|
||||
const dir = (home || '/home') + '/.config/bare-os/timers'
|
||||
let names = []
|
||||
try {
|
||||
names = vfs && typeof vfs.readdir === 'function' ? await vfs.readdir(dir) : []
|
||||
} catch {
|
||||
names = []
|
||||
}
|
||||
const rows = []
|
||||
const list = Array.isArray(names) ? names : []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const name = String(list[i] || '')
|
||||
if (!name.endsWith('.timer') || name.indexOf('agent-') !== 0) continue
|
||||
const path = dir + '/' + name
|
||||
const preview = await bareAgentReadTextFile(ctx, path)
|
||||
rows.push({ id: name.replace(/\.timer$/, ''), path, preview: preview.slice(0, 400) })
|
||||
}
|
||||
appendProgress('list_scheduled ' + String(rows.length))
|
||||
return bareAgentJsonResult({ ok: true, count: rows.length, timers: rows })
|
||||
}
|
||||
|
||||
if (toolName === 'fuzzy_find') {
|
||||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||||
const root =
|
||||
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
||||
if (typeof bareAgentFuzzyFind !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'fuzzy_unavailable' })
|
||||
}
|
||||
appendProgress('fuzzy_find ' + query)
|
||||
const hits = await bareAgentFuzzyFind(ctx, root, query, { max: args.max })
|
||||
return bareAgentJsonResult({ ok: true, query, root, hits })
|
||||
}
|
||||
|
||||
if (toolName === 'read_many') {
|
||||
const pathsIn = Array.isArray(args.paths) ? args.paths : []
|
||||
if (!pathsIn.length) return bareAgentJsonResult({ ok: false, error: 'paths_required' })
|
||||
const maxB =
|
||||
typeof args.max_bytes_each === 'number' && Number.isFinite(args.max_bytes_each)
|
||||
? Math.min(Math.max(Math.floor(args.max_bytes_each), 200), 200000)
|
||||
: 32000
|
||||
const files = []
|
||||
for (let i = 0; i < pathsIn.length && i < 16; i++) {
|
||||
const path = String(pathsIn[i] || '').trim()
|
||||
if (!path || !bareAgentPathAllowed(path)) {
|
||||
files.push({ path, ok: false, error: 'path_not_allowed' })
|
||||
continue
|
||||
}
|
||||
let text = await bareAgentReadTextFile(ctx, path)
|
||||
const truncated = text.length > maxB
|
||||
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
|
||||
files.push({ path, ok: true, content: text, truncated })
|
||||
}
|
||||
appendProgress('read_many ' + String(files.length))
|
||||
return bareAgentJsonResult({ ok: true, files })
|
||||
}
|
||||
|
||||
if (toolName === 'wait_for') {
|
||||
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
||||
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
||||
let re
|
||||
try {
|
||||
re = new RegExp(pattern, args.ignore_case ? 'i' : '')
|
||||
} catch (e) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'bad_regex' })
|
||||
}
|
||||
const timeoutMs = Math.min(
|
||||
120000,
|
||||
Math.max(200, Math.floor(Number(args.timeout_ms) || 15000))
|
||||
)
|
||||
const intervalMs = Math.min(
|
||||
5000,
|
||||
Math.max(100, Math.floor(Number(args.interval_ms) || 400))
|
||||
)
|
||||
const path = typeof args.path === 'string' ? args.path.trim() : ''
|
||||
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
||||
if (!path && !command) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_or_command_required' })
|
||||
}
|
||||
appendProgress('wait_for ' + pattern)
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let last = ''
|
||||
while (Date.now() <= deadline) {
|
||||
if (path) last = await bareAgentReadTextFile(ctx, path)
|
||||
else if (command && execLine) {
|
||||
const r = await captureExec(command, Math.min(intervalMs + 2000, 15000))
|
||||
last = r && r.stdout_stderr ? String(r.stdout_stderr) : ''
|
||||
}
|
||||
re.lastIndex = 0
|
||||
if (re.test(last)) {
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
matched: true,
|
||||
elapsed_ms: timeoutMs - Math.max(0, deadline - Date.now()),
|
||||
sample: last.slice(0, 800)
|
||||
})
|
||||
}
|
||||
if (Date.now() + intervalMs > deadline) break
|
||||
await new Promise(function (resolve) {
|
||||
setTimeout(resolve, intervalMs)
|
||||
})
|
||||
}
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'wait_timeout',
|
||||
sample: last.slice(0, 400)
|
||||
})
|
||||
}
|
||||
|
||||
if (toolName === 'undo_last_edit') {
|
||||
const editsPath = paths.edits || paths.dir + '/edits.json'
|
||||
if (typeof bareAgentPopEdit !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'undo_unavailable' })
|
||||
}
|
||||
const rec = await bareAgentPopEdit(ctx, editsPath)
|
||||
if (!rec || !rec.path) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'nothing_to_undo' })
|
||||
}
|
||||
await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || ''))
|
||||
appendProgress('undo_last_edit ' + rec.path)
|
||||
return bareAgentJsonResult({ ok: true, path: rec.path, tool: rec.tool || null })
|
||||
}
|
||||
|
||||
if (toolName === 'git_diff') {
|
||||
const cwd =
|
||||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||||
(ctx.env && typeof ctx.env === 'object'
|
||||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||||
: '') ||
|
||||
home ||
|
||||
'/home'
|
||||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||||
const stat = Boolean(args.stat)
|
||||
const cmd =
|
||||
'git -C ' +
|
||||
bareAgentShellQuote(cwd) +
|
||||
' diff' +
|
||||
(stat ? ' --stat' : '') +
|
||||
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||||
appendProgress('git_diff ' + cwd)
|
||||
const r = await captureExec(cmd, 30000)
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'history_search') {
|
||||
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
||||
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
||||
const histPath = paths.history || paths.dir + '/history.json'
|
||||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||||
const hits =
|
||||
typeof bareAgentHistorySearch === 'function'
|
||||
? bareAgentHistorySearch(Array.isArray(raw) ? raw : [], query, args.max)
|
||||
: []
|
||||
appendProgress('history_search ' + query)
|
||||
return bareAgentJsonResult({ ok: true, query, hits })
|
||||
}
|
||||
|
||||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
|
||||
@@ -284,8 +284,8 @@ TOOL CALLING. Prefer specialized tools over bash: grep not run_command grep; rea
|
||||
COMMUNICATION. Write for a reader who has not seen tool calls. Lead with the answer. Define project terms on first use. State facts literally. The final message must stand alone. Do not invent acronyms.
|
||||
|
||||
TOOL MAP (schemas are already attached — use them):
|
||||
- Files: read_file, write_file, edit_file, search_replace, apply_patch, create_directory, list_directory, file_stat, glob_files, grep, search_files, move_path, delete_path, list_bin
|
||||
- Code / harness: run_command, run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, list_skills, read_skill, edit_agent_config, git_status, schedule_task, unschedule_task
|
||||
- Files: read_file, read_many, write_file, edit_file, search_replace, apply_patch, undo_last_edit, create_directory, list_directory, file_stat, glob_files, fuzzy_find, grep, search_files, move_path, delete_path, list_bin
|
||||
- Code / harness: run_command (optional cwd), run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, list_skills, read_skill, edit_agent_config, git_status, git_diff, history_search, schedule_task, unschedule_task, list_scheduled, wait_for
|
||||
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
|
||||
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
|
||||
- Docs: read_man_page, apropos_man (documentation search only)
|
||||
@@ -1431,6 +1431,17 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
if (instructions)
|
||||
systemContent +=
|
||||
'\n\n## Session notes\n' + instructions.slice(0, instructionsBudget)
|
||||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||||
try {
|
||||
const startHook = await bareAgentRunHooks(ctx, paths.hooks, 'SessionStart', '', {}, '')
|
||||
if (startHook && startHook.inject) {
|
||||
systemContent +=
|
||||
'\n\n## SessionStart hook\n' + String(startHook.inject).slice(0, 1500)
|
||||
}
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
if (runOpts && runOpts.extraSystemFile) {
|
||||
try {
|
||||
const extraPath = String(runOpts.extraSystemFile).trim()
|
||||
@@ -2443,7 +2454,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
bareAgentWriteOut(ctx, stdout, spinner + '\r')
|
||||
statusLineActive = true
|
||||
|
||||
const resultStr = await bareAgentDispatchTool({
|
||||
let resultStr = await bareAgentDispatchTool({
|
||||
ctx,
|
||||
toolName: name,
|
||||
argsJson: argsStr,
|
||||
@@ -2455,6 +2466,32 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
manCacheRef,
|
||||
onTaskComplete
|
||||
})
|
||||
if (typeof bareAgentRunHooks === 'function' && paths.hooks) {
|
||||
try {
|
||||
let parsedArgs = {}
|
||||
try {
|
||||
parsedArgs = JSON.parse(argsStr || '{}')
|
||||
} catch {
|
||||
parsedArgs = {}
|
||||
}
|
||||
const post = await bareAgentRunHooks(
|
||||
ctx,
|
||||
paths.hooks,
|
||||
'PostToolUse',
|
||||
name,
|
||||
parsedArgs,
|
||||
resultStr
|
||||
)
|
||||
if (post && post.inject) {
|
||||
resultStr =
|
||||
resultStr +
|
||||
'\n[hook PostToolUse] ' +
|
||||
String(post.inject).slice(0, 800)
|
||||
}
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
|
||||
clearStatusLine()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"agent --status",
|
||||
"agent --plan REQUEST...",
|
||||
"agent --auto GOAL...",
|
||||
"agent skills | todos | plan | compact | reset"
|
||||
"agent skills | todos | plan | compact | reset | undo | hooks | history"
|
||||
],
|
||||
"description": "Runs the in-guest ReAct coding agent (QVAC local or OpenAI-compatible REST). Full guest admin by default (denylist). Tools include grep, apply_patch, web_search, git_status, todos, plan mode, and skills. History lives in ~/.agent/history.json.",
|
||||
"options": [
|
||||
|
||||
@@ -19,14 +19,21 @@ Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find`
|
||||
|
||||
## Code / harness
|
||||
|
||||
- `run_command` — any guest shell line (`command_deny` empty by default).
|
||||
- `run_command` — any guest shell line (`cwd` optional; last cwd persisted). `command_deny` empty by default.
|
||||
- `run_js_script` — required for agent-authored JS. Node is not installed. Writes `~/.agent/_tmp_agent_run.mjs` and runs it on the Bare kernel. Prefer `async function run(ctx, argv)`.
|
||||
- `run_js_script_at_path` — existing absolute `.mjs`.
|
||||
- `todo_write` — session todos (`merge=true` to update by id).
|
||||
- `enter_plan_mode` / `exit_plan_mode` — plan mode is read-only except `~/.agent/plan.md`.
|
||||
- `memory_search` / `memory_get` / `memory_append` — MEMORY.md and daily logs.
|
||||
- `list_skills` / `read_skill` — compact catalog then full SKILL.md.
|
||||
- `schedule_task` / `unschedule_task` — guest timers (`~/.config/bare-os/timers/agent-*.timer`).
|
||||
- `schedule_task` / `unschedule_task` / `list_scheduled` — guest timers (`agent-*.timer`).
|
||||
- `fuzzy_find` — filename search when you remember part of a name.
|
||||
- `read_many` — several files in one call.
|
||||
- `wait_for` — poll a file or command until a regex matches (sync, timeout).
|
||||
- `undo_last_edit` — restore the last snapshot from `~/.agent/edits.json`.
|
||||
- `git_diff` — `git diff` / `--stat`.
|
||||
- `history_search` — keyword search of this session's history.
|
||||
- glob/grep honor `.gitignore`, `.agentignore`, and `.grokignore` at the walk root.
|
||||
- `edit_agent_config` — shallow merge of known `~/.agent/config.json` keys.
|
||||
|
||||
## Kernel / ops
|
||||
|
||||
@@ -52,19 +52,23 @@ async function run(ctx, argv) {
|
||||
!newFlag &&
|
||||
!compactFlag &&
|
||||
!resetFlag &&
|
||||
rest.length === 1 &&
|
||||
(sub === 'status' ||
|
||||
sub === 'skills' ||
|
||||
sub === 'todos' ||
|
||||
sub === 'plan' ||
|
||||
sub === 'compact' ||
|
||||
sub === 'reset')
|
||||
((rest.length === 1 &&
|
||||
(sub === 'status' ||
|
||||
sub === 'skills' ||
|
||||
sub === 'todos' ||
|
||||
sub === 'plan' ||
|
||||
sub === 'compact' ||
|
||||
sub === 'reset' ||
|
||||
sub === 'undo' ||
|
||||
sub === 'hooks' ||
|
||||
sub === 'history')) ||
|
||||
(sub === 'history' && rest[0] === 'history'))
|
||||
) {
|
||||
if (sub === 'status') statusFlag = true
|
||||
else if (sub === 'reset') resetFlag = true
|
||||
else if (sub === 'compact') compactFlag = true
|
||||
else {
|
||||
await bareAgentRunInspectSubcommand(ctx, argv0, sub)
|
||||
await bareAgentRunInspectSubcommand(ctx, argv0, sub, rest.slice(1).join(' '))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -178,7 +182,7 @@ function bareAgentCliUsage(argv0) {
|
||||
' --compact\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' skills | todos | plan\n' +
|
||||
' skills | todos | plan | undo | hooks | history\n' +
|
||||
'\n' +
|
||||
'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' +
|
||||
'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' +
|
||||
@@ -199,6 +203,9 @@ function bareAgentCliUsage(argv0) {
|
||||
' skills List discovered SKILL.md ids\n' +
|
||||
' todos Print session todos\n' +
|
||||
' plan Print ~/.agent/plan.md\n' +
|
||||
' undo Restore the last file edit snapshot\n' +
|
||||
' hooks List ~/.agent/hooks/*.json\n' +
|
||||
' history [query] Search or tail chat history\n' +
|
||||
'\n' +
|
||||
'Examples:\n' +
|
||||
' ' +
|
||||
@@ -269,7 +276,7 @@ async function bareAgentPrintStatus(ctx) {
|
||||
* @param {string} argv0
|
||||
* @param {string} sub
|
||||
*/
|
||||
async function bareAgentRunInspectSubcommand(ctx, argv0, sub) {
|
||||
async function bareAgentRunInspectSubcommand(ctx, argv0, sub, extra) {
|
||||
const home = bareAgentResolveHome(ctx)
|
||||
const paths = bareAgentPaths(home)
|
||||
try {
|
||||
@@ -309,6 +316,65 @@ async function bareAgentRunInspectSubcommand(ctx, argv0, sub) {
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'undo') {
|
||||
if (typeof bareAgentPopEdit !== 'function') {
|
||||
ctx.console.error(argv0 + ': undo unavailable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const rec = await bareAgentPopEdit(ctx, paths.edits)
|
||||
if (!rec || !rec.path) {
|
||||
ctx.console.log(argv0 + ': nothing to undo')
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || ''))
|
||||
ctx.console.log(argv0 + ': restored ' + rec.path)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'hooks') {
|
||||
let names = []
|
||||
try {
|
||||
names = ctx.vfs && typeof ctx.vfs.readdir === 'function'
|
||||
? await ctx.vfs.readdir(paths.hooks)
|
||||
: []
|
||||
} catch {
|
||||
names = []
|
||||
}
|
||||
const json = (Array.isArray(names) ? names : []).filter(function (n) {
|
||||
return /\.json$/i.test(String(n || ''))
|
||||
})
|
||||
ctx.console.log(
|
||||
json.length
|
||||
? json.map(function (n) {
|
||||
return paths.hooks + '/' + n
|
||||
}).join('\n')
|
||||
: argv0 + ': no hooks in ' + paths.hooks
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'history') {
|
||||
const q = String(extra || '').trim()
|
||||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||||
if (q && typeof bareAgentHistorySearch === 'function') {
|
||||
const hits = bareAgentHistorySearch(messages, q, 12)
|
||||
ctx.console.log(
|
||||
hits.length
|
||||
? hits
|
||||
.map(function (h) {
|
||||
return '[' + h.role + '] ' + h.snippet
|
||||
})
|
||||
.join('\n')
|
||||
: argv0 + ': no history matches'
|
||||
)
|
||||
} else {
|
||||
ctx.console.log(argv0 + ': ' + String(messages.length) + ' messages in history')
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
|
||||
@@ -149,7 +149,14 @@ test('agent-tools exposes agent ops tools', async (t) => {
|
||||
'memory_append',
|
||||
'list_skills',
|
||||
'schedule_task',
|
||||
'unschedule_task'
|
||||
'unschedule_task',
|
||||
'list_scheduled',
|
||||
'fuzzy_find',
|
||||
'read_many',
|
||||
'wait_for',
|
||||
'undo_last_edit',
|
||||
'git_diff',
|
||||
'history_search'
|
||||
]) {
|
||||
t.ok(
|
||||
TOOLS.includes("name: '" + toolName + "'"),
|
||||
@@ -210,6 +217,9 @@ test('agent CLI exposes production flags and inspect subcommands', async (t) =>
|
||||
t.ok(CLI.includes('--compact'))
|
||||
t.ok(CLI.includes("sub === 'skills'"))
|
||||
t.ok(CLI.includes("sub === 'todos'"))
|
||||
t.ok(CLI.includes("sub === 'undo'"))
|
||||
t.ok(CLI.includes("sub === 'hooks'"))
|
||||
t.ok(CLI.includes("sub === 'history'"))
|
||||
t.ok(TUI.includes('planMode'))
|
||||
t.ok(TUI.includes('modelOverride'))
|
||||
t.ok(TUI.includes('extraSystemFile'))
|
||||
|
||||
@@ -535,6 +535,82 @@ test('schedule interval parse and memory_append', async (t) => {
|
||||
t.ok(String(files.get(sched.path) || '').includes('agent --auto'))
|
||||
})
|
||||
|
||||
test('gitignore, fuzzy, undo, history search', async (t) => {
|
||||
const s = loadPort()
|
||||
const rules = s.bareAgentParseIgnoreRules('*.log\n!keep.log\nbuild/\n')
|
||||
t.ok(s.bareAgentIgnoreMatch('foo.log', false, rules))
|
||||
t.absent(s.bareAgentIgnoreMatch('keep.log', false, rules))
|
||||
t.ok(s.bareAgentIgnoreMatch('build/out.js', true, rules) || s.bareAgentIgnoreMatch('build', true, rules))
|
||||
t.ok(s.bareAgentFuzzyScore('readme', '/home/x/README.md') > s.bareAgentFuzzyScore('readme', '/home/x/a.js'))
|
||||
const hits = s.bareAgentHistorySearch(
|
||||
[
|
||||
{ role: 'user', content: 'fix the holesail tunnel' },
|
||||
{ role: 'assistant', content: 'ok' }
|
||||
],
|
||||
'holesail',
|
||||
4
|
||||
)
|
||||
t.ok(hits.length >= 1)
|
||||
t.is(hits[0].role, 'user')
|
||||
|
||||
const d = loadDispatch()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
'/home/guest/src/keep.js': 'const keep = 1\n',
|
||||
'/home/guest/src/skip.log': 'noise\n',
|
||||
'/home/guest/.gitignore': '*.log\n'
|
||||
})
|
||||
await vfs.mkdir('/home/guest/src')
|
||||
await vfs.mkdir('/home/guest/.agent')
|
||||
const ctx = { vfs, b4a }
|
||||
const paths = {
|
||||
dir: '/home/guest/.agent',
|
||||
edits: '/home/guest/.agent/edits.json'
|
||||
}
|
||||
const globbed = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'glob_files',
|
||||
args: { pattern: '**/*', root: '/home/guest' },
|
||||
configRef: { current: {} },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(globbed.ok)
|
||||
t.ok(globbed.files.some((p) => /keep\.js$/.test(p)))
|
||||
t.absent(globbed.files.some((p) => /skip\.log$/.test(p)))
|
||||
|
||||
const wrote = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'write_file',
|
||||
args: { path: '/home/guest/src/keep.js', content: 'changed\n' },
|
||||
configRef: { current: {} },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(wrote.ok)
|
||||
t.is(files.get('/home/guest/src/keep.js'), 'changed\n')
|
||||
const undone = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'undo_last_edit',
|
||||
args: {},
|
||||
configRef: { current: {} },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(undone.ok)
|
||||
t.ok(String(files.get('/home/guest/src/keep.js') || '').includes('const keep'))
|
||||
|
||||
const waited = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'wait_for',
|
||||
args: { path: '/home/guest/src/keep.js', pattern: 'keep', timeout_ms: 500, interval_ms: 100 },
|
||||
configRef: { current: {} },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(waited.ok)
|
||||
t.ok(waited.matched)
|
||||
})
|
||||
|
||||
test('search result parser extracts DDG-style topics', async (t) => {
|
||||
const s = loadPort()
|
||||
const rows = s.bareAgentParseSearchResults(
|
||||
|
||||
Reference in New Issue
Block a user