@minhspark/codex-mcp-bridge 1.18.0 → 1.18.2
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/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 +47 -17
- package/src/claude-desktop-context.mjs +18 -17
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/native-relay.mjs +2 -0
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.2] - 2026-09-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Restore native relay compatibility with Codex Desktop 26.924 by including the required caller source in native tool requests, allowing pipe discovery and Desktop operations to recover automatically.
|
|
12
|
+
|
|
13
|
+
## [1.18.1] - 2026-09-25
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Report npm installation completion with an unchanged version instead of claiming the package is already up to date after a same-version reinstall.
|
|
18
|
+
- 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.
|
|
19
|
+
- Report Desktop task verification failures separately from missing peer endpoints in session discovery and bridge status.
|
|
20
|
+
|
|
7
21
|
## [1.18.0] - 2026-09-23
|
|
8
22
|
|
|
9
23
|
### Changed
|
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.18.
|
|
25
|
+
const VERSION = "1.18.2";
|
|
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
|
@@ -26,7 +26,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
|
|
|
26
26
|
|
|
27
27
|
exitForVersionRequest(import.meta.url);
|
|
28
28
|
|
|
29
|
-
const VERSION = "1.18.
|
|
29
|
+
const VERSION = "1.18.2";
|
|
30
30
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
31
31
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
32
32
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
@@ -164,6 +164,40 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
|
|
|
164
164
|
? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
|
|
165
165
|
}
|
|
166
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
|
+
|
|
167
201
|
function assertSessionRoot(session, label = "Claude session") {
|
|
168
202
|
return rootPolicy.assert(session?.cwd, `${label} working directory`);
|
|
169
203
|
}
|
|
@@ -340,17 +374,14 @@ registerTool(
|
|
|
340
374
|
if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
|
|
341
375
|
const account = readClaudeAccountContext();
|
|
342
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: [] } };
|
|
343
|
-
const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!sessions.length) return textResult(desktopOnly ? missingDesktopSession : "No live Claude Code session found.");
|
|
352
|
-
return { ...textResult(`${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`),
|
|
353
|
-
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 } };
|
|
354
385
|
} catch (err) {
|
|
355
386
|
return failure(err);
|
|
356
387
|
}
|
|
@@ -646,9 +677,7 @@ registerTool(
|
|
|
646
677
|
await peer.start();
|
|
647
678
|
const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
|
|
648
679
|
const accounts = readBridgeAccounts();
|
|
649
|
-
const
|
|
650
|
-
.filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
|
|
651
|
-
.filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
|
|
680
|
+
const { sessions: eligible, discovery } = inspectDiscovery(sessions, accounts.claude);
|
|
652
681
|
const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
|
|
653
682
|
const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
|
|
654
683
|
const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
|
|
@@ -666,7 +695,8 @@ registerTool(
|
|
|
666
695
|
`Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
|
|
667
696
|
`Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
|
|
668
697
|
`live sessions: ${eligible.length}`,
|
|
669
|
-
|
|
698
|
+
`excluded: ${discovery.nonDesktopCount} non-Desktop session(s); ${discovery.candidateCount - eligible.length} Desktop candidate(s) failed task verification`,
|
|
699
|
+
...discoveryDetails(discovery),
|
|
670
700
|
`relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
|
|
671
701
|
`delivery: ${delivery.describe()}`,
|
|
672
702
|
`inbox: ${visibleInbox.length} pending message(s)`,
|
|
@@ -676,7 +706,7 @@ registerTool(
|
|
|
676
706
|
];
|
|
677
707
|
const state = runtime.status();
|
|
678
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`}`);
|
|
679
|
-
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 } : {}) } };
|
|
680
710
|
} catch (err) {
|
|
681
711
|
return failure(err);
|
|
682
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
|
@@ -34,7 +34,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
|
|
|
34
34
|
|
|
35
35
|
exitForVersionRequest(import.meta.url);
|
|
36
36
|
|
|
37
|
-
const VERSION = "1.18.
|
|
37
|
+
const VERSION = "1.18.2";
|
|
38
38
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
39
39
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
40
40
|
|
|
@@ -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.18.
|
|
37
|
+
const VERSION = "1.18.2";
|
|
38
38
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
39
39
|
|
|
40
40
|
function errorResponse(code, message, sent) {
|
package/src/native-relay.mjs
CHANGED
|
@@ -168,6 +168,7 @@ export function resolveRelayThreadId(env = process.env) {
|
|
|
168
168
|
export function nativeDispatchParams({ executorThreadId, targetThreadId, message }) {
|
|
169
169
|
return {
|
|
170
170
|
arguments: { threadId: targetThreadId, prompt: message },
|
|
171
|
+
callerSource: "codex",
|
|
171
172
|
callId: `codex-native-relay-${randomUUID()}`,
|
|
172
173
|
namespace: "codex_app",
|
|
173
174
|
threadId: executorThreadId,
|
|
@@ -246,6 +247,7 @@ export function nativeDesktopOperationParams({ executorThreadId, operation, argu
|
|
|
246
247
|
validateDesktopOperation(operation, args);
|
|
247
248
|
return {
|
|
248
249
|
arguments: args,
|
|
250
|
+
callerSource: "codex",
|
|
249
251
|
callId: `codex-native-relay-${randomUUID()}`,
|
|
250
252
|
namespace: "codex_app",
|
|
251
253
|
threadId: executorThreadId,
|