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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user