@minhspark/codex-mcp-bridge 1.15.1 → 1.16.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.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +97 -8
- package/src/claude-desktop-context.mjs +19 -13
- package/src/claude-session-creation.mjs +216 -0
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +40 -3
- package/src/platform.mjs +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.16.0] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Prepare a separate native Claude Desktop Code conversation in an existing project with `start_claude_session`, then verify its new task identity and exact initial prompt through `read_claude_creation`. The official Desktop link requires user confirmation and Send; opening a composer is reported as `awaiting_user`, never as completed creation.
|
|
12
|
+
- Retain bounded creation receipts across automatic reload, prevent duplicate or competing composer launches, reject old tasks resumed as new processes, and allow explicit cancellation through `abandon_claude_creation`. Existing clients can use `send_to_claude_session` with `target: "new"` and inspect its request ID through `read_claude_delivery`.
|
|
13
|
+
- Detect a submitted new conversation that remains in No folder as `awaiting_project_confirmation`, preserve its native identity, and verify completion after the user moves it to the requested project. Recognize Claude Desktop's initial No folder banner without accepting a different or later user prompt.
|
|
14
|
+
|
|
15
|
+
## [1.15.2] - 2026-09-14
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Restore Codex-to-Claude Desktop messaging on Windows with Claude Code 2.1.270, which advertises its process FILETIME as `procStart` instead of `procStartFt`. Normalize both fields in session records and authentication keys, retain live process verification, and reject malformed or conflicting identities.
|
|
20
|
+
- Advertise both Windows identity fields on bridge peer records and authentication keys so current Claude sessions and older peers can validate the same endpoint without restarting Desktop.
|
|
21
|
+
|
|
7
22
|
## [1.15.1] - 2026-09-14
|
|
8
23
|
|
|
9
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -400,9 +400,18 @@ Under `owned`, a thread a human opened is **unreachable rather than merely restr
|
|
|
400
400
|
|
|
401
401
|
## Tools — `claude-bridge` (runs inside Codex)
|
|
402
402
|
|
|
403
|
+
When the user explicitly requests a separate conversation, call `start_claude_session` with a stable `requestId`, exact existing project `cwd`, and initial `prompt`. This uses the [official Claude Desktop Code link](https://support.claude.com/en/articles/14729294-open-claude-desktop-with-a-link), which prefills the composer but requires the user to confirm the folder and press Send. It is not unattended session creation. The bridge never patches Desktop, writes session metadata, starts a substitute CLI, or silently sends into an old conversation.
|
|
404
|
+
|
|
405
|
+
Retain the creation receipt and call `read_claude_creation`. Only `created` identifies a verified new native task and live session; it does not claim an assistant reply. Once created, use the returned exact session and task IDs with the existing send/read tools. One pending request reserves the current account's composer, including across different projects. If the user cancels it, `abandon_claude_creation` releases that reservation without discarding evidence or claiming that the old composer was closed. Compatible automatic reload preserves creation receipts; after a full restart, an unknown request is not proof that creation failed.
|
|
406
|
+
|
|
407
|
+
Clients whose tool list has not refreshed can use `send_to_claude_session` with `target: "new"`, `expectedCwd`, and `message`. It returns a creation receipt instead of a message receipt. Repeated calls with the same payload in the same verified Codex turn reuse that request; inspect its 64-character `requestId` with `read_claude_delivery`. A later explicit new-session request in a new turn has a separate identity.
|
|
408
|
+
|
|
403
409
|
| Tool | What it does | Hints |
|
|
404
410
|
|---|---|---|
|
|
405
411
|
| `list_claude_sessions` | Lists Claude Code sessions running on this machine (name, pid, sessionId, cwd, entrypoint). | read-only |
|
|
412
|
+
| `start_claude_session` | Opens the official Claude Code composer for a new conversation in an existing project. Returns `awaiting_user`; the user confirms the folder and presses Send in Claude Desktop. Reuse the same `requestId` to avoid reopening. | writes |
|
|
413
|
+
| `read_claude_creation` | Verifies a creation request using a new native task ID, exact initial user prompt, account, cwd, and live process identity. Reports `awaiting_project_confirmation` if the prompt arrived in No folder or another directory; only the requested project can become `created`. A resumed old task never counts as newly created. | read-only |
|
|
414
|
+
| `abandon_claude_creation` | Releases a pending composer reservation only when the user cancels it. Retains the receipt; does not close Desktop or prove the prompt was never submitted. | writes |
|
|
406
415
|
| `send_to_claude_session` | Sends to a Claude inbox and waits for a correlated reply. Receipt statuses distinguish `reply_received`, `sent_unconfirmed`, `reply_timeout`, and receiver policy outcomes such as `held` or `refused`. `waitSec: 0` sends without waiting for confirmation. Prefer an exact `sessionId` because session names can change. | destructive |
|
|
407
416
|
| `read_claude_delivery` | Inspects the latest recipient receipt or correlated reply by original message ID without resending or clearing the inbox. Receipts belong to this MCP process; an unknown ID after reconnect never proves non-delivery. | read-only |
|
|
408
417
|
| `read_claude_inbox` | Reads and consumes the oldest requested page of messages, including late replies, preserving unread messages. Includes reply forwarding status. | destructive |
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs
|
|
|
22
22
|
|
|
23
23
|
exitForVersionRequest(import.meta.url);
|
|
24
24
|
|
|
25
|
-
const VERSION = "1.
|
|
25
|
+
const VERSION = "1.16.0";
|
|
26
26
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
27
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
28
28
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
import { CodexAppServerClient } from "./app-server-client.mjs";
|
|
7
|
-
import { PLATFORM_LABEL } from "./platform.mjs";
|
|
8
|
-
import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
|
|
8
|
+
import { PLATFORM_LABEL, openClaudeCodeComposer } from "./platform.mjs";
|
|
9
|
+
import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript, readInitialTranscriptMessage } from "./peer-protocol.mjs";
|
|
9
10
|
import { createThreadDelivery } from "./thread-delivery.mjs";
|
|
10
11
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
11
12
|
import { desktopTasksConfigured } from "./native-relay.mjs";
|
|
12
13
|
import { createRuntimeState } from "./runtime-state.mjs";
|
|
13
|
-
import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
|
|
14
|
+
import { readClaudeDesktopContext, readClaudeDesktopTasks } from "./claude-desktop-context.mjs";
|
|
15
|
+
import { ClaudeSessionCreation } from "./claude-session-creation.mjs";
|
|
14
16
|
import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
|
|
15
17
|
import { assertRecipientClass, preflightFailure } from "./recipient-preflight.mjs";
|
|
16
18
|
import { readCodexSenderContext } from "./codex-sender-context.mjs";
|
|
@@ -22,7 +24,7 @@ import { resolveClaudeDesktopSession } from "./claude-session-router.mjs";
|
|
|
22
24
|
|
|
23
25
|
exitForVersionRequest(import.meta.url);
|
|
24
26
|
|
|
25
|
-
const VERSION = "1.
|
|
27
|
+
const VERSION = "1.16.0";
|
|
26
28
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
27
29
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
28
30
|
|
|
@@ -55,6 +57,16 @@ const forwarding = {
|
|
|
55
57
|
threadId: process.env.CODEX_THREAD_ID ?? null,
|
|
56
58
|
};
|
|
57
59
|
|
|
60
|
+
const creations = new ClaudeSessionCreation({
|
|
61
|
+
open: openClaudeCodeComposer,
|
|
62
|
+
listTasks: (account) => readClaudeDesktopTasks({ account }),
|
|
63
|
+
listSessions: () => listClaudeSessions(),
|
|
64
|
+
readContext: (session, account) => readClaudeDesktopContext(session, { account }),
|
|
65
|
+
readTranscript: readInitialTranscriptMessage,
|
|
66
|
+
readAccount: readClaudeAccountContext,
|
|
67
|
+
assertProcess: assertClaudeSessionProcess,
|
|
68
|
+
});
|
|
69
|
+
|
|
58
70
|
const replyForwarder = new ReplyForwarder({
|
|
59
71
|
minIntervalMs: FORWARD_MIN_INTERVAL_MS,
|
|
60
72
|
maxPerSession: FORWARD_MAX_PER_SESSION,
|
|
@@ -72,7 +84,7 @@ const reload = createReloadControl({
|
|
|
72
84
|
entry: "claude-bridge.mjs",
|
|
73
85
|
inspect: () => peer.reloadReason() ?? replyForwarder.reloadReason() ?? clientReloadReason(codex),
|
|
74
86
|
quiesce: async () => { await peer.quiesce(); codex.close(); },
|
|
75
|
-
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId }),
|
|
87
|
+
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId, creations: creations.exportState() }),
|
|
76
88
|
restore: (state) => {
|
|
77
89
|
assertRoutingReload(state.desktopOnly, desktopOnly);
|
|
78
90
|
if (state.threadId !== null && (typeof state.threadId !== "string" || !state.threadId.trim())) {
|
|
@@ -81,6 +93,7 @@ const reload = createReloadControl({
|
|
|
81
93
|
peer.restoreReloadState(state.peer);
|
|
82
94
|
replyForwarder.restoreReloadState(state.forwarding);
|
|
83
95
|
forwarding.threadId = state.threadId;
|
|
96
|
+
if (state.creations !== undefined) creations.restoreState(state.creations);
|
|
84
97
|
},
|
|
85
98
|
activate: () => peer.start(),
|
|
86
99
|
resume: () => peer.resume(),
|
|
@@ -134,6 +147,31 @@ function assertSender(meta) {
|
|
|
134
147
|
return sender;
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
async function startClaudeCreation({ requestId, cwd, prompt }, meta) {
|
|
151
|
+
runtime.assertCurrent();
|
|
152
|
+
if (!desktopOnly) throw new Error("New Claude sessions require Desktop-only mode");
|
|
153
|
+
const sender = assertSender(meta);
|
|
154
|
+
const accounts = readBridgeAccounts();
|
|
155
|
+
const selectedAccounts = requireBridgeAccounts(accounts);
|
|
156
|
+
const receipt = await creations.start({ requestId, cwd, prompt, account: accounts.claude, senderThreadId: sender.threadId,
|
|
157
|
+
beforeOpen: () => {
|
|
158
|
+
runtime.assertCurrent();
|
|
159
|
+
assertAccountIdentity(selectedAccounts);
|
|
160
|
+
const current = assertSender(meta);
|
|
161
|
+
if (JSON.stringify(current) !== JSON.stringify(sender)) throw new Error("The calling Codex turn changed before opening Claude Desktop");
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function inspectClaudeCreation(requestId, meta) {
|
|
168
|
+
const sender = assertSender(meta);
|
|
169
|
+
const accounts = readBridgeAccounts();
|
|
170
|
+
requireBridgeAccounts(accounts);
|
|
171
|
+
const receipt = await creations.inspect(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
172
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
173
|
+
}
|
|
174
|
+
|
|
137
175
|
function forwardToCodexThread(record) {
|
|
138
176
|
const threadId = record.replyThreadId ?? (desktopOnly ? null : forwarding.threadId);
|
|
139
177
|
if (!threadId) return;
|
|
@@ -178,6 +216,49 @@ function registerTool(name, definition, handler) {
|
|
|
178
216
|
});
|
|
179
217
|
}
|
|
180
218
|
|
|
219
|
+
registerTool(
|
|
220
|
+
"start_claude_session",
|
|
221
|
+
{
|
|
222
|
+
title: "Prepare a new Claude Desktop Code session",
|
|
223
|
+
description: "Use only when the user explicitly requests a NEW Claude Desktop session. Open the official Code composer in an existing project with a prefilled prompt. Claude Desktop requires the user to confirm the folder and press Send; opening the link is not session creation. Retain requestId and inspect read_claude_creation. Never substitute an existing task or launch a CLI session.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
requestId: z.string().describe("Stable UUID for this creation; reuse exactly when inspecting or retrying"),
|
|
226
|
+
cwd: z.string().describe("Exact absolute directory of the existing Claude Desktop project"),
|
|
227
|
+
prompt: z.string().describe("Initial prompt for the NEW conversation; shown for user confirmation"),
|
|
228
|
+
},
|
|
229
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
230
|
+
},
|
|
231
|
+
async (args, extra) => startClaudeCreation(args, extra?._meta),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
registerTool(
|
|
235
|
+
"read_claude_creation",
|
|
236
|
+
{
|
|
237
|
+
title: "Verify a new Claude Desktop session",
|
|
238
|
+
description: "Inspect an existing creation request without reopening or sending. A created receipt requires a new native task, matching account/project, live process identity, and the exact initial user prompt. awaiting_project_confirmation means the prompt arrived in a new task but its folder still differs; the user must adopt the requested project before creation can be confirmed. An old task resumed with a new process never counts as a new session.",
|
|
239
|
+
inputSchema: { requestId: z.string().describe("Original requestId returned by start_claude_session") },
|
|
240
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
241
|
+
},
|
|
242
|
+
async ({ requestId }, extra) => inspectClaudeCreation(requestId, extra?._meta),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
registerTool(
|
|
246
|
+
"abandon_claude_creation",
|
|
247
|
+
{
|
|
248
|
+
title: "Release a cancelled Claude creation request",
|
|
249
|
+
description: "Use only when the user explicitly cancels a pending creation request. Release its composer reservation while retaining the receipt. This does not close Claude Desktop or prove that the prefilled prompt was never submitted; the old request remains inspectable.",
|
|
250
|
+
inputSchema: { requestId: z.string().describe("Exact creation requestId the user cancelled") },
|
|
251
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
252
|
+
},
|
|
253
|
+
async ({ requestId }, extra) => {
|
|
254
|
+
const sender = assertSender(extra?._meta);
|
|
255
|
+
const accounts = readBridgeAccounts();
|
|
256
|
+
requireBridgeAccounts(accounts);
|
|
257
|
+
const receipt = await creations.abandon(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
258
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
181
262
|
registerTool(
|
|
182
263
|
"list_claude_sessions",
|
|
183
264
|
{
|
|
@@ -221,13 +302,14 @@ registerTool(
|
|
|
221
302
|
title: "Send a message to a Claude session",
|
|
222
303
|
description:
|
|
223
304
|
"Send a message to a running Claude Code session's peer transport and wait for its reply. " +
|
|
305
|
+
"Only when the user requests a NEW conversation, target new prepares the native Desktop composer in expectedCwd; it requires user confirmation and returns a creation receipt, not a delivery receipt. " +
|
|
224
306
|
"A socket write alone does not confirm that Claude received the message. Set waitSec to 0 to send without confirmation. " +
|
|
225
307
|
"Every send is refused while earlier messages to that session still await replies, including waitSec 0; " +
|
|
226
308
|
"wait for those replies and read_claude_inbox before trying again. Desktop-only mode refuses CLI or unknown " +
|
|
227
309
|
"entrypoints, partial names, ambiguous targets, and a missing or mismatched expectedCwd or expectedTaskId before sending. Use target auto and expectedCwd for automatic current-account discovery; expectedTaskId is optional only when auto finds exactly one matching task. " +
|
|
228
310
|
"The sender's current permissions are verified per call; environment overrides and manual binding cannot bypass an unknown sender. It never creates a replacement session.",
|
|
229
311
|
inputSchema: {
|
|
230
|
-
target: z.string().describe("Use auto for current-account discovery in expectedCwd,
|
|
312
|
+
target: z.string().describe("Use auto for current-account discovery in expectedCwd, an exact session identity, or new to prepare a separate Desktop conversation requiring user confirmation"),
|
|
231
313
|
message: z.string().describe("The message text to deliver"),
|
|
232
314
|
expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
|
|
233
315
|
expectedTaskId: z.string().optional().describe("Exact native Claude Desktop task ID from list_claude_sessions; verify its title in the app before sending"),
|
|
@@ -249,6 +331,12 @@ registerTool(
|
|
|
249
331
|
async ({ target, message, waitSec, expectedCwd, expectedTaskId }, extra) => {
|
|
250
332
|
try {
|
|
251
333
|
runtime.assertCurrent();
|
|
334
|
+
if (target === "new") {
|
|
335
|
+
if (expectedTaskId !== undefined) throw new Error("A new Claude conversation cannot target an existing task ID");
|
|
336
|
+
const sender = assertSender(extra?._meta);
|
|
337
|
+
const requestId = createHash("sha256").update(JSON.stringify([sender.threadId, sender.turnId, expectedCwd, message])).digest("hex");
|
|
338
|
+
return await startClaudeCreation({ requestId, cwd: expectedCwd, prompt: message }, extra?._meta);
|
|
339
|
+
}
|
|
252
340
|
const accounts = desktopOnly ? readBridgeAccounts() : null;
|
|
253
341
|
const selectedAccounts = desktopOnly ? requireBridgeAccounts(accounts) : null;
|
|
254
342
|
const found = desktopOnly ? resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskId,
|
|
@@ -339,11 +427,12 @@ registerTool(
|
|
|
339
427
|
"read_claude_delivery",
|
|
340
428
|
{
|
|
341
429
|
title: "Inspect a Claude message receipt without resending",
|
|
342
|
-
description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
|
|
430
|
+
description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Also accepts the 64-character requestId returned by target new to inspect creation without reopening. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
|
|
343
431
|
inputSchema: { msgId: z.string().describe("The original message ID returned by send_to_claude_session") },
|
|
344
432
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
345
433
|
},
|
|
346
|
-
async ({ msgId }) => {
|
|
434
|
+
async ({ msgId }, extra) => {
|
|
435
|
+
if (/^[0-9a-f]{64}$/i.test(msgId)) return inspectClaudeCreation(msgId, extra?._meta);
|
|
347
436
|
const receipt = readReceipt(msgId);
|
|
348
437
|
if (!receipt) return textResult("This MCP process has no receipt for that message ID. It may belong to a previous process; do not infer failure or resend. Inspect the original Claude Desktop session.", true);
|
|
349
438
|
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { receipt } };
|
|
@@ -133,22 +133,11 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
133
133
|
(typeof session.bridgeSessionId !== "string" || !session.bridgeSessionId.trim())) {
|
|
134
134
|
return result("mismatch", "The live Claude bridge session identity is invalid.");
|
|
135
135
|
}
|
|
136
|
-
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
137
136
|
let records;
|
|
138
137
|
try {
|
|
138
|
+
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
139
139
|
if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
|
|
140
|
-
|
|
141
|
-
const budget = { bytes: 0, versions: new Map(), contents: new Map() };
|
|
142
|
-
records = files.map((file) => readMetadata(file, budget));
|
|
143
|
-
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
144
|
-
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) {
|
|
145
|
-
return result("mismatch", "Claude Desktop task metadata changed during inspection; inspect the existing task again.");
|
|
146
|
-
}
|
|
147
|
-
for (const file of files) {
|
|
148
|
-
const current = fs.lstatSync(file);
|
|
149
|
-
if (!current.isFile() || !sameVersion(current, budget.versions.get(file))) throw new Error("metadata changed");
|
|
150
|
-
if (!rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
|
|
151
|
-
}
|
|
140
|
+
records = readClaudeDesktopTasks({ platform, env, root, account });
|
|
152
141
|
} catch {
|
|
153
142
|
return result("mismatch", "Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.");
|
|
154
143
|
}
|
|
@@ -202,3 +191,20 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
202
191
|
accountFingerprint: account?.fingerprint,
|
|
203
192
|
});
|
|
204
193
|
}
|
|
194
|
+
|
|
195
|
+
export function readClaudeDesktopTasks({ platform = process.platform, env = process.env, root, account } = {}) {
|
|
196
|
+
if (account !== undefined && (account.status !== "verified" || !ACCOUNT_ID.test(account.accountId ?? "") || !account.root)) {
|
|
197
|
+
throw new Error("Claude Desktop account is not verified");
|
|
198
|
+
}
|
|
199
|
+
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
200
|
+
const files = metadataFiles(directory, account?.accountId);
|
|
201
|
+
const budget = { bytes: 0, versions: new Map(), contents: new Map() };
|
|
202
|
+
const records = files.map((file) => readMetadata(file, budget));
|
|
203
|
+
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
204
|
+
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
const current = fs.lstatSync(file);
|
|
207
|
+
if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || !rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
|
|
208
|
+
}
|
|
209
|
+
return records;
|
|
210
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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", "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
|
+
if (typeof text !== "string" || !/^<system-reminder>\r?\nThe user started this session without choosing a project folder,/.test(text)) return false;
|
|
39
|
+
const closing = text.indexOf("</system-reminder>");
|
|
40
|
+
if (closing < 0 || closing > 32768) return false;
|
|
41
|
+
const body = text.slice("<system-reminder>".length, closing);
|
|
42
|
+
if (body.includes("<system-reminder") || body.includes("</system-reminder")) return false;
|
|
43
|
+
const suffix = text.slice(closing + "</system-reminder>".length);
|
|
44
|
+
const separator = /^\r?\n\r?\n/.exec(suffix);
|
|
45
|
+
return separator !== null && suffix.slice(separator[0].length) === expected;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function receipt(request) {
|
|
49
|
+
return structuredClone({ requestId: request.requestId, status: request.status, cwd: request.cwd, promptSubmitted: request.status === "created" || request.submissionObserved === true, reason: request.reason, ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(request.sessionId ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ClaudeSessionCreation {
|
|
53
|
+
constructor({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath = (directory) => {
|
|
54
|
+
const canonical = fs.realpathSync.native(directory);
|
|
55
|
+
if (!fs.statSync(canonical).isDirectory()) throw new Error("Not a directory");
|
|
56
|
+
return canonical;
|
|
57
|
+
}, platform = process.platform }) {
|
|
58
|
+
for (const [name, dependency] of Object.entries({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath })) {
|
|
59
|
+
if (typeof dependency !== "function") throw new TypeError(`${name} must be a function`);
|
|
60
|
+
}
|
|
61
|
+
Object.assign(this, { open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath, platform });
|
|
62
|
+
this.requests = new Map();
|
|
63
|
+
this.queue = Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async canonical(directory) {
|
|
67
|
+
const paths = this.platform === "win32" ? path.win32 : path.posix;
|
|
68
|
+
if (!string(directory, 8192) || !paths.isAbsolute(directory)) fail("CLAUDE_CREATION_CWD_INVALID", "The project directory must be an existing absolute directory.");
|
|
69
|
+
let canonical;
|
|
70
|
+
try { canonical = await this.realpath(directory); } catch { fail("CLAUDE_CREATION_CWD_INVALID", "The project directory does not exist or cannot be resolved."); }
|
|
71
|
+
if (!string(canonical, 8192) || !paths.isAbsolute(canonical)) fail("CLAUDE_CREATION_CWD_INVALID", "The canonical project directory is invalid.");
|
|
72
|
+
return canonical;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sameCwd(left, right) {
|
|
76
|
+
return this.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async checkAccount(account, expected) {
|
|
80
|
+
const provided = accountIdentity(account);
|
|
81
|
+
const current = accountIdentity(await this.readAccount());
|
|
82
|
+
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.");
|
|
83
|
+
return provided;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
start(args) {
|
|
87
|
+
const pending = this.queue.then(() => this.startLocked(args));
|
|
88
|
+
this.queue = pending.catch(() => {});
|
|
89
|
+
return pending;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen }) {
|
|
93
|
+
if (!REQUEST_ID.test(requestId ?? "") || typeof requestId !== "string") fail("CLAUDE_CREATION_REQUEST_INVALID", "requestId must be a UUID or 32–128 hexadecimal characters.");
|
|
94
|
+
if (!string(senderThreadId, 256)) fail("CLAUDE_CREATION_SENDER_INVALID", "A verified sender thread identity is required.");
|
|
95
|
+
const identity = await this.checkAccount(account);
|
|
96
|
+
const canonical = await this.canonical(cwd);
|
|
97
|
+
const initialMessage = augmentedPrompt(prompt, requestId);
|
|
98
|
+
const prior = this.requests.get(requestId);
|
|
99
|
+
if (prior) {
|
|
100
|
+
if (!sameAccount(identity, prior.account) || prior.senderThreadId !== senderThreadId || !this.sameCwd(prior.cwd, canonical) || prior.prompt !== prompt) {
|
|
101
|
+
fail("CLAUDE_CREATION_REQUEST_CONFLICT", "This requestId is already bound to a different creation payload.");
|
|
102
|
+
}
|
|
103
|
+
return receipt(prior);
|
|
104
|
+
}
|
|
105
|
+
if (this.requests.size >= MAX_REQUESTS) fail("CLAUDE_CREATION_LIMIT", "The creation receipt limit has been reached; existing requests remain available for inspection.");
|
|
106
|
+
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.");
|
|
108
|
+
}
|
|
109
|
+
const tasks = await this.listTasks(account);
|
|
110
|
+
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.");
|
|
111
|
+
let existingProject = false;
|
|
112
|
+
for (const task of tasks) {
|
|
113
|
+
if (task.isArchived !== false) continue;
|
|
114
|
+
try { if (this.sameCwd(await this.canonical(task.cwd), canonical)) existingProject = true; } catch {}
|
|
115
|
+
}
|
|
116
|
+
if (!existingProject) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The canonical directory must already belong to a nonarchived Claude Desktop task.");
|
|
117
|
+
const url = `claude://code/new?q=${encodeURIComponent(initialMessage)}&folder=${encodeURIComponent(canonical)}`;
|
|
118
|
+
if (url.length > 30000) fail("CLAUDE_CREATION_URL_TOO_LONG", "The encoded Claude Desktop link exceeds 30000 characters.");
|
|
119
|
+
await this.checkAccount(account, identity);
|
|
120
|
+
if (beforeOpen !== undefined) {
|
|
121
|
+
if (typeof beforeOpen !== "function") fail("CLAUDE_CREATION_PREFLIGHT_INVALID", "beforeOpen must be a function.");
|
|
122
|
+
await beforeOpen();
|
|
123
|
+
}
|
|
124
|
+
const request = { requestId, cwd: canonical, prompt, initialMessage, account: identity, senderThreadId, baseline: [...new Set(tasks.map((task) => task.taskId))], status: "launch_uncertain", reason: "The Desktop launch outcome is uncertain; inspect this request before taking further action." };
|
|
125
|
+
this.requests.set(requestId, request);
|
|
126
|
+
try {
|
|
127
|
+
await this.open(url);
|
|
128
|
+
request.status = "awaiting_user";
|
|
129
|
+
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
|
+
} catch {
|
|
131
|
+
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
|
+
}
|
|
133
|
+
return receipt(request);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async inspect(requestId, { account, senderThreadId }) {
|
|
137
|
+
const request = this.requests.get(requestId);
|
|
138
|
+
if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
|
|
139
|
+
if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
|
|
140
|
+
await this.checkAccount(account, request.account);
|
|
141
|
+
if (request.status === "created") return receipt(request);
|
|
142
|
+
const sessions = await this.listSessions(account);
|
|
143
|
+
if (!Array.isArray(sessions) || sessions.length > MAX_TASKS) fail("CLAUDE_CREATION_SESSIONS_INVALID", "Live native sessions could not be inspected within the limit.");
|
|
144
|
+
const matches = new Map();
|
|
145
|
+
for (const session of sessions) {
|
|
146
|
+
if (!session?.alive || session.entrypoint !== "claude-desktop" || !string(session.sessionId, 256)) continue;
|
|
147
|
+
try {
|
|
148
|
+
const observedCwd = await this.canonical(session.cwd);
|
|
149
|
+
if (await this.assertProcess(session) === false) continue;
|
|
150
|
+
const context = await this.readContext(session, account);
|
|
151
|
+
if (context?.status !== "matched" || context.isArchived || !string(context.taskId, 256) || request.baseline.includes(context.taskId)) continue;
|
|
152
|
+
if (context.fileTaskId !== undefined && context.fileTaskId !== context.taskId) continue;
|
|
153
|
+
if (context.accountFingerprint && context.accountFingerprint !== request.account.fingerprint) continue;
|
|
154
|
+
if (!this.sameCwd(await this.canonical(context.cwd), observedCwd)) continue;
|
|
155
|
+
const transcript = await this.readTranscript(session.sessionId, observedCwd, 1000);
|
|
156
|
+
if (transcript?.truncated || transcript?.incomplete) continue;
|
|
157
|
+
const firstUser = transcript?.messages?.find((message) => message.role === "user");
|
|
158
|
+
if (!matchesInitialMessage(firstUser?.text, request.initialMessage)) continue;
|
|
159
|
+
if (await this.assertProcess(session) === false) continue;
|
|
160
|
+
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 });
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
await this.checkAccount(account, request.account);
|
|
164
|
+
if (matches.size > 1) {
|
|
165
|
+
request.submissionObserved = true;
|
|
166
|
+
request.ambiguitySeen = true;
|
|
167
|
+
request.status = "ambiguous";
|
|
168
|
+
for (const key of ["sessionId", "taskId", "title", "observedCwd"]) delete request[key];
|
|
169
|
+
request.reason = "Multiple new native sessions contain the exact creation message; no session was selected.";
|
|
170
|
+
} else if (matches.size === 1 && request.status !== "ambiguous" && !request.ambiguitySeen) {
|
|
171
|
+
const match = [...matches.values()][0];
|
|
172
|
+
const projectConfirmed = this.sameCwd(match.observedCwd, request.cwd);
|
|
173
|
+
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." });
|
|
174
|
+
}
|
|
175
|
+
return receipt(request);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
abandon(requestId, args) {
|
|
179
|
+
const pending = this.queue.then(() => this.abandonLocked(requestId, args));
|
|
180
|
+
this.queue = pending.catch(() => {});
|
|
181
|
+
return pending;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async abandonLocked(requestId, { account, senderThreadId }) {
|
|
185
|
+
const request = this.requests.get(requestId);
|
|
186
|
+
if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
|
|
187
|
+
if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
|
|
188
|
+
await this.checkAccount(account, request.account);
|
|
189
|
+
if (request.status === "created") return receipt(request);
|
|
190
|
+
if (request.status === "ambiguous") request.ambiguitySeen = true;
|
|
191
|
+
request.status = "abandoned";
|
|
192
|
+
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.";
|
|
193
|
+
return receipt(request);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
exportState() {
|
|
197
|
+
return structuredClone({ version: 1, requests: [...this.requests.values()] });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
restoreState(state) {
|
|
201
|
+
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.");
|
|
202
|
+
const restored = new Map();
|
|
203
|
+
const paths = this.platform === "win32" ? path.win32 : path.posix;
|
|
204
|
+
for (const request of state.requests) {
|
|
205
|
+
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.");
|
|
206
|
+
const hasSession = ["created", "awaiting_project_confirmation"].includes(request.status) || request.sessionId !== undefined;
|
|
207
|
+
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
|
+
if (request.submissionObserved !== undefined && typeof request.submissionObserved !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission marker is invalid.");
|
|
209
|
+
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
|
+
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
|
+
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 } : {}) }));
|
|
213
|
+
}
|
|
214
|
+
this.requests = restored;
|
|
215
|
+
}
|
|
216
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import { assertClaudeSenderContext, readClaudeSenderContext, requireClaudeSender
|
|
|
33
33
|
|
|
34
34
|
exitForVersionRequest(import.meta.url);
|
|
35
35
|
|
|
36
|
-
const VERSION = "1.
|
|
36
|
+
const VERSION = "1.16.0";
|
|
37
37
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -30,7 +30,7 @@ import { createRuntimeState } from "./runtime-state.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.
|
|
33
|
+
const VERSION = "1.16.0";
|
|
34
34
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
35
35
|
|
|
36
36
|
function errorResponse(code, message, sent) {
|
package/src/peer-protocol.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function readPeerToken(socket) {
|
|
|
99
99
|
try {
|
|
100
100
|
const key = JSON.parse(fs.readFileSync(peerKeyPath(candidates[0].pid, socket), "utf8"));
|
|
101
101
|
if (typeof key.peerToken !== "string" || !/^[0-9a-f]{32}$/i.test(key.peerToken)) throw new Error("Invalid peer key");
|
|
102
|
-
const identity =
|
|
102
|
+
const identity = readPeerProcessIdentity(key);
|
|
103
103
|
if (identity && identity !== readProcessStart(candidates[0].pid)) throw new Error("Peer process identity changed");
|
|
104
104
|
return key.peerToken;
|
|
105
105
|
} catch {
|
|
@@ -108,6 +108,17 @@ function readPeerToken(socket) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
export function readPeerProcessIdentity(entry, platform = process.platform) {
|
|
112
|
+
if (platform !== "win32") return entry.procStart ?? null;
|
|
113
|
+
const values = ["procStart", "procStartFt"].filter((field) => Object.hasOwn(entry, field)).map((field) => entry[field]);
|
|
114
|
+
if (!values.length) return null;
|
|
115
|
+
if (values.some((value) => typeof value !== "string" || !/^[1-9]\d{0,19}$/.test(value) || BigInt(value) > 18446744073709551615n)
|
|
116
|
+
|| values.some((value) => value !== values[0])) {
|
|
117
|
+
throw new Error("Invalid or conflicting Windows peer process identity");
|
|
118
|
+
}
|
|
119
|
+
return values[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
/**
|
|
112
123
|
* MCP servers are spawned with an empty PATH, so `ps` must be addressed by its
|
|
113
124
|
* absolute path - a bare lookup throws ENOENT and takes the whole server down
|
|
@@ -173,6 +184,9 @@ export function listClaudeSessions({ includeDead = false, includeBridges = false
|
|
|
173
184
|
if (entry.entrypoint === BRIDGE_ENTRYPOINT && !includeBridges) continue;
|
|
174
185
|
const alive = isProcessAlive(entry.pid) && hasMessagingEndpoint(entry);
|
|
175
186
|
if (!alive && !includeDead) continue;
|
|
187
|
+
let processStart;
|
|
188
|
+
try { processStart = readPeerProcessIdentity(entry); }
|
|
189
|
+
catch { continue; }
|
|
176
190
|
rows.push({
|
|
177
191
|
pid: entry.pid,
|
|
178
192
|
name: entry.name ?? null,
|
|
@@ -182,7 +196,7 @@ export function listClaudeSessions({ includeDead = false, includeBridges = false
|
|
|
182
196
|
kind: entry.kind ?? null,
|
|
183
197
|
entrypoint: entry.entrypoint ?? null,
|
|
184
198
|
startedAt: entry.startedAt ?? null,
|
|
185
|
-
processStart
|
|
199
|
+
processStart,
|
|
186
200
|
socket: entry.messagingSocketPath,
|
|
187
201
|
alive,
|
|
188
202
|
});
|
|
@@ -285,6 +299,29 @@ export function readTranscript(sessionId, cwd, limit = 10) {
|
|
|
285
299
|
return { file, messages: messages.slice(-limit) };
|
|
286
300
|
}
|
|
287
301
|
|
|
302
|
+
export function readInitialTranscriptMessage(sessionId, cwd) {
|
|
303
|
+
const file = findTranscriptFile(sessionId, cwd);
|
|
304
|
+
if (!fs.existsSync(file)) return { file, messages: [] };
|
|
305
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
306
|
+
try {
|
|
307
|
+
const stat = fs.fstatSync(descriptor);
|
|
308
|
+
if (!stat.isFile()) return { file, messages: [] };
|
|
309
|
+
const buffer = Buffer.alloc(Math.min(stat.size, 1024 * 1024));
|
|
310
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
311
|
+
const text = buffer.subarray(0, bytes).toString("utf8");
|
|
312
|
+
for (const line of text.slice(0, text.lastIndexOf("\n") + 1).split("\n")) {
|
|
313
|
+
if (!line.trim()) continue;
|
|
314
|
+
let entry;
|
|
315
|
+
try { entry = JSON.parse(line); } catch { return { file, messages: [] }; }
|
|
316
|
+
if (entry.isMeta || entry.isSidechain || entry.message?.role !== "user") continue;
|
|
317
|
+
const content = entry.message.content;
|
|
318
|
+
const initial = (Array.isArray(content) ? content.filter((part) => part?.type === "text").map((part) => part.text ?? "").join("\n") : String(content ?? "")).trim();
|
|
319
|
+
return { file, messages: initial ? [{ role: "user", text: initial }] : [] };
|
|
320
|
+
}
|
|
321
|
+
return { file, messages: [] };
|
|
322
|
+
} finally { fs.closeSync(descriptor); }
|
|
323
|
+
}
|
|
324
|
+
|
|
288
325
|
/**
|
|
289
326
|
* Claude Code records an injected peer message in one of two shapes. An idle
|
|
290
327
|
* recipient starts a new turn with a user entry whose uuid is the message id
|
|
@@ -420,7 +457,7 @@ export class PeerEndpoint {
|
|
|
420
457
|
});
|
|
421
458
|
if (!IS_WINDOWS) fs.chmodSync(this.socketPath, 0o600);
|
|
422
459
|
|
|
423
|
-
const processIdentity = procStart ? (IS_WINDOWS ? { procStartFt: procStart } : { procStart }) : {};
|
|
460
|
+
const processIdentity = procStart ? (IS_WINDOWS ? { procStart, procStartFt: procStart } : { procStart }) : {};
|
|
424
461
|
this.registry = {
|
|
425
462
|
pid: this.pid,
|
|
426
463
|
sessionId: crypto.randomUUID(),
|
package/src/platform.mjs
CHANGED
|
@@ -468,3 +468,21 @@ export async function openThreadInCodexApp(threadId, { activate = true } = {}) {
|
|
|
468
468
|
await execFileAsync("open", args, { timeout: 10000 });
|
|
469
469
|
return url;
|
|
470
470
|
}
|
|
471
|
+
|
|
472
|
+
export async function openClaudeCodeComposer(url, { platform = process.platform, env = process.env, run = execFileAsync } = {}) {
|
|
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))) {
|
|
476
|
+
throw new Error("Invalid Claude Desktop Code creation link");
|
|
477
|
+
}
|
|
478
|
+
if (platform === "win32") {
|
|
479
|
+
const shell = path.win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
480
|
+
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`;
|
|
482
|
+
await run(shell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 10000 });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const command = platform === "darwin" ? "/usr/bin/open" : platform === "linux" ? "/usr/bin/xdg-open" : null;
|
|
486
|
+
if (!command) throw new Error("Claude Desktop Code links are unsupported on this platform");
|
|
487
|
+
await run(command, [url], { windowsHide: true, timeout: 10000 });
|
|
488
|
+
}
|