@livx.cc/agentx 0.99.44 → 0.99.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tools.ts","../src/llm.ts","../src/logging.ts","../src/redact.ts","../src/tools.structured.ts","../src/tools.web.ts","../src/shell.sandbox.ts","../src/tools.shell.ts"],"sourcesContent":["import type { IFilesystem } from '@livx.cc/wcli/core';\nimport { CommandExecutor, registerHeadlessCommands } from '@livx.cc/wcli/core';\nimport type { Tool, ChatLike } from './llm';\nimport { imageRefResult } from './llm';\nimport { forComponent } from './logging';\nimport { grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool, fuzzyLineReplace } from './tools.structured';\nimport { todoWriteTool, type TodoItem } from './todo';\nimport { webFetchTool, webSearchTool, webSearchAnthropicTool } from './tools.web';\nimport { OverlayFilesystem } from './OverlayFilesystem';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\nimport type { SandboxJobRegistry } from './tools.jobs';\n\nconst log = forComponent('tools');\n\n/** A structured multiple-choice question the model can pose to a human. */\nexport interface UserQuestion {\n question: string;\n header?: string;\n options: { label: string; description?: string }[];\n multiSelect?: boolean;\n}\n\n/**\n * The host / human-in-the-loop seam (the \"third seam\" beyond LLM + filesystem).\n * Injected per host: a CLI reads stdin, a browser renders a dialog, edge/headless\n * omits it. Unifies user-questions, permission/plan approvals, and notifications.\n */\nexport type HostEvent =\n | { kind: 'text_delta'; message: string }\n | { kind: 'speak_utterance'; message: string }\n | { kind: 'thinking_delta'; message: string }\n | { kind: 'tool_use'; id: string; name: string; input: unknown }\n | { kind: 'tool_result'; id: string; output: string; isError?: boolean }\n | { kind: 'tool_result_image'; id: string; dataUrl: string }\n // Display-only tool activity from a self-executing backend (e.g. Cursor CLI). NOT dispatched by the\n // Agent — purely for host UI (rich hosts read name/input/status for cards; plain hosts print `message`).\n | { kind: 'tool_activity'; id?: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error'; truncated?: { args?: boolean; result?: boolean }; message: string }\n | { kind: string; message: string; data?: unknown };\n\nexport interface HostBridge {\n /** Ask the user a structured question; resolve to the chosen label(s) / free text. */\n ask?(q: UserQuestion): Promise<string>;\n /** Request approval for a sensitive action (permission 'ask' / plan approval). */\n confirm?(prompt: string, meta?: { tool: string; input: unknown }): Promise<boolean>;\n /** Emit a progress / notification event to the host UI (non-blocking). */\n notify?(event: HostEvent): void;\n}\n\nexport interface ToolContext {\n fs: IFilesystem;\n exec: CommandExecutor;\n /** path -> content snapshot at last Read/Edit; powers the read-before-edit staleness guard. */\n readState: Map<string, string>;\n /** optional host interaction channel; absent => autonomous/headless. */\n host?: HostBridge;\n /** optional run-cancellation signal (mirrors AgentOptions.signal); lets abort-aware tools\n * (e.g. the real shell) kill in-flight work when the run is cancelled. */\n signal?: AbortSignal;\n /** the agent's working todo list (TodoWrite planning aid); replaced wholesale per call. */\n todos: TodoItem[];\n /** optional syntax guardrail: if set, write-class tools refuse to persist a broken result. */\n lint?: (path: string, content: string) => string | null;\n /** optional PDF text extraction (node hosts wire a pdftotext-backed impl); absent => Read explains. */\n pdfText?: (path: string) => Promise<string>;\n /** optional model handle for tools that run their own LLM pass (e.g. Review, a self-critique).\n * Populated by the Agent from its own ai/model; absent => such tools degrade to a no-op. */\n ai?: ChatLike;\n model?: string;\n /** optional sandbox background-job registry; enables `bash({background:true})`. Absent => no backgrounding. */\n jobs?: SandboxJobRegistry;\n /** optional incremental-output channel: long-running tools (e.g. the real Shell) stream chunks\n * here mid-run. Wired PER CALL by Agent.dispatch to Hooks.onToolOutput (cleared when the call\n * settles — a late emit is a silent no-op). Fire-and-forget: never awaited, never part of the result. */\n emit?: (chunk: string) => void;\n /** Wrap a HUMAN-blocking await (permission/plan confirm, an interactive question) so the time\n * spent parked on the user is excluded from the run's wall-clock kill-switches. Wired by the Agent;\n * absent => no accounting (the promise is awaited as-is). Idle prompt time must not count as work. */\n parkHuman?<T>(p: Promise<T>): Promise<T>;\n /** Set by `AskUserQuestion` when the model has a decision that is genuinely the user's to make but\n * no human is reachable (headless/piped/edge). The Agent drains it after the tool batch and ends\n * the run with `finishReason:'needs_input'`, handing the question to the caller instead of letting\n * the agent guess. Cleared at the start of every run. */\n needsInput?: UserQuestion;\n}\n\nexport interface AgentTool {\n name: string;\n description: string;\n parameters: object; // JSON Schema for the function's arguments\n /**\n * Longest wall-clock time this tool can legitimately take before it returns on its own.\n * Declared so a TRANSPORT above the loop (e.g. the cursor helper's per-call watchdog) can derive a\n * deadline with real headroom instead of carrying an independently-maintained magic number that\n * drifts into a tie with the tool's own cap — a tie the transport wins, destroying the tool's real\n * result. Omit for tools that return promptly.\n */\n maxDurationMs?: number;\n /**\n * Is `maxDurationMs` SELF-ENFORCED — does this tool abort itself at that bound and return?\n * An enforced bound cannot be overrun, so a transport above it needs only modest slack; an ADVISORY\n * one (the default) may be exceeded, so the transport keeps generous headroom over it. Declaring a\n * bound you do not actually enforce is worse than declaring none — say `true` only when you abort.\n */\n maxDurationEnforced?: boolean;\n run(args: any, ctx: ToolContext): Promise<string | { text: string; images?: { mimeType: string; data: string }[] }>;\n /** Optional: return a copy of this tool bound to a different real-disk working directory.\n * Implemented by tools that escape the VFS (the real `Shell`), which are cwd-bound at construction\n * and would otherwise keep pointing at the PARENT's tree when the tool instance is inherited by an\n * isolated child agent. `subagent.ts` calls this for a worktree child; tools without it pass through.\n * Return `undefined` for a capability that CANNOT follow the child (the background-job companions,\n * bound to the parent's job registry) — the tool is then dropped from the child's toolbelt.\n * An implementation must carry over any post-construction mutation the host made to the instance\n * (a renamed `name`/`description`) — rebuilding from the factory's options alone loses it. */\n withCwd?(cwd: string): AgentTool | undefined;\n}\n\n/** Build a tool context bound to a filesystem backend (Mem / Disk / …) and an optional host. */\nexport function makeContext(fs: IFilesystem, host?: HostBridge): ToolContext {\n const exec = new CommandExecutor(fs);\n registerHeadlessCommands(exec);\n return { fs, exec, readState: new Map(), host, todos: [] };\n}\n\n/** Convert AgentTools into the ai.libx.js `tools` array for chat(). */\nexport function toWireTools(tools: AgentTool[]): Tool[] {\n return tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n}\n\nconst numberLines = (content: string, offset = 0, limit?: number): string => {\n const lines = content.split('\\n');\n const start = Math.max(0, offset);\n const end = limit != null ? start + limit : lines.length;\n return lines\n .slice(start, end)\n .map((l, i) => `${start + i + 1}\\t${l}`)\n .join('\\n');\n};\n\n/** Keep huge tool output high-signal: head+tail with an omission marker (cuts re-runs to \"parse the wall\"). */\nexport function truncateOutput(s: string, headLines = 80, tailLines = 20): string {\n const lines = s.split('\\n');\n if (lines.length <= headLines + tailLines + 1) return s;\n const omitted = lines.length - headLines - tailLines;\n return [...lines.slice(0, headLines), `… (${omitted} lines omitted — narrow the command to see more) …`, ...lines.slice(-tailLines)].join('\\n');\n}\n\n/** Run any shell command line over the VFS (ls/cat/grep/find/head/tail/echo/mkdir/rm/mv/wc, pipes, redirects, &&/||/;). */\nexport const bashTool: AgentTool = {\n name: 'bash',\n description:\n 'Run a shell command. Supports ls, cat, grep, find, head, tail, echo, mkdir, rm, mv, cp, wc, pipes (|), redirects (>, >>), and chaining (&&, ||, ;). Best for: running tests/builds, file operations (mkdir/mv/rm), and piped workflows. For searching file contents, prefer `Grep` (structured results, no re-parse). For finding files by name, prefer `Glob`.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the command line to execute' },\n background: { type: 'boolean', description: 'run detached over an isolated overlay (writes commit when it finishes); returns a job id to poll with JobOutput. Only worth it for slow work (remote VFS / long pipelines).' },\n },\n },\n async run({ command, background }, ctx) {\n if (background && ctx.jobs) return startBashJob(String(command ?? ''), ctx);\n const r = await ctx.exec.execute(String(command ?? ''));\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n if (r.exitCode !== 0) {\n const err = (r.error ?? '').trim();\n return `[exit ${r.exitCode}]${err ? ' ' + err : ''}${out ? '\\n' + out : ''}`;\n }\n return out || '(command succeeded, no output)'; // explicit sentinel: don't re-run to \"check\"\n },\n};\n\n/** Kick a bash command into the background over an isolated overlay; its writes commit only on success.\n * A kill (abort) before completion skips the commit — the parent VFS is never touched mid-flight. */\nfunction startBashJob(command: string, ctx: ToolContext): string {\n const baseFs = ctx.fs;\n const id = ctx.jobs!.start(\n async ({ signal }) => {\n const overlay = new OverlayFilesystem(baseFs);\n const exec = new CommandExecutor(overlay);\n registerHeadlessCommands(exec);\n const r = await exec.execute(command); // wcli is sync-to-completion; abort can only gate the commit below\n if (signal.aborted) return '[killed before commit]';\n await overlay.commit(); // atomically flush this job's writes down into the parent VFS\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n return r.exitCode !== 0 ? `[exit ${r.exitCode}] ${(r.error ?? '').trim()}\\n${out}`.trim() : out || '(command succeeded, no output)';\n },\n { kind: 'bash', label: command.slice(0, 60) },\n );\n return `Started background job ${id} — poll with JobOutput({id:\"${id}\"}) / JobStatus, stop with JobKill.`;\n}\n\n/** Image extensions the Read tool returns as a visual block (when the fs can read bytes). */\n/** The real on-disk path behind a VFS path, when the fs exposes one; null (with a reason) otherwise —\n * callers then fall back to inlining the bytes. */\nfunction realPathOf(fs: { realPath?: (p: string) => string }, path: string): string | null {\n if (typeof fs.realPath !== 'function') return null;\n try { return fs.realPath(path); }\n catch (e) { log.debug(`realPath unavailable for ${path} — inlining image bytes instead`, e); return null; }\n}\n\nconst IMG_MIME: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };\n\n/** Heuristic: is this utf8-decoded string actually a binary file? A NUL byte never occurs in text; a\n * dense run of U+FFFD replacement chars means the bytes weren't valid utf8. Cheap — scans a prefix. */\nfunction isBinary(raw: string): boolean {\n if (raw === '') return false; // empty is handled separately (could be a legit 0-byte text file)\n const head = raw.length > 8192 ? raw.slice(0, 8192) : raw;\n let repl = 0;\n for (let i = 0; i < head.length; i++) {\n const c = head.charCodeAt(i);\n if (c === 0x00) return true; // NUL byte -> definitively binary\n if (c === 0xfffd) repl++; // utf8 decode failure marker\n }\n return repl / head.length > 0.1;\n}\n\n/** Byte length of a file — exact via readFileBytes when the fs exposes it, else utf8 byte estimate. */\nasync function byteSize(fs: unknown, path: string, raw: string): Promise<number> {\n const bfs = fs as { readFileBytes?: (p: string) => Promise<Uint8Array> };\n if (typeof bfs.readFileBytes === 'function') {\n try { return (await bfs.readFileBytes(path)).length; } catch { /* fall through to estimate */ }\n }\n return Buffer.byteLength(raw, 'utf8');\n}\n\n/** Read a text file as 1-indexed numbered lines; arms the staleness guard for Edit. */\nexport const readTool: AgentTool = {\n name: 'Read',\n description:\n 'Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.',\n parameters: {\n type: 'object',\n required: ['path'],\n properties: {\n path: { type: 'string' },\n offset: { type: 'number' },\n limit: { type: 'number' },\n },\n },\n async run({ path, offset, limit }, ctx) {\n // Image file → return it as a visual block. The adapter turns a tool result whose JSON carries a\n // `dataUrl` into an image tool_result the model can see. Needs a binary-capable fs (disk default);\n // the utf8 VFS (sandbox/Mem) can't, so we say so instead of corrupting the bytes.\n const ext = String(path).toLowerCase().split('.').pop() ?? '';\n // PDF → extracted text when the host wired an extractor (CLI: pdftotext); else say how to proceed.\n if (ext === 'pdf') {\n if (!ctx.pdfText) return `[${path} is a PDF — text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`; // jail-aware: hidden paths read as absent\n const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();\n return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;\n }\n if (IMG_MIME[ext]) {\n const fs = ctx.fs as { readFileBytes?: (p: string) => Promise<Uint8Array>; realPath?: (p: string) => string };\n if (typeof fs.readFileBytes !== 'function') {\n return `[${path} is an image, but this filesystem can't read binary — attach it as @${path} instead, or run on disk.]`;\n }\n // Persist a REFERENCE, not the payload: the send path (expandImagesForSend) turns it back into the\n // same `{dataUrl}` body the wire adapter renders, for the recent images only — so a screenshot Read\n // costs ~80 bytes in the transcript/session JSON instead of megabytes of immortal base64.\n const real = realPathOf(fs, path);\n if (real) {\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`;\n return imageRefResult(real, IMG_MIME[ext]);\n }\n const bytes = await fs.readFileBytes(path);\n const b64 = Buffer.from(bytes).toString('base64');\n return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });\n }\n const raw = await ctx.fs.readFile(path);\n // Binary guard: a non-text file (a .png without a recognized ext, .zip, .bin, …) read as utf8 is\n // either empty or replacement-char garbage — both silently pollute context. A silent empty tool\n // result is plausibly what tips a delegated model (cursor/composer) into an empty reply, where a\n // native model tolerates it. Return an explicit, non-empty marker so the model always sees SOMETHING.\n if (isBinary(raw)) {\n const bytes = await byteSize(ctx.fs, path, raw);\n return `[binary file: ${path} — ${bytes} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;\n }\n ctx.readState.set(ctx.fs.resolvePath(path), raw); // staleness guard tracks the REAL content\n if (raw === '') return `[empty file: ${path} — 0 bytes]`;\n // Mask secret values in config files so keys can live there usably-but-hidden (line count is preserved).\n const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;\n const total = content === '' ? 0 : content.split('\\n').length;\n const start = Math.max(0, offset ?? 0);\n const body = numberLines(content, start, limit);\n // snippet-with-pointer: when the slice doesn't cover the whole file, tell the\n // model what it's missing + how to pull it — so it expands precisely instead of re-reading blind.\n const shownEnd = limit != null ? Math.min(start + limit, total) : total;\n const shownCount = Math.max(0, shownEnd - start);\n if (shownCount >= total) return body; // whole file shown — no footer\n if (shownCount === 0) return `[no lines in range (offset ${start}${limit != null ? `, limit ${limit}` : ''}) — file has ${total} line(s)]`;\n return `${body}\\n\\n[lines ${start + 1}–${shownEnd} of ${total} · re-Read with offset/limit for the rest]`;\n },\n};\n\n/** Replace an exact, unique substring; requires a prior Read and guards against stale edits. */\nexport const editTool: AgentTool = {\n name: 'Edit',\n description:\n 'Replace an exact substring in a file. Requires a prior Read of the same file. `old_string` must occur exactly once — include surrounding context to disambiguate.',\n parameters: {\n type: 'object',\n required: ['path', 'old_string', 'new_string'],\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n },\n },\n async run({ path, old_string, new_string }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n const current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const count = old_string === '' ? 0 : current.split(old_string).length - 1;\n if (count > 1) throw new Error(`old_string is not unique in ${path} (${count} matches). Provide more surrounding context.`);\n let next: string, note = '';\n if (count === 1) {\n next = current.replace(old_string, () => new_string); // exact: function replacer, no $-pattern expansion\n } else {\n // exact match failed — try a whitespace-tolerant unique match before giving up (cuts re-read churn)\n const fuzzy = fuzzyLineReplace(current, old_string, new_string);\n if (fuzzy == null) throw new Error(`old_string not found in ${path}.`);\n next = fuzzy;\n note = ' (whitespace-tolerant match)';\n }\n if (ctx.lint) { const err = ctx.lint(path, next); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, next);\n ctx.readState.set(key, next);\n return `Edited ${path}${note}`;\n },\n};\n\n/** Session-exit tool: the model calls this when the user wants to end the conversation.\n * The `onExit` callback is injected by the host (CLI sets it to flip a flag that breaks the REPL loop). */\nexport function exitSessionTool(onExit: () => void): AgentTool {\n return {\n name: 'ExitSession',\n description:\n 'End the current session and exit the CLI. Call this when the user says goodbye, asks to quit, ' +\n 'or clearly indicates they want to stop the conversation (e.g. \"ok bye\", \"that\\'s all\", \"exit\", \"goodnight\").',\n parameters: { type: 'object', properties: {} },\n async run() {\n onExit();\n return 'Session ending. Goodbye!';\n },\n };\n}\n\nexport function defaultTools(): AgentTool[] {\n return [bashTool, readTool, editTool];\n}\n\n/**\n * The full catalog of selectable tools, keyed by name. The evolve loop's mutation\n * surface picks from this registry; embedders can build a custom tool set by name.\n */\nexport function toolRegistry(): Record<string, AgentTool> {\n const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool, webSearchAnthropicTool];\n return Object.fromEntries(all.map((t) => [t.name, t]));\n}\n\n/** Resolve a list of tool names against `available` (host-supplied tools, e.g. a parent agent's\n * toolbelt or mounted MCP tools) first, then the built-in registry. Unknown names throw.\n * Without `available` an allowlist could only ever name a built-in — so a def could not scope a\n * child to a tool its parent injected (`Bash`, `ToolSearch`, …). */\nexport function toolsByName(names: string[], available?: AgentTool[]): AgentTool[] {\n const reg = toolRegistry();\n const extra = Object.fromEntries((available ?? []).map((t) => [t.name, t]));\n return names.map((n) => {\n const t = extra[n] ?? reg[n];\n if (!t) throw new Error(`unknown tool '${n}'. Known: ${[...new Set([...Object.keys(extra), ...Object.keys(reg)])].join(', ')}`);\n return t;\n });\n}\n","/**\n * Wire types mirroring ai.libx.js (OpenAI-style chat). We type the transport\n * structurally via `ChatLike`, so an ai.libx.js `AIClient` is a drop-in — and a\n * `FakeAIClient` works in tests — with no hard runtime dependency on ai.libx.js.\n */\n\nimport { readFileSync, statSync, realpathSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { randomBytes } from 'node:crypto';\nimport { forComponent } from './logging';\n\nconst log = forComponent('llm');\n\nexport type Role = 'system' | 'user' | 'assistant' | 'tool';\n\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string }; // arguments is a JSON string\n}\n\n/** One part of a multimodal message (mirrors ai.libx.js ContentPart) — text or an image URL/data-URI. */\nexport interface ContentPart {\n type: 'text' | 'image_url';\n text?: string;\n image_url?: { url: string };\n}\n\n/** A message's content is either plain text or an array of multimodal parts (images + text). */\nexport type MessageContent = string | ContentPart[];\n\nexport interface Message {\n role: Role;\n content: MessageContent;\n name?: string;\n tool_call_id?: string;\n tool_calls?: ToolCall[];\n}\n\n/** Flatten any message content to its text (string as-is; parts → concatenated text) — for length\n * estimation, summaries, and display. Non-text parts (images) contribute a short placeholder. */\nexport function contentText(content: MessageContent | undefined): string {\n if (content == null) return '';\n if (typeof content === 'string') return content;\n return content.map((p) => (p.type === 'text' ? (p.text ?? '') : '[image]')).join(p_sep(content));\n}\nconst p_sep = (parts: ContentPart[]): string => (parts.length > 1 ? '\\n' : '');\n\n/** Build an image content part from a data-URI or http(s) URL. */\nexport function imagePart(url: string): ContentPart {\n return { type: 'image_url', image_url: { url } };\n}\n\n/** Stub left when an inline data-URI image is elided from the stored transcript (RAM + disk). */\nexport const IMAGE_ELIDE_STUB = '[image elided — re-attach @path or /paste to view again]';\n\n/** True when content carries an inline base64 image (the heavy case — http(s) URLs are tiny). */\nexport function messageHasInlineImage(content: MessageContent | undefined): boolean {\n if (content == null || typeof content === 'string') return false;\n return content.some((p) => p.type === 'image_url' && (p.image_url?.url ?? '').startsWith('data:'));\n}\n\n/** Byte-weight for context budgeting — counts base64 payloads, not the `[image]` display placeholder. */\nexport function contentBytes(content: MessageContent | undefined): number {\n if (content == null) return 0;\n if (typeof content === 'string') return content.length;\n let n = 0;\n for (const p of content) {\n if (p.type === 'text') { n += (p.text ?? '').length; continue; }\n const url = p.image_url?.url ?? '';\n const ref = parseImageRef(url);\n // A ref is ~60 chars on disk but becomes the whole file on the wire IF it survives the send-time\n // window. Callers that budget the real payload run `expandImagesForSend` FIRST (Agent.trimContext)\n // or use `sendBytes` — this is the honest weight for a still-unexpanded ref.\n n += (ref ? ref.bytes : url.length) || 7;\n }\n return n;\n}\n\n/** Replace inline data-URI image parts with a text stub; http(s) URLs, image REFS and plain text\n * unchanged — a ref is ~60 bytes and stays re-expandable, so there is nothing to reclaim. */\nexport function elideInlineImages(content: MessageContent, stub = IMAGE_ELIDE_STUB): MessageContent {\n return elideImageParts(content, (url) => url.startsWith('data:'), stub);\n}\n\n/** Replace EVERY heavy image part — inline base64 AND refs — with the stub. This is the destructive\n * form used by the `/compact` elision ladder: a ref is cheap on disk but weighs its whole file on the\n * wire while it's inside the send window, so it must be shave-able or the ladder falls through to\n * shredding (then dropping) the user's own text. `elideInlineImages` stays the non-destructive form. */\nexport function elideImages(content: MessageContent, stub = IMAGE_ELIDE_STUB): MessageContent {\n return elideImageParts(content, (url) => url.startsWith('data:') || isImageRefUrl(url), stub);\n}\n\nfunction elideImageParts(content: MessageContent, hit: (url: string) => boolean, stub: string): MessageContent {\n if (typeof content === 'string') return content;\n let changed = false;\n const out: ContentPart[] = [];\n for (const p of content) {\n if (p.type !== 'image_url' || !hit(p.image_url?.url ?? '')) { out.push(p); continue; }\n changed = true;\n out.push({ type: 'text', text: stub });\n }\n return changed ? out : content;\n}\n\n/**\n * A persist-safe REFERENCE to an image FILE. Stored in the transcript (and the session JSON) instead\n * of the megabytes of base64 — `expandImagesForSend` turns it back into a data-URI in the OUTGOING\n * request only, so neither the heap nor the disk ever holds historical payloads.\n *\n * SECURITY: a ref is a CAPABILITY, not a path. The URL carries an unguessable token; the real path\n * lives only in this in-process registry, and only HOST code that already did the jail/permission\n * check can mint one (`mintImageRef`, used by the CLI's @image attach and the image `Read` tool).\n * Tool results are MODEL-CONTROLLED text (bash prints whatever it likes) — parsing a path out of them\n * could never be the authorization. An unknown/evicted token is simply not a ref: it degrades to the\n * elision stub, never a file read. That also covers a resumed session in a fresh process.\n * Shape stays inside `image_url.url` so old readers/serializers need no schema change:\n * `agentx-image:<mime>;<token>;<path>`\n * The trailing path is INERT BOOKKEEPING — `parseImageRef` never reads it, so it can't authorize\n * anything. It exists only so `remintImageRefs` can re-grant the capability on session load, after\n * re-checking the path against an allowed root (see there).\n */\nexport const IMAGE_REF_SCHEME = 'agentx-image:';\n\nexport interface ImageRef {\n mime: string;\n /** Absolute on-disk path. Never derived from model output — only from a mint-time capability. */\n path: string;\n /** base64 wire weight, stat'ed AT MINT TIME (so an overwritten-in-place path can never go stale). */\n bytes: number;\n}\n\n/** Bounded so a long session can't grow the registry without limit; oldest minted token is evicted\n * first (and an evicted ref degrades to the stub, exactly like a missing file). */\nconst REF_REGISTRY_MAX = 256;\nconst refRegistry = new Map<string, ImageRef>();\n\n/**\n * Mint an image-ref URL for an ALREADY-AUTHORIZED absolute path. Host-only: call this after the\n * jail/permission check, never on a path that came out of model output.\n */\nexport function mintImageRef(path: string, mime: string): string {\n const token = randomBytes(16).toString('hex');\n let bytes = 0;\n try { bytes = Math.ceil(statSync(path).size * 4 / 3); } // base64 expansion — what actually goes on the wire\n catch (e) { log.warn(`image ref stat failed, weighing as 0: ${path}`, e); }\n refRegistry.set(token, { mime, path, bytes });\n while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value as string);\n return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;\n}\n\n/** Test/host hook: forget every minted ref (simulates a fresh process / resumed session). */\nexport function clearImageRefs(): void { refRegistry.clear(); }\n\n/** Build an image REFERENCE part for an authorized path. */\nexport function imageRefPart(path: string, mime: string): ContentPart {\n return { type: 'image_url', image_url: { url: mintImageRef(path, mime) } };\n}\n\n/** Does this URL claim to be an image ref? (Claim only — resolution is `parseImageRef`.) */\nconst isImageRefUrl = (url: string | undefined): boolean => !!url && url.startsWith(IMAGE_REF_SCHEME);\n\n/** Resolve an image-ref URL through the registry; null when it isn't one, or the token is unknown\n * (forged by the model, evicted, or from a previous process). NEVER trusts the URL's own text. */\nexport function parseImageRef(url: string | undefined): ImageRef | null {\n const parts = refUrlParts(url);\n const ref = parts && refRegistry.get(parts.token);\n if (!ref) { log.debug('image ref not in registry — treating as non-image text'); return null; }\n return ref;\n}\n\n/** Split a ref URL into its literal fields. `path` here is UNTRUSTED bookkeeping — never a capability. */\nfunction refUrlParts(url: string | undefined): { mime: string; token: string; path: string } | null {\n if (!isImageRefUrl(url)) return null;\n const rest = url!.slice(IMAGE_REF_SCHEME.length);\n const a = rest.indexOf(';');\n if (a < 0) return null;\n const b = rest.indexOf(';', a + 1);\n return { mime: rest.slice(0, a), token: b < 0 ? rest.slice(a + 1) : rest.slice(a + 1, b), path: b < 0 ? '' : rest.slice(b + 1) };\n}\n\n/**\n * Session LOAD: re-grant the capability for stored image parts whose token died with the previous\n * process, so a resumed session's recent screenshot is visible again (parity with the pre-ref\n * behaviour, where `elideStaleImages` kept the last inline image).\n *\n * A session file is WRITABLE BY THE MODEL (it can Write/Edit it, and the user may hand-edit it), so a\n * stored path is untrusted input — its provenance proves nothing. The ROOT CHECK is what makes this\n * safe: only a path whose REALPATH (symlinks resolved) sits inside one of `roots` is re-minted.\n * Anything else is left unresolvable and degrades to the elision stub, with a warn.\n *\n * Only image PARTS in user/assistant content are considered. Tool-result strings stay inert forever —\n * those are the model's own stdout and must never mint anything.\n * Pure view: returns a new array only when something changed.\n */\nexport function remintImageRefs(messages: Message[], roots: string[]): Message[] {\n const allowed: string[] = [];\n for (const r of roots) {\n try { allowed.push(realpathSync(resolve(r))); } catch (e) { log.debug(`image-ref root unavailable: ${r}`, e); }\n }\n if (allowed.length === 0) return messages;\n const inside = (p: string): boolean => allowed.some((root) => p === root || p.startsWith(root + sep));\n let changed = false;\n const out = messages.map((m) => {\n if ((m.role !== 'user' && m.role !== 'assistant') || typeof m.content === 'string' || m.content == null) return m;\n let touched = false;\n const parts = m.content.map((p) => {\n if (p.type !== 'image_url') return p;\n const f = refUrlParts(p.image_url?.url);\n if (!f || refRegistry.has(f.token)) return p; // not a ref, or still live in this process\n let real: string;\n try { real = realpathSync(f.path); } catch (e) { log.warn(`resumed image ref unreadable, will stub: ${f.path}`, e); return p; }\n if (!inside(real)) { log.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`); return p; }\n touched = true;\n return { type: 'image_url', image_url: { url: mintImageRef(real, f.mime) } } as ContentPart;\n });\n if (!touched) return m;\n changed = true;\n return { ...m, content: parts };\n });\n return changed ? out : messages;\n}\n\n/** True when content carries an image REF part (weightless on disk, expanded at send time). The\n * CLAIM is enough: an unresolvable ref (resumed session / evicted token) still counts as image-bearing\n * so the send path turns it into the stub instead of shipping the unresolvable scheme to the provider. */\nexport function messageHasImageRef(content: MessageContent | undefined): boolean {\n if (content == null || typeof content === 'string') return false;\n return content.some((p) => p.type === 'image_url' && isImageRefUrl(p.image_url?.url));\n}\n\n/** Any image at all — inline base64 (legacy sessions) or a ref. Drives `keepRecent` windows. */\nexport function messageHasImage(content: MessageContent | undefined): boolean {\n return messageHasInlineImage(content) || messageHasImageRef(content);\n}\n\n/** `Read` on an image persists this instead of the base64; expanded to `{dataUrl}` at send time.\n * `path` must already have passed the filesystem's jail check (see `mintImageRef`). */\nexport function imageRefResult(path: string, mime: string): string {\n return JSON.stringify({ imageRef: mintImageRef(path, mime), image: path });\n}\n\n/** The registered ref behind a tool message's `{\"imageRef\":…}` JSON body, if that's what it is.\n * A model-forged body parses fine but resolves to null — the token isn't in the registry. */\nexport function toolImageRef(content: MessageContent | undefined): ImageRef | null {\n if (typeof content !== 'string' || !content.startsWith('{\"imageRef\":')) return null;\n try { return parseImageRef(JSON.parse(content).imageRef); } catch (e) { log.debug('bad imageRef tool result', e); return null; }\n}\n\n/** Elide inline images from all but the most-recent `keepRecent` image-bearing messages (mutates copies). */\nexport function elideStaleImages(messages: Message[], keepRecent = 1): Message[] {\n const idx = messages.map((m, i) => (messageHasImage(m.content) ? i : -1)).filter((i) => i >= 0);\n const drop = new Set(idx.slice(0, Math.max(0, idx.length - keepRecent)));\n if (drop.size === 0) return messages;\n let changed = false;\n const out = messages.map((m, i) => {\n if (!drop.has(i)) return m;\n const c = elideInlineImages(m.content);\n if (c === m.content) return m;\n changed = true;\n return { ...m, content: c };\n });\n return changed ? out : messages;\n}\n\n/**\n * The send-time verdict per message index. User ATTACHMENTS and tool-result image `Read`s get\n * INDEPENDENT windows: an image the model Reads mid-turn must never evict the image the user just\n * pasted (the spec's non-goal — vision on the paste turn must keep working), and vice-versa.\n */\nfunction imageWindows(messages: Message[], keepRecent: number): Set<number> {\n const attach: number[] = [];\n const tool: number[] = [];\n messages.forEach((m, i) => {\n if (toolImageRef(m.content)) tool.push(i);\n else if (messageHasImage(m.content)) attach.push(i);\n });\n const k = Math.max(0, keepRecent);\n return new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);\n}\n\n/** Is this message image-bearing at all (either kind)? */\nconst bearsImage = (m: Message): boolean => messageHasImage(m.content) || toolImageRef(m.content) != null;\n\n/**\n * SEND-PATH expansion: image REFS become real data-URIs only in the outgoing request, and only for the\n * most-recent `keepRecent` messages of EACH kind (user attachment / tool image Read) — older ones (refs\n * AND legacy inline base64) collapse to the elision stub. Pure view: the caller's messages are never mutated.\n * A missing/unreadable file (or an unregistered ref) degrades to the same stub and is LOGGED — never throws.\n */\nexport function expandImagesForSend(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): Message[] {\n if (!messages.some(bearsImage)) return messages;\n const keep = imageWindows(messages, keepRecent);\n let changed = false;\n const out = messages.map((m, i) => {\n const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);\n if (content === m.content) return m;\n changed = true;\n return { ...m, content };\n });\n return changed ? out : messages;\n}\n\n/**\n * Byte-weight of a transcript AS IT WILL GO ON THE WIRE — refs inside the keep window weigh their\n * file's base64 size, everything stale weighs the stub — WITHOUT materializing a single byte of\n * base64. This is the honest number for `/context` and the CLI footer, where the old behaviour made\n * every historical screenshot immortal and produced false \"90% full\" warnings.\n */\nexport function sendBytes(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): number {\n let n = 0;\n for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;\n return n;\n}\n\n/** `sendBytes` split per message — same window, same weights. Lets a budgeter drop/shave message-by-\n * message without re-deriving the window (and without charging a stale ref its file weight). */\nexport function sendBytesPer(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): number[] {\n const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;\n return messages.map((m, i) =>\n wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0));\n}\n\n/** contentBytes, but aware of whether this message's images survive the send-time window. */\nfunction wireBytes(content: MessageContent | undefined, kept: boolean, stub: string): number {\n const tool = toolImageRef(content);\n if (tool) return kept ? tool.bytes : stub.length;\n if (content == null || typeof content === 'string') return contentBytes(content);\n if (kept) return contentBytes(content);\n let n = 0;\n for (const p of content) {\n if (p.type === 'text') { n += (p.text ?? '').length; continue; }\n const url = p.image_url?.url ?? '';\n n += url.startsWith('data:') || isImageRefUrl(url) ? stub.length : url.length || 7;\n }\n return n;\n}\n\n/** Read a referenced image as a data-URI; null (logged) when the file is gone/unreadable. */\nfunction readRefDataUrl(ref: ImageRef): string | null {\n try { return `data:${ref.mime};base64,${readFileSync(ref.path).toString('base64')}`; }\n catch (e) { log.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e); return null; }\n}\n\n/** Refs → data-URIs (tool `{imageRef}` bodies → the `{dataUrl}` shape the wire adapter renders). */\nfunction expandContent(content: MessageContent, stub: string): MessageContent {\n const tool = toolImageRef(content);\n if (tool) {\n const url = readRefDataUrl(tool);\n return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;\n }\n if (typeof content === 'string') return content;\n let changed = false;\n const out = content.map((p) => {\n if (p.type !== 'image_url' || !isImageRefUrl(p.image_url?.url)) return p;\n changed = true;\n // Unresolvable (resumed session / evicted token) → stub. Content PARTS are host-authored, so an\n // unknown one is a lost capability, not model text — and the scheme must never reach the provider.\n const ref = parseImageRef(p.image_url?.url);\n const url = ref && readRefDataUrl(ref);\n return url ? imagePart(url) : ({ type: 'text', text: stub } as ContentPart);\n });\n return changed ? out : content;\n}\n\n/** Stale image-bearing content as it goes on the wire: no base64, no ref the provider can't resolve. */\nfunction stripContent(content: MessageContent, stub: string): MessageContent {\n if (toolImageRef(content)) return stub;\n if (typeof content === 'string') return content;\n let changed = false;\n const out = content.map((p) => {\n const url = p.type === 'image_url' ? (p.image_url?.url ?? '') : '';\n if (!url.startsWith('data:') && !isImageRefUrl(url)) return p;\n changed = true;\n return { type: 'text', text: stub } as ContentPart;\n });\n return changed ? out : content;\n}\n\nexport interface Tool {\n type: 'function';\n function: { name: string; description?: string; parameters: object };\n}\n\nexport interface ChatResponse {\n content: string;\n finishReason?: string;\n toolCalls?: ToolCall[];\n model?: string;\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n /** Tools a self-executing runtime (cursor/claude-code) ran inside its OWN loop — correlated from\n * the display-only `toolActivity` stream. Not dispatched by us; persisted so the transcript keeps\n * a faithful record (audit + resume) instead of being narration-only. */\n delegatedTools?: { id: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error' }[];\n /** A delegated runtime (cursor/claude-code) ran tools this turn but never CLOSED it: either no assistant\n * text followed the last tool (it went silent post-work), or the text it did emit ends on an action it\n * announced and never took. Signals the loop's closure reflex (see AgentOptions.closeDelegatedTurns). */\n endedWithoutClosing?: boolean;\n}\n\n/**\n * Does this assistant text end mid-plan — on an action the model ANNOUNCED but never took?\n *\n * Cursor/composer narrates as it works (\"Searching the codebase…\", \"Also checking for X\"), and its run\n * can settle FINISHED while the narration is still mid-plan. The turn then looks closed (finishReason\n * 'stop', text present) but delivered no answer. Measured over the local cursor session corpus: ~4.6%\n * of tool-using turns end this way.\n *\n * Matches only FIRST-PERSON commitments (\"Let me…\", \"I'll…\", \"I need to…\") and progressive narration\n * (\"Checking…\", \"Expanding…\"). Deliberately excludes \"Next step: …\" / \"Next run will…\" and \"Let me know\n * if…\" — those are legitimate things to say TO the user, and nudging them would talk over a finished\n * answer. A false positive costs one extra step, so the rule stays conservative rather than clever.\n *\n * The two arms are deliberately scoped differently:\n *\n * COMMITMENT — first-person SINGULAR \"I'll / let me / I'm going to\", plus the one obligation form the\n * corpus actually stalled on: \"I need to…\". These carry their own subject and tense, so they stay\n * unambiguous anywhere in the sentence and may be matched clause-by-clause — the observed stuck turn\n * hid its commitment in a trailing \"…, so I need to verify …\" clause. Past-tense reports of the same\n * (\"I needed to\") are excluded: they describe work already reasoned about, not work still pending.\n * Second person (\"you'll need to…\") is excluded — that is a recommendation to the user.\n *\n * Deliberately NOT matched: \"we …\" and the softer obligations \"should / must / have to / want to\".\n * They read as recommendations TO the user on a FINISHED answer (\"All tests pass. We should be good to\n * go.\", \"I have to admit, the numbers were surprising.\"), the same category as the NEXT_STEP and\n * SIGN_OFF carve-outs — and nudging a finished answer spends a metered step talking over it. No turn in\n * the corpus needed them, so the conservative bias keeps them out.\n *\n * PROGRESSIVE — a bare gerund (\"Checking the images…\") is only a commitment at the HEAD of the final\n * sentence. Mid-sentence it is almost always a participial modifier of a COMPLETED action\n * (\"I pruned the cache, reclaiming 2.6 GB\"), so it is not matched clause-wise.\n */\nconst COMMITMENT =\n /^(?:also\\s+|now\\s+|then\\s+|so\\s+)*(?:let me\\b(?!\\s+know\\b)|i'?ll\\b|i will\\b|i'?m going to\\b|i\\s+(?:still\\s+|now\\s+|first\\s+)*need to\\b)/i;\nconst PROGRESSIVE =\n /^(?:also\\s+|now\\s+|then\\s+)*(?:re-?)?(?:check|search|scan|read|verify|run|inspect|expand|look|examin|continu|proceed|try|fetch|load|open|test|build|grep|find|review|trac|dig|explor)\\w*ing\\b/i;\nconst NEXT_STEP = /^next (?:step|run|up)\\b/i;\n// \"Let me know if…\" is a SIGN-OFF, not an announced action. It only ever reached the `let me\\b` arm on\n// delegated turns before; once the sawTool gate is dropped (announce-and-stop turns run no tools at all)\n// it would fire on every polite closing line, so exclude it explicitly (in COMMITMENT and here).\nconst SIGN_OFF = /^let me know\\b/i;\n\n/** Split a sentence into clauses at punctuation and coordinators, so a trailing commitment is visible. */\nfunction clauses(sentence: string): string[] {\n return sentence\n .split(/[,;:]\\s*|\\s+(?:so|but|and|then|because|however|therefore|thus|although|though)\\s+/i)\n .map((c) => c.trim())\n .filter(Boolean);\n}\n\nexport function endsOnAnnouncedAction(text: string): boolean {\n const sentences = text.split(/(?<=[.!?])\\s+|\\n+/).map((s) => s.trim()).filter(Boolean);\n const last = sentences[sentences.length - 1];\n if (!last || NEXT_STEP.test(last) || SIGN_OFF.test(last)) return false;\n if (PROGRESSIVE.test(last)) return true;\n return clauses(last).some((c) => !NEXT_STEP.test(c) && !SIGN_OFF.test(c) && COMMITMENT.test(c));\n}\n\n/**\n * One incremental event from a streamed `chat({stream:true})` call — mirrors\n * ai.libx.js's `StreamChunk` (OpenAI-style): each chunk carries a `content`\n * text delta; the terminal chunk carries `finishReason` and the accumulated\n * `toolCalls`. Consuming the stream and folding the deltas reconstructs the\n * same `ChatResponse` the non-stream path returns.\n */\nexport interface StreamChunk {\n content: string;\n finishReason?: string;\n index?: number;\n toolCalls?: ToolCall[]; // accumulated tool calls (typically on the final chunk)\n reasoningContent?: string;\n /** Display-only tool activity from a self-executing backend (Cursor CLI) — surfaced as a host\n * `tool_activity` event, never dispatched. Mirrors ai.libx.js's StreamChunk.toolActivity. */\n toolActivity?: { id?: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error'; truncated?: { args?: boolean; result?: boolean } };\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number }; // on the terminal chunk, when the provider reports it\n}\n\nexport interface ChatOptions {\n model: string;\n messages: Message[];\n tools?: Tool[];\n toolChoice?: unknown;\n stream?: boolean;\n /** Cancel the request/stream. Forwarded to providers that honor it; the Agent also stops consuming on abort. */\n signal?: AbortSignal;\n [k: string]: unknown;\n}\n\n/** Minimal shape of an ai.libx.js AIClient that the Agent drives. */\nexport interface ChatLike {\n chat(options: ChatOptions): Promise<ChatResponse | AsyncIterable<StreamChunk>>;\n}\n","// Import the log module directly from libx.js source: libx.js's main bundle\n// doesn't re-export `log` as a named ESM export, and source-importing keeps\n// libx.js patches live (no rebuild) — matching the `bun link` workflow.\nimport { log } from 'libx.js/src/modules/log';\n\n/** Component-scoped logger (libx.js). debug/verbose gated via DEBUG env/localStorage. */\nexport const forComponent = (name: string) => log.forComponent(name);\nexport { log };\n","/**\n * Mask secret-looking values in arbitrary text before it reaches the model.\n *\n * Two complementary seams use this: real-shell output (`cat .env`, `printenv`) and the\n * `Read` tool (so provider keys stored in `.agent/settings.json` are usable-but-masked).\n * The FS jail hides whole secret FILES by name; this hides secret VALUES wherever they\n * surface in otherwise-legitimate content.\n *\n * Both regexes are linear (no nested quantifiers) — safe against catastrophic backtracking\n * and cheap enough to run on every tool output (see tests/redact.bench).\n */\n\nexport const REDACTED = '‹redacted›';\n\n/** Config/control files that may carry provider keys — readers (Read/Grep) mask secret VALUES in\n * these while keeping the rest readable. (Whole secret FILES like .env are hidden by the FS jail.) */\nexport const CONFIG_FILE_RE = /(^|\\/)\\.(agent|claude)\\/(settings(\\.[\\w-]+)?\\.json|config\\.(json|js|mjs|cjs|ts))$/i;\n\n// (A) `NAME=value` / `\"name\": \"value\"` pairs where NAME looks like a secret. Masks the value only,\n// so the agent still sees WHICH key exists (useful config context) without the secret itself.\nconst SECRET_PAIR =\n /((?:^|[\\s,{[])(?:export\\s+)?[\"']?[\\w.\\-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_?KEY|AUTH(?:_?TOKEN)?|BEARER)[\\w.\\-]*[\"']?\\s*[:=]\\s*)([\"']?)([^\\s\"',{}\\]]+)/gi;\n\n// (B) Bare tokens by well-known shape — catches secrets that appear without an obvious key\n// (Authorization headers, URLs, JSON dumps). Conservative prefixes to avoid false positives.\nconst SECRET_TOKEN =\n /\\b(sk-ant-[\\w-]{12,}|sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[\\w]{20,}|xox[baprs]-[\\w-]{10,}|AKIA[0-9A-Z]{12,}|AIza[\\w-]{20,}|eyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,})\\b/g;\n\n/** Return `text` with secret values masked. Cheap no-op when nothing matches. */\nexport function redactSecrets(text: string): string {\n if (!text) return text;\n return text\n .replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`)\n .replace(SECRET_TOKEN, REDACTED);\n}\n","import type { IFilesystem } from '@livx.cc/wcli/core';\nimport type { AgentTool, ToolContext } from './tools';\nimport type { ChatResponse } from './llm';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\n\n/**\n * Structured file tools — typed results straight from the VFS, no `bash` parsing.\n * These close the efficiency gap to Claude Code (its Grep/Glob/Write return one\n * structured result where our agent otherwise drives find/grep pipelines and re-reads).\n * Backend-agnostic: pure IFilesystem walks, so they run on Mem/Disk/IndexedDb alike.\n */\n\n/** Throw the moment a run is cancelled, so a long file walk/scan bails instead of pegging the CPU\n * uninterruptibly. dispatch() catches the throw → the run loop sees `aborted` and ends the turn. */\nfunction ckAbort(signal?: AbortSignal): void {\n if (signal?.aborted) throw new Error('aborted');\n}\n\n/**\n * Directory names never worth walking. Pruned by NAME at every level (not by path), because the\n * cost is where they nest: a real-FS root of a JS deployment is mostly `node_modules`, and on a\n * measured 412k-file app root a single `Glob` spent 31s almost entirely inside it.\n */\nexport const DEFAULT_SKIP_DIRS = ['node_modules', '.git', 'dist', '.tmp'];\n\n/** Depth below a root at which the walk stops. Deep enough for any real source tree; a bound at all\n * is what turns a symlink cycle from a hang into a truncated result. */\nexport const DEFAULT_MAX_DEPTH = 24;\n\n/** How the file-scanning tools (Glob/Grep) decide WHAT to walk. */\nexport interface FileScanOptions {\n /** Roots to walk when a call names no `path`. Default: `[fs.getCwd()]`, i.e. today's behaviour.\n * A host with its data outside the cwd passes both, so a search can reach it at all. */\n roots?: string[];\n /** Directory names pruned during the walk. Default: {@link DEFAULT_SKIP_DIRS}. */\n skipDirs?: string[];\n /** Max depth below each root. Default: {@link DEFAULT_MAX_DEPTH}. */\n maxDepth?: number;\n}\n\ninterface WalkState {\n skip: Set<string>;\n maxDepth: number;\n /** Real paths already visited — the cycle/duplication guard. A `vendor` symlink pointing back\n * into the tree (or `node_modules/.bin`) otherwise re-walks the same subtree repeatedly.\n * PER ROOT, not shared: with one shared set a second root that is a symlink to (or into) the\n * first is \"already seen\" and silently yields nothing — the caller asked for a place and got an\n * empty answer, which is the absent-vs-unreachable confusion this file is trying to kill. */\n seen: Set<string>;\n /** Directory names actually pruned, and depth cuts actually made — reported to the caller, never\n * swallowed: a walk that refused to look somewhere must say so, or `(no matches)` is a lie. */\n prunedDirs: Set<string>;\n depthCuts: number;\n}\n\n/** `canonicalPath` (symlinks resolved) is an optional capability on disk-backed filesystems; on\n * backends without it the VFS path IS the identity, so it is its own canonical form. Note it must\n * be the symlink-resolving one: `realPath` is path arithmetic and reports a link and its target as\n * two different places, which cannot break a cycle. */\nfunction realOf(fs: IFilesystem, p: string): string {\n const rp = (fs as { canonicalPath?: (path: string) => string }).canonicalPath;\n if (typeof rp !== 'function') return p;\n try { return rp.call(fs, p) || p; } catch { return p; }\n}\n\n/** Recursively list every file path under `dir` (VFS-absolute), depth-first, sorted. Cancellable:\n * a cancelled run throws between entries so a huge tree walk (real-FS disk mode) doesn't wedge.\n * Prunes {@link DEFAULT_SKIP_DIRS}, bounds depth, and never enters the same real dir twice. */\nasync function walkFiles(fs: IFilesystem, dir: string, signal?: AbortSignal, out: string[] = [], st?: WalkState, depth = 0): Promise<string[]> {\n const state: WalkState = st ?? { skip: new Set(DEFAULT_SKIP_DIRS), maxDepth: DEFAULT_MAX_DEPTH, seen: new Set(), prunedDirs: new Set(), depthCuts: 0 };\n if (depth > state.maxDepth) { state.depthCuts++; return out; }\n const real = realOf(fs, dir);\n if (state.seen.has(real)) return out;\n state.seen.add(real);\n let entries: string[];\n try { entries = await fs.readDir(dir); } catch { return out; }\n for (const name of entries.sort()) {\n ckAbort(signal);\n if (state.skip.has(name)) { state.prunedDirs.add(name); continue; }\n const p = dir === '/' ? `/${name}` : `${dir}/${name}`;\n if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out, state, depth + 1);\n else out.push(p);\n }\n return out;\n}\n\n/** The roots a call actually walks: an explicit `path` wins, else the configured roots, else the\n * cwd. Nested roots are collapsed — walking `/a` and `/a/b` would double-report everything in b. */\nfunction scanRoots(fs: IFilesystem, path: unknown, opts?: FileScanOptions): string[] {\n const asked = path == null ? [] : (Array.isArray(path) ? path : [path]).map((p) => String(p)).filter(Boolean);\n const roots = (asked.length ? asked : (opts?.roots?.length ? opts.roots : [fsCwd(fs)])).map((r) => (r.length > 1 && r.endsWith('/') ? r.slice(0, -1) : r));\n return [...new Set(roots)].filter((r, _i, all) => !all.some((other) => other !== r && r.startsWith(`${other}/`)));\n}\n\n/** What a walk refused to look at — appended to results so a pruned/truncated search never reads\n * as an exhaustive one. Mirrors Grep's existing \"skipped N unreadable files\" note. */\nfunction scanNote(states: WalkState[]): string {\n const pruned = [...new Set(states.flatMap((st) => [...st.prunedDirs]))].sort();\n const cuts = states.reduce((n, st) => n + st.depthCuts, 0);\n const parts: string[] = [];\n if (pruned.length) parts.push(`skipped ${pruned.join(', ')} (pass skipDirs to include them)`);\n if (cuts) parts.push(`stopped at depth ${states[0]?.maxDepth} in ${cuts} place${cuts === 1 ? '' : 's'} (pass maxDepth to go deeper)`);\n return parts.length ? `\\n[scan: ${parts.join('; ')}]` : '';\n}\n\n/** Walk every root once, with the configured prune/depth/cycle guards. Per-call `skipDirs`/`maxDepth`\n * override the construction-time ones, so a consumer of the default singletons is never stuck with\n * a prune it needs to see past (`Glob '**\\/dist/*'`, a `.tmp/` workspace). */\nasync function walkRoots(\n fs: IFilesystem, roots: string[], signal: AbortSignal | undefined, opts?: FileScanOptions, call?: FileScanOptions,\n): Promise<{ files: string[]; note: string }> {\n const skip = new Set(call?.skipDirs ?? opts?.skipDirs ?? DEFAULT_SKIP_DIRS);\n const maxDepth = call?.maxDepth ?? opts?.maxDepth ?? DEFAULT_MAX_DEPTH;\n const out: string[] = [];\n const states: WalkState[] = [];\n for (const root of roots) {\n // A fresh `seen` per root: cross-root dedupe belongs to the RESULT (callers Set-dedupe), not to\n // the walk, where it silently erases a whole root that aliases another.\n const state: WalkState = { skip, maxDepth, seen: new Set(), prunedDirs: new Set(), depthCuts: 0 };\n states.push(state);\n await walkFiles(fs, root, signal, out, state, 0);\n }\n return { files: out, note: scanNote(states) };\n}\n\n/** Per-call scan overrides, parsed from tool args. */\nfunction callScan(args: { skipDirs?: unknown; maxDepth?: unknown }): FileScanOptions {\n const sd = args.skipDirs;\n return {\n skipDirs: Array.isArray(sd) ? sd.map(String) : typeof sd === 'string' ? sd.split(',').map((x) => x.trim()).filter(Boolean) : undefined,\n maxDepth: args.maxDepth == null ? undefined : Number(args.maxDepth),\n };\n}\n\nconst SCAN_PARAMS = {\n path: { type: 'string', description: 'optional directory (or list) to search instead of the default roots; absolute VFS path' },\n skipDirs: { type: 'string', description: `comma-separated directory names to prune (default: ${DEFAULT_SKIP_DIRS.join(', ')}); pass an empty string to prune nothing` },\n maxDepth: { type: 'number', description: `max directory depth below each root (default ${DEFAULT_MAX_DEPTH})` },\n} as const;\n\n/** Anchor a relative glob at a specific root (an absolute pattern anchors to itself). Per-ROOT, not\n * per-cwd: with two roots, anchoring `**\\/*.md` at the cwd alone would silently match nothing in\n * the other one — the \"search can't reach it, so it reports absent\" failure this all exists for. */\nfunction anchoredGlobAt(root: string, glob: string): RegExp {\n if (glob.startsWith('/')) return globToRegExp(glob);\n return globToRegExp(`${root === '/' ? '' : root}/${glob}`);\n}\n\n/** A path/scope matcher over several roots: matches if the pattern matches when anchored at ANY. */\nfunction globMatcher(roots: string[], glob: string): (p: string) => boolean {\n const res = roots.map((r) => anchoredGlobAt(r, glob));\n return (p: string) => res.some((re) => re.test(p));\n}\n\n/**\n * Translate a glob (`**`, `*`, `?`) into an anchored RegExp over VFS-absolute paths.\n * `caseInsensitive` is used by the JailedFilesystem denylist so `/.ENV` can't slip past\n * a `.env` rule (file-matching tools keep the default case-sensitive behavior).\n */\nexport function globToRegExp(glob: string, caseInsensitive = false): RegExp {\n const g = glob.startsWith('/') ? glob : `/${glob}`;\n let re = '';\n for (let i = 0; i < g.length; i++) {\n const c = g[i];\n if (c === '*') {\n if (g[i + 1] === '*') { re += '.*'; i++; if (g[i + 1] === '/') i++; }\n else re += '[^/]*';\n } else if (c === '?') re += '[^/]';\n else re += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n }\n return new RegExp(`^${re}$`, caseInsensitive ? 'i' : '');\n}\n\n/** The dir structured tools walk/anchor at — the FS's working dir. Keeps Glob/Grep/RepoMap scoped\n * to the project (or, in CC-parity disk mode where root '/' is the real machine, the launch dir)\n * instead of crawling the whole filesystem. Backward-compatible: Mem/jailed-at-cwd backends report '/'. */\nconst fsCwd = (fs: IFilesystem): string => fs.getCwd();\n\n/** Compile a glob, resolving a relative pattern under the FS cwd (mirrors a shell's cwd-relative globs).\n * Absolute patterns are used as-is. With cwd '/' this is identical to the old `/`-anchored behavior. */\nfunction anchoredGlob(fs: IFilesystem, glob: string): RegExp {\n const cwd = fsCwd(fs);\n const base = cwd === '/' ? '' : cwd;\n return globToRegExp(glob.startsWith('/') ? glob : `${base}/${glob}`);\n}\n\n/** List paths matching a glob, sorted — the structured alternative to `find`. */\nexport function makeGlobTool(opts?: FileScanOptions): AgentTool {\n return {\n name: 'Glob',\n description:\n 'Find files by glob pattern (e.g. \"**/*.ts\", \"src/**/*.test.ts\"). Returns sorted paths, one per line. ' +\n 'Space-separated patterns combine; `!`-prefix excludes (e.g. \"**/*.ts !**/*.test.ts\"). Optional `path` to search a specific directory. Prefer over `bash find` for locating files — one call, structured output.',\n parameters: {\n type: 'object',\n required: ['pattern'],\n properties: {\n pattern: { type: 'string', description: 'glob pattern(s); ** matches across directories; prefix a pattern with ! to exclude' },\n ...SCAN_PARAMS,\n },\n },\n async run({ pattern, path, skipDirs, maxDepth }, ctx) {\n const roots = scanRoots(ctx.fs, path, opts);\n const pats = String(pattern ?? '').trim().split(/\\s+/).filter(Boolean);\n const include = pats.filter((p) => !p.startsWith('!')).map((p) => globMatcher(roots, p));\n const exclude = pats.filter((p) => p.startsWith('!')).map((p) => globMatcher(roots, p.slice(1)));\n const includes = include.length ? include : [globMatcher(roots, '**')]; // only-excludes → start from everything\n const { files, note } = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));\n const hits = files.filter((p) => includes.some((m) => m(p)) && !exclude.some((m) => m(p)));\n return (hits.length ? [...new Set(hits)].sort().join('\\n') : '(no matches)') + note;\n },\n };\n}\n\n/** Search file contents by regex, returning typed `path:line:text` hits with optional context. */\nexport function makeGrepTool(opts?: FileScanOptions): AgentTool {\n return {\n name: 'Grep',\n description:\n 'Search file contents by regex. Returns `path:line: text` hits. Optional `glob` to scope files, `path` to search a specific directory, `context` for surrounding lines, `filesOnly` for matching paths only. Prefer over `bash grep` for file content search — structured results, no re-parse needed. Use `bash` instead for running commands, tests, or piped workflows.',\n parameters: {\n type: 'object',\n required: ['pattern'],\n properties: {\n pattern: { type: 'string', description: 'JS regular expression' },\n glob: { type: 'string', description: 'optional file glob to restrict the search' },\n ...SCAN_PARAMS,\n context: { type: 'number', description: 'lines of context before/after each hit' },\n filesOnly: { type: 'boolean', description: 'only list matching file paths' },\n },\n },\n async run({ pattern, glob, path, skipDirs, maxDepth, context, filesOnly }, ctx) {\n let re: RegExp;\n try { re = new RegExp(String(pattern ?? '')); } catch (e) { throw new Error(`invalid regex: ${String(e)}`); }\n const roots = scanRoots(ctx.fs, path, opts);\n const scope = glob ? globMatcher(roots, String(glob)) : null;\n const walked = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));\n const files = [...new Set(walked.files)].sort().filter((p) => !scope || scope(p));\n const ctxN = Math.max(0, Number(context ?? 0));\n const out: string[] = [];\n const matched: string[] = [];\n let skipped = 0; // unreadable files (permissions, races) — surfaced, not silently dropped\n for (const path of files) {\n ckAbort(ctx.signal); // cancellable per-file: a wide grep over a big tree stops on Esc, not after\n let content: string;\n try { content = await ctx.fs.readFile(path); } catch { skipped++; continue; }\n const lines = content.split('\\n');\n const mask = CONFIG_FILE_RE.test(path); // mask secret values from config files in the hits\n let fileHit = false;\n for (let i = 0; i < lines.length; i++) {\n if (!re.test(lines[i])) continue;\n fileHit = true;\n if (filesOnly) break;\n const lo = Math.max(0, i - ctxN), hi = Math.min(lines.length - 1, i + ctxN);\n for (let j = lo; j <= hi; j++) out.push(`${path}:${j + 1}: ${mask ? redactSecrets(lines[j]) : lines[j]}`);\n }\n if (fileHit) matched.push(path);\n }\n const note = (skipped ? `\\n[skipped ${skipped} unreadable file${skipped === 1 ? '' : 's'}]` : '') + walked.note;\n if (filesOnly) return (matched.length ? matched.join('\\n') : '(no matches)') + note;\n return (out.length ? out.join('\\n') : '(no matches)') + note;\n },\n };\n}\n\n/** Default instances — cwd-rooted, standard prunes. Hosts that need other roots build their own. */\nexport const globTool: AgentTool = makeGlobTool();\nexport const grepTool: AgentTool = makeGrepTool();\n\n/** A line is a top-level declaration worth showing in the repo map. */\nconst SIG_RE = /^\\s*(export\\b|(?:export\\s+)?(?:async\\s+)?function\\s+\\*?\\w|(?:export\\s+)?(?:abstract\\s+)?class\\s+\\w|(?:export\\s+)?interface\\s+\\w|(?:export\\s+)?type\\s+\\w|(?:export\\s+)?enum\\s+\\w)/;\nconst isCode = (p: string) => /\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p);\nconst isDoc = (p: string) => /\\.(md|mdx|txt)$/.test(p);\n\n/** Extract heading hierarchy + first-paragraph summaries from a markdown file. */\nfunction docOutlineOf(content: string, cap = 20): string[] {\n const out: string[] = [];\n const lines = content.split('\\n');\n for (let i = 0; i < lines.length && out.length < cap; i++) {\n const hm = lines[i].match(/^(#{1,4})\\s+(.+)/);\n if (hm) {\n out.push(hm[0].slice(0, 120));\n for (let j = i + 1; j < lines.length; j++) {\n const l = lines[j].trim();\n if (!l) continue;\n if (l.startsWith('#')) break;\n out.push(' ' + l.slice(0, 120));\n break;\n }\n }\n }\n return out;\n}\n\n/** One file's exported/top-level signatures (no bodies), capped. */\nfunction signaturesOf(content: string, cap = 40): string[] {\n const out: string[] = [];\n for (const line of content.split('\\n')) {\n if (!SIG_RE.test(line)) continue;\n let sig = line.trim();\n const brace = sig.indexOf('{');\n if (brace > 0) sig = sig.slice(0, brace).trim(); // drop the body\n sig = sig.replace(/\\s*=>?\\s*$/, '').replace(/=\\s*$/, '').slice(0, 120);\n if (sig && !out.includes(sig)) out.push(sig);\n if (out.length >= cap) break;\n }\n return out;\n}\n\n/**\n * Compact map of a VFS — code signatures and/or doc outlines. Edge-safe (pure IFilesystem walk).\n * `mode`: \"code\" (default) = top-level signatures; \"docs\" = heading outlines; \"all\" = both.\n */\nexport async function repoIndex(fs: IFilesystem, glob?: string, mode: 'code' | 'docs' | 'all' = 'code', signal?: AbortSignal): Promise<string> {\n const scope = glob ? anchoredGlob(fs, String(glob)) : null;\n const filter = mode === 'code' ? isCode : mode === 'docs' ? isDoc : (p: string) => isCode(p) || isDoc(p);\n const files = (await walkFiles(fs, fsCwd(fs), signal)).filter((p) => (scope ? scope.test(p) : filter(p)));\n const blocks: string[] = [];\n let shown = 0;\n for (const path of files) {\n ckAbort(signal);\n let content: string;\n try { content = await fs.readFile(path); } catch { continue; }\n const entries = isDoc(path) ? docOutlineOf(content) : signaturesOf(content);\n if (entries.length) { blocks.push(`${path}\\n${entries.map((s) => ' ' + s).join('\\n')}`); shown += entries.length; }\n if (shown >= 400) { blocks.push('… (map truncated; narrow with `glob`)'); break; }\n }\n const label = mode === 'code' ? 'code signatures' : mode === 'docs' ? 'document outlines' : 'entries';\n return blocks.length ? blocks.join('\\n') : `(no ${label} found)`;\n}\n\n/** Compact map of the codebase or document workspace — orient in ONE call, not many. */\nexport const repoMapTool: AgentTool = {\n name: 'RepoMap',\n description:\n 'Get a compact map of the workspace: code signatures and/or document outlines in ONE call. `scope`: \"code\" (default) = functions/classes/types; \"docs\" = markdown headings + summaries; \"all\" = both. Call once to orient before diving into specific files — avoids many exploratory Glob/Read calls.',\n parameters: {\n type: 'object',\n properties: {\n glob: { type: 'string', description: 'optional file glob to scope (default: all matching files)' },\n scope: { type: 'string', enum: ['code', 'docs', 'all'], description: 'what to map: \"code\" (default), \"docs\", or \"all\"' },\n },\n },\n run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || 'code', ctx.signal),\n};\n\n/**\n * Whitespace-tolerant fallback for Edit: locate `oldStr` in `content` ignoring each line's\n * leading/trailing whitespace, and replace the UNIQUE matching region with `newStr`. Returns\n * null if there isn't exactly one match (caller then errors, forcing a re-read) — so it never\n * guesses. This kills the common re-Read+retry cascade when an exact Edit fails on indentation drift.\n */\nexport function fuzzyLineReplace(content: string, oldStr: string, newStr: string): string | null {\n const norm = (s: string) => s.trim();\n const cl = content.split('\\n');\n const ol = oldStr.split('\\n').map(norm);\n while (ol.length && ol[ol.length - 1] === '') ol.pop();\n while (ol.length && ol[0] === '') ol.shift();\n if (!ol.length) return null;\n const matches: number[] = [];\n for (let i = 0; i + ol.length <= cl.length; i++) {\n let ok = true;\n for (let j = 0; j < ol.length; j++) if (norm(cl[i + j]) !== ol[j]) { ok = false; break; }\n if (ok) matches.push(i);\n }\n if (matches.length !== 1) return null; // not found, or ambiguous → don't guess\n const i = matches[0];\n return [...cl.slice(0, i), ...newStr.split('\\n'), ...cl.slice(i + ol.length)].join('\\n');\n}\n\n/** Read-before-overwrite guard, shared by Write and ApplyEdits' whole-file branch: blindly replacing\n * an existing file the agent never Read destroys its unseen content (the taskify-board incident).\n * `key` is the resolved path (the readState key); a fresh Write arms readState, so re-writing your own\n * output passes. Returns true when the write would clobber un-Read content and should be refused. */\nasync function wouldClobberUnread(ctx: ToolContext, path: string, key: string): Promise<boolean> {\n return !ctx.readState.has(key) && (await ctx.fs.exists(path));\n}\n\n/** Create or overwrite a file, creating parent directories as needed (mkdir -p). */\nexport const writeTool: AgentTool = {\n name: 'Write',\n description:\n 'Create or overwrite a file with the given contents, creating parent directories as needed. Use for new files instead of `bash echo >`. To replace an EXISTING file you must Read it first — overwriting an un-Read file is refused (it would destroy unseen content).',\n parameters: {\n type: 'object',\n required: ['path', 'content'],\n properties: { path: { type: 'string' }, content: { type: 'string' } },\n },\n async run({ path, content }, ctx) {\n const body = String(content ?? '');\n const key = ctx.fs.resolvePath(path);\n if (await wouldClobberUnread(ctx, path, key))\n throw new Error(`Refusing to overwrite ${path}: it exists and hasn't been Read. Read it first, then Write to replace it (or Edit to change part of it).`);\n if (ctx.lint) { const err = ctx.lint(path, body); if (err) throw new Error(err); }\n await mkdirp(ctx.fs, parentDir(key));\n await ctx.fs.writeFile(path, body);\n ctx.readState.set(key, body); // arm Edit on a freshly written file\n return `Wrote ${path}`;\n },\n};\n\n/** Apply an ordered list of exact-substring edits to one file in a single call. */\nexport const multiEditTool: AgentTool = {\n name: 'MultiEdit',\n description:\n 'Apply several exact-substring replacements to one file in order, in a single call. Requires a prior Read. Each `old_string` must be unique at the time it is applied. All-or-nothing: if any edit fails, none are written.',\n parameters: {\n type: 'object',\n required: ['path', 'edits'],\n properties: {\n path: { type: 'string' },\n edits: {\n type: 'array',\n items: {\n type: 'object',\n required: ['old_string', 'new_string'],\n properties: { old_string: { type: 'string' }, new_string: { type: 'string' } },\n },\n },\n },\n },\n async run({ path, edits }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n let current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const list = Array.isArray(edits) ? edits : [];\n if (!list.length) throw new Error('edits must be a non-empty array');\n for (const [i, e] of list.entries()) {\n const count = e.old_string === '' ? 0 : current.split(e.old_string).length - 1;\n if (count === 0) throw new Error(`edit ${i}: old_string not found in ${path}.`);\n if (count > 1) throw new Error(`edit ${i}: old_string is not unique in ${path} (${count} matches).`);\n current = current.replace(e.old_string, () => e.new_string);\n }\n if (ctx.lint) { const err = ctx.lint(path, current); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, current);\n ctx.readState.set(key, current);\n return `Applied ${list.length} edit(s) to ${path}`;\n },\n};\n\n/**\n * Cross-file batch edit — the multi-file refactor primitive. One call edits MANY files:\n * each entry with an `old_string` replaces that exact, UNIQUE substring (read fresh + verified,\n * so NO prior Read is needed — locate the sites with Grep, whose output is the text to match);\n * each entry WITHOUT `old_string` writes `new_string` as the whole file (creates it + parent dirs).\n * Validate-all-before-write → atomic across files. Collapses \"Grep + N×(Read+Edit)\" into \"Grep + ApplyEdits\".\n */\nexport const applyEditsTool: AgentTool = {\n name: 'ApplyEdits',\n description:\n 'Apply edits across one or MORE files in a single call — for cross-file refactors (rename/extract/move). edits=[{path, old_string?, new_string}]. WITH old_string: replace that exact substring (must be UNIQUE in the file — add surrounding context; read fresh + verified, no prior Read needed). WITHOUT old_string: write new_string as the whole file (creates it + parent dirs; but to OVERWRITE a file that already exists you must Read it first, else use old_string to edit part of it). Locate sites first with Grep (its output shows the exact text). Atomic: validated across all files before any write.',\n parameters: {\n type: 'object',\n required: ['edits'],\n properties: {\n edits: {\n type: 'array',\n items: {\n type: 'object',\n required: ['path', 'new_string'],\n properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } },\n },\n },\n },\n },\n async run({ edits }, ctx) {\n const list = Array.isArray(edits) ? edits : [];\n if (!list.length) throw new Error('edits must be a non-empty array of {path, old_string?, new_string}');\n const planned = new Map<string, string>(); // resolved path -> final content (validate ALL before writing → atomic)\n for (const [i, e] of list.entries()) {\n const p = ctx.fs.resolvePath(String(e.path));\n const old = e.old_string == null ? '' : String(e.old_string);\n const neu = String(e.new_string ?? '');\n if (old === '') { // whole-file write / create\n // Same read-before-overwrite guard as Write (skip if an earlier edit in THIS batch already staged p).\n if (!planned.has(p) && (await wouldClobberUnread(ctx, String(e.path), p)))\n throw new Error(`edit ${i}: refusing to overwrite ${e.path} with a whole-file write — it exists and hasn't been Read. Read it first, or pass old_string to edit part of it.`);\n planned.set(p, neu); continue;\n }\n let cur = planned.has(p) ? planned.get(p)! : await ctx.fs.readFile(p).catch(() => { throw new Error(`edit ${i}: file not found: ${e.path}`); });\n const count = cur.split(old).length - 1;\n if (count > 1) throw new Error(`edit ${i}: old_string is not unique in ${e.path} (${count} matches) — add more context`);\n if (count === 1) cur = cur.replace(old, () => neu);\n else {\n const fz = fuzzyLineReplace(cur, old, neu);\n if (fz == null) throw new Error(`edit ${i}: old_string not found in ${e.path}`);\n cur = fz;\n }\n planned.set(p, cur);\n }\n if (ctx.lint) for (const [p, content] of planned) { const err = ctx.lint(p, content); if (err) throw new Error(err); } // validate ALL before any write\n for (const [p, content] of planned) { await mkdirp(ctx.fs, parentDir(p)); await ctx.fs.writeFile(p, content); ctx.readState.set(p, content); }\n return `Applied ${list.length} edit(s) across ${planned.size} file(s): ${[...planned.keys()].join(', ')}`;\n },\n};\n\nfunction parentDir(abs: string): string {\n const i = abs.lastIndexOf('/');\n return i <= 0 ? '/' : abs.slice(0, i);\n}\n\n/** mkdir -p over the VFS (idempotent, top-down). */\nexport async function mkdirp(fs: IFilesystem, dir: string): Promise<void> {\n if (dir === '/' || (await fs.exists(dir))) return;\n await mkdirp(fs, parentDir(dir));\n if (!(await fs.exists(dir))) await fs.createDir(dir);\n}\n\n/**\n * Review — verification WITHOUT execution: a fresh-context, adversarial critic pass over the\n * changes the agent just made. Verification-as-review (stolen from the review-fix-commit skill):\n * for an agent with no test runner, \"re-read to be sure\" is weak, but a COLD reviewer that reads\n * only {task + the produced files} (not the author's chain-of-thought) catches missed edge cases,\n * unstated implications, and subtle logic bugs a happy-path one-shot misses. Costs one model call.\n *\n * The `notes` arg is the with/without-CONTEXT knob: omit it for a pure cold review (the default,\n * least biased); pass it to feed the reviewer extra context. We don't decide which is better —\n * the self-evolution loop discovers it via its prompt rules.\n *\n * Degrades to a no-op notice if no model handle is wired (ctx.ai/ctx.model).\n */\nexport function reviewTool(): AgentTool {\n return {\n name: 'Review',\n description:\n 'Critically review your changes before finishing (verification without running code). Pass the task and the paths you changed; a fresh-context senior reviewer reads them COLD and returns concrete issues or \"LGTM\". Fix what it raises, then finish. Optional `notes` feeds the reviewer extra context.',\n parameters: {\n type: 'object',\n required: ['task', 'paths'],\n properties: {\n task: { type: 'string', description: 'what was asked — the spec/requirements to check the changes against' },\n paths: { type: 'array', items: { type: 'string' }, description: 'the files you changed/created, to be reviewed' },\n notes: { type: 'string', description: 'OPTIONAL extra context for the reviewer (rationale, constraints). Omit for a pure cold review.' },\n },\n },\n async run({ task, paths, notes }, ctx: ToolContext) {\n if (!ctx.ai || !ctx.model) return '[Review] no model handle wired — skipped.';\n const list: string[] = Array.isArray(paths) ? paths.map(String) : [];\n if (!list.length) return 'Error: pass the paths you changed in `paths`.';\n const files: string[] = [];\n for (const p of list.slice(0, 12)) {\n try {\n const body = await ctx.fs.readFile(p);\n files.push(`--- ${p} ---\\n${body.length > 4000 ? body.slice(0, 4000) + '\\n…(truncated)' : body}`);\n } catch {\n files.push(`--- ${p} ---\\n[could not read]`);\n }\n }\n const prompt =\n 'You are a senior engineer doing a critical code review. Review the changes below AGAINST THE TASK with deep, skeptical thinking — default to finding problems.\\n' +\n 'Focus on: correctness, edge cases (empty/boundary/negative inputs), unstated-but-implied requirements, and subtle logic bugs. Do NOT comment on style.\\n\\n' +\n `TASK:\\n${String(task ?? '').trim()}\\n\\n` +\n (notes ? `CONTEXT FROM THE AUTHOR:\\n${String(notes).trim()}\\n\\n` : '') +\n `CHANGED FILES:\\n${files.join('\\n\\n')}\\n\\n` +\n 'Reply with a short numbered list of concrete, actionable issues. If the changes correctly and completely satisfy the task with no edge cases missed, reply with exactly: LGTM';\n try {\n const r = (await ctx.ai.chat({ model: ctx.model, messages: [{ role: 'user', content: prompt }], stream: false })) as ChatResponse;\n const text = (r?.content ?? '').trim();\n return text || 'LGTM';\n } catch (e: any) {\n return `[Review] model error: ${e?.message ?? e} — skipped.`;\n }\n },\n };\n}\n","import type { AgentTool } from './tools';\nimport { forComponent } from './logging';\n\n/**\n * Web tools — `WebFetch` (retrieve a URL as readable text) and `WebSearch` (ranked\n * results via a configured provider). Opt-in (NOT in the default tool set): network\n * access is a deliberate capability. Factory-built with an injectable `fetch` so they\n * stay edge-portable and unit-testable without real network. `fetch` is read at call\n * time, so a no-network runtime simply has the tool return an error.\n */\nconst log = forComponent('web');\n\n/** Strip HTML to readable text — dependency-free: drop script/style/comments, block tags → newlines, decode common entities. */\nexport function htmlToText(html: string): string {\n let s = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<title[\\s\\S]*?<\\/title>/gi, ' ') // drop title text (don't leak it into context)\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ') // …same for noscript / textarea content\n .replace(/<textarea[\\s\\S]*?<\\/textarea>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<\\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, '\\n')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ');\n s = s\n .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#0?39;/g, \"'\").replace(/&#x27;/gi, \"'\");\n return s\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .split('\\n').map((l) => l.trim()).join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nexport interface WebFetchOptions {\n /** Override the global fetch (tests inject a mock; edge runtimes can supply their own). */\n fetch?: typeof globalThis.fetch;\n maxBytes?: number; // cap the downloaded body (default 2 MB)\n maxChars?: number; // cap the returned text (default 100k)\n timeoutMs?: number; // request timeout (default 15s)\n /** Allow fetching private/loopback/link-local hosts (default false — blocks basic SSRF). */\n allowPrivateHosts?: boolean;\n}\n\n/**\n * Block obvious SSRF targets by hostname/IP literal (loopback, private ranges, link-local incl.\n * cloud metadata 169.254.169.254, `.internal`). Pure/edge-safe — no DNS, so DNS-rebinding and\n * redirect-to-internal are NOT covered (an embedder needing that should supply a vetting `fetch`).\n */\nexport function isPrivateHost(host: string): boolean {\n const h = host.toLowerCase().replace(/^\\[|\\]$/g, ''); // strip IPv6 brackets\n if (h === '' || h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.internal')) return true;\n if (h === '::1' || h === '::' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; // IPv6 loopback/link-local/ULA\n const m = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (m) {\n const a = +m[1], b = +m[2];\n return a === 0 || a === 127 || a === 10 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n return false;\n}\n\n/** Lazily-loaded node DNS resolver (absent on edge/browser) — closes DNS-rebinding (a public\n * hostname resolving to an internal IP) on the real-network path. Resolves null where unavailable. */\nlet _dnsLookup: ((h: string, opts?: any) => Promise<{ address: string }[]>) | null | undefined;\nasync function resolveIps(host: string): Promise<string[] | null> {\n if (_dnsLookup === undefined) {\n try { _dnsLookup = (await import('node:dns/promises')).lookup as any; }\n catch { _dnsLookup = null; } // edge/browser: no DNS — rely on the literal isPrivateHost check\n }\n if (!_dnsLookup) return null;\n try { return (await _dnsLookup(host, { all: true } as any)).map((a) => a.address); } catch { return null; }\n}\n\n/** Read a response body but stop at `maxBytes` of ACTUAL bytes (cancel the stream) — no unbounded download. */\nasync function readCapped(res: Response, maxBytes: number): Promise<string> {\n const reader = (res.body as any)?.getReader?.();\n if (!reader) { const t = await res.text(); return t.length > maxBytes ? t.slice(0, maxBytes) : t; }\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) { chunks.push(value); total += value.length; }\n if (total >= maxBytes) { try { await reader.cancel(); } catch { /* already closed */ } break; }\n }\n const out = new Uint8Array(Math.min(total, maxBytes));\n let off = 0;\n for (const c of chunks) { if (off >= out.length) break; const take = Math.min(c.length, out.length - off); out.set(c.subarray(0, take), off); off += take; }\n return new TextDecoder().decode(out);\n}\n\n/** Build a WebFetch tool. */\nexport function makeWebFetchTool(options: WebFetchOptions = {}): AgentTool {\n const maxBytes = options.maxBytes ?? 2_000_000;\n const maxChars = options.maxChars ?? 100_000;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebFetch',\n description:\n 'Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.',\n // Declared so a transport above the loop sizes its deadline around this one (see AgentTool.maxDurationMs).\n maxDurationMs: timeoutMs,\n parameters: { type: 'object', required: ['url'], properties: { url: { type: 'string', description: 'absolute http(s) URL' } } },\n async run({ url }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n const customFetch = !!options.fetch; // injected fetch (tests/edge) owns its own vetting → skip DNS\n const u = String(url ?? '');\n try { new URL(u); } catch { return `Error: invalid URL: ${u}`; }\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n // Reject a host that's a private/internal IP literal, or (on the real-network path) a name that\n // RESOLVES to one — re-checked on EVERY redirect hop so an external page can't bounce us internal.\n const hostBlock = async (hostname: string): Promise<string | null> => {\n if (options.allowPrivateHosts) return null;\n if (isPrivateHost(hostname)) return hostname;\n if (!customFetch) { const ips = await resolveIps(hostname); if (ips) for (const ip of ips) if (isPrivateHost(ip)) return `${hostname} → ${ip}`; }\n return null;\n };\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n let current = u;\n let res: Response;\n for (let hop = 0; ; hop++) {\n const pu = new URL(current);\n if (pu.protocol !== 'http:' && pu.protocol !== 'https:') return `Error: only http/https URLs are allowed (got \"${pu.protocol}\")`;\n const blocked = await hostBlock(pu.hostname);\n if (blocked) return `Error: refusing to fetch a private/internal address (${blocked}) — set allowPrivateHosts to override`;\n res = await doFetch(current, { signal: ctl.signal, redirect: 'manual', headers: { 'user-agent': 'agentx (+https://github.com/Livshitz/agentx)' } });\n if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n if (hop >= 5) return `Error fetching ${u}: too many redirects`;\n current = new URL(res.headers.get('location')!, current).toString(); // re-validated at loop top\n continue;\n }\n break;\n }\n const type = res.headers.get('content-type') ?? '';\n const body = await readCapped(res, maxBytes);\n const text = /html/i.test(type) || /^\\s*<(?:!doctype|html)/i.test(body) ? htmlToText(body) : body.trim();\n const capped = text.length > maxChars ? text.slice(0, maxChars) + `\\n… [truncated at ${maxChars} chars]` : text;\n return `${res.status} ${res.statusText} · ${new URL(current).host}\\n\\n${capped}`;\n } catch (e: any) {\n log.debug(`WebFetch ${u} failed`, e);\n return `Error fetching ${u}: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\nexport interface WebSearchOptions {\n fetch?: typeof globalThis.fetch;\n /** Provider: 'auto' (default) prefers Firecrawl, then Tavily (by key presence), else keyless DuckDuckGo.\n * 'anthropic' delegates to a cheap model with Anthropic's native server-side web_search. */\n provider?: 'auto' | 'tavily' | 'duckduckgo' | 'anthropic' | 'firecrawl';\n /** API key for Tavily (default: process.env.TAVILY_API_KEY). */\n apiKey?: string;\n /** Tavily endpoint override. */\n endpoint?: string;\n /** API key for Firecrawl (default: process.env.FIRECRAWL_API_KEY). */\n firecrawlApiKey?: string;\n /** Firecrawl search endpoint override. */\n firecrawlEndpoint?: string;\n maxResults?: number; // default 5\n timeoutMs?: number; // default 15s\n /** Tool name (default 'WebSearch'). Set to register a provider-pinned variant, e.g. 'WebSearchAnthropic'. */\n name?: string;\n /** Tool description override (default: the generic ranked-results blurb). */\n description?: string;\n /** Anthropic key for provider 'anthropic' (default: process.env.ANTHROPIC_API_KEY). */\n anthropicApiKey?: string;\n /** Model for provider 'anthropic' (default 'claude-haiku-4-5'). */\n model?: string;\n}\n\ninterface SearchHit { title: string; url: string; snippet: string }\n\n/** Decode a DuckDuckGo HTML result href: results are `//duckduckgo.com/l/?uddg=<encoded-target>` redirects. */\nexport function decodeDdgUrl(href: string): string {\n const m = href.match(/[?&]uddg=([^&]+)/);\n if (m) { try { return decodeURIComponent(m[1]); } catch { /* fall through */ } }\n return href.startsWith('//') ? 'https:' + href : href;\n}\n\n/** Parse DuckDuckGo's HTML results page into hits (title/url/snippet) — dependency-free, zips anchors to snippets in order. */\nexport function parseDdgHtml(html: string, max: number): SearchHit[] {\n const anchors = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__a[^\"]*\"[^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/g)];\n const snippets = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__snippet[^\"]*\"[^>]*>([\\s\\S]*?)<\\/a>/g)].map((m) => htmlToText(m[1]));\n const hits: SearchHit[] = [];\n for (let i = 0; i < anchors.length && hits.length < max; i++) {\n const url = decodeDdgUrl(anchors[i][1]);\n try { if (isPrivateHost(new URL(url).hostname)) continue; } catch { continue; } // skip junk/internal redirects\n hits.push({ title: htmlToText(anchors[i][2]) || '(untitled)', url, snippet: snippets[i] ?? '' });\n }\n return hits;\n}\n\nfunction formatHits(hits: SearchHit[]): string {\n if (!hits.length) return '(no results)';\n return hits.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet.replace(/\\s+/g, ' ').slice(0, 240)}`).join('\\n\\n');\n}\n\n/** Provider 'firecrawl': agent-oriented search API (POST /v1/search, Bearer auth). Maps Firecrawl's\n * result objects into the same hit shape as Tavily/DDG. HTTP-only, no SDK. */\nasync function firecrawlSearch(q: string, opts: { key: string; endpoint: string; maxResults: number; fetch: typeof globalThis.fetch; signal: AbortSignal }): Promise<string> {\n const res = await opts.fetch(opts.endpoint, {\n method: 'POST',\n signal: opts.signal,\n headers: { authorization: `Bearer ${opts.key}`, 'content-type': 'application/json' },\n body: JSON.stringify({ query: q, limit: opts.maxResults }),\n });\n if (!res.ok) return `Error: Firecrawl search returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.data) ? data.data.slice(0, opts.maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.description ?? r.markdown ?? '') })));\n}\n\n/**\n * Provider 'anthropic': delegate the search to a cheap model running Anthropic's native server-side\n * web_search tool. Anthropic runs the search and reads the results; we prompt for un-editorialized\n * findings (one claim per line, each with its source URL) and relay them verbatim. Raw HTTP — no SDK,\n * stays edge-portable. Slower and costlier than DDG/Tavily, but better-sourced (native search index).\n */\nasync function anthropicSearch(q: string, opts: { key: string; model: string; fetch: typeof globalThis.fetch; signal: AbortSignal }): Promise<string> {\n const res = await opts.fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n signal: opts.signal,\n headers: { 'x-api-key': opts.key, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({\n model: opts.model,\n max_tokens: 1024,\n // Basic variant: Haiku-tier doesn't support the _20260209 dynamic-filtering variant (Opus 4.6+/Sonnet 4.6 only).\n tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 5 }],\n messages: [{ role: 'user', content:\n `Search the web for: ${q}\\n\\nReturn only the relevant findings as concise bullet points, each with its source URL in parentheses. ` +\n `Do not add a preamble, conclusion, opinion, or commentary. If sources conflict, list each claim with its source rather than resolving it.` }],\n }),\n });\n if (!res.ok) return `Error: Anthropic search returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n if (data?.stop_reason === 'refusal') return 'Error: Anthropic search refused the query';\n let text = '';\n for (const block of data?.content ?? []) if (block?.type === 'text') text += block.text;\n return text.trim() || '(no results)';\n}\n\n/**\n * Build a WebSearch tool. Keyless by default (DuckDuckGo HTML) so it works in any deployment with no\n * setup; on provider 'auto' it upgrades to an agent-oriented API by key presence — Firecrawl\n * (FIRECRAWL_API_KEY) preferred, then Tavily (TAVILY_API_KEY). Provider 'anthropic' is a separate path\n * (native search via a cheap model) exposed as the WebSearchAnthropic tool.\n */\nexport function makeWebSearchTool(options: WebSearchOptions = {}): AgentTool {\n const tavilyEndpoint = options.endpoint ?? 'https://api.tavily.com/search';\n const firecrawlEndpoint = options.firecrawlEndpoint ?? 'https://api.firecrawl.dev/v1/search';\n const maxResults = options.maxResults ?? 5;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: options.name ?? 'WebSearch',\n description: options.description ??\n 'Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic — then WebFetch a result URL to read it in full.',\n parameters: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } },\n async run({ query }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n const q = String(query ?? '').trim();\n if (!q) return 'Error: empty query';\n const key = options.apiKey ?? process.env.TAVILY_API_KEY;\n const fcKey = options.firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY;\n const provider = options.provider ?? 'auto';\n // auto priority ladder: Firecrawl > Tavily > keyless DuckDuckGo (by key presence).\n const useFirecrawl = provider === 'firecrawl' || (provider === 'auto' && !!fcKey);\n const useTavily = provider === 'tavily' || (provider === 'auto' && !useFirecrawl && !!key);\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n if (provider === 'anthropic') {\n const akey = options.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!akey) return 'Error: WebSearchAnthropic requires ANTHROPIC_API_KEY (set in env)';\n return await anthropicSearch(q, { key: akey, model: options.model ?? 'claude-haiku-4-5', fetch: doFetch, signal: ctl.signal });\n }\n if (useFirecrawl) {\n if (!fcKey) return 'Error: Firecrawl provider selected but FIRECRAWL_API_KEY is not set';\n return await firecrawlSearch(q, { key: fcKey, endpoint: firecrawlEndpoint, maxResults, fetch: doFetch, signal: ctl.signal });\n }\n if (useTavily) {\n if (!key) return 'Error: Tavily provider selected but TAVILY_API_KEY is not set';\n const res = await doFetch(tavilyEndpoint, {\n method: 'POST',\n signal: ctl.signal,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ api_key: key, query: q, max_results: maxResults }),\n });\n if (!res.ok) return `Error: search provider returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.results) ? data.results.slice(0, maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.content ?? '') })));\n }\n // Keyless: DuckDuckGo HTML endpoint (no key, edge-portable).\n const res = await doFetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(q), {\n signal: ctl.signal,\n headers: { 'user-agent': 'Mozilla/5.0 (compatible; agentx/1.0; +https://github.com/Livshitz/agentx)' },\n });\n if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;\n return formatHits(parseDdgHtml(await res.text(), maxResults));\n } catch (e: any) {\n log.debug('WebSearch failed', e);\n return `Error searching: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/** Default instances (registered in the tool registry; opt-in by name). */\nexport const webFetchTool = makeWebFetchTool();\nexport const webSearchTool = makeWebSearchTool();\nconst ANTHROPIC_SEARCH_DESC =\n \"High-quality web search via Anthropic's native search index. Returns concise, sourced findings — one claim per line, each with its source URL. Prefer this over WebSearch when accuracy and citations matter; it is slower (~3–8s) and bills the Anthropic API account per call (one cheap-model turn + search fee).\";\n\n/** Anthropic-native search as a provider-pinned WebSearch variant. The credential is API-key billing\n * (x-api-key); pass `anthropicApiKey` to inject a resolved key, else it falls back to ANTHROPIC_API_KEY. */\nexport function makeWebSearchAnthropicTool(opts: { anthropicApiKey?: string; model?: string } = {}): AgentTool {\n return makeWebSearchTool({ provider: 'anthropic', name: 'WebSearchAnthropic', description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });\n}\n/** Env-keyed singleton: registered in the tool registry so 'WebSearchAnthropic' resolves by name. The\n * CLI default-on path builds its own instance with a config-resolved key (see cli/core.ts). */\nexport const webSearchAnthropicTool = makeWebSearchAnthropicTool();\n","/**\n * Tier-1 OS sandbox for the real `Shell` tool (mind/03-roadmap.md \"capability tiers\").\n *\n * Wraps the spawned `/bin/sh` in the platform's process sandbox so a hostile/buggy command\n * can read the machine but can only WRITE inside an allowlist (cwd + tmp + extra `writePaths`),\n * and gets no network unless granted:\n * - macOS: `sandbox-exec` (seatbelt) with a generated profile\n * - Linux: `bwrap` (bubblewrap) with `--ro-bind / /` + writable binds\n *\n * Pure argv builders (unit-testable, no node imports) + an async wrapper-binary locator.\n * This complements — does not replace — env secret-scrubbing and the permission prompt:\n * the FS jail can't contain a real process; this makes the *process* itself contained.\n */\n\nexport class OsSandboxOptions {\n /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */\n network = false;\n /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */\n writePaths: string[] = [];\n}\n\nexport interface SandboxWrap {\n bin: string;\n args: string[]; // full argv: wrapper flags + /bin/sh -c <command>\n}\n\n/** Writable allowlist shared by both platforms: cwd, the tmp roots, /dev. */\nfunction writable(cwd: string, o: OsSandboxOptions, tmpDir?: string): string[] {\n const set = new Set<string>([cwd, '/tmp', '/private/tmp', '/private/var/folders', '/dev', ...(tmpDir ? [tmpDir] : []), ...o.writePaths]);\n return [...set];\n}\n\nconst sbQuote = (p: string) => `\"${p.replace(/([\"\\\\])/g, '\\\\$1')}\"`;\n\n/** macOS seatbelt profile: allow everything, then deny writes/network, then re-allow the allowlist\n * (seatbelt resolves conflicts by specificity, so subpath allows override the broad deny). */\nexport function seatbeltProfile(cwd: string, o: OsSandboxOptions, tmpDir?: string): string {\n const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(' ');\n return [\n '(version 1)',\n '(allow default)',\n ...(o.network ? [] : ['(deny network*)']),\n '(deny file-write*)',\n `(allow file-write* ${allows})`,\n ].join('\\n');\n}\n\n/** Build the wrapped argv for `sh -c <command>`, or null if `platform` has no supported wrapper. */\nexport function sandboxArgv(command: string, cwd: string, opts: Partial<OsSandboxOptions> = {}, platform: string = process.platform, tmpDir?: string): SandboxWrap | null {\n const o = { ...new OsSandboxOptions(), ...opts };\n if (platform === 'darwin') {\n return { bin: '/usr/bin/sandbox-exec', args: ['-p', seatbeltProfile(cwd, o, tmpDir), '/bin/sh', '-c', command] };\n }\n if (platform === 'linux') {\n const binds = writable(cwd, o, tmpDir).filter((p) => p !== '/dev' && !p.startsWith('/private')).flatMap((p) => ['--bind-try', p, p]);\n return {\n bin: 'bwrap',\n args: ['--ro-bind', '/', '/', ...binds, '--dev', '/dev', '--proc', '/proc', '--die-with-parent', ...(o.network ? [] : ['--unshare-net']), '/bin/sh', '-c', command],\n };\n }\n return null;\n}\n\n/** Locate the wrapper binary for this platform; null = sandboxing unavailable here. */\nexport async function findSandboxWrapper(platform: string = process.platform): Promise<string | null> {\n const { existsSync } = await import('node:fs');\n if (platform === 'darwin') return existsSync('/usr/bin/sandbox-exec') ? '/usr/bin/sandbox-exec' : null;\n if (platform === 'linux') {\n for (const dir of (process.env.PATH ?? '/usr/bin:/bin').split(':')) if (dir && existsSync(`${dir}/bwrap`)) return `${dir}/bwrap`;\n return null;\n }\n return null;\n}\n","import type { AgentTool } from './tools';\nimport { truncateOutput } from './tools';\nimport { redactSecrets } from './redact';\nimport { forComponent } from './logging';\nimport { sandboxArgv, findSandboxWrapper, type OsSandboxOptions } from './shell.sandbox';\n\n/**\n * Real shell tool — node-only, OPT-IN, and deliberately NOT edge-portable.\n *\n * ⚠️ Unlike the default VFS `bash` (a sandboxed JS interpreter over the virtual filesystem),\n * this spawns a REAL `/bin/sh` process. It can run `bun`, `git`, `ssh`, scripts, deploys —\n * and, by the same token, it is NOT sandboxed: only cwd-binding constrains it. It is a\n * deliberate host escalation, kept out of `defaultTools()`/`toolRegistry()` and out of the\n * edge-safe `src/index.ts` (same policy as `mcp.client.ts`). A host opts in explicitly:\n *\n * tools: [...defaultTools(), makeRealShellTool({ cwd: nodeDiskRoot })]\n *\n * Mirrors `tools.web.ts`: a factory with an injectable `spawn` (tests + edge never import\n * node:child_process), an options bag, abort + timeout honored, output capped. Safety beyond\n * cwd-binding is the host's to add (e.g. a PermissionPolicy `decision:'ask'` per command, or\n * an OS sandbox wrapper) — see mind/03-roadmap.md \"OS-level access — capability tiers\".\n */\n\nconst log = forComponent('shell');\n\n/** Normalize shell output for return: trim trailing newlines, mask secret values, then size-truncate.\n * Redaction runs BEFORE truncation so a masked tail can't smuggle a secret past the line cap. */\nconst clean = (s: string): string => truncateOutput(redactSecrets(s.replace(/\\n+$/, '')));\n\n/** The slice of node's `child_process.spawn` we depend on — injectable so tests supply a fake. */\nexport type SpawnFn = (\n command: string,\n args: string[],\n options: {\n cwd?: string;\n env?: Record<string, string | undefined>;\n signal?: AbortSignal;\n /** stdio layout. We force stdin to /dev/null so a child can't block on (or steal) the REPL's input. */\n stdio?: ['ignore', 'pipe', 'pipe'];\n /** Run in a new session/process group (setsid) — detaches from the controlling terminal. See DETACHED. */\n detached?: boolean;\n },\n) => SpawnedProcess;\n\n/** Minimal `ChildProcess` surface this tool uses. */\nexport interface SpawnedProcess {\n stdout?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n stderr?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n on(ev: 'close', cb: (code: number | null) => void): void;\n on(ev: 'error', cb: (err: Error) => void): void;\n kill(signal?: string): void;\n /** Child PID — present on the real node child; used to signal the whole process group on abort. */\n pid?: number;\n}\n\n/**\n * Detach every spawned child from the REPL's controlling terminal.\n *\n * `stdio: ['ignore', 'pipe', 'pipe']` — stdin is /dev/null (clean EOF; nothing to block on).\n * `detached: true` — setsid() puts the child in its OWN session with NO controlling tty.\n *\n * Without this a child inherits the agent's tty: an interactive prompt (`sudo`, `ssh`, a git\n * credential helper) opens `/dev/tty` directly and then RACES the REPL's raw-mode input reader for\n * the user's keystrokes — a deadlock that also captures whatever the user types (e.g. a password)\n * into the agent instead of the program. Detached, those programs find no tty and fail FAST with a\n * legible error (\"sudo: a terminal is required …\") which the model can act on (tell the user to run\n * it via `!`), instead of hanging until the 120s timeout. Side benefit: the child is a process-group\n * leader, so abort/timeout can reap the whole subtree, not just /bin/sh. */\nconst DETACHED = { stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'], detached: true };\n\n/** Signal a child's WHOLE process group (`-pid`). Children are group leaders (DETACHED), so `proc.kill`\n * hits only /bin/sh and orphans descendants; this reaps the subtree. Best-effort — the group may\n * already be gone, and the fake spawn in tests has no real pid. Returns false when nothing was signaled. */\nfunction killGroup(proc: SpawnedProcess | undefined, signal: 'SIGTERM' | 'SIGKILL'): boolean {\n if (!proc?.pid) return false;\n try { process.kill(-proc.pid, signal); return true; } catch { return false; /* already exited / no such group */ }\n}\n\nexport interface RealShellOptions {\n /** Working directory the shell is bound to (typically a NodeDiskFilesystem `baseDir`). Required. */\n cwd: string;\n /** Shell binary for `-c` (e.g. the user's `$SHELL`). Default `/bin/sh`. Only honored when NOT\n * OS-sandboxed (the sandbox wrapper pins its own shell). */\n shell?: string;\n /** Override the spawner (tests inject a fake; default lazily imports node:child_process). */\n spawn?: SpawnFn;\n /** Per-command wall-clock cap (kill on overrun). Default 120s. */\n timeoutMs?: number;\n /** Ceiling for the model-supplied `timeoutMs` argument (a long build can ask for more than the\n * default, but not for an unbounded turn). Default 10min. */\n maxTimeoutMs?: number;\n /** Extra env merged over the (optionally scrubbed) base env for the child. */\n env?: Record<string, string>;\n /** Strip likely-secret vars (API keys, tokens, cloud creds) from the child's env. Default ON.\n * The FS jail does NOT contain a real process, so this is the seam that keeps `echo $ANTHROPIC_API_KEY`\n * from leaking the host's secrets to a spawned command. `false` passes `process.env` through verbatim. */\n redactEnv?: boolean;\n /** Job registry enabling `Shell({background:true})` (long-running processes). Pair with `makeShellJobTools`. */\n registry?: ShellJobRegistry;\n /** Tier-1 OS sandbox: wrap /bin/sh in sandbox-exec (macOS) / bwrap (Linux) — writes confined to\n * cwd+tmp, network blocked unless granted. `true` = defaults; commands FAIL (don't silently run\n * unsandboxed) if no wrapper exists on this platform. See src/shell.sandbox.ts. */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n /** Does a background job outlive the current run? False on one-shot hosts (the process exits and\n * `killOnExit` reaps the job), which the Shell tool's description must say rather than promise\n * cross-turn survival it won't get. Default true. */\n jobsSurviveRun?: boolean;\n}\n\n/** Resolve the (bin,args) to spawn for `command`, honoring the optional OS sandbox.\n * Throws when sandboxing was requested but this platform has no wrapper — fail closed. */\nasync function spawnArgvFor(command: string, cwd: string, osSandbox?: boolean | Partial<OsSandboxOptions>): Promise<{ bin: string; args: string[] }> {\n if (!osSandbox) return { bin: '/bin/sh', args: ['-c', command] };\n const opts = osSandbox === true ? {} : osSandbox;\n const wrapper = await findSandboxWrapper();\n const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;\n if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);\n return wrapped;\n}\n\n/** Env var names that look like secrets and are dropped before spawning (unless redactEnv:false). */\nconst SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;\n\n/** Build the child's env: `process.env` minus likely-secrets (when redacting), plus explicit `env`. */\nfunction childEnv(opts: { env?: Record<string, string>; redactEnv?: boolean }): Record<string, string | undefined> {\n const base: Record<string, string | undefined> = {};\n const redact = opts.redactEnv !== false; // default ON\n for (const [k, v] of Object.entries(process.env)) if (!(redact && SECRET_ENV_RE.test(k))) base[k] = v;\n return { ...base, ...opts.env };\n}\n\n/** Lazily resolve node's spawn (kept out of any eager edge import path). */\nlet _spawn: SpawnFn | undefined;\nasync function nodeSpawn(): Promise<SpawnFn> {\n if (!_spawn) _spawn = (await import('node:child_process')).spawn as unknown as SpawnFn;\n return _spawn;\n}\n\n// ---------------------------------------------------------------------------\n// Background jobs — long-running processes the agent starts, polls, and kills.\n// ---------------------------------------------------------------------------\nexport type JobStatus = 'running' | 'exited' | 'killed' | 'error';\n\nexport interface ShellJobConfig {\n cwd: string;\n spawn?: SpawnFn;\n env?: Record<string, string>;\n redactEnv?: boolean;\n /** Tail buffer cap per job (bytes); older output is dropped. Default 256 KB. */\n maxBuffer?: number;\n /** Kill all jobs on process exit (the CLI sets this; tests leave it off to avoid global handlers). */\n killOnExit?: boolean;\n /** Tier-1 OS sandbox for background jobs too (same semantics as RealShellOptions.osSandbox). */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n /**\n * Called once when a background job reaches a terminal state. Claude-Code parity: a background task\n * that finishes has to reach the MODEL, or `background:true` is a write-only hole — the model starts\n * a job, forgets it, and never collects the result. The host wires this to `agent.inject(...)`, which\n * folds the notice in at the next step boundary (and re-opens the turn if the model was about to stop).\n */\n onExit?: JobExitHandler;\n}\n\nexport interface JobExitNotice { id: string; command: string; status: JobStatus; exitCode?: number; tail: string }\nexport type JobExitHandler = (n: JobExitNotice) => void;\n\n/** The exact text a completion notice becomes. Shared so every host (CLI, presets, embedders) pushes the\n * same thing back to the model instead of each inventing its own wording. */\nexport function formatJobExit(n: JobExitNotice): string {\n return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ''}] \\`${n.command}\\`\\n`\n + (n.tail ? `${n.tail}\\n` : '(no output)\\n')\n + `Read the full output with ShellOutput({id:\"${n.id}\"}).`;\n}\n\ninterface Job { command: string; buf: string; status: JobStatus; exitCode?: number; proc?: SpawnedProcess; }\n\n/**\n * Per-session registry of background `/bin/sh` jobs. Backs `Shell({background:true})` and the\n * `ShellOutput`/`ShellStatus`/`ShellKill` tools. Output accumulates into a tail-capped ring so a\n * chatty process can't OOM. Bounded + killable; the CLI wires `killOnExit` so children are reaped.\n */\nexport class ShellJobRegistry {\n private jobs = new Map<string, Job>();\n private seq = 0;\n constructor(private cfg: ShellJobConfig) {\n if (cfg.killOnExit && typeof process !== 'undefined') process.once('exit', () => this.killAll());\n }\n\n async start(command: string): Promise<string> {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: '', status: 'running' };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max); // ring: keep the tail\n };\n try {\n const spawn = this.cfg.spawn ?? (await nodeSpawn());\n const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: '/bin/sh', args: ['-c', command] };\n const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });\n job.proc = proc;\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); this.notifyExit(id, job); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; this.notifyExit(id, job); } });\n } catch (e: any) {\n job.status = 'error';\n job.buf = `failed to spawn: ${e?.message ?? e}`;\n }\n this.jobs.set(id, job);\n return id;\n }\n\n /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */\n private notified = new Set<string>();\n private notifyExit(id: string, job: Job): void {\n if (this.notified.has(id) || !this.cfg.onExit) return;\n this.notified.add(id);\n try {\n this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4000) });\n } catch { /* a host notification must never take down the job registry */ }\n }\n\n /**\n * Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle\n * once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not\n * pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.\n */\n setOnExit(fn: JobExitHandler | undefined): void { this.cfg.onExit = fn; }\n\n /** Whether a finished job will actually be reported to the model. The Shell tool's description reads\n * this so it can't promise a completion notice on a host that discards it. */\n get notifiesOnExit(): boolean { return !!this.cfg.onExit; }\n\n /** Current tail output for a job (null = no such job). */\n output(id: string): string | null { return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? '' : null); }\n\n status(id: string): { status: JobStatus; exitCode?: number; bytes: number } | null {\n const j = this.jobs.get(id);\n return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;\n }\n\n list(): Array<{ id: string; command: string; status: JobStatus }> {\n return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));\n }\n\n /**\n * Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its\n * timeout is not necessarily a hung command — killing it throws away work that was nearly done and,\n * worse, a launcher that spawned its own detached worker leaves that worker running with nothing\n * tracking it. Adopting hands the model a handle instead: the command keeps going, its completion\n * is reported like any other job, and `seed` carries the output produced before the handoff.\n */\n adopt(command: string, proc: SpawnedProcess, seed = ''): string {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: seed.slice(-max), status: 'running', proc };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max);\n };\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); this.notifyExit(id, job); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; this.notifyExit(id, job); } });\n this.jobs.set(id, job);\n return id;\n }\n\n kill(id: string): boolean {\n const j = this.jobs.get(id);\n if (!j) return false;\n // Group-kill: bg children are detached (own group), so SIGTERM the whole subtree — not just /bin/sh —\n // else a forked server survives the kill and the agent's exit teardown. Fall back to the pid for fakes.\n if (j.status === 'running') { if (!killGroup(j.proc, 'SIGTERM')) { try { j.proc?.kill('SIGTERM'); } catch { /* already gone */ } } j.status = 'killed'; }\n return true;\n }\n\n killAll(): void { for (const id of this.jobs.keys()) this.kill(id); }\n}\n\n/** Build an opt-in real-shell tool bound to `options.cwd`. */\nexport function makeRealShellTool(options: RealShellOptions): AgentTool {\n const defaultTimeoutMs = options.timeoutMs ?? 120_000;\n const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 600_000, defaultTimeoutMs);\n // The background promise must match what THIS host actually does — a lie here is what makes\n // `background:true` a black hole the model trusts. Evaluated lazily (the registry's onExit may be wired\n // after the tool is built, via setOnExit).\n // NOTIFICATION and SURVIVAL are independent facts governed by different things — `registry.onExit`\n // (is a completion pushed to the model?) and `jobsSurviveRun` (does the host outlive this run?). Reading\n // both off `notifiesOnExit` made the unwired preset tell the model a job \"does NOT survive the end of\n // this run\" while an identically-configured wired registry said the opposite. Each clause states what\n // actually governs it.\n const backgroundDoc = () => {\n const base = 'Set `background:true` for long-running processes (servers, watchers) and for anything that already timed out in the foreground — '\n + 'returns a job id immediately; poll with ShellOutput/ShellStatus, stop with ShellKill. ';\n const notify = options.registry?.notifiesOnExit\n ? 'Its completion is reported back to you when it finishes.'\n : 'NOTE: nothing will tell you when it finishes — poll ShellOutput/ShellStatus yourself, or you will never see its result.';\n const survival = options.jobsSurviveRun === false\n ? 'The job does NOT survive the end of this run, so do not stop while you still need its result.'\n : 'It keeps running across turns.';\n return `${base}${notify} ${survival}`;\n };\n return {\n name: 'Shell',\n // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The\n // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like\n // shraga-ee, to the parent's session), so a background job started from the child would run\n // outside the child's isolation and report into the parent. Background stays a parent capability\n // (`makeShellJobTools`' companions drop out of an isolated child entirely — see their `withCwd`).\n // Only `run` is rebuilt: everything else is carried over from THIS instance, because a host may\n // have mutated the tool AFTER construction (shraga-ee renames `Shell` -> `Bash` and its system\n // prompt says `Bash` everywhere). Rebuilding from `options` alone silently dropped that rename,\n // so a worktree child advertised `Shell` while being told to call `Bash` — an unknown-tool error\n // on its first call, and a hard failure for an agentType def whose allowlist names `Bash`.\n // `description` is the ONE thing taken from the rebound tool instead: it is derived from the\n // options and states whether `background:true` works — the rebind drops the registry, so carrying\n // the parent's text over would advertise a background capability the child does not have. (The\n // spread also collapses the getter below to a plain value, which is why this override is explicit.)\n withCwd(cwd: string): AgentTool {\n const rebound = makeRealShellTool({ ...options, cwd, registry: undefined });\n return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };\n },\n get description() {\n return 'Run a shell command via /bin/sh in the working directory. ' +\n 'Executes any installed binary — ls, cat, grep, git, bun, node, curl, scripts, etc. ' +\n 'Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. ' +\n 'Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input ' +\n 'fail fast rather than hang — for privileged actions use a non-interactive flag (e.g. `sudo -n`), ' +\n 'or ask the user to run the command themselves. ' +\n (options.registry?.adopt\n ? `A command still running after ${defaultTimeoutMs}ms is NOT killed — it is handed to a background job and you get its id, so you can keep checking on it (\\`[still running]\\`). Pass \\`timeoutMs\\` (max ${maxTimeoutMs}) to wait longer in the foreground, and always bound network commands yourself (e.g. \\`curl -m 10\\`). `\n : `Each command is killed after ${defaultTimeoutMs}ms (result \\`[exit 124]\\` with whatever output it produced) — pass \\`timeoutMs\\` (max ${maxTimeoutMs}) for a legitimately slower command, and always bound network commands yourself (e.g. \\`curl -m 10\\`). `) +\n backgroundDoc();\n },\n // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).\n maxDurationMs: maxTimeoutMs,\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the shell command line to execute' },\n background: { type: 'boolean', description: 'run detached and return a job id immediately (for servers/watchers/long builds)' },\n timeoutMs: { type: 'number', description: 'wall-clock cap for THIS command, overriding the default (clamped to the tool max)' },\n },\n },\n async run({ command, background, timeoutMs: askedTimeoutMs, timeout: aliasTimeout }, ctx) {\n const cmd = String(command ?? '');\n // `timeout` is an UNADVERTISED alias for `timeoutMs`, same unit (ms). Models reach for the\n // shorter name from muscle memory (observed: composer-2.5 sending `{\"timeout\":180000}`), and\n // an unknown key is silently dropped — the command then dies at the 120s default while the\n // model believes it asked for 180s and reads an unexplained kill. Accepted, never documented,\n // so the schema still teaches exactly one name.\n const asked = Number.isFinite(askedTimeoutMs) ? askedTimeoutMs : aliasTimeout;\n const timeoutMs = Number.isFinite(asked) && Number(asked) > 0\n ? Math.min(Number(asked), maxTimeoutMs)\n : defaultTimeoutMs;\n if (!cmd.trim()) return '[exit 1] empty command';\n if (background) {\n if (!options.registry) return 'Error: background execution is not enabled in this host (no job registry).';\n const id = await options.registry.start(cmd);\n return `Started background job ${id}. Poll output with ShellOutput({id:\"${id}\"}), check ShellStatus({id:\"${id}\"}), stop with ShellKill({id:\"${id}\"}).`;\n }\n const spawn = options.spawn ?? (await nodeSpawn());\n // Sandbox-off keeps this path await-free (after spawn resolution) so an abort racing the call\n // start still lands before listener registration, exactly as pre-sandbox semantics.\n let argv = { bin: options.shell || '/bin/sh', args: ['-c', cmd] };\n if (options.osSandbox) {\n try {\n argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);\n } catch (e: any) {\n return `[exit 1] ${e?.message ?? e}`; // fail closed — never run unsandboxed when sandboxing was asked for\n }\n }\n // Compose abort: the run's signal (ctx.signal) OR our per-command timeout both kill the child.\n const ctl = new AbortController();\n const onAbort = () => ctl.abort();\n if (ctx.signal) { if (ctx.signal.aborted) ctl.abort(); else ctx.signal.addEventListener('abort', onAbort, { once: true }); }\n let timedOut = false;\n // Set by the promise body once the child exists, so the timer can hand it over instead of killing it.\n let onDeadline: () => void = () => { timedOut = true; ctl.abort(); };\n const timer = setTimeout(() => { onDeadline(); }, timeoutMs);\n // The child is its own process-group leader (DETACHED): node's `signal` kills only /bin/sh, so on\n // abort also SIGKILL the whole group to reap any descendants (see killGroup).\n // Incremental output → ctx.emit (when the host listens), coalesced to ≥250ms / ≥1KB batches\n // so a chatty child doesn't spam hooks. Redacted per batch (the final result is re-redacted\n // whole — a secret split across batch boundaries can slip the per-batch pass, hence the cap\n // on what consumers may do with chunks: display/digest, never persistence).\n let pend = '';\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n const flushEmit = (ctx: { emit?: (s: string) => void }) => {\n if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }\n if (pend) { ctx.emit?.(redactSecrets(pend)); pend = ''; }\n };\n try {\n return await new Promise<string>((resolve) => {\n let out = '';\n let settled = false;\n const finish = (s: string) => { if (settled) return; settled = true; resolve(s); };\n let proc: SpawnedProcess;\n try {\n proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });\n } catch (e: any) {\n return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);\n }\n // Reap the detached group when the CANCEL fires (node's `signal` only hits /bin/sh).\n if (ctl.signal.aborted) killGroup(proc, 'SIGKILL');\n else ctl.signal.addEventListener('abort', () => killGroup(proc, 'SIGKILL'), { once: true });\n // Deadline reached: hand the still-running child to the job registry rather than killing it.\n // Only a host with a registry can do this — elsewhere the deadline stays a kill, because a\n // child nothing tracks is worse than a dead one.\n const registry = options.registry;\n if (registry?.adopt) {\n onDeadline = () => {\n timedOut = true;\n if (settled) return;\n flushEmit(ctx);\n // The registry attaches its own collectors from here on; ours would only grow a buffer\n // nobody reads. `off` is not part of the minimal stream shape, hence the guarded call.\n (proc.stdout as any)?.off?.('data', collect);\n (proc.stderr as any)?.off?.('data', collect);\n const id = registry.adopt!(cmd, proc, out);\n finish(handoffFor(timeoutMs, id, clean(out), !!registry.notifiesOnExit));\n };\n }\n const collect = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n out += s;\n if (ctx.emit && !settled) {\n pend += s;\n if (pend.length >= 1024) flushEmit(ctx);\n else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);\n }\n };\n proc.stdout?.on('data', collect);\n proc.stderr?.on('data', collect);\n proc.on('error', (err: any) => {\n // AbortError fires here when ctl.abort() kills the child — report timeout vs cancel.\n if (err?.name === 'AbortError' || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n log.debug('shell spawn error', err);\n finish(`[exit 1] ${err?.message ?? err}${out ? '\\n' + clean(out) : ''}`);\n });\n proc.on('close', (code: number | null) => {\n flushEmit(ctx); // drain the coalesce buffer before settling (still pre-resolve, so ctx.emit is live)\n if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n const body = clean(out);\n if (code && code !== 0) return finish(`[exit ${code}]${body ? '\\n' + body : ''}`);\n finish(body || '(command succeeded, no output)');\n });\n });\n } finally {\n clearTimeout(timer);\n if (flushTimer) clearTimeout(flushTimer); // no emits after the call settles\n ctx.signal?.removeEventListener('abort', onAbort);\n }\n },\n };\n}\n\n/** Deadline message when the child was ADOPTED rather than killed: the work continues, so say how to follow it. */\nfunction handoffFor(timeoutMs: number, id: string, body: string, notifies: boolean): string {\n const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} — NOT killed, it is still going. `\n + `Check on it with ShellOutput({id:\"${id}\"}) / ShellStatus({id:\"${id}\"}), stop it with ShellKill({id:\"${id}\"}). `\n + (notifies\n ? 'Its completion will be reported to you, so you may continue with other work meanwhile.'\n : 'Nothing will tell you when it finishes — poll it yourself before you rely on its result.');\n return body ? `${head}\\nOutput so far:\\n${body}` : head;\n}\n\n/** Abort message: timeout vs external cancel, preserving any partial output. */\nfunction reasonFor(timedOut: boolean, timeoutMs: number, body: string): string {\n // A foreground timeout is a DEAD END unless the result says how to get out of it: re-issuing the same\n // command is the one thing that cannot work, and it is exactly what a model does when the message is\n // just a code. Name the two recoveries (background, or narrow the command).\n const head = timedOut\n ? `[exit 124] timed out after ${timeoutMs}ms (killed). Re-running this command unchanged will time out again. `\n + `Either re-run it with background:true (returns a job id immediately; poll with ShellOutput/ShellStatus, `\n + `stop with ShellKill) or narrow it so it can finish — bound it, scope it, or ask for less.`\n : '[exit 130] cancelled (killed)';\n return body ? `${head}\\nPartial output before the kill:\\n${body}` : head;\n}\n\nconst NO_JOB = (id: string) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;\n\n/** Build the background-job companion tools (ShellOutput / ShellStatus / ShellKill) over a registry.\n * Each closes over THIS registry, i.e. this agent's jobs in this agent's cwd, so none of them can be\n * rebound to an isolated child's root: `withCwd` returns `undefined` and they are dropped from the\n * child's toolbelt. That keeps the capability coherent — the child's `Shell` cannot START a background\n * job (its registry is dropped), so it must not be able to inspect or SIGTERM the parent's either. */\nexport function makeShellJobTools(registry: ShellJobRegistry): AgentTool[] {\n const idParam = { type: 'object', properties: { id: { type: 'string', description: 'the job id from Shell({background:true})' } } };\n const dropOnRebind = { withCwd: () => undefined };\n return [\n {\n ...dropOnRebind,\n name: 'ShellOutput',\n description: 'Read the accumulated output (tail) of a background Shell job by id.',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n const out = registry.output(String(id));\n if (out == null) return NO_JOB(String(id));\n const st = registry.status(String(id))!;\n return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ''}]\\n${clean(out) || '(no output yet)'}`;\n },\n },\n {\n ...dropOnRebind,\n name: 'ShellStatus',\n description: 'Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.',\n parameters: idParam,\n async run({ id }) {\n if (!id) {\n const jobs = registry.list();\n return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join('\\n') : '(no background jobs)';\n }\n const st = registry.status(String(id));\n return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ''} · ${st.bytes} byte(s) buffered` : NO_JOB(String(id));\n },\n },\n {\n ...dropOnRebind,\n name: 'ShellKill',\n description: 'Stop a running background Shell job by id (SIGTERM).',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));\n },\n },\n ];\n}\n"],"mappings":"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","names":["log","path","log","res","log","log","ctx","resolve"]}
1
+ {"version":3,"sources":["../src/tools.ts","../src/llm.ts","../src/logging.ts","../src/redact.ts","../src/tools.structured.ts","../src/tools.web.ts","../src/shell.sandbox.ts","../src/tools.shell.ts"],"sourcesContent":["import type { IFilesystem } from '@livx.cc/wcli/core';\nimport { CommandExecutor, registerHeadlessCommands } from '@livx.cc/wcli/core';\nimport type { Tool, ChatLike } from './llm';\nimport { imageRefResult } from './llm';\nimport { forComponent } from './logging';\nimport { grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool, fuzzyLineReplace } from './tools.structured';\nimport { todoWriteTool, type TodoItem } from './todo';\nimport { webFetchTool, webSearchTool, webSearchAnthropicTool } from './tools.web';\nimport { OverlayFilesystem } from './OverlayFilesystem';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\nimport type { SandboxJobRegistry } from './tools.jobs';\n\nconst log = forComponent('tools');\n\n/** A structured multiple-choice question the model can pose to a human. */\nexport interface UserQuestion {\n question: string;\n header?: string;\n options: { label: string; description?: string }[];\n multiSelect?: boolean;\n}\n\n/**\n * The host / human-in-the-loop seam (the \"third seam\" beyond LLM + filesystem).\n * Injected per host: a CLI reads stdin, a browser renders a dialog, edge/headless\n * omits it. Unifies user-questions, permission/plan approvals, and notifications.\n */\nexport type HostEvent =\n | { kind: 'text_delta'; message: string }\n | { kind: 'speak_utterance'; message: string }\n | { kind: 'thinking_delta'; message: string }\n | { kind: 'tool_use'; id: string; name: string; input: unknown }\n | { kind: 'tool_result'; id: string; output: string; isError?: boolean }\n | { kind: 'tool_result_image'; id: string; dataUrl: string }\n // Display-only tool activity from a self-executing backend (e.g. Cursor CLI). NOT dispatched by the\n // Agent — purely for host UI (rich hosts read name/input/status for cards; plain hosts print `message`).\n | { kind: 'tool_activity'; id?: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error'; truncated?: { args?: boolean; result?: boolean }; message: string }\n | { kind: string; message: string; data?: unknown };\n\nexport interface HostBridge {\n /** Ask the user a structured question; resolve to the chosen label(s) / free text. */\n ask?(q: UserQuestion): Promise<string>;\n /** Request approval for a sensitive action (permission 'ask' / plan approval). */\n confirm?(prompt: string, meta?: { tool: string; input: unknown }): Promise<boolean>;\n /** Emit a progress / notification event to the host UI (non-blocking). */\n notify?(event: HostEvent): void;\n}\n\nexport interface ToolContext {\n fs: IFilesystem;\n exec: CommandExecutor;\n /** path -> content snapshot at last Read/Edit; powers the read-before-edit staleness guard. */\n readState: Map<string, string>;\n /** optional host interaction channel; absent => autonomous/headless. */\n host?: HostBridge;\n /** optional run-cancellation signal (mirrors AgentOptions.signal); lets abort-aware tools\n * (e.g. the real shell) kill in-flight work when the run is cancelled. */\n signal?: AbortSignal;\n /** the agent's working todo list (TodoWrite planning aid); replaced wholesale per call. */\n todos: TodoItem[];\n /** optional syntax guardrail: if set, write-class tools refuse to persist a broken result. */\n lint?: (path: string, content: string) => string | null;\n /** optional PDF text extraction (node hosts wire a pdftotext-backed impl); absent => Read explains. */\n pdfText?: (path: string) => Promise<string>;\n /** optional model handle for tools that run their own LLM pass (e.g. Review, a self-critique).\n * Populated by the Agent from its own ai/model; absent => such tools degrade to a no-op. */\n ai?: ChatLike;\n model?: string;\n /** optional sandbox background-job registry; enables `bash({background:true})`. Absent => no backgrounding. */\n jobs?: SandboxJobRegistry;\n /** optional incremental-output channel: long-running tools (e.g. the real Shell) stream chunks\n * here mid-run. Wired PER CALL by Agent.dispatch to Hooks.onToolOutput (cleared when the call\n * settles — a late emit is a silent no-op). Fire-and-forget: never awaited, never part of the result. */\n emit?: (chunk: string) => void;\n /** Wrap a HUMAN-blocking await (permission/plan confirm, an interactive question) so the time\n * spent parked on the user is excluded from the run's wall-clock kill-switches. Wired by the Agent;\n * absent => no accounting (the promise is awaited as-is). Idle prompt time must not count as work. */\n parkHuman?<T>(p: Promise<T>): Promise<T>;\n /** Set by `AskUserQuestion` when the model has a decision that is genuinely the user's to make but\n * no human is reachable (headless/piped/edge). The Agent drains it after the tool batch and ends\n * the run with `finishReason:'needs_input'`, handing the question to the caller instead of letting\n * the agent guess. Cleared at the start of every run. */\n needsInput?: UserQuestion;\n}\n\nexport interface AgentTool {\n name: string;\n description: string;\n parameters: object; // JSON Schema for the function's arguments\n /**\n * Longest wall-clock time this tool can legitimately take before it returns on its own.\n * Declared so a TRANSPORT above the loop (e.g. the cursor helper's per-call watchdog) can derive a\n * deadline with real headroom instead of carrying an independently-maintained magic number that\n * drifts into a tie with the tool's own cap — a tie the transport wins, destroying the tool's real\n * result. Omit for tools that return promptly.\n */\n maxDurationMs?: number;\n /**\n * Is `maxDurationMs` SELF-ENFORCED — does this tool abort itself at that bound and return?\n * An enforced bound cannot be overrun, so a transport above it needs only modest slack; an ADVISORY\n * one (the default) may be exceeded, so the transport keeps generous headroom over it. Declaring a\n * bound you do not actually enforce is worse than declaring none — say `true` only when you abort.\n */\n maxDurationEnforced?: boolean;\n run(args: any, ctx: ToolContext): Promise<string | { text: string; images?: { mimeType: string; data: string }[] }>;\n /** Optional: return a copy of this tool bound to a different real-disk working directory.\n * Implemented by tools that escape the VFS (the real `Shell`), which are cwd-bound at construction\n * and would otherwise keep pointing at the PARENT's tree when the tool instance is inherited by an\n * isolated child agent. `subagent.ts` calls this for a worktree child; tools without it pass through.\n * Return `undefined` for a capability that CANNOT follow the child (the background-job companions,\n * bound to the parent's job registry) — the tool is then dropped from the child's toolbelt.\n * An implementation must carry over any post-construction mutation the host made to the instance\n * (a renamed `name`/`description`) — rebuilding from the factory's options alone loses it. */\n withCwd?(cwd: string): AgentTool | undefined;\n}\n\n/** Build a tool context bound to a filesystem backend (Mem / Disk / …) and an optional host. */\nexport function makeContext(fs: IFilesystem, host?: HostBridge): ToolContext {\n const exec = new CommandExecutor(fs);\n registerHeadlessCommands(exec);\n return { fs, exec, readState: new Map(), host, todos: [] };\n}\n\n/** Convert AgentTools into the ai.libx.js `tools` array for chat(). */\nexport function toWireTools(tools: AgentTool[]): Tool[] {\n return tools.map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }));\n}\n\nconst numberLines = (content: string, offset = 0, limit?: number): string => {\n const lines = content.split('\\n');\n const start = Math.max(0, offset);\n const end = limit != null ? start + limit : lines.length;\n return lines\n .slice(start, end)\n .map((l, i) => `${start + i + 1}\\t${l}`)\n .join('\\n');\n};\n\n/** Keep huge tool output high-signal: head+tail with an omission marker (cuts re-runs to \"parse the wall\"). */\nexport function truncateOutput(s: string, headLines = 80, tailLines = 20): string {\n const lines = s.split('\\n');\n if (lines.length <= headLines + tailLines + 1) return s;\n const omitted = lines.length - headLines - tailLines;\n return [...lines.slice(0, headLines), `… (${omitted} lines omitted — narrow the command to see more) …`, ...lines.slice(-tailLines)].join('\\n');\n}\n\n/** Run any shell command line over the VFS (ls/cat/grep/find/head/tail/echo/mkdir/rm/mv/wc, pipes, redirects, &&/||/;). */\nexport const bashTool: AgentTool = {\n name: 'bash',\n description:\n 'Run a shell command. Supports ls, cat, grep, find, head, tail, echo, mkdir, rm, mv, cp, wc, pipes (|), redirects (>, >>), and chaining (&&, ||, ;). Best for: running tests/builds, file operations (mkdir/mv/rm), and piped workflows. For searching file contents, prefer `Grep` (structured results, no re-parse). For finding files by name, prefer `Glob`.',\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the command line to execute' },\n background: { type: 'boolean', description: 'run detached over an isolated overlay (writes commit when it finishes); returns a job id to poll with JobOutput. Only worth it for slow work (remote VFS / long pipelines).' },\n },\n },\n async run({ command, background }, ctx) {\n if (background && ctx.jobs) return startBashJob(String(command ?? ''), ctx);\n const r = await ctx.exec.execute(String(command ?? ''));\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n if (r.exitCode !== 0) {\n const err = (r.error ?? '').trim();\n return `[exit ${r.exitCode}]${err ? ' ' + err : ''}${out ? '\\n' + out : ''}`;\n }\n return out || '(command succeeded, no output)'; // explicit sentinel: don't re-run to \"check\"\n },\n};\n\n/** Kick a bash command into the background over an isolated overlay; its writes commit only on success.\n * A kill (abort) before completion skips the commit — the parent VFS is never touched mid-flight. */\nfunction startBashJob(command: string, ctx: ToolContext): string {\n const baseFs = ctx.fs;\n const id = ctx.jobs!.start(\n async ({ signal }) => {\n const overlay = new OverlayFilesystem(baseFs);\n const exec = new CommandExecutor(overlay);\n registerHeadlessCommands(exec);\n const r = await exec.execute(command); // wcli is sync-to-completion; abort can only gate the commit below\n if (signal.aborted) return '[killed before commit]';\n await overlay.commit(); // atomically flush this job's writes down into the parent VFS\n const out = truncateOutput((r.output ?? '').replace(/\\n+$/, ''));\n return r.exitCode !== 0 ? `[exit ${r.exitCode}] ${(r.error ?? '').trim()}\\n${out}`.trim() : out || '(command succeeded, no output)';\n },\n { kind: 'bash', label: command.slice(0, 60) },\n );\n return `Started background job ${id} — poll with JobOutput({id:\"${id}\"}) / JobStatus, stop with JobKill.`;\n}\n\n/** Image extensions the Read tool returns as a visual block (when the fs can read bytes). */\n/** The real on-disk path behind a VFS path, when the fs exposes one; null (with a reason) otherwise —\n * callers then fall back to inlining the bytes. */\nfunction realPathOf(fs: { realPath?: (p: string) => string }, path: string): string | null {\n if (typeof fs.realPath !== 'function') return null;\n try { return fs.realPath(path); }\n catch (e) { log.debug(`realPath unavailable for ${path} — inlining image bytes instead`, e); return null; }\n}\n\nconst IMG_MIME: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };\n\n/** Heuristic: is this utf8-decoded string actually a binary file? A NUL byte never occurs in text; a\n * dense run of U+FFFD replacement chars means the bytes weren't valid utf8. Cheap — scans a prefix. */\nfunction isBinary(raw: string): boolean {\n if (raw === '') return false; // empty is handled separately (could be a legit 0-byte text file)\n const head = raw.length > 8192 ? raw.slice(0, 8192) : raw;\n let repl = 0;\n for (let i = 0; i < head.length; i++) {\n const c = head.charCodeAt(i);\n if (c === 0x00) return true; // NUL byte -> definitively binary\n if (c === 0xfffd) repl++; // utf8 decode failure marker\n }\n return repl / head.length > 0.1;\n}\n\n/** Byte length of a file — exact via readFileBytes when the fs exposes it, else utf8 byte estimate. */\nasync function byteSize(fs: unknown, path: string, raw: string): Promise<number> {\n const bfs = fs as { readFileBytes?: (p: string) => Promise<Uint8Array> };\n if (typeof bfs.readFileBytes === 'function') {\n try { return (await bfs.readFileBytes(path)).length; } catch { /* fall through to estimate */ }\n }\n return Buffer.byteLength(raw, 'utf8');\n}\n\n/** Read a text file as 1-indexed numbered lines; arms the staleness guard for Edit. */\nexport const readTool: AgentTool = {\n name: 'Read',\n description:\n 'Read a file. Text files return 1-indexed numbered lines (with optional `offset`/`limit` and a re-Read pointer for partial reads). Image files (png/jpg/jpeg/gif/webp) return the picture itself so you can SEE it. PDFs return their extracted text. Always Read a file before Editing it.',\n parameters: {\n type: 'object',\n required: ['path'],\n properties: {\n path: { type: 'string' },\n offset: { type: 'number' },\n limit: { type: 'number' },\n },\n },\n async run({ path, offset, limit }, ctx) {\n // Image file → return it as a visual block. The adapter turns a tool result whose JSON carries a\n // `dataUrl` into an image tool_result the model can see. Needs a binary-capable fs (disk default);\n // the utf8 VFS (sandbox/Mem) can't, so we say so instead of corrupting the bytes.\n const ext = String(path).toLowerCase().split('.').pop() ?? '';\n // PDF → extracted text when the host wired an extractor (CLI: pdftotext); else say how to proceed.\n if (ext === 'pdf') {\n if (!ctx.pdfText) return `[${path} is a PDF — text extraction isn't available in this environment (install poppler's pdftotext and run on disk).]`;\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`; // jail-aware: hidden paths read as absent\n const text = (await ctx.pdfText(ctx.fs.resolvePath(path))).trim();\n return text ? numberLines(text, Math.max(0, offset ?? 0), limit) : `[${path}: no extractable text (scanned/image-only PDF?)]`;\n }\n if (IMG_MIME[ext]) {\n const fs = ctx.fs as { readFileBytes?: (p: string) => Promise<Uint8Array>; realPath?: (p: string) => string };\n if (typeof fs.readFileBytes !== 'function') {\n return `[${path} is an image, but this filesystem can't read binary — attach it as @${path} instead, or run on disk.]`;\n }\n // Persist a REFERENCE, not the payload: the send path (expandImagesForSend) turns it back into the\n // same `{dataUrl}` body the wire adapter renders, for the recent images only — so a screenshot Read\n // costs ~80 bytes in the transcript/session JSON instead of megabytes of immortal base64.\n const real = realPathOf(fs, path);\n if (real) {\n if (!(await ctx.fs.exists(path))) return `Error: File not found: ${path}`;\n return imageRefResult(real, IMG_MIME[ext]);\n }\n const bytes = await fs.readFileBytes(path);\n const b64 = Buffer.from(bytes).toString('base64');\n return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });\n }\n const raw = await ctx.fs.readFile(path);\n // Binary guard: a non-text file (a .png without a recognized ext, .zip, .bin, …) read as utf8 is\n // either empty or replacement-char garbage — both silently pollute context. A silent empty tool\n // result is plausibly what tips a delegated model (cursor/composer) into an empty reply, where a\n // native model tolerates it. Return an explicit, non-empty marker so the model always sees SOMETHING.\n if (isBinary(raw)) {\n const bytes = await byteSize(ctx.fs, path, raw);\n return `[binary file: ${path} — ${bytes} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;\n }\n ctx.readState.set(ctx.fs.resolvePath(path), raw); // staleness guard tracks the REAL content\n if (raw === '') return `[empty file: ${path} — 0 bytes]`;\n // Mask secret values in config files so keys can live there usably-but-hidden (line count is preserved).\n const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;\n const total = content === '' ? 0 : content.split('\\n').length;\n const start = Math.max(0, offset ?? 0);\n const body = numberLines(content, start, limit);\n // snippet-with-pointer: when the slice doesn't cover the whole file, tell the\n // model what it's missing + how to pull it — so it expands precisely instead of re-reading blind.\n const shownEnd = limit != null ? Math.min(start + limit, total) : total;\n const shownCount = Math.max(0, shownEnd - start);\n if (shownCount >= total) return body; // whole file shown — no footer\n if (shownCount === 0) return `[no lines in range (offset ${start}${limit != null ? `, limit ${limit}` : ''}) — file has ${total} line(s)]`;\n return `${body}\\n\\n[lines ${start + 1}–${shownEnd} of ${total} · re-Read with offset/limit for the rest]`;\n },\n};\n\n/** Replace an exact, unique substring; requires a prior Read and guards against stale edits. */\nexport const editTool: AgentTool = {\n name: 'Edit',\n description:\n 'Replace an exact substring in a file. Requires a prior Read of the same file. `old_string` must occur exactly once — include surrounding context to disambiguate.',\n parameters: {\n type: 'object',\n required: ['path', 'old_string', 'new_string'],\n properties: {\n path: { type: 'string' },\n old_string: { type: 'string' },\n new_string: { type: 'string' },\n },\n },\n async run({ path, old_string, new_string }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n const current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const count = old_string === '' ? 0 : current.split(old_string).length - 1;\n if (count > 1) throw new Error(`old_string is not unique in ${path} (${count} matches). Provide more surrounding context.`);\n let next: string, note = '';\n if (count === 1) {\n next = current.replace(old_string, () => new_string); // exact: function replacer, no $-pattern expansion\n } else {\n // exact match failed — try a whitespace-tolerant unique match before giving up (cuts re-read churn)\n const fuzzy = fuzzyLineReplace(current, old_string, new_string);\n if (fuzzy == null) throw new Error(`old_string not found in ${path}.`);\n next = fuzzy;\n note = ' (whitespace-tolerant match)';\n }\n if (ctx.lint) { const err = ctx.lint(path, next); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, next);\n ctx.readState.set(key, next);\n return `Edited ${path}${note}`;\n },\n};\n\n/** Session-exit tool: the model calls this when the user wants to end the conversation.\n * The `onExit` callback is injected by the host (CLI sets it to flip a flag that breaks the REPL loop). */\nexport function exitSessionTool(onExit: () => void): AgentTool {\n return {\n name: 'ExitSession',\n description:\n 'End the current session and exit the CLI. Call this when the user says goodbye, asks to quit, ' +\n 'or clearly indicates they want to stop the conversation (e.g. \"ok bye\", \"that\\'s all\", \"exit\", \"goodnight\").',\n parameters: { type: 'object', properties: {} },\n async run() {\n onExit();\n return 'Session ending. Goodbye!';\n },\n };\n}\n\nexport function defaultTools(): AgentTool[] {\n return [bashTool, readTool, editTool];\n}\n\n/**\n * The full catalog of selectable tools, keyed by name. The evolve loop's mutation\n * surface picks from this registry; embedders can build a custom tool set by name.\n */\nexport function toolRegistry(): Record<string, AgentTool> {\n const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool, webSearchAnthropicTool];\n return Object.fromEntries(all.map((t) => [t.name, t]));\n}\n\n/** Resolve a list of tool names against `available` (host-supplied tools, e.g. a parent agent's\n * toolbelt or mounted MCP tools) first, then the built-in registry. Unknown names throw.\n * Without `available` an allowlist could only ever name a built-in — so a def could not scope a\n * child to a tool its parent injected (`Bash`, `ToolSearch`, …). */\nexport function toolsByName(names: string[], available?: AgentTool[]): AgentTool[] {\n const reg = toolRegistry();\n const extra = Object.fromEntries((available ?? []).map((t) => [t.name, t]));\n return names.map((n) => {\n const t = extra[n] ?? reg[n];\n if (!t) throw new Error(`unknown tool '${n}'. Known: ${[...new Set([...Object.keys(extra), ...Object.keys(reg)])].join(', ')}`);\n return t;\n });\n}\n","/**\n * Wire types mirroring ai.libx.js (OpenAI-style chat). We type the transport\n * structurally via `ChatLike`, so an ai.libx.js `AIClient` is a drop-in — and a\n * `FakeAIClient` works in tests — with no hard runtime dependency on ai.libx.js.\n */\n\nimport { readFileSync, statSync, realpathSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nimport { randomBytes } from 'node:crypto';\nimport { forComponent } from './logging';\n\nconst log = forComponent('llm');\n\nexport type Role = 'system' | 'user' | 'assistant' | 'tool';\n\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string }; // arguments is a JSON string\n}\n\n/** One part of a multimodal message (mirrors ai.libx.js ContentPart) — text or an image URL/data-URI. */\nexport interface ContentPart {\n type: 'text' | 'image_url';\n text?: string;\n image_url?: { url: string };\n}\n\n/** A message's content is either plain text or an array of multimodal parts (images + text). */\nexport type MessageContent = string | ContentPart[];\n\nexport interface Message {\n role: Role;\n content: MessageContent;\n name?: string;\n tool_call_id?: string;\n tool_calls?: ToolCall[];\n /** The assistant step's reasoning, kept ONLY for providers that need it passed back (see passesBackReasoning). */\n reasoning_content?: string;\n}\n\n/**\n * Providers whose ai.libx.js adapter sends `reasoning_content` back to the vendor: DeepSeek (thinking +\n * tools 400s without it) and Moonshot/Kimi (\"preserve each historical assistant message's reasoning\").\n * Everyone else ignores the field, so persisting it there would only bloat the transcript — cursor, for\n * one, streams its WHOLE reply as reasoning.\n */\nexport function passesBackReasoning(model: string | undefined): boolean {\n return !!model && /^(deepseek|moonshot)\\//.test(model);\n}\n\n/** Flatten any message content to its text (string as-is; parts → concatenated text) — for length\n * estimation, summaries, and display. Non-text parts (images) contribute a short placeholder. */\nexport function contentText(content: MessageContent | undefined): string {\n if (content == null) return '';\n if (typeof content === 'string') return content;\n return content.map((p) => (p.type === 'text' ? (p.text ?? '') : '[image]')).join(p_sep(content));\n}\nconst p_sep = (parts: ContentPart[]): string => (parts.length > 1 ? '\\n' : '');\n\n/** Build an image content part from a data-URI or http(s) URL. */\nexport function imagePart(url: string): ContentPart {\n return { type: 'image_url', image_url: { url } };\n}\n\n/** Stub left when an inline data-URI image is elided from the stored transcript (RAM + disk). */\nexport const IMAGE_ELIDE_STUB = '[image elided — re-attach @path or /paste to view again]';\n\n/** True when content carries an inline base64 image (the heavy case — http(s) URLs are tiny). */\nexport function messageHasInlineImage(content: MessageContent | undefined): boolean {\n if (content == null || typeof content === 'string') return false;\n return content.some((p) => p.type === 'image_url' && (p.image_url?.url ?? '').startsWith('data:'));\n}\n\n/** Byte-weight for context budgeting — counts base64 payloads, not the `[image]` display placeholder. */\nexport function contentBytes(content: MessageContent | undefined): number {\n if (content == null) return 0;\n if (typeof content === 'string') return content.length;\n let n = 0;\n for (const p of content) {\n if (p.type === 'text') { n += (p.text ?? '').length; continue; }\n const url = p.image_url?.url ?? '';\n const ref = parseImageRef(url);\n // A ref is ~60 chars on disk but becomes the whole file on the wire IF it survives the send-time\n // window. Callers that budget the real payload run `expandImagesForSend` FIRST (Agent.trimContext)\n // or use `sendBytes` — this is the honest weight for a still-unexpanded ref.\n n += (ref ? ref.bytes : url.length) || 7;\n }\n return n;\n}\n\n/** Replace inline data-URI image parts with a text stub; http(s) URLs, image REFS and plain text\n * unchanged — a ref is ~60 bytes and stays re-expandable, so there is nothing to reclaim. */\nexport function elideInlineImages(content: MessageContent, stub = IMAGE_ELIDE_STUB): MessageContent {\n return elideImageParts(content, (url) => url.startsWith('data:'), stub);\n}\n\n/** Replace EVERY heavy image part — inline base64 AND refs — with the stub. This is the destructive\n * form used by the `/compact` elision ladder: a ref is cheap on disk but weighs its whole file on the\n * wire while it's inside the send window, so it must be shave-able or the ladder falls through to\n * shredding (then dropping) the user's own text. `elideInlineImages` stays the non-destructive form. */\nexport function elideImages(content: MessageContent, stub = IMAGE_ELIDE_STUB): MessageContent {\n return elideImageParts(content, (url) => url.startsWith('data:') || isImageRefUrl(url), stub);\n}\n\nfunction elideImageParts(content: MessageContent, hit: (url: string) => boolean, stub: string): MessageContent {\n if (typeof content === 'string') return content;\n let changed = false;\n const out: ContentPart[] = [];\n for (const p of content) {\n if (p.type !== 'image_url' || !hit(p.image_url?.url ?? '')) { out.push(p); continue; }\n changed = true;\n out.push({ type: 'text', text: stub });\n }\n return changed ? out : content;\n}\n\n/**\n * A persist-safe REFERENCE to an image FILE. Stored in the transcript (and the session JSON) instead\n * of the megabytes of base64 — `expandImagesForSend` turns it back into a data-URI in the OUTGOING\n * request only, so neither the heap nor the disk ever holds historical payloads.\n *\n * SECURITY: a ref is a CAPABILITY, not a path. The URL carries an unguessable token; the real path\n * lives only in this in-process registry, and only HOST code that already did the jail/permission\n * check can mint one (`mintImageRef`, used by the CLI's @image attach and the image `Read` tool).\n * Tool results are MODEL-CONTROLLED text (bash prints whatever it likes) — parsing a path out of them\n * could never be the authorization. An unknown/evicted token is simply not a ref: it degrades to the\n * elision stub, never a file read. That also covers a resumed session in a fresh process.\n * Shape stays inside `image_url.url` so old readers/serializers need no schema change:\n * `agentx-image:<mime>;<token>;<path>`\n * The trailing path is INERT BOOKKEEPING — `parseImageRef` never reads it, so it can't authorize\n * anything. It exists only so `remintImageRefs` can re-grant the capability on session load, after\n * re-checking the path against an allowed root (see there).\n */\nexport const IMAGE_REF_SCHEME = 'agentx-image:';\n\nexport interface ImageRef {\n mime: string;\n /** Absolute on-disk path. Never derived from model output — only from a mint-time capability. */\n path: string;\n /** base64 wire weight, stat'ed AT MINT TIME (so an overwritten-in-place path can never go stale). */\n bytes: number;\n}\n\n/** Bounded so a long session can't grow the registry without limit; oldest minted token is evicted\n * first (and an evicted ref degrades to the stub, exactly like a missing file). */\nconst REF_REGISTRY_MAX = 256;\nconst refRegistry = new Map<string, ImageRef>();\n\n/**\n * Mint an image-ref URL for an ALREADY-AUTHORIZED absolute path. Host-only: call this after the\n * jail/permission check, never on a path that came out of model output.\n */\nexport function mintImageRef(path: string, mime: string): string {\n const token = randomBytes(16).toString('hex');\n let bytes = 0;\n try { bytes = Math.ceil(statSync(path).size * 4 / 3); } // base64 expansion — what actually goes on the wire\n catch (e) { log.warn(`image ref stat failed, weighing as 0: ${path}`, e); }\n refRegistry.set(token, { mime, path, bytes });\n while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value as string);\n return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;\n}\n\n/** Test/host hook: forget every minted ref (simulates a fresh process / resumed session). */\nexport function clearImageRefs(): void { refRegistry.clear(); }\n\n/** Build an image REFERENCE part for an authorized path. */\nexport function imageRefPart(path: string, mime: string): ContentPart {\n return { type: 'image_url', image_url: { url: mintImageRef(path, mime) } };\n}\n\n/** Does this URL claim to be an image ref? (Claim only — resolution is `parseImageRef`.) */\nconst isImageRefUrl = (url: string | undefined): boolean => !!url && url.startsWith(IMAGE_REF_SCHEME);\n\n/** Resolve an image-ref URL through the registry; null when it isn't one, or the token is unknown\n * (forged by the model, evicted, or from a previous process). NEVER trusts the URL's own text. */\nexport function parseImageRef(url: string | undefined): ImageRef | null {\n const parts = refUrlParts(url);\n const ref = parts && refRegistry.get(parts.token);\n if (!ref) { log.debug('image ref not in registry — treating as non-image text'); return null; }\n return ref;\n}\n\n/** Split a ref URL into its literal fields. `path` here is UNTRUSTED bookkeeping — never a capability. */\nfunction refUrlParts(url: string | undefined): { mime: string; token: string; path: string } | null {\n if (!isImageRefUrl(url)) return null;\n const rest = url!.slice(IMAGE_REF_SCHEME.length);\n const a = rest.indexOf(';');\n if (a < 0) return null;\n const b = rest.indexOf(';', a + 1);\n return { mime: rest.slice(0, a), token: b < 0 ? rest.slice(a + 1) : rest.slice(a + 1, b), path: b < 0 ? '' : rest.slice(b + 1) };\n}\n\n/**\n * Session LOAD: re-grant the capability for stored image parts whose token died with the previous\n * process, so a resumed session's recent screenshot is visible again (parity with the pre-ref\n * behaviour, where `elideStaleImages` kept the last inline image).\n *\n * A session file is WRITABLE BY THE MODEL (it can Write/Edit it, and the user may hand-edit it), so a\n * stored path is untrusted input — its provenance proves nothing. The ROOT CHECK is what makes this\n * safe: only a path whose REALPATH (symlinks resolved) sits inside one of `roots` is re-minted.\n * Anything else is left unresolvable and degrades to the elision stub, with a warn.\n *\n * Only image PARTS in user/assistant content are considered. Tool-result strings stay inert forever —\n * those are the model's own stdout and must never mint anything.\n * Pure view: returns a new array only when something changed.\n */\nexport function remintImageRefs(messages: Message[], roots: string[]): Message[] {\n const allowed: string[] = [];\n for (const r of roots) {\n try { allowed.push(realpathSync(resolve(r))); } catch (e) { log.debug(`image-ref root unavailable: ${r}`, e); }\n }\n if (allowed.length === 0) return messages;\n const inside = (p: string): boolean => allowed.some((root) => p === root || p.startsWith(root + sep));\n let changed = false;\n const out = messages.map((m) => {\n if ((m.role !== 'user' && m.role !== 'assistant') || typeof m.content === 'string' || m.content == null) return m;\n let touched = false;\n const parts = m.content.map((p) => {\n if (p.type !== 'image_url') return p;\n const f = refUrlParts(p.image_url?.url);\n if (!f || refRegistry.has(f.token)) return p; // not a ref, or still live in this process\n let real: string;\n try { real = realpathSync(f.path); } catch (e) { log.warn(`resumed image ref unreadable, will stub: ${f.path}`, e); return p; }\n if (!inside(real)) { log.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`); return p; }\n touched = true;\n return { type: 'image_url', image_url: { url: mintImageRef(real, f.mime) } } as ContentPart;\n });\n if (!touched) return m;\n changed = true;\n return { ...m, content: parts };\n });\n return changed ? out : messages;\n}\n\n/** True when content carries an image REF part (weightless on disk, expanded at send time). The\n * CLAIM is enough: an unresolvable ref (resumed session / evicted token) still counts as image-bearing\n * so the send path turns it into the stub instead of shipping the unresolvable scheme to the provider. */\nexport function messageHasImageRef(content: MessageContent | undefined): boolean {\n if (content == null || typeof content === 'string') return false;\n return content.some((p) => p.type === 'image_url' && isImageRefUrl(p.image_url?.url));\n}\n\n/** Any image at all — inline base64 (legacy sessions) or a ref. Drives `keepRecent` windows. */\nexport function messageHasImage(content: MessageContent | undefined): boolean {\n return messageHasInlineImage(content) || messageHasImageRef(content);\n}\n\n/** `Read` on an image persists this instead of the base64; expanded to `{dataUrl}` at send time.\n * `path` must already have passed the filesystem's jail check (see `mintImageRef`). */\nexport function imageRefResult(path: string, mime: string): string {\n return JSON.stringify({ imageRef: mintImageRef(path, mime), image: path });\n}\n\n/** The registered ref behind a tool message's `{\"imageRef\":…}` JSON body, if that's what it is.\n * A model-forged body parses fine but resolves to null — the token isn't in the registry. */\nexport function toolImageRef(content: MessageContent | undefined): ImageRef | null {\n if (typeof content !== 'string' || !content.startsWith('{\"imageRef\":')) return null;\n try { return parseImageRef(JSON.parse(content).imageRef); } catch (e) { log.debug('bad imageRef tool result', e); return null; }\n}\n\n/** Elide inline images from all but the most-recent `keepRecent` image-bearing messages (mutates copies). */\nexport function elideStaleImages(messages: Message[], keepRecent = 1): Message[] {\n const idx = messages.map((m, i) => (messageHasImage(m.content) ? i : -1)).filter((i) => i >= 0);\n const drop = new Set(idx.slice(0, Math.max(0, idx.length - keepRecent)));\n if (drop.size === 0) return messages;\n let changed = false;\n const out = messages.map((m, i) => {\n if (!drop.has(i)) return m;\n const c = elideInlineImages(m.content);\n if (c === m.content) return m;\n changed = true;\n return { ...m, content: c };\n });\n return changed ? out : messages;\n}\n\n/**\n * The send-time verdict per message index. User ATTACHMENTS and tool-result image `Read`s get\n * INDEPENDENT windows: an image the model Reads mid-turn must never evict the image the user just\n * pasted (the spec's non-goal — vision on the paste turn must keep working), and vice-versa.\n */\nfunction imageWindows(messages: Message[], keepRecent: number): Set<number> {\n const attach: number[] = [];\n const tool: number[] = [];\n messages.forEach((m, i) => {\n if (toolImageRef(m.content)) tool.push(i);\n else if (messageHasImage(m.content)) attach.push(i);\n });\n const k = Math.max(0, keepRecent);\n return new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);\n}\n\n/** Is this message image-bearing at all (either kind)? */\nconst bearsImage = (m: Message): boolean => messageHasImage(m.content) || toolImageRef(m.content) != null;\n\n/**\n * SEND-PATH expansion: image REFS become real data-URIs only in the outgoing request, and only for the\n * most-recent `keepRecent` messages of EACH kind (user attachment / tool image Read) — older ones (refs\n * AND legacy inline base64) collapse to the elision stub. Pure view: the caller's messages are never mutated.\n * A missing/unreadable file (or an unregistered ref) degrades to the same stub and is LOGGED — never throws.\n */\nexport function expandImagesForSend(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): Message[] {\n if (!messages.some(bearsImage)) return messages;\n const keep = imageWindows(messages, keepRecent);\n let changed = false;\n const out = messages.map((m, i) => {\n const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);\n if (content === m.content) return m;\n changed = true;\n return { ...m, content };\n });\n return changed ? out : messages;\n}\n\n/**\n * Byte-weight of a transcript AS IT WILL GO ON THE WIRE — refs inside the keep window weigh their\n * file's base64 size, everything stale weighs the stub — WITHOUT materializing a single byte of\n * base64. This is the honest number for `/context` and the CLI footer, where the old behaviour made\n * every historical screenshot immortal and produced false \"90% full\" warnings.\n */\nexport function sendBytes(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): number {\n let n = 0;\n for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;\n return n;\n}\n\n/** `sendBytes` split per message — same window, same weights. Lets a budgeter drop/shave message-by-\n * message without re-deriving the window (and without charging a stale ref its file weight). */\nexport function sendBytesPer(messages: Message[], keepRecent = 1, stub = IMAGE_ELIDE_STUB): number[] {\n const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;\n return messages.map((m, i) =>\n wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0) + (m.reasoning_content?.length ?? 0));\n}\n\n/** contentBytes, but aware of whether this message's images survive the send-time window. */\nfunction wireBytes(content: MessageContent | undefined, kept: boolean, stub: string): number {\n const tool = toolImageRef(content);\n if (tool) return kept ? tool.bytes : stub.length;\n if (content == null || typeof content === 'string') return contentBytes(content);\n if (kept) return contentBytes(content);\n let n = 0;\n for (const p of content) {\n if (p.type === 'text') { n += (p.text ?? '').length; continue; }\n const url = p.image_url?.url ?? '';\n n += url.startsWith('data:') || isImageRefUrl(url) ? stub.length : url.length || 7;\n }\n return n;\n}\n\n/** Read a referenced image as a data-URI; null (logged) when the file is gone/unreadable. */\nfunction readRefDataUrl(ref: ImageRef): string | null {\n try { return `data:${ref.mime};base64,${readFileSync(ref.path).toString('base64')}`; }\n catch (e) { log.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e); return null; }\n}\n\n/** Refs → data-URIs (tool `{imageRef}` bodies → the `{dataUrl}` shape the wire adapter renders). */\nfunction expandContent(content: MessageContent, stub: string): MessageContent {\n const tool = toolImageRef(content);\n if (tool) {\n const url = readRefDataUrl(tool);\n return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;\n }\n if (typeof content === 'string') return content;\n let changed = false;\n const out = content.map((p) => {\n if (p.type !== 'image_url' || !isImageRefUrl(p.image_url?.url)) return p;\n changed = true;\n // Unresolvable (resumed session / evicted token) → stub. Content PARTS are host-authored, so an\n // unknown one is a lost capability, not model text — and the scheme must never reach the provider.\n const ref = parseImageRef(p.image_url?.url);\n const url = ref && readRefDataUrl(ref);\n return url ? imagePart(url) : ({ type: 'text', text: stub } as ContentPart);\n });\n return changed ? out : content;\n}\n\n/** Stale image-bearing content as it goes on the wire: no base64, no ref the provider can't resolve. */\nfunction stripContent(content: MessageContent, stub: string): MessageContent {\n if (toolImageRef(content)) return stub;\n if (typeof content === 'string') return content;\n let changed = false;\n const out = content.map((p) => {\n const url = p.type === 'image_url' ? (p.image_url?.url ?? '') : '';\n if (!url.startsWith('data:') && !isImageRefUrl(url)) return p;\n changed = true;\n return { type: 'text', text: stub } as ContentPart;\n });\n return changed ? out : content;\n}\n\nexport interface Tool {\n type: 'function';\n function: { name: string; description?: string; parameters: object };\n}\n\nexport interface ChatResponse {\n content: string;\n finishReason?: string;\n toolCalls?: ToolCall[];\n model?: string;\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n /** The step's accumulated reasoning/thinking text (provider-dependent). */\n reasoningContent?: string;\n /** Tools a self-executing runtime (cursor/claude-code) ran inside its OWN loop — correlated from\n * the display-only `toolActivity` stream. Not dispatched by us; persisted so the transcript keeps\n * a faithful record (audit + resume) instead of being narration-only. */\n delegatedTools?: { id: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error' }[];\n /** A delegated runtime (cursor/claude-code) ran tools this turn but never CLOSED it: either no assistant\n * text followed the last tool (it went silent post-work), or the text it did emit ends on an action it\n * announced and never took. Signals the loop's closure reflex (see AgentOptions.closeDelegatedTurns). */\n endedWithoutClosing?: boolean;\n}\n\n/**\n * Does this assistant text end mid-plan — on an action the model ANNOUNCED but never took?\n *\n * Cursor/composer narrates as it works (\"Searching the codebase…\", \"Also checking for X\"), and its run\n * can settle FINISHED while the narration is still mid-plan. The turn then looks closed (finishReason\n * 'stop', text present) but delivered no answer. Measured over the local cursor session corpus: ~4.6%\n * of tool-using turns end this way.\n *\n * Matches only FIRST-PERSON commitments (\"Let me…\", \"I'll…\", \"I need to…\") and progressive narration\n * (\"Checking…\", \"Expanding…\"). Deliberately excludes \"Next step: …\" / \"Next run will…\" and \"Let me know\n * if…\" — those are legitimate things to say TO the user, and nudging them would talk over a finished\n * answer. A false positive costs one extra step, so the rule stays conservative rather than clever.\n *\n * The two arms are deliberately scoped differently:\n *\n * COMMITMENT — first-person SINGULAR \"I'll / let me / I'm going to\", plus the one obligation form the\n * corpus actually stalled on: \"I need to…\". These carry their own subject and tense, so they stay\n * unambiguous anywhere in the sentence and may be matched clause-by-clause — the observed stuck turn\n * hid its commitment in a trailing \"…, so I need to verify …\" clause. Past-tense reports of the same\n * (\"I needed to\") are excluded: they describe work already reasoned about, not work still pending.\n * Second person (\"you'll need to…\") is excluded — that is a recommendation to the user.\n *\n * Deliberately NOT matched: \"we …\" and the softer obligations \"should / must / have to / want to\".\n * They read as recommendations TO the user on a FINISHED answer (\"All tests pass. We should be good to\n * go.\", \"I have to admit, the numbers were surprising.\"), the same category as the NEXT_STEP and\n * SIGN_OFF carve-outs — and nudging a finished answer spends a metered step talking over it. No turn in\n * the corpus needed them, so the conservative bias keeps them out.\n *\n * PROGRESSIVE — a bare gerund (\"Checking the images…\") is only a commitment at the HEAD of the final\n * sentence. Mid-sentence it is almost always a participial modifier of a COMPLETED action\n * (\"I pruned the cache, reclaiming 2.6 GB\"), so it is not matched clause-wise.\n */\nconst COMMITMENT =\n /^(?:also\\s+|now\\s+|then\\s+|so\\s+)*(?:let me\\b(?!\\s+know\\b)|i'?ll\\b|i will\\b|i'?m going to\\b|i\\s+(?:still\\s+|now\\s+|first\\s+)*need to\\b)/i;\nconst PROGRESSIVE =\n /^(?:also\\s+|now\\s+|then\\s+)*(?:re-?)?(?:check|search|scan|read|verify|run|inspect|expand|look|examin|continu|proceed|try|fetch|load|open|test|build|grep|find|review|trac|dig|explor)\\w*ing\\b/i;\nconst NEXT_STEP = /^next (?:step|run|up)\\b/i;\n// \"Let me know if…\" is a SIGN-OFF, not an announced action. It only ever reached the `let me\\b` arm on\n// delegated turns before; once the sawTool gate is dropped (announce-and-stop turns run no tools at all)\n// it would fire on every polite closing line, so exclude it explicitly (in COMMITMENT and here).\nconst SIGN_OFF = /^let me know\\b/i;\n\n/** Split a sentence into clauses at punctuation and coordinators, so a trailing commitment is visible. */\nfunction clauses(sentence: string): string[] {\n return sentence\n .split(/[,;:]\\s*|\\s+(?:so|but|and|then|because|however|therefore|thus|although|though)\\s+/i)\n .map((c) => c.trim())\n .filter(Boolean);\n}\n\nexport function endsOnAnnouncedAction(text: string): boolean {\n const sentences = text.split(/(?<=[.!?])\\s+|\\n+/).map((s) => s.trim()).filter(Boolean);\n const last = sentences[sentences.length - 1];\n if (!last || NEXT_STEP.test(last) || SIGN_OFF.test(last)) return false;\n if (PROGRESSIVE.test(last)) return true;\n return clauses(last).some((c) => !NEXT_STEP.test(c) && !SIGN_OFF.test(c) && COMMITMENT.test(c));\n}\n\n/**\n * One incremental event from a streamed `chat({stream:true})` call — mirrors\n * ai.libx.js's `StreamChunk` (OpenAI-style): each chunk carries a `content`\n * text delta; the terminal chunk carries `finishReason` and the accumulated\n * `toolCalls`. Consuming the stream and folding the deltas reconstructs the\n * same `ChatResponse` the non-stream path returns.\n */\nexport interface StreamChunk {\n content: string;\n finishReason?: string;\n index?: number;\n toolCalls?: ToolCall[]; // accumulated tool calls (typically on the final chunk)\n reasoningContent?: string;\n /** Display-only tool activity from a self-executing backend (Cursor CLI) — surfaced as a host\n * `tool_activity` event, never dispatched. Mirrors ai.libx.js's StreamChunk.toolActivity. */\n toolActivity?: { id?: string; name: string; input?: unknown; output?: unknown; status: 'running' | 'completed' | 'error'; truncated?: { args?: boolean; result?: boolean } };\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number }; // on the terminal chunk, when the provider reports it\n}\n\nexport interface ChatOptions {\n model: string;\n messages: Message[];\n tools?: Tool[];\n toolChoice?: unknown;\n stream?: boolean;\n /** Cancel the request/stream. Forwarded to providers that honor it; the Agent also stops consuming on abort. */\n signal?: AbortSignal;\n [k: string]: unknown;\n}\n\n/** Minimal shape of an ai.libx.js AIClient that the Agent drives. */\nexport interface ChatLike {\n chat(options: ChatOptions): Promise<ChatResponse | AsyncIterable<StreamChunk>>;\n}\n","// Import the log module directly from libx.js source: libx.js's main bundle\n// doesn't re-export `log` as a named ESM export, and source-importing keeps\n// libx.js patches live (no rebuild) — matching the `bun link` workflow.\nimport { log } from 'libx.js/src/modules/log';\n\n/** Component-scoped logger (libx.js). debug/verbose gated via DEBUG env/localStorage. */\nexport const forComponent = (name: string) => log.forComponent(name);\nexport { log };\n","/**\n * Mask secret-looking values in arbitrary text before it reaches the model.\n *\n * Two complementary seams use this: real-shell output (`cat .env`, `printenv`) and the\n * `Read` tool (so provider keys stored in `.agent/settings.json` are usable-but-masked).\n * The FS jail hides whole secret FILES by name; this hides secret VALUES wherever they\n * surface in otherwise-legitimate content.\n *\n * Both regexes are linear (no nested quantifiers) — safe against catastrophic backtracking\n * and cheap enough to run on every tool output (see tests/redact.bench).\n */\n\nexport const REDACTED = '‹redacted›';\n\n/** Config/control files that may carry provider keys — readers (Read/Grep) mask secret VALUES in\n * these while keeping the rest readable. (Whole secret FILES like .env are hidden by the FS jail.) */\nexport const CONFIG_FILE_RE = /(^|\\/)\\.(agent|claude)\\/(settings(\\.[\\w-]+)?\\.json|config\\.(json|js|mjs|cjs|ts))$/i;\n\n// (A) `NAME=value` / `\"name\": \"value\"` pairs where NAME looks like a secret. Masks the value only,\n// so the agent still sees WHICH key exists (useful config context) without the secret itself.\nconst SECRET_PAIR =\n /((?:^|[\\s,{[])(?:export\\s+)?[\"']?[\\w.\\-]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE_KEY|ACCESS_?KEY|AUTH(?:_?TOKEN)?|BEARER)[\\w.\\-]*[\"']?\\s*[:=]\\s*)([\"']?)([^\\s\"',{}\\]]+)/gi;\n\n// (B) Bare tokens by well-known shape — catches secrets that appear without an obvious key\n// (Authorization headers, URLs, JSON dumps). Conservative prefixes to avoid false positives.\nconst SECRET_TOKEN =\n /\\b(sk-ant-[\\w-]{12,}|sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[\\w]{20,}|xox[baprs]-[\\w-]{10,}|AKIA[0-9A-Z]{12,}|AIza[\\w-]{20,}|eyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,})\\b/g;\n\n/** Return `text` with secret values masked. Cheap no-op when nothing matches. */\nexport function redactSecrets(text: string): string {\n if (!text) return text;\n return text\n .replace(SECRET_PAIR, (_m, head, quote, _val) => `${head}${quote}${REDACTED}`)\n .replace(SECRET_TOKEN, REDACTED);\n}\n","import type { IFilesystem } from '@livx.cc/wcli/core';\nimport type { AgentTool, ToolContext } from './tools';\nimport type { ChatResponse } from './llm';\nimport { redactSecrets, CONFIG_FILE_RE } from './redact';\n\n/**\n * Structured file tools — typed results straight from the VFS, no `bash` parsing.\n * These close the efficiency gap to Claude Code (its Grep/Glob/Write return one\n * structured result where our agent otherwise drives find/grep pipelines and re-reads).\n * Backend-agnostic: pure IFilesystem walks, so they run on Mem/Disk/IndexedDb alike.\n */\n\n/** Throw the moment a run is cancelled, so a long file walk/scan bails instead of pegging the CPU\n * uninterruptibly. dispatch() catches the throw → the run loop sees `aborted` and ends the turn. */\nfunction ckAbort(signal?: AbortSignal): void {\n if (signal?.aborted) throw new Error('aborted');\n}\n\n/**\n * Directory names never worth walking. Pruned by NAME at every level (not by path), because the\n * cost is where they nest: a real-FS root of a JS deployment is mostly `node_modules`, and on a\n * measured 412k-file app root a single `Glob` spent 31s almost entirely inside it.\n */\nexport const DEFAULT_SKIP_DIRS = ['node_modules', '.git', 'dist', '.tmp'];\n\n/** Depth below a root at which the walk stops. Deep enough for any real source tree; a bound at all\n * is what turns a symlink cycle from a hang into a truncated result. */\nexport const DEFAULT_MAX_DEPTH = 24;\n\n/** How the file-scanning tools (Glob/Grep) decide WHAT to walk. */\nexport interface FileScanOptions {\n /** Roots to walk when a call names no `path`. Default: `[fs.getCwd()]`, i.e. today's behaviour.\n * A host with its data outside the cwd passes both, so a search can reach it at all. */\n roots?: string[];\n /** Directory names pruned during the walk. Default: {@link DEFAULT_SKIP_DIRS}. */\n skipDirs?: string[];\n /** Max depth below each root. Default: {@link DEFAULT_MAX_DEPTH}. */\n maxDepth?: number;\n}\n\ninterface WalkState {\n skip: Set<string>;\n maxDepth: number;\n /** Real paths already visited — the cycle/duplication guard. A `vendor` symlink pointing back\n * into the tree (or `node_modules/.bin`) otherwise re-walks the same subtree repeatedly.\n * PER ROOT, not shared: with one shared set a second root that is a symlink to (or into) the\n * first is \"already seen\" and silently yields nothing — the caller asked for a place and got an\n * empty answer, which is the absent-vs-unreachable confusion this file is trying to kill. */\n seen: Set<string>;\n /** Directory names actually pruned, and depth cuts actually made — reported to the caller, never\n * swallowed: a walk that refused to look somewhere must say so, or `(no matches)` is a lie. */\n prunedDirs: Set<string>;\n depthCuts: number;\n}\n\n/** `canonicalPath` (symlinks resolved) is an optional capability on disk-backed filesystems; on\n * backends without it the VFS path IS the identity, so it is its own canonical form. Note it must\n * be the symlink-resolving one: `realPath` is path arithmetic and reports a link and its target as\n * two different places, which cannot break a cycle. */\nfunction realOf(fs: IFilesystem, p: string): string {\n const rp = (fs as { canonicalPath?: (path: string) => string }).canonicalPath;\n if (typeof rp !== 'function') return p;\n try { return rp.call(fs, p) || p; } catch { return p; }\n}\n\n/** Recursively list every file path under `dir` (VFS-absolute), depth-first, sorted. Cancellable:\n * a cancelled run throws between entries so a huge tree walk (real-FS disk mode) doesn't wedge.\n * Prunes {@link DEFAULT_SKIP_DIRS}, bounds depth, and never enters the same real dir twice. */\nasync function walkFiles(fs: IFilesystem, dir: string, signal?: AbortSignal, out: string[] = [], st?: WalkState, depth = 0): Promise<string[]> {\n const state: WalkState = st ?? { skip: new Set(DEFAULT_SKIP_DIRS), maxDepth: DEFAULT_MAX_DEPTH, seen: new Set(), prunedDirs: new Set(), depthCuts: 0 };\n if (depth > state.maxDepth) { state.depthCuts++; return out; }\n const real = realOf(fs, dir);\n if (state.seen.has(real)) return out;\n state.seen.add(real);\n let entries: string[];\n try { entries = await fs.readDir(dir); } catch { return out; }\n for (const name of entries.sort()) {\n ckAbort(signal);\n if (state.skip.has(name)) { state.prunedDirs.add(name); continue; }\n const p = dir === '/' ? `/${name}` : `${dir}/${name}`;\n if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out, state, depth + 1);\n else out.push(p);\n }\n return out;\n}\n\n/** The roots a call actually walks: an explicit `path` wins, else the configured roots, else the\n * cwd. Nested roots are collapsed — walking `/a` and `/a/b` would double-report everything in b. */\nfunction scanRoots(fs: IFilesystem, path: unknown, opts?: FileScanOptions): string[] {\n const asked = path == null ? [] : (Array.isArray(path) ? path : [path]).map((p) => String(p)).filter(Boolean);\n const roots = (asked.length ? asked : (opts?.roots?.length ? opts.roots : [fsCwd(fs)])).map((r) => (r.length > 1 && r.endsWith('/') ? r.slice(0, -1) : r));\n return [...new Set(roots)].filter((r, _i, all) => !all.some((other) => other !== r && r.startsWith(`${other}/`)));\n}\n\n/** What a walk refused to look at — appended to results so a pruned/truncated search never reads\n * as an exhaustive one. Mirrors Grep's existing \"skipped N unreadable files\" note. */\nfunction scanNote(states: WalkState[]): string {\n const pruned = [...new Set(states.flatMap((st) => [...st.prunedDirs]))].sort();\n const cuts = states.reduce((n, st) => n + st.depthCuts, 0);\n const parts: string[] = [];\n if (pruned.length) parts.push(`skipped ${pruned.join(', ')} (pass skipDirs to include them)`);\n if (cuts) parts.push(`stopped at depth ${states[0]?.maxDepth} in ${cuts} place${cuts === 1 ? '' : 's'} (pass maxDepth to go deeper)`);\n return parts.length ? `\\n[scan: ${parts.join('; ')}]` : '';\n}\n\n/** Walk every root once, with the configured prune/depth/cycle guards. Per-call `skipDirs`/`maxDepth`\n * override the construction-time ones, so a consumer of the default singletons is never stuck with\n * a prune it needs to see past (`Glob '**\\/dist/*'`, a `.tmp/` workspace). */\nasync function walkRoots(\n fs: IFilesystem, roots: string[], signal: AbortSignal | undefined, opts?: FileScanOptions, call?: FileScanOptions,\n): Promise<{ files: string[]; note: string }> {\n const skip = new Set(call?.skipDirs ?? opts?.skipDirs ?? DEFAULT_SKIP_DIRS);\n const maxDepth = call?.maxDepth ?? opts?.maxDepth ?? DEFAULT_MAX_DEPTH;\n const out: string[] = [];\n const states: WalkState[] = [];\n for (const root of roots) {\n // A fresh `seen` per root: cross-root dedupe belongs to the RESULT (callers Set-dedupe), not to\n // the walk, where it silently erases a whole root that aliases another.\n const state: WalkState = { skip, maxDepth, seen: new Set(), prunedDirs: new Set(), depthCuts: 0 };\n states.push(state);\n await walkFiles(fs, root, signal, out, state, 0);\n }\n return { files: out, note: scanNote(states) };\n}\n\n/** Per-call scan overrides, parsed from tool args. */\nfunction callScan(args: { skipDirs?: unknown; maxDepth?: unknown }): FileScanOptions {\n const sd = args.skipDirs;\n return {\n skipDirs: Array.isArray(sd) ? sd.map(String) : typeof sd === 'string' ? sd.split(',').map((x) => x.trim()).filter(Boolean) : undefined,\n maxDepth: args.maxDepth == null ? undefined : Number(args.maxDepth),\n };\n}\n\nconst SCAN_PARAMS = {\n path: { type: 'string', description: 'optional directory (or list) to search instead of the default roots; absolute VFS path' },\n skipDirs: { type: 'string', description: `comma-separated directory names to prune (default: ${DEFAULT_SKIP_DIRS.join(', ')}); pass an empty string to prune nothing` },\n maxDepth: { type: 'number', description: `max directory depth below each root (default ${DEFAULT_MAX_DEPTH})` },\n} as const;\n\n/** Anchor a relative glob at a specific root (an absolute pattern anchors to itself). Per-ROOT, not\n * per-cwd: with two roots, anchoring `**\\/*.md` at the cwd alone would silently match nothing in\n * the other one — the \"search can't reach it, so it reports absent\" failure this all exists for. */\nfunction anchoredGlobAt(root: string, glob: string): RegExp {\n if (glob.startsWith('/')) return globToRegExp(glob);\n return globToRegExp(`${root === '/' ? '' : root}/${glob}`);\n}\n\n/** A path/scope matcher over several roots: matches if the pattern matches when anchored at ANY. */\nfunction globMatcher(roots: string[], glob: string): (p: string) => boolean {\n const res = roots.map((r) => anchoredGlobAt(r, glob));\n return (p: string) => res.some((re) => re.test(p));\n}\n\n/**\n * Translate a glob (`**`, `*`, `?`) into an anchored RegExp over VFS-absolute paths.\n * `caseInsensitive` is used by the JailedFilesystem denylist so `/.ENV` can't slip past\n * a `.env` rule (file-matching tools keep the default case-sensitive behavior).\n */\nexport function globToRegExp(glob: string, caseInsensitive = false): RegExp {\n const g = glob.startsWith('/') ? glob : `/${glob}`;\n let re = '';\n for (let i = 0; i < g.length; i++) {\n const c = g[i];\n if (c === '*') {\n if (g[i + 1] === '*') { re += '.*'; i++; if (g[i + 1] === '/') i++; }\n else re += '[^/]*';\n } else if (c === '?') re += '[^/]';\n else re += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n }\n return new RegExp(`^${re}$`, caseInsensitive ? 'i' : '');\n}\n\n/** The dir structured tools walk/anchor at — the FS's working dir. Keeps Glob/Grep/RepoMap scoped\n * to the project (or, in CC-parity disk mode where root '/' is the real machine, the launch dir)\n * instead of crawling the whole filesystem. Backward-compatible: Mem/jailed-at-cwd backends report '/'. */\nconst fsCwd = (fs: IFilesystem): string => fs.getCwd();\n\n/** Compile a glob, resolving a relative pattern under the FS cwd (mirrors a shell's cwd-relative globs).\n * Absolute patterns are used as-is. With cwd '/' this is identical to the old `/`-anchored behavior. */\nfunction anchoredGlob(fs: IFilesystem, glob: string): RegExp {\n const cwd = fsCwd(fs);\n const base = cwd === '/' ? '' : cwd;\n return globToRegExp(glob.startsWith('/') ? glob : `${base}/${glob}`);\n}\n\n/** List paths matching a glob, sorted — the structured alternative to `find`. */\nexport function makeGlobTool(opts?: FileScanOptions): AgentTool {\n return {\n name: 'Glob',\n description:\n 'Find files by glob pattern (e.g. \"**/*.ts\", \"src/**/*.test.ts\"). Returns sorted paths, one per line. ' +\n 'Space-separated patterns combine; `!`-prefix excludes (e.g. \"**/*.ts !**/*.test.ts\"). Optional `path` to search a specific directory. Prefer over `bash find` for locating files — one call, structured output.',\n parameters: {\n type: 'object',\n required: ['pattern'],\n properties: {\n pattern: { type: 'string', description: 'glob pattern(s); ** matches across directories; prefix a pattern with ! to exclude' },\n ...SCAN_PARAMS,\n },\n },\n async run({ pattern, path, skipDirs, maxDepth }, ctx) {\n const roots = scanRoots(ctx.fs, path, opts);\n const pats = String(pattern ?? '').trim().split(/\\s+/).filter(Boolean);\n const include = pats.filter((p) => !p.startsWith('!')).map((p) => globMatcher(roots, p));\n const exclude = pats.filter((p) => p.startsWith('!')).map((p) => globMatcher(roots, p.slice(1)));\n const includes = include.length ? include : [globMatcher(roots, '**')]; // only-excludes → start from everything\n const { files, note } = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));\n const hits = files.filter((p) => includes.some((m) => m(p)) && !exclude.some((m) => m(p)));\n return (hits.length ? [...new Set(hits)].sort().join('\\n') : '(no matches)') + note;\n },\n };\n}\n\n/** Search file contents by regex, returning typed `path:line:text` hits with optional context. */\nexport function makeGrepTool(opts?: FileScanOptions): AgentTool {\n return {\n name: 'Grep',\n description:\n 'Search file contents by regex. Returns `path:line: text` hits. Optional `glob` to scope files, `path` to search a specific directory, `context` for surrounding lines, `filesOnly` for matching paths only. Prefer over `bash grep` for file content search — structured results, no re-parse needed. Use `bash` instead for running commands, tests, or piped workflows.',\n parameters: {\n type: 'object',\n required: ['pattern'],\n properties: {\n pattern: { type: 'string', description: 'JS regular expression' },\n glob: { type: 'string', description: 'optional file glob to restrict the search' },\n ...SCAN_PARAMS,\n context: { type: 'number', description: 'lines of context before/after each hit' },\n filesOnly: { type: 'boolean', description: 'only list matching file paths' },\n },\n },\n async run({ pattern, glob, path, skipDirs, maxDepth, context, filesOnly }, ctx) {\n let re: RegExp;\n try { re = new RegExp(String(pattern ?? '')); } catch (e) { throw new Error(`invalid regex: ${String(e)}`); }\n const roots = scanRoots(ctx.fs, path, opts);\n const scope = glob ? globMatcher(roots, String(glob)) : null;\n const walked = await walkRoots(ctx.fs, roots, ctx.signal, opts, callScan({ skipDirs, maxDepth }));\n const files = [...new Set(walked.files)].sort().filter((p) => !scope || scope(p));\n const ctxN = Math.max(0, Number(context ?? 0));\n const out: string[] = [];\n const matched: string[] = [];\n let skipped = 0; // unreadable files (permissions, races) — surfaced, not silently dropped\n for (const path of files) {\n ckAbort(ctx.signal); // cancellable per-file: a wide grep over a big tree stops on Esc, not after\n let content: string;\n try { content = await ctx.fs.readFile(path); } catch { skipped++; continue; }\n const lines = content.split('\\n');\n const mask = CONFIG_FILE_RE.test(path); // mask secret values from config files in the hits\n let fileHit = false;\n for (let i = 0; i < lines.length; i++) {\n if (!re.test(lines[i])) continue;\n fileHit = true;\n if (filesOnly) break;\n const lo = Math.max(0, i - ctxN), hi = Math.min(lines.length - 1, i + ctxN);\n for (let j = lo; j <= hi; j++) out.push(`${path}:${j + 1}: ${mask ? redactSecrets(lines[j]) : lines[j]}`);\n }\n if (fileHit) matched.push(path);\n }\n const note = (skipped ? `\\n[skipped ${skipped} unreadable file${skipped === 1 ? '' : 's'}]` : '') + walked.note;\n if (filesOnly) return (matched.length ? matched.join('\\n') : '(no matches)') + note;\n return (out.length ? out.join('\\n') : '(no matches)') + note;\n },\n };\n}\n\n/** Default instances — cwd-rooted, standard prunes. Hosts that need other roots build their own. */\nexport const globTool: AgentTool = makeGlobTool();\nexport const grepTool: AgentTool = makeGrepTool();\n\n/** A line is a top-level declaration worth showing in the repo map. */\nconst SIG_RE = /^\\s*(export\\b|(?:export\\s+)?(?:async\\s+)?function\\s+\\*?\\w|(?:export\\s+)?(?:abstract\\s+)?class\\s+\\w|(?:export\\s+)?interface\\s+\\w|(?:export\\s+)?type\\s+\\w|(?:export\\s+)?enum\\s+\\w)/;\nconst isCode = (p: string) => /\\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p);\nconst isDoc = (p: string) => /\\.(md|mdx|txt)$/.test(p);\n\n/** Extract heading hierarchy + first-paragraph summaries from a markdown file. */\nfunction docOutlineOf(content: string, cap = 20): string[] {\n const out: string[] = [];\n const lines = content.split('\\n');\n for (let i = 0; i < lines.length && out.length < cap; i++) {\n const hm = lines[i].match(/^(#{1,4})\\s+(.+)/);\n if (hm) {\n out.push(hm[0].slice(0, 120));\n for (let j = i + 1; j < lines.length; j++) {\n const l = lines[j].trim();\n if (!l) continue;\n if (l.startsWith('#')) break;\n out.push(' ' + l.slice(0, 120));\n break;\n }\n }\n }\n return out;\n}\n\n/** One file's exported/top-level signatures (no bodies), capped. */\nfunction signaturesOf(content: string, cap = 40): string[] {\n const out: string[] = [];\n for (const line of content.split('\\n')) {\n if (!SIG_RE.test(line)) continue;\n let sig = line.trim();\n const brace = sig.indexOf('{');\n if (brace > 0) sig = sig.slice(0, brace).trim(); // drop the body\n sig = sig.replace(/\\s*=>?\\s*$/, '').replace(/=\\s*$/, '').slice(0, 120);\n if (sig && !out.includes(sig)) out.push(sig);\n if (out.length >= cap) break;\n }\n return out;\n}\n\n/**\n * Compact map of a VFS — code signatures and/or doc outlines. Edge-safe (pure IFilesystem walk).\n * `mode`: \"code\" (default) = top-level signatures; \"docs\" = heading outlines; \"all\" = both.\n */\nexport async function repoIndex(fs: IFilesystem, glob?: string, mode: 'code' | 'docs' | 'all' = 'code', signal?: AbortSignal): Promise<string> {\n const scope = glob ? anchoredGlob(fs, String(glob)) : null;\n const filter = mode === 'code' ? isCode : mode === 'docs' ? isDoc : (p: string) => isCode(p) || isDoc(p);\n const files = (await walkFiles(fs, fsCwd(fs), signal)).filter((p) => (scope ? scope.test(p) : filter(p)));\n const blocks: string[] = [];\n let shown = 0;\n for (const path of files) {\n ckAbort(signal);\n let content: string;\n try { content = await fs.readFile(path); } catch { continue; }\n const entries = isDoc(path) ? docOutlineOf(content) : signaturesOf(content);\n if (entries.length) { blocks.push(`${path}\\n${entries.map((s) => ' ' + s).join('\\n')}`); shown += entries.length; }\n if (shown >= 400) { blocks.push('… (map truncated; narrow with `glob`)'); break; }\n }\n const label = mode === 'code' ? 'code signatures' : mode === 'docs' ? 'document outlines' : 'entries';\n return blocks.length ? blocks.join('\\n') : `(no ${label} found)`;\n}\n\n/** Compact map of the codebase or document workspace — orient in ONE call, not many. */\nexport const repoMapTool: AgentTool = {\n name: 'RepoMap',\n description:\n 'Get a compact map of the workspace: code signatures and/or document outlines in ONE call. `scope`: \"code\" (default) = functions/classes/types; \"docs\" = markdown headings + summaries; \"all\" = both. Call once to orient before diving into specific files — avoids many exploratory Glob/Read calls.',\n parameters: {\n type: 'object',\n properties: {\n glob: { type: 'string', description: 'optional file glob to scope (default: all matching files)' },\n scope: { type: 'string', enum: ['code', 'docs', 'all'], description: 'what to map: \"code\" (default), \"docs\", or \"all\"' },\n },\n },\n run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || 'code', ctx.signal),\n};\n\n/**\n * Whitespace-tolerant fallback for Edit: locate `oldStr` in `content` ignoring each line's\n * leading/trailing whitespace, and replace the UNIQUE matching region with `newStr`. Returns\n * null if there isn't exactly one match (caller then errors, forcing a re-read) — so it never\n * guesses. This kills the common re-Read+retry cascade when an exact Edit fails on indentation drift.\n */\nexport function fuzzyLineReplace(content: string, oldStr: string, newStr: string): string | null {\n const norm = (s: string) => s.trim();\n const cl = content.split('\\n');\n const ol = oldStr.split('\\n').map(norm);\n while (ol.length && ol[ol.length - 1] === '') ol.pop();\n while (ol.length && ol[0] === '') ol.shift();\n if (!ol.length) return null;\n const matches: number[] = [];\n for (let i = 0; i + ol.length <= cl.length; i++) {\n let ok = true;\n for (let j = 0; j < ol.length; j++) if (norm(cl[i + j]) !== ol[j]) { ok = false; break; }\n if (ok) matches.push(i);\n }\n if (matches.length !== 1) return null; // not found, or ambiguous → don't guess\n const i = matches[0];\n return [...cl.slice(0, i), ...newStr.split('\\n'), ...cl.slice(i + ol.length)].join('\\n');\n}\n\n/** Read-before-overwrite guard, shared by Write and ApplyEdits' whole-file branch: blindly replacing\n * an existing file the agent never Read destroys its unseen content (the taskify-board incident).\n * `key` is the resolved path (the readState key); a fresh Write arms readState, so re-writing your own\n * output passes. Returns true when the write would clobber un-Read content and should be refused. */\nasync function wouldClobberUnread(ctx: ToolContext, path: string, key: string): Promise<boolean> {\n return !ctx.readState.has(key) && (await ctx.fs.exists(path));\n}\n\n/** Create or overwrite a file, creating parent directories as needed (mkdir -p). */\nexport const writeTool: AgentTool = {\n name: 'Write',\n description:\n 'Create or overwrite a file with the given contents, creating parent directories as needed. Use for new files instead of `bash echo >`. To replace an EXISTING file you must Read it first — overwriting an un-Read file is refused (it would destroy unseen content).',\n parameters: {\n type: 'object',\n required: ['path', 'content'],\n properties: { path: { type: 'string' }, content: { type: 'string' } },\n },\n async run({ path, content }, ctx) {\n const body = String(content ?? '');\n const key = ctx.fs.resolvePath(path);\n if (await wouldClobberUnread(ctx, path, key))\n throw new Error(`Refusing to overwrite ${path}: it exists and hasn't been Read. Read it first, then Write to replace it (or Edit to change part of it).`);\n if (ctx.lint) { const err = ctx.lint(path, body); if (err) throw new Error(err); }\n await mkdirp(ctx.fs, parentDir(key));\n await ctx.fs.writeFile(path, body);\n ctx.readState.set(key, body); // arm Edit on a freshly written file\n return `Wrote ${path}`;\n },\n};\n\n/** Apply an ordered list of exact-substring edits to one file in a single call. */\nexport const multiEditTool: AgentTool = {\n name: 'MultiEdit',\n description:\n 'Apply several exact-substring replacements to one file in order, in a single call. Requires a prior Read. Each `old_string` must be unique at the time it is applied. All-or-nothing: if any edit fails, none are written.',\n parameters: {\n type: 'object',\n required: ['path', 'edits'],\n properties: {\n path: { type: 'string' },\n edits: {\n type: 'array',\n items: {\n type: 'object',\n required: ['old_string', 'new_string'],\n properties: { old_string: { type: 'string' }, new_string: { type: 'string' } },\n },\n },\n },\n },\n async run({ path, edits }, ctx) {\n const key = ctx.fs.resolvePath(path);\n const snapshot = ctx.readState.get(key);\n if (snapshot == null) throw new Error(`File has not been read yet: ${path}. Read it before editing.`);\n let current = await ctx.fs.readFile(path);\n if (current !== snapshot) throw new Error(`File ${path} changed since it was read (stale). Re-read before editing.`);\n const list = Array.isArray(edits) ? edits : [];\n if (!list.length) throw new Error('edits must be a non-empty array');\n for (const [i, e] of list.entries()) {\n const count = e.old_string === '' ? 0 : current.split(e.old_string).length - 1;\n if (count === 0) throw new Error(`edit ${i}: old_string not found in ${path}.`);\n if (count > 1) throw new Error(`edit ${i}: old_string is not unique in ${path} (${count} matches).`);\n current = current.replace(e.old_string, () => e.new_string);\n }\n if (ctx.lint) { const err = ctx.lint(path, current); if (err) throw new Error(err); }\n await ctx.fs.writeFile(path, current);\n ctx.readState.set(key, current);\n return `Applied ${list.length} edit(s) to ${path}`;\n },\n};\n\n/**\n * Cross-file batch edit — the multi-file refactor primitive. One call edits MANY files:\n * each entry with an `old_string` replaces that exact, UNIQUE substring (read fresh + verified,\n * so NO prior Read is needed — locate the sites with Grep, whose output is the text to match);\n * each entry WITHOUT `old_string` writes `new_string` as the whole file (creates it + parent dirs).\n * Validate-all-before-write → atomic across files. Collapses \"Grep + N×(Read+Edit)\" into \"Grep + ApplyEdits\".\n */\nexport const applyEditsTool: AgentTool = {\n name: 'ApplyEdits',\n description:\n 'Apply edits across one or MORE files in a single call — for cross-file refactors (rename/extract/move). edits=[{path, old_string?, new_string}]. WITH old_string: replace that exact substring (must be UNIQUE in the file — add surrounding context; read fresh + verified, no prior Read needed). WITHOUT old_string: write new_string as the whole file (creates it + parent dirs; but to OVERWRITE a file that already exists you must Read it first, else use old_string to edit part of it). Locate sites first with Grep (its output shows the exact text). Atomic: validated across all files before any write.',\n parameters: {\n type: 'object',\n required: ['edits'],\n properties: {\n edits: {\n type: 'array',\n items: {\n type: 'object',\n required: ['path', 'new_string'],\n properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } },\n },\n },\n },\n },\n async run({ edits }, ctx) {\n const list = Array.isArray(edits) ? edits : [];\n if (!list.length) throw new Error('edits must be a non-empty array of {path, old_string?, new_string}');\n const planned = new Map<string, string>(); // resolved path -> final content (validate ALL before writing → atomic)\n for (const [i, e] of list.entries()) {\n const p = ctx.fs.resolvePath(String(e.path));\n const old = e.old_string == null ? '' : String(e.old_string);\n const neu = String(e.new_string ?? '');\n if (old === '') { // whole-file write / create\n // Same read-before-overwrite guard as Write (skip if an earlier edit in THIS batch already staged p).\n if (!planned.has(p) && (await wouldClobberUnread(ctx, String(e.path), p)))\n throw new Error(`edit ${i}: refusing to overwrite ${e.path} with a whole-file write — it exists and hasn't been Read. Read it first, or pass old_string to edit part of it.`);\n planned.set(p, neu); continue;\n }\n let cur = planned.has(p) ? planned.get(p)! : await ctx.fs.readFile(p).catch(() => { throw new Error(`edit ${i}: file not found: ${e.path}`); });\n const count = cur.split(old).length - 1;\n if (count > 1) throw new Error(`edit ${i}: old_string is not unique in ${e.path} (${count} matches) — add more context`);\n if (count === 1) cur = cur.replace(old, () => neu);\n else {\n const fz = fuzzyLineReplace(cur, old, neu);\n if (fz == null) throw new Error(`edit ${i}: old_string not found in ${e.path}`);\n cur = fz;\n }\n planned.set(p, cur);\n }\n if (ctx.lint) for (const [p, content] of planned) { const err = ctx.lint(p, content); if (err) throw new Error(err); } // validate ALL before any write\n for (const [p, content] of planned) { await mkdirp(ctx.fs, parentDir(p)); await ctx.fs.writeFile(p, content); ctx.readState.set(p, content); }\n return `Applied ${list.length} edit(s) across ${planned.size} file(s): ${[...planned.keys()].join(', ')}`;\n },\n};\n\nfunction parentDir(abs: string): string {\n const i = abs.lastIndexOf('/');\n return i <= 0 ? '/' : abs.slice(0, i);\n}\n\n/** mkdir -p over the VFS (idempotent, top-down). */\nexport async function mkdirp(fs: IFilesystem, dir: string): Promise<void> {\n if (dir === '/' || (await fs.exists(dir))) return;\n await mkdirp(fs, parentDir(dir));\n if (!(await fs.exists(dir))) await fs.createDir(dir);\n}\n\n/**\n * Review — verification WITHOUT execution: a fresh-context, adversarial critic pass over the\n * changes the agent just made. Verification-as-review (stolen from the review-fix-commit skill):\n * for an agent with no test runner, \"re-read to be sure\" is weak, but a COLD reviewer that reads\n * only {task + the produced files} (not the author's chain-of-thought) catches missed edge cases,\n * unstated implications, and subtle logic bugs a happy-path one-shot misses. Costs one model call.\n *\n * The `notes` arg is the with/without-CONTEXT knob: omit it for a pure cold review (the default,\n * least biased); pass it to feed the reviewer extra context. We don't decide which is better —\n * the self-evolution loop discovers it via its prompt rules.\n *\n * Degrades to a no-op notice if no model handle is wired (ctx.ai/ctx.model).\n */\nexport function reviewTool(): AgentTool {\n return {\n name: 'Review',\n description:\n 'Critically review your changes before finishing (verification without running code). Pass the task and the paths you changed; a fresh-context senior reviewer reads them COLD and returns concrete issues or \"LGTM\". Fix what it raises, then finish. Optional `notes` feeds the reviewer extra context.',\n parameters: {\n type: 'object',\n required: ['task', 'paths'],\n properties: {\n task: { type: 'string', description: 'what was asked — the spec/requirements to check the changes against' },\n paths: { type: 'array', items: { type: 'string' }, description: 'the files you changed/created, to be reviewed' },\n notes: { type: 'string', description: 'OPTIONAL extra context for the reviewer (rationale, constraints). Omit for a pure cold review.' },\n },\n },\n async run({ task, paths, notes }, ctx: ToolContext) {\n if (!ctx.ai || !ctx.model) return '[Review] no model handle wired — skipped.';\n const list: string[] = Array.isArray(paths) ? paths.map(String) : [];\n if (!list.length) return 'Error: pass the paths you changed in `paths`.';\n const files: string[] = [];\n for (const p of list.slice(0, 12)) {\n try {\n const body = await ctx.fs.readFile(p);\n files.push(`--- ${p} ---\\n${body.length > 4000 ? body.slice(0, 4000) + '\\n…(truncated)' : body}`);\n } catch {\n files.push(`--- ${p} ---\\n[could not read]`);\n }\n }\n const prompt =\n 'You are a senior engineer doing a critical code review. Review the changes below AGAINST THE TASK with deep, skeptical thinking — default to finding problems.\\n' +\n 'Focus on: correctness, edge cases (empty/boundary/negative inputs), unstated-but-implied requirements, and subtle logic bugs. Do NOT comment on style.\\n\\n' +\n `TASK:\\n${String(task ?? '').trim()}\\n\\n` +\n (notes ? `CONTEXT FROM THE AUTHOR:\\n${String(notes).trim()}\\n\\n` : '') +\n `CHANGED FILES:\\n${files.join('\\n\\n')}\\n\\n` +\n 'Reply with a short numbered list of concrete, actionable issues. If the changes correctly and completely satisfy the task with no edge cases missed, reply with exactly: LGTM';\n try {\n const r = (await ctx.ai.chat({ model: ctx.model, messages: [{ role: 'user', content: prompt }], stream: false })) as ChatResponse;\n const text = (r?.content ?? '').trim();\n return text || 'LGTM';\n } catch (e: any) {\n return `[Review] model error: ${e?.message ?? e} — skipped.`;\n }\n },\n };\n}\n","import type { AgentTool } from './tools';\nimport { forComponent } from './logging';\n\n/**\n * Web tools — `WebFetch` (retrieve a URL as readable text) and `WebSearch` (ranked\n * results via a configured provider). Opt-in (NOT in the default tool set): network\n * access is a deliberate capability. Factory-built with an injectable `fetch` so they\n * stay edge-portable and unit-testable without real network. `fetch` is read at call\n * time, so a no-network runtime simply has the tool return an error.\n */\nconst log = forComponent('web');\n\n/** Strip HTML to readable text — dependency-free: drop script/style/comments, block tags → newlines, decode common entities. */\nexport function htmlToText(html: string): string {\n let s = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<title[\\s\\S]*?<\\/title>/gi, ' ') // drop title text (don't leak it into context)\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ') // …same for noscript / textarea content\n .replace(/<textarea[\\s\\S]*?<\\/textarea>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<\\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, '\\n')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ');\n s = s\n .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#0?39;/g, \"'\").replace(/&#x27;/gi, \"'\");\n return s\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .split('\\n').map((l) => l.trim()).join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nexport interface WebFetchOptions {\n /** Override the global fetch (tests inject a mock; edge runtimes can supply their own). */\n fetch?: typeof globalThis.fetch;\n maxBytes?: number; // cap the downloaded body (default 2 MB)\n maxChars?: number; // cap the returned text (default 100k)\n timeoutMs?: number; // request timeout (default 15s)\n /** Allow fetching private/loopback/link-local hosts (default false — blocks basic SSRF). */\n allowPrivateHosts?: boolean;\n}\n\n/**\n * Block obvious SSRF targets by hostname/IP literal (loopback, private ranges, link-local incl.\n * cloud metadata 169.254.169.254, `.internal`). Pure/edge-safe — no DNS, so DNS-rebinding and\n * redirect-to-internal are NOT covered (an embedder needing that should supply a vetting `fetch`).\n */\nexport function isPrivateHost(host: string): boolean {\n const h = host.toLowerCase().replace(/^\\[|\\]$/g, ''); // strip IPv6 brackets\n if (h === '' || h === 'localhost' || h.endsWith('.localhost') || h.endsWith('.internal')) return true;\n if (h === '::1' || h === '::' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; // IPv6 loopback/link-local/ULA\n const m = h.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (m) {\n const a = +m[1], b = +m[2];\n return a === 0 || a === 127 || a === 10 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n return false;\n}\n\n/** Lazily-loaded node DNS resolver (absent on edge/browser) — closes DNS-rebinding (a public\n * hostname resolving to an internal IP) on the real-network path. Resolves null where unavailable. */\nlet _dnsLookup: ((h: string, opts?: any) => Promise<{ address: string }[]>) | null | undefined;\nasync function resolveIps(host: string): Promise<string[] | null> {\n if (_dnsLookup === undefined) {\n try { _dnsLookup = (await import('node:dns/promises')).lookup as any; }\n catch { _dnsLookup = null; } // edge/browser: no DNS — rely on the literal isPrivateHost check\n }\n if (!_dnsLookup) return null;\n try { return (await _dnsLookup(host, { all: true } as any)).map((a) => a.address); } catch { return null; }\n}\n\n/** Read a response body but stop at `maxBytes` of ACTUAL bytes (cancel the stream) — no unbounded download. */\nasync function readCapped(res: Response, maxBytes: number): Promise<string> {\n const reader = (res.body as any)?.getReader?.();\n if (!reader) { const t = await res.text(); return t.length > maxBytes ? t.slice(0, maxBytes) : t; }\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) { chunks.push(value); total += value.length; }\n if (total >= maxBytes) { try { await reader.cancel(); } catch { /* already closed */ } break; }\n }\n const out = new Uint8Array(Math.min(total, maxBytes));\n let off = 0;\n for (const c of chunks) { if (off >= out.length) break; const take = Math.min(c.length, out.length - off); out.set(c.subarray(0, take), off); off += take; }\n return new TextDecoder().decode(out);\n}\n\n/** Build a WebFetch tool. */\nexport function makeWebFetchTool(options: WebFetchOptions = {}): AgentTool {\n const maxBytes = options.maxBytes ?? 2_000_000;\n const maxChars = options.maxChars ?? 100_000;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: 'WebFetch',\n description:\n 'Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.',\n // Declared so a transport above the loop sizes its deadline around this one (see AgentTool.maxDurationMs).\n maxDurationMs: timeoutMs,\n parameters: { type: 'object', required: ['url'], properties: { url: { type: 'string', description: 'absolute http(s) URL' } } },\n async run({ url }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n const customFetch = !!options.fetch; // injected fetch (tests/edge) owns its own vetting → skip DNS\n const u = String(url ?? '');\n try { new URL(u); } catch { return `Error: invalid URL: ${u}`; }\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n // Reject a host that's a private/internal IP literal, or (on the real-network path) a name that\n // RESOLVES to one — re-checked on EVERY redirect hop so an external page can't bounce us internal.\n const hostBlock = async (hostname: string): Promise<string | null> => {\n if (options.allowPrivateHosts) return null;\n if (isPrivateHost(hostname)) return hostname;\n if (!customFetch) { const ips = await resolveIps(hostname); if (ips) for (const ip of ips) if (isPrivateHost(ip)) return `${hostname} → ${ip}`; }\n return null;\n };\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n let current = u;\n let res: Response;\n for (let hop = 0; ; hop++) {\n const pu = new URL(current);\n if (pu.protocol !== 'http:' && pu.protocol !== 'https:') return `Error: only http/https URLs are allowed (got \"${pu.protocol}\")`;\n const blocked = await hostBlock(pu.hostname);\n if (blocked) return `Error: refusing to fetch a private/internal address (${blocked}) — set allowPrivateHosts to override`;\n res = await doFetch(current, { signal: ctl.signal, redirect: 'manual', headers: { 'user-agent': 'agentx (+https://github.com/Livshitz/agentx)' } });\n if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {\n if (hop >= 5) return `Error fetching ${u}: too many redirects`;\n current = new URL(res.headers.get('location')!, current).toString(); // re-validated at loop top\n continue;\n }\n break;\n }\n const type = res.headers.get('content-type') ?? '';\n const body = await readCapped(res, maxBytes);\n const text = /html/i.test(type) || /^\\s*<(?:!doctype|html)/i.test(body) ? htmlToText(body) : body.trim();\n const capped = text.length > maxChars ? text.slice(0, maxChars) + `\\n… [truncated at ${maxChars} chars]` : text;\n return `${res.status} ${res.statusText} · ${new URL(current).host}\\n\\n${capped}`;\n } catch (e: any) {\n log.debug(`WebFetch ${u} failed`, e);\n return `Error fetching ${u}: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\nexport interface WebSearchOptions {\n fetch?: typeof globalThis.fetch;\n /** Provider: 'auto' (default) prefers Firecrawl, then Tavily (by key presence), else keyless DuckDuckGo.\n * 'anthropic' delegates to a cheap model with Anthropic's native server-side web_search. */\n provider?: 'auto' | 'tavily' | 'duckduckgo' | 'anthropic' | 'firecrawl';\n /** API key for Tavily (default: process.env.TAVILY_API_KEY). */\n apiKey?: string;\n /** Tavily endpoint override. */\n endpoint?: string;\n /** API key for Firecrawl (default: process.env.FIRECRAWL_API_KEY). */\n firecrawlApiKey?: string;\n /** Firecrawl search endpoint override. */\n firecrawlEndpoint?: string;\n maxResults?: number; // default 5\n timeoutMs?: number; // default 15s\n /** Tool name (default 'WebSearch'). Set to register a provider-pinned variant, e.g. 'WebSearchAnthropic'. */\n name?: string;\n /** Tool description override (default: the generic ranked-results blurb). */\n description?: string;\n /** Anthropic key for provider 'anthropic' (default: process.env.ANTHROPIC_API_KEY). */\n anthropicApiKey?: string;\n /** Model for provider 'anthropic' (default 'claude-haiku-4-5'). */\n model?: string;\n}\n\ninterface SearchHit { title: string; url: string; snippet: string }\n\n/** Decode a DuckDuckGo HTML result href: results are `//duckduckgo.com/l/?uddg=<encoded-target>` redirects. */\nexport function decodeDdgUrl(href: string): string {\n const m = href.match(/[?&]uddg=([^&]+)/);\n if (m) { try { return decodeURIComponent(m[1]); } catch { /* fall through */ } }\n return href.startsWith('//') ? 'https:' + href : href;\n}\n\n/** Parse DuckDuckGo's HTML results page into hits (title/url/snippet) — dependency-free, zips anchors to snippets in order. */\nexport function parseDdgHtml(html: string, max: number): SearchHit[] {\n const anchors = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__a[^\"]*\"[^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/g)];\n const snippets = [...html.matchAll(/<a[^>]*class=\"[^\"]*result__snippet[^\"]*\"[^>]*>([\\s\\S]*?)<\\/a>/g)].map((m) => htmlToText(m[1]));\n const hits: SearchHit[] = [];\n for (let i = 0; i < anchors.length && hits.length < max; i++) {\n const url = decodeDdgUrl(anchors[i][1]);\n try { if (isPrivateHost(new URL(url).hostname)) continue; } catch { continue; } // skip junk/internal redirects\n hits.push({ title: htmlToText(anchors[i][2]) || '(untitled)', url, snippet: snippets[i] ?? '' });\n }\n return hits;\n}\n\nfunction formatHits(hits: SearchHit[]): string {\n if (!hits.length) return '(no results)';\n return hits.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet.replace(/\\s+/g, ' ').slice(0, 240)}`).join('\\n\\n');\n}\n\n/** Provider 'firecrawl': agent-oriented search API (POST /v1/search, Bearer auth). Maps Firecrawl's\n * result objects into the same hit shape as Tavily/DDG. HTTP-only, no SDK. */\nasync function firecrawlSearch(q: string, opts: { key: string; endpoint: string; maxResults: number; fetch: typeof globalThis.fetch; signal: AbortSignal }): Promise<string> {\n const res = await opts.fetch(opts.endpoint, {\n method: 'POST',\n signal: opts.signal,\n headers: { authorization: `Bearer ${opts.key}`, 'content-type': 'application/json' },\n body: JSON.stringify({ query: q, limit: opts.maxResults }),\n });\n if (!res.ok) return `Error: Firecrawl search returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.data) ? data.data.slice(0, opts.maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.description ?? r.markdown ?? '') })));\n}\n\n/**\n * Provider 'anthropic': delegate the search to a cheap model running Anthropic's native server-side\n * web_search tool. Anthropic runs the search and reads the results; we prompt for un-editorialized\n * findings (one claim per line, each with its source URL) and relay them verbatim. Raw HTTP — no SDK,\n * stays edge-portable. Slower and costlier than DDG/Tavily, but better-sourced (native search index).\n */\nasync function anthropicSearch(q: string, opts: { key: string; model: string; fetch: typeof globalThis.fetch; signal: AbortSignal }): Promise<string> {\n const res = await opts.fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n signal: opts.signal,\n headers: { 'x-api-key': opts.key, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },\n body: JSON.stringify({\n model: opts.model,\n max_tokens: 1024,\n // Basic variant: Haiku-tier doesn't support the _20260209 dynamic-filtering variant (Opus 4.6+/Sonnet 4.6 only).\n tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 5 }],\n messages: [{ role: 'user', content:\n `Search the web for: ${q}\\n\\nReturn only the relevant findings as concise bullet points, each with its source URL in parentheses. ` +\n `Do not add a preamble, conclusion, opinion, or commentary. If sources conflict, list each claim with its source rather than resolving it.` }],\n }),\n });\n if (!res.ok) return `Error: Anthropic search returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n if (data?.stop_reason === 'refusal') return 'Error: Anthropic search refused the query';\n let text = '';\n for (const block of data?.content ?? []) if (block?.type === 'text') text += block.text;\n return text.trim() || '(no results)';\n}\n\n/**\n * Build a WebSearch tool. Keyless by default (DuckDuckGo HTML) so it works in any deployment with no\n * setup; on provider 'auto' it upgrades to an agent-oriented API by key presence — Firecrawl\n * (FIRECRAWL_API_KEY) preferred, then Tavily (TAVILY_API_KEY). Provider 'anthropic' is a separate path\n * (native search via a cheap model) exposed as the WebSearchAnthropic tool.\n */\nexport function makeWebSearchTool(options: WebSearchOptions = {}): AgentTool {\n const tavilyEndpoint = options.endpoint ?? 'https://api.tavily.com/search';\n const firecrawlEndpoint = options.firecrawlEndpoint ?? 'https://api.firecrawl.dev/v1/search';\n const maxResults = options.maxResults ?? 5;\n const timeoutMs = options.timeoutMs ?? 15_000;\n return {\n name: options.name ?? 'WebSearch',\n description: options.description ??\n 'Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic — then WebFetch a result URL to read it in full.',\n parameters: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } },\n async run({ query }) {\n const doFetch = options.fetch ?? globalThis.fetch;\n if (!doFetch) return 'Error: no network (fetch) available in this runtime';\n const q = String(query ?? '').trim();\n if (!q) return 'Error: empty query';\n const key = options.apiKey ?? process.env.TAVILY_API_KEY;\n const fcKey = options.firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY;\n const provider = options.provider ?? 'auto';\n // auto priority ladder: Firecrawl > Tavily > keyless DuckDuckGo (by key presence).\n const useFirecrawl = provider === 'firecrawl' || (provider === 'auto' && !!fcKey);\n const useTavily = provider === 'tavily' || (provider === 'auto' && !useFirecrawl && !!key);\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n if (provider === 'anthropic') {\n const akey = options.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!akey) return 'Error: WebSearchAnthropic requires ANTHROPIC_API_KEY (set in env)';\n return await anthropicSearch(q, { key: akey, model: options.model ?? 'claude-haiku-4-5', fetch: doFetch, signal: ctl.signal });\n }\n if (useFirecrawl) {\n if (!fcKey) return 'Error: Firecrawl provider selected but FIRECRAWL_API_KEY is not set';\n return await firecrawlSearch(q, { key: fcKey, endpoint: firecrawlEndpoint, maxResults, fetch: doFetch, signal: ctl.signal });\n }\n if (useTavily) {\n if (!key) return 'Error: Tavily provider selected but TAVILY_API_KEY is not set';\n const res = await doFetch(tavilyEndpoint, {\n method: 'POST',\n signal: ctl.signal,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ api_key: key, query: q, max_results: maxResults }),\n });\n if (!res.ok) return `Error: search provider returned ${res.status} ${res.statusText}`;\n const data: any = await res.json();\n const results = Array.isArray(data?.results) ? data.results.slice(0, maxResults) : [];\n return formatHits(results.map((r: any) => ({ title: r.title ?? '(untitled)', url: r.url ?? '', snippet: String(r.content ?? '') })));\n }\n // Keyless: DuckDuckGo HTML endpoint (no key, edge-portable).\n const res = await doFetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(q), {\n signal: ctl.signal,\n headers: { 'user-agent': 'Mozilla/5.0 (compatible; agentx/1.0; +https://github.com/Livshitz/agentx)' },\n });\n if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;\n return formatHits(parseDdgHtml(await res.text(), maxResults));\n } catch (e: any) {\n log.debug('WebSearch failed', e);\n return `Error searching: ${e?.name === 'AbortError' ? `timed out after ${timeoutMs}ms` : (e?.message ?? e)}`;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/** Default instances (registered in the tool registry; opt-in by name). */\nexport const webFetchTool = makeWebFetchTool();\nexport const webSearchTool = makeWebSearchTool();\nconst ANTHROPIC_SEARCH_DESC =\n \"High-quality web search via Anthropic's native search index. Returns concise, sourced findings — one claim per line, each with its source URL. Prefer this over WebSearch when accuracy and citations matter; it is slower (~3–8s) and bills the Anthropic API account per call (one cheap-model turn + search fee).\";\n\n/** Anthropic-native search as a provider-pinned WebSearch variant. The credential is API-key billing\n * (x-api-key); pass `anthropicApiKey` to inject a resolved key, else it falls back to ANTHROPIC_API_KEY. */\nexport function makeWebSearchAnthropicTool(opts: { anthropicApiKey?: string; model?: string } = {}): AgentTool {\n return makeWebSearchTool({ provider: 'anthropic', name: 'WebSearchAnthropic', description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });\n}\n/** Env-keyed singleton: registered in the tool registry so 'WebSearchAnthropic' resolves by name. The\n * CLI default-on path builds its own instance with a config-resolved key (see cli/core.ts). */\nexport const webSearchAnthropicTool = makeWebSearchAnthropicTool();\n","/**\n * Tier-1 OS sandbox for the real `Shell` tool (mind/03-roadmap.md \"capability tiers\").\n *\n * Wraps the spawned `/bin/sh` in the platform's process sandbox so a hostile/buggy command\n * can read the machine but can only WRITE inside an allowlist (cwd + tmp + extra `writePaths`),\n * and gets no network unless granted:\n * - macOS: `sandbox-exec` (seatbelt) with a generated profile\n * - Linux: `bwrap` (bubblewrap) with `--ro-bind / /` + writable binds\n *\n * Pure argv builders (unit-testable, no node imports) + an async wrapper-binary locator.\n * This complements — does not replace — env secret-scrubbing and the permission prompt:\n * the FS jail can't contain a real process; this makes the *process* itself contained.\n */\n\nexport class OsSandboxOptions {\n /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */\n network = false;\n /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */\n writePaths: string[] = [];\n}\n\nexport interface SandboxWrap {\n bin: string;\n args: string[]; // full argv: wrapper flags + /bin/sh -c <command>\n}\n\n/** Writable allowlist shared by both platforms: cwd, the tmp roots, /dev. */\nfunction writable(cwd: string, o: OsSandboxOptions, tmpDir?: string): string[] {\n const set = new Set<string>([cwd, '/tmp', '/private/tmp', '/private/var/folders', '/dev', ...(tmpDir ? [tmpDir] : []), ...o.writePaths]);\n return [...set];\n}\n\nconst sbQuote = (p: string) => `\"${p.replace(/([\"\\\\])/g, '\\\\$1')}\"`;\n\n/** macOS seatbelt profile: allow everything, then deny writes/network, then re-allow the allowlist\n * (seatbelt resolves conflicts by specificity, so subpath allows override the broad deny). */\nexport function seatbeltProfile(cwd: string, o: OsSandboxOptions, tmpDir?: string): string {\n const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(' ');\n return [\n '(version 1)',\n '(allow default)',\n ...(o.network ? [] : ['(deny network*)']),\n '(deny file-write*)',\n `(allow file-write* ${allows})`,\n ].join('\\n');\n}\n\n/** Build the wrapped argv for `sh -c <command>`, or null if `platform` has no supported wrapper. */\nexport function sandboxArgv(command: string, cwd: string, opts: Partial<OsSandboxOptions> = {}, platform: string = process.platform, tmpDir?: string): SandboxWrap | null {\n const o = { ...new OsSandboxOptions(), ...opts };\n if (platform === 'darwin') {\n return { bin: '/usr/bin/sandbox-exec', args: ['-p', seatbeltProfile(cwd, o, tmpDir), '/bin/sh', '-c', command] };\n }\n if (platform === 'linux') {\n const binds = writable(cwd, o, tmpDir).filter((p) => p !== '/dev' && !p.startsWith('/private')).flatMap((p) => ['--bind-try', p, p]);\n return {\n bin: 'bwrap',\n args: ['--ro-bind', '/', '/', ...binds, '--dev', '/dev', '--proc', '/proc', '--die-with-parent', ...(o.network ? [] : ['--unshare-net']), '/bin/sh', '-c', command],\n };\n }\n return null;\n}\n\n/** Locate the wrapper binary for this platform; null = sandboxing unavailable here. */\nexport async function findSandboxWrapper(platform: string = process.platform): Promise<string | null> {\n const { existsSync } = await import('node:fs');\n if (platform === 'darwin') return existsSync('/usr/bin/sandbox-exec') ? '/usr/bin/sandbox-exec' : null;\n if (platform === 'linux') {\n for (const dir of (process.env.PATH ?? '/usr/bin:/bin').split(':')) if (dir && existsSync(`${dir}/bwrap`)) return `${dir}/bwrap`;\n return null;\n }\n return null;\n}\n","import type { AgentTool } from './tools';\nimport { truncateOutput } from './tools';\nimport { redactSecrets } from './redact';\nimport { forComponent } from './logging';\nimport { sandboxArgv, findSandboxWrapper, type OsSandboxOptions } from './shell.sandbox';\n\n/**\n * Real shell tool — node-only, OPT-IN, and deliberately NOT edge-portable.\n *\n * ⚠️ Unlike the default VFS `bash` (a sandboxed JS interpreter over the virtual filesystem),\n * this spawns a REAL `/bin/sh` process. It can run `bun`, `git`, `ssh`, scripts, deploys —\n * and, by the same token, it is NOT sandboxed: only cwd-binding constrains it. It is a\n * deliberate host escalation, kept out of `defaultTools()`/`toolRegistry()` and out of the\n * edge-safe `src/index.ts` (same policy as `mcp.client.ts`). A host opts in explicitly:\n *\n * tools: [...defaultTools(), makeRealShellTool({ cwd: nodeDiskRoot })]\n *\n * Mirrors `tools.web.ts`: a factory with an injectable `spawn` (tests + edge never import\n * node:child_process), an options bag, abort + timeout honored, output capped. Safety beyond\n * cwd-binding is the host's to add (e.g. a PermissionPolicy `decision:'ask'` per command, or\n * an OS sandbox wrapper) — see mind/03-roadmap.md \"OS-level access — capability tiers\".\n */\n\nconst log = forComponent('shell');\n\n/** Normalize shell output for return: trim trailing newlines, mask secret values, then size-truncate.\n * Redaction runs BEFORE truncation so a masked tail can't smuggle a secret past the line cap. */\nconst clean = (s: string): string => truncateOutput(redactSecrets(s.replace(/\\n+$/, '')));\n\n/** The slice of node's `child_process.spawn` we depend on — injectable so tests supply a fake. */\nexport type SpawnFn = (\n command: string,\n args: string[],\n options: {\n cwd?: string;\n env?: Record<string, string | undefined>;\n signal?: AbortSignal;\n /** stdio layout. We force stdin to /dev/null so a child can't block on (or steal) the REPL's input. */\n stdio?: ['ignore', 'pipe', 'pipe'];\n /** Run in a new session/process group (setsid) — detaches from the controlling terminal. See DETACHED. */\n detached?: boolean;\n },\n) => SpawnedProcess;\n\n/** Minimal `ChildProcess` surface this tool uses. */\nexport interface SpawnedProcess {\n stdout?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n stderr?: { on(ev: 'data', cb: (chunk: any) => void): void } | null;\n on(ev: 'close', cb: (code: number | null) => void): void;\n on(ev: 'error', cb: (err: Error) => void): void;\n kill(signal?: string): void;\n /** Child PID — present on the real node child; used to signal the whole process group on abort. */\n pid?: number;\n}\n\n/**\n * Detach every spawned child from the REPL's controlling terminal.\n *\n * `stdio: ['ignore', 'pipe', 'pipe']` — stdin is /dev/null (clean EOF; nothing to block on).\n * `detached: true` — setsid() puts the child in its OWN session with NO controlling tty.\n *\n * Without this a child inherits the agent's tty: an interactive prompt (`sudo`, `ssh`, a git\n * credential helper) opens `/dev/tty` directly and then RACES the REPL's raw-mode input reader for\n * the user's keystrokes — a deadlock that also captures whatever the user types (e.g. a password)\n * into the agent instead of the program. Detached, those programs find no tty and fail FAST with a\n * legible error (\"sudo: a terminal is required …\") which the model can act on (tell the user to run\n * it via `!`), instead of hanging until the 120s timeout. Side benefit: the child is a process-group\n * leader, so abort/timeout can reap the whole subtree, not just /bin/sh. */\nconst DETACHED = { stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'], detached: true };\n\n/** Signal a child's WHOLE process group (`-pid`). Children are group leaders (DETACHED), so `proc.kill`\n * hits only /bin/sh and orphans descendants; this reaps the subtree. Best-effort — the group may\n * already be gone, and the fake spawn in tests has no real pid. Returns false when nothing was signaled. */\nfunction killGroup(proc: SpawnedProcess | undefined, signal: 'SIGTERM' | 'SIGKILL'): boolean {\n if (!proc?.pid) return false;\n try { process.kill(-proc.pid, signal); return true; } catch { return false; /* already exited / no such group */ }\n}\n\nexport interface RealShellOptions {\n /** Working directory the shell is bound to (typically a NodeDiskFilesystem `baseDir`). Required. */\n cwd: string;\n /** Shell binary for `-c` (e.g. the user's `$SHELL`). Default `/bin/sh`. Only honored when NOT\n * OS-sandboxed (the sandbox wrapper pins its own shell). */\n shell?: string;\n /** Override the spawner (tests inject a fake; default lazily imports node:child_process). */\n spawn?: SpawnFn;\n /** Per-command wall-clock cap (kill on overrun). Default 120s. */\n timeoutMs?: number;\n /** Ceiling for the model-supplied `timeoutMs` argument (a long build can ask for more than the\n * default, but not for an unbounded turn). Default 10min. */\n maxTimeoutMs?: number;\n /** Extra env merged over the (optionally scrubbed) base env for the child. */\n env?: Record<string, string>;\n /** Strip likely-secret vars (API keys, tokens, cloud creds) from the child's env. Default ON.\n * The FS jail does NOT contain a real process, so this is the seam that keeps `echo $ANTHROPIC_API_KEY`\n * from leaking the host's secrets to a spawned command. `false` passes `process.env` through verbatim. */\n redactEnv?: boolean;\n /** Job registry enabling `Shell({background:true})` (long-running processes). Pair with `makeShellJobTools`. */\n registry?: ShellJobRegistry;\n /** Tier-1 OS sandbox: wrap /bin/sh in sandbox-exec (macOS) / bwrap (Linux) — writes confined to\n * cwd+tmp, network blocked unless granted. `true` = defaults; commands FAIL (don't silently run\n * unsandboxed) if no wrapper exists on this platform. See src/shell.sandbox.ts. */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n /** Does a background job outlive the current run? False on one-shot hosts (the process exits and\n * `killOnExit` reaps the job), which the Shell tool's description must say rather than promise\n * cross-turn survival it won't get. Default true. */\n jobsSurviveRun?: boolean;\n}\n\n/** Resolve the (bin,args) to spawn for `command`, honoring the optional OS sandbox.\n * Throws when sandboxing was requested but this platform has no wrapper — fail closed. */\nasync function spawnArgvFor(command: string, cwd: string, osSandbox?: boolean | Partial<OsSandboxOptions>): Promise<{ bin: string; args: string[] }> {\n if (!osSandbox) return { bin: '/bin/sh', args: ['-c', command] };\n const opts = osSandbox === true ? {} : osSandbox;\n const wrapper = await findSandboxWrapper();\n const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;\n if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);\n return wrapped;\n}\n\n/** Env var names that look like secrets and are dropped before spawning (unless redactEnv:false). */\nconst SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;\n\n/** Build the child's env: `process.env` minus likely-secrets (when redacting), plus explicit `env`. */\nfunction childEnv(opts: { env?: Record<string, string>; redactEnv?: boolean }): Record<string, string | undefined> {\n const base: Record<string, string | undefined> = {};\n const redact = opts.redactEnv !== false; // default ON\n for (const [k, v] of Object.entries(process.env)) if (!(redact && SECRET_ENV_RE.test(k))) base[k] = v;\n return { ...base, ...opts.env };\n}\n\n/** Lazily resolve node's spawn (kept out of any eager edge import path). */\nlet _spawn: SpawnFn | undefined;\nasync function nodeSpawn(): Promise<SpawnFn> {\n if (!_spawn) _spawn = (await import('node:child_process')).spawn as unknown as SpawnFn;\n return _spawn;\n}\n\n// ---------------------------------------------------------------------------\n// Background jobs — long-running processes the agent starts, polls, and kills.\n// ---------------------------------------------------------------------------\nexport type JobStatus = 'running' | 'exited' | 'killed' | 'error';\n\nexport interface ShellJobConfig {\n cwd: string;\n spawn?: SpawnFn;\n env?: Record<string, string>;\n redactEnv?: boolean;\n /** Tail buffer cap per job (bytes); older output is dropped. Default 256 KB. */\n maxBuffer?: number;\n /** Kill all jobs on process exit (the CLI sets this; tests leave it off to avoid global handlers). */\n killOnExit?: boolean;\n /** Tier-1 OS sandbox for background jobs too (same semantics as RealShellOptions.osSandbox). */\n osSandbox?: boolean | Partial<OsSandboxOptions>;\n /**\n * Called once when a background job reaches a terminal state. Claude-Code parity: a background task\n * that finishes has to reach the MODEL, or `background:true` is a write-only hole — the model starts\n * a job, forgets it, and never collects the result. The host wires this to `agent.inject(...)`, which\n * folds the notice in at the next step boundary (and re-opens the turn if the model was about to stop).\n */\n onExit?: JobExitHandler;\n}\n\nexport interface JobExitNotice { id: string; command: string; status: JobStatus; exitCode?: number; tail: string }\nexport type JobExitHandler = (n: JobExitNotice) => void;\n\n/** The exact text a completion notice becomes. Shared so every host (CLI, presets, embedders) pushes the\n * same thing back to the model instead of each inventing its own wording. */\nexport function formatJobExit(n: JobExitNotice): string {\n return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ''}] \\`${n.command}\\`\\n`\n + (n.tail ? `${n.tail}\\n` : '(no output)\\n')\n + `Read the full output with ShellOutput({id:\"${n.id}\"}).`;\n}\n\ninterface Job { command: string; buf: string; status: JobStatus; exitCode?: number; proc?: SpawnedProcess; }\n\n/**\n * Per-session registry of background `/bin/sh` jobs. Backs `Shell({background:true})` and the\n * `ShellOutput`/`ShellStatus`/`ShellKill` tools. Output accumulates into a tail-capped ring so a\n * chatty process can't OOM. Bounded + killable; the CLI wires `killOnExit` so children are reaped.\n */\nexport class ShellJobRegistry {\n private jobs = new Map<string, Job>();\n private seq = 0;\n constructor(private cfg: ShellJobConfig) {\n if (cfg.killOnExit && typeof process !== 'undefined') process.once('exit', () => this.killAll());\n }\n\n async start(command: string): Promise<string> {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: '', status: 'running' };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max); // ring: keep the tail\n };\n try {\n const spawn = this.cfg.spawn ?? (await nodeSpawn());\n const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: '/bin/sh', args: ['-c', command] };\n const proc = spawn(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg), ...DETACHED });\n job.proc = proc;\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); this.notifyExit(id, job); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; this.notifyExit(id, job); } });\n } catch (e: any) {\n job.status = 'error';\n job.buf = `failed to spawn: ${e?.message ?? e}`;\n }\n this.jobs.set(id, job);\n return id;\n }\n\n /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */\n private notified = new Set<string>();\n private notifyExit(id: string, job: Job): void {\n if (this.notified.has(id) || !this.cfg.onExit) return;\n this.notified.add(id);\n try {\n this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4000) });\n } catch { /* a host notification must never take down the job registry */ }\n }\n\n /**\n * Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle\n * once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not\n * pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.\n */\n setOnExit(fn: JobExitHandler | undefined): void { this.cfg.onExit = fn; }\n\n /** Whether a finished job will actually be reported to the model. The Shell tool's description reads\n * this so it can't promise a completion notice on a host that discards it. */\n get notifiesOnExit(): boolean { return !!this.cfg.onExit; }\n\n /** Current tail output for a job (null = no such job). */\n output(id: string): string | null { return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? '' : null); }\n\n status(id: string): { status: JobStatus; exitCode?: number; bytes: number } | null {\n const j = this.jobs.get(id);\n return j ? { status: j.status, exitCode: j.exitCode, bytes: j.buf.length } : null;\n }\n\n list(): Array<{ id: string; command: string; status: JobStatus }> {\n return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));\n }\n\n /**\n * Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its\n * timeout is not necessarily a hung command — killing it throws away work that was nearly done and,\n * worse, a launcher that spawned its own detached worker leaves that worker running with nothing\n * tracking it. Adopting hands the model a handle instead: the command keeps going, its completion\n * is reported like any other job, and `seed` carries the output produced before the handoff.\n */\n adopt(command: string, proc: SpawnedProcess, seed = ''): string {\n const id = `job-${++this.seq}`;\n const max = this.cfg.maxBuffer ?? 256 * 1024;\n const job: Job = { command, buf: seed.slice(-max), status: 'running', proc };\n const append = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n job.buf = (job.buf + s).slice(-max);\n };\n proc.stdout?.on('data', append);\n proc.stderr?.on('data', append);\n proc.on('error', (err: any) => { if (job.status === 'running') { job.status = 'error'; append(`\\n[error] ${err?.message ?? err}`); this.notifyExit(id, job); } });\n proc.on('close', (code: number | null) => { if (job.status === 'running') { job.status = 'exited'; job.exitCode = code ?? undefined; this.notifyExit(id, job); } });\n this.jobs.set(id, job);\n return id;\n }\n\n kill(id: string): boolean {\n const j = this.jobs.get(id);\n if (!j) return false;\n // Group-kill: bg children are detached (own group), so SIGTERM the whole subtree — not just /bin/sh —\n // else a forked server survives the kill and the agent's exit teardown. Fall back to the pid for fakes.\n if (j.status === 'running') { if (!killGroup(j.proc, 'SIGTERM')) { try { j.proc?.kill('SIGTERM'); } catch { /* already gone */ } } j.status = 'killed'; }\n return true;\n }\n\n killAll(): void { for (const id of this.jobs.keys()) this.kill(id); }\n}\n\n/** Build an opt-in real-shell tool bound to `options.cwd`. */\nexport function makeRealShellTool(options: RealShellOptions): AgentTool {\n const defaultTimeoutMs = options.timeoutMs ?? 120_000;\n const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 600_000, defaultTimeoutMs);\n // The background promise must match what THIS host actually does — a lie here is what makes\n // `background:true` a black hole the model trusts. Evaluated lazily (the registry's onExit may be wired\n // after the tool is built, via setOnExit).\n // NOTIFICATION and SURVIVAL are independent facts governed by different things — `registry.onExit`\n // (is a completion pushed to the model?) and `jobsSurviveRun` (does the host outlive this run?). Reading\n // both off `notifiesOnExit` made the unwired preset tell the model a job \"does NOT survive the end of\n // this run\" while an identically-configured wired registry said the opposite. Each clause states what\n // actually governs it.\n const backgroundDoc = () => {\n const base = 'Set `background:true` for long-running processes (servers, watchers) and for anything that already timed out in the foreground — '\n + 'returns a job id immediately; poll with ShellOutput/ShellStatus, stop with ShellKill. ';\n const notify = options.registry?.notifiesOnExit\n ? 'Its completion is reported back to you when it finishes.'\n : 'NOTE: nothing will tell you when it finishes — poll ShellOutput/ShellStatus yourself, or you will never see its result.';\n const survival = options.jobsSurviveRun === false\n ? 'The job does NOT survive the end of this run, so do not stop while you still need its result.'\n : 'It keeps running across turns.';\n return `${base}${notify} ${survival}`;\n };\n return {\n name: 'Shell',\n // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The\n // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like\n // shraga-ee, to the parent's session), so a background job started from the child would run\n // outside the child's isolation and report into the parent. Background stays a parent capability\n // (`makeShellJobTools`' companions drop out of an isolated child entirely — see their `withCwd`).\n // Only `run` is rebuilt: everything else is carried over from THIS instance, because a host may\n // have mutated the tool AFTER construction (shraga-ee renames `Shell` -> `Bash` and its system\n // prompt says `Bash` everywhere). Rebuilding from `options` alone silently dropped that rename,\n // so a worktree child advertised `Shell` while being told to call `Bash` — an unknown-tool error\n // on its first call, and a hard failure for an agentType def whose allowlist names `Bash`.\n // `description` is the ONE thing taken from the rebound tool instead: it is derived from the\n // options and states whether `background:true` works — the rebind drops the registry, so carrying\n // the parent's text over would advertise a background capability the child does not have. (The\n // spread also collapses the getter below to a plain value, which is why this override is explicit.)\n withCwd(cwd: string): AgentTool {\n const rebound = makeRealShellTool({ ...options, cwd, registry: undefined });\n return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };\n },\n get description() {\n return 'Run a shell command via /bin/sh in the working directory. ' +\n 'Executes any installed binary — ls, cat, grep, git, bun, node, curl, scripts, etc. ' +\n 'Returns combined stdout+stderr; non-zero exits are prefixed `[exit N]`. ' +\n 'Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input ' +\n 'fail fast rather than hang — for privileged actions use a non-interactive flag (e.g. `sudo -n`), ' +\n 'or ask the user to run the command themselves. ' +\n (options.registry?.adopt\n ? `A command still running after ${defaultTimeoutMs}ms is NOT killed — it is handed to a background job and you get its id, so you can keep checking on it (\\`[still running]\\`). Pass \\`timeoutMs\\` (max ${maxTimeoutMs}) to wait longer in the foreground, and always bound network commands yourself (e.g. \\`curl -m 10\\`). `\n : `Each command is killed after ${defaultTimeoutMs}ms (result \\`[exit 124]\\` with whatever output it produced) — pass \\`timeoutMs\\` (max ${maxTimeoutMs}) for a legitimately slower command, and always bound network commands yourself (e.g. \\`curl -m 10\\`). `) +\n backgroundDoc();\n },\n // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).\n maxDurationMs: maxTimeoutMs,\n parameters: {\n type: 'object',\n required: ['command'],\n properties: {\n command: { type: 'string', description: 'the shell command line to execute' },\n background: { type: 'boolean', description: 'run detached and return a job id immediately (for servers/watchers/long builds)' },\n timeoutMs: { type: 'number', description: 'wall-clock cap for THIS command, overriding the default (clamped to the tool max)' },\n },\n },\n async run({ command, background, timeoutMs: askedTimeoutMs, timeout: aliasTimeout }, ctx) {\n const cmd = String(command ?? '');\n // `timeout` is an UNADVERTISED alias for `timeoutMs`, same unit (ms). Models reach for the\n // shorter name from muscle memory (observed: composer-2.5 sending `{\"timeout\":180000}`), and\n // an unknown key is silently dropped — the command then dies at the 120s default while the\n // model believes it asked for 180s and reads an unexplained kill. Accepted, never documented,\n // so the schema still teaches exactly one name.\n const asked = Number.isFinite(askedTimeoutMs) ? askedTimeoutMs : aliasTimeout;\n const timeoutMs = Number.isFinite(asked) && Number(asked) > 0\n ? Math.min(Number(asked), maxTimeoutMs)\n : defaultTimeoutMs;\n if (!cmd.trim()) return '[exit 1] empty command';\n if (background) {\n if (!options.registry) return 'Error: background execution is not enabled in this host (no job registry).';\n const id = await options.registry.start(cmd);\n return `Started background job ${id}. Poll output with ShellOutput({id:\"${id}\"}), check ShellStatus({id:\"${id}\"}), stop with ShellKill({id:\"${id}\"}).`;\n }\n const spawn = options.spawn ?? (await nodeSpawn());\n // Sandbox-off keeps this path await-free (after spawn resolution) so an abort racing the call\n // start still lands before listener registration, exactly as pre-sandbox semantics.\n let argv = { bin: options.shell || '/bin/sh', args: ['-c', cmd] };\n if (options.osSandbox) {\n try {\n argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);\n } catch (e: any) {\n return `[exit 1] ${e?.message ?? e}`; // fail closed — never run unsandboxed when sandboxing was asked for\n }\n }\n // Compose abort: the run's signal (ctx.signal) OR our per-command timeout both kill the child.\n const ctl = new AbortController();\n const onAbort = () => ctl.abort();\n if (ctx.signal) { if (ctx.signal.aborted) ctl.abort(); else ctx.signal.addEventListener('abort', onAbort, { once: true }); }\n let timedOut = false;\n // Set by the promise body once the child exists, so the timer can hand it over instead of killing it.\n let onDeadline: () => void = () => { timedOut = true; ctl.abort(); };\n const timer = setTimeout(() => { onDeadline(); }, timeoutMs);\n // The child is its own process-group leader (DETACHED): node's `signal` kills only /bin/sh, so on\n // abort also SIGKILL the whole group to reap any descendants (see killGroup).\n // Incremental output → ctx.emit (when the host listens), coalesced to ≥250ms / ≥1KB batches\n // so a chatty child doesn't spam hooks. Redacted per batch (the final result is re-redacted\n // whole — a secret split across batch boundaries can slip the per-batch pass, hence the cap\n // on what consumers may do with chunks: display/digest, never persistence).\n let pend = '';\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n const flushEmit = (ctx: { emit?: (s: string) => void }) => {\n if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }\n if (pend) { ctx.emit?.(redactSecrets(pend)); pend = ''; }\n };\n try {\n return await new Promise<string>((resolve) => {\n let out = '';\n let settled = false;\n const finish = (s: string) => { if (settled) return; settled = true; resolve(s); };\n let proc: SpawnedProcess;\n try {\n proc = spawn(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal, ...DETACHED });\n } catch (e: any) {\n return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);\n }\n // Reap the detached group when the CANCEL fires (node's `signal` only hits /bin/sh).\n if (ctl.signal.aborted) killGroup(proc, 'SIGKILL');\n else ctl.signal.addEventListener('abort', () => killGroup(proc, 'SIGKILL'), { once: true });\n // Deadline reached: hand the still-running child to the job registry rather than killing it.\n // Only a host with a registry can do this — elsewhere the deadline stays a kill, because a\n // child nothing tracks is worse than a dead one.\n const registry = options.registry;\n if (registry?.adopt) {\n onDeadline = () => {\n timedOut = true;\n if (settled) return;\n flushEmit(ctx);\n // The registry attaches its own collectors from here on; ours would only grow a buffer\n // nobody reads. `off` is not part of the minimal stream shape, hence the guarded call.\n (proc.stdout as any)?.off?.('data', collect);\n (proc.stderr as any)?.off?.('data', collect);\n const id = registry.adopt!(cmd, proc, out);\n finish(handoffFor(timeoutMs, id, clean(out), !!registry.notifiesOnExit));\n };\n }\n const collect = (chunk: any) => {\n const s = typeof chunk === 'string' ? chunk : chunk?.toString?.('utf8') ?? '';\n out += s;\n if (ctx.emit && !settled) {\n pend += s;\n if (pend.length >= 1024) flushEmit(ctx);\n else flushTimer ??= setTimeout(() => flushEmit(ctx), 250);\n }\n };\n proc.stdout?.on('data', collect);\n proc.stderr?.on('data', collect);\n proc.on('error', (err: any) => {\n // AbortError fires here when ctl.abort() kills the child — report timeout vs cancel.\n if (err?.name === 'AbortError' || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n log.debug('shell spawn error', err);\n finish(`[exit 1] ${err?.message ?? err}${out ? '\\n' + clean(out) : ''}`);\n });\n proc.on('close', (code: number | null) => {\n flushEmit(ctx); // drain the coalesce buffer before settling (still pre-resolve, so ctx.emit is live)\n if (ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));\n const body = clean(out);\n if (code && code !== 0) return finish(`[exit ${code}]${body ? '\\n' + body : ''}`);\n finish(body || '(command succeeded, no output)');\n });\n });\n } finally {\n clearTimeout(timer);\n if (flushTimer) clearTimeout(flushTimer); // no emits after the call settles\n ctx.signal?.removeEventListener('abort', onAbort);\n }\n },\n };\n}\n\n/** Deadline message when the child was ADOPTED rather than killed: the work continues, so say how to follow it. */\nfunction handoffFor(timeoutMs: number, id: string, body: string, notifies: boolean): string {\n const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} — NOT killed, it is still going. `\n + `Check on it with ShellOutput({id:\"${id}\"}) / ShellStatus({id:\"${id}\"}), stop it with ShellKill({id:\"${id}\"}). `\n + (notifies\n ? 'Its completion will be reported to you, so you may continue with other work meanwhile.'\n : 'Nothing will tell you when it finishes — poll it yourself before you rely on its result.');\n return body ? `${head}\\nOutput so far:\\n${body}` : head;\n}\n\n/** Abort message: timeout vs external cancel, preserving any partial output. */\nfunction reasonFor(timedOut: boolean, timeoutMs: number, body: string): string {\n // A foreground timeout is a DEAD END unless the result says how to get out of it: re-issuing the same\n // command is the one thing that cannot work, and it is exactly what a model does when the message is\n // just a code. Name the two recoveries (background, or narrow the command).\n const head = timedOut\n ? `[exit 124] timed out after ${timeoutMs}ms (killed). Re-running this command unchanged will time out again. `\n + `Either re-run it with background:true (returns a job id immediately; poll with ShellOutput/ShellStatus, `\n + `stop with ShellKill) or narrow it so it can finish — bound it, scope it, or ask for less.`\n : '[exit 130] cancelled (killed)';\n return body ? `${head}\\nPartial output before the kill:\\n${body}` : head;\n}\n\nconst NO_JOB = (id: string) => `Error: no background job '${id}'. Use ShellStatus with no id to list jobs, or start one with Shell({background:true}).`;\n\n/** Build the background-job companion tools (ShellOutput / ShellStatus / ShellKill) over a registry.\n * Each closes over THIS registry, i.e. this agent's jobs in this agent's cwd, so none of them can be\n * rebound to an isolated child's root: `withCwd` returns `undefined` and they are dropped from the\n * child's toolbelt. That keeps the capability coherent — the child's `Shell` cannot START a background\n * job (its registry is dropped), so it must not be able to inspect or SIGTERM the parent's either. */\nexport function makeShellJobTools(registry: ShellJobRegistry): AgentTool[] {\n const idParam = { type: 'object', properties: { id: { type: 'string', description: 'the job id from Shell({background:true})' } } };\n const dropOnRebind = { withCwd: () => undefined };\n return [\n {\n ...dropOnRebind,\n name: 'ShellOutput',\n description: 'Read the accumulated output (tail) of a background Shell job by id.',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n const out = registry.output(String(id));\n if (out == null) return NO_JOB(String(id));\n const st = registry.status(String(id))!;\n return `[${st.status}${st.exitCode != null ? ` exit ${st.exitCode}` : ''}]\\n${clean(out) || '(no output yet)'}`;\n },\n },\n {\n ...dropOnRebind,\n name: 'ShellStatus',\n description: 'Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.',\n parameters: idParam,\n async run({ id }) {\n if (!id) {\n const jobs = registry.list();\n return jobs.length ? jobs.map((j) => `${j.id} ${j.status} ${j.command}`).join('\\n') : '(no background jobs)';\n }\n const st = registry.status(String(id));\n return st ? `${st.status}${st.exitCode != null ? ` (exit ${st.exitCode})` : ''} · ${st.bytes} byte(s) buffered` : NO_JOB(String(id));\n },\n },\n {\n ...dropOnRebind,\n name: 'ShellKill',\n description: 'Stop a running background Shell job by id (SIGTERM).',\n parameters: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },\n async run({ id }) {\n return registry.kill(String(id)) ? `Killed job ${id}.` : NO_JOB(String(id));\n },\n },\n ];\n}\n"],"mappings":"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","names":["log","path","log","res","log","log","ctx","resolve"]}