@minhspark/codex-mcp-bridge 1.26.8 → 1.26.10
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +45 -0
- package/docs/daily-handoffs.md +18 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +3 -5
- package/scripts/install-codex-mcp.mjs +2 -5
- package/scripts/install-native-relay.mjs +4 -7
- package/scripts/setup.mjs +2 -1
- package/scripts/update.mjs +22 -13
- package/src/antigravity-bridge.mjs +50 -10
- package/src/antigravity-deliveries.mjs +27 -2
- package/src/antigravity-desktop.mjs +206 -2
- package/src/antigravity-outbound-targets.mjs +1 -1
- package/src/antigravity-sidecar.mjs +5 -4
- package/src/background-codex-worker.mjs +26 -4
- package/src/claude-bridge.mjs +1 -1
- package/src/codex-mcp-registration.mjs +21 -0
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +116 -14
- package/src/platform.mjs +19 -18
- package/src/recipient-preflight.mjs +3 -2
- package/src/setup.mjs +18 -7
package/src/peer-protocol.mjs
CHANGED
|
@@ -284,6 +284,7 @@ export function assertClaudeSessionProcess(session) {
|
|
|
284
284
|
* rather than by reconstructing the slug.
|
|
285
285
|
*/
|
|
286
286
|
function findTranscriptFile(sessionId, cwd) {
|
|
287
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 128 || /[^a-zA-Z0-9_-]/.test(sessionId)) return null;
|
|
287
288
|
const dir = projectsDir();
|
|
288
289
|
const guess = path.join(dir, String(cwd ?? "").replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`);
|
|
289
290
|
if (fs.existsSync(guess)) return guess;
|
|
@@ -297,7 +298,7 @@ function findTranscriptFile(sessionId, cwd) {
|
|
|
297
298
|
|
|
298
299
|
export function readTranscript(sessionId, cwd, limit = 10) {
|
|
299
300
|
const file = findTranscriptFile(sessionId, cwd);
|
|
300
|
-
if (!fs.existsSync(file)) return { file, messages: [] };
|
|
301
|
+
if (!file || !fs.existsSync(file)) return { file, messages: [] };
|
|
301
302
|
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
302
303
|
const messages = [];
|
|
303
304
|
for (const line of lines) {
|
|
@@ -325,7 +326,7 @@ export function readTranscript(sessionId, cwd, limit = 10) {
|
|
|
325
326
|
|
|
326
327
|
export function readInitialTranscriptMessage(sessionId, cwd) {
|
|
327
328
|
const file = findTranscriptFile(sessionId, cwd);
|
|
328
|
-
if (!fs.existsSync(file)) return { file, messages: [] };
|
|
329
|
+
if (!file || !fs.existsSync(file)) return { file, messages: [] };
|
|
329
330
|
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
330
331
|
try {
|
|
331
332
|
const stat = fs.fstatSync(descriptor);
|
|
@@ -388,6 +389,46 @@ function isTerminalDelivery(receipt) {
|
|
|
388
389
|
return ["refused", "denied", "expired", "dropped"].includes(receipt?.status) || isInterruptedDelivery(receipt);
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
function resumableTranscriptDelivery(id, sent, receipt) {
|
|
393
|
+
const uuid = value => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
394
|
+
const session = sent?.transcriptSession;
|
|
395
|
+
return Boolean(sent) && uuid(id) && !sent.reply && !sent.failed && !isTerminalDelivery(receipt)
|
|
396
|
+
&& Number.isFinite(sent.sentAt) && (!receipt || receipt.status === "delivered" && receipt.fromSocket === sent.targetSocket)
|
|
397
|
+
&& session?.entrypoint === "claude-desktop" && uuid(session.sessionId)
|
|
398
|
+
&& typeof session.cwd === "string" && path.isAbsolute(session.cwd)
|
|
399
|
+
&& typeof sent.targetSocket === "string" && sent.targetSocket && session.socket === sent.targetSocket
|
|
400
|
+
&& session.desktop?.status === "matched" && typeof session.desktop.taskId === "string"
|
|
401
|
+
&& session.desktop.taskId.startsWith("local_") && uuid(session.desktop.taskId.slice(6))
|
|
402
|
+
&& session.desktop.cwd === session.cwd
|
|
403
|
+
&& /^[a-f0-9]{64}$/i.test(sent.accountContext?.claude ?? "") && /^[a-f0-9]{64}$/i.test(sent.accountContext?.codex ?? "")
|
|
404
|
+
&& session.desktop.accountFingerprint === sent.accountContext.claude
|
|
405
|
+
&& hasTranscriptDeliveryRoot(session.sessionId, session.cwd, id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasTranscriptDeliveryRoot(sessionId, cwd, msgId) {
|
|
409
|
+
const file = findTranscriptFile(sessionId, cwd);
|
|
410
|
+
if (!file) return false;
|
|
411
|
+
let lines;
|
|
412
|
+
try { lines = fs.readFileSync(file, "utf8").split("\n"); }
|
|
413
|
+
catch { return false; }
|
|
414
|
+
let roots = 0;
|
|
415
|
+
const ids = new Set();
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (!line.trim()) continue;
|
|
418
|
+
let entry;
|
|
419
|
+
try { entry = JSON.parse(line); } catch { return false; }
|
|
420
|
+
if (!entry || typeof entry !== "object" || entry.isSidechain) continue;
|
|
421
|
+
if (typeof entry.uuid === "string") {
|
|
422
|
+
if (ids.has(entry.uuid)) return false;
|
|
423
|
+
ids.add(entry.uuid);
|
|
424
|
+
}
|
|
425
|
+
if (!injectedMessageRoot(entry, msgId)) continue;
|
|
426
|
+
if (entry.entrypoint !== "claude-desktop" || entry.sessionId !== sessionId || entry.cwd !== cwd) return false;
|
|
427
|
+
roots++;
|
|
428
|
+
}
|
|
429
|
+
return roots === 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
391
432
|
export function readTranscriptReply(sessionId, cwd, msgId) {
|
|
392
433
|
return readTranscriptOutcome(sessionId, cwd, msgId)?.reply ?? null;
|
|
393
434
|
}
|
|
@@ -460,6 +501,7 @@ function compactContinuation(entry, root, entriesById, transcriptIds, sessionId,
|
|
|
460
501
|
// user turn's answer to the original request or claim its side effects stopped.
|
|
461
502
|
export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
462
503
|
const file = findTranscriptFile(sessionId, cwd);
|
|
504
|
+
if (!file) return null;
|
|
463
505
|
let lines;
|
|
464
506
|
try { lines = fs.readFileSync(file, "utf8").split("\n"); }
|
|
465
507
|
catch { return null; }
|
|
@@ -509,6 +551,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
509
551
|
}
|
|
510
552
|
}
|
|
511
553
|
const descendants = new Map();
|
|
554
|
+
const monitorTasksByEntry = new Map();
|
|
512
555
|
const backgroundCalls = new Map();
|
|
513
556
|
const openLeaves = new Set();
|
|
514
557
|
const passiveLeaves = new Set();
|
|
@@ -520,7 +563,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
520
563
|
interruptions.push({ entryId: entry.uuid, backgroundTaskIds: [...outstanding.keys()] });
|
|
521
564
|
};
|
|
522
565
|
for (const entry of orderedEntries) {
|
|
523
|
-
if (entry === root) { descendants.set(entry.uuid, new Map()); openLeaves.add(entry.uuid); continue; }
|
|
566
|
+
if (entry === root) { descendants.set(entry.uuid, new Map()); monitorTasksByEntry.set(entry.uuid, new Set()); openLeaves.add(entry.uuid); continue; }
|
|
524
567
|
let parentUuid = entry.parentUuid;
|
|
525
568
|
if (!ambiguous && !interruptions.length && descendants.has(entry.logicalParentUuid)
|
|
526
569
|
&& openLeaves.has(entry.logicalParentUuid)) {
|
|
@@ -549,6 +592,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
549
592
|
const role = entry.message?.role;
|
|
550
593
|
const content = entry.message?.content;
|
|
551
594
|
const outstanding = new Map(descendants.get(parentUuid));
|
|
595
|
+
const monitorTasks = new Set(monitorTasksByEntry.get(parentUuid));
|
|
552
596
|
const toolResults = Array.isArray(content) && content.length > 0 && content.every((part) => part?.type === "tool_result");
|
|
553
597
|
const imageCompanion = isImageToolCompanion(entry, entriesById);
|
|
554
598
|
if (role === "user" && !toolResults && !imageCompanion && !compactSummaries.has(entry.uuid)) {
|
|
@@ -575,13 +619,20 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
575
619
|
}
|
|
576
620
|
if (typeof taskNotification === "string" && /^<task-notification>\s[\s\S]*<\/task-notification>$/.test(taskNotification.trim())) {
|
|
577
621
|
const field = (name) => {
|
|
622
|
+
if (taskNotification.split(`<${name}>`).length !== 2 || taskNotification.split(`</${name}>`).length !== 2) return null;
|
|
578
623
|
const matches = [...taskNotification.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
|
|
579
624
|
return matches.length === 1 ? matches[0][1] : null;
|
|
580
625
|
};
|
|
581
626
|
const taskId = field("task-id");
|
|
627
|
+
const hasField = name => taskNotification.split(`<${name}>`).length === 2
|
|
628
|
+
&& taskNotification.split(`</${name}>`).length === 2
|
|
629
|
+
&& new RegExp(`<${name}>[\\s\\S]+?</${name}>`).test(taskNotification);
|
|
630
|
+
const monitorProgress = monitorTasks.has(taskId) && !outstanding.has(taskId)
|
|
631
|
+
&& hasField("summary") && hasField("event")
|
|
632
|
+
&& !/<\/?(?:status|tool-use-id)(?:\s|>)/.test(taskNotification);
|
|
582
633
|
if (!outstanding.has(taskId) || outstanding.get(taskId) !== field("tool-use-id")
|
|
583
634
|
|| !["completed", "failed", "killed"].includes(field("status"))) {
|
|
584
|
-
if (role === "user") continue;
|
|
635
|
+
if (role === "user" && !monitorProgress) continue;
|
|
585
636
|
} else outstanding.delete(taskId);
|
|
586
637
|
}
|
|
587
638
|
if (typeof entry.uuid !== "string" || !entry.uuid.trim()) continue;
|
|
@@ -600,6 +651,18 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
|
|
|
600
651
|
// Parallel tool results and hook records can be leaf siblings of the real
|
|
601
652
|
// continuation. They are completed observations, not separate reply turns.
|
|
602
653
|
const parentEntry = entriesById.get(entry.parentUuid);
|
|
654
|
+
const monitorTaskId = entry.toolUseResult?.taskId;
|
|
655
|
+
if (role === "user" && toolResults && content.length === 1 && typeof monitorTaskId === "string" && /^[\w-]{1,128}$/.test(monitorTaskId)
|
|
656
|
+
&& entry.sourceToolAssistantUUID === parentEntry?.uuid
|
|
657
|
+
&& parentEntry?.message?.role === "assistant"
|
|
658
|
+
&& Number.isSafeInteger(entry.toolUseResult.timeoutMs) && entry.toolUseResult.timeoutMs > 0
|
|
659
|
+
&& typeof entry.toolUseResult.persistent === "boolean") {
|
|
660
|
+
const matches = content.filter(part => !part.is_error && typeof part.tool_use_id === "string" && part.tool_use_id
|
|
661
|
+
&& Array.isArray(parentEntry.message.content)
|
|
662
|
+
&& parentEntry.message.content.filter(call => call.type === "tool_use" && call.name === "Monitor" && call.id === part.tool_use_id).length === 1);
|
|
663
|
+
if (matches.length === 1) monitorTasks.add(monitorTaskId);
|
|
664
|
+
}
|
|
665
|
+
monitorTasksByEntry.set(entry.uuid, monitorTasks);
|
|
603
666
|
const matchedResults = toolResults && !entry.toolUseResult?.backgroundTaskId
|
|
604
667
|
&& content.every((part) => Array.isArray(parentEntry?.message?.content)
|
|
605
668
|
&& parentEntry.message.content.some((call) => call.type === "tool_use" && call.id === part.tool_use_id));
|
|
@@ -654,6 +717,7 @@ export class PeerEndpoint {
|
|
|
654
717
|
this.sentMessages = new Map();
|
|
655
718
|
this.pendingMessages = new Map();
|
|
656
719
|
this.responsePoll = null;
|
|
720
|
+
this.transcriptPollingPaused = false;
|
|
657
721
|
this.connections = new Set();
|
|
658
722
|
this.activityRevision = 0;
|
|
659
723
|
this.reloadPaused = false;
|
|
@@ -779,6 +843,8 @@ export class PeerEndpoint {
|
|
|
779
843
|
}
|
|
780
844
|
|
|
781
845
|
this.started = true;
|
|
846
|
+
this.transcriptPollingPaused = false;
|
|
847
|
+
this.#startTranscriptPolling();
|
|
782
848
|
this.log(`peer "${this.name}" listening on ${this.socketPath}`);
|
|
783
849
|
}
|
|
784
850
|
|
|
@@ -903,11 +969,20 @@ export class PeerEndpoint {
|
|
|
903
969
|
}
|
|
904
970
|
}
|
|
905
971
|
|
|
972
|
+
#startTranscriptPolling() {
|
|
973
|
+
if (this.reloadPaused || this.transcriptPollingPaused || this.responsePoll
|
|
974
|
+
|| ![...this.pendingMessages.values()].some(entry => entry.transcriptSession)) return;
|
|
975
|
+
this.responsePoll = globalThis.setInterval(() => this.#refreshTranscriptReplies(), 250);
|
|
976
|
+
this.responsePoll.unref();
|
|
977
|
+
}
|
|
978
|
+
|
|
906
979
|
#refreshTranscriptReplies() {
|
|
980
|
+
if (this.reloadPaused || this.transcriptPollingPaused) return;
|
|
907
981
|
for (const [msgId, pending] of this.pendingMessages) {
|
|
908
982
|
if (!pending.transcriptSession) continue;
|
|
909
983
|
const { sessionId, cwd } = pending.transcriptSession;
|
|
910
984
|
const file = findTranscriptFile(sessionId, cwd);
|
|
985
|
+
if (!file) continue;
|
|
911
986
|
let stamp;
|
|
912
987
|
try { const stat = fs.statSync(file, { bigint: true }); stamp = `${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}`; }
|
|
913
988
|
catch { continue; }
|
|
@@ -1051,6 +1126,7 @@ export class PeerEndpoint {
|
|
|
1051
1126
|
);
|
|
1052
1127
|
error.code = "PEER_REPLY_PENDING";
|
|
1053
1128
|
error.sent = false;
|
|
1129
|
+
error.msgId = [...this.pendingMessages].find(([, entry]) => entry.targetSocket === targetSocket)?.[0];
|
|
1054
1130
|
throw error;
|
|
1055
1131
|
}
|
|
1056
1132
|
const since = Date.now();
|
|
@@ -1059,10 +1135,7 @@ export class PeerEndpoint {
|
|
|
1059
1135
|
let msgId = crypto.randomUUID();
|
|
1060
1136
|
this.pendingMessages.set(msgId, { targetSocket, transcriptSession });
|
|
1061
1137
|
this.sentMessages.set(msgId, { targetSocket, transcriptSession, sentAt: since, replyThreadId, permissionMode, senderApprovalPolicy, ...(senderReview ? { senderReview: { ...senderReview } } : {}), ...(recipient ? { recipient: { ...recipient } } : {}), ...(accountContext ? { accountContext: Object.freeze({ ...accountContext }) } : {}), ...(scopeBindings ? { scopeBindings: Object.freeze(structuredClone(scopeBindings)) } : {}) });
|
|
1062
|
-
|
|
1063
|
-
this.responsePoll = globalThis.setInterval(() => this.#refreshTranscriptReplies(), 250);
|
|
1064
|
-
this.responsePoll.unref();
|
|
1065
|
-
}
|
|
1138
|
+
this.#startTranscriptPolling();
|
|
1066
1139
|
try {
|
|
1067
1140
|
const sentId = await this.send(targetSocket, text, { priority, msgId, permissionMode, beforeSend });
|
|
1068
1141
|
if (sentId !== msgId) {
|
|
@@ -1191,9 +1264,21 @@ export class PeerEndpoint {
|
|
|
1191
1264
|
reloadReason() {
|
|
1192
1265
|
this.#refreshTranscriptReplies();
|
|
1193
1266
|
if (this.connections.size) return "Peer socket connections are still open";
|
|
1194
|
-
if (this.requestQueues.size
|
|
1267
|
+
if (this.requestQueues.size) return "Claude messages are still being sent";
|
|
1268
|
+
const counts = new Map();
|
|
1269
|
+
for (const [id, pending] of this.pendingMessages) {
|
|
1270
|
+
const sent = this.sentMessages.get(id);
|
|
1271
|
+
if (!resumableTranscriptDelivery(id, sent, this.deliveryReceipts.get(id))
|
|
1272
|
+
|| pending.targetSocket !== sent.targetSocket || JSON.stringify(pending.transcriptSession) !== JSON.stringify(sent.transcriptSession)) {
|
|
1273
|
+
return "Claude messages still have pending or unconfirmed delivery";
|
|
1274
|
+
}
|
|
1275
|
+
counts.set(sent.targetSocket, (counts.get(sent.targetSocket) ?? 0) + 1);
|
|
1276
|
+
}
|
|
1277
|
+
if (counts.size !== this.unconfirmedReplies.size || [...counts].some(([socket, count]) => this.unconfirmedReplies.get(socket) !== count)) {
|
|
1278
|
+
return "Claude messages still have pending or unconfirmed delivery";
|
|
1279
|
+
}
|
|
1195
1280
|
for (const [id, sent] of this.sentMessages) {
|
|
1196
|
-
if (!sent.reply && !sent.failed && !isTerminalDelivery(this.deliveryReceipts.get(id))) {
|
|
1281
|
+
if (!sent.reply && !sent.failed && !isTerminalDelivery(this.deliveryReceipts.get(id)) && !this.pendingMessages.has(id)) {
|
|
1197
1282
|
return "A Claude delivery outcome remains unconfirmed";
|
|
1198
1283
|
}
|
|
1199
1284
|
}
|
|
@@ -1243,14 +1328,17 @@ export class PeerEndpoint {
|
|
|
1243
1328
|
const reason = this.reloadReason();
|
|
1244
1329
|
if (reason || this.started && !this.reloadPaused) throw new Error(reason ?? "Quiesce the peer endpoint before exporting state");
|
|
1245
1330
|
return cloneReloadState({ name: this.name, messageSequence: this.messageSequence, inbox: this.inbox,
|
|
1246
|
-
sentMessages: [...this.sentMessages], deliveryReceipts: [...this.deliveryReceipts]
|
|
1331
|
+
sentMessages: [...this.sentMessages], deliveryReceipts: [...this.deliveryReceipts],
|
|
1332
|
+
pendingMessages: [...this.pendingMessages].map(([id, pending]) => [id, { targetSocket: pending.targetSocket, transcriptSession: pending.transcriptSession }]) });
|
|
1247
1333
|
}
|
|
1248
1334
|
|
|
1249
1335
|
restoreReloadState(input) {
|
|
1250
|
-
if (this.started || this.connections.size || this.sentMessages.size || this.inbox.length
|
|
1336
|
+
if (this.started || this.connections.size || this.sentMessages.size || this.inbox.length || this.pendingMessages.size
|
|
1337
|
+
|| this.unconfirmedReplies.size || this.requestQueues.size || this.responsePoll) throw new Error("Peer reload state requires an empty dormant endpoint");
|
|
1251
1338
|
const state = cloneReloadState(input);
|
|
1252
1339
|
if (!state || typeof state.name !== "string" || !state.name || !Number.isSafeInteger(state.messageSequence) || state.messageSequence < 0
|
|
1253
|
-
|| !Array.isArray(state.inbox) || !Array.isArray(state.sentMessages) || !Array.isArray(state.deliveryReceipts)
|
|
1340
|
+
|| !Array.isArray(state.inbox) || !Array.isArray(state.sentMessages) || !Array.isArray(state.deliveryReceipts)
|
|
1341
|
+
|| state.pendingMessages !== undefined && !Array.isArray(state.pendingMessages)) throw new Error("Invalid peer reload state");
|
|
1254
1342
|
const restoreMap = (entries) => {
|
|
1255
1343
|
const restored = new Map();
|
|
1256
1344
|
for (const pair of entries) {
|
|
@@ -1262,6 +1350,17 @@ export class PeerEndpoint {
|
|
|
1262
1350
|
};
|
|
1263
1351
|
const sentMessages = restoreMap(state.sentMessages);
|
|
1264
1352
|
const deliveryReceipts = restoreMap(state.deliveryReceipts);
|
|
1353
|
+
const pendingMessages = restoreMap(state.pendingMessages ?? []);
|
|
1354
|
+
const unconfirmedReplies = new Map();
|
|
1355
|
+
for (const [id, pending] of pendingMessages) {
|
|
1356
|
+
const sent = sentMessages.get(id);
|
|
1357
|
+
if (!resumableTranscriptDelivery(id, sent, deliveryReceipts.get(id))
|
|
1358
|
+
|| pending.targetSocket !== sent.targetSocket || JSON.stringify(pending.transcriptSession) !== JSON.stringify(sent.transcriptSession)) {
|
|
1359
|
+
throw new Error("Invalid pending Claude transcript reload record");
|
|
1360
|
+
}
|
|
1361
|
+
delete pending.transcriptStamp;
|
|
1362
|
+
unconfirmedReplies.set(sent.targetSocket, (unconfirmedReplies.get(sent.targetSocket) ?? 0) + 1);
|
|
1363
|
+
}
|
|
1265
1364
|
for (const record of state.inbox) {
|
|
1266
1365
|
if (!record || typeof record.text !== "string" || !Number.isSafeInteger(record.sequence) || record.sequence < 1
|
|
1267
1366
|
|| record.sequence > state.messageSequence || !Number.isFinite(record.receivedAt)) throw new Error("Invalid inbox reload record");
|
|
@@ -1269,7 +1368,7 @@ export class PeerEndpoint {
|
|
|
1269
1368
|
for (const [id, sent] of sentMessages) {
|
|
1270
1369
|
if (typeof sent.targetSocket !== "string" || !sent.targetSocket || !Number.isFinite(sent.sentAt)
|
|
1271
1370
|
|| sent.reply && (typeof sent.reply.text !== "string" || sent.reply.inReplyTo !== id)
|
|
1272
|
-
|| !sent.reply && !sent.failed && !isTerminalDelivery(deliveryReceipts.get(id))
|
|
1371
|
+
|| !sent.reply && !sent.failed && !isTerminalDelivery(deliveryReceipts.get(id)) && !pendingMessages.has(id)
|
|
1273
1372
|
|| isInterruptedDelivery(deliveryReceipts.get(id)) && deliveryReceipts.get(id).inReplyTo !== id) {
|
|
1274
1373
|
throw new Error("Invalid or unconfirmed Claude delivery reload record");
|
|
1275
1374
|
}
|
|
@@ -1279,6 +1378,9 @@ export class PeerEndpoint {
|
|
|
1279
1378
|
this.inbox = state.inbox;
|
|
1280
1379
|
this.sentMessages = sentMessages;
|
|
1281
1380
|
this.deliveryReceipts = deliveryReceipts;
|
|
1381
|
+
this.pendingMessages = pendingMessages;
|
|
1382
|
+
this.unconfirmedReplies = unconfirmedReplies;
|
|
1383
|
+
this.transcriptPollingPaused = true;
|
|
1282
1384
|
}
|
|
1283
1385
|
|
|
1284
1386
|
#removeRegistration() {
|
package/src/platform.mjs
CHANGED
|
@@ -88,9 +88,10 @@ function workspaceRoots() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function isRunnable(candidate) {
|
|
91
|
-
if (!candidate ||
|
|
92
|
-
if (IS_WINDOWS) return true;
|
|
91
|
+
if (typeof candidate !== "string" || !candidate || candidate.includes("\0")) return false;
|
|
93
92
|
try {
|
|
93
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile()) return false;
|
|
94
|
+
if (IS_WINDOWS) return true;
|
|
94
95
|
accessSync(candidate, constants.X_OK);
|
|
95
96
|
return true;
|
|
96
97
|
} catch {
|
|
@@ -98,9 +99,9 @@ function isRunnable(candidate) {
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function windowsCodexCandidates() {
|
|
102
|
-
const roaming =
|
|
103
|
-
const local =
|
|
102
|
+
function windowsCodexCandidates(env, runnable) {
|
|
103
|
+
const roaming = env.APPDATA;
|
|
104
|
+
const local = env.LOCALAPPDATA;
|
|
104
105
|
const versionedRoot = local && path.join(local, "OpenAI", "Codex", "bin");
|
|
105
106
|
let versioned = [];
|
|
106
107
|
if (versionedRoot) {
|
|
@@ -108,7 +109,7 @@ function windowsCodexCandidates() {
|
|
|
108
109
|
versioned = readdirSync(versionedRoot, { withFileTypes: true })
|
|
109
110
|
.filter((entry) => entry.isDirectory())
|
|
110
111
|
.map((entry) => path.join(versionedRoot, entry.name, "codex.exe"))
|
|
111
|
-
.filter((candidate) =>
|
|
112
|
+
.filter((candidate) => runnable(candidate))
|
|
112
113
|
.sort((left, right) => {
|
|
113
114
|
try {
|
|
114
115
|
return statSync(path.dirname(right)).mtimeMs - statSync(path.dirname(left)).mtimeMs;
|
|
@@ -119,16 +120,16 @@ function windowsCodexCandidates() {
|
|
|
119
120
|
} catch {}
|
|
120
121
|
}
|
|
121
122
|
return [
|
|
122
|
-
|
|
123
|
+
env.CODEX_CLI_PATH,
|
|
123
124
|
...versioned,
|
|
124
125
|
local && path.join(local, "Programs", "OpenAI", "Codex", "bin", "codex.exe"),
|
|
125
126
|
roaming && path.join(roaming, "npm", "codex.cmd"),
|
|
126
|
-
|
|
127
|
+
env.ProgramFiles && path.join(env.ProgramFiles, "nodejs", "codex.cmd"),
|
|
127
128
|
];
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function unixCodexCandidates() {
|
|
131
|
-
const home =
|
|
131
|
+
function unixCodexCandidates(env) {
|
|
132
|
+
const home = env.HOME ?? env.USERPROFILE ?? os.homedir();
|
|
132
133
|
return [
|
|
133
134
|
path.join(home, ".local", "bin", "codex"),
|
|
134
135
|
path.join(home, ".npm-global", "bin", "codex"),
|
|
@@ -147,17 +148,17 @@ function unixCodexCandidates() {
|
|
|
147
148
|
* two versions write that same state, so prefer the app's binary whenever the
|
|
148
149
|
* app is installed and fall back to the standalone CLI installs otherwise.
|
|
149
150
|
*/
|
|
150
|
-
function macosCodexCandidates() {
|
|
151
|
+
function macosCodexCandidates(env) {
|
|
151
152
|
return [
|
|
152
153
|
hasCodexDesktopApp() ? CODEX_DESKTOP_BIN_MACOS : null,
|
|
153
|
-
...unixCodexCandidates(),
|
|
154
|
+
...unixCodexCandidates(env),
|
|
154
155
|
];
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
function pathCandidates() {
|
|
158
|
-
const separator =
|
|
159
|
-
const names =
|
|
160
|
-
return (
|
|
158
|
+
function pathCandidates(env, platform) {
|
|
159
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
160
|
+
const names = platform === "win32" ? ["codex.exe", "codex.cmd"] : ["codex"];
|
|
161
|
+
return (env.PATH ?? "")
|
|
161
162
|
.split(separator)
|
|
162
163
|
.filter(Boolean)
|
|
163
164
|
.flatMap((dir) => names.map((name) => path.join(dir, name)));
|
|
@@ -217,8 +218,8 @@ export function resolveCodexBin(explicit, {
|
|
|
217
218
|
explicit,
|
|
218
219
|
env.CODEX_BIN,
|
|
219
220
|
...(discovered ?? [
|
|
220
|
-
...(
|
|
221
|
-
...pathCandidates(),
|
|
221
|
+
...(platform === "win32" ? windowsCodexCandidates(env, runnable) : platform === "darwin" ? macosCodexCandidates(env) : unixCodexCandidates(env)),
|
|
222
|
+
...pathCandidates(env, platform),
|
|
222
223
|
]),
|
|
223
224
|
].filter(Boolean);
|
|
224
225
|
|
|
@@ -55,8 +55,9 @@ export function bridgeDiagnostic(error, options = {}) {
|
|
|
55
55
|
phase: name(previous?.phase ?? options.phase ?? error?.phase ?? error?.diagnostic?.stage, deliveryState === "not_sent" ? "preflight" : options.readOnly ? "inspection" : "delivery"),
|
|
56
56
|
deliveryState,
|
|
57
57
|
sent: deliveryState === "not_sent" ? false : deliveryState === "sent" ? true : null,
|
|
58
|
-
retrySafe: deliveryState === "not_sent",
|
|
59
|
-
nextAction:
|
|
58
|
+
retrySafe: deliveryState === "not_sent" && code !== "PEER_REPLY_PENDING",
|
|
59
|
+
nextAction: code === "PEER_REPLY_PENDING" ? "Read read_claude_delivery with the original msgId. Wait for its correlated outcome; do not resend or start another bridge to bypass pending work."
|
|
60
|
+
: deliveryState === "not_sent" ? "Inspect the reported cause and bridge status before retrying. Do not open or create chats to load a relay."
|
|
60
61
|
: deliveryState === "not_applicable" ? "Inspect bridge status and include this diagnostic in the report."
|
|
61
62
|
: "Inspect the original delivery receipt. Do not resend automatically or create a replacement chat.",
|
|
62
63
|
reason: diagnosticText(previous?.reason ?? error?.preflight?.reason ?? error?.message ?? error),
|
package/src/setup.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveCodexBin, resolvePackagedCodexBin } from "./platform.mjs";
|
|
|
8
8
|
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
9
9
|
import { stableInstallPath } from "./install-paths.mjs";
|
|
10
10
|
import { resolveClaudeDesktopConfigRoot } from "./desktop-account-context.mjs";
|
|
11
|
+
import { CODEX_MCP_TIMEOUT_MS } from "./codex-mcp-registration.mjs";
|
|
11
12
|
|
|
12
13
|
const ROOT = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
13
14
|
const CODEX_PACKAGE = "@openai/codex@0.166.0";
|
|
@@ -64,7 +65,7 @@ export function setupFailureSummary(value) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function setupArguments(args) {
|
|
67
|
-
const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--help"]);
|
|
68
|
+
const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--progress", "--help"]);
|
|
68
69
|
if (args.some((arg) => !allowed.has(arg))) throw new Error("Unknown setup option; use codex-bridge-setup --help");
|
|
69
70
|
if (args.includes("--check") && args.includes("--dry-run")) throw new Error("Use --check or --dry-run, not both");
|
|
70
71
|
return Object.fromEntries([...allowed].map((arg) => [arg.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), args.includes(arg)]));
|
|
@@ -148,6 +149,13 @@ export function setupEnvironment(env, paths, codex) {
|
|
|
148
149
|
CLAUDE_DESKTOP_CONFIG: paths.desktop, CODEX_EXE: codex, CODEX_BIN: codex };
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function codexInventoryFailure(error) {
|
|
153
|
+
const detail = error?.message?.includes("ETIMEDOUT") || error?.code === "ETIMEDOUT"
|
|
154
|
+
? `Codex CLI did not list its MCP servers within ${CODEX_MCP_TIMEOUT_MS / 1000} seconds; the computer may be busy. Rerun the check when it is idle`
|
|
155
|
+
: error?.message ?? String(error);
|
|
156
|
+
return Object.assign(new Error(detail), error?.code ? { code: error.code } : {});
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
function configurationCheck(label, check, components) {
|
|
152
160
|
try { const details = check(); components.push({ name: label, status: "configured", ...details }); }
|
|
153
161
|
catch (error) { components.push(failedComponent(label, error)); }
|
|
@@ -171,15 +179,16 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
171
179
|
let inventory;
|
|
172
180
|
configurationCheck("Codex CLI", () => {
|
|
173
181
|
codex = ensureSetupCodex({ env, paths, platform, download: false, run, resolve }).binary;
|
|
174
|
-
inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex),
|
|
175
|
-
|
|
182
|
+
try { inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex), CODEX_MCP_TIMEOUT_MS)); }
|
|
183
|
+
catch (error) { throw codexInventoryFailure(error); }
|
|
184
|
+
if (!Array.isArray(inventory)) { inventory = undefined; throw new Error("Codex returned an invalid MCP inventory"); }
|
|
176
185
|
}, components);
|
|
177
186
|
const native = platform === "win32" || platform === "darwin";
|
|
178
187
|
for (const [name, worker] of [["claude-bridge", "claude-bridge.mjs"], ...(native ? [["codex-native-relay", "native-relay-companion.mjs"]] : []), ["antigravity-bridge", "antigravity-bridge.mjs"]]) {
|
|
179
188
|
configurationCheck(`Codex / ${name}`, () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
if (!inventory) throw new Error("Not checked because Codex CLI did not report its MCP servers; the registration may still be present");
|
|
190
|
+
const entry = inventory.find((server) => server?.name === name);
|
|
191
|
+
if (!entry) throw new Error("MCP registration is missing");
|
|
183
192
|
const details = assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
|
|
184
193
|
if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
|
|
185
194
|
return details;
|
|
@@ -228,7 +237,7 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
228
237
|
return { configured: components.every((item) => item.status === "configured"), liveVerified: false, components };
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
240
|
+
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin, onProgress = () => {} } = {}) {
|
|
232
241
|
const options = setupArguments(args);
|
|
233
242
|
if (options.help) return { help: `Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nUpdate the package and apply managed settings in an interactive terminal with: ${platform === "win32" ? "npx.cmd" : "npx"} -y @minhspark/codex-mcp-bridge@latest\nThe updater installs the package with lifecycle scripts disabled, then directly configures Claude Desktop, Claude Code, Codex, Antigravity and the independent worker relay. CODEX_BRIDGE_SKIP_SETUP=1 opts out. Saved configuration, background relay health and loaded native app connections are verified separately.` };
|
|
234
243
|
if (options.postinstall && !automaticSetupEnabled(env)) return { skipped: true, reason: "Automatic configuration runs only for a global npm install; use codex-bridge-setup for manual setup." };
|
|
@@ -243,6 +252,7 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
243
252
|
const base = setupEnvironment(env, paths, runtime.binary);
|
|
244
253
|
const components = [];
|
|
245
254
|
const install = (name, script, arguments_, environment = base) => {
|
|
255
|
+
onProgress(name);
|
|
246
256
|
try {
|
|
247
257
|
const output = run(process.execPath, [path.join(root, "scripts", script), ...arguments_], environment, 180000);
|
|
248
258
|
let details = {};
|
|
@@ -265,6 +275,7 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
265
275
|
install("Codex / claude-bridge", "install-codex-mcp.mjs", ["--setup"]);
|
|
266
276
|
if (code) install("Antigravity / all components", "install-antigravity-bridge.mjs", ["--all", "--setup"]);
|
|
267
277
|
install("Independent background relay", "setup-background-relay.mjs", ["--json"]);
|
|
278
|
+
onProgress("Verifying saved settings");
|
|
268
279
|
const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
|
|
269
280
|
return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
|
|
270
281
|
cliInstalled: runtime.installed, components, verification: inspection.components,
|