@mmmjk/context-bridge 0.1.0

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.
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isoNowZ, uuid7Str } from "../../canonical/ids.js";
4
+ import { createSession } from "../../canonical/schema.js";
5
+ import { readJsonl } from "../../utils/jsonl.js";
6
+ import { CODEX_HOME } from "./paths.js";
7
+ import { parseApplyPatch } from "./apply-patch-parser.js";
8
+ const SKIP_TYPES = new Set(["session_meta", "turn_context"]);
9
+ const EVENT_DUPES = new Set(["user_message", "agent_message", "exec_command_end", "patch_apply_end", "web_search_end", "task_started", "task_complete", "turn_started", "turn_complete", "token_count", "thread_name_updated", "thread_rolled_back", "turn_aborted", "view_image_tool_call", "context_compacted"]);
10
+ export function ingest(jsonlPath) {
11
+ const rows = readJsonl(jsonlPath);
12
+ if (!rows.length)
13
+ throw new Error(`Empty or unreadable Codex session: ${jsonlPath}`);
14
+ const metaLine = rows.find((r) => r.type === "session_meta") ?? rows[0];
15
+ const meta = metaLine.payload ?? {};
16
+ const turn = (rows.find((r) => r.type === "turn_context")?.payload ?? {});
17
+ const cwd = String(meta.cwd ?? path.dirname(jsonlPath));
18
+ const moments = rows.flatMap((row) => translateLine(row, jsonlPath, cwd)).sort((a, b) => a.ts.localeCompare(b.ts));
19
+ return createSession({
20
+ id: uuid7Str(),
21
+ source_harness: "codex",
22
+ source_session_id: String(meta.id ?? path.basename(jsonlPath, ".jsonl")),
23
+ source_session_path: jsonlPath,
24
+ cwd,
25
+ git: {
26
+ branch: meta.git?.branch ?? null,
27
+ commit: meta.git?.commit_hash ?? null,
28
+ repo_url: meta.git?.repository_url ?? null,
29
+ },
30
+ model_hint: { provider: "openai", name: turn.model, reasoning_effort: turn.effort },
31
+ started_at: String(meta.timestamp ?? isoNowZ()),
32
+ ended_at: rows.at(-1)?.timestamp ? String(rows.at(-1)?.timestamp) : null,
33
+ permissions: { approval: turn.approval_policy, sandbox: turn.sandbox_policy?.type },
34
+ moments,
35
+ source_metadata: { codex: { originator: meta.originator, cli_version: meta.cli_version, source: meta.source, model_provider: meta.model_provider, thread_name: readCodexThreadName(meta.id ? String(meta.id) : null) } },
36
+ });
37
+ }
38
+ export function readCodexThreadName(sessionId) {
39
+ if (!sessionId)
40
+ return null;
41
+ const idx = path.join(CODEX_HOME, "session_index.jsonl");
42
+ if (!existsSync(idx))
43
+ return null;
44
+ let found = null;
45
+ for (const line of readFileSync(idx, "utf8").split(/\r?\n/)) {
46
+ if (!line.trim())
47
+ continue;
48
+ try {
49
+ const ev = JSON.parse(line);
50
+ const name = String(ev.thread_name ?? "");
51
+ if (ev.id === sessionId && name && !name.startsWith("[from "))
52
+ found = name;
53
+ }
54
+ catch { }
55
+ }
56
+ return found;
57
+ }
58
+ function translateLine(row, srcFile, cwd) {
59
+ if (SKIP_TYPES.has(String(row.type)))
60
+ return [];
61
+ const ts = String(row.timestamp ?? isoNowZ());
62
+ const payload = row.payload ?? {};
63
+ const source_ref = { file: srcFile };
64
+ if (row.type === "event_msg") {
65
+ const sub = payload.type;
66
+ if (EVENT_DUPES.has(String(sub)))
67
+ return [];
68
+ if (sub === "error")
69
+ return [{ kind: "error", ts, source_ref, message: String(payload.message ?? "") }];
70
+ return [];
71
+ }
72
+ if (row.type === "compacted")
73
+ return [{ kind: "summary_compaction", ts, source_ref, summary_text: "(codex remote compaction; encrypted_content not portable)", lossy: true, lossy_reason: "pre-compaction history not preserved across harnesses" }];
74
+ if (row.type !== "response_item")
75
+ return [];
76
+ return translateResponseItem(payload, ts, source_ref, cwd);
77
+ }
78
+ function translateResponseItem(payload, ts, source_ref, cwd) {
79
+ if (payload.type === "message")
80
+ return translateMessage(payload, ts, source_ref);
81
+ if (payload.type === "reasoning") {
82
+ const text = (payload.summary ?? []).map((s) => s.text).filter(Boolean).join("\n");
83
+ return text ? [{ kind: "thinking", ts, source_ref, text, format: "summary", lossy: true, lossy_reason: "harness-specific signature/encrypted_content not portable" }] : [];
84
+ }
85
+ if (payload.type === "function_call") {
86
+ const raw = payload.arguments;
87
+ let args = {};
88
+ try {
89
+ args = typeof raw === "string" ? JSON.parse(raw) : raw ?? {};
90
+ }
91
+ catch {
92
+ args = { _raw: raw };
93
+ }
94
+ const [tool, mapped] = codexFnToCanonical(String(payload.name ?? ""), args, cwd, payload.namespace ? String(payload.namespace) : undefined);
95
+ return [{ kind: "tool_call", ts, source_ref, tool, call_id: String(payload.call_id ?? ""), args: mapped, wire_native: { harness: "codex", name: payload.name, namespace: payload.namespace, input: args } }];
96
+ }
97
+ if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output")
98
+ return [{ kind: "tool_result", ts, source_ref, call_id: String(payload.call_id ?? ""), output_text: normalizeOutput(payload.output) }];
99
+ if (payload.type === "custom_tool_call")
100
+ return translateCustomToolCall(payload, ts, source_ref);
101
+ if (payload.type === "web_search_call")
102
+ return [{ kind: "tool_call", ts, source_ref, tool: "web_search", call_id: String(payload.call_id ?? `ws_${ts}`), args: { query: (payload.action?.query ?? "") }, wire_native: { harness: "codex", name: "web_search", input: payload } }];
103
+ return [];
104
+ }
105
+ function translateMessage(payload, ts, source_ref) {
106
+ const text = (payload.content ?? []).filter((c) => c.type === "input_text" || c.type === "output_text").map((c) => String(c.text ?? "")).join("\n");
107
+ if (payload.role === "user")
108
+ return [{ kind: "user_text", ts, source_ref, text }];
109
+ if (payload.role === "assistant")
110
+ return [{ kind: "assistant_text", ts, source_ref, text, phase: payload.phase ?? null }];
111
+ return [{ kind: "attachment", ts, source_ref, subtype: `${String(payload.role ?? "unknown")}_note`, data: { text } }];
112
+ }
113
+ function codexFnToCanonical(name, args, cwd, namespace) {
114
+ if (namespace)
115
+ return ["mcp_call", { server: namespace, tool: name, args }];
116
+ if (["exec_command", "shell", "shell_command", "local_shell"].includes(name))
117
+ return classifyShellCmd(String(args.cmd ?? args.command ?? ""), args, cwd);
118
+ if (name === "update_plan")
119
+ return ["update_plan", { items: (args.plan ?? []).map((p) => ({ title: p.step ?? "", status: p.status ?? "pending" })) }];
120
+ if (name === "view_image")
121
+ return ["view_image", { path: args.path ?? "" }];
122
+ return ["shell", { command: `echo '(codex tool ${name} args: ${JSON.stringify(args)})'` }];
123
+ }
124
+ function classifyShellCmd(cmd, args, cwd) {
125
+ const s = cmd.trim();
126
+ if (s.startsWith("rg --files"))
127
+ return ["find_files", { pattern: "*", path: "." }];
128
+ if (s.startsWith("rg "))
129
+ return ["search_text", { pattern: s.split(/\s+/)[1] ?? "", path: "." }];
130
+ if (s.startsWith("cat "))
131
+ return ["read_file", { path: s.split(/\s+/).at(-1) ?? "" }];
132
+ return ["shell", { command: cmd, workdir: args.workdir ?? cwd }];
133
+ }
134
+ function translateCustomToolCall(payload, ts, source_ref) {
135
+ if (payload.name !== "apply_patch")
136
+ return [{ kind: "tool_call", ts, source_ref, tool: "shell", call_id: String(payload.call_id ?? ""), args: { command: `echo '(custom tool ${payload.name})'` } }];
137
+ const out = [];
138
+ const ops = parseApplyPatch(String(payload.input ?? ""));
139
+ ops.forEach((op, idx) => {
140
+ const call_id = `${String(payload.call_id ?? "call_p")}__${idx}`;
141
+ if (op.kind === "add")
142
+ out.push({ kind: "tool_call", ts, source_ref, tool: "write_file", call_id, args: { path: op.path, content: (op.add_lines ?? []).join("\n") + ((op.add_lines?.length ?? 0) ? "\n" : "") } });
143
+ if (op.kind === "delete")
144
+ out.push({ kind: "tool_call", ts, source_ref, tool: "delete_file", call_id, args: { path: op.path } });
145
+ if (op.kind === "update" && op.move_to)
146
+ out.push({ kind: "tool_call", ts, source_ref, tool: "move_file", call_id, args: { from: op.path, to: op.move_to } });
147
+ else if (op.kind === "update") {
148
+ const hunks = op.hunks ?? [];
149
+ if (hunks.length > 1)
150
+ out.push({ kind: "tool_call", ts, source_ref, tool: "multi_edit_file", call_id, args: { path: op.path, edits: hunks.map((h) => ({ old: h.lines.filter((l) => l.op === "-").map((l) => l.text).join("\n"), new: h.lines.filter((l) => l.op === "+").map((l) => l.text).join("\n") })) } });
151
+ else
152
+ out.push({ kind: "tool_call", ts, source_ref, tool: "edit_file", call_id, args: { path: op.path, old: hunks[0]?.lines.filter((l) => l.op === "-").map((l) => l.text).join("\n") ?? "", new: hunks[0]?.lines.filter((l) => l.op === "+").map((l) => l.text).join("\n") ?? "" } });
153
+ }
154
+ });
155
+ return out;
156
+ }
157
+ function normalizeOutput(v) {
158
+ if (typeof v !== "string")
159
+ return JSON.stringify(v ?? "");
160
+ try {
161
+ const parsed = JSON.parse(v);
162
+ if (typeof parsed === "object" && parsed && "output" in parsed)
163
+ return String(parsed.output ?? "");
164
+ }
165
+ catch { }
166
+ return v;
167
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { existsSync, statSync, unlinkSync } from "node:fs";
4
+ import { readJsonFile, writeJsonFile } from "../../utils/jsonl.js";
5
+ export let MANIFEST_PATH = path.join(homedir(), ".cache", "context-bridge", "codex-write-manifest.json");
6
+ export function setManifestPath(p) { MANIFEST_PATH = p; }
7
+ export function recordWrite(rolloutPath, codexId) {
8
+ if (!existsSync(rolloutPath))
9
+ return;
10
+ const st = statSync(rolloutPath);
11
+ const data = readJsonFile(MANIFEST_PATH, {});
12
+ data[codexId] = { path: rolloutPath, size: st.size, mtime: st.mtimeMs / 1000 };
13
+ writeJsonFile(MANIFEST_PATH, data);
14
+ }
15
+ export function get(codexId) {
16
+ return readJsonFile(MANIFEST_PATH, {})[codexId];
17
+ }
18
+ export function clear() {
19
+ if (existsSync(MANIFEST_PATH))
20
+ unlinkSync(MANIFEST_PATH);
21
+ }
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ export let CODEX_HOME = path.join(homedir(), ".codex");
4
+ export function setCodexHome(p) { CODEX_HOME = p; }
5
+ export function codexPath(uuid, tsIso, codexHome) {
6
+ const home = codexHome ?? CODEX_HOME;
7
+ const d = new Date(tsIso);
8
+ const yyyy = String(d.getUTCFullYear()).padStart(4, "0");
9
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
10
+ const dd = String(d.getUTCDate()).padStart(2, "0");
11
+ const hh = String(d.getUTCHours()).padStart(2, "0");
12
+ const mi = String(d.getUTCMinutes()).padStart(2, "0");
13
+ const ss = String(d.getUTCSeconds()).padStart(2, "0");
14
+ return path.join(home, "sessions", yyyy, mm, dd, `rollout-${yyyy}-${mm}-${dd}T${hh}-${mi}-${ss}-${uuid}.jsonl`);
15
+ }
@@ -0,0 +1,90 @@
1
+ import { appendFileSync, existsSync, mkdirSync, utimesSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { uuid7Str } from "../../canonical/ids.js";
4
+ import { writeJsonl } from "../../utils/jsonl.js";
5
+ import { buildFileState, patchForDelete, patchForEdit, patchForMove, patchForMultiEdit, patchForWrite, toRelative } from "./apply-patch.js";
6
+ import { codexPath, CODEX_HOME } from "./paths.js";
7
+ import { recordWrite } from "./manifest.js";
8
+ export function render(session, opts = {}) {
9
+ const id = opts.session_id ?? uuid7Str();
10
+ const home = opts.target_dir ?? CODEX_HOME;
11
+ const outPath = codexPath(id, session.started_at, home);
12
+ const rows = [];
13
+ rows.push({ timestamp: session.started_at, type: "session_meta", payload: { id, timestamp: session.started_at, cwd: session.cwd, ...(opts.copy_mode ? {} : { originator: "context-bridge", source_harness: session.source_harness }), cli_version: "0.1.0", source: "cli", model_provider: opts.model_provider ?? "openai" } });
14
+ rows.push({ timestamp: bumpMs(session.started_at, 1), type: "turn_context", payload: { turn_id: uuid7Str(), cwd: session.cwd, current_date: session.started_at.slice(0, 10), timezone: opts.timezone_name ?? "Asia/Shanghai", approval_policy: session.permissions?.approval ?? "never", sandbox_policy: { type: session.permissions?.sandbox ?? "danger-full-access" }, summary: "none" } });
15
+ const snaps = buildFileState(session.moments);
16
+ for (const m of session.moments)
17
+ rows.push(...renderMoment(m, session, snaps[m.kind === "tool_call" ? m.call_id : ""]));
18
+ mkdirSync(path.dirname(outPath), { recursive: true });
19
+ writeJsonl(outPath, rows);
20
+ appendThreadName(home, id, makeThreadName(session, opts.copy_mode));
21
+ backdateMtime(outPath, session.ended_at ?? session.started_at);
22
+ recordWrite(outPath, id);
23
+ return { session_id: id, primary_path: outPath, resume_command: `codex exec resume ${id} "<your prompt>" -o /tmp/context-bridge-resume.md`, warnings: [] };
24
+ }
25
+ function renderMoment(m, session, fileState) {
26
+ if (m.kind === "user_text") {
27
+ const payload = { type: "message", role: "user", content: [{ type: "input_text", text: m.text }] };
28
+ return [{ timestamp: m.ts, type: "response_item", payload }, { timestamp: bumpMs(m.ts, 1), type: "event_msg", payload: { type: "user_message", message: m.text } }];
29
+ }
30
+ if (m.kind === "assistant_text") {
31
+ const payload = { type: "message", role: "assistant", content: [{ type: "output_text", text: m.text }], phase: m.phase ?? "final_answer" };
32
+ return [{ timestamp: m.ts, type: "response_item", payload }, { timestamp: bumpMs(m.ts, 1), type: "event_msg", payload: { type: "agent_message", message: m.text } }];
33
+ }
34
+ if (m.kind === "attachment") {
35
+ const text = m.subtype === "skill_listing" ? summarizeSkillListing(String(m.data?.content ?? "")) : JSON.stringify(m.data ?? {});
36
+ return [{ timestamp: m.ts, type: "response_item", payload: { type: "message", role: "developer", content: [{ type: "input_text", text }] } }];
37
+ }
38
+ if (m.kind === "tool_call")
39
+ return [{ timestamp: m.ts, type: "response_item", payload: renderToolCall(m, session.cwd, fileState) }];
40
+ if (m.kind === "tool_result")
41
+ return [{ timestamp: m.ts, type: "response_item", payload: { type: "function_call_output", call_id: m.call_id, output: m.output_text ?? "" } }];
42
+ return [];
43
+ }
44
+ function renderToolCall(m, cwd, fileState) {
45
+ const args = m.args ?? {};
46
+ if (m.tool === "shell")
47
+ return { type: "function_call", name: "exec_command", arguments: JSON.stringify({ cmd: args.command ?? "", workdir: cwd, yield_time_ms: 1000 }), call_id: m.call_id };
48
+ if (m.tool === "read_file")
49
+ return { type: "function_call", name: "exec_command", arguments: JSON.stringify({ cmd: `cat -n ${args.path}`, workdir: cwd, yield_time_ms: 1000 }), call_id: m.call_id };
50
+ if (["write_file", "edit_file", "multi_edit_file", "delete_file", "move_file"].includes(m.tool)) {
51
+ const p = String(args.path ?? args.from ?? "");
52
+ const rel = toRelative(p, cwd);
53
+ let input = "";
54
+ if (m.tool === "write_file")
55
+ input = patchForWrite(rel, String(args.content ?? ""), fileState?.get(p) !== undefined);
56
+ if (m.tool === "edit_file")
57
+ input = patchForEdit(rel, String(args.old ?? ""), String(args.new ?? ""), fileState?.get(p), Boolean(args.replace_all));
58
+ if (m.tool === "multi_edit_file")
59
+ input = patchForMultiEdit(rel, args.edits ?? [], fileState?.get(p));
60
+ if (m.tool === "delete_file")
61
+ input = patchForDelete(rel);
62
+ if (m.tool === "move_file")
63
+ input = patchForMove(toRelative(String(args.from ?? args.source ?? ""), cwd), toRelative(String(args.to ?? args.destination ?? ""), cwd));
64
+ return { type: "custom_tool_call", name: "apply_patch", call_id: m.call_id, input, status: "completed" };
65
+ }
66
+ return { type: "function_call", name: "exec_command", arguments: JSON.stringify({ cmd: `echo '(translated ${m.tool} call; canonical args: ${JSON.stringify(args)})'`, workdir: cwd }), call_id: m.call_id };
67
+ }
68
+ function summarizeSkillListing(content) {
69
+ const names = [...content.matchAll(/^- ([^:]+):/gm)].map((m) => `- ${m[1]}`);
70
+ return `Available skills (translated from Claude Code skill_listing attachment):\n${names.join("\n") || content.slice(0, 2000)}`;
71
+ }
72
+ function makeThreadName(session, copyMode = false) {
73
+ const custom = session.source_metadata?.claude_code?.custom_title;
74
+ const body = typeof custom === "string" && custom ? custom : session.moments.find((m) => m.kind === "user_text")?.text.slice(0, 80) ?? session.source_session_id;
75
+ return copyMode ? body : `[from ${session.source_harness}] ${body}`;
76
+ }
77
+ function appendThreadName(home, id, name) {
78
+ const p = path.join(home, "session_index.jsonl");
79
+ mkdirSync(path.dirname(p), { recursive: true });
80
+ appendFileSync(p, JSON.stringify({ id, thread_name: name, updated_at: new Date().toISOString() }) + "\n", "utf8");
81
+ }
82
+ function bumpMs(iso, ms) {
83
+ const d = new Date(iso);
84
+ return new Date(d.getTime() + ms).toISOString();
85
+ }
86
+ function backdateMtime(p, iso) {
87
+ const d = new Date(iso);
88
+ if (!Number.isNaN(d.getTime()) && existsSync(p))
89
+ utimesSync(p, d, d);
90
+ }
@@ -0,0 +1,16 @@
1
+ export function renderToolResult(callId, outputText = "", isError = false) {
2
+ return {
3
+ type: "function_call_output",
4
+ call_id: callId,
5
+ output: isError ? JSON.stringify({ output: outputText, metadata: { exit_code: 1 } }) : outputText,
6
+ };
7
+ }
8
+ export function renderShellTool(call) {
9
+ const args = call.args ?? {};
10
+ return {
11
+ type: "function_call",
12
+ name: "exec_command",
13
+ arguments: JSON.stringify({ cmd: args.command ?? "", workdir: args.workdir }),
14
+ call_id: call.call_id,
15
+ };
16
+ }
@@ -0,0 +1,44 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ export function isoNowZ() {
3
+ return new Date().toISOString();
4
+ }
5
+ export function uuid7Str() {
6
+ const ms = BigInt(Date.now());
7
+ const rand = randomBytes(10);
8
+ const randA = ((rand[0] << 4) | (rand[1] >> 4)) & 0x0fff;
9
+ let randB = BigInt(rand[1] & 0x0f);
10
+ for (const b of rand.slice(2))
11
+ randB = (randB << 8n) | BigInt(b);
12
+ return formatUuid7(ms, randA, randB);
13
+ }
14
+ export function deterministicUuid7(seed, tsIso) {
15
+ const ms = BigInt(Date.parse(tsIso || new Date().toISOString()));
16
+ const h = createHash("sha256").update(seed).digest();
17
+ const randA = ((h[0] << 4) | (h[1] >> 4)) & 0x0fff;
18
+ let randB = BigInt(h[1] & 0x0f);
19
+ for (const b of h.slice(2, 10))
20
+ randB = (randB << 8n) | BigInt(b);
21
+ return formatUuid7(ms, randA, randB);
22
+ }
23
+ export function deterministicUuid4(seed) {
24
+ const b = Buffer.from(createHash("md5").update(seed).digest());
25
+ b[6] = (b[6] & 0x0f) | 0x40;
26
+ b[8] = (b[8] & 0x3f) | 0x80;
27
+ return bytesToUuid(b);
28
+ }
29
+ export function uuid4Str() {
30
+ return randomUUID();
31
+ }
32
+ function formatUuid7(ms, randA, randB) {
33
+ const value = ((ms & 0xffffffffffffn) << 80n) |
34
+ (0x7n << 76n) |
35
+ (BigInt(randA) << 64n) |
36
+ (0x2n << 62n) |
37
+ (randB & 0x3fffffffffffffffn);
38
+ const hex = value.toString(16).padStart(32, "0");
39
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
40
+ }
41
+ function bytesToUuid(b) {
42
+ const hex = b.toString("hex");
43
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
44
+ }
@@ -0,0 +1,28 @@
1
+ export function createSession(input) {
2
+ return {
3
+ schema_version: input.schema_version ?? "1.0.0",
4
+ git: {},
5
+ model_hint: {},
6
+ permissions: { writable_roots: [], network_access: false },
7
+ skills_inventory: [],
8
+ mcp_servers: [],
9
+ memory_files: [],
10
+ subagent_transcripts: {},
11
+ artifacts: [],
12
+ source_metadata: {},
13
+ ...input,
14
+ moments: input.moments ?? [],
15
+ };
16
+ }
17
+ export function isUserText(m) {
18
+ return m.kind === "user_text";
19
+ }
20
+ export function isAssistantText(m) {
21
+ return m.kind === "assistant_text";
22
+ }
23
+ export function isToolCall(m) {
24
+ return m.kind === "tool_call";
25
+ }
26
+ export function isToolResult(m) {
27
+ return m.kind === "tool_result";
28
+ }
@@ -0,0 +1,21 @@
1
+ export const CANONICAL_TOOLS = {
2
+ shell: { description: "Run a shell command" },
3
+ read_file: { description: "Read a file" },
4
+ write_file: { description: "Write a file" },
5
+ edit_file: { description: "Edit a file" },
6
+ multi_edit_file: { description: "Apply multiple edits to a file" },
7
+ delete_file: { description: "Delete a file" },
8
+ move_file: { description: "Move or rename a file" },
9
+ find_files: { description: "Find files by pattern" },
10
+ search_text: { description: "Search text in files" },
11
+ web_search: { description: "Search the web" },
12
+ web_fetch: { description: "Fetch a web page" },
13
+ update_plan: { description: "Update the task plan" },
14
+ ask_user: { description: "Ask the user for input" },
15
+ subagent_dispatch: { description: "Dispatch a subagent" },
16
+ mcp_call: { description: "Call an MCP tool" },
17
+ view_image: { description: "View a local image" },
18
+ };
19
+ export function isCanonicalTool(name) {
20
+ return Object.prototype.hasOwnProperty.call(CANONICAL_TOOLS, name);
21
+ }