cosmoremote 2.0.22 → 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 +348 -151
- 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/cli.js +13 -1
- package/dist/cli.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 +19 -1
- package/dist/session.js +369 -20
- package/dist/session.js.map +1 -1
- package/package.json +2 -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);
|
|
@@ -54,6 +58,58 @@ function refreshProjectFolders() {
|
|
|
54
58
|
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), ".cosmoremote");
|
|
55
59
|
const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, "config.json");
|
|
56
60
|
const SESSION_CONTEXTS_FILE = (0, path_1.join)(CONFIG_DIR, "session-contexts.json");
|
|
61
|
+
function getPackageVersion() {
|
|
62
|
+
try {
|
|
63
|
+
const packageJsonPath = (0, path_1.join)(__dirname, "..", "package.json");
|
|
64
|
+
const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, "utf-8"));
|
|
65
|
+
return packageJson.version ?? "0.0.0";
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "0.0.0";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function announceCapabilities() {
|
|
72
|
+
if (!currentRelayWs || currentRelayWs.readyState !== ws_1.WebSocket.OPEN)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
currentRelayWs.send(JSON.stringify({
|
|
76
|
+
type: "capabilities",
|
|
77
|
+
bridgeVersion: getPackageVersion(),
|
|
78
|
+
availableClis: getAvailableCliNames(),
|
|
79
|
+
folders: projectFolders,
|
|
80
|
+
conversations: importableConversations,
|
|
81
|
+
workspaceRoot,
|
|
82
|
+
}));
|
|
83
|
+
console.log(` [relay] announced capabilities clis=${getAvailableCliNames().join(",") || "none"} folders=${projectFolders.length} conversations=${importableConversations.length}`);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(` [relay] failed to announce capabilities:`, err);
|
|
87
|
+
}
|
|
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
|
+
}
|
|
57
113
|
function generatePairingCode() {
|
|
58
114
|
return String(Math.floor(Math.random() * 999999)).padStart(6, "0");
|
|
59
115
|
}
|
|
@@ -118,16 +174,26 @@ function loadSessionContexts() {
|
|
|
118
174
|
const raw = (0, fs_1.readFileSync)(SESSION_CONTEXTS_FILE, "utf-8");
|
|
119
175
|
const parsed = JSON.parse(raw);
|
|
120
176
|
const now = Date.now();
|
|
177
|
+
let dropped = 0;
|
|
121
178
|
for (const [sessionId, entry] of Object.entries(parsed)) {
|
|
122
179
|
if (!entry || typeof entry.expiresAt !== "number" || entry.expiresAt <= now)
|
|
123
180
|
continue;
|
|
124
181
|
sessionContexts.set(sessionId, {
|
|
125
182
|
claudeSessionId: typeof entry.claudeSessionId === "string" ? entry.claudeSessionId : undefined,
|
|
126
183
|
codexThreadId: typeof entry.codexThreadId === "string" ? entry.codexThreadId : undefined,
|
|
184
|
+
cursorSessionId: typeof entry.cursorSessionId === "string" ? entry.cursorSessionId : undefined,
|
|
127
185
|
expiresAt: entry.expiresAt,
|
|
128
186
|
});
|
|
129
187
|
}
|
|
130
|
-
|
|
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
|
+
}
|
|
131
197
|
}
|
|
132
198
|
catch (err) {
|
|
133
199
|
console.error(" [context] Failed to read session contexts:", err);
|
|
@@ -153,6 +219,7 @@ function upsertSessionContext(sessionId, patch) {
|
|
|
153
219
|
const entry = {
|
|
154
220
|
claudeSessionId: patch.claudeSessionId ?? existing?.claudeSessionId,
|
|
155
221
|
codexThreadId: patch.codexThreadId ?? existing?.codexThreadId,
|
|
222
|
+
cursorSessionId: patch.cursorSessionId ?? existing?.cursorSessionId,
|
|
156
223
|
expiresAt: Date.now() + SESSION_CONTEXT_TTL_MS,
|
|
157
224
|
};
|
|
158
225
|
sessionContexts.set(sessionId, entry);
|
|
@@ -201,6 +268,53 @@ function getClaudeContext(sessionId) {
|
|
|
201
268
|
function setClaudeContext(sessionId, claudeId) {
|
|
202
269
|
upsertSessionContext(sessionId, { claudeSessionId: claudeId });
|
|
203
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
|
+
}
|
|
204
318
|
function getCodexContext(sessionId) {
|
|
205
319
|
const entry = sessionContexts.get(sessionId);
|
|
206
320
|
if (!entry)
|
|
@@ -215,6 +329,66 @@ function getCodexContext(sessionId) {
|
|
|
215
329
|
function setCodexContext(sessionId, threadId) {
|
|
216
330
|
upsertSessionContext(sessionId, { codexThreadId: threadId });
|
|
217
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
|
+
}
|
|
218
392
|
function checkLocalLimit(sessionToken) {
|
|
219
393
|
const now = Date.now();
|
|
220
394
|
let entry = localMessageCounts.get(sessionToken);
|
|
@@ -264,6 +438,116 @@ function normalizeHttpBaseUrl(rawUrl) {
|
|
|
264
438
|
const parsed = new URL(normalized);
|
|
265
439
|
return `${parsed.protocol}//${parsed.host}`;
|
|
266
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
|
+
}
|
|
267
551
|
function sha256Hex(input) {
|
|
268
552
|
return crypto_1.default.createHash("sha256").update(input).digest("hex");
|
|
269
553
|
}
|
|
@@ -376,6 +660,7 @@ async function startBridge(opts) {
|
|
|
376
660
|
setInterval(() => {
|
|
377
661
|
refreshImportableConversations();
|
|
378
662
|
refreshProjectFolders();
|
|
663
|
+
announceCapabilities();
|
|
379
664
|
}, 60_000);
|
|
380
665
|
loadSessionContexts();
|
|
381
666
|
// Check for updates in background (non-blocking)
|
|
@@ -404,12 +689,24 @@ async function startBridge(opts) {
|
|
|
404
689
|
console.log(" [env] XDG_CACHE_HOME:", process.env.XDG_CACHE_HOME || "unset");
|
|
405
690
|
console.log(" [env] BACKEND_URL:", opts.backendUrl || process.env.BACKEND_URL || "unset");
|
|
406
691
|
console.log(" Available CLIs:");
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
706
|
console.log(` Workspace: ${workspaceRoot}`);
|
|
410
707
|
console.log();
|
|
411
|
-
if (!clis.claude && !clis.codex) {
|
|
412
|
-
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.");
|
|
413
710
|
process.exit(1);
|
|
414
711
|
}
|
|
415
712
|
if (opts.backendUrl) {
|
|
@@ -524,7 +821,7 @@ function handleConnection(ws, sessionToken) {
|
|
|
524
821
|
const ticket = verifyPromptTicket(msg.promptTicket, msg);
|
|
525
822
|
console.log(` [ticket] verified session=${ticket.sessionId.substring(0, 8)} msg=${ticket.messageId.substring(0, 8)} exp=${ticket.expiresAt}`);
|
|
526
823
|
connectionSessionIds.add(msg.sessionId);
|
|
527
|
-
handlePrompt(ws, msg);
|
|
824
|
+
handlePrompt(ws, msg, connectionSessionIds);
|
|
528
825
|
}
|
|
529
826
|
catch (err) {
|
|
530
827
|
const error = err instanceof Error ? err.message : "Invalid prompt ticket";
|
|
@@ -537,18 +834,8 @@ function handleConnection(ws, sessionToken) {
|
|
|
537
834
|
console.log(` [bridge] incoming kill session=${String(msg.sessionId || "-").substring(0, 8)}`);
|
|
538
835
|
const session = sessions.get(msg.sessionId);
|
|
539
836
|
if (session) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
setClaudeContext(msg.sessionId, session.claudeSessionId);
|
|
543
|
-
console.log(` [bridge] saved claude context before kill=${session.claudeSessionId}`);
|
|
544
|
-
}
|
|
545
|
-
if (session.codexThreadId) {
|
|
546
|
-
setCodexContext(msg.sessionId, session.codexThreadId);
|
|
547
|
-
console.log(` [bridge] saved codex context before kill=${session.codexThreadId}`);
|
|
548
|
-
}
|
|
549
|
-
sessions.delete(msg.sessionId);
|
|
550
|
-
sessionsCreating.delete(msg.sessionId);
|
|
551
|
-
session.kill();
|
|
837
|
+
killSession(msg.sessionId, "local kill request");
|
|
838
|
+
connectionSessionIds.delete(msg.sessionId);
|
|
552
839
|
ws.send(JSON.stringify({ type: "done", sessionId: msg.sessionId, content: "[killed]" }));
|
|
553
840
|
}
|
|
554
841
|
}
|
|
@@ -563,72 +850,33 @@ function handleConnection(ws, sessionToken) {
|
|
|
563
850
|
for (const sessionId of connectionSessionIds) {
|
|
564
851
|
const session = sessions.get(sessionId);
|
|
565
852
|
if (session) {
|
|
566
|
-
|
|
567
|
-
session.kill();
|
|
568
|
-
sessions.delete(sessionId);
|
|
569
|
-
sessionsCreating.delete(sessionId);
|
|
853
|
+
killSession(sessionId, "client disconnect");
|
|
570
854
|
}
|
|
571
855
|
}
|
|
572
856
|
connectionSessionIds.clear();
|
|
573
857
|
});
|
|
574
858
|
}
|
|
575
|
-
function handlePrompt(ws, msg) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
sessionId: msg.sessionId,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
initialCodexThreadId: getCodexContext(msg.sessionId),
|
|
597
|
-
});
|
|
598
|
-
sessions.set(msg.sessionId, session);
|
|
599
|
-
sessionsCreating.delete(msg.sessionId);
|
|
600
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} created mapSize=${sessions.size} resuming=${Boolean(getClaudeContext(msg.sessionId) || getCodexContext(msg.sessionId))}`);
|
|
601
|
-
session.on("output", (text) => {
|
|
602
|
-
ws.send(JSON.stringify({ type: "output", sessionId: msg.sessionId, content: text }));
|
|
603
|
-
relaySend({ type: "output", sessionId: msg.sessionId, content: text });
|
|
604
|
-
});
|
|
605
|
-
session.on("status", (text) => {
|
|
606
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} status: ${text}`);
|
|
607
|
-
ws.send(JSON.stringify({ type: "status", sessionId: msg.sessionId, content: text }));
|
|
608
|
-
relaySend({ type: "status", sessionId: msg.sessionId, content: text });
|
|
609
|
-
});
|
|
610
|
-
session.on("error", (text) => {
|
|
611
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} error: ${text.substring(0, 120)}`);
|
|
612
|
-
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, content: text }));
|
|
613
|
-
relaySend({ type: "error", sessionId: msg.sessionId, content: text });
|
|
614
|
-
});
|
|
615
|
-
session.on("done", (result) => {
|
|
616
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} done code=${result.code} outputLen=${result.fullOutput.length}`);
|
|
617
|
-
const s = sessions.get(msg.sessionId);
|
|
618
|
-
if (s?.claudeSessionId) {
|
|
619
|
-
setClaudeContext(msg.sessionId, s.claudeSessionId);
|
|
620
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} saved claude context=${s.claudeSessionId}`);
|
|
621
|
-
}
|
|
622
|
-
if (s?.codexThreadId) {
|
|
623
|
-
setCodexContext(msg.sessionId, s.codexThreadId);
|
|
624
|
-
console.log(` [bridge] session=${msg.sessionId.substring(0, 8)} saved codex thread=${s.codexThreadId}`);
|
|
625
|
-
}
|
|
626
|
-
ws.send(JSON.stringify({ type: "done", sessionId: msg.sessionId, content: result.fullOutput }));
|
|
627
|
-
relaySend({ type: "done", sessionId: msg.sessionId, content: result.fullOutput });
|
|
628
|
-
sessions.delete(msg.sessionId);
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
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
|
+
});
|
|
632
880
|
}
|
|
633
881
|
function connectToBackendRelay(backendUrl) {
|
|
634
882
|
if (backendRelayUrl === backendUrl && currentRelayWs?.readyState === ws_1.WebSocket.OPEN) {
|
|
@@ -646,6 +894,7 @@ function connectToBackendRelay(backendUrl) {
|
|
|
646
894
|
console.log(` [relay] connecting to ${url}`);
|
|
647
895
|
const ws = new ws_1.WebSocket(url);
|
|
648
896
|
let pingInterval = null;
|
|
897
|
+
const relaySessionIds = new Set();
|
|
649
898
|
const startAt = Date.now();
|
|
650
899
|
ws.on("open", () => {
|
|
651
900
|
console.log(` ✓ Connected to backend relay after ${Date.now() - startAt}ms`);
|
|
@@ -655,18 +904,7 @@ function connectToBackendRelay(backendUrl) {
|
|
|
655
904
|
// Announce indexed folders + importable conversations so the iOS folder
|
|
656
905
|
// picker and "Continue ended one" mode have data on first open. The
|
|
657
906
|
// backend caches this in-memory keyed by macId.
|
|
658
|
-
|
|
659
|
-
ws.send(JSON.stringify({
|
|
660
|
-
type: "capabilities",
|
|
661
|
-
folders: projectFolders,
|
|
662
|
-
conversations: importableConversations,
|
|
663
|
-
workspaceRoot,
|
|
664
|
-
}));
|
|
665
|
-
console.log(` [relay] announced capabilities folders=${projectFolders.length} conversations=${importableConversations.length}`);
|
|
666
|
-
}
|
|
667
|
-
catch (err) {
|
|
668
|
-
console.error(` [relay] failed to announce capabilities:`, err);
|
|
669
|
-
}
|
|
907
|
+
announceCapabilities();
|
|
670
908
|
if (pendingRelayMessages.length > 0) {
|
|
671
909
|
console.log(` ↺ Flushing ${pendingRelayMessages.length} queued relay messages`);
|
|
672
910
|
const toFlush = pendingRelayMessages.splice(0);
|
|
@@ -690,22 +928,14 @@ function connectToBackendRelay(backendUrl) {
|
|
|
690
928
|
return;
|
|
691
929
|
if (msg.type === "prompt") {
|
|
692
930
|
console.log(` [relay] ← prompt from backend session=${String(msg.sessionId || "-").substring(0, 8)} cli=${msg.cli || "unknown"}`);
|
|
693
|
-
handlePromptFromRelay(
|
|
931
|
+
handlePromptFromRelay(msg, relaySessionIds);
|
|
694
932
|
}
|
|
695
933
|
if (msg.type === "kill") {
|
|
696
934
|
console.log(` [relay] ← kill from backend session=${String(msg.sessionId || "-").substring(0, 8)}`);
|
|
697
935
|
const session = sessions.get(msg.sessionId);
|
|
698
936
|
if (session) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
setClaudeContext(msg.sessionId, session.claudeSessionId);
|
|
702
|
-
}
|
|
703
|
-
if (session.codexThreadId) {
|
|
704
|
-
setCodexContext(msg.sessionId, session.codexThreadId);
|
|
705
|
-
}
|
|
706
|
-
sessions.delete(msg.sessionId);
|
|
707
|
-
sessionsCreating.delete(msg.sessionId);
|
|
708
|
-
session.kill();
|
|
937
|
+
killSession(msg.sessionId, "relay kill request");
|
|
938
|
+
relaySessionIds.delete(msg.sessionId);
|
|
709
939
|
}
|
|
710
940
|
}
|
|
711
941
|
}
|
|
@@ -717,6 +947,13 @@ function connectToBackendRelay(backendUrl) {
|
|
|
717
947
|
ws.on("close", (code, reason) => {
|
|
718
948
|
if (pingInterval)
|
|
719
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();
|
|
720
957
|
const delay = relayRetryDelay;
|
|
721
958
|
console.log(` Backend relay disconnected after ${Date.now() - startAt}ms code=${code} reason="${reason?.toString() ?? ""}". Reconnecting in ${delay / 1000}s...`);
|
|
722
959
|
backendRelayConnecting = false;
|
|
@@ -749,54 +986,14 @@ function relaySend(data) {
|
|
|
749
986
|
pendingRelayMessages.push(data);
|
|
750
987
|
}
|
|
751
988
|
}
|
|
752
|
-
function handlePromptFromRelay(
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
relaySend({ type: "
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
console.log(` [relay] session=${msg.sessionId.substring(0, 8)} already being created, skipping duplicate`);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
let session = sessions.get(msg.sessionId);
|
|
765
|
-
if (!session) {
|
|
766
|
-
sessionsCreating.add(msg.sessionId);
|
|
767
|
-
session = new session_1.CLISession({
|
|
768
|
-
sessionId: msg.sessionId,
|
|
769
|
-
cliBinary: binary,
|
|
770
|
-
workingDir: msg.workingDir || workspaceRoot,
|
|
771
|
-
initialClaudeSessionId: getClaudeContext(msg.sessionId),
|
|
772
|
-
initialCodexThreadId: getCodexContext(msg.sessionId),
|
|
773
|
-
});
|
|
774
|
-
sessions.set(msg.sessionId, session);
|
|
775
|
-
sessionsCreating.delete(msg.sessionId);
|
|
776
|
-
session.on("output", (text) => {
|
|
777
|
-
relaySend({ type: "output", sessionId: msg.sessionId, content: text });
|
|
778
|
-
});
|
|
779
|
-
session.on("status", (text) => {
|
|
780
|
-
console.log(` [relay] session=${msg.sessionId.substring(0, 8)} status: ${text}`);
|
|
781
|
-
relaySend({ type: "status", sessionId: msg.sessionId, content: text });
|
|
782
|
-
});
|
|
783
|
-
session.on("error", (text) => {
|
|
784
|
-
console.log(` ✗ Session error: ${text.substring(0, 100)}`);
|
|
785
|
-
relaySend({ type: "error", sessionId: msg.sessionId, content: text });
|
|
786
|
-
});
|
|
787
|
-
session.on("done", (result) => {
|
|
788
|
-
console.log(` ✓ Session done (exit ${result.code}), output length: ${result.fullOutput.length}`);
|
|
789
|
-
const s = sessions.get(msg.sessionId);
|
|
790
|
-
if (s?.claudeSessionId) {
|
|
791
|
-
setClaudeContext(msg.sessionId, s.claudeSessionId);
|
|
792
|
-
}
|
|
793
|
-
if (s?.codexThreadId) {
|
|
794
|
-
setCodexContext(msg.sessionId, s.codexThreadId);
|
|
795
|
-
}
|
|
796
|
-
relaySend({ type: "done", sessionId: msg.sessionId, content: result.fullOutput });
|
|
797
|
-
sessions.delete(msg.sessionId);
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
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
|
+
});
|
|
801
998
|
}
|
|
802
999
|
//# sourceMappingURL=bridge.js.map
|