Further Agent updates
This commit is contained in:
@@ -47,8 +47,15 @@ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({
|
||||
wait_for: 1,
|
||||
read_many: 1,
|
||||
git_diff: 1,
|
||||
git_log: 1,
|
||||
git_show: 1,
|
||||
git_blame: 1,
|
||||
history_search: 1,
|
||||
list_scheduled: 1,
|
||||
find_symbol: 1,
|
||||
diff_files: 1,
|
||||
export_session: 1,
|
||||
remember: 1,
|
||||
todo_write: 1,
|
||||
enter_plan_mode: 1,
|
||||
exit_plan_mode: 1,
|
||||
@@ -808,7 +815,9 @@ async function bareAgentGrepFiles(ctx, opts) {
|
||||
200,
|
||||
Math.max(1, Math.floor(Number(o.max_matches) || 50))
|
||||
)
|
||||
const filesOnly = Boolean(o.files_with_matches)
|
||||
const mode = String(o.output_mode || '').toLowerCase()
|
||||
const filesOnly = Boolean(o.files_with_matches) || mode === 'files_with_matches'
|
||||
const countOnly = Boolean(o.count) || mode === 'count'
|
||||
const glob = typeof o.glob === 'string' && o.glob.trim() ? o.glob.trim() : ''
|
||||
const files = glob
|
||||
? await bareAgentGlobFiles(ctx, root, glob, { maxFiles: 800, maxDepth: 12 })
|
||||
@@ -817,6 +826,8 @@ async function bareAgentGrepFiles(ctx, opts) {
|
||||
const matches = []
|
||||
/** @type {string[]} */
|
||||
const filesHit = []
|
||||
/** @type {{ path: string, count: number }[]} */
|
||||
const counts = []
|
||||
let truncated = false
|
||||
for (let f = 0; f < files.length; f++) {
|
||||
const path = files[f]
|
||||
@@ -825,11 +836,13 @@ async function bareAgentGrepFiles(ctx, opts) {
|
||||
const lines = text.split('\n')
|
||||
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
||||
let fileHit = false
|
||||
let fileCount = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
re.lastIndex = 0
|
||||
if (!re.test(lines[i])) continue
|
||||
fileHit = true
|
||||
if (filesOnly) break
|
||||
fileCount++
|
||||
if (filesOnly || countOnly) continue
|
||||
const start = Math.max(0, i - before)
|
||||
const end = Math.min(lines.length, i + 1 + after)
|
||||
const slice = lines.slice(start, end)
|
||||
@@ -856,6 +869,13 @@ async function bareAgentGrepFiles(ctx, opts) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (fileHit && countOnly) {
|
||||
counts.push({ path, count: fileCount })
|
||||
if (counts.length >= maxMatches) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (truncated) break
|
||||
}
|
||||
if (filesOnly) {
|
||||
@@ -864,16 +884,32 @@ async function bareAgentGrepFiles(ctx, opts) {
|
||||
pattern,
|
||||
root,
|
||||
glob: glob || null,
|
||||
output_mode: 'files_with_matches',
|
||||
files_with_matches: filesHit,
|
||||
count: filesHit.length,
|
||||
truncated
|
||||
}
|
||||
}
|
||||
if (countOnly) {
|
||||
let total = 0
|
||||
for (let i = 0; i < counts.length; i++) total += counts[i].count
|
||||
return {
|
||||
ok: true,
|
||||
pattern,
|
||||
root,
|
||||
glob: glob || null,
|
||||
output_mode: 'count',
|
||||
files: counts,
|
||||
count: total,
|
||||
truncated
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
pattern,
|
||||
root,
|
||||
glob: glob || null,
|
||||
output_mode: 'content',
|
||||
matches,
|
||||
count: matches.length,
|
||||
truncated
|
||||
@@ -1515,3 +1551,434 @@ function bareAgentScheduleId(id) {
|
||||
if (s.indexOf('agent-') !== 0) s = 'agent-' + s
|
||||
return s.slice(0, 40)
|
||||
}
|
||||
|
||||
/**
|
||||
* User-turn rewind points (Grok /rewind). Index 0 is the first user message.
|
||||
* @param {unknown[]} messages
|
||||
*/
|
||||
function bareAgentRewindPoints(messages) {
|
||||
/** @type {{ userIndex: number, messageIndex: number, preview: string }[]} */
|
||||
const points = []
|
||||
if (!Array.isArray(messages)) return points
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||||
if (!m || String(m.role || '') !== 'user') continue
|
||||
points.push({
|
||||
userIndex: points.length,
|
||||
messageIndex: i,
|
||||
preview: String(m.content || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 140)
|
||||
})
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the last N user turns (and everything after that user message).
|
||||
* userIndex rewinds to that user turn (keeps messages before it).
|
||||
* keep_user keeps the target user message so the prompt can be retried.
|
||||
* @param {unknown[]} messages
|
||||
* @param {{ steps?: number, userIndex?: number, keep_user?: boolean }} [opts]
|
||||
*/
|
||||
function bareAgentRewindHistory(messages, opts) {
|
||||
const list = Array.isArray(messages) ? messages.slice() : []
|
||||
const points = bareAgentRewindPoints(list)
|
||||
if (!points.length) return { ok: false, error: 'nothing_to_rewind', messages: list, dropped: 0 }
|
||||
const o = opts && typeof opts === 'object' ? opts : {}
|
||||
let target
|
||||
if (o.userIndex != null && Number.isFinite(Number(o.userIndex))) {
|
||||
target = points[Math.floor(Number(o.userIndex))]
|
||||
if (!target) return { ok: false, error: 'bad_user_index', messages: list, dropped: 0 }
|
||||
} else {
|
||||
const steps = Math.min(points.length, Math.max(1, Math.floor(Number(o.steps) || 1)))
|
||||
target = points[points.length - steps]
|
||||
}
|
||||
const keepUser = Boolean(o.keep_user)
|
||||
const cut = keepUser ? target.messageIndex : target.messageIndex - 1
|
||||
const next = cut < 0 ? [] : list.slice(0, cut + 1)
|
||||
return {
|
||||
ok: true,
|
||||
messages: next,
|
||||
dropped: list.length - next.length,
|
||||
target: target,
|
||||
keep_user: keepUser
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown transcript (Grok /export).
|
||||
* @param {unknown[]} messages
|
||||
*/
|
||||
function bareAgentExportTranscript(messages) {
|
||||
const lines = [
|
||||
'# Agent session export',
|
||||
'',
|
||||
'Exported: ' + new Date().toISOString(),
|
||||
''
|
||||
]
|
||||
const list = Array.isArray(messages) ? messages : []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const m = list[i] && typeof list[i] === 'object' ? list[i] : {}
|
||||
const role = String(m.role || 'unknown')
|
||||
let content = String(m.content || '')
|
||||
if (role === 'tool' && content.length > 1200) content = content.slice(0, 1200) + '\n… truncated'
|
||||
const calls = Array.isArray(m.tool_calls) ? m.tool_calls : []
|
||||
const names = []
|
||||
for (let c = 0; c < calls.length; c++) {
|
||||
const fn = calls[c] && calls[c].function ? calls[c].function : {}
|
||||
if (fn && fn.name) names.push(String(fn.name))
|
||||
}
|
||||
lines.push('## ' + String(i + 1) + '. ' + role)
|
||||
lines.push('')
|
||||
if (names.length) lines.push('tools: ' + names.join(', '))
|
||||
lines.push(content || '(empty)')
|
||||
lines.push('')
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-based unified diff (Grok-style file compare, no git required).
|
||||
* @param {string} oldText
|
||||
* @param {string} newText
|
||||
* @param {{ from?: string, to?: string, context?: number }} [opts]
|
||||
*/
|
||||
function bareAgentUnifiedDiff(oldText, newText, opts) {
|
||||
const pathA = String((opts && opts.from) || 'a')
|
||||
const pathB = String((opts && opts.to) || 'b')
|
||||
const ctxN = Math.min(8, Math.max(0, Math.floor(Number(opts && opts.context) || 3)))
|
||||
const a = String(oldText || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
const b = String(newText || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
if (a.length && a[a.length - 1] === '') a.pop()
|
||||
if (b.length && b[b.length - 1] === '') b.pop()
|
||||
if (a.join('\n') === b.join('\n')) {
|
||||
return { ok: true, identical: true, text: '', added: 0, removed: 0 }
|
||||
}
|
||||
const maxLines = 800
|
||||
if (a.length > maxLines || b.length > maxLines) {
|
||||
return {
|
||||
ok: true,
|
||||
truncated: true,
|
||||
identical: false,
|
||||
text:
|
||||
'--- ' +
|
||||
pathA +
|
||||
'\n+++ ' +
|
||||
pathB +
|
||||
'\n@@ files too large for inline LCS diff (' +
|
||||
a.length +
|
||||
'/' +
|
||||
b.length +
|
||||
' lines) @@\n',
|
||||
added: 0,
|
||||
removed: 0
|
||||
}
|
||||
}
|
||||
const n = a.length
|
||||
const m = b.length
|
||||
/** @type {number[][]} */
|
||||
const dp = new Array(n + 1)
|
||||
for (let i = 0; i <= n; i++) {
|
||||
dp[i] = new Array(m + 1)
|
||||
for (let j = 0; j <= m; j++) dp[i][j] = 0
|
||||
}
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1] + 1
|
||||
: dp[i - 1][j] >= dp[i][j - 1]
|
||||
? dp[i - 1][j]
|
||||
: dp[i][j - 1]
|
||||
}
|
||||
}
|
||||
/** @type {{ op: string, line: string }[]} */
|
||||
const ops = []
|
||||
let i = n
|
||||
let j = m
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
||||
ops.push({ op: ' ', line: a[i - 1] })
|
||||
i--
|
||||
j--
|
||||
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||
ops.push({ op: '+', line: b[j - 1] })
|
||||
j--
|
||||
} else {
|
||||
ops.push({ op: '-', line: a[i - 1] })
|
||||
i--
|
||||
}
|
||||
}
|
||||
ops.reverse()
|
||||
let added = 0
|
||||
let removed = 0
|
||||
for (let k = 0; k < ops.length; k++) {
|
||||
if (ops[k].op === '+') added++
|
||||
else if (ops[k].op === '-') removed++
|
||||
}
|
||||
const lines = ['--- ' + pathA, '+++ ' + pathB]
|
||||
let idx = 0
|
||||
while (idx < ops.length) {
|
||||
while (idx < ops.length && ops[idx].op === ' ') idx++
|
||||
if (idx >= ops.length) break
|
||||
let start = Math.max(0, idx - ctxN)
|
||||
let end = idx
|
||||
while (end < ops.length) {
|
||||
if (ops[end].op !== ' ') {
|
||||
end++
|
||||
continue
|
||||
}
|
||||
let run = 0
|
||||
let p = end
|
||||
while (p < ops.length && ops[p].op === ' ') {
|
||||
run++
|
||||
p++
|
||||
}
|
||||
if (run > ctxN * 2) {
|
||||
end += ctxN
|
||||
break
|
||||
}
|
||||
end = p
|
||||
}
|
||||
end = Math.min(ops.length, end)
|
||||
let oldLine = 1
|
||||
let newLine = 1
|
||||
for (let k = 0; k < start; k++) {
|
||||
if (ops[k].op !== '+') oldLine++
|
||||
if (ops[k].op !== '-') newLine++
|
||||
}
|
||||
let oldCount = 0
|
||||
let newCount = 0
|
||||
for (let k = start; k < end; k++) {
|
||||
if (ops[k].op !== '+') oldCount++
|
||||
if (ops[k].op !== '-') newCount++
|
||||
}
|
||||
lines.push(
|
||||
'@@ -' +
|
||||
String(oldLine) +
|
||||
',' +
|
||||
String(oldCount) +
|
||||
' +' +
|
||||
String(newLine) +
|
||||
',' +
|
||||
String(newCount) +
|
||||
' @@'
|
||||
)
|
||||
for (let k = start; k < end; k++) {
|
||||
lines.push(ops[k].op + ops[k].line)
|
||||
}
|
||||
idx = end
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
identical: false,
|
||||
truncated: false,
|
||||
text: lines.join('\n') + '\n',
|
||||
added,
|
||||
removed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition-oriented regex for a symbol name (JS / Python / Rust / C-like).
|
||||
* @param {string} name
|
||||
*/
|
||||
function bareAgentSymbolRegex(name) {
|
||||
const esc = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
if (!esc) return ''
|
||||
return (
|
||||
'(?:(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s+' +
|
||||
esc +
|
||||
'\\b|(?:export\\s+)?(?:default\\s+)?class\\s+' +
|
||||
esc +
|
||||
'\\b|(?:export\\s+)?(?:const|let|var)\\s+' +
|
||||
esc +
|
||||
'\\b|def\\s+' +
|
||||
esc +
|
||||
'\\s*\\(|fn\\s+' +
|
||||
esc +
|
||||
'\\b|' +
|
||||
esc +
|
||||
'\\s*=\\s*(?:async\\s+)?(?:function|\\(|class))'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} root
|
||||
* @param {string} name
|
||||
* @param {{ glob?: string, max?: number }} [opts]
|
||||
*/
|
||||
async function bareAgentFindSymbol(ctx, root, name, opts) {
|
||||
const pattern = bareAgentSymbolRegex(name)
|
||||
if (!pattern) return { ok: false, error: 'name_required' }
|
||||
const out = await bareAgentGrepFiles(ctx, {
|
||||
pattern,
|
||||
root,
|
||||
glob: opts && opts.glob,
|
||||
max_matches: opts && opts.max ? opts.max : 40
|
||||
})
|
||||
if (!out || out.ok === false) return out
|
||||
return {
|
||||
ok: true,
|
||||
name,
|
||||
root,
|
||||
matches: out.matches || [],
|
||||
count: out.count || 0,
|
||||
truncated: Boolean(out.truncated)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grok list_dir-style BFS tree (bounded).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} root
|
||||
* @param {{ max?: number, maxDepth?: number }} [opts]
|
||||
*/
|
||||
async function bareAgentRenderTree(ctx, root, opts) {
|
||||
const vfs = ctx && ctx.vfs
|
||||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||||
return { ok: false, error: 'readdir unavailable' }
|
||||
}
|
||||
const base = String(root || '/').replace(/\/+$/, '') || '/'
|
||||
const maxItems = Math.min(Math.max(Number(opts && opts.max) || 200, 20), 800)
|
||||
const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 6, 1), 12)
|
||||
const skip = Object.create(null)
|
||||
skip['.git'] = 1
|
||||
skip['node_modules'] = 1
|
||||
skip['.bare-os'] = 1
|
||||
/** @type {string[]} */
|
||||
const lines = [base + '/']
|
||||
/** @type {{ dir: string, prefix: string, depth: number }[]} */
|
||||
const queue = [{ dir: base, prefix: '', depth: 0 }]
|
||||
let count = 1
|
||||
let truncated = false
|
||||
while (queue.length && count < maxItems) {
|
||||
const cur = queue.shift()
|
||||
if (!cur) break
|
||||
let names = []
|
||||
try {
|
||||
names = await vfs.readdir(cur.dir)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!Array.isArray(names)) continue
|
||||
names = names
|
||||
.map(function (n) {
|
||||
return String(n || '')
|
||||
})
|
||||
.filter(function (n) {
|
||||
return n && n !== '.' && n !== '..' && !skip[n]
|
||||
})
|
||||
.sort()
|
||||
for (let i = 0; i < names.length && count < maxItems; i++) {
|
||||
const name = names[i]
|
||||
const full = (cur.dir === '/' ? '' : cur.dir) + '/' + name
|
||||
const isDir = await bareAgentVfsIsDir(ctx, full)
|
||||
const last = i === names.length - 1
|
||||
const branch = last ? '`-- ' : '|-- '
|
||||
lines.push(cur.prefix + branch + name + (isDir ? '/' : ''))
|
||||
count++
|
||||
if (isDir && cur.depth + 1 < maxDepth) {
|
||||
queue.push({
|
||||
dir: full,
|
||||
prefix: cur.prefix + (last ? ' ' : '| '),
|
||||
depth: cur.depth + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
if (count >= maxItems) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return { ok: true, path: base, tree: lines.join('\n'), count, truncated }
|
||||
}
|
||||
|
||||
/**
|
||||
* VFS copy (file or directory). Dest parents are created.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} from
|
||||
* @param {string} to
|
||||
*/
|
||||
async function bareAgentCopyPath(ctx, from, to) {
|
||||
const src = String(from || '').replace(/\/+$/, '')
|
||||
let dest = String(to || '')
|
||||
if (!src || !dest) return { ok: false, error: 'from_and_to_required' }
|
||||
const srcIsDir = await bareAgentVfsIsDir(ctx, src)
|
||||
const destIsDir = await bareAgentVfsIsDir(ctx, dest.replace(/\/+$/, ''))
|
||||
if (destIsDir) {
|
||||
const base = src.split('/').pop() || 'copy'
|
||||
dest = dest.replace(/\/+$/, '') + '/' + base
|
||||
}
|
||||
if (!srcIsDir) {
|
||||
const text = await bareAgentReadTextFile(ctx, src)
|
||||
await bareAgentWriteTextFile(ctx, dest, text)
|
||||
return { ok: true, from: src, to: dest, kind: 'file' }
|
||||
}
|
||||
const files = await bareAgentVfsWalkFiles(ctx, src, { maxFiles: 400, maxDepth: 12 })
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const rel = files[i].slice(src.length)
|
||||
const text = await bareAgentReadTextFile(ctx, files[i])
|
||||
await bareAgentWriteTextFile(ctx, dest + rel, text)
|
||||
}
|
||||
return { ok: true, from: src, to: dest, kind: 'directory', files: files.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* SKILL.md body with YAML frontmatter (Grok skill format).
|
||||
* @param {{ name: string, description: string, body?: string }} spec
|
||||
*/
|
||||
function bareAgentSkillMarkdown(spec) {
|
||||
const name = String((spec && spec.name) || '').trim() || 'skill'
|
||||
const description = String((spec && spec.description) || '').trim() || name
|
||||
const body = String((spec && spec.body) || '').trim() || '# ' + name + '\n'
|
||||
return (
|
||||
'---\nname: ' +
|
||||
name.replace(/\n/g, ' ') +
|
||||
'\ndescription: ' +
|
||||
description.replace(/\n/g, ' ') +
|
||||
'\n---\n\n' +
|
||||
body +
|
||||
'\n'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk-up Grok/Claude/Cursor skill roots (project-local).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} startDir
|
||||
*/
|
||||
async function bareAgentDiscoverProjectSkillRoots(ctx, startDir) {
|
||||
const vfs = ctx && ctx.vfs
|
||||
if (!vfs || typeof vfs.readdir !== 'function') return []
|
||||
/** @type {{ path: string, source: string }[]} */
|
||||
const roots = []
|
||||
const seen = Object.create(null)
|
||||
let dir = String(startDir || '').replace(/\/+$/, '') || '/'
|
||||
const suffixes = ['.grok/skills', '.agents/skills', '.claude/skills', '.cursor/skills']
|
||||
for (let hop = 0; hop < 12; hop++) {
|
||||
for (let i = 0; i < suffixes.length; i++) {
|
||||
const p = (dir === '/' ? '' : dir) + '/' + suffixes[i]
|
||||
if (seen[p]) continue
|
||||
seen[p] = 1
|
||||
try {
|
||||
const names = await vfs.readdir(p)
|
||||
if (Array.isArray(names) && names.length) roots.push({ path: p, source: 'project' })
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
}
|
||||
if (dir === '/') break
|
||||
const parent = dir.replace(/\/[^/]+$/, '') || '/'
|
||||
if (parent === dir) break
|
||||
dir = parent
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
@@ -102,6 +102,22 @@ async function bareAgentDiscoverSkills(ctx, paths) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -117,7 +133,7 @@ async function bareAgentSkillsCompactPrompt(ctx, paths, maxChars) {
|
||||
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/`) when names match.\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'
|
||||
|
||||
@@ -281,7 +281,12 @@ function bareAgentToolDefinitions() {
|
||||
include_stat: {
|
||||
type: 'boolean',
|
||||
description: 'If true, call stat on each entry (slower; default false)'
|
||||
}
|
||||
},
|
||||
tree: {
|
||||
type: 'boolean',
|
||||
description: 'Grok-style bounded BFS tree instead of a flat listing'
|
||||
},
|
||||
max_depth: { type: 'integer', description: 'Tree depth (default 6)' }
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
@@ -1117,6 +1122,10 @@ function bareAgentToolDefinitions() {
|
||||
files_with_matches: {
|
||||
type: 'boolean',
|
||||
description: 'Return matching paths only'
|
||||
},
|
||||
output_mode: {
|
||||
type: 'string',
|
||||
description: 'content (default) | files_with_matches | count'
|
||||
}
|
||||
},
|
||||
required: ['pattern']
|
||||
@@ -1362,6 +1371,168 @@ function bareAgentToolDefinitions() {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_log',
|
||||
description:
|
||||
'git log --oneline (optional -N / path) via /bin/git. Prefer over raw run_command.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cwd: { type: 'string' },
|
||||
max: { type: 'integer', description: 'Default 20, cap 80' },
|
||||
path: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_show',
|
||||
description: 'git show <rev> (optional path / --stat) via /bin/git.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cwd: { type: 'string' },
|
||||
rev: { type: 'string', description: 'Commit-ish (default HEAD)' },
|
||||
path: { type: 'string' },
|
||||
stat: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'git_blame',
|
||||
description: 'git blame a file via /bin/git.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cwd: { type: 'string' },
|
||||
path: { type: 'string' }
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'copy_path',
|
||||
description:
|
||||
'Copy a file or directory on the VFS (parents created). Prefer this over run_command cp.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_path: { type: 'string' },
|
||||
to_path: { type: 'string' }
|
||||
},
|
||||
required: ['from_path', 'to_path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'diff_files',
|
||||
description:
|
||||
'Unified diff of two UTF-8 files (no git). Prefer this when comparing two paths that are not a git hunk.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_path: { type: 'string' },
|
||||
to_path: { type: 'string' },
|
||||
context: { type: 'integer', description: 'Context lines (default 3)' }
|
||||
},
|
||||
required: ['from_path', 'to_path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'find_symbol',
|
||||
description:
|
||||
'Find likely definitions of a symbol (function/class/const/def/fn) under a root. Prefer over ad-hoc grep for go-to-definition.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
root: { type: 'string' },
|
||||
glob: { type: 'string' },
|
||||
max: { type: 'integer' }
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_skill',
|
||||
description:
|
||||
'Write a Grok-style SKILL.md (YAML frontmatter + body) under ~/.agent/workspace/skills/<id>/.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Folder name (kebab-case)' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
body: { type: 'string', description: 'Markdown instructions after frontmatter' }
|
||||
},
|
||||
required: ['id', 'description']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'remember',
|
||||
description:
|
||||
'Save a durable FACT to MEMORY.md now (Grok /remember). Same store as memory_append.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string' },
|
||||
daily: { type: 'boolean' }
|
||||
},
|
||||
required: ['text']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'rewind_session',
|
||||
description:
|
||||
'Grok /rewind: drop the last N user turns from ~/.agent/history.json (and everything after). Default 1 turn.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
steps: { type: 'integer', description: 'How many user turns to drop (default 1)' },
|
||||
user_index: { type: 'integer', description: 'Rewind to this 0-based user turn instead' },
|
||||
keep_user: { type: 'boolean', description: 'Keep the target user prompt' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'export_session',
|
||||
description:
|
||||
'Grok /export: write the current chat history as Markdown. Default ~/.agent/export.md.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -1484,6 +1655,13 @@ async function bareAgentDispatchTool(o) {
|
||||
if (toolName === 'ripgrep') {
|
||||
return bareAgentDispatchTool({ ...o, toolName: 'grep' })
|
||||
}
|
||||
if (toolName === 'remember') {
|
||||
return bareAgentDispatchTool({
|
||||
...o,
|
||||
toolName: 'memory_append',
|
||||
argsJson: JSON.stringify({ ...args, kind: 'FACT' })
|
||||
})
|
||||
}
|
||||
|
||||
const vfs = ctx.vfs
|
||||
const mutateDenyPrefixes = (function () {
|
||||
@@ -2487,6 +2665,14 @@ async function bareAgentDispatchTool(o) {
|
||||
if (!bareAgentPathAllowed(dir)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (Boolean(args.tree) && typeof bareAgentRenderTree === 'function') {
|
||||
appendProgress('list_directory tree ' + dir)
|
||||
const tree = await bareAgentRenderTree(ctx, dir, {
|
||||
max: maxEnt,
|
||||
maxDepth: args.max_depth
|
||||
})
|
||||
return bareAgentJsonResult(tree)
|
||||
}
|
||||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||||
}
|
||||
@@ -3509,7 +3695,8 @@ async function bareAgentDispatchTool(o) {
|
||||
after: args.after,
|
||||
context: args.context,
|
||||
max_matches: args.max_matches,
|
||||
files_with_matches: Boolean(args.files_with_matches)
|
||||
files_with_matches: Boolean(args.files_with_matches),
|
||||
output_mode: typeof args.output_mode === 'string' ? args.output_mode : ''
|
||||
})
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
@@ -3976,6 +4163,170 @@ async function bareAgentDispatchTool(o) {
|
||||
return bareAgentJsonResult({ ok: true, query, hits })
|
||||
}
|
||||
|
||||
if (toolName === 'git_log' || toolName === 'git_show' || toolName === 'git_blame') {
|
||||
const cwd =
|
||||
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
||||
(ctx.env && typeof ctx.env === 'object'
|
||||
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
||||
: '') ||
|
||||
home ||
|
||||
'/home'
|
||||
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
const quoted = bareAgentShellQuote(cwd)
|
||||
let cmd = 'git -C ' + quoted + ' '
|
||||
if (toolName === 'git_log') {
|
||||
const max = Math.min(80, Math.max(1, Math.floor(Number(args.max) || 20)))
|
||||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||||
cmd += 'log --oneline -' + String(max) + (p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||||
} else if (toolName === 'git_show') {
|
||||
const rev = typeof args.rev === 'string' && args.rev.trim() ? args.rev.trim() : 'HEAD'
|
||||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||||
cmd +=
|
||||
'show ' +
|
||||
(args.stat ? '--stat ' : '') +
|
||||
bareAgentShellQuote(rev) +
|
||||
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
||||
} else {
|
||||
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
||||
if (!p) return bareAgentJsonResult({ ok: false, error: 'path_required' })
|
||||
cmd += 'blame -- ' + bareAgentShellQuote(p)
|
||||
}
|
||||
appendProgress(toolName + ' ' + cwd)
|
||||
const r = await captureExec(cmd, 30000)
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'copy_path') {
|
||||
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
||||
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
||||
if (
|
||||
!from ||
|
||||
!to ||
|
||||
from.includes('..') ||
|
||||
to.includes('..') ||
|
||||
!bareAgentPathAllowed(from) ||
|
||||
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes)
|
||||
) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (typeof bareAgentCopyPath !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'copy_unavailable' })
|
||||
}
|
||||
appendProgress('copy_path ' + from)
|
||||
const out = await bareAgentCopyPath(ctx, from, to)
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
|
||||
if (toolName === 'diff_files') {
|
||||
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
||||
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
||||
if (!from || !to || !bareAgentPathAllowed(from) || !bareAgentPathAllowed(to)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (typeof bareAgentUnifiedDiff !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'diff_unavailable' })
|
||||
}
|
||||
const a = await bareAgentReadTextFile(ctx, from)
|
||||
const b = await bareAgentReadTextFile(ctx, to)
|
||||
appendProgress('diff_files ' + from)
|
||||
const out = bareAgentUnifiedDiff(a, b, {
|
||||
from,
|
||||
to,
|
||||
context: args.context
|
||||
})
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
|
||||
if (toolName === 'find_symbol') {
|
||||
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
||||
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
||||
const root =
|
||||
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
||||
if (!bareAgentPathAllowed(root) || typeof bareAgentFindSymbol !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'find_symbol_unavailable' })
|
||||
}
|
||||
appendProgress('find_symbol ' + name)
|
||||
const out = await bareAgentFindSymbol(ctx, root, name, {
|
||||
glob: typeof args.glob === 'string' ? args.glob : '',
|
||||
max: args.max
|
||||
})
|
||||
return bareAgentJsonResult(out)
|
||||
}
|
||||
|
||||
if (toolName === 'create_skill') {
|
||||
const id = String(args.id || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
|
||||
const destRoot =
|
||||
(typeof paths.workspaceSkills === 'string' && paths.workspaceSkills) ||
|
||||
(typeof paths.workspace === 'string' ? paths.workspace + '/skills' : paths.dir + '/workspace/skills')
|
||||
const dest = destRoot + '/' + id + '/SKILL.md'
|
||||
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
const md =
|
||||
typeof bareAgentSkillMarkdown === 'function'
|
||||
? bareAgentSkillMarkdown({
|
||||
name: typeof args.name === 'string' ? args.name : id,
|
||||
description: typeof args.description === 'string' ? args.description : id,
|
||||
body: typeof args.body === 'string' ? args.body : ''
|
||||
})
|
||||
: String(args.body || '')
|
||||
await bareAgentWriteTextFile(ctx, dest, md)
|
||||
appendProgress('create_skill ' + id)
|
||||
return bareAgentJsonResult({ ok: true, id, path: dest })
|
||||
}
|
||||
|
||||
if (toolName === 'rewind_session') {
|
||||
const histPath = paths.history || paths.dir + '/history.json'
|
||||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||||
if (typeof bareAgentRewindHistory !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'rewind_unavailable' })
|
||||
}
|
||||
const out = bareAgentRewindHistory(Array.isArray(raw) ? raw : [], {
|
||||
steps: args.steps,
|
||||
userIndex: args.user_index,
|
||||
keep_user: args.keep_user
|
||||
})
|
||||
if (!out.ok) return bareAgentJsonResult(out)
|
||||
await bareAgentSaveHistory(ctx, histPath, out.messages)
|
||||
appendProgress('rewind_session dropped=' + String(out.dropped))
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
dropped: out.dropped,
|
||||
remaining: out.messages.length,
|
||||
target: out.target || null,
|
||||
keep_user: Boolean(out.keep_user)
|
||||
})
|
||||
}
|
||||
|
||||
if (toolName === 'export_session') {
|
||||
const histPath = paths.history || paths.dir + '/history.json'
|
||||
const dest =
|
||||
(typeof args.path === 'string' && args.path.trim()) ||
|
||||
(paths.dir ? paths.dir + '/export.md' : '/tmp/agent-export.md')
|
||||
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
||||
const md =
|
||||
typeof bareAgentExportTranscript === 'function'
|
||||
? bareAgentExportTranscript(Array.isArray(raw) ? raw : [])
|
||||
: ''
|
||||
await bareAgentWriteTextFile(ctx, dest, md)
|
||||
appendProgress('export_session ' + dest)
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
path: dest,
|
||||
messages: Array.isArray(raw) ? raw.length : 0
|
||||
})
|
||||
}
|
||||
|
||||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
|
||||
@@ -267,7 +267,7 @@ ACCESS (denylist, not allowlist). You already have full guest admin. NEVER ASK w
|
||||
- ask_user_question is only for a real product choice the user must make. Never use it (or chat) to request permission.
|
||||
|
||||
CODING LOOP. Multi-step work: todo_write (merge=true). Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that plan file. Do not stop after a plan-only reply — keep calling tools until verified.
|
||||
1. Discover: glob_files, grep (preferred over search_files / run_command grep), list_directory, read_file, memory_search / memory_get, web_search then web_fetch, git_status, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md from cwd is already injected when present.
|
||||
1. Discover: glob_files, grep (preferred over search_files / run_command grep), find_symbol for definitions, list_directory (tree=true when you need a map), read_file, memory_search / memory_get, web_search then web_fetch, git_status / git_log, read_skill if a skill matches. Read before you edit. Walk-up AGENTS.md and .grok/skills from cwd are already injected when present.
|
||||
2. Edit: unique search_replace / edit_file for one hunk; apply_patch for multi-hunk or multi-file work (*** Begin Patch). Create with write_file / create_directory. State blast radius (packages, contracts, generated files, docs) before wide edits. Match surrounding style. No placeholders. Comments only for non-obvious constraints.
|
||||
3. Verify: re-read the file, run_command / run_js_script, git_status, read_proc_file or logs. Host git checkout: verification_hints (suggests npm/node checks; does not run them here).
|
||||
4. Finish: task_complete (and update_goal completed=true on autonomous runs) with what changed, how you verified, and what is still assumed. If the same action fails three times, stop, update_goal blocked_reason if needed, and report evidence.
|
||||
@@ -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, 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
|
||||
- Files: read_file, read_many, write_file, edit_file, search_replace, apply_patch, undo_last_edit, create_directory, list_directory (tree=true for BFS), file_stat, glob_files, fuzzy_find, grep (output_mode content|files_with_matches|count), search_files, move_path, copy_path, diff_files, delete_path, find_symbol, list_bin
|
||||
- Code / harness: run_command (optional cwd), run_js_script, run_js_script_at_path, todo_write, enter_plan_mode, exit_plan_mode, memory_search, memory_get, memory_append, remember, list_skills, read_skill, create_skill, edit_agent_config, git_status, git_diff, git_log, git_show, git_blame, history_search, rewind_session, export_session, schedule_task, unschedule_task, list_scheduled, wait_for
|
||||
- Kernel / ops: read_proc_file, runtime_diagnostic_bundle, get_system_info, get_resource_limits, get_swarm_peers, list_services, service_status, list_timers, read_cron_log, read_audit_log, read_boot_policy, read_kernel_extension_resolution, get_initd_graph, read_unit_journal, inspect_ipc_backpressure, get_network_summary, tail_telemetry_streams, pkg_index_lookup
|
||||
- Checks: list_verification_scripts, run_maintenance_gate, run_contract_checks, summarize_build_drift, verification_hints
|
||||
- Docs: read_man_page, apropos_man (documentation search only)
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"agent --status",
|
||||
"agent --plan REQUEST...",
|
||||
"agent --auto GOAL...",
|
||||
"agent skills | todos | plan | compact | reset | undo | hooks | history"
|
||||
"agent skills | todos | plan | compact | reset | undo | hooks | history | rewind | export | remember | recap"
|
||||
],
|
||||
"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.",
|
||||
"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 helpers, find_symbol, session rewind/export, todos, plan mode, and skills. History lives in ~/.agent/history.json.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "--setup, --config",
|
||||
|
||||
@@ -12,7 +12,7 @@ Workspace files are already in the system prompt. Skills index is already attach
|
||||
|
||||
## Coding loop
|
||||
|
||||
1. Discover — glob_files, grep, list_directory, read_file (offset/limit), git_status, web_search. Read before you edit.
|
||||
1. Discover — glob_files, grep, find_symbol, list_directory (tree=true), read_file (offset/limit), git_status / git_log, web_search. Read before you edit.
|
||||
2. Track — todo_write (merge=true) for multi-step work. Large unknown surface: enter_plan_mode, write ~/.agent/plan.md, exit_plan_mode, then implement. Plan mode is read-only except that file.
|
||||
3. Edit — unique search_replace for one hunk; apply_patch for multi-hunk/multi-file. write_file to create or overwrite. Match surrounding style. No placeholders.
|
||||
4. Verify — re-read, run_command / run_js_script, read_proc_file or logs. Host checkout: verification_hints (does not run npm here).
|
||||
|
||||
@@ -8,12 +8,14 @@ You already have these tools. Schemas are attached. NEVER ASK to use them.
|
||||
- `write_file` — create or overwrite (parents created).
|
||||
- `edit_file` / `search_replace` — unique `old_string` unless `replace_all`.
|
||||
- `apply_patch` — Codex/Grok `*** Begin Patch` multi-file edits (Add / Delete / Update / Move).
|
||||
- `create_directory`, `list_directory`, `file_stat`, `glob_files`.
|
||||
- `grep` — VFS-native regex search (glob, ignore_case, context). Prefer over `search_files` / `run_command grep`.
|
||||
- `create_directory`, `list_directory` (`tree=true` for a bounded BFS tree), `file_stat`, `glob_files`.
|
||||
- `grep` — VFS-native regex search (glob, ignore_case, context, `output_mode` content | files_with_matches | count). Prefer over `search_files` / `run_command grep`.
|
||||
- `find_symbol` — definition-oriented search (function/class/const/def/fn).
|
||||
- `search_files` — `grep -Rnl` via the guest shell (legacy).
|
||||
- `move_path`, `delete_path` — enabled. Base system is read-only.
|
||||
- `move_path`, `copy_path`, `diff_files`, `delete_path` — enabled. Base system is read-only.
|
||||
- `list_bin` — guest `/bin` utilities (POSIX-in-JS, not GNU).
|
||||
- `git_status` — `git status --short`, `diff --stat`, recent log.
|
||||
- `git_log` / `git_show` / `git_blame` — prefer these over raw `run_command git`.
|
||||
|
||||
Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find` / shell grep.
|
||||
|
||||
@@ -24,8 +26,9 @@ Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find`
|
||||
- `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.
|
||||
- `memory_search` / `memory_get` / `memory_append` / `remember` — MEMORY.md and daily logs.
|
||||
- `list_skills` / `read_skill` / `create_skill` — catalog, full SKILL.md, or author a new workspace skill.
|
||||
- Project skills are also walked from `.grok/skills`, `.agents/skills`, `.claude/skills`, `.cursor/skills`.
|
||||
- `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.
|
||||
@@ -33,6 +36,8 @@ Prefer `list_directory` / `glob_files` / `file_stat` / `grep` over `ls` / `find`
|
||||
- `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.
|
||||
- `rewind_session` — Grok `/rewind`: drop the last N user turns from `~/.agent/history.json`.
|
||||
- `export_session` — Grok `/export`: write a Markdown transcript (default `~/.agent/export.md`).
|
||||
- glob/grep honor `.gitignore`, `.agentignore`, and `.grokignore` at the walk root.
|
||||
- `edit_agent_config` — shallow merge of known `~/.agent/config.json` keys.
|
||||
|
||||
|
||||
@@ -61,8 +61,15 @@ async function run(ctx, argv) {
|
||||
sub === 'reset' ||
|
||||
sub === 'undo' ||
|
||||
sub === 'hooks' ||
|
||||
sub === 'history')) ||
|
||||
(sub === 'history' && rest[0] === 'history'))
|
||||
sub === 'history' ||
|
||||
sub === 'rewind' ||
|
||||
sub === 'export' ||
|
||||
sub === 'remember' ||
|
||||
sub === 'recap')) ||
|
||||
(sub === 'history' && rest[0] === 'history') ||
|
||||
(sub === 'rewind' && rest[0] === 'rewind') ||
|
||||
(sub === 'export' && rest[0] === 'export') ||
|
||||
(sub === 'remember' && rest[0] === 'remember'))
|
||||
) {
|
||||
if (sub === 'status') statusFlag = true
|
||||
else if (sub === 'reset') resetFlag = true
|
||||
@@ -182,7 +189,7 @@ function bareAgentCliUsage(argv0) {
|
||||
' --compact\n' +
|
||||
' ' +
|
||||
argv0 +
|
||||
' skills | todos | plan | undo | hooks | history\n' +
|
||||
' skills | todos | plan | undo | hooks | history | rewind | export | remember | recap\n' +
|
||||
'\n' +
|
||||
'Production coding/OS agent. Default backend is QVAC (local); or any OpenAI-compatible REST API.\n' +
|
||||
'Config: ~/.agent/config.json. Full guest admin (denylist). NEVER ASK — it just works.\n' +
|
||||
@@ -206,6 +213,10 @@ function bareAgentCliUsage(argv0) {
|
||||
' undo Restore the last file edit snapshot\n' +
|
||||
' hooks List ~/.agent/hooks/*.json\n' +
|
||||
' history [query] Search or tail chat history\n' +
|
||||
' rewind [N] Drop the last N user turns from history (default 1)\n' +
|
||||
' export [path] Write a Markdown transcript (default ~/.agent/export.md)\n' +
|
||||
' remember TEXT Append a FACT to MEMORY.md\n' +
|
||||
' recap Print the last user + assistant pair\n' +
|
||||
'\n' +
|
||||
'Examples:\n' +
|
||||
' ' +
|
||||
@@ -375,6 +386,87 @@ async function bareAgentRunInspectSubcommand(ctx, argv0, sub, extra) {
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'rewind') {
|
||||
const steps = Math.max(1, Math.floor(Number(extra) || 1))
|
||||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||||
if (typeof bareAgentRewindHistory !== 'function') {
|
||||
ctx.console.error(argv0 + ': rewind unavailable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const out = bareAgentRewindHistory(messages, { steps })
|
||||
if (!out.ok) {
|
||||
ctx.console.log(argv0 + ': ' + (out.error || 'nothing to rewind'))
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
await bareAgentSaveHistory(ctx, paths.history, out.messages)
|
||||
ctx.console.log(
|
||||
argv0 +
|
||||
': rewound ' +
|
||||
String(out.dropped) +
|
||||
' message(s), ' +
|
||||
String(out.messages.length) +
|
||||
' remain'
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'export') {
|
||||
const dest = String(extra || '').trim() || paths.dir + '/export.md'
|
||||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||||
const md =
|
||||
typeof bareAgentExportTranscript === 'function'
|
||||
? bareAgentExportTranscript(messages)
|
||||
: ''
|
||||
await bareAgentWriteTextFile(ctx, dest, md)
|
||||
ctx.console.log(argv0 + ': exported ' + String(messages.length) + ' messages to ' + dest)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'remember') {
|
||||
const text = String(extra || '').trim()
|
||||
if (!text) {
|
||||
ctx.console.error(argv0 + ': remember requires a note')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const dest = paths.workspace + '/MEMORY.md'
|
||||
const line = '- FACT — ' + text.replace(/\s+/g, ' ')
|
||||
let prev = ''
|
||||
try {
|
||||
prev = await bareAgentReadTextFile(ctx, dest)
|
||||
} catch {
|
||||
prev = ''
|
||||
}
|
||||
await bareAgentWriteTextFile(ctx, dest, (prev ? prev.replace(/\s*$/, '') + '\n' : '') + line + '\n')
|
||||
ctx.console.log(argv0 + ': remembered in ' + dest)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (sub === 'recap') {
|
||||
const messages = await bareAgentLoadHistory(ctx, paths.history)
|
||||
let lastUser = ''
|
||||
let lastAsst = ''
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const m = messages[i] && typeof messages[i] === 'object' ? messages[i] : null
|
||||
if (!m) continue
|
||||
const role = String(m.role || '')
|
||||
if (!lastAsst && role === 'assistant') lastAsst = String(m.content || '')
|
||||
if (!lastUser && role === 'user') lastUser = String(m.content || '')
|
||||
if (lastUser && lastAsst) break
|
||||
}
|
||||
ctx.console.log(
|
||||
lastUser || lastAsst
|
||||
? 'USER\n' +
|
||||
lastUser.slice(0, 1200) +
|
||||
'\n\nASSISTANT\n' +
|
||||
lastAsst.slice(0, 2000)
|
||||
: argv0 + ': no recap (empty history)'
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
|
||||
@@ -156,7 +156,17 @@ test('agent-tools exposes agent ops tools', async (t) => {
|
||||
'wait_for',
|
||||
'undo_last_edit',
|
||||
'git_diff',
|
||||
'history_search'
|
||||
'history_search',
|
||||
'git_log',
|
||||
'git_show',
|
||||
'git_blame',
|
||||
'copy_path',
|
||||
'diff_files',
|
||||
'find_symbol',
|
||||
'create_skill',
|
||||
'remember',
|
||||
'rewind_session',
|
||||
'export_session'
|
||||
]) {
|
||||
t.ok(
|
||||
TOOLS.includes("name: '" + toolName + "'"),
|
||||
@@ -220,6 +230,10 @@ test('agent CLI exposes production flags and inspect subcommands', async (t) =>
|
||||
t.ok(CLI.includes("sub === 'undo'"))
|
||||
t.ok(CLI.includes("sub === 'hooks'"))
|
||||
t.ok(CLI.includes("sub === 'history'"))
|
||||
t.ok(CLI.includes("sub === 'rewind'"))
|
||||
t.ok(CLI.includes("sub === 'export'"))
|
||||
t.ok(CLI.includes("sub === 'remember'"))
|
||||
t.ok(CLI.includes("sub === 'recap'"))
|
||||
t.ok(TUI.includes('planMode'))
|
||||
t.ok(TUI.includes('modelOverride'))
|
||||
t.ok(TUI.includes('extraSystemFile'))
|
||||
|
||||
@@ -611,6 +611,182 @@ test('gitignore, fuzzy, undo, history search', async (t) => {
|
||||
t.ok(waited.matched)
|
||||
})
|
||||
|
||||
test('rewind, export, unified diff, symbol regex', async (t) => {
|
||||
const s = loadPort()
|
||||
const hist = [
|
||||
{ role: 'user', content: 'first' },
|
||||
{ role: 'assistant', content: 'ok1' },
|
||||
{ role: 'user', content: 'second' },
|
||||
{ role: 'assistant', content: 'ok2' }
|
||||
]
|
||||
const points = s.bareAgentRewindPoints(hist)
|
||||
t.is(points.length, 2)
|
||||
const rewound = s.bareAgentRewindHistory(hist, { steps: 1 })
|
||||
t.ok(rewound.ok)
|
||||
t.is(rewound.dropped, 2)
|
||||
t.is(rewound.messages.length, 2)
|
||||
t.is(rewound.messages[0].content, 'first')
|
||||
const md = s.bareAgentExportTranscript(hist)
|
||||
t.ok(md.includes('# Agent session export'))
|
||||
t.ok(md.includes('first'))
|
||||
const diff = s.bareAgentUnifiedDiff('a\nb\nc\n', 'a\nB\nc\n', { from: '/a', to: '/b' })
|
||||
t.ok(diff.ok)
|
||||
t.absent(diff.identical)
|
||||
t.ok(String(diff.text).includes('-b'))
|
||||
t.ok(String(diff.text).includes('+B'))
|
||||
const re = new RegExp(s.bareAgentSymbolRegex('bareAgentCopyPath'))
|
||||
t.ok(re.test('async function bareAgentCopyPath(ctx, from, to) {'))
|
||||
t.absent(re.test('const x = bareAgentCopyPath'))
|
||||
})
|
||||
|
||||
test('copy, tree, find_symbol, create_skill, rewind dispatch', async (t) => {
|
||||
const d = loadDispatch()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
'/home/guest/src/lib.js': 'async function helloWorld() {\n return 1\n}\n',
|
||||
'/home/guest/src/a.txt': 'alpha\n',
|
||||
'/home/guest/src/b.txt': 'beta\n',
|
||||
'/home/guest/.agent/history.json': JSON.stringify(
|
||||
[
|
||||
{ role: 'user', content: 'one' },
|
||||
{ role: 'assistant', content: 'ok' },
|
||||
{ role: 'user', content: 'two' },
|
||||
{ role: 'assistant', content: 'ok2' }
|
||||
],
|
||||
null,
|
||||
2
|
||||
)
|
||||
})
|
||||
await vfs.mkdir('/home/guest/src')
|
||||
await vfs.mkdir('/home/guest/.agent')
|
||||
const ctx = { vfs, b4a }
|
||||
const paths = {
|
||||
dir: '/home/guest/.agent',
|
||||
history: '/home/guest/.agent/history.json',
|
||||
workspace: '/home/guest/.agent/workspace',
|
||||
workspaceSkills: '/home/guest/.agent/workspace/skills',
|
||||
workspaceMemory: '/home/guest/.agent/workspace/memory'
|
||||
}
|
||||
|
||||
const copied = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'copy_path',
|
||||
args: { from_path: '/home/guest/src/a.txt', to_path: '/home/guest/src/a.copy.txt' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(copied.ok)
|
||||
t.is(files.get('/home/guest/src/a.copy.txt'), 'alpha\n')
|
||||
|
||||
const tree = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'list_directory',
|
||||
args: { path: '/home/guest/src', tree: true },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(tree.ok)
|
||||
t.ok(String(tree.tree || '').includes('lib.js'))
|
||||
|
||||
const found = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'find_symbol',
|
||||
args: { name: 'helloWorld', root: '/home/guest/src' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(found.ok)
|
||||
t.ok(found.count >= 1)
|
||||
|
||||
const counted = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'grep',
|
||||
args: { pattern: 'hello', root: '/home/guest/src', output_mode: 'count' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(counted.ok)
|
||||
t.is(counted.output_mode, 'count')
|
||||
t.ok(counted.count >= 1)
|
||||
|
||||
const skill = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'create_skill',
|
||||
args: { id: 'demo-skill', description: 'Demo', body: '# Demo\n\nDo the thing.\n' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(skill.ok)
|
||||
t.ok(String(files.get('/home/guest/.agent/workspace/skills/demo-skill/SKILL.md') || '').includes('name: demo-skill'))
|
||||
|
||||
const rewound = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'rewind_session',
|
||||
args: { steps: 1 },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(rewound.ok)
|
||||
t.is(rewound.dropped, 2)
|
||||
const hist = JSON.parse(files.get('/home/guest/.agent/history.json') || '[]')
|
||||
t.is(hist.length, 2)
|
||||
|
||||
const remembered = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'remember',
|
||||
args: { text: 'holesail keys live in ~/.holesail' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(remembered.ok)
|
||||
t.is(remembered.kind, 'FACT')
|
||||
t.ok(String(files.get('/home/guest/.agent/workspace/MEMORY.md') || '').includes('holesail keys'))
|
||||
|
||||
const exported = await dispatch(d, {
|
||||
ctx,
|
||||
paths,
|
||||
toolName: 'export_session',
|
||||
args: { path: '/home/guest/.agent/export.md' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(exported.ok)
|
||||
t.ok(String(files.get('/home/guest/.agent/export.md') || '').includes('# Agent session export'))
|
||||
})
|
||||
|
||||
test('copy from read-only system path and project skill roots', async (t) => {
|
||||
const d = loadDispatch()
|
||||
const { vfs, files, b4a } = makeVfs({
|
||||
'/bin/sh': 'readonly-shell\n',
|
||||
'/home/guest/proj/.grok/skills/demo/SKILL.md':
|
||||
'---\nname: demo\ndescription: Project skill\n---\n\n# Demo\n'
|
||||
})
|
||||
await vfs.mkdir('/bin')
|
||||
await vfs.mkdir('/home/guest')
|
||||
const ctx = { vfs, b4a, env: { PWD: '/home/guest/proj', HOME: '/home/guest' } }
|
||||
const copied = await dispatch(d, {
|
||||
ctx,
|
||||
paths: { dir: '/home/guest/.agent', workspace: '/home/guest/.agent/workspace' },
|
||||
toolName: 'copy_path',
|
||||
args: { from_path: '/bin/sh', to_path: '/home/guest/sh.copy' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.ok(copied.ok)
|
||||
t.is(files.get('/home/guest/sh.copy'), 'readonly-shell\n')
|
||||
|
||||
const blocked = await dispatch(d, {
|
||||
ctx,
|
||||
paths: { dir: '/home/guest/.agent' },
|
||||
toolName: 'copy_path',
|
||||
args: { from_path: '/home/guest/sh.copy', to_path: '/bin/sh.hijack' },
|
||||
home: '/home/guest'
|
||||
})
|
||||
t.absent(blocked.ok)
|
||||
t.is(blocked.error, 'path_not_allowed')
|
||||
|
||||
const s = loadPort()
|
||||
const roots = await s.bareAgentDiscoverProjectSkillRoots(ctx, '/home/guest/proj/src')
|
||||
t.ok(roots.some((r) => String(r.path || '').endsWith('/.grok/skills')))
|
||||
})
|
||||
|
||||
test('search result parser extracts DDG-style topics', async (t) => {
|
||||
const s = loadPort()
|
||||
const rows = s.bareAgentParseSearchResults(
|
||||
|
||||
Reference in New Issue
Block a user