@minhspark/codex-mcp-bridge 1.27.1 → 2.0.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.
@@ -1,306 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
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", "awaiting_session", "submission_uncertain", "launch_uncertain", "ambiguous", "awaiting_project_confirmation"]);
6
- const STATUSES = new Set([...PENDING, "created", "abandoned"]);
7
- const MAX_REQUESTS = 32;
8
- const MAX_TASKS = 8192;
9
-
10
- function fail(code, message) {
11
- throw Object.assign(new Error(message), { code });
12
- }
13
-
14
- function string(value, max = 1024) {
15
- return typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\0");
16
- }
17
-
18
- function accountIdentity(account) {
19
- if (account?.status !== "verified" || !string(account.fingerprint) || !string(account.root, 8192)) {
20
- fail("CLAUDE_ACCOUNT_UNVERIFIED", "The current Claude Desktop account must be verified.");
21
- }
22
- return { fingerprint: account.fingerprint, root: account.root };
23
- }
24
-
25
- function sameAccount(left, right) {
26
- return left.fingerprint === right.fingerprint && left.root === right.root;
27
- }
28
-
29
- function augmentedPrompt(prompt, requestId) {
30
- if (!string(prompt, 14000) || !prompt.trim()) fail("CLAUDE_CREATION_PROMPT_INVALID", "Provide a nonempty prompt of at most 14000 characters.");
31
- const submitted = `${prompt.trim()}\n\n[Codex creation request: ${requestId}]`;
32
- if (submitted.length > 14000) fail("CLAUDE_CREATION_PROMPT_TOO_LONG", "The prompt including its correlation marker exceeds 14000 characters.");
33
- return submitted;
34
- }
35
-
36
- function matchesInitialMessage(text, expected) {
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;
40
- if (typeof text !== "string" || !/^<system-reminder>\r?\nThe user started this session without choosing a project folder,/.test(text)) return false;
41
- const closing = text.indexOf("</system-reminder>");
42
- if (closing < 0 || closing > 32768) return false;
43
- const body = text.slice("<system-reminder>".length, closing);
44
- if (body.includes("<system-reminder") || body.includes("</system-reminder")) return false;
45
- const suffix = text.slice(closing + "</system-reminder>".length);
46
- const separator = /^\r?\n\r?\n/.exec(suffix);
47
- return separator !== null && normalized(suffix.slice(separator[0].length)) === normalized(expected);
48
- }
49
-
50
- function receipt(request) {
51
- const submitted = request.status === "created" || request.submissionObserved === true;
52
- return structuredClone({ requestId: request.requestId, status: request.status, cwd: request.cwd,
53
- // false means no verified observation, not proof that Desktop did not submit.
54
- promptSubmitted: submitted, submissionStatus: submitted ? "observed" : "not_observed",
55
- reason: request.reason, ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(request.sessionId ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}),
56
- ...(PENDING.has(request.status) ? { directoryVerification: {
57
- required: true, expectedCwd: request.cwd, verified: false,
58
- detail: "The prefilled folder chip is not evidence of the actual session directory. Claude Desktop may create a No folder scratch session. An unobserved submission may be outside the authorized discovery scope; do not expand scope or resend to find it. Inspect Desktop before taking further action. For a fresh setup, prefer the intended project's sidebar New session button, verify the actual cwd with a read-only bootstrap, then dispatch work to the verified task. Do not replace a pending request without explicit cancellation.",
59
- } } : {}) });
60
- }
61
-
62
- export class ClaudeSessionCreation {
63
- constructor({ open, submit, trust, settle = async () => {}, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, resolveProject, realpath = (directory) => {
64
- const canonical = fs.realpathSync.native(directory);
65
- if (!fs.statSync(canonical).isDirectory()) throw new Error("Not a directory");
66
- return canonical;
67
- }, platform = process.platform }) {
68
- for (const [name, dependency] of Object.entries({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath })) {
69
- if (typeof dependency !== "function") throw new TypeError(`${name} must be a function`);
70
- }
71
- Object.assign(this, { open, submit, trust, settle, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, resolveProject, realpath, platform });
72
- this.requests = new Map();
73
- this.queue = Promise.resolve();
74
- }
75
-
76
- async canonical(directory) {
77
- const paths = this.platform === "win32" ? path.win32 : path.posix;
78
- if (!string(directory, 8192) || !paths.isAbsolute(directory)) fail("CLAUDE_CREATION_CWD_INVALID", "The project directory must be an existing absolute directory.");
79
- let canonical;
80
- try { canonical = await this.realpath(directory); } catch { fail("CLAUDE_CREATION_CWD_INVALID", "The project directory does not exist or cannot be resolved."); }
81
- if (!string(canonical, 8192) || !paths.isAbsolute(canonical)) fail("CLAUDE_CREATION_CWD_INVALID", "The canonical project directory is invalid.");
82
- return canonical;
83
- }
84
-
85
- sameCwd(left, right) {
86
- if (typeof left !== "string" || typeof right !== "string") return false;
87
- return this.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
88
- }
89
-
90
- async checkAccount(account, expected) {
91
- const provided = accountIdentity(account);
92
- const current = accountIdentity(await this.readAccount());
93
- if (!sameAccount(provided, current) || (expected && !sameAccount(provided, expected))) fail("CLAUDE_CREATION_ACCOUNT_CHANGED", "Claude Desktop account changed; this creation request cannot be used in another account.");
94
- return provided;
95
- }
96
-
97
- start(args) {
98
- const pending = this.queue.then(() => this.startLocked(args));
99
- this.queue = pending.catch(() => {});
100
- return pending;
101
- }
102
-
103
- async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen, autoSubmit = false, trustProject = false }) {
104
- if (!REQUEST_ID.test(requestId ?? "") || typeof requestId !== "string") fail("CLAUDE_CREATION_REQUEST_INVALID", "requestId must be a UUID or 32–128 hexadecimal characters.");
105
- if (!string(senderThreadId, 256)) fail("CLAUDE_CREATION_SENDER_INVALID", "A verified sender thread identity is required.");
106
- const identity = await this.checkAccount(account);
107
- const canonical = await this.canonical(cwd);
108
- const initialMessage = augmentedPrompt(prompt, requestId);
109
- const prior = this.requests.get(requestId);
110
- if (prior) {
111
- if (!sameAccount(identity, prior.account) || prior.senderThreadId !== senderThreadId || !this.sameCwd(prior.cwd, canonical) || prior.prompt !== prompt) {
112
- fail("CLAUDE_CREATION_REQUEST_CONFLICT", "This requestId is already bound to a different creation payload.");
113
- }
114
- if (autoSubmit && !prior.submissionAttempted && ["awaiting_user", "launch_uncertain"].includes(prior.status)) {
115
- const observed = await this.inspect(requestId, { account, senderThreadId });
116
- if (observed.promptSubmitted) return observed;
117
- return this.completeRequest(prior, { account, senderThreadId, beforeOpen, trustProject });
118
- }
119
- return receipt(prior);
120
- }
121
- if (this.requests.size >= MAX_REQUESTS) fail("CLAUDE_CREATION_LIMIT", "The creation receipt limit has been reached; existing requests remain available for inspection.");
122
- for (const existing of this.requests.values()) {
123
- const composerReleased = existing.status === "awaiting_project_confirmation" && existing.submissionObserved === true;
124
- 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.");
125
- }
126
- const tasks = await this.listTasks(account);
127
- const validTasks = value => Array.isArray(value) && value.length <= MAX_TASKS && value.every(task => string(task?.taskId, 256) && (task.fileTaskId === undefined || task.fileTaskId === task.taskId)) && new Set(value.map(task => task.taskId)).size === value.length;
128
- if (!validTasks(tasks)) fail("CLAUDE_CREATION_TASKS_INVALID", "The existing native task baseline cannot be verified.");
129
- const baseline = new Set(tasks.map(task => task.taskId));
130
- const projectCwd = async task => this.resolveProject ? this.resolveProject(task) : this.canonical(task.cwd);
131
- let existingProject;
132
- for (const task of tasks) {
133
- if (task.isArchived !== false) continue;
134
- try { if (this.sameCwd(await projectCwd(task), canonical)) { existingProject = task; break; } } catch {}
135
- }
136
- if (!existingProject) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The canonical directory must already belong to a nonarchived Claude Desktop task.");
137
- if (autoSubmit && typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
138
- if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
139
- const url = `claude://code/new?q=${encodeURIComponent(initialMessage)}&folder=${encodeURIComponent(canonical)}`;
140
- if (url.length > 30000) fail("CLAUDE_CREATION_URL_TOO_LONG", "The encoded Claude Desktop link exceeds 30000 characters.");
141
- await this.checkAccount(account, identity);
142
- if (beforeOpen !== undefined) {
143
- if (typeof beforeOpen !== "function") fail("CLAUDE_CREATION_PREFLIGHT_INVALID", "beforeOpen must be a function.");
144
- await beforeOpen();
145
- }
146
- if (this.resolveProject) {
147
- const currentTasks = await this.listTasks(account);
148
- if (!validTasks(currentTasks)) fail("CLAUDE_CREATION_TASKS_INVALID", "The current native task baseline cannot be verified.");
149
- const matches = currentTasks.filter(task => task.taskId === existingProject.taskId && task.fileTaskId === existingProject.fileTaskId && task.cwd === existingProject.cwd && task.isArchived === false);
150
- if (matches.length !== 1 || !this.sameCwd(await projectCwd(matches[0]), canonical)) {
151
- fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The saved project or its authorized current directory changed before opening Desktop.");
152
- }
153
- for (const task of currentTasks) baseline.add(task.taskId);
154
- if (baseline.size > MAX_TASKS) fail("CLAUDE_CREATION_TASKS_INVALID", "The native task baseline exceeds the creation limit.");
155
- if (beforeOpen) await beforeOpen();
156
- }
157
- const projectSource = this.resolveProject ? { taskId: existingProject.taskId, fileTaskId: existingProject.fileTaskId, cwd: existingProject.cwd } : undefined;
158
- const request = { requestId, cwd: canonical, prompt, initialMessage, account: identity, senderThreadId, baseline: [...baseline], ...(projectSource ? { projectSource } : {}), status: "launch_uncertain", reason: "The Desktop launch outcome is uncertain; inspect this request before taking further action." };
159
- await this.revalidateProjectSource(request, account);
160
- this.requests.set(requestId, request);
161
- try {
162
- await this.open(url);
163
- if (autoSubmit) await this.settle();
164
- request.status = "awaiting_user";
165
- request.reason = "Claude Desktop was asked to prefill a new session; the bridge did not press Send. The folder chip alone does not confirm the actual directory. Verify the created task's cwd before dispatching work; absence of a discovered task does not prove that the prompt was not submitted.";
166
- } catch {
167
- 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.";
168
- }
169
- if (request.status === "awaiting_user" && autoSubmit) {
170
- return this.completeRequest(request, { account, senderThreadId, beforeOpen, trustProject });
171
- }
172
- return receipt(request);
173
- }
174
-
175
- async revalidateProjectSource(request, account) {
176
- if (!this.resolveProject) return;
177
- const source = request.projectSource;
178
- if (!source) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The original saved project source is unavailable; submission is not authorized.");
179
- const tasks = await this.listTasks(account);
180
- if (!Array.isArray(tasks) || tasks.length > MAX_TASKS) fail("CLAUDE_CREATION_TASKS_INVALID", "The saved project source cannot be verified.");
181
- const matches = tasks.filter(task => task?.taskId === source.taskId);
182
- if (matches.length !== 1 || matches[0].fileTaskId !== source.fileTaskId || matches[0].cwd !== source.cwd || matches[0].isArchived !== false ||
183
- !this.sameCwd(await this.resolveProject(matches[0]), request.cwd)) {
184
- fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The original saved project or its authorization changed; submission is not authorized.");
185
- }
186
- }
187
-
188
- async completeRequest(request, { account, senderThreadId, beforeOpen, trustProject }) {
189
- if (typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
190
- if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
191
- await this.checkAccount(account, request.account);
192
- if (beforeOpen) await beforeOpen();
193
- await this.revalidateProjectSource(request, account);
194
- if (trustProject) {
195
- const trusted = await this.trust({ cwd: request.cwd, prompt: request.initialMessage });
196
- if (!["trusted", "absent"].includes(trusted?.status)) {
197
- request.status = "awaiting_user";
198
- request.reason = trusted?.reason ?? "Native workspace trust is not confirmed; inspect the exact project composer.";
199
- return receipt(request);
200
- }
201
- if (trusted.status === "trusted") await this.settle();
202
- }
203
- await this.checkAccount(account, request.account);
204
- if (beforeOpen) await beforeOpen();
205
- await this.revalidateProjectSource(request, account);
206
- request.submissionAttempted = true;
207
- request.status = "submission_uncertain";
208
- request.reason = "Native submission was attempted; inspect this request before retrying. The bridge will not press Send again.";
209
- try {
210
- const submission = await this.submit({ prompt: request.initialMessage, cwd: request.cwd, beforeSubmit: async () => {
211
- await this.checkAccount(account, request.account);
212
- if (beforeOpen) await beforeOpen();
213
- await this.revalidateProjectSource(request, account);
214
- } });
215
- if (submission?.status === "blocked" && submission.submissionAttempted === false) delete request.submissionAttempted;
216
- request.status = submission?.status === "submitted" ? "awaiting_session" : submission?.status === "blocked" ? "awaiting_user" : "submission_uncertain";
217
- request.reason = submission?.reason ?? "Inspect the original native composer and creation receipt; submission has not been verified.";
218
- } catch {}
219
- return this.inspect(request.requestId, { account, senderThreadId });
220
- }
221
-
222
- async inspect(requestId, { account, senderThreadId }) {
223
- const request = this.requests.get(requestId);
224
- if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
225
- if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
226
- await this.checkAccount(account, request.account);
227
- if (request.status === "created") return receipt(request);
228
- const sessions = await this.listSessions(account);
229
- if (!Array.isArray(sessions) || sessions.length > MAX_TASKS) fail("CLAUDE_CREATION_SESSIONS_INVALID", "Live native sessions could not be inspected within the limit.");
230
- const matches = new Map();
231
- for (const session of sessions) {
232
- if (!session?.alive || session.entrypoint !== "claude-desktop" || !string(session.sessionId, 256)) continue;
233
- try {
234
- const observedCwd = await this.canonical(session.cwd);
235
- if (await this.assertProcess(session) === false) continue;
236
- const context = await this.readContext(session, account);
237
- if (context?.status !== "matched" || context.isArchived || !string(context.taskId, 256) || request.baseline.includes(context.taskId)) continue;
238
- if (context.fileTaskId !== undefined && context.fileTaskId !== context.taskId) continue;
239
- if (context.accountFingerprint && context.accountFingerprint !== request.account.fingerprint) continue;
240
- if (!this.sameCwd(await this.canonical(context.cwd), observedCwd)) continue;
241
- const transcript = await this.readTranscript(session.sessionId, observedCwd, 1000);
242
- if (transcript?.truncated || transcript?.incomplete) continue;
243
- const firstUser = transcript?.messages?.find((message) => message.role === "user");
244
- if (!matchesInitialMessage(firstUser?.text, request.initialMessage)) continue;
245
- if (await this.assertProcess(session) === false) continue;
246
- matches.set(JSON.stringify([context.taskId, session.sessionId]), { sessionId: session.sessionId, taskId: context.taskId, title: string(context.title, 65536) && context.title.trim() ? context.title.trim().slice(0, 4096) : null, observedCwd });
247
- } catch {}
248
- }
249
- await this.checkAccount(account, request.account);
250
- if (matches.size > 1) {
251
- request.submissionObserved = true;
252
- request.ambiguitySeen = true;
253
- request.status = "ambiguous";
254
- for (const key of ["sessionId", "taskId", "title", "observedCwd"]) delete request[key];
255
- request.reason = "Multiple new native sessions contain the exact creation message; no session was selected.";
256
- } else if (matches.size === 1 && request.status !== "ambiguous" && !request.ambiguitySeen) {
257
- const match = [...matches.values()][0];
258
- const projectConfirmed = this.sameCwd(match.observedCwd, request.cwd);
259
- Object.assign(request, match, { submissionObserved: true, status: projectConfirmed ? "created" : "awaiting_project_confirmation", reason: projectConfirmed ? "A new live native Claude Desktop task has the exact initial user message, verified project, account, and process identity." : "The prompt was submitted in a new native Claude Desktop task, but its actual directory differs from the requested project. Select or adopt the requested project folder in Claude Desktop and inspect this request again; project creation is not confirmed." });
260
- }
261
- return receipt(request);
262
- }
263
-
264
- abandon(requestId, args) {
265
- const pending = this.queue.then(() => this.abandonLocked(requestId, args));
266
- this.queue = pending.catch(() => {});
267
- return pending;
268
- }
269
-
270
- async abandonLocked(requestId, { account, senderThreadId }) {
271
- const request = this.requests.get(requestId);
272
- if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
273
- if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
274
- await this.checkAccount(account, request.account);
275
- if (request.status === "created") return receipt(request);
276
- if (request.status === "ambiguous") request.ambiguitySeen = true;
277
- request.status = "abandoned";
278
- request.reason = "The creation request was abandoned and its composer lock released. Claude Desktop and its composer may still be open, and the prompt could still be submitted later. The receipt is retained for inspection and will never relaunch.";
279
- return receipt(request);
280
- }
281
-
282
- exportState() {
283
- return structuredClone({ version: 1, requests: [...this.requests.values()] });
284
- }
285
-
286
- restoreState(state) {
287
- if (this.requests.size || state?.version !== 1 || !Array.isArray(state.requests) || state.requests.length > MAX_REQUESTS) fail("CLAUDE_CREATION_STATE_INVALID", "Creation state cannot be restored.");
288
- const restored = new Map();
289
- const paths = this.platform === "win32" ? path.win32 : path.posix;
290
- for (const request of state.requests) {
291
- if (!request || typeof request.requestId !== "string" || !REQUEST_ID.test(request.requestId) || restored.has(request.requestId) || !string(request.cwd, 8192) || !paths.isAbsolute(request.cwd) || !string(request.senderThreadId, 256) || !string(request.account?.fingerprint) || !string(request.account?.root, 8192) || !STATUSES.has(request.status) || !string(request.reason, 4096) || !Array.isArray(request.baseline) || request.baseline.length > MAX_TASKS || request.baseline.some((id) => !string(id, 256)) || request.initialMessage !== augmentedPrompt(request.prompt, request.requestId)) fail("CLAUDE_CREATION_STATE_INVALID", "A retained creation receipt is invalid.");
292
- const hasSession = ["created", "awaiting_project_confirmation"].includes(request.status) || request.sessionId !== undefined;
293
- 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.");
294
- if (request.submissionObserved !== undefined && typeof request.submissionObserved !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission marker is invalid.");
295
- if (request.submissionAttempted !== undefined && typeof request.submissionAttempted !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission attempt is invalid.");
296
- 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.");
297
- 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.");
298
- if (request.ambiguitySeen !== undefined && typeof request.ambiguitySeen !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained ambiguity marker is invalid.");
299
- if (request.projectSource !== undefined && (!string(request.projectSource?.taskId, 256) || !request.baseline.includes(request.projectSource.taskId) ||
300
- (request.projectSource.fileTaskId !== undefined && request.projectSource.fileTaskId !== request.projectSource.taskId) ||
301
- !string(request.projectSource.cwd, 8192) || !paths.isAbsolute(request.projectSource.cwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A retained project source is invalid.");
302
- 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, ...(request.projectSource ? { projectSource: { taskId: request.projectSource.taskId, fileTaskId: request.projectSource.fileTaskId, cwd: request.projectSource.cwd } } : {}), 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 } : {}) }));
303
- }
304
- this.requests = restored;
305
- }
306
- }