Further Agent updates

This commit is contained in:
2026-08-18 15:51:13 -04:00
parent a94de5be1d
commit 2ad8ae1666
22 changed files with 3043 additions and 60 deletions
@@ -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
}