cosmoremote 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.js +320 -149
- package/dist/bridge.js.map +1 -1
- package/dist/cli-conversations.d.ts +3 -1
- package/dist/cli-conversations.js +1 -0
- package/dist/cli-conversations.js.map +1 -1
- package/dist/detect.d.ts +9 -0
- package/dist/detect.js +48 -13
- package/dist/detect.js.map +1 -1
- package/dist/session.d.ts +18 -1
- package/dist/session.js +362 -18
- package/dist/session.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -33,6 +33,10 @@ let promptTicketPublicKeySource = null;
|
|
|
33
33
|
let workspaceRoot = process.cwd();
|
|
34
34
|
let importableConversations = [];
|
|
35
35
|
let projectFolders = [];
|
|
36
|
+
const MAX_CONCURRENT_CLI_SESSIONS = (() => {
|
|
37
|
+
const raw = Number(process.env.COSMOREMOTE_MAX_CONCURRENT_CLI_SESSIONS);
|
|
38
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
|
|
39
|
+
})();
|
|
36
40
|
function refreshImportableConversations() {
|
|
37
41
|
try {
|
|
38
42
|
importableConversations = (0, cli_conversations_1.listImportableConversations)(workspaceRoot);
|
|
@@ -71,16 +75,41 @@ function announceCapabilities() {
|
|
|
71
75
|
currentRelayWs.send(JSON.stringify({
|
|
72
76
|
type: "capabilities",
|
|
73
77
|
bridgeVersion: getPackageVersion(),
|
|
78
|
+
availableClis: getAvailableCliNames(),
|
|
74
79
|
folders: projectFolders,
|
|
75
80
|
conversations: importableConversations,
|
|
76
81
|
workspaceRoot,
|
|
77
82
|
}));
|
|
78
|
-
console.log(` [relay] announced capabilities folders=${projectFolders.length} conversations=${importableConversations.length}`);
|
|
83
|
+
console.log(` [relay] announced capabilities clis=${getAvailableCliNames().join(",") || "none"} folders=${projectFolders.length} conversations=${importableConversations.length}`);
|
|
79
84
|
}
|
|
80
85
|
catch (err) {
|
|
81
86
|
console.error(` [relay] failed to announce capabilities:`, err);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
89
|
+
function getAvailableCliNames() {
|
|
90
|
+
const out = [];
|
|
91
|
+
if (clis?.claude)
|
|
92
|
+
out.push("CLAUDE");
|
|
93
|
+
if (clis?.codex)
|
|
94
|
+
out.push("CODEX");
|
|
95
|
+
if (clis?.cursor)
|
|
96
|
+
out.push("CURSOR");
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function normalizedCliName(cli) {
|
|
100
|
+
const upper = String(cli ?? "").toUpperCase();
|
|
101
|
+
if (upper === "CODEX" || upper === "CURSOR" || upper === "CLAUDE")
|
|
102
|
+
return upper;
|
|
103
|
+
return "CLAUDE";
|
|
104
|
+
}
|
|
105
|
+
function binaryFor(cli) {
|
|
106
|
+
const binaries = {
|
|
107
|
+
CODEX: clis.codex,
|
|
108
|
+
CURSOR: clis.cursor,
|
|
109
|
+
CLAUDE: clis.claude,
|
|
110
|
+
};
|
|
111
|
+
return binaries[String(cli ?? "").toUpperCase()] ?? clis.claude;
|
|
112
|
+
}
|
|
84
113
|
function generatePairingCode() {
|
|
85
114
|
return String(Math.floor(Math.random() * 999999)).padStart(6, "0");
|
|
86
115
|
}
|
|
@@ -145,16 +174,26 @@ function loadSessionContexts() {
|
|
|
145
174
|
const raw = (0, fs_1.readFileSync)(SESSION_CONTEXTS_FILE, "utf-8");
|
|
146
175
|
const parsed = JSON.parse(raw);
|
|
147
176
|
const now = Date.now();
|
|
177
|
+
let dropped = 0;
|
|
148
178
|
for (const [sessionId, entry] of Object.entries(parsed)) {
|
|
149
179
|
if (!entry || typeof entry.expiresAt !== "number" || entry.expiresAt <= now)
|
|
150
180
|
continue;
|
|
151
181
|
sessionContexts.set(sessionId, {
|
|
152
182
|
claudeSessionId: typeof entry.claudeSessionId === "string" ? entry.claudeSessionId : undefined,
|
|
153
183
|
codexThreadId: typeof entry.codexThreadId === "string" ? entry.codexThreadId : undefined,
|
|
184
|
+
cursorSessionId: typeof entry.cursorSessionId === "string" ? entry.cursorSessionId : undefined,
|
|
154
185
|
expiresAt: entry.expiresAt,
|
|
155
186
|
});
|
|
156
187
|
}
|
|
157
|
-
|
|
188
|
+
// Self-heal: older bridge versions persisted claudeSessionId even when the
|
|
189
|
+
// turn was killed before Claude wrote its first message. Those entries
|
|
190
|
+
// point at non-existent .jsonl files and trigger "No conversation found"
|
|
191
|
+
// on the next prompt. We can't tell from the entry alone whether the file
|
|
192
|
+
// exists yet (workingDir lives on the session, not the context), so we
|
|
193
|
+
// defer the check to when the prompt arrives — see handlePromptFromRelay.
|
|
194
|
+
if (sessionContexts.size > 0) {
|
|
195
|
+
console.log(` [context] loaded ${sessionContexts.size} persisted CLI contexts (dropped=${dropped})`);
|
|
196
|
+
}
|
|
158
197
|
}
|
|
159
198
|
catch (err) {
|
|
160
199
|
console.error(" [context] Failed to read session contexts:", err);
|
|
@@ -180,6 +219,7 @@ function upsertSessionContext(sessionId, patch) {
|
|
|
180
219
|
const entry = {
|
|
181
220
|
claudeSessionId: patch.claudeSessionId ?? existing?.claudeSessionId,
|
|
182
221
|
codexThreadId: patch.codexThreadId ?? existing?.codexThreadId,
|
|
222
|
+
cursorSessionId: patch.cursorSessionId ?? existing?.cursorSessionId,
|
|
183
223
|
expiresAt: Date.now() + SESSION_CONTEXT_TTL_MS,
|
|
184
224
|
};
|
|
185
225
|
sessionContexts.set(sessionId, entry);
|
|
@@ -228,6 +268,53 @@ function getClaudeContext(sessionId) {
|
|
|
228
268
|
function setClaudeContext(sessionId, claudeId) {
|
|
229
269
|
upsertSessionContext(sessionId, { claudeSessionId: claudeId });
|
|
230
270
|
}
|
|
271
|
+
// Claude Code persists each conversation as a JSONL file under
|
|
272
|
+
// ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. The "encoded cwd" is the
|
|
273
|
+
// absolute working dir with every "/" replaced by "-" (e.g. /Users/matheus →
|
|
274
|
+
// -Users-matheus). If the file is missing, Claude will refuse to --resume the
|
|
275
|
+
// id, surfacing as "No conversation found with session ID: <uuid>" on stderr.
|
|
276
|
+
//
|
|
277
|
+
// We use this check to gate persistence of claudeSessionId so a turn that was
|
|
278
|
+
// killed before Claude wrote its first turn never poisons session-contexts.json.
|
|
279
|
+
function claudeSessionFileExists(workingDir, claudeSessionId) {
|
|
280
|
+
if (!workingDir || !claudeSessionId)
|
|
281
|
+
return false;
|
|
282
|
+
const encoded = workingDir.replaceAll("/", "-");
|
|
283
|
+
const path = (0, path_1.join)((0, os_1.homedir)(), ".claude", "projects", encoded, `${claudeSessionId}.jsonl`);
|
|
284
|
+
return (0, fs_1.existsSync)(path);
|
|
285
|
+
}
|
|
286
|
+
// Guarded variant of setClaudeContext — only persists the mapping if Claude
|
|
287
|
+
// actually wrote conversation history to disk. Without this guard, killed or
|
|
288
|
+
// failed-init sessions left stale entries pointing at non-existent Claude
|
|
289
|
+
// session ids, causing every subsequent prompt to retry with --resume and fail.
|
|
290
|
+
function persistClaudeContextIfValid(sessionId, claudeSessionId, workingDir) {
|
|
291
|
+
if (claudeSessionFileExists(workingDir, claudeSessionId)) {
|
|
292
|
+
setClaudeContext(sessionId, claudeSessionId);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Evict any pre-existing entry — it's stale and would trigger --resume
|
|
296
|
+
// failures on the next prompt.
|
|
297
|
+
const existing = sessionContexts.get(sessionId);
|
|
298
|
+
if (existing?.claudeSessionId === claudeSessionId) {
|
|
299
|
+
clearSessionContext(sessionId);
|
|
300
|
+
console.log(` [bridge] dropped stale claude context session=${sessionId.substring(0, 8)} claude=${claudeSessionId.substring(0, 8)} (no jsonl on disk)`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Wraps getClaudeContext with on-disk verification. Returns undefined when the
|
|
305
|
+
// persisted claudeSessionId points at a .jsonl that no longer exists — that
|
|
306
|
+
// way fresh CLISession spawns use --session-id (which creates) instead of
|
|
307
|
+
// --resume (which fails). Also self-heals by evicting the bad entry.
|
|
308
|
+
function getValidClaudeContext(sessionId, workingDir) {
|
|
309
|
+
const claudeSessionId = getClaudeContext(sessionId);
|
|
310
|
+
if (!claudeSessionId)
|
|
311
|
+
return undefined;
|
|
312
|
+
if (claudeSessionFileExists(workingDir, claudeSessionId))
|
|
313
|
+
return claudeSessionId;
|
|
314
|
+
clearSessionContext(sessionId);
|
|
315
|
+
console.log(` [bridge] evicted stale claude context session=${sessionId.substring(0, 8)} claude=${claudeSessionId.substring(0, 8)} (jsonl missing)`);
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
231
318
|
function getCodexContext(sessionId) {
|
|
232
319
|
const entry = sessionContexts.get(sessionId);
|
|
233
320
|
if (!entry)
|
|
@@ -242,6 +329,66 @@ function getCodexContext(sessionId) {
|
|
|
242
329
|
function setCodexContext(sessionId, threadId) {
|
|
243
330
|
upsertSessionContext(sessionId, { codexThreadId: threadId });
|
|
244
331
|
}
|
|
332
|
+
function getCursorContext(sessionId) {
|
|
333
|
+
const entry = sessionContexts.get(sessionId);
|
|
334
|
+
if (!entry)
|
|
335
|
+
return undefined;
|
|
336
|
+
if (Date.now() > entry.expiresAt) {
|
|
337
|
+
sessionContexts.delete(sessionId);
|
|
338
|
+
saveSessionContexts();
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return entry.cursorSessionId;
|
|
342
|
+
}
|
|
343
|
+
function setCursorContext(sessionId, cursorSessionId) {
|
|
344
|
+
upsertSessionContext(sessionId, { cursorSessionId });
|
|
345
|
+
}
|
|
346
|
+
function cursorSessionFileExists(cursorSessionId) {
|
|
347
|
+
if (!cursorSessionId)
|
|
348
|
+
return false;
|
|
349
|
+
// cursor-agent stores transcripts in TWO shapes:
|
|
350
|
+
// global: ~/.cursor/projects/agent-transcripts/<id>/<id>.jsonl
|
|
351
|
+
// per-project: ~/.cursor/projects/<project-slug>/agent-transcripts/<id>/<id>.jsonl
|
|
352
|
+
// A `cursor-agent --workspace <cwd>` run (what the bridge does) writes to the
|
|
353
|
+
// per-project path, so checking only the global one evicts every valid resume id.
|
|
354
|
+
const projectsRoot = (0, path_1.join)((0, os_1.homedir)(), ".cursor", "projects");
|
|
355
|
+
const rel = (0, path_1.join)("agent-transcripts", cursorSessionId, `${cursorSessionId}.jsonl`);
|
|
356
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectsRoot, rel)))
|
|
357
|
+
return true;
|
|
358
|
+
try {
|
|
359
|
+
for (const entry of (0, fs_1.readdirSync)(projectsRoot, { withFileTypes: true })) {
|
|
360
|
+
if (!entry.isDirectory())
|
|
361
|
+
continue;
|
|
362
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectsRoot, entry.name, rel)))
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// projects root missing/unreadable — treat as no transcript
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
function persistCursorContextIfValid(sessionId, cursorSessionId) {
|
|
372
|
+
if (cursorSessionFileExists(cursorSessionId)) {
|
|
373
|
+
setCursorContext(sessionId, cursorSessionId);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const existing = sessionContexts.get(sessionId);
|
|
377
|
+
if (existing?.cursorSessionId === cursorSessionId) {
|
|
378
|
+
clearSessionContext(sessionId);
|
|
379
|
+
console.log(` [bridge] dropped stale cursor context session=${sessionId.substring(0, 8)} cursor=${cursorSessionId.substring(0, 8)} (no jsonl on disk)`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function getValidCursorContext(sessionId) {
|
|
383
|
+
const cursorSessionId = getCursorContext(sessionId);
|
|
384
|
+
if (!cursorSessionId)
|
|
385
|
+
return undefined;
|
|
386
|
+
if (cursorSessionFileExists(cursorSessionId))
|
|
387
|
+
return cursorSessionId;
|
|
388
|
+
clearSessionContext(sessionId);
|
|
389
|
+
console.log(` [bridge] evicted stale cursor context session=${sessionId.substring(0, 8)} cursor=${cursorSessionId.substring(0, 8)} (jsonl missing)`);
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
245
392
|
function checkLocalLimit(sessionToken) {
|
|
246
393
|
const now = Date.now();
|
|
247
394
|
let entry = localMessageCounts.get(sessionToken);
|
|
@@ -291,6 +438,116 @@ function normalizeHttpBaseUrl(rawUrl) {
|
|
|
291
438
|
const parsed = new URL(normalized);
|
|
292
439
|
return `${parsed.protocol}//${parsed.host}`;
|
|
293
440
|
}
|
|
441
|
+
function resolveAllowedWorkingDir(rawWorkingDir) {
|
|
442
|
+
const cwd = (0, path_1.resolve)(rawWorkingDir || workspaceRoot);
|
|
443
|
+
if (!(0, cli_conversations_1.isWithin)(workspaceRoot, cwd)) {
|
|
444
|
+
return { error: `Working directory is outside the allowed workspace root. cwd=${cwd} workspaceRoot=${workspaceRoot}` };
|
|
445
|
+
}
|
|
446
|
+
return { cwd };
|
|
447
|
+
}
|
|
448
|
+
function persistSessionContextsFromSession(sessionId, session) {
|
|
449
|
+
if (session.claudeSessionId) {
|
|
450
|
+
persistClaudeContextIfValid(sessionId, session.claudeSessionId, session.workingDir);
|
|
451
|
+
}
|
|
452
|
+
if (session.codexThreadId) {
|
|
453
|
+
setCodexContext(sessionId, session.codexThreadId);
|
|
454
|
+
console.log(` [bridge] session=${sessionId.substring(0, 8)} saved codex thread=${session.codexThreadId}`);
|
|
455
|
+
}
|
|
456
|
+
if (session.cursorSessionId) {
|
|
457
|
+
persistCursorContextIfValid(sessionId, session.cursorSessionId);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function killSession(sessionId, reason) {
|
|
461
|
+
const session = sessions.get(sessionId);
|
|
462
|
+
if (!session)
|
|
463
|
+
return;
|
|
464
|
+
console.log(` [bridge] killing session=${sessionId.substring(0, 8)} reason=${reason}`);
|
|
465
|
+
persistSessionContextsFromSession(sessionId, session);
|
|
466
|
+
sessions.delete(sessionId);
|
|
467
|
+
sessionsCreating.delete(sessionId);
|
|
468
|
+
session.kill();
|
|
469
|
+
}
|
|
470
|
+
function handlePromptCommon(msg, handlers) {
|
|
471
|
+
console.log(` [${handlers.source}] prompt session=${msg.sessionId.substring(0, 8)} cli=${msg.cli || "unknown"} wd=${msg.workingDir || "n/a"}`);
|
|
472
|
+
const cliName = normalizedCliName(msg.cli);
|
|
473
|
+
const cliType = cliName.toLowerCase();
|
|
474
|
+
const binary = binaryFor(msg.cli);
|
|
475
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} cliType=${cliType} binary=${binary || "missing"} workingDir=${msg.workingDir}`);
|
|
476
|
+
handlers.sendStatus(`Bridge received prompt. Preparing ${cliType}...`);
|
|
477
|
+
if (!binary) {
|
|
478
|
+
const install = (0, detect_1.installCommandFor)(cliName);
|
|
479
|
+
handlers.sendError(install
|
|
480
|
+
? `${cliType} CLI is not installed on this Mac. Install it with: ${install}`
|
|
481
|
+
: `${cliType} CLI not found on this machine`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const cwdResult = resolveAllowedWorkingDir(msg.workingDir);
|
|
485
|
+
if ("error" in cwdResult) {
|
|
486
|
+
console.log(` [${handlers.source}] rejecting session=${msg.sessionId.substring(0, 8)} reason=${cwdResult.error}`);
|
|
487
|
+
handlers.sendError(cwdResult.error);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const cwd = cwdResult.cwd;
|
|
491
|
+
if (sessionsCreating.has(msg.sessionId)) {
|
|
492
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} already being created, skipping duplicate`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
let session = sessions.get(msg.sessionId);
|
|
496
|
+
if (!session) {
|
|
497
|
+
const activeCount = sessions.size + sessionsCreating.size;
|
|
498
|
+
if (activeCount >= MAX_CONCURRENT_CLI_SESSIONS) {
|
|
499
|
+
const error = `Bridge is busy: ${activeCount}/${MAX_CONCURRENT_CLI_SESSIONS} CLI sessions are active. Try again after a session finishes.`;
|
|
500
|
+
console.log(` [${handlers.source}] rejecting session=${msg.sessionId.substring(0, 8)} reason=${error}`);
|
|
501
|
+
handlers.sendError(error);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
sessionsCreating.add(msg.sessionId);
|
|
505
|
+
try {
|
|
506
|
+
session = new session_1.CLISession({
|
|
507
|
+
sessionId: msg.sessionId,
|
|
508
|
+
cliBinary: binary,
|
|
509
|
+
workingDir: cwd,
|
|
510
|
+
initialClaudeSessionId: getValidClaudeContext(msg.sessionId, cwd),
|
|
511
|
+
initialCodexThreadId: getCodexContext(msg.sessionId),
|
|
512
|
+
initialCursorSessionId: getValidCursorContext(msg.sessionId),
|
|
513
|
+
});
|
|
514
|
+
sessions.set(msg.sessionId, session);
|
|
515
|
+
handlers.ownerSessionIds?.add(msg.sessionId);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
sessionsCreating.delete(msg.sessionId);
|
|
519
|
+
}
|
|
520
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} created mapSize=${sessions.size} resuming=${Boolean(getClaudeContext(msg.sessionId) || getCodexContext(msg.sessionId) || getCursorContext(msg.sessionId))}`);
|
|
521
|
+
handlers.sendStatus(`Starting ${cliType} in ${cwd}`);
|
|
522
|
+
session.on("output", (text) => {
|
|
523
|
+
handlers.sendOutput(text);
|
|
524
|
+
});
|
|
525
|
+
session.on("status", (text) => {
|
|
526
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} status: ${text}`);
|
|
527
|
+
handlers.sendStatus(text);
|
|
528
|
+
});
|
|
529
|
+
session.on("error", (text) => {
|
|
530
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} error: ${text.substring(0, 120)}`);
|
|
531
|
+
handlers.sendError(text);
|
|
532
|
+
});
|
|
533
|
+
session.on("done", (result) => {
|
|
534
|
+
console.log(` [${handlers.source}] session=${msg.sessionId.substring(0, 8)} done code=${result.code} outputLen=${result.fullOutput.length}`);
|
|
535
|
+
const current = sessions.get(msg.sessionId);
|
|
536
|
+
if (current) {
|
|
537
|
+
persistSessionContextsFromSession(msg.sessionId, current);
|
|
538
|
+
}
|
|
539
|
+
handlers.sendDone(result.fullOutput);
|
|
540
|
+
if (!current?.hasTurnInFlight && !current?.hasQueuedPrompts) {
|
|
541
|
+
sessions.delete(msg.sessionId);
|
|
542
|
+
handlers.ownerSessionIds?.delete(msg.sessionId);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
handlers.ownerSessionIds?.add(msg.sessionId);
|
|
548
|
+
}
|
|
549
|
+
session.sendPrompt(msg.content, Array.isArray(msg.attachments) ? msg.attachments : []);
|
|
550
|
+
}
|
|
294
551
|
function sha256Hex(input) {
|
|
295
552
|
return crypto_1.default.createHash("sha256").update(input).digest("hex");
|
|
296
553
|
}
|
|
@@ -432,12 +689,24 @@ async function startBridge(opts) {
|
|
|
432
689
|
console.log(" [env] XDG_CACHE_HOME:", process.env.XDG_CACHE_HOME || "unset");
|
|
433
690
|
console.log(" [env] BACKEND_URL:", opts.backendUrl || process.env.BACKEND_URL || "unset");
|
|
434
691
|
console.log(" Available CLIs:");
|
|
435
|
-
|
|
436
|
-
|
|
692
|
+
const cliLines = [
|
|
693
|
+
["Claude Code", "CLAUDE", clis.claude],
|
|
694
|
+
["Codex ", "CODEX", clis.codex],
|
|
695
|
+
["Cursor ", "CURSOR", clis.cursor],
|
|
696
|
+
];
|
|
697
|
+
for (const [label, key, path] of cliLines) {
|
|
698
|
+
if (path) {
|
|
699
|
+
console.log(` ${label}: ${path}`);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
console.log(` ${label}: not found`);
|
|
703
|
+
console.log(` → install on this Mac with: ${(0, detect_1.installCommandFor)(key)}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
437
706
|
console.log(` Workspace: ${workspaceRoot}`);
|
|
438
707
|
console.log();
|
|
439
|
-
if (!clis.claude && !clis.codex) {
|
|
440
|
-
console.error(" ✗ No supported CLIs found. Install Claude Code or
|
|
708
|
+
if (!clis.claude && !clis.codex && !clis.cursor) {
|
|
709
|
+
console.error(" ✗ No supported CLIs found. Install Claude Code, Codex, or Cursor Agent first.");
|
|
441
710
|
process.exit(1);
|
|
442
711
|
}
|
|
443
712
|
if (opts.backendUrl) {
|
|
@@ -552,7 +821,7 @@ function handleConnection(ws, sessionToken) {
|
|
|
552
821
|
const ticket = verifyPromptTicket(msg.promptTicket, msg);
|
|
553
822
|
console.log(` [ticket] verified session=${ticket.sessionId.substring(0, 8)} msg=${ticket.messageId.substring(0, 8)} exp=${ticket.expiresAt}`);
|
|
554
823
|
connectionSessionIds.add(msg.sessionId);
|
|
555
|
-
handlePrompt(ws, msg);
|
|
824
|
+
handlePrompt(ws, msg, connectionSessionIds);
|
|
556
825
|
}
|
|
557
826
|
catch (err) {
|
|
558
827
|
const error = err instanceof Error ? err.message : "Invalid prompt ticket";
|
|
@@ -565,18 +834,8 @@ function handleConnection(ws, sessionToken) {
|
|
|
565
834
|
console.log(` [bridge] incoming kill session=${String(msg.sessionId || "-").substring(0, 8)}`);
|
|
566
835
|
const session = sessions.get(msg.sessionId);
|
|
567
836
|
if (session) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
setClaudeContext(msg.sessionId, session.claudeSessionId);
|
|
571
|
-
console.log(` [bridge] saved claude context before kill=${session.claudeSessionId}`);
|
|
572
|
-
}
|
|
573
|
-
if (session.codexThreadId) {
|
|
574
|
-
setCodexContext(msg.sessionId, session.codexThreadId);
|
|
575
|
-
console.log(` [bridge] saved codex context before kill=${session.codexThreadId}`);
|
|
576
|
-
}
|
|
577
|
-
sessions.delete(msg.sessionId);
|
|
578
|
-
sessionsCreating.delete(msg.sessionId);
|
|
579
|
-
session.kill();
|
|
837
|
+
killSession(msg.sessionId, "local kill request");
|
|
838
|
+
connectionSessionIds.delete(msg.sessionId);
|
|
580
839
|
ws.send(JSON.stringify({ type: "done", sessionId: msg.sessionId, content: "[killed]" }));
|
|
581
840
|
}
|
|
582
841
|
}
|
|
@@ -591,78 +850,33 @@ function handleConnection(ws, sessionToken) {
|
|
|
591
850
|
for (const sessionId of connectionSessionIds) {
|
|
592
851
|
const session = sessions.get(sessionId);
|
|
593
852
|
if (session) {
|
|
594
|
-
|
|
595
|
-
session.kill();
|
|
596
|
-
sessions.delete(sessionId);
|
|
597
|
-
sessionsCreating.delete(sessionId);
|
|
853
|
+
killSession(sessionId, "client disconnect");
|
|
598
854
|
}
|
|
599
855
|
}
|
|
600
856
|
connectionSessionIds.clear();
|
|
601
857
|
});
|
|
602
858
|
}
|
|
603
|
-
function handlePrompt(ws, msg) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
session = new session_1.CLISession({
|
|
625
|
-
sessionId: msg.sessionId,
|
|
626
|
-
cliBinary: binary,
|
|
627
|
-
workingDir: msg.workingDir || workspaceRoot,
|
|
628
|
-
initialClaudeSessionId: getClaudeContext(msg.sessionId),
|
|
629
|
-
initialCodexThreadId: getCodexContext(msg.sessionId),
|
|
630
|
-
});
|
|
631
|
-
sessions.set(msg.sessionId, session);
|
|
632
|
-
sessionsCreating.delete(msg.sessionId);
|
|
633
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} created mapSize=${sessions.size} resuming=${Boolean(getClaudeContext(msg.sessionId) || getCodexContext(msg.sessionId))}`);
|
|
634
|
-
sendStatus(`Starting ${cliType} in ${msg.workingDir || workspaceRoot}`);
|
|
635
|
-
session.on("output", (text) => {
|
|
636
|
-
ws.send(JSON.stringify({ type: "output", sessionId: msg.sessionId, content: text }));
|
|
637
|
-
relaySend({ type: "output", sessionId: msg.sessionId, content: text });
|
|
638
|
-
});
|
|
639
|
-
session.on("status", (text) => {
|
|
640
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} status: ${text}`);
|
|
641
|
-
ws.send(JSON.stringify({ type: "status", sessionId: msg.sessionId, content: text }));
|
|
642
|
-
relaySend({ type: "status", sessionId: msg.sessionId, content: text });
|
|
643
|
-
});
|
|
644
|
-
session.on("error", (text) => {
|
|
645
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} error: ${text.substring(0, 120)}`);
|
|
646
|
-
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, content: text }));
|
|
647
|
-
relaySend({ type: "error", sessionId: msg.sessionId, content: text });
|
|
648
|
-
});
|
|
649
|
-
session.on("done", (result) => {
|
|
650
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} done code=${result.code} outputLen=${result.fullOutput.length}`);
|
|
651
|
-
const s = sessions.get(msg.sessionId);
|
|
652
|
-
if (s?.claudeSessionId) {
|
|
653
|
-
setClaudeContext(msg.sessionId, s.claudeSessionId);
|
|
654
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} saved claude context=${s.claudeSessionId}`);
|
|
655
|
-
}
|
|
656
|
-
if (s?.codexThreadId) {
|
|
657
|
-
setCodexContext(msg.sessionId, s.codexThreadId);
|
|
658
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} saved codex thread=${s.codexThreadId}`);
|
|
659
|
-
}
|
|
660
|
-
ws.send(JSON.stringify({ type: "done", sessionId: msg.sessionId, content: result.fullOutput }));
|
|
661
|
-
relaySend({ type: "done", sessionId: msg.sessionId, content: result.fullOutput });
|
|
662
|
-
sessions.delete(msg.sessionId);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
session.sendPrompt(msg.content, Array.isArray(msg.attachments) ? msg.attachments : []);
|
|
859
|
+
function handlePrompt(ws, msg, ownerSessionIds) {
|
|
860
|
+
handlePromptCommon(msg, {
|
|
861
|
+
source: "bridge",
|
|
862
|
+
ownerSessionIds,
|
|
863
|
+
sendOutput: (content) => {
|
|
864
|
+
ws.send(JSON.stringify({ type: "output", sessionId: msg.sessionId, content }));
|
|
865
|
+
relaySend({ type: "output", sessionId: msg.sessionId, content });
|
|
866
|
+
},
|
|
867
|
+
sendStatus: (content) => {
|
|
868
|
+
ws.send(JSON.stringify({ type: "status", sessionId: msg.sessionId, content }));
|
|
869
|
+
relaySend({ type: "status", sessionId: msg.sessionId, content });
|
|
870
|
+
},
|
|
871
|
+
sendError: (content) => {
|
|
872
|
+
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, content }));
|
|
873
|
+
relaySend({ type: "error", sessionId: msg.sessionId, content });
|
|
874
|
+
},
|
|
875
|
+
sendDone: (content) => {
|
|
876
|
+
ws.send(JSON.stringify({ type: "done", sessionId: msg.sessionId, content }));
|
|
877
|
+
relaySend({ type: "done", sessionId: msg.sessionId, content });
|
|
878
|
+
},
|
|
879
|
+
});
|
|
666
880
|
}
|
|
667
881
|
function connectToBackendRelay(backendUrl) {
|
|
668
882
|
if (backendRelayUrl === backendUrl && currentRelayWs?.readyState === ws_1.WebSocket.OPEN) {
|
|
@@ -680,6 +894,7 @@ function connectToBackendRelay(backendUrl) {
|
|
|
680
894
|
console.log(` [relay] connecting to ${url}`);
|
|
681
895
|
const ws = new ws_1.WebSocket(url);
|
|
682
896
|
let pingInterval = null;
|
|
897
|
+
const relaySessionIds = new Set();
|
|
683
898
|
const startAt = Date.now();
|
|
684
899
|
ws.on("open", () => {
|
|
685
900
|
console.log(` ✓ Connected to backend relay after ${Date.now() - startAt}ms`);
|
|
@@ -713,22 +928,14 @@ function connectToBackendRelay(backendUrl) {
|
|
|
713
928
|
return;
|
|
714
929
|
if (msg.type === "prompt") {
|
|
715
930
|
console.log(` [relay] ← prompt from backend session=${String(msg.sessionId || "-").substring(0, 8)} cli=${msg.cli || "unknown"}`);
|
|
716
|
-
handlePromptFromRelay(
|
|
931
|
+
handlePromptFromRelay(msg, relaySessionIds);
|
|
717
932
|
}
|
|
718
933
|
if (msg.type === "kill") {
|
|
719
934
|
console.log(` [relay] ← kill from backend session=${String(msg.sessionId || "-").substring(0, 8)}`);
|
|
720
935
|
const session = sessions.get(msg.sessionId);
|
|
721
936
|
if (session) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
setClaudeContext(msg.sessionId, session.claudeSessionId);
|
|
725
|
-
}
|
|
726
|
-
if (session.codexThreadId) {
|
|
727
|
-
setCodexContext(msg.sessionId, session.codexThreadId);
|
|
728
|
-
}
|
|
729
|
-
sessions.delete(msg.sessionId);
|
|
730
|
-
sessionsCreating.delete(msg.sessionId);
|
|
731
|
-
session.kill();
|
|
937
|
+
killSession(msg.sessionId, "relay kill request");
|
|
938
|
+
relaySessionIds.delete(msg.sessionId);
|
|
732
939
|
}
|
|
733
940
|
}
|
|
734
941
|
}
|
|
@@ -740,6 +947,13 @@ function connectToBackendRelay(backendUrl) {
|
|
|
740
947
|
ws.on("close", (code, reason) => {
|
|
741
948
|
if (pingInterval)
|
|
742
949
|
clearInterval(pingInterval);
|
|
950
|
+
if (currentRelayWs === ws) {
|
|
951
|
+
currentRelayWs = null;
|
|
952
|
+
}
|
|
953
|
+
for (const sessionId of relaySessionIds) {
|
|
954
|
+
killSession(sessionId, "relay disconnect");
|
|
955
|
+
}
|
|
956
|
+
relaySessionIds.clear();
|
|
743
957
|
const delay = relayRetryDelay;
|
|
744
958
|
console.log(` Backend relay disconnected after ${Date.now() - startAt}ms code=${code} reason="${reason?.toString() ?? ""}". Reconnecting in ${delay / 1000}s...`);
|
|
745
959
|
backendRelayConnecting = false;
|
|
@@ -772,57 +986,14 @@ function relaySend(data) {
|
|
|
772
986
|
pendingRelayMessages.push(data);
|
|
773
987
|
}
|
|
774
988
|
}
|
|
775
|
-
function handlePromptFromRelay(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
relaySend({ type: "
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
if (sessionsCreating.has(msg.sessionId)) {
|
|
786
|
-
console.log(` [relay] session=${msg.sessionId.substring(0, 8)} already being created, skipping duplicate`);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
let session = sessions.get(msg.sessionId);
|
|
790
|
-
if (!session) {
|
|
791
|
-
sessionsCreating.add(msg.sessionId);
|
|
792
|
-
session = new session_1.CLISession({
|
|
793
|
-
sessionId: msg.sessionId,
|
|
794
|
-
cliBinary: binary,
|
|
795
|
-
workingDir: msg.workingDir || workspaceRoot,
|
|
796
|
-
initialClaudeSessionId: getClaudeContext(msg.sessionId),
|
|
797
|
-
initialCodexThreadId: getCodexContext(msg.sessionId),
|
|
798
|
-
});
|
|
799
|
-
sessions.set(msg.sessionId, session);
|
|
800
|
-
sessionsCreating.delete(msg.sessionId);
|
|
801
|
-
sendStatus(`Starting ${cliType} in ${msg.workingDir || workspaceRoot}`);
|
|
802
|
-
session.on("output", (text) => {
|
|
803
|
-
relaySend({ type: "output", sessionId: msg.sessionId, content: text });
|
|
804
|
-
});
|
|
805
|
-
session.on("status", (text) => {
|
|
806
|
-
console.log(` [relay] session=${msg.sessionId.substring(0, 8)} status: ${text}`);
|
|
807
|
-
relaySend({ type: "status", sessionId: msg.sessionId, content: text });
|
|
808
|
-
});
|
|
809
|
-
session.on("error", (text) => {
|
|
810
|
-
console.log(` ✗ Session error: ${text.substring(0, 100)}`);
|
|
811
|
-
relaySend({ type: "error", sessionId: msg.sessionId, content: text });
|
|
812
|
-
});
|
|
813
|
-
session.on("done", (result) => {
|
|
814
|
-
console.log(` ✓ Session done (exit ${result.code}), output length: ${result.fullOutput.length}`);
|
|
815
|
-
const s = sessions.get(msg.sessionId);
|
|
816
|
-
if (s?.claudeSessionId) {
|
|
817
|
-
setClaudeContext(msg.sessionId, s.claudeSessionId);
|
|
818
|
-
}
|
|
819
|
-
if (s?.codexThreadId) {
|
|
820
|
-
setCodexContext(msg.sessionId, s.codexThreadId);
|
|
821
|
-
}
|
|
822
|
-
relaySend({ type: "done", sessionId: msg.sessionId, content: result.fullOutput });
|
|
823
|
-
sessions.delete(msg.sessionId);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
session.sendPrompt(msg.content, Array.isArray(msg.attachments) ? msg.attachments : []);
|
|
989
|
+
function handlePromptFromRelay(msg, ownerSessionIds) {
|
|
990
|
+
handlePromptCommon(msg, {
|
|
991
|
+
source: "relay",
|
|
992
|
+
ownerSessionIds,
|
|
993
|
+
sendOutput: (content) => relaySend({ type: "output", sessionId: msg.sessionId, content }),
|
|
994
|
+
sendStatus: (content) => relaySend({ type: "status", sessionId: msg.sessionId, content }),
|
|
995
|
+
sendError: (content) => relaySend({ type: "error", sessionId: msg.sessionId, content }),
|
|
996
|
+
sendDone: (content) => relaySend({ type: "done", sessionId: msg.sessionId, content }),
|
|
997
|
+
});
|
|
827
998
|
}
|
|
828
999
|
//# sourceMappingURL=bridge.js.map
|