/** * Guest-safe Grok Build harness ports: todos, plan mode, memory, glob, * AGENTS.md walk-up, JSON hooks, and search_replace uniqueness. */ var BARE_AGENT_PLAN_READONLY_TOOLS = Object.freeze({ read_file: 1, list_directory: 1, list_dir: 1, file_stat: 1, search_files: 1, grep: 1, glob_files: 1, glob: 1, get_system_info: 1, list_bin: 1, read_man_page: 1, apropos_man: 1, read_proc_file: 1, get_swarm_peers: 1, get_resource_limits: 1, web_fetch: 1, web_search: 1, git_status: 1, read_skill: 1, list_services: 1, service_status: 1, list_timers: 1, read_cron_log: 1, read_audit_log: 1, read_boot_policy: 1, read_kernel_extension_resolution: 1, get_initd_graph: 1, read_unit_journal: 1, inspect_ipc_backpressure: 1, get_network_summary: 1, tail_telemetry_streams: 1, pkg_index_lookup: 1, list_verification_scripts: 1, verification_hints: 1, runtime_diagnostic_bundle: 1, memory_search: 1, memory_get: 1, memory_append: 1, list_skills: 1, fuzzy_find: 1, 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, ask_user_question: 1, update_goal: 1, autonomous_run_status: 1, get_hrpc_bridge_health: 1, get_hrpc_allowlist_status: 1 }) var BARE_AGENT_PLAN_WRITE_TOOLS = Object.freeze({ write_file: 1, edit_file: 1, search_replace: 1, apply_patch: 1 }) /** * @param {string} pattern * @returns {RegExp | null} */ function bareAgentGlobToRegExp(pattern) { const src = String(pattern || '').trim() if (!src) return null let out = '^' for (let i = 0; i < src.length; i++) { const c = src.charAt(i) if (c === '*' && src.charAt(i + 1) === '*') { const next = src.charAt(i + 2) if (next === '/' || next === '') { out += '.*' i += next === '/' ? 2 : 1 continue } } if (c === '*') { out += '[^/]*' continue } if (c === '?') { out += '[^/]' continue } if ('\\^$+()[]{}|.'.indexOf(c) !== -1) out += '\\' + c else out += c } out += '$' try { return new RegExp(out) } catch { return null } } /** * @param {string} rel * @param {string} pattern */ function bareAgentGlobMatch(rel, pattern) { const re = bareAgentGlobToRegExp(pattern) if (!re) return false const n = String(rel || '').replace(/^\/+/, '') if (re.test(n)) return true const base = n.split('/').pop() || n return re.test(base) } /** * @param {unknown} updates * @param {{ merge?: boolean }} [opts] * @param {{ id: string, content: string, status: string }[]} [prev] */ function bareAgentTodoApply(updates, opts, prev) { const merge = Boolean(opts && opts.merge) /** @type {{ id: string, content: string, status: string }[]} */ const list = merge && Array.isArray(prev) ? prev.slice() : [] const byId = Object.create(null) for (let i = 0; i < list.length; i++) byId[list[i].id] = i const rows = Array.isArray(updates) ? updates : [] const seen = Object.create(null) for (let i = 0; i < rows.length; i++) { const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {} const id = String(row.id || '').trim() if (!id) throw new Error('todo_id_required') if (seen[id]) throw new Error('duplicate_todo_id:' + id) seen[id] = 1 const statusRaw = String(row.status || 'pending').trim().toLowerCase() const status = statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled' ? statusRaw : 'pending' const content = String(row.content == null ? '' : row.content).trim() if (byId[id] != null) { const cur = list[byId[id]] if (content) cur.content = content cur.status = status } else { list.push({ id, content: content || id, status }) byId[id] = list.length - 1 } } return list } /** * @param {{ id: string, content: string, status: string }[]} todos */ function bareAgentTodoSummarize(todos) { const rows = Array.isArray(todos) ? todos : [] let pending = 0 let inProgress = 0 let completed = 0 const lines = [] for (let i = 0; i < rows.length; i++) { const t = rows[i] const st = String(t.status || 'pending') if (st === 'completed' || st === 'cancelled') completed++ else if (st === 'in_progress') inProgress++ else pending++ lines.push('- [' + st + '] ' + t.id + ': ' + String(t.content || '').slice(0, 160)) } return { total: rows.length, pending, in_progress: inProgress, completed, open: pending + inProgress, text: lines.join('\n') } } /** * @param {{ open?: number, turnsSinceTodoWrite?: number, nudgeEnabled?: boolean }} opts */ function bareAgentTodoNudgeText(opts) { const o = opts && typeof opts === 'object' ? opts : {} if (o.nudgeEnabled === false) return '' const turns = Number(o.turnsSinceTodoWrite) || 0 const open = Number(o.open) || 0 if (open > 0 && turns >= 3) { return ( 'Open todos remain (' + String(open) + '). Use todo_write to mark progress or complete items before stopping.' ) } if (open === 0 && turns >= 5) { return 'Multi-step work: call todo_write to track remaining steps (merge=true to update one id).' } return '' } /** * @param {string} toolName * @param {Record} args * @param {{ plan?: string }} paths */ function bareAgentPlanModeToolAllowed(toolName, args, paths) { const name = String(toolName || '') if (BARE_AGENT_PLAN_READONLY_TOOLS[name]) return true if (!BARE_AGENT_PLAN_WRITE_TOOLS[name]) return false const plan = String((paths && paths.plan) || '') const p = String(args.path || args.file_path || '') return Boolean(plan && p && p === plan) } /** * @param {string} hay * @param {string} needle */ function bareAgentCountOccurrences(hay, needle) { if (!needle) return 0 let n = 0 let from = 0 while (from <= hay.length) { const i = hay.indexOf(needle, from) if (i === -1) break n++ from = i + Math.max(needle.length, 1) } return n } /** * @param {string} text * @param {string} oldStr * @param {string} newStr * @param {boolean} replaceAll */ function bareAgentSearchReplaceApply(text, oldStr, newStr, replaceAll) { const prev = String(text == null ? '' : text) if (!oldStr) { if (prev.trim()) { return { ok: false, error: 'empty_old_string_cannot_overwrite' } } return { ok: true, next: String(newStr == null ? '' : newStr), replacements: 1 } } const count = bareAgentCountOccurrences(prev, oldStr) if (count === 0) return { ok: false, error: 'old_string not found', count: 0 } if (count > 1 && !replaceAll) { return { ok: false, error: 'old_string not unique', count, hint: 'add surrounding lines to make the match unique, or set replace_all true' } } const next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr) return { ok: true, next, replacements: replaceAll ? count : 1 } } /** * @param {string} text * @param {{ offset?: number, limit?: number, numbered?: boolean }} [opts] */ function bareAgentSliceFileLines(text, opts) { const o = opts && typeof opts === 'object' ? opts : {} const raw = String(text == null ? '' : text) const lines = raw.split('\n') if (lines.length && lines[lines.length - 1] === '') lines.pop() const offset = typeof o.offset === 'number' && o.offset >= 1 ? Math.floor(o.offset) : 1 const limit = typeof o.limit === 'number' && o.limit >= 1 ? Math.floor(o.limit) : lines.length const start = Math.min(lines.length, offset - 1) const end = Math.min(lines.length, start + limit) const slice = lines.slice(start, end) const numbered = o.numbered !== false const body = numbered ? slice .map(function (line, i) { return String(start + i + 1) + '→' + line }) .join('\n') : slice.join('\n') return { content: body, start_line: start + 1, end_line: end, total_lines: lines.length, truncated: start > 0 || end < lines.length } } /** * @param {string} query */ function bareAgentMemoryTokens(query) { return String(query || '') .toLowerCase() .split(/[^a-z0-9_./-]+/) .filter(function (t) { return t.length > 1 }) } /** * @param {string} text * @param {string[]} tokens */ function bareAgentMemoryScore(text, tokens) { const low = String(text || '').toLowerCase() if (!tokens.length || !low) return 0 let hits = 0 for (let i = 0; i < tokens.length; i++) { if (low.indexOf(tokens[i]) !== -1) hits++ } return hits / tokens.length } /** * @param {Record} ctx * @param {string} startDir * @param {number} [maxHops] * @returns {Promise} */ async function bareAgentDiscoverAgentsMdPaths(ctx, startDir, maxHops) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return [] /** @type {string[]} */ const found = [] const seen = Object.create(null) let dir = String(startDir || '').replace(/\/+$/, '') || '/' const hops = Math.min(Math.max(Number(maxHops) || 12, 1), 24) const names = ['AGENTS.md', 'Claude.md', 'CLAUDE.md'] for (let n = 0; n < hops; n++) { for (let i = 0; i < names.length; i++) { const p = (dir === '/' ? '' : dir) + '/' + names[i] if (seen[p]) continue seen[p] = 1 try { const buf = await vfs.readFile(p) if (buf && buf.length) found.push(p) } catch { /* missing */ } } const rulesDirs = [dir + '/.grok/rules', dir + '/.agent/rules'] for (let r = 0; r < rulesDirs.length; r++) { if (typeof vfs.readdir !== 'function') continue let entries = [] try { entries = await vfs.readdir(rulesDirs[r]) } catch { continue } if (!Array.isArray(entries)) continue entries = entries.slice().sort() for (let e = 0; e < entries.length; e++) { const name = String(entries[e] || '') if (!/\.md$/i.test(name)) continue const p = rulesDirs[r] + '/' + name if (seen[p]) continue seen[p] = 1 try { const buf = await vfs.readFile(p) if (buf && buf.length) found.push(p) } catch { /* skip */ } } } if (dir === '/' || dir === '') break const slash = dir.lastIndexOf('/') dir = slash <= 0 ? '/' : dir.slice(0, slash) } return found } /** * @param {Record} ctx * @param {string[]} files * @param {number} [maxChars] */ async function bareAgentLoadAgentsMdPrompt(ctx, files, maxChars) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' const cap = Math.min(Math.max(Number(maxChars) || 6000, 500), 16000) const parts = [] let used = 0 for (let i = 0; i < files.length; i++) { if (used >= cap) break try { const buf = await vfs.readFile(files[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 body = String(t || '').trim() if (!body) continue const block = '### ' + files[i] + '\n' + body const room = cap - used parts.push(block.slice(0, room)) used += Math.min(block.length, room) } catch { /* skip */ } } if (!parts.length) return '' return ( '## Discovered project agent files (cwd walk-up, Grok-style)\n' + parts.join('\n\n') ) } /** * @param {Record} hook * @param {string} toolName * @param {Record} args */ function bareAgentHookDenies(hook, toolName, args) { if (!hook || typeof hook !== 'object') return '' const event = String(hook.event || hook.type || 'PreToolUse') if (event !== 'PreToolUse' && event !== 'pre') return '' const tools = Array.isArray(hook.tools) ? hook.tools.map(String) : [] if (tools.length && tools.indexOf(toolName) === -1) return '' const reSrc = String(hook.deny_regex || hook.denyRegex || '').trim() if (!reSrc) return '' let re try { re = new RegExp(reSrc, 'i') } catch { return '' } const blob = toolName + ' ' + String(args.command || args.path || args.file_path || args.pattern || '') if (!re.test(blob)) return '' return String(hook.reason || hook.message || 'blocked by ~/.agent/hooks').slice(0, 240) } /** * @param {Record} ctx * @param {string} hooksDir * @param {string} toolName * @param {Record} args */ async function bareAgentRunPreToolHooks(ctx, hooksDir, toolName, args) { if (typeof bareAgentRunHooks === 'function') { const out = await bareAgentRunHooks(ctx, hooksDir, 'PreToolUse', toolName, args, '') return out.deny || '' } return '' } /** * @param {Record} ctx * @param {unknown} buf */ function bareAgentPortDecode(ctx, buf) { if (!buf || !buf.length) return '' if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ) { return ctx.b4a.toString(buf) } return String(new TextDecoder().decode(buf)) } /** * @param {Record} ctx * @param {string} text */ function bareAgentPortEncode(ctx, text) { if ( typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function' ) { return ctx.b4a.from(String(text == null ? '' : text)) } return new TextEncoder().encode(String(text == null ? '' : text)) } /** * @param {Record} ctx * @param {string} path */ async function bareAgentReadTextFile(ctx, path) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' try { const buf = await vfs.readFile(path) return bareAgentPortDecode(ctx, buf) } catch { return '' } } /** * @param {Record} ctx * @param {string} path * @param {string} text */ async function bareAgentWriteTextFile(ctx, path, text) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.writeFile !== 'function') { throw new Error('vfs.writeFile unavailable') } if (typeof vfs.mkdir === 'function') { const dir = String(path || '').replace(/\/[^/]+$/, '') if (dir && dir !== path) { try { await vfs.mkdir(dir, { recursive: true }) } catch { /* parent may already exist */ } } } await vfs.writeFile(path, bareAgentPortEncode(ctx, text)) } /** * @param {Record} ctx * @param {string} path * @param {unknown} fallback */ async function bareAgentReadJsonFile(ctx, path, fallback) { const t = await bareAgentReadTextFile(ctx, path) if (!t.trim()) return fallback try { return JSON.parse(t) } catch { return fallback } } /** * @param {Record} ctx * @param {string} path * @param {unknown} value */ async function bareAgentWriteJsonFile(ctx, path, value) { await bareAgentWriteTextFile(ctx, path, JSON.stringify(value, null, 2) + '\n') } /** * @param {Record} ctx * @param {string} path */ async function bareAgentVfsIsDir(ctx, path) { const vfs = ctx && ctx.vfs if (!vfs) return false if (typeof vfs.lstat === 'function' || typeof vfs.stat === 'function') { try { const st = typeof vfs.lstat === 'function' ? await vfs.lstat(path) : await vfs.stat(path) if (st && typeof st.isDirectory === 'function') return Boolean(st.isDirectory()) if (st && typeof st === 'object' && 'isDirectory' in st) { return Boolean(/** @type {{ isDirectory?: unknown }} */ (st).isDirectory) } } catch { return false } } if (typeof vfs.readdir === 'function') { try { await vfs.readdir(path) return true } catch { return false } } return false } /** * @param {Record} ctx * @param {string} root * @param {{ maxFiles?: number, maxDepth?: number }} [opts] * @returns {Promise} */ async function bareAgentVfsWalkFiles(ctx, root, opts) { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.readdir !== 'function') return [] const maxFiles = Math.min(Math.max(Number(opts && opts.maxFiles) || 400, 1), 4000) const maxDepth = Math.min(Math.max(Number(opts && opts.maxDepth) || 8, 1), 16) const skip = Object.create(null) 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: rootN, depth: 0 }] while (queue.length && out.length < maxFiles) { const cur = queue.shift() if (!cur) break let names try { names = await vfs.readdir(cur.dir) } catch { continue } if (!Array.isArray(names)) continue for (let i = 0; i < names.length && out.length < maxFiles; i++) { 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 { out.push(full) } } } return out } /** * @param {Record} ctx * @param {string} root * @param {string} pattern * @param {{ maxFiles?: number, maxDepth?: number }} [opts] */ async function bareAgentGlobFiles(ctx, root, pattern, opts) { const files = await bareAgentVfsWalkFiles(ctx, root, opts) const rootN = String(root || '/').replace(/\/+$/, '') || '/' /** @type {string[]} */ const hits = [] for (let i = 0; i < files.length; i++) { const abs = files[i] let rel = abs if (rootN !== '/' && abs.indexOf(rootN + '/') === 0) rel = abs.slice(rootN.length + 1) else if (abs.charAt(0) === '/') rel = abs.slice(1) if (bareAgentGlobMatch(rel, pattern) || bareAgentGlobMatch(abs, pattern)) { hits.push(abs) } } return hits } /** * @param {Record} ctx * @param {string[]} files * @param {string} query * @param {{ maxHits?: number, snippet?: number }} [opts] */ async function bareAgentMemorySearchFiles(ctx, files, query, opts) { const tokens = bareAgentMemoryTokens(query) const maxHits = Math.min(Math.max(Number(opts && opts.maxHits) || 8, 1), 24) const snippet = Math.min(Math.max(Number(opts && opts.snippet) || 280, 80), 1200) /** @type {{ path: string, score: number, snippet: string }[]} */ const scored = [] for (let i = 0; i < files.length; i++) { const path = files[i] const text = await bareAgentReadTextFile(ctx, path) if (!text) continue const score = tokens.length ? bareAgentMemoryScore(text, tokens) : 0.15 if (score <= 0) continue let snip = text.trim().replace(/\s+/g, ' ').slice(0, snippet) if (tokens.length) { const low = text.toLowerCase() let at = -1 for (let t = 0; t < tokens.length; t++) { const j = low.indexOf(tokens[t]) if (j !== -1 && (at === -1 || j < at)) at = j } if (at >= 0) { const start = Math.max(0, at - 40) snip = (start > 0 ? '…' : '') + text.slice(start, start + snippet).replace(/\s+/g, ' ') } } scored.push({ path, score: Math.round(score * 1000) / 1000, snippet: snip }) } scored.sort(function (a, b) { return b.score - a.score }) return scored.slice(0, maxHits) } /** * @param {Record} ctx * @param {string} path */ async function bareAgentLoadTodos(ctx, path) { const raw = await bareAgentReadJsonFile(ctx, path, []) if (!Array.isArray(raw)) return [] /** @type {{ id: string, content: string, status: string }[]} */ const out = [] for (let i = 0; i < raw.length; i++) { const row = raw[i] && typeof raw[i] === 'object' ? raw[i] : null if (!row) continue const id = String(row.id || '').trim() if (!id) continue const statusRaw = String(row.status || 'pending').trim().toLowerCase() const status = statusRaw === 'in_progress' || statusRaw === 'completed' || statusRaw === 'cancelled' ? statusRaw : 'pending' out.push({ id, content: String(row.content == null ? id : row.content), status }) } return out } /** * @param {Record} ctx * @param {string} path * @param {{ id: string, content: string, status: string }[]} todos */ async function bareAgentSaveTodos(ctx, path, todos) { await bareAgentWriteJsonFile(ctx, path, Array.isArray(todos) ? todos : []) } /** * @param {Record} args */ function bareAgentToolPathArg(args) { if (!args || typeof args !== 'object') return '' if (typeof args.path === 'string' && args.path.trim()) return args.path.trim() if (typeof args.file_path === 'string' && args.file_path.trim()) { return args.file_path.trim() } return '' } /** * @param {string} text */ function bareAgentLooksBinaryText(text) { const s = String(text || '') if (!s) return false if (s.indexOf('\0') !== -1) return true let bad = 0 const n = Math.min(s.length, 800) for (let i = 0; i < n; i++) { const c = s.charCodeAt(i) if (c === 9 || c === 10 || c === 13) continue if (c < 32) bad++ } return bad > 8 } /** * Guest-native grep (Grok-style). VFS walk + JS regex — no host rg/node. * @param {Record} ctx * @param {{ * pattern: string, * root?: string, * glob?: string, * ignore_case?: boolean, * before?: number, * after?: number, * context?: number, * max_matches?: number, * files_with_matches?: boolean * }} opts */ async function bareAgentGrepFiles(ctx, opts) { const o = opts && typeof opts === 'object' ? opts : {} const pattern = String(o.pattern || '') if (!pattern) return { ok: false, error: 'pattern_required' } let re try { re = new RegExp(pattern, o.ignore_case ? 'i' : '') } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) return { ok: false, error: 'bad_regex', detail: msg } } const root = String(o.root || '/').replace(/\/+$/, '') || '/' const before = Math.min( 8, Math.max(0, Math.floor(Number(o.before != null ? o.before : o.context) || 0)) ) const after = Math.min( 8, Math.max(0, Math.floor(Number(o.after != null ? o.after : o.context) || 0)) ) const maxMatches = Math.min( 200, Math.max(1, Math.floor(Number(o.max_matches) || 50)) ) 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 }) : await bareAgentVfsWalkFiles(ctx, root, { maxFiles: 800, maxDepth: 12 }) /** @type {{ path: string, line?: number, text?: string }[]} */ 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] const text = await bareAgentReadTextFile(ctx, path) if (!text || bareAgentLooksBinaryText(text)) continue 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 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) const body = before || after ? slice .map(function (ln, j) { const n = start + j + 1 const mark = n === i + 1 ? ':' : '-' return String(n) + mark + ln }) .join('\n') : String(i + 1) + ':' + lines[i] matches.push({ path, line: i + 1, text: body }) if (matches.length >= maxMatches) { truncated = true break } } if (fileHit && filesOnly) { filesHit.push(path) if (filesHit.length >= maxMatches) { truncated = true break } } if (fileHit && countOnly) { counts.push({ path, count: fileCount }) if (counts.length >= maxMatches) { truncated = true break } } if (truncated) break } if (filesOnly) { return { ok: true, 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 } } /** * Codex / Grok apply_patch parser (Begin Patch … End Patch). * @param {string} text * @returns {{ ok: boolean, error?: string, ops?: object[] }} */ function bareAgentParseApplyPatch(text) { const raw = String(text || '').replace(/\r\n/g, '\n') if (!raw.trim()) return { ok: false, error: 'empty_patch' } const all = raw.split('\n') let start = 0 let end = all.length for (let i = 0; i < all.length; i++) { if (/^\s*\*\*\*\s*Begin Patch\s*$/i.test(all[i])) { start = i + 1 break } } for (let i = all.length - 1; i >= 0; i--) { if (/^\s*\*\*\*\s*End Patch\s*$/i.test(all[i])) { end = i break } } const body = all.slice(start, end) /** @type {object[]} */ const ops = [] /** @type {Record | null} */ let cur = null /** @type {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean } | null} */ let chunk = null function flushChunk() { if (cur && cur.type === 'update' && chunk) { const chunks = Array.isArray(cur.chunks) ? cur.chunks : [] chunks.push(chunk) cur.chunks = chunks chunk = null } } function flushOp() { flushChunk() if (cur) ops.push(cur) cur = null } for (let i = 0; i < body.length; i++) { const line = body[i] if (/^\s*\*\*\*\s*Add File:\s*/i.test(line)) { flushOp() cur = { type: 'add', path: line.replace(/^\s*\*\*\*\s*Add File:\s*/i, '').trim(), content: '' } continue } if (/^\s*\*\*\*\s*Delete File:\s*/i.test(line)) { flushOp() cur = { type: 'delete', path: line.replace(/^\s*\*\*\*\s*Delete File:\s*/i, '').trim() } continue } if (/^\s*\*\*\*\s*Update File:\s*/i.test(line)) { flushOp() cur = { type: 'update', path: line.replace(/^\s*\*\*\*\s*Update File:\s*/i, '').trim(), move_to: '', chunks: [] } continue } if (/^\s*\*\*\*\s*Move to:\s*/i.test(line) && cur && cur.type === 'update') { cur.move_to = line.replace(/^\s*\*\*\*\s*Move to:\s*/i, '').trim() continue } if (/^\s*\*\*\*\s*End of File\s*$/i.test(line)) { if (chunk) chunk.is_end_of_file = true flushChunk() continue } if (line.indexOf('@@') === 0 && cur && cur.type === 'update') { flushChunk() chunk = { context: line.replace(/^@@\s?/, '').trim(), old_lines: [], new_lines: [] } continue } if (cur && cur.type === 'add') { const bodyLine = line.charAt(0) === '+' ? line.slice(1) : line cur.content = cur.content ? String(cur.content) + '\n' + bodyLine : bodyLine continue } if (cur && cur.type === 'update') { if (!chunk) { chunk = { context: '', old_lines: [], new_lines: [] } } const tag = line.charAt(0) const rest = line.length ? line.slice(1) : '' if (tag === '-') chunk.old_lines.push(rest) else if (tag === '+') chunk.new_lines.push(rest) else { const ctxLine = tag === ' ' ? rest : line chunk.old_lines.push(ctxLine) chunk.new_lines.push(ctxLine) } } } flushOp() if (!ops.length) return { ok: false, error: 'no_patch_ops' } return { ok: true, ops } } /** * @param {string} text * @param {{ context: string, old_lines: string[], new_lines: string[], is_end_of_file?: boolean }} chunk */ function bareAgentApplyPatchChunk(text, chunk) { const src = String(text == null ? '' : text) const oldBlock = (chunk.old_lines || []).join('\n') const newBlock = (chunk.new_lines || []).join('\n') if (!oldBlock && !newBlock) { return { ok: false, error: 'empty_chunk' } } if (!oldBlock) { const next = src ? src.replace(/\s*$/, '') + (src.endsWith('\n') ? '' : '\n') + newBlock + '\n' : newBlock + (newBlock.endsWith('\n') ? '' : '\n') return { ok: true, next } } let from = 0 if (chunk.context) { const at = src.indexOf(chunk.context) if (at === -1) { return { ok: false, error: 'chunk_context_not_found', context: chunk.context } } from = at } const hay = src.slice(from) const count = bareAgentCountOccurrences(hay, oldBlock) if (count === 0) return { ok: false, error: 'chunk_old_not_found' } if (count > 1 && !chunk.context) { return { ok: false, error: 'chunk_old_not_unique', count, hint: 'add @@ context or more surrounding lines' } } const next = src.slice(0, from) + hay.replace(oldBlock, newBlock) return { ok: true, next } } /** * @param {Record} ctx * @param {object[]} ops * @param {{ home?: string, denyPrefixes?: unknown }} [opts] */ async function bareAgentApplyPatchOps(ctx, ops, opts) { const home = String((opts && opts.home) || '') const deny = opts && opts.denyPrefixes /** @type {object[]} */ const results = [] const rows = Array.isArray(ops) ? ops : [] for (let i = 0; i < rows.length; i++) { const op = rows[i] && typeof rows[i] === 'object' ? rows[i] : {} let path = String(op.path || '').trim() if (path && path.charAt(0) !== '/' && home) { path = home.replace(/\/+$/, '') + '/' + path.replace(/^\.\//, '') } if (!path) { results.push({ ok: false, error: 'path_required' }) continue } if ( typeof bareAgentPathAllowedMutate === 'function' && !bareAgentPathAllowedMutate(path, deny) ) { results.push({ ok: false, path, error: 'path_not_allowed' }) continue } if (op.type === 'add') { const content = String(op.content == null ? '' : op.content) await bareAgentWriteTextFile( ctx, path, content.endsWith('\n') ? content : content + '\n' ) results.push({ ok: true, op: 'add', path }) continue } if (op.type === 'delete') { const vfs = ctx && ctx.vfs if (!vfs || typeof vfs.unlink !== 'function') { results.push({ ok: false, path, error: 'vfs.unlink unavailable' }) continue } try { await vfs.unlink(path) results.push({ ok: true, op: 'delete', path }) } catch (e) { const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e) results.push({ ok: false, path, error: msg }) } continue } if (op.type === 'update') { let text = await bareAgentReadTextFile(ctx, path) if (!text && text !== '') { results.push({ ok: false, path, error: 'missing_file' }) continue } const chunks = Array.isArray(op.chunks) ? op.chunks : [] let failed = null for (let c = 0; c < chunks.length; c++) { const applied = bareAgentApplyPatchChunk(text, chunks[c]) if (!applied.ok) { failed = applied break } text = applied.next } if (failed) { results.push({ ok: false, path, error: failed.error, hint: failed.hint || null }) continue } const dest = String(op.move_to || '').trim() || path const destAbs = dest.charAt(0) === '/' ? dest : home ? home.replace(/\/+$/, '') + '/' + dest.replace(/^\.\//, '') : dest if ( destAbs !== path && typeof bareAgentPathAllowedMutate === 'function' && !bareAgentPathAllowedMutate(destAbs, deny) ) { results.push({ ok: false, path, error: 'move_path_not_allowed', dest: destAbs }) continue } await bareAgentWriteTextFile(ctx, destAbs, text) if (destAbs !== path && ctx.vfs && typeof ctx.vfs.unlink === 'function') { try { await ctx.vfs.unlink(path) } catch { /* keep original if unlink fails */ } } results.push({ ok: true, op: destAbs !== path ? 'move' : 'update', path, dest: destAbs !== path ? destAbs : undefined, chunks: chunks.length }) continue } results.push({ ok: false, path, error: 'unknown_op' }) } const failed = results.filter(function (r) { return !r.ok }) return { ok: failed.length === 0, applied: results.length - failed.length, failed: failed.length, results } } /** * Parse DuckDuckGo instant-answer JSON into a compact result list. * @param {unknown} payload * @param {number} [max] */ function bareAgentParseSearchResults(payload, max) { const cap = Math.min(Math.max(Number(max) || 8, 1), 16) /** @type {{ title: string, url: string, snippet: string }[]} */ const out = [] const seen = Object.create(null) function add(title, url, snippet) { const u = String(url || '').trim() if (!u || seen[u] || out.length >= cap) return if (!/^https?:\/\//i.test(u)) return seen[u] = 1 out.push({ title: String(title || u).slice(0, 160), url: u, snippet: String(snippet || '').replace(/\s+/g, ' ').trim().slice(0, 280) }) } const obj = payload && typeof payload === 'object' ? payload : {} const rec = /** @type {Record} */ (obj) if (rec.AbstractURL || rec.Abstract) { add( String(rec.Heading || rec.AbstractSource || 'Abstract'), String(rec.AbstractURL || ''), String(rec.AbstractText || rec.Abstract || '') ) } const results = Array.isArray(rec.Results) ? rec.Results : [] for (let i = 0; i < results.length; i++) { const row = results[i] && typeof results[i] === 'object' ? results[i] : {} add(row.Text || row.Name, row.FirstURL, row.Text) } const related = Array.isArray(rec.RelatedTopics) ? rec.RelatedTopics : [] function walk(list) { for (let i = 0; i < list.length && out.length < cap; i++) { const row = list[i] && typeof list[i] === 'object' ? list[i] : {} if (Array.isArray(row.Topics)) walk(row.Topics) else add(row.Text, row.FirstURL, row.Text) } } walk(related) return out } /** * Parse Grok-style intervals (5m / 2h / 1d) or a five-field cron line. * @param {string} raw * @returns {{ kind: 'everyMs', everyMs: number } | { kind: 'calendar', onCalendar: string } | { error: string }} */ function bareAgentParseScheduleInterval(raw) { const s = String(raw || '').trim() if (!s) return { error: 'interval_required' } const compact = s.replace(/\s+/g, '') const m = /^(\d+)(ms|s|m|h|d)$/i.exec(compact) if (m) { const n = Number(m[1]) const unit = m[2].toLowerCase() let ms = 0 if (unit === 'ms') ms = n else if (unit === 's') ms = n * 1000 else if (unit === 'm') ms = n * 60 * 1000 else if (unit === 'h') ms = n * 60 * 60 * 1000 else ms = n * 24 * 60 * 60 * 1000 if (ms < 1000) return { error: 'interval_too_short', min_ms: 1000 } if (ms > 86400000) return { error: 'interval_too_long', max_ms: 86400000 } return { kind: 'everyMs', everyMs: ms } } const fields = s.split(/\s+/) if (fields.length === 5) return { kind: 'calendar', onCalendar: s } return { error: 'bad_interval', hint: 'use 5m, 2h, 1d, or five cron fields' } } /** * @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} 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} 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} hook * @param {string} event * @param {string} toolName * @param {Record} 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} ctx * @param {string} hooksDir * @param {string} event * @param {string} [toolName] * @param {Record} [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} 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} 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() .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') if (!s) s = 'task' 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} 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} 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} 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} 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 }