@minhspark/codex-mcp-bridge 1.13.9 → 1.15.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 +47 -0
- package/README.md +53 -24
- package/package.json +6 -6
- package/scripts/check.mjs +6 -1
- package/scripts/install-claude-desktop.mjs +22 -7
- package/scripts/install-codex-mcp.mjs +8 -5
- package/scripts/install-native-relay.mjs +15 -9
- package/src/bridge-account-context.mjs +77 -0
- package/src/claude-bridge-launcher.mjs +3 -0
- package/src/claude-bridge.mjs +91 -28
- package/src/claude-desktop-context.mjs +11 -6
- package/src/claude-sender-context.mjs +206 -0
- package/src/claude-session-router.mjs +92 -0
- package/src/codex-account-context.mjs +116 -0
- package/src/codex-bridge-launcher.mjs +3 -0
- package/src/codex-mcp-registration.mjs +14 -9
- package/src/desktop-account-context.mjs +140 -0
- package/src/desktop-task-receipts.mjs +3 -1
- package/src/index.mjs +91 -9
- package/src/mcp-supervisor.mjs +326 -0
- package/src/native-relay-companion.mjs +173 -34
- package/src/native-relay-launcher.mjs +3 -0
- package/src/native-relay.mjs +234 -50
- package/src/peer-protocol.mjs +120 -17
- package/src/platform.mjs +4 -1
- package/src/release-snapshot.mjs +198 -0
- package/src/reload-control.mjs +102 -0
- package/src/reply-forwarder.mjs +43 -0
- package/src/thread-delivery.mjs +27 -11
package/src/native-relay.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { execFile } from "node:child_process";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
8
|
import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir } from "./platform.mjs";
|
|
9
|
+
import { assertAccountIdentity } from "./bridge-account-context.mjs";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The Codex Desktop app owns the per-thread writer lock of every thread it has
|
|
@@ -35,6 +36,7 @@ import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir } from "./platform.mjs";
|
|
|
35
36
|
export const NATIVE_DISPATCH_METHOD = "tools/call";
|
|
36
37
|
|
|
37
38
|
export const RELAY_PROTOCOL_VERSION = 1;
|
|
39
|
+
export const ACCOUNT_RELAY_PROTOCOL_VERSION = 2;
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* A relay frame carries one chat message, so a megabyte-scale line is either a
|
|
@@ -70,6 +72,28 @@ export function desktopTaskSocketPath(env = process.env) {
|
|
|
70
72
|
return env.CODEX_NATIVE_RELAY_SOCKET ?? `${relaySocketPath(env)}-desktop-tasks`;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
export function accountRelaySocketPath(env = process.env) {
|
|
76
|
+
return `${relaySocketPath(env)}-accounts-v2`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function relayAccountContext(value) {
|
|
80
|
+
if (value === undefined) return undefined;
|
|
81
|
+
if (!exactObject(value, ["claude", "codex"]) || Object.keys(value).length !== 2 || [value.claude, value.codex].some((item) => typeof item !== "string" || !/^[a-f0-9]{64}$/.test(item))) {
|
|
82
|
+
throw beforeWriteError(new NativeRelayError("A bound relay request requires exact Claude and Codex account fingerprints", "RELAY_BAD_REQUEST"));
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze({ claude: value.claude, codex: value.codex });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function beforeWriteError(error) {
|
|
88
|
+
const failure = new NativeRelayError(error?.message ?? "Relay preflight failed before dispatch", typeof error?.code === "string" ? error.code : "RELAY_PREFLIGHT_FAILED", { reachedCompanion: false });
|
|
89
|
+
failure.sent = false;
|
|
90
|
+
return failure;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateBeforeSend(beforeSend) {
|
|
94
|
+
if (beforeSend !== undefined && typeof beforeSend !== "function") throw beforeWriteError(new NativeRelayError("Relay beforeSend must be a function", "RELAY_BAD_REQUEST"));
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
export function desktopTasksConfigured(env = process.env) {
|
|
74
98
|
return env.CODEX_BRIDGE_DESKTOP_TASKS !== undefined
|
|
75
99
|
? env.CODEX_BRIDGE_DESKTOP_TASKS === "1"
|
|
@@ -394,26 +418,141 @@ async function readParentCommandLine(parentPid, platform) {
|
|
|
394
418
|
return stdout.trim();
|
|
395
419
|
}
|
|
396
420
|
|
|
421
|
+
export function nativeToolsPipeCandidatesFromWindowsSnapshot(snapshot, { parentPid, localAppData = process.env.LOCALAPPDATA } = {}) {
|
|
422
|
+
if (!Number.isSafeInteger(parentPid) || parentPid <= 0 || !Array.isArray(snapshot?.ancestors)
|
|
423
|
+
|| snapshot.ancestors.length < 2 || snapshot.ancestors.length > 4 || !Array.isArray(snapshot.pipes)) return null;
|
|
424
|
+
const ancestors = snapshot.ancestors;
|
|
425
|
+
let expectedPid = parentPid;
|
|
426
|
+
const visited = new Set();
|
|
427
|
+
for (const ancestor of ancestors) {
|
|
428
|
+
if (!Number.isSafeInteger(ancestor?.pid) || ancestor.pid !== expectedPid || visited.has(ancestor.pid)
|
|
429
|
+
|| typeof ancestor.executablePath !== "string" || /[\r\n\0]/.test(ancestor.executablePath)) return null;
|
|
430
|
+
visited.add(ancestor.pid);
|
|
431
|
+
expectedPid = ancestor.parentPid;
|
|
432
|
+
}
|
|
433
|
+
const desktop = ancestors.at(-1);
|
|
434
|
+
const appServer = ancestors.at(-2);
|
|
435
|
+
const desktopPath = path.win32.normalize(desktop.executablePath);
|
|
436
|
+
if (!/^[a-z]:\\Program Files\\WindowsApps\\OpenAI\.Codex_[0-9.]+_(?:x64|arm64)__2p2nqsd0c76g0\\app\\ChatGPT\.exe$/i.test(desktopPath)) return null;
|
|
437
|
+
if (ancestors.slice(0, -2).some((ancestor) => path.win32.basename(ancestor.executablePath).toLowerCase() !== "node.exe")) return null;
|
|
438
|
+
const serverPath = path.win32.normalize(appServer.executablePath);
|
|
439
|
+
const bundledServer = path.win32.join(path.win32.dirname(desktopPath), "resources", "codex.exe");
|
|
440
|
+
const downloadedRoot = localAppData ? path.win32.join(localAppData, "OpenAI", "Codex", "bin") : null;
|
|
441
|
+
const downloadedRelative = downloadedRoot ? path.win32.relative(downloadedRoot, serverPath) : "";
|
|
442
|
+
if (serverPath.toLowerCase() !== bundledServer.toLowerCase() && !/^[a-f0-9]{8,64}\\codex\.exe$/i.test(downloadedRelative)) return null;
|
|
443
|
+
if (typeof appServer.commandLine !== "string" || /[\r\n\0]/.test(appServer.commandLine)) return null;
|
|
444
|
+
const args = splitDesktopCommandLine(appServer.commandLine, "win32");
|
|
445
|
+
if (!args.length || path.win32.normalize(args[0]).toLowerCase() !== serverPath.toLowerCase()) return null;
|
|
446
|
+
let command = null;
|
|
447
|
+
for (let index = 1; index < args.length; index++) {
|
|
448
|
+
if (args[index] === "-c" || args[index] === "--config") index++;
|
|
449
|
+
else if (!args[index].startsWith("-")) { command = args[index]; break; }
|
|
450
|
+
}
|
|
451
|
+
if (command !== "app-server") return null;
|
|
452
|
+
const candidates = snapshot.pipes.filter((pipe) => pipe?.serverPid === desktop.pid
|
|
453
|
+
&& typeof pipe.path === "string" && /^\\\\\.\\pipe\\codex-browser-use-[a-z0-9-]+$/i.test(pipe.path));
|
|
454
|
+
const paths = [...new Set(candidates.map((pipe) => pipe.path))].sort();
|
|
455
|
+
return paths;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function readWindowsNativePipeSnapshot(parentPid) {
|
|
459
|
+
if (!Number.isSafeInteger(parentPid) || parentPid <= 0) return null;
|
|
460
|
+
const powershell = path.win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
461
|
+
const script = String.raw`
|
|
462
|
+
$ErrorActionPreference = 'Stop'
|
|
463
|
+
$ancestors = @()
|
|
464
|
+
$nextProcessId = ${parentPid}
|
|
465
|
+
for ($depth = 0; $depth -lt 4; $depth++) {
|
|
466
|
+
$ancestor = Get-CimInstance Win32_Process -Filter "ProcessId=$nextProcessId"
|
|
467
|
+
if (!$ancestor) { break }
|
|
468
|
+
$ancestors += @{ pid = [int]$ancestor.ProcessId; parentPid = [int]$ancestor.ParentProcessId; executablePath = $ancestor.ExecutablePath; commandLine = $ancestor.CommandLine }
|
|
469
|
+
if ($ancestor.Name -ieq 'ChatGPT.exe') { break }
|
|
470
|
+
if ($ancestor.Name -ine 'node.exe' -and $ancestor.Name -ine 'codex.exe') { break }
|
|
471
|
+
$nextProcessId = [int]$ancestor.ParentProcessId
|
|
472
|
+
}
|
|
473
|
+
$pipes = @()
|
|
474
|
+
if ($ancestors.Count -ge 2 -and $ancestor.Name -ieq 'ChatGPT.exe') {
|
|
475
|
+
Add-Type -TypeDefinition @'
|
|
476
|
+
using System;
|
|
477
|
+
using System.Runtime.InteropServices;
|
|
478
|
+
using Microsoft.Win32.SafeHandles;
|
|
479
|
+
public static class CodexNativePipeOwner {
|
|
480
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
481
|
+
public static extern bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId);
|
|
482
|
+
}
|
|
483
|
+
'@
|
|
484
|
+
$names = @([System.IO.Directory]::GetFiles('\\.\pipe\') | Where-Object { $_ -match '^\\\\\.\\pipe\\codex-browser-use-[a-zA-Z0-9-]+$' } | Sort-Object | Select-Object -First 128)
|
|
485
|
+
foreach ($pipePath in $names) {
|
|
486
|
+
$client = $null
|
|
487
|
+
try {
|
|
488
|
+
$client = [System.IO.Pipes.NamedPipeClientStream]::new('.', $pipePath.Substring(9), [System.IO.Pipes.PipeDirection]::InOut, [System.IO.Pipes.PipeOptions]::Asynchronous)
|
|
489
|
+
$client.Connect(30)
|
|
490
|
+
$serverProcessId = [uint32]0
|
|
491
|
+
if ([CodexNativePipeOwner]::GetNamedPipeServerProcessId($client.SafePipeHandle, [ref]$serverProcessId) -and $serverProcessId -eq $ancestor.ProcessId) {
|
|
492
|
+
$pipes += @{ path = $pipePath; serverPid = [int]$serverProcessId }
|
|
493
|
+
}
|
|
494
|
+
} catch {} finally { if ($client) { $client.Dispose() } }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
@{ ancestors = @($ancestors); pipes = @($pipes) } | ConvertTo-Json -Depth 4 -Compress
|
|
498
|
+
`;
|
|
499
|
+
const { stdout } = await execFileAsync(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
|
|
500
|
+
timeout: 5000, maxBuffer: 128 * 1024, windowsHide: true,
|
|
501
|
+
});
|
|
502
|
+
return stdout.trim() ? JSON.parse(stdout.trim()) : null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function probeWindowsNativeToolsPipe(socketPath, { env, timeoutMs }) {
|
|
506
|
+
const executorThreadId = resolveRelayThreadId(env).threadId;
|
|
507
|
+
const client = new NativeToolsClient({ env, socketPath, timeoutMs });
|
|
508
|
+
const timer = globalThis.setTimeout(() => client.close(), timeoutMs);
|
|
509
|
+
try {
|
|
510
|
+
const nativeResult = await client.dispatchDesktop({ executorThreadId, operation: "list_projects", arguments: {} });
|
|
511
|
+
return Array.isArray(decodeNativeToolResult(nativeResult)?.projects);
|
|
512
|
+
} finally {
|
|
513
|
+
globalThis.clearTimeout(timer);
|
|
514
|
+
client.close();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
397
518
|
export async function resolveNativeToolsPipePath({
|
|
398
519
|
env = process.env,
|
|
399
520
|
parentPid = process.ppid,
|
|
400
521
|
platform = process.platform,
|
|
401
522
|
readParentCommandLine: readParent = readParentCommandLine,
|
|
523
|
+
readWindowsSnapshot = readWindowsNativePipeSnapshot,
|
|
524
|
+
probeWindowsPipe = probeWindowsNativeToolsPipe,
|
|
525
|
+
windowsDiscoveryTimeoutMs = 7000,
|
|
402
526
|
} = {}) {
|
|
403
527
|
if (env.CODEX_APP_TOOLS_PIPE_PATH) return env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
404
528
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
529
|
+
const inherited = nativeToolsPipeFromCommandLine(await readParent(parentPid, platform), { platform });
|
|
530
|
+
if (inherited) return inherited;
|
|
531
|
+
} catch {}
|
|
532
|
+
if (platform !== "win32") return null;
|
|
533
|
+
const deadline = Date.now() + Math.max(1, Math.min(7000, Number(windowsDiscoveryTimeoutMs) || 7000));
|
|
534
|
+
try {
|
|
535
|
+
const candidates = nativeToolsPipeCandidatesFromWindowsSnapshot(await readWindowsSnapshot(parentPid), { parentPid, localAppData: env.LOCALAPPDATA }) ?? [];
|
|
536
|
+
for (const candidate of candidates) {
|
|
537
|
+
const remaining = deadline - Date.now();
|
|
538
|
+
if (remaining <= 0) break;
|
|
539
|
+
try {
|
|
540
|
+
if (await probeWindowsPipe(candidate, { env, timeoutMs: Math.min(750, remaining) })) return candidate;
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
407
543
|
return null;
|
|
408
|
-
}
|
|
544
|
+
} catch { return null; }
|
|
409
545
|
}
|
|
410
546
|
|
|
411
547
|
export class NativeToolsClient {
|
|
412
|
-
constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }) } = {}) {
|
|
548
|
+
constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }), assertAccount = assertAccountIdentity } = {}) {
|
|
413
549
|
this.env = env;
|
|
414
550
|
this.socketPath = socketPath;
|
|
551
|
+
this.explicitSocketPath = socketPath || null;
|
|
552
|
+
this.hasDiscoveredSocket = false;
|
|
415
553
|
this.timeoutMs = timeoutMs;
|
|
416
554
|
this.resolveSocketPath = resolveSocketPath;
|
|
555
|
+
this.assertAccount = assertAccount;
|
|
417
556
|
this.socket = null;
|
|
418
557
|
this.connectingSocket = null;
|
|
419
558
|
this.connecting = null;
|
|
@@ -432,6 +571,7 @@ export class NativeToolsClient {
|
|
|
432
571
|
throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
|
|
433
572
|
}
|
|
434
573
|
this.socketPath = socketPath;
|
|
574
|
+
if (socketPath && !this.explicitSocketPath) this.hasDiscoveredSocket = true;
|
|
435
575
|
if (!this.socketPath) {
|
|
436
576
|
throw new NativeRelayError("CODEX_APP_TOOLS_PIPE_PATH is missing from the environment and parent Desktop app-server configuration; launch the companion from Codex Desktop", "NATIVE_PIPE_UNAVAILABLE");
|
|
437
577
|
}
|
|
@@ -440,14 +580,17 @@ export class NativeToolsClient {
|
|
|
440
580
|
this.connectingSocket = socket;
|
|
441
581
|
let buffer = Buffer.alloc(0);
|
|
442
582
|
let connected = false;
|
|
583
|
+
let failed = false;
|
|
443
584
|
const timer = globalThis.setTimeout(() => {
|
|
444
|
-
|
|
445
|
-
socket.destroy();
|
|
585
|
+
fail(new NativeRelayError("Timed out connecting to the Codex Desktop native tools pipe", "NATIVE_PIPE_UNAVAILABLE"));
|
|
446
586
|
}, this.timeoutMs);
|
|
447
587
|
const fail = (err) => {
|
|
588
|
+
if (failed) return;
|
|
589
|
+
failed = true;
|
|
448
590
|
globalThis.clearTimeout(timer);
|
|
449
591
|
if (!connected) reject(err);
|
|
450
592
|
if (this.socket === socket) this.socket = null;
|
|
593
|
+
if (!this.explicitSocketPath && this.connectionGeneration === generation) this.socketPath = null;
|
|
451
594
|
for (const pending of this.pending.values()) {
|
|
452
595
|
if (pending.socket === socket) pending.reject(err);
|
|
453
596
|
}
|
|
@@ -503,15 +646,17 @@ export class NativeToolsClient {
|
|
|
503
646
|
}
|
|
504
647
|
}
|
|
505
648
|
|
|
506
|
-
async dispatch(args) {
|
|
507
|
-
return this.#request(nativeDispatchParams(args));
|
|
649
|
+
async dispatch(args, options) {
|
|
650
|
+
return this.#request(nativeDispatchParams(args), options);
|
|
508
651
|
}
|
|
509
652
|
|
|
510
|
-
async dispatchDesktop(args) {
|
|
511
|
-
return this.#request(nativeDesktopOperationParams(args));
|
|
653
|
+
async dispatchDesktop(args, options) {
|
|
654
|
+
return this.#request(nativeDesktopOperationParams(args), options);
|
|
512
655
|
}
|
|
513
656
|
|
|
514
|
-
async #request(params) {
|
|
657
|
+
async #request(params, { beforeSend, accountContext } = {}) {
|
|
658
|
+
validateBeforeSend(beforeSend);
|
|
659
|
+
const accounts = relayAccountContext(accountContext);
|
|
515
660
|
const id = this.nextId++;
|
|
516
661
|
const payload = Buffer.from(JSON.stringify({
|
|
517
662
|
jsonrpc: "2.0",
|
|
@@ -522,31 +667,50 @@ export class NativeToolsClient {
|
|
|
522
667
|
if (payload.length > MAX_FRAME_BYTES) {
|
|
523
668
|
throw new NativeRelayError("Native dispatch exceeds the frame limit", "RELAY_MESSAGE_TOO_LARGE");
|
|
524
669
|
}
|
|
525
|
-
|
|
670
|
+
try {
|
|
671
|
+
await this.connect();
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw beforeWriteError(error);
|
|
674
|
+
}
|
|
526
675
|
const header = Buffer.alloc(4);
|
|
527
676
|
header.writeUInt32LE(payload.length);
|
|
528
677
|
return new Promise((resolve, reject) => {
|
|
678
|
+
const socket = this.socket;
|
|
679
|
+
let dispatched = false;
|
|
529
680
|
const finish = (fn, value) => {
|
|
530
681
|
if (!this.pending.delete(id)) return;
|
|
531
682
|
globalThis.clearTimeout(timer);
|
|
532
683
|
fn(value);
|
|
533
684
|
};
|
|
534
685
|
const timer = globalThis.setTimeout(() => {
|
|
535
|
-
finish(reject,
|
|
686
|
+
finish(reject, dispatched
|
|
687
|
+
? new NativeRelayError("Codex Desktop native dispatch timed out; delivery may have occurred", "NATIVE_DELIVERY_UNCONFIRMED")
|
|
688
|
+
: beforeWriteError(new NativeRelayError("Codex Desktop native send deadline elapsed before dispatch", "NATIVE_PIPE_UNAVAILABLE")));
|
|
536
689
|
}, this.timeoutMs);
|
|
537
690
|
this.pending.set(id, {
|
|
538
|
-
socket
|
|
691
|
+
socket,
|
|
539
692
|
resolve: (value) => finish(resolve, value),
|
|
540
|
-
reject: (err) => finish(reject, err),
|
|
693
|
+
reject: (err) => finish(reject, dispatched ? err : beforeWriteError(err)),
|
|
541
694
|
});
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
695
|
+
void (async () => {
|
|
696
|
+
try {
|
|
697
|
+
const checked = beforeSend?.();
|
|
698
|
+
if (checked && typeof checked.then === "function") await checked;
|
|
699
|
+
if (!this.pending.has(id)) return;
|
|
700
|
+
if (accounts) {
|
|
701
|
+
const verified = this.assertAccount(accounts);
|
|
702
|
+
if (verified && typeof verified.then === "function") await verified;
|
|
703
|
+
}
|
|
704
|
+
if (!this.pending.has(id)) return;
|
|
705
|
+
if (!socket || socket.destroyed || this.socket !== socket) throw new NativeRelayError("Native tools pipe closed before dispatch", "NATIVE_PIPE_UNAVAILABLE");
|
|
706
|
+
dispatched = true;
|
|
707
|
+
socket.write(Buffer.concat([header, payload]), (err) => {
|
|
708
|
+
if (err) finish(reject, new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED"));
|
|
709
|
+
});
|
|
710
|
+
} catch (err) {
|
|
711
|
+
finish(reject, dispatched ? new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED") : beforeWriteError(err));
|
|
712
|
+
}
|
|
713
|
+
})();
|
|
550
714
|
});
|
|
551
715
|
}
|
|
552
716
|
|
|
@@ -554,6 +718,7 @@ export class NativeToolsClient {
|
|
|
554
718
|
this.connectionGeneration++;
|
|
555
719
|
const socket = this.socket;
|
|
556
720
|
this.socket = null;
|
|
721
|
+
if (!this.explicitSocketPath) this.socketPath = null;
|
|
557
722
|
for (const pending of this.pending.values()) {
|
|
558
723
|
pending.reject(new NativeRelayError("Native tools client closed before confirming delivery", "NATIVE_DELIVERY_UNCONFIRMED"));
|
|
559
724
|
}
|
|
@@ -603,9 +768,10 @@ export function nativeRelayStatus(env = process.env) {
|
|
|
603
768
|
* socket cannot go stale while Codex Desktop restarts underneath it.
|
|
604
769
|
*/
|
|
605
770
|
export class NativeDesktopRelay {
|
|
606
|
-
constructor({ env = process.env, socketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
|
|
771
|
+
constructor({ env = process.env, socketPath = null, accountSocketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
|
|
607
772
|
this.env = env;
|
|
608
773
|
this.explicitSocketPath = socketPath;
|
|
774
|
+
this.accountSocketPath = accountSocketPath ?? socketPath ?? accountRelaySocketPath(env);
|
|
609
775
|
this.timeoutMs = timeoutMs;
|
|
610
776
|
this.log = log;
|
|
611
777
|
}
|
|
@@ -614,27 +780,31 @@ export class NativeDesktopRelay {
|
|
|
614
780
|
return this.explicitSocketPath ?? relaySocketPath(this.env);
|
|
615
781
|
}
|
|
616
782
|
|
|
617
|
-
status() {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
783
|
+
status({ accountContext } = {}) {
|
|
784
|
+
const socketPath = accountContext ? this.accountSocketPath : this.explicitSocketPath;
|
|
785
|
+
if (!socketPath) return nativeRelayStatus(this.env);
|
|
786
|
+
const status = nativeRelayStatus({ ...this.env, CODEX_NATIVE_RELAY_SOCKET: socketPath });
|
|
787
|
+
return { ...status, socketPath };
|
|
621
788
|
}
|
|
622
789
|
|
|
623
790
|
get available() {
|
|
624
791
|
return this.status().enabled;
|
|
625
792
|
}
|
|
626
793
|
|
|
627
|
-
async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs } = {}) {
|
|
628
|
-
const
|
|
629
|
-
|
|
794
|
+
async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
|
|
795
|
+
const accounts = relayAccountContext(accountContext);
|
|
796
|
+
const request = { v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, targetThreadId, message, ...(accounts ? { accountContext: accounts } : {}) };
|
|
797
|
+
return this.#request(request, timeoutMs, beforeSend);
|
|
630
798
|
}
|
|
631
799
|
|
|
632
|
-
async requestDesktop(operation, args, { timeoutMs = this.timeoutMs } = {}) {
|
|
800
|
+
async requestDesktop(operation, args, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
|
|
633
801
|
validateDesktopOperation(operation, args);
|
|
634
|
-
|
|
802
|
+
const accounts = relayAccountContext(accountContext);
|
|
803
|
+
return this.#request({ v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, operation, arguments: args, ...(accounts ? { accountContext: accounts } : {}) }, timeoutMs, beforeSend);
|
|
635
804
|
}
|
|
636
805
|
|
|
637
|
-
async #request(request, timeoutMs) {
|
|
806
|
+
async #request(request, timeoutMs, beforeSend) {
|
|
807
|
+
validateBeforeSend(beforeSend);
|
|
638
808
|
const line = `${JSON.stringify(request)}\n`;
|
|
639
809
|
if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
|
|
640
810
|
throw new NativeRelayError(
|
|
@@ -643,20 +813,21 @@ export class NativeDesktopRelay {
|
|
|
643
813
|
);
|
|
644
814
|
}
|
|
645
815
|
|
|
646
|
-
const response = await this.#roundTrip(line, timeoutMs);
|
|
647
|
-
if (response?.ok === true && response.v ===
|
|
816
|
+
const response = await this.#roundTrip(line, timeoutMs, beforeSend, request.accountContext ? this.accountSocketPath : this.socketPath);
|
|
817
|
+
if (response?.ok === true && response.v === request.v) {
|
|
648
818
|
if (request.operation && response.operation !== request.operation) throw new NativeRelayError("The relay answered a different Desktop operation; do not retry this request", "RELAY_BAD_RESPONSE", { reachedCompanion: true });
|
|
649
819
|
return response;
|
|
650
820
|
}
|
|
651
|
-
|
|
821
|
+
const error = new NativeRelayError(
|
|
652
822
|
response?.error?.message ?? "the Codex Desktop relay refused the message",
|
|
653
823
|
response?.error?.code ?? "NATIVE_DISPATCH_FAILED",
|
|
654
|
-
{ reachedCompanion:
|
|
824
|
+
{ reachedCompanion: response?.error?.sent !== false },
|
|
655
825
|
);
|
|
826
|
+
if (response?.error?.sent === false) error.sent = false;
|
|
827
|
+
throw error;
|
|
656
828
|
}
|
|
657
829
|
|
|
658
|
-
#roundTrip(line, timeoutMs) {
|
|
659
|
-
const socketPath = this.socketPath;
|
|
830
|
+
#roundTrip(line, timeoutMs, beforeSend, socketPath) {
|
|
660
831
|
return new Promise((resolve, reject) => {
|
|
661
832
|
let buffer = Buffer.alloc(0);
|
|
662
833
|
let settled = false;
|
|
@@ -675,18 +846,29 @@ export class NativeDesktopRelay {
|
|
|
675
846
|
() =>
|
|
676
847
|
finish(
|
|
677
848
|
reject,
|
|
678
|
-
new NativeRelayError(
|
|
849
|
+
dispatched ? new NativeRelayError(
|
|
679
850
|
`The Codex Desktop relay did not answer within ${timeoutMs}ms`,
|
|
680
851
|
"RELAY_TIMEOUT",
|
|
681
|
-
{ reachedCompanion:
|
|
682
|
-
),
|
|
852
|
+
{ reachedCompanion: true },
|
|
853
|
+
) : beforeWriteError(new NativeRelayError("The Codex Desktop relay send deadline elapsed before dispatch", "RELAY_UNREACHABLE")),
|
|
683
854
|
),
|
|
684
855
|
timeoutMs,
|
|
685
856
|
);
|
|
686
857
|
|
|
687
858
|
socket.on("connect", () => {
|
|
688
|
-
|
|
689
|
-
|
|
859
|
+
if (settled) return;
|
|
860
|
+
void (async () => {
|
|
861
|
+
try {
|
|
862
|
+
const checked = beforeSend?.();
|
|
863
|
+
if (checked && typeof checked.then === "function") await checked;
|
|
864
|
+
if (settled) return;
|
|
865
|
+
if (socket.destroyed) throw new NativeRelayError("The relay socket closed before dispatch", "RELAY_UNREACHABLE");
|
|
866
|
+
dispatched = true;
|
|
867
|
+
socket.write(line);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
finish(reject, dispatched ? new NativeRelayError(`Relay write failed: ${error.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true }) : beforeWriteError(error));
|
|
870
|
+
}
|
|
871
|
+
})();
|
|
690
872
|
});
|
|
691
873
|
socket.on("data", (chunk) => {
|
|
692
874
|
buffer = Buffer.concat([buffer, chunk]);
|
|
@@ -717,17 +899,19 @@ export class NativeDesktopRelay {
|
|
|
717
899
|
socket.on("error", (err) =>
|
|
718
900
|
finish(
|
|
719
901
|
reject,
|
|
720
|
-
|
|
902
|
+
dispatched
|
|
903
|
+
? new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true })
|
|
904
|
+
: beforeWriteError(new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_UNREACHABLE")),
|
|
721
905
|
),
|
|
722
906
|
);
|
|
723
907
|
socket.on("close", () =>
|
|
724
908
|
finish(
|
|
725
909
|
reject,
|
|
726
|
-
new NativeRelayError(
|
|
910
|
+
dispatched ? new NativeRelayError(
|
|
727
911
|
`The Codex Desktop relay at ${socketPath} closed before answering`,
|
|
728
|
-
|
|
729
|
-
{ reachedCompanion:
|
|
730
|
-
),
|
|
912
|
+
"RELAY_DELIVERY_UNCONFIRMED",
|
|
913
|
+
{ reachedCompanion: true },
|
|
914
|
+
) : beforeWriteError(new NativeRelayError(`The Codex Desktop relay at ${socketPath} closed before dispatch`, "RELAY_UNREACHABLE")),
|
|
731
915
|
),
|
|
732
916
|
);
|
|
733
917
|
});
|