@minhspark/codex-mcp-bridge 1.19.6 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  const REQUEST_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32,128})$/i;
5
- const PENDING = new Set(["awaiting_user", "launch_uncertain", "ambiguous", "awaiting_project_confirmation"]);
5
+ const PENDING = new Set(["awaiting_user", "awaiting_session", "submission_uncertain", "launch_uncertain", "ambiguous", "awaiting_project_confirmation"]);
6
6
  const STATUSES = new Set([...PENDING, "created", "abandoned"]);
7
7
  const MAX_REQUESTS = 32;
8
8
  const MAX_TASKS = 8192;
@@ -35,6 +35,8 @@ function augmentedPrompt(prompt, requestId) {
35
35
 
36
36
  function matchesInitialMessage(text, expected) {
37
37
  if (text === expected) return true;
38
+ const normalized = (value) => typeof value === "string" ? value.replace(/\r\n/g, "\n").split("\n").filter((line) => line !== "").join("\n") : null;
39
+ if (normalized(text) === normalized(expected)) return true;
38
40
  if (typeof text !== "string" || !/^<system-reminder>\r?\nThe user started this session without choosing a project folder,/.test(text)) return false;
39
41
  const closing = text.indexOf("</system-reminder>");
40
42
  if (closing < 0 || closing > 32768) return false;
@@ -42,7 +44,7 @@ function matchesInitialMessage(text, expected) {
42
44
  if (body.includes("<system-reminder") || body.includes("</system-reminder")) return false;
43
45
  const suffix = text.slice(closing + "</system-reminder>".length);
44
46
  const separator = /^\r?\n\r?\n/.exec(suffix);
45
- return separator !== null && suffix.slice(separator[0].length) === expected;
47
+ return separator !== null && normalized(suffix.slice(separator[0].length)) === normalized(expected);
46
48
  }
47
49
 
48
50
  function receipt(request) {
@@ -50,7 +52,7 @@ function receipt(request) {
50
52
  }
51
53
 
52
54
  export class ClaudeSessionCreation {
53
- constructor({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath = (directory) => {
55
+ constructor({ open, submit, trust, settle = async () => {}, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath = (directory) => {
54
56
  const canonical = fs.realpathSync.native(directory);
55
57
  if (!fs.statSync(canonical).isDirectory()) throw new Error("Not a directory");
56
58
  return canonical;
@@ -58,7 +60,7 @@ export class ClaudeSessionCreation {
58
60
  for (const [name, dependency] of Object.entries({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath })) {
59
61
  if (typeof dependency !== "function") throw new TypeError(`${name} must be a function`);
60
62
  }
61
- Object.assign(this, { open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath, platform });
63
+ Object.assign(this, { open, submit, trust, settle, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath, platform });
62
64
  this.requests = new Map();
63
65
  this.queue = Promise.resolve();
64
66
  }
@@ -89,7 +91,7 @@ export class ClaudeSessionCreation {
89
91
  return pending;
90
92
  }
91
93
 
92
- async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen }) {
94
+ async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen, autoSubmit = false, trustProject = false }) {
93
95
  if (!REQUEST_ID.test(requestId ?? "") || typeof requestId !== "string") fail("CLAUDE_CREATION_REQUEST_INVALID", "requestId must be a UUID or 32–128 hexadecimal characters.");
94
96
  if (!string(senderThreadId, 256)) fail("CLAUDE_CREATION_SENDER_INVALID", "A verified sender thread identity is required.");
95
97
  const identity = await this.checkAccount(account);
@@ -100,11 +102,17 @@ export class ClaudeSessionCreation {
100
102
  if (!sameAccount(identity, prior.account) || prior.senderThreadId !== senderThreadId || !this.sameCwd(prior.cwd, canonical) || prior.prompt !== prompt) {
101
103
  fail("CLAUDE_CREATION_REQUEST_CONFLICT", "This requestId is already bound to a different creation payload.");
102
104
  }
105
+ if (autoSubmit && !prior.submissionAttempted && ["awaiting_user", "launch_uncertain"].includes(prior.status)) {
106
+ const observed = await this.inspect(requestId, { account, senderThreadId });
107
+ if (observed.promptSubmitted) return observed;
108
+ return this.completeRequest(prior, { account, senderThreadId, beforeOpen, trustProject });
109
+ }
103
110
  return receipt(prior);
104
111
  }
105
112
  if (this.requests.size >= MAX_REQUESTS) fail("CLAUDE_CREATION_LIMIT", "The creation receipt limit has been reached; existing requests remain available for inspection.");
106
113
  for (const existing of this.requests.values()) {
107
- if (sameAccount(existing.account, identity) && PENDING.has(existing.status)) fail("CLAUDE_CREATION_PENDING", "A creation request is already pending for this account; inspect or explicitly abandon that request before opening another project in the shared composer.");
114
+ const composerReleased = existing.status === "awaiting_project_confirmation" && existing.submissionObserved === true;
115
+ if (sameAccount(existing.account, identity) && PENDING.has(existing.status) && !composerReleased) fail("CLAUDE_CREATION_PENDING", "A creation request is already pending for this account; inspect or explicitly abandon that request before opening another project in the shared composer.");
108
116
  }
109
117
  const tasks = await this.listTasks(account);
110
118
  if (!Array.isArray(tasks) || tasks.length > MAX_TASKS || tasks.some((task) => !string(task?.taskId, 256) || (task.fileTaskId !== undefined && task.fileTaskId !== task.taskId))) fail("CLAUDE_CREATION_TASKS_INVALID", "The existing native task baseline cannot be verified.");
@@ -114,6 +122,8 @@ export class ClaudeSessionCreation {
114
122
  try { if (this.sameCwd(await this.canonical(task.cwd), canonical)) existingProject = true; } catch {}
115
123
  }
116
124
  if (!existingProject) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The canonical directory must already belong to a nonarchived Claude Desktop task.");
125
+ if (autoSubmit && typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
126
+ if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
117
127
  const url = `claude://code/new?q=${encodeURIComponent(initialMessage)}&folder=${encodeURIComponent(canonical)}`;
118
128
  if (url.length > 30000) fail("CLAUDE_CREATION_URL_TOO_LONG", "The encoded Claude Desktop link exceeds 30000 characters.");
119
129
  await this.checkAccount(account, identity);
@@ -125,14 +135,49 @@ export class ClaudeSessionCreation {
125
135
  this.requests.set(requestId, request);
126
136
  try {
127
137
  await this.open(url);
138
+ if (autoSubmit) await this.settle();
128
139
  request.status = "awaiting_user";
129
140
  request.reason = "Claude Desktop was asked to prefill a new session. Confirm the folder if prompted and press Send; no prompt has been submitted by the bridge.";
130
141
  } catch {
131
142
  request.reason = "The Desktop launcher failed or returned an uncertain outcome. This request will not be launched again; inspect it to check whether the session was created.";
132
143
  }
144
+ if (request.status === "awaiting_user" && autoSubmit) {
145
+ return this.completeRequest(request, { account, senderThreadId, beforeOpen, trustProject });
146
+ }
133
147
  return receipt(request);
134
148
  }
135
149
 
150
+ async completeRequest(request, { account, senderThreadId, beforeOpen, trustProject }) {
151
+ if (typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
152
+ if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
153
+ await this.checkAccount(account, request.account);
154
+ if (beforeOpen) await beforeOpen();
155
+ if (trustProject) {
156
+ const trusted = await this.trust({ cwd: request.cwd, prompt: request.initialMessage });
157
+ if (!["trusted", "absent"].includes(trusted?.status)) {
158
+ request.status = "awaiting_user";
159
+ request.reason = trusted?.reason ?? "Native workspace trust is not confirmed; inspect the exact project composer.";
160
+ return receipt(request);
161
+ }
162
+ if (trusted.status === "trusted") await this.settle();
163
+ }
164
+ await this.checkAccount(account, request.account);
165
+ if (beforeOpen) await beforeOpen();
166
+ request.submissionAttempted = true;
167
+ request.status = "submission_uncertain";
168
+ request.reason = "Native submission was attempted; inspect this request before retrying. The bridge will not press Send again.";
169
+ try {
170
+ const submission = await this.submit({ prompt: request.initialMessage, cwd: request.cwd, beforeSubmit: async () => {
171
+ await this.checkAccount(account, request.account);
172
+ if (beforeOpen) await beforeOpen();
173
+ } });
174
+ if (submission?.status === "blocked" && submission.submissionAttempted === false) delete request.submissionAttempted;
175
+ request.status = submission?.status === "submitted" ? "awaiting_session" : submission?.status === "blocked" ? "awaiting_user" : "submission_uncertain";
176
+ request.reason = submission?.reason ?? "Inspect the original native composer and creation receipt; submission has not been verified.";
177
+ } catch {}
178
+ return this.inspect(request.requestId, { account, senderThreadId });
179
+ }
180
+
136
181
  async inspect(requestId, { account, senderThreadId }) {
137
182
  const request = this.requests.get(requestId);
138
183
  if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
@@ -206,10 +251,11 @@ export class ClaudeSessionCreation {
206
251
  const hasSession = ["created", "awaiting_project_confirmation"].includes(request.status) || request.sessionId !== undefined;
207
252
  if (hasSession && (!string(request.sessionId, 256) || !string(request.taskId, 256) || request.baseline.includes(request.taskId) || (request.title !== null && !string(request.title, 4096)))) fail("CLAUDE_CREATION_STATE_INVALID", "A submitted creation receipt is invalid.");
208
253
  if (request.submissionObserved !== undefined && typeof request.submissionObserved !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission marker is invalid.");
254
+ if (request.submissionAttempted !== undefined && typeof request.submissionAttempted !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission attempt is invalid.");
209
255
  if ((request.status === "awaiting_project_confirmation" || request.observedCwd !== undefined) && (!string(request.observedCwd, 8192) || !paths.isAbsolute(request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A retained observed directory is invalid.");
210
256
  if (request.status === "awaiting_project_confirmation" && (request.submissionObserved !== true || this.sameCwd(request.cwd, request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A pending project confirmation is invalid.");
211
257
  if (request.ambiguitySeen !== undefined && typeof request.ambiguitySeen !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained ambiguity marker is invalid.");
212
- restored.set(request.requestId, structuredClone({ requestId: request.requestId, cwd: request.cwd, prompt: request.prompt, initialMessage: request.initialMessage, account: { fingerprint: request.account.fingerprint, root: request.account.root }, senderThreadId: request.senderThreadId, baseline: request.baseline, status: request.status, reason: request.reason, ...(request.ambiguitySeen || request.status === "ambiguous" ? { ambiguitySeen: true } : {}), ...(request.submissionObserved ? { submissionObserved: true } : {}), ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(hasSession ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) }));
258
+ restored.set(request.requestId, structuredClone({ requestId: request.requestId, cwd: request.cwd, prompt: request.prompt, initialMessage: request.initialMessage, account: { fingerprint: request.account.fingerprint, root: request.account.root }, senderThreadId: request.senderThreadId, baseline: request.baseline, status: request.status, reason: request.reason, ...(request.submissionAttempted ? { submissionAttempted: true } : {}), ...(request.ambiguitySeen || request.status === "ambiguous" ? { ambiguitySeen: true } : {}), ...(request.submissionObserved ? { submissionObserved: true } : {}), ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(hasSession ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) }));
213
259
  }
214
260
  this.requests = restored;
215
261
  }
@@ -1,5 +1,6 @@
1
1
  import { assertClaudeSessionCwd } from "./peer-protocol.mjs";
2
2
  import { preflightFailure } from "./recipient-preflight.mjs";
3
+ import { setTimeout as delay } from "node:timers/promises";
3
4
 
4
5
  function assertCwd(session, expectedCwd) {
5
6
  try {
@@ -23,6 +24,36 @@ function uniqueLiveSessions(sessions) {
23
24
  return [...unique.values()];
24
25
  }
25
26
 
27
+ export async function recoverClaudeDesktopSession(args, { listTasks, listSessions, open, beforeOpen, sleep = delay, timeoutMs = 15000 }) {
28
+ try { return { session: resolveClaudeDesktopSession(args) }; }
29
+ catch (error) {
30
+ if (error.preflight?.code !== "CLAUDE_SESSION_NOT_FOUND" || args.target !== "auto" || !args.expectedTaskId) throw error;
31
+ }
32
+ const assertTask = async () => {
33
+ await beforeOpen();
34
+ const tasks = await listTasks();
35
+ if (!Array.isArray(tasks) || tasks.length > 8192) throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", "Saved Desktop tasks cannot be verified.");
36
+ const matches = tasks.filter((task) => task.taskId === args.expectedTaskId);
37
+ if (matches.length !== 1 || matches[0].fileTaskId !== matches[0].taskId || matches[0].isArchived !== false) {
38
+ throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", "The exact saved Desktop task is missing, archived, or ambiguous; no replacement was opened.");
39
+ }
40
+ assertCwd(matches[0], args.expectedCwd);
41
+ return matches[0];
42
+ };
43
+ const task = await assertTask();
44
+ try { await open(task.taskId); }
45
+ catch { return { recovery: { status: "launch_uncertain", sent: false, taskId: task.taskId, cwd: args.expectedCwd, reason: "The exact Desktop task could not be confirmed open. Inspect this task before retrying; no message was sent." } }; }
46
+ const deadline = Date.now() + timeoutMs;
47
+ for (;;) {
48
+ await assertTask();
49
+ try { return { session: resolveClaudeDesktopSession({ ...args, sessions: await listSessions() }) }; }
50
+ catch (error) { if (error.preflight?.code !== "CLAUDE_SESSION_NOT_FOUND") throw error; }
51
+ if (Date.now() >= deadline) break;
52
+ await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
53
+ }
54
+ return { recovery: { status: "awaiting_session", sent: false, taskId: task.taskId, cwd: args.expectedCwd, reason: "The exact saved task was opened in Claude Desktop and is reconnecting. Retry target auto with this expectedTaskId; no message has been sent." } };
55
+ }
56
+
26
57
  export function sameClaudeDesktopRecipient(selected, current) {
27
58
  return Boolean(
28
59
  selected && current &&
package/src/index.mjs CHANGED
@@ -35,7 +35,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
35
35
 
36
36
  exitForVersionRequest(import.meta.url);
37
37
 
38
- const VERSION = "1.19.6";
38
+ const VERSION = "1.20.0";
39
39
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
40
40
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
41
41
 
@@ -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.19.6";
37
+ const VERSION = "1.20.0";
38
38
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
39
39
 
40
40
  function errorResponse(code, message, sent) {
package/src/platform.mjs CHANGED
@@ -471,14 +471,16 @@ export async function openThreadInCodexApp(threadId, { activate = true } = {}) {
471
471
 
472
472
  export async function openClaudeCodeComposer(url, { platform = process.platform, env = process.env, run = execFileAsync } = {}) {
473
473
  const parsed = new URL(url);
474
- if (parsed.protocol !== "claude:" || parsed.hostname !== "code" || parsed.pathname !== "/new" || parsed.username || parsed.password || parsed.port || parsed.hash
475
- || [...parsed.searchParams.keys()].some((key) => !["q", "folder"].includes(key))) {
474
+ const keys = parsed.pathname === "/continue" ? ["session"] : ["q", "folder"];
475
+ if (parsed.protocol !== "claude:" || parsed.hostname !== "code" || !["/new", "/continue"].includes(parsed.pathname) || parsed.username || parsed.password || parsed.port || parsed.hash
476
+ || [...parsed.searchParams.keys()].some((key) => !keys.includes(key) || parsed.searchParams.getAll(key).length !== 1)
477
+ || (parsed.pathname === "/continue" && !/^local_[0-9a-f-]{36}$/i.test(parsed.searchParams.get("session") ?? ""))) {
476
478
  throw new Error("Invalid Claude Desktop Code creation link");
477
479
  }
478
480
  if (platform === "win32") {
479
481
  const shell = path.win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
480
482
  const encodedUrl = Buffer.from(url, "utf8").toString("base64");
481
- const script = `$ErrorActionPreference='Stop'; $creationUrl=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedUrl}')); Start-Process -FilePath $creationUrl -ErrorAction Stop`;
483
+ const script = String.raw`$ErrorActionPreference='Stop'; $creationUrl=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedUrl}')); try { Start-Process -FilePath $creationUrl -ErrorAction Stop } catch { $desktopPaths=@(Get-Process claude -ErrorAction SilentlyContinue | ForEach-Object { $_.Path } | Where-Object { $_ -match '\\(?:AnthropicClaude\\app-[^\\]+|WindowsApps\\Claude_[^\\]+\\app)\\claude\.exe$' -and (Test-Path -LiteralPath $_) } | Select-Object -Unique); if ($desktopPaths.Count -eq 0 -and $env:LOCALAPPDATA) { $desktopPaths=@(Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'AnthropicClaude') -Directory -Filter 'app-*' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | ForEach-Object { Join-Path $_.FullName 'claude.exe' } | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1) }; if ($desktopPaths.Count -ne 1) { throw }; Start-Process -FilePath $desktopPaths[0] -ArgumentList ('"'+$creationUrl+'"') -WindowStyle Hidden -ErrorAction Stop }`;
482
484
  await run(shell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 10000 });
483
485
  return;
484
486
  }
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  export function createRuntimeState({ directory = path.dirname(fileURLToPath(import.meta.url)), configuration = () => null } = {}) {
7
7
  const fingerprint = () => {
8
8
  const hash = createHash("sha256");
9
- for (const file of ["../package.json", ...readdirSync(directory).filter((file) => file.endsWith(".mjs")).sort()]) {
9
+ for (const file of ["../package.json", ...readdirSync(directory).filter((file) => /\.(?:mjs|ps1)$/.test(file)).sort()]) {
10
10
  hash.update(file).update("\0").update(readFileSync(path.join(directory, file))).update("\0");
11
11
  }
12
12
  return hash.digest("hex");