@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,442 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, realpathSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ import { copySession, translate } from "./translator.js";
7
+ import { isAgentBridgeCc, isAgentBridgeCodex, syncOnce } from "./sync.js";
8
+ import { serve } from "./mcp-server.js";
9
+ import { cleanGenerated, dedupeGenerated, displayPrompt, findSession, listSessions, sourceDisplay } from "./session-index.js";
10
+ import { syncTitles } from "./title-sync.js";
11
+ const COMMANDS = ["translate", "copy", "export", "import", "inspect", "list", "smoke", "sync", "watch", "clean", "dedupe", "install-hook", "mcp"];
12
+ const HOOK_MARKERS = ["context_bridge.cli sync"];
13
+ const HELP_TEXT = `context-bridge: cross-agent session translator
14
+
15
+ Usage:
16
+ context-bridge <command> [options]
17
+ node dist/src/cli.js <command> [options]
18
+
19
+ Commands:
20
+ translate Translate one session JSONL file between harnesses.
21
+ Usage: translate [--from <claude-code|codex>] [--to <claude-code|codex>] [--target-dir <dir>] [--allow-generated] <session-id|session.jsonl>
22
+ Example: translate <session-id>
23
+
24
+ copy One-shot copy into the opposite harness as a new independent session.
25
+ Usage: copy [--from <claude-code|codex>] [--to <claude-code|codex>] [--target-dir <dir>] <session-id|session.jsonl>
26
+ Example: copy <session-id>
27
+
28
+ export Alias for translate. Use when exporting from the source harness.
29
+ Usage: export [--from <claude-code|codex>] [--to <claude-code|codex>] <session.jsonl>
30
+
31
+ import Alias for translate. Use when importing into the target harness.
32
+ Usage: import [--from <claude-code|codex>] [--to <claude-code|codex>] <session.jsonl>
33
+
34
+ inspect Print one indexed session summary as JSON.
35
+ Usage: inspect <session-id>
36
+
37
+ list List recent sessions in a table.
38
+ Usage: list [--harness <claude-code|codex|both>] [--source <all|native|claude|codex>] [--days <n>] [-n <n>|--limit <n>]
39
+ Example: list --harness codex --days 7 -n 10
40
+
41
+ smoke Translate one session and print the resume command without live model execution.
42
+ Usage: smoke [--from <claude-code|codex>] [--to <claude-code|codex>] [--target-dir <dir>] [--prompt <text>] [--allow-generated] <session-id|session.jsonl>
43
+ Example: smoke <session-id>
44
+
45
+ sync Batch-sync recent sessions.
46
+ Usage: sync [--direction <both|cc-to-codex|codex-to-cc>] [--days <n>] [--force] [--include-active]
47
+ Example: sync --direction both --days 365
48
+
49
+ watch Re-run sync on an interval, or once through the watch path.
50
+ Usage: watch [--once] [--direction <both|cc-to-codex|codex-to-cc>] [--days <n>] [-i <sec>|--interval <sec>] [--force] [--include-active]
51
+ Example: watch --direction both --days 1 -i 30
52
+
53
+ clean Remove generated translated sessions. Use --dry-run first to preview.
54
+ Usage: clean [--dry-run]
55
+
56
+ dedupe Remove duplicate generated translated sessions. Use --dry-run first to preview.
57
+ Usage: dedupe [--dry-run]
58
+
59
+ install-hook
60
+ Install automatic sync hooks for Claude Code or Codex.
61
+ Usage: install-hook [--target <claude-code|codex>] [--direction <cc-to-codex|codex-to-cc|both>]
62
+ Example: install-hook --target codex
63
+
64
+ mcp Run the stdio MCP server or print an MCP host config snippet.
65
+ Usage: mcp serve
66
+ Usage: mcp config-snippet
67
+
68
+ Common harnesses:
69
+ claude-code, codex
70
+
71
+ Generated files:
72
+ Claude Code: ~/.claude/projects/.../*.jsonl
73
+ Codex: ~/.codex/sessions/.../*.jsonl`;
74
+ export function main(argv = process.argv.slice(2)) {
75
+ const cmd = argv[0];
76
+ if (!cmd || cmd === "--help" || cmd === "-h") {
77
+ console.log(HELP_TEXT);
78
+ return 0;
79
+ }
80
+ try {
81
+ if (cmd === "translate" || cmd === "export" || cmd === "import")
82
+ return translateCmd(argv.slice(1));
83
+ if (cmd === "copy")
84
+ return copyCmd(argv.slice(1));
85
+ if (cmd === "list")
86
+ return listCmd(argv.slice(1));
87
+ if (cmd === "inspect")
88
+ return inspectCmd(argv.slice(1));
89
+ if (cmd === "smoke")
90
+ return smokeCmd(argv.slice(1));
91
+ if (cmd === "sync")
92
+ return syncCmd(argv.slice(1));
93
+ if (cmd === "watch")
94
+ return watchCmd(argv.slice(1));
95
+ if (cmd === "clean")
96
+ return cleanCmd(argv.slice(1));
97
+ if (cmd === "dedupe")
98
+ return dedupeCmd(argv.slice(1));
99
+ if (cmd === "install-hook")
100
+ return installHookCmd(argv.slice(1));
101
+ if (cmd === "mcp" && argv[1] === "serve") {
102
+ serve();
103
+ return 0;
104
+ }
105
+ if (cmd === "mcp" && argv[1] === "config-snippet") {
106
+ console.log(JSON.stringify({ mcpServers: { "context-bridge": { command: "context-bridge", args: ["mcp", "serve"] } } }, null, 2));
107
+ return 0;
108
+ }
109
+ console.error(`unknown command: ${cmd}`);
110
+ return 2;
111
+ }
112
+ catch (e) {
113
+ console.error(e instanceof Error ? e.message : String(e));
114
+ return 1;
115
+ }
116
+ }
117
+ function option(args, name, fallback) {
118
+ const i = args.indexOf(name);
119
+ return i >= 0 ? args[i + 1] : fallback;
120
+ }
121
+ function has(args, name) {
122
+ return args.includes(name);
123
+ }
124
+ function positional(args) {
125
+ const optionsWithValue = new Set(["--from", "--to", "--target-dir", "--harness", "--source", "--days", "--limit", "-n", "-i", "--interval", "--direction", "--prompt", "--target"]);
126
+ for (let i = 0; i < args.length; i++) {
127
+ const arg = args[i];
128
+ if (optionsWithValue.has(arg)) {
129
+ i++;
130
+ continue;
131
+ }
132
+ if (!arg.startsWith("-"))
133
+ return arg;
134
+ }
135
+ return undefined;
136
+ }
137
+ function numberOption(args, name, fallback) {
138
+ const value = option(args, name);
139
+ return value == null ? fallback : Number(value);
140
+ }
141
+ function translateCmd(args) {
142
+ const input = positional(args);
143
+ if (!input)
144
+ throw new Error("session id or source path required");
145
+ const resolved = resolveSessionInput(input);
146
+ assertNotGeneratedSource(resolved.path, args);
147
+ const from = option(args, "--from") ?? resolved.harness ?? inferHarnessFromSessionPath(resolved.path);
148
+ const to = option(args, "--to", from === "codex" ? "claude-code" : "codex");
149
+ const res = translate({
150
+ source_path: resolved.path,
151
+ source_harness: from,
152
+ target_harness: to,
153
+ target_dir: option(args, "--target-dir"),
154
+ });
155
+ console.log(`session_id: ${res.session_id}`);
156
+ console.log(`output: ${res.primary_path}`);
157
+ console.log("\nResume command:");
158
+ console.log(` ${res.resume_command}`);
159
+ return 0;
160
+ }
161
+ function copyCmd(args) {
162
+ const input = positional(args);
163
+ if (!input)
164
+ throw new Error("session id or source path required");
165
+ const resolved = resolveSessionInput(input);
166
+ const from = option(args, "--from") ?? resolved.harness ?? inferHarnessFromSessionPath(resolved.path);
167
+ const to = option(args, "--to", from === "codex" ? "claude-code" : "codex");
168
+ const res = copySession({
169
+ source_path: resolved.path,
170
+ source_harness: from,
171
+ target_harness: to,
172
+ target_dir: option(args, "--target-dir"),
173
+ });
174
+ console.log(`session_id: ${res.session_id}`);
175
+ console.log(`output: ${res.primary_path}`);
176
+ console.log("\nResume command:");
177
+ console.log(` ${res.resume_command}`);
178
+ return 0;
179
+ }
180
+ function listCmd(args) {
181
+ const rows = listSessions({
182
+ harness: option(args, "--harness", option(args, "--from", "claude-code")),
183
+ source: option(args, "--source", "all"),
184
+ days: numberOption(args, "--days", 365),
185
+ limit: numberOption(args, "-n", numberOption(args, "--limit", 20)),
186
+ include_translated: has(args, "--include-translated"),
187
+ });
188
+ console.log(formatSessionTable(rows));
189
+ return 0;
190
+ }
191
+ function formatSessionTable(rows) {
192
+ if (!rows.length)
193
+ return "no sessions found";
194
+ const tableRows = rows.map((s) => ({
195
+ modified: formatLocalTimestamp(s.mtime_iso),
196
+ harness: s.harness,
197
+ session: shortenMiddle(s.session_id, 36),
198
+ size: formatBytes(s.size_bytes),
199
+ source: sourceDisplay(s.source_harness),
200
+ cwd: shortenMiddle(s.cwd, 72),
201
+ prompt: truncate((displayPrompt(s) ?? "").replace(/\s+/g, " ").trim(), 120),
202
+ }));
203
+ const columns = [
204
+ { key: "modified", label: "Modified" },
205
+ { key: "harness", label: "Harness" },
206
+ { key: "session", label: "Session" },
207
+ { key: "size", label: "Size", align: "right" },
208
+ { key: "source", label: "Source" },
209
+ { key: "cwd", label: "CWD" },
210
+ { key: "prompt", label: "Display Prompt" },
211
+ ];
212
+ const widths = columns.map((col) => Math.max(col.label.length, ...tableRows.map((row) => row[col.key].length)));
213
+ const header = columns.map((col, i) => pad(col.label, widths[i], col.align)).join(" ");
214
+ const divider = widths.map((w) => "-".repeat(w)).join(" ");
215
+ const body = tableRows.map((row) => columns.map((col, i) => pad(row[col.key], widths[i], col.align)).join(" "));
216
+ return [header, divider, ...body].join("\n");
217
+ }
218
+ function formatBytes(bytes) {
219
+ if (bytes < 1024)
220
+ return `${bytes} B`;
221
+ if (bytes < 1024 * 1024)
222
+ return `${(bytes / 1024).toFixed(1)} KB`;
223
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
224
+ }
225
+ function formatLocalTimestamp(value) {
226
+ const date = new Date(value);
227
+ if (Number.isNaN(date.getTime()))
228
+ return value;
229
+ const y = date.getFullYear();
230
+ const m = String(date.getMonth() + 1).padStart(2, "0");
231
+ const d = String(date.getDate()).padStart(2, "0");
232
+ const hh = String(date.getHours()).padStart(2, "0");
233
+ const mm = String(date.getMinutes()).padStart(2, "0");
234
+ const ss = String(date.getSeconds()).padStart(2, "0");
235
+ return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
236
+ }
237
+ function pad(value, width, align) {
238
+ return align === "right" ? value.padStart(width) : value.padEnd(width);
239
+ }
240
+ function truncate(value, max) {
241
+ if (value.length <= max)
242
+ return value;
243
+ return `${value.slice(0, Math.max(0, max - 1))}…`;
244
+ }
245
+ function shortenMiddle(value, max) {
246
+ if (value.length <= max)
247
+ return value;
248
+ if (max <= 1)
249
+ return "…";
250
+ const left = Math.ceil((max - 1) / 2);
251
+ const right = Math.floor((max - 1) / 2);
252
+ return `${value.slice(0, left)}…${value.slice(value.length - right)}`;
253
+ }
254
+ function inspectCmd(args) {
255
+ const id = positional(args);
256
+ if (!id)
257
+ throw new Error("session id required");
258
+ const found = findSession(id);
259
+ if (!found)
260
+ throw new Error(`session not found: ${id}`);
261
+ console.log(JSON.stringify(found, null, 2));
262
+ return 0;
263
+ }
264
+ function smokeCmd(args) {
265
+ const input = positional(args);
266
+ if (!input)
267
+ throw new Error("session id or source path required");
268
+ const resolved = resolveSessionInput(input);
269
+ assertNotGeneratedSource(resolved.path, args);
270
+ const from = option(args, "--from") ?? resolved.harness ?? inferHarnessFromSessionPath(resolved.path);
271
+ const to = option(args, "--to", from === "codex" ? "claude-code" : "codex");
272
+ const res = translate({ source_path: resolved.path, source_harness: from, target_harness: to, target_dir: option(args, "--target-dir") });
273
+ console.log(`translated: ${res.primary_path}`);
274
+ console.log(`resume: ${res.resume_command}`);
275
+ const prompt = option(args, "--prompt");
276
+ if (prompt)
277
+ console.log("live resume execution is intentionally not run by smoke in this build; run the printed resume command with the prompt manually.");
278
+ return 0;
279
+ }
280
+ function assertNotGeneratedSource(sourcePath, args) {
281
+ if (has(args, "--allow-generated"))
282
+ return;
283
+ const harness = inferHarnessFromSessionPath(sourcePath);
284
+ const generated = harness === "codex" ? isAgentBridgeCodex(sourcePath) : isAgentBridgeCc(sourcePath);
285
+ if (generated)
286
+ throw new Error("refusing to translate a generated context-bridge session; pass --allow-generated to override");
287
+ }
288
+ function resolveSessionInput(input) {
289
+ const found = findSession(input);
290
+ if (found)
291
+ return { path: found.path, harness: found.harness };
292
+ if (existsSync(input))
293
+ return { path: input };
294
+ throw new Error(`session not found: ${input}`);
295
+ }
296
+ function inferHarnessFromSessionPath(file) {
297
+ const normalized = path.normalize(file);
298
+ if (normalized.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`) || path.basename(file).startsWith("rollout-"))
299
+ return "codex";
300
+ if (normalized.includes(`${path.sep}.claude${path.sep}projects${path.sep}`))
301
+ return "claude-code";
302
+ try {
303
+ for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
304
+ if (!line.trim())
305
+ continue;
306
+ const row = JSON.parse(line);
307
+ if (row.type === "session_meta" || row.type === "turn_context" || row.type === "response_item")
308
+ return "codex";
309
+ if (row.sessionId || row.type === "user" || row.type === "assistant" || row.type === "custom-title")
310
+ return "claude-code";
311
+ }
312
+ }
313
+ catch { }
314
+ return "claude-code";
315
+ }
316
+ function syncCmd(args) {
317
+ const titles = syncTitles();
318
+ const stats = syncOnce({
319
+ direction: option(args, "--direction", "both"),
320
+ days: Number(option(args, "--days", "365")),
321
+ force: has(args, "--force"),
322
+ include_active: has(args, "--include-active"),
323
+ });
324
+ console.log(`summary: +${stats.translated} translated, ${stats.skipped_existing} unchanged, ${stats.skipped_conflict} conflicts (skipped), ${stats.skipped_active} active (skipped), ${stats.skipped_too_big} too-big (skipped), ${stats.skipped_empty} empty (skipped), ${stats.failed} failed, titles ${titles.updated_claude_code + titles.updated_codex} updated`);
325
+ return stats.failed ? 1 : 0;
326
+ }
327
+ function watchCmd(args) {
328
+ const once = has(args, "--once");
329
+ const intervalSec = numberOption(args, "-i", numberOption(args, "--interval", 30));
330
+ const run = () => {
331
+ const stats = syncOnce({ direction: option(args, "--direction", "both"), days: numberOption(args, "--days", 365), force: has(args, "--force"), include_active: has(args, "--include-active") });
332
+ console.log(`${new Date().toISOString()} summary: +${stats.translated} translated, ${stats.skipped_existing} unchanged, ${stats.skipped_conflict} conflicts (skipped), ${stats.failed} failed`);
333
+ return stats.failed ? 1 : 0;
334
+ };
335
+ const code = run();
336
+ if (once)
337
+ return code;
338
+ setInterval(run, Math.max(1, intervalSec) * 1000);
339
+ return 0;
340
+ }
341
+ function cleanCmd(args) {
342
+ const dryRun = has(args, "--dry-run");
343
+ const result = cleanGenerated({ dry_run: dryRun });
344
+ for (const p of result.removed)
345
+ console.log(`${dryRun ? "would remove" : "removed"} ${p}`);
346
+ for (const p of result.skipped)
347
+ console.log(`skipped ${p}`);
348
+ console.log(`summary: ${result.removed.length} ${dryRun ? "matched" : "removed"}, ${result.skipped.length} skipped`);
349
+ return result.skipped.length ? 1 : 0;
350
+ }
351
+ function dedupeCmd(args) {
352
+ const dryRun = has(args, "--dry-run");
353
+ const result = dedupeGenerated({ dry_run: dryRun });
354
+ for (const p of result.removed)
355
+ console.log(`${dryRun ? "would remove duplicate" : "removed duplicate"} ${p}`);
356
+ for (const p of result.skipped)
357
+ console.log(`skipped ${p}`);
358
+ console.log(`summary: ${result.removed.length} duplicates ${dryRun ? "matched" : "removed"}, ${result.skipped.length} skipped`);
359
+ return result.skipped.length ? 1 : 0;
360
+ }
361
+ function installHookCmd(args) {
362
+ const target = option(args, "--target", "claude-code");
363
+ const direction = option(args, "--direction", target === "claude-code" ? "cc-to-codex" : "codex-to-cc");
364
+ let cmdStr = `node ${path.resolve(fileURLToPath(import.meta.url))} sync --direction ${direction} --days 1`;
365
+ cmdStr += " # context_bridge.cli sync";
366
+ if (target === "claude-code" && ["cc-to-codex", "both"].includes(direction))
367
+ cmdStr += " --include-active";
368
+ cmdStr += " >/dev/null 2>&1";
369
+ if (target === "claude-code")
370
+ return installCcHook(cmdStr);
371
+ if (target === "codex")
372
+ return installCodexNotify(cmdStr);
373
+ throw new Error(`unknown target: ${target}`);
374
+ }
375
+ function installCcHook(cmdStr) {
376
+ const settingsPath = path.join(homedir(), ".claude", "settings.json");
377
+ mkdirSync(path.dirname(settingsPath), { recursive: true });
378
+ const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, "utf8") || "{}") : {};
379
+ settings.hooks ??= {};
380
+ settings.hooks.Stop ??= [];
381
+ let group = settings.hooks.Stop.find((g) => g.matcher === "*" || g.matcher == null);
382
+ if (!group) {
383
+ group = { matcher: "*", hooks: [] };
384
+ settings.hooks.Stop.push(group);
385
+ }
386
+ group.hooks ??= [];
387
+ const existing = group.hooks.find((h) => typeof h.command === "string" && HOOK_MARKERS.some((marker) => String(h.command).includes(marker)));
388
+ if (existing) {
389
+ if (existing.command === cmdStr) {
390
+ console.log("CC Stop hook already installed; nothing to do.");
391
+ return 0;
392
+ }
393
+ existing.command = cmdStr;
394
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
395
+ console.log(`updated Stop hook in ${settingsPath}`);
396
+ return 0;
397
+ }
398
+ group.hooks.push({ type: "command", command: cmdStr });
399
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
400
+ console.log(`added Stop hook to ${settingsPath}`);
401
+ return 0;
402
+ }
403
+ function installCodexNotify(cmdStr) {
404
+ const configPath = path.join(homedir(), ".codex", "config.toml");
405
+ if (!existsSync(configPath))
406
+ throw new Error(`${configPath} does not exist`);
407
+ const raw = readFileSync(configPath, "utf8");
408
+ const existingMatch = raw.match(/^notify\s*=\s*(.*)$/m);
409
+ let inner = cmdStr;
410
+ if (existingMatch) {
411
+ const current = existingMatch[1];
412
+ if (HOOK_MARKERS.some((marker) => current.includes(marker))) {
413
+ console.log("Codex notify hook already chains our sync; nothing to do.");
414
+ return 0;
415
+ }
416
+ const shMatch = current.match(/^\["sh",\s*"-c",\s*"([\s\S]*)"\]$/);
417
+ inner = `${shMatch ? JSON.parse(`"${shMatch[1]}"`) : current}; ${cmdStr}`;
418
+ }
419
+ const line = `notify = ${JSON.stringify(["sh", "-c", inner])}`;
420
+ const next = existingMatch ? raw.replace(/^notify\s*=.*$/m, line) : `${line}\n${raw}`;
421
+ copyFileSync(configPath, `${configPath}.bak`);
422
+ writeFileSync(configPath, next, "utf8");
423
+ console.log(`updated ${configPath}`);
424
+ return 0;
425
+ }
426
+ function isDirectCliExecution() {
427
+ const entry = process.argv[1];
428
+ if (!entry)
429
+ return false;
430
+ try {
431
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
432
+ }
433
+ catch {
434
+ return path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url));
435
+ }
436
+ }
437
+ if (isDirectCliExecution()) {
438
+ const code = main();
439
+ if (!(process.argv[2] === "mcp" && process.argv[3] === "serve")) {
440
+ process.exit(code);
441
+ }
442
+ }
@@ -0,0 +1,75 @@
1
+ import { stdin, stdout } from "node:process";
2
+ import { syncOnce } from "./sync.js";
3
+ import { translate } from "./translator.js";
4
+ import { findSession, listSessions, resumeCommand } from "./session-index.js";
5
+ import { syncTitles } from "./title-sync.js";
6
+ const tools = [
7
+ "list_sessions",
8
+ "translate_session",
9
+ "sync_now",
10
+ "find_session",
11
+ "prepare_resume",
12
+ "resume_with_prompt",
13
+ ];
14
+ export function serve() {
15
+ let buffer = "";
16
+ stdin.setEncoding("utf8");
17
+ stdin.on("data", (chunk) => {
18
+ buffer += chunk;
19
+ let idx;
20
+ while ((idx = buffer.indexOf("\n")) >= 0) {
21
+ const line = buffer.slice(0, idx).trim();
22
+ buffer = buffer.slice(idx + 1);
23
+ if (line)
24
+ handleLine(line);
25
+ }
26
+ });
27
+ }
28
+ function handleLine(line) {
29
+ let req;
30
+ try {
31
+ req = JSON.parse(line);
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ const id = req.id;
37
+ const method = String(req.method ?? "");
38
+ if (method === "initialize")
39
+ return respond(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "context-bridge", version: "0.6.0" } });
40
+ if (method === "tools/list")
41
+ return respond(id, { tools: tools.map((name) => ({ name, description: `${name} tool`, inputSchema: { type: "object" } })) });
42
+ if (method === "tools/call") {
43
+ const params = req.params;
44
+ const name = String(params.name);
45
+ const args = params.arguments ?? {};
46
+ let result;
47
+ if (name === "sync_now")
48
+ result = { title_sync: syncTitles(), ...syncOnce({ direction: String(args.direction ?? "both"), days: Number(args.days ?? 365), max_bytes: Number(args.max_bytes ?? 100 * 1024 * 1024) }) };
49
+ else if (name === "list_sessions") {
50
+ const sessions = listSessions({ harness: String(args.harness ?? "claude-code"), source: String(args.source ?? "all"), days: Number(args.days ?? 365), limit: Number(args.limit ?? 20), include_translated: Boolean(args.include_translated) });
51
+ result = { harness: args.harness ?? "claude-code", count: sessions.length, sessions };
52
+ }
53
+ else if (name === "find_session") {
54
+ result = findSession(String(args.session_id ?? ""));
55
+ }
56
+ else if (name === "translate_session") {
57
+ const out = translate({ source_path: String(args.source_path ?? ""), source_harness: String(args.from_harness ?? args.from ?? "claude-code"), target_harness: String(args.to_harness ?? args.to ?? "codex") });
58
+ result = { target_session_id: out.session_id, target_path: out.primary_path, resume_command: out.resume_command, warnings: out.warnings };
59
+ }
60
+ else if (name === "prepare_resume") {
61
+ const found = findSession(String(args.session_id ?? ""));
62
+ result = found ? { target_session_id: found.session_id, target_path: found.path, harness: found.harness, source_harness: found.source_harness, resume_command: found.resume_command, start_command: found.start_command, launch_context: found.launch_context, cwd: found.cwd, translated: found.translated } : null;
63
+ }
64
+ else if (name === "resume_with_prompt") {
65
+ const found = findSession(String(args.session_id ?? ""));
66
+ result = found ? { output: "", exit_code: null, target_session_id: found.session_id, command: `${resumeCommand(found.harness, found.session_id, found.cwd)} ${JSON.stringify(String(args.prompt ?? ""))}`, translated: found.translated } : null;
67
+ }
68
+ else
69
+ result = { ok: false, error: `unknown tool: ${name}` };
70
+ return respond(id, { content: [{ type: "text", text: JSON.stringify(result) }] });
71
+ }
72
+ }
73
+ function respond(id, result) {
74
+ stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
75
+ }
@@ -0,0 +1,20 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { existsSync, unlinkSync } from "node:fs";
4
+ import { readJsonFile, writeJsonFile } from "./utils/jsonl.js";
5
+ export let PAIR_MAP_PATH = path.join(homedir(), ".cache", "context-bridge", "pair-map.json");
6
+ export function setPairMapPath(p) { PAIR_MAP_PATH = p; }
7
+ export function read() {
8
+ const data = readJsonFile(PAIR_MAP_PATH, {});
9
+ return { cc_to_codex: data.cc_to_codex ?? {}, codex_to_cc: data.codex_to_cc ?? {} };
10
+ }
11
+ export function record(pair) {
12
+ const data = read();
13
+ data.cc_to_codex[pair.cc_id] = pair.codex_id;
14
+ data.codex_to_cc[pair.codex_id] = pair.cc_id;
15
+ writeJsonFile(PAIR_MAP_PATH, data);
16
+ }
17
+ export function clear() {
18
+ if (existsSync(PAIR_MAP_PATH))
19
+ unlinkSync(PAIR_MAP_PATH);
20
+ }