@minhspark/codex-mcp-bridge 1.17.0 → 1.18.1
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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -0
- package/package.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/scripts/npm-footer.ps1 +1 -1
- package/scripts/npm-footer.sh +1 -1
- package/src/claude-bridge.mjs +52 -20
- package/src/claude-desktop-context.mjs +18 -17
- package/src/index.mjs +7 -5
- package/src/native-relay-companion.mjs +1 -1
- package/src/prompt-guidance.mjs +30 -0
- package/src/vscode-bridge.mjs +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.18.1] - 2026-09-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Report npm installation completion with an unchanged version instead of claiming the package is already up to date after a same-version reinstall.
|
|
12
|
+
- Discover Claude Desktop sessions when accumulated task configuration exceeds 32 MiB. Retain only bounded identity fields and SHA-256 fingerprints while preserving account isolation, duplicate detection, and concurrent-change checks.
|
|
13
|
+
- Report Desktop task verification failures separately from missing peer endpoints in session discovery and bridge status.
|
|
14
|
+
|
|
15
|
+
## [1.18.0] - 2026-09-23
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Guide agents to write every cross-agent prompt in English with a fixed section order: Goal, Context, Task, Scope, Constraints, Done when, Reply format. The Codex, Claude and VS Code bridges share one definition in their server instructions, sending-tool descriptions and prompt fields, and text supplied by the user is still sent unchanged.
|
|
20
|
+
|
|
7
21
|
## [1.17.0] - 2026-09-21
|
|
8
22
|
|
|
9
23
|
### Added
|
package/README.md
CHANGED
|
@@ -231,6 +231,22 @@ Example requests:
|
|
|
231
231
|
|
|
232
232
|
Use the exact project directory and destination task. If several Claude sessions match, specify the task ID. A `reply_received` receipt confirms a reply; a timeout does not mean the task stopped, so inspect it before retrying.
|
|
233
233
|
|
|
234
|
+
The sending tools ask agents to write each prompt in English with these sections, dropping any that do not apply: `Goal`, `Context`, `Task`, `Scope`, `Constraints`, `Done when`, `Reply format`. The first line names the sender, the project and the purpose. Text the user supplied is sent unchanged.
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
[From Claude Code · my-app · edit coordination]
|
|
238
|
+
|
|
239
|
+
## Goal
|
|
240
|
+
Avoid conflicting edits while Claude Code patches `src/export.ps1`.
|
|
241
|
+
|
|
242
|
+
## Task
|
|
243
|
+
1. Do not modify `src/export.ps1` or `README.txt` until told otherwise.
|
|
244
|
+
2. Write any unsaved edits to those files to disk now.
|
|
245
|
+
|
|
246
|
+
## Reply format
|
|
247
|
+
Exactly one line: `DONE — changed: <files>` or `DONE — no changes`.
|
|
248
|
+
```
|
|
249
|
+
|
|
234
250
|
## Important behavior
|
|
235
251
|
|
|
236
252
|
- Desktop mode uses the native relay and the apps' permissions; it does not fall back to an external app-server.
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs
|
|
|
22
22
|
|
|
23
23
|
exitForVersionRequest(import.meta.url);
|
|
24
24
|
|
|
25
|
-
const VERSION = "1.
|
|
25
|
+
const VERSION = "1.18.1";
|
|
26
26
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
27
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
28
28
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/scripts/npm-footer.ps1
CHANGED
|
@@ -130,7 +130,7 @@ try {
|
|
|
130
130
|
} elseif ($before.Status -eq 'missing') {
|
|
131
131
|
Write-Host "Successfully installed: @minhspark/codex-mcp-bridge v$($after.Version)"
|
|
132
132
|
} elseif ($before.Version -ceq $after.Version) {
|
|
133
|
-
Write-Host "
|
|
133
|
+
Write-Host "Installation completed: @minhspark/codex-mcp-bridge v$($after.Version) (version unchanged)"
|
|
134
134
|
} else {
|
|
135
135
|
Write-Host "Successfully updated: @minhspark/codex-mcp-bridge v$($before.Version) -> v$($after.Version)"
|
|
136
136
|
}
|
package/scripts/npm-footer.sh
CHANGED
|
@@ -70,7 +70,7 @@ npm() {
|
|
|
70
70
|
if [ -z "$bridge_after" ] || [ "$bridge_before_verified" -ne 1 ] || [ "$bridge_after_root" != "$bridge_root" ]; then
|
|
71
71
|
printf 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.\n' >&2
|
|
72
72
|
elif [ "$bridge_after_root" = "$bridge_root" ] && [ "$bridge_after" = "$bridge_before" ]; then
|
|
73
|
-
printf '
|
|
73
|
+
printf 'Installation completed: @minhspark/codex-mcp-bridge v%s (version unchanged)\n' "$bridge_after"
|
|
74
74
|
elif [ "$bridge_after_root" = "$bridge_root" ] && [ -n "$bridge_before" ]; then
|
|
75
75
|
printf 'Successfully updated: @minhspark/codex-mcp-bridge v%s -> v%s\n' "$bridge_before" "$bridge_after"
|
|
76
76
|
else
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -22,10 +22,11 @@ import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
|
22
22
|
import { assertAccountIdentity, bindUnsolicitedClaudeMessageAccount, publicAccountState, readBridgeAccounts, requireBridgeAccounts, sameAccountIdentity } from "./bridge-account-context.mjs";
|
|
23
23
|
import { resolveClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
|
|
24
24
|
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
25
|
+
import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
|
|
25
26
|
|
|
26
27
|
exitForVersionRequest(import.meta.url);
|
|
27
28
|
|
|
28
|
-
const VERSION = "1.
|
|
29
|
+
const VERSION = "1.18.1";
|
|
29
30
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
30
31
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
31
32
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
@@ -163,6 +164,40 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
|
|
|
163
164
|
? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
function inspectDiscovery(sessions, account, expectedCwd) {
|
|
168
|
+
const eligible = [];
|
|
169
|
+
const excluded = [];
|
|
170
|
+
let nonDesktopCount = 0;
|
|
171
|
+
let candidateCount = 0;
|
|
172
|
+
for (const session of sessions) {
|
|
173
|
+
if (rootPolicy.enabled && !rootPolicy.allows(session.cwd)) continue;
|
|
174
|
+
if (expectedCwd !== undefined) {
|
|
175
|
+
try { assertClaudeSessionCwd(session, expectedCwd); }
|
|
176
|
+
catch { continue; }
|
|
177
|
+
}
|
|
178
|
+
if (desktopOnly && session.entrypoint !== "claude-desktop") {
|
|
179
|
+
nonDesktopCount += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
candidateCount += 1;
|
|
183
|
+
const inspected = withDesktopContext(session, account);
|
|
184
|
+
if (!desktopOnly || inspected.desktop?.status === "matched") {
|
|
185
|
+
eligible.push(inspected);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const status = inspected.desktop?.status ?? "missing";
|
|
189
|
+
const reason = inspected.desktop?.reason ?? "The Desktop task identity could not be verified.";
|
|
190
|
+
const previous = excluded.find((entry) => entry.status === status && entry.reason === reason);
|
|
191
|
+
if (previous) previous.count += 1;
|
|
192
|
+
else excluded.push({ status, reason, count: 1 });
|
|
193
|
+
}
|
|
194
|
+
return { sessions: eligible, discovery: { candidateCount, matchedCount: eligible.length, nonDesktopCount, excluded } };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function discoveryDetails(discovery) {
|
|
198
|
+
return discovery.excluded.map((entry) => `Desktop task verification: ${entry.count} candidate(s) ${entry.status} - ${entry.reason}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
166
201
|
function assertSessionRoot(session, label = "Claude session") {
|
|
167
202
|
return rootPolicy.assert(session?.cwd, `${label} working directory`);
|
|
168
203
|
}
|
|
@@ -261,7 +296,8 @@ const server = new McpServer(
|
|
|
261
296
|
"The sender class follows the verified approval policy; automated review flags are reported but never change it. " +
|
|
262
297
|
"A Desktop recipient whose settings refuse or hold inbound messages, or whose task metadata shows a different permission class without an explicit accept, is refused before sending, because Claude Desktop cannot show the approval dialog. " +
|
|
263
298
|
"Never launch a CLI session or an external app-server as a substitute. A receipt confirms a reply, not visual verification in the app. " +
|
|
264
|
-
"A held receipt does not prove that Desktop exposes an approval button; verify the UI before asking the user to approve."
|
|
299
|
+
"A held receipt does not prove that Desktop exposes an approval button; verify the UI before asking the user to approve. " +
|
|
300
|
+
AGENT_PROMPT_GUIDANCE,
|
|
265
301
|
},
|
|
266
302
|
);
|
|
267
303
|
|
|
@@ -280,7 +316,7 @@ registerTool(
|
|
|
280
316
|
inputSchema: {
|
|
281
317
|
requestId: z.string().describe("Stable UUID for this creation; reuse exactly when inspecting or retrying"),
|
|
282
318
|
cwd: z.string().describe("Exact absolute directory of the existing Claude Desktop project"),
|
|
283
|
-
prompt: z.string().describe(
|
|
319
|
+
prompt: z.string().describe(`Initial prompt for the NEW conversation; shown for user confirmation. ${PROMPT_FIELD_HINT}`),
|
|
284
320
|
},
|
|
285
321
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
286
322
|
},
|
|
@@ -338,17 +374,14 @@ registerTool(
|
|
|
338
374
|
if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
|
|
339
375
|
const account = readClaudeAccountContext();
|
|
340
376
|
if (desktopOnly && account.status !== "verified") return { ...textResult(`Claude account ${account.status}: ${account.reason} Sign in and retry discovery.`, true), structuredContent: { account: publicAccountState(account), sessions: [] } };
|
|
341
|
-
const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (!sessions.length) return textResult(desktopOnly ? missingDesktopSession : "No live Claude Code session found.");
|
|
350
|
-
return { ...textResult(`${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`),
|
|
351
|
-
structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session) } };
|
|
377
|
+
const { sessions, discovery } = inspectDiscovery(listClaudeSessions({ includeDead: includeDead ?? false }).filter((s) => s.pid !== process.pid), account, expectedCwd);
|
|
378
|
+
const summary = sessions.length
|
|
379
|
+
? `${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`
|
|
380
|
+
: discovery.excluded.length
|
|
381
|
+
? `Found ${discovery.candidateCount} registered Claude Desktop candidate(s) with messaging endpoints, but their Desktop task identities could not be verified. Delivery remains blocked.`
|
|
382
|
+
: desktopOnly ? missingDesktopSession : "No live Claude Code session found.";
|
|
383
|
+
return { ...textResult([summary, ...discoveryDetails(discovery)].join("\n")),
|
|
384
|
+
structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session), discovery } };
|
|
352
385
|
} catch (err) {
|
|
353
386
|
return failure(err);
|
|
354
387
|
}
|
|
@@ -369,7 +402,7 @@ registerTool(
|
|
|
369
402
|
"The sender's current permissions are verified per call; environment overrides and manual binding cannot bypass an unknown sender. It never creates a replacement session.",
|
|
370
403
|
inputSchema: {
|
|
371
404
|
target: z.string().describe("Use auto for current-account discovery in expectedCwd, an exact session identity, or new to prepare a separate Desktop conversation requiring user confirmation"),
|
|
372
|
-
message: z.string().describe(
|
|
405
|
+
message: z.string().describe(`The message text to deliver. ${PROMPT_FIELD_HINT}`),
|
|
373
406
|
expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
|
|
374
407
|
expectedTaskId: z.string().optional().describe("Exact native Claude Desktop task ID from list_claude_sessions; verify its title in the app before sending"),
|
|
375
408
|
waitSec: z
|
|
@@ -644,9 +677,7 @@ registerTool(
|
|
|
644
677
|
await peer.start();
|
|
645
678
|
const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
|
|
646
679
|
const accounts = readBridgeAccounts();
|
|
647
|
-
const
|
|
648
|
-
.filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
|
|
649
|
-
.filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
|
|
680
|
+
const { sessions: eligible, discovery } = inspectDiscovery(sessions, accounts.claude);
|
|
650
681
|
const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
|
|
651
682
|
const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
|
|
652
683
|
const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
|
|
@@ -664,7 +695,8 @@ registerTool(
|
|
|
664
695
|
`Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
|
|
665
696
|
`Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
|
|
666
697
|
`live sessions: ${eligible.length}`,
|
|
667
|
-
|
|
698
|
+
`excluded: ${discovery.nonDesktopCount} non-Desktop session(s); ${discovery.candidateCount - eligible.length} Desktop candidate(s) failed task verification`,
|
|
699
|
+
...discoveryDetails(discovery),
|
|
668
700
|
`relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
|
|
669
701
|
`delivery: ${delivery.describe()}`,
|
|
670
702
|
`inbox: ${visibleInbox.length} pending message(s)`,
|
|
@@ -674,7 +706,7 @@ registerTool(
|
|
|
674
706
|
];
|
|
675
707
|
const state = runtime.status();
|
|
676
708
|
lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
|
|
677
|
-
return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
|
|
709
|
+
return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, discovery, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
|
|
678
710
|
} catch (err) {
|
|
679
711
|
return failure(err);
|
|
680
712
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -7,7 +8,7 @@ const ACCOUNT_ID = new RegExp(`^${UUID}$`, "i");
|
|
|
7
8
|
const TASK_FILE = new RegExp(`^(local_${UUID})\\.json$`, "i");
|
|
8
9
|
const MAX_ENTRIES = 8192;
|
|
9
10
|
const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
10
|
-
const
|
|
11
|
+
const MAX_IDENTITY_BYTES = 32 * 1024 * 1024;
|
|
11
12
|
const VERSION_FIELDS = ["size", "mtimeMs", "ctimeMs", "ino", "dev"];
|
|
12
13
|
|
|
13
14
|
const sameVersion = (left, right) => VERSION_FIELDS.every((field) => left[field] === right[field]);
|
|
@@ -69,18 +70,16 @@ function readMetadata(file, budget) {
|
|
|
69
70
|
try {
|
|
70
71
|
const before = fs.fstatSync(descriptor);
|
|
71
72
|
if (!before.isFile() || before.size > MAX_FILE_BYTES) throw new Error("file limit");
|
|
72
|
-
budget.bytes += before.size;
|
|
73
|
-
if (budget.bytes > MAX_TOTAL_BYTES) throw new Error("total limit");
|
|
74
73
|
const bytes = Buffer.alloc(before.size);
|
|
75
74
|
if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
|
|
76
75
|
const after = fs.fstatSync(descriptor);
|
|
77
76
|
const current = fs.lstatSync(file);
|
|
78
77
|
if (!sameVersion(before, after) || current.isSymbolicLink() || !sameVersion(current, after)) throw new Error("metadata changed");
|
|
79
78
|
budget.versions.set(file, current);
|
|
80
|
-
budget.
|
|
79
|
+
budget.digests.set(file, createHash("sha256").update(bytes).digest("hex"));
|
|
81
80
|
const data = JSON.parse(bytes.toString("utf8"));
|
|
82
81
|
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("invalid metadata");
|
|
83
|
-
|
|
82
|
+
const record = {
|
|
84
83
|
taskId: data.sessionId,
|
|
85
84
|
fileTaskId: TASK_FILE.exec(path.basename(file))[1],
|
|
86
85
|
cliSessionId: data.cliSessionId,
|
|
@@ -90,18 +89,14 @@ function readMetadata(file, budget) {
|
|
|
90
89
|
isArchived: data.isArchived,
|
|
91
90
|
permissionMode: data.permissionMode,
|
|
92
91
|
};
|
|
92
|
+
budget.bytes += Buffer.byteLength(JSON.stringify(record));
|
|
93
|
+
if (budget.bytes > MAX_IDENTITY_BYTES) throw new Error("identity limit");
|
|
94
|
+
return record;
|
|
93
95
|
} finally {
|
|
94
96
|
fs.closeSync(descriptor);
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
/**
|
|
99
|
-
* Stat fields cannot prove a record is unchanged: a same-size rewrite inside
|
|
100
|
-
* one filesystem timestamp tick (about 16 ms on Windows) keeps size, times
|
|
101
|
-
* and inode identical. The final consistency pass therefore compares the
|
|
102
|
-
* bytes themselves; the records are small JSON files, so the extra read is
|
|
103
|
-
* cheap and the check is deterministic on every platform.
|
|
104
|
-
*/
|
|
105
100
|
function rereadMetadata(file) {
|
|
106
101
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
107
102
|
try {
|
|
@@ -109,7 +104,7 @@ function rereadMetadata(file) {
|
|
|
109
104
|
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error("file limit");
|
|
110
105
|
const bytes = Buffer.alloc(stat.size);
|
|
111
106
|
if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
|
|
112
|
-
return bytes;
|
|
107
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
113
108
|
} finally {
|
|
114
109
|
fs.closeSync(descriptor);
|
|
115
110
|
}
|
|
@@ -138,8 +133,14 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
138
133
|
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
139
134
|
if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
|
|
140
135
|
records = readClaudeDesktopTasks({ platform, env, root, account });
|
|
141
|
-
} catch {
|
|
142
|
-
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const detail = {
|
|
138
|
+
"file limit": " A task metadata file exceeds the 4 MiB per-file limit.",
|
|
139
|
+
"identity limit": " Task identity fields exceed the 32 MiB retained-data limit.",
|
|
140
|
+
"scan limit": " The task directory exceeds the 8192-entry scan limit.",
|
|
141
|
+
"metadata changed": " Task metadata changed during inspection; retry discovery.",
|
|
142
|
+
}[error.message] ?? "";
|
|
143
|
+
return result("mismatch", `Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.${detail}`);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
const primary = records.filter((record) => record.cliSessionId === session.sessionId);
|
|
@@ -198,13 +199,13 @@ export function readClaudeDesktopTasks({ platform = process.platform, env = proc
|
|
|
198
199
|
}
|
|
199
200
|
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
200
201
|
const files = metadataFiles(directory, account?.accountId);
|
|
201
|
-
const budget = { bytes: 0, versions: new Map(),
|
|
202
|
+
const budget = { bytes: 0, versions: new Map(), digests: new Map() };
|
|
202
203
|
const records = files.map((file) => readMetadata(file, budget));
|
|
203
204
|
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
204
205
|
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
|
|
205
206
|
for (const file of files) {
|
|
206
207
|
const current = fs.lstatSync(file);
|
|
207
|
-
if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) ||
|
|
208
|
+
if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || rereadMetadata(file) !== budget.digests.get(file)) throw new Error("metadata changed");
|
|
208
209
|
}
|
|
209
210
|
return records;
|
|
210
211
|
}
|
package/src/index.mjs
CHANGED
|
@@ -30,10 +30,11 @@ import { createRuntimeState } from "./runtime-state.mjs";
|
|
|
30
30
|
import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
|
|
31
31
|
import { accountIdentity, assertAccountIdentity, publicAccountState, readBridgeAccounts, requireBridgeAccounts } from "./bridge-account-context.mjs";
|
|
32
32
|
import { assertClaudeSenderContext, readClaudeSenderContext, requireClaudeSenderContext } from "./claude-sender-context.mjs";
|
|
33
|
+
import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
|
|
33
34
|
|
|
34
35
|
exitForVersionRequest(import.meta.url);
|
|
35
36
|
|
|
36
|
-
const VERSION = "1.
|
|
37
|
+
const VERSION = "1.18.1";
|
|
37
38
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
38
39
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
39
40
|
|
|
@@ -345,7 +346,8 @@ const server = new McpServer(
|
|
|
345
346
|
"requested cwd. With Desktop tasks enabled it assigns the exact saved project and starts visibly in " +
|
|
346
347
|
"Codex Desktop using Desktop permissions. Otherwise it releases the bridge writer lock and opens the exact thread in " +
|
|
347
348
|
"Codex Desktop. Use send_to_codex_thread only when an existing threadId is intentional; use " +
|
|
348
|
-
"list_codex_threads or read_codex_thread to inspect sessions and codex_bridge_status to inspect wiring."
|
|
349
|
+
"list_codex_threads or read_codex_thread to inspect sessions and codex_bridge_status to inspect wiring. " +
|
|
350
|
+
AGENT_PROMPT_GUIDANCE,
|
|
349
351
|
},
|
|
350
352
|
);
|
|
351
353
|
|
|
@@ -400,7 +402,7 @@ registerTool(
|
|
|
400
402
|
"return Codex's reply, and hand the session to Codex Desktop without leaving the bridge writer lock behind.",
|
|
401
403
|
inputSchema: {
|
|
402
404
|
cwd: z.string().describe("Absolute project directory where Codex must work"),
|
|
403
|
-
prompt: z.string().describe(
|
|
405
|
+
prompt: z.string().describe(`The complete task Claude is delegating to Codex. ${PROMPT_FIELD_HINT}`),
|
|
404
406
|
name: z.string().min(1).max(200).optional().describe("Optional Codex session title; otherwise one is derived from the prompt"),
|
|
405
407
|
timeoutSec: z
|
|
406
408
|
.number()
|
|
@@ -494,7 +496,7 @@ registerTool(
|
|
|
494
496
|
"Do not close the task, create a replacement, or ask the user to copy the message manually.",
|
|
495
497
|
inputSchema: {
|
|
496
498
|
threadId: z.string().describe("Codex thread id (UUID) - get it from list_codex_threads"),
|
|
497
|
-
prompt: z.string().describe(
|
|
499
|
+
prompt: z.string().describe(`The message to send to Codex as a new user turn. ${PROMPT_FIELD_HINT}`),
|
|
498
500
|
timeoutSec: z
|
|
499
501
|
.number()
|
|
500
502
|
.int()
|
|
@@ -703,7 +705,7 @@ registerTool(
|
|
|
703
705
|
description: "Start a Codex task. In Desktop mode include the initial prompt to create and assign a visible task atomically; use delegate_to_codex to also wait for its reply.",
|
|
704
706
|
inputSchema: {
|
|
705
707
|
cwd: z.string().describe("Absolute working directory for the new Codex session"),
|
|
706
|
-
prompt: z.string().min(1).optional().describe(
|
|
708
|
+
prompt: z.string().min(1).optional().describe(`Initial task; required with CODEX_BRIDGE_DESKTOP_TASKS=1, starts immediately. ${PROMPT_FIELD_HINT}`),
|
|
707
709
|
model: z.string().optional().describe("Model override, e.g. gpt-5.6-luna"),
|
|
708
710
|
name: z.string().min(1).max(200).optional().describe("Optional title to show for the new Codex session"),
|
|
709
711
|
},
|
|
@@ -34,7 +34,7 @@ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
|
34
34
|
|
|
35
35
|
exitForVersionRequest(import.meta.url);
|
|
36
36
|
|
|
37
|
-
const VERSION = "1.
|
|
37
|
+
const VERSION = "1.18.1";
|
|
38
38
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
39
39
|
|
|
40
40
|
function errorResponse(code, message, sent) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every message this bridge carries is a prompt one agent wrote for another.
|
|
3
|
+
* A single paragraph that runs the request into the reply format leaves the
|
|
4
|
+
* recipient guessing where the task ends, so the sending tools all describe
|
|
5
|
+
* one shape. It lives here so the Codex, Claude and VS Code bridges cannot
|
|
6
|
+
* drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export const PROMPT_SECTIONS = Object.freeze([
|
|
9
|
+
"Goal",
|
|
10
|
+
"Context",
|
|
11
|
+
"Task",
|
|
12
|
+
"Scope",
|
|
13
|
+
"Constraints",
|
|
14
|
+
"Done when",
|
|
15
|
+
"Reply format",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const AGENT_PROMPT_GUIDANCE =
|
|
19
|
+
"Write every prompt you compose for another agent in English, in this order, omitting sections that do not apply: " +
|
|
20
|
+
"a first line naming the sender, project and purpose, e.g. [From Claude Code · <project> · <purpose>]; " +
|
|
21
|
+
"## Goal, the outcome in one sentence; ## Context, measured facts, paths with line numbers and existing code to reuse; " +
|
|
22
|
+
"## Task, numbered imperative steps; ## Scope, the files that may and must not change; " +
|
|
23
|
+
"## Constraints, conventions and tests, plus: if the current state differs from this description, stop and report instead of reconciling it; " +
|
|
24
|
+
"## Done when, checkable criteria; ## Reply format, the exact shape of the reply. " +
|
|
25
|
+
"A short coordination message needs only Goal, Context, Task and Reply format. " +
|
|
26
|
+
"Send text the user supplied verbatim, and keep file names, identifiers and user-facing strings in their original language.";
|
|
27
|
+
|
|
28
|
+
export const PROMPT_FIELD_HINT =
|
|
29
|
+
`The prompt for the recipient agent, in English with the sections ${PROMPT_SECTIONS.join(", ")} ` +
|
|
30
|
+
"(omit any that do not apply); text the user supplied is sent verbatim";
|
package/src/vscode-bridge.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { findRollout, readState, readCodexSenderContext } from "./codex-sender-c
|
|
|
10
10
|
import { readProcessAncestry } from "./claude-sender-context.mjs";
|
|
11
11
|
import { listClaudeSessions, assertClaudeSessionCwd, assertClaudeSessionProcess, PeerEndpoint } from "./peer-protocol.mjs";
|
|
12
12
|
import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
|
|
13
|
+
import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
|
|
13
14
|
|
|
14
15
|
const host = process.argv[2];
|
|
15
16
|
if (!["claude", "codex"].includes(host)) throw new Error("Specify the calling extension: claude or codex");
|
|
@@ -102,7 +103,7 @@ tool("vscode_bridge_status", "Verify this extension caller and list same-project
|
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
if (host === "claude") {
|
|
105
|
-
tool("send_to_codex_vscode",
|
|
106
|
+
tool("send_to_codex_vscode", `Send to an existing Codex extension task in this project, preserving its selected permissions. Never retries or opens another app. Use read_codex_vscode_reply after submission. ${AGENT_PROMPT_GUIDANCE}`, { threadId: z.string(), message: z.string().min(1).max(50000).describe(PROMPT_FIELD_HINT) }, async ({ threadId, message }, sender, meta) => {
|
|
106
107
|
if (unresolved.has(threadId)) throw new Error("An earlier send is unresolved. Read its reply before sending again.");
|
|
107
108
|
const inspect = () => {
|
|
108
109
|
const state = codexTask(threadId, sender.cwd);
|
|
@@ -127,7 +128,7 @@ if (host === "claude") {
|
|
|
127
128
|
return { threadId, ...completion };
|
|
128
129
|
});
|
|
129
130
|
} else {
|
|
130
|
-
tool("send_to_claude_vscode",
|
|
131
|
+
tool("send_to_claude_vscode", `Send to the exact Claude Code VS Code session in this project. Never changes recipient permissions. Do not ask the recipient to call back while this synchronous request waits. ${AGENT_PROMPT_GUIDANCE}`, { sessionId: z.string(), message: z.string().min(1).max(50000).describe(PROMPT_FIELD_HINT), waitSec: z.number().int().min(0).max(45).default(30) }, async ({ sessionId, message, waitSec }, sender, meta) => {
|
|
131
132
|
const inspect = () => {
|
|
132
133
|
const matches = listClaudeSessions().filter((session) => session.entrypoint === "claude-vscode" && session.sessionId === sessionId);
|
|
133
134
|
if (matches.length !== 1) throw new Error("No unique live Claude Code VS Code session matches");
|