@shawnowen/comet-mcp 2.3.1 → 2.4.2

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.
Files changed (65) hide show
  1. package/README.md +97 -19
  2. package/dist/alert-dispatcher.d.ts +23 -0
  3. package/dist/alert-dispatcher.js +101 -0
  4. package/dist/binding-reaper.d.ts +46 -0
  5. package/dist/binding-reaper.js +73 -0
  6. package/dist/bound-session.d.ts +23 -0
  7. package/dist/bound-session.js +119 -0
  8. package/dist/bridge-config.d.ts +6 -0
  9. package/dist/bridge-config.js +78 -0
  10. package/dist/cdp-client.d.ts +40 -4
  11. package/dist/cdp-client.js +502 -155
  12. package/dist/comet-ai.d.ts +15 -0
  13. package/dist/comet-ai.js +114 -38
  14. package/dist/delegate-binding.d.ts +19 -0
  15. package/dist/delegate-binding.js +73 -0
  16. package/dist/http-server.js +2188 -47
  17. package/dist/index.js +3545 -788
  18. package/dist/observer.d.ts +47 -0
  19. package/dist/observer.js +516 -0
  20. package/dist/project-config.d.ts +46 -0
  21. package/dist/project-config.js +166 -0
  22. package/dist/session-registry.d.ts +57 -0
  23. package/dist/session-registry.js +500 -0
  24. package/dist/sidecar-artifacts.d.ts +49 -0
  25. package/dist/sidecar-artifacts.js +146 -0
  26. package/dist/snapshot-capture.d.ts +3 -0
  27. package/dist/snapshot-capture.js +91 -0
  28. package/dist/tab-group-archive.js +3 -1
  29. package/dist/tab-groups.d.ts +28 -1
  30. package/dist/tab-groups.js +205 -3
  31. package/dist/types.d.ts +237 -0
  32. package/dist/window-bindings.d.ts +160 -0
  33. package/dist/window-bindings.js +561 -0
  34. package/extension/background.js +1577 -300
  35. package/extension/icons/icon.svg +9 -0
  36. package/extension/icons/icon128.png +0 -0
  37. package/extension/icons/icon16.png +0 -0
  38. package/extension/icons/icon48.png +0 -0
  39. package/extension/manifest.json +34 -4
  40. package/extension/perplexity-capability-manifest.json +1181 -0
  41. package/extension/perplexity-capability-manifest.schema.json +142 -0
  42. package/extension/session-logic.js +3054 -0
  43. package/extension/session-manager.html +311 -0
  44. package/extension/sidepanel.css +5338 -528
  45. package/extension/sidepanel.html +282 -2
  46. package/extension/sidepanel.js +10604 -950
  47. package/extension/window-policy.js +162 -0
  48. package/package.json +10 -7
  49. package/vendor/lifecycle-mcp-adapter.mjs +103 -0
  50. package/vendor/lifecycle-metadata.mjs +252 -0
  51. package/vendor/readiness-report.mjs +742 -0
  52. package/dist/cdp-client.d.ts.map +0 -1
  53. package/dist/cdp-client.js.map +0 -1
  54. package/dist/comet-ai.d.ts.map +0 -1
  55. package/dist/comet-ai.js.map +0 -1
  56. package/dist/http-server.d.ts.map +0 -1
  57. package/dist/http-server.js.map +0 -1
  58. package/dist/index.d.ts.map +0 -1
  59. package/dist/index.js.map +0 -1
  60. package/dist/tab-group-archive.d.ts.map +0 -1
  61. package/dist/tab-group-archive.js.map +0 -1
  62. package/dist/tab-groups.d.ts.map +0 -1
  63. package/dist/tab-groups.js.map +0 -1
  64. package/dist/types.d.ts.map +0 -1
  65. package/dist/types.js.map +0 -1
@@ -0,0 +1,500 @@
1
+ // Session Registry — per-agent session management with isolation guarantees (Spec 034)
2
+ // NEVER kills the browser process. NEVER closes other agents' tabs.
3
+ import CDP from "chrome-remote-interface";
4
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join, dirname } from "path";
7
+ import { CometCDPClient } from "./cdp-client.js";
8
+ import { CometAI } from "./comet-ai.js";
9
+ import { tabGroupsClient } from "./tab-groups.js";
10
+ import { captureTaskThreadSnapshot } from "./snapshot-capture.js";
11
+ import { dispatchAlert } from "./alert-dispatcher.js";
12
+ import { deriveCodexSessionIdentity, windowBindingStore } from "./window-bindings.js";
13
+ const MANIFEST_PATH = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
14
+ const ORPHAN_THRESHOLD_MS = 120 * 60 * 1000; // 120 minutes (Spec 016, Clarification Q2 — activity-based, resets on any CDP/heartbeat/tool activity)
15
+ // ---- Advisory Lock (FR-017) ----
16
+ class Mutex {
17
+ queue = Promise.resolve();
18
+ async acquire() {
19
+ let release;
20
+ const next = new Promise((resolve) => {
21
+ release = resolve;
22
+ });
23
+ const prev = this.queue;
24
+ this.queue = next;
25
+ await prev;
26
+ return release;
27
+ }
28
+ }
29
+ function resolveAgentRuntimeProfile(profile) {
30
+ const requested = profile?.trim() || "agent";
31
+ const canonical = requested === "oe"
32
+ ? "agent"
33
+ : requested === "moon" || requested === "human"
34
+ ? "human"
35
+ : requested;
36
+ if (canonical !== "agent") {
37
+ throw new Error(`PROFILE_OWNERSHIP_VIOLATION: profile ${requested} is human-owned or unknown and cannot be used by normal agent sessions`);
38
+ }
39
+ return {
40
+ profileId: "agent",
41
+ profileAlias: requested === "agent" ? "oe" : requested,
42
+ profileOwner: "agent",
43
+ };
44
+ }
45
+ // Spec 037: Generate a concise session name from a task goal description
46
+ export function generateSessionName(taskGoal) {
47
+ const fillers = [
48
+ /^(please\s+)/i,
49
+ /^(i\s+want\s+to\s+)/i,
50
+ /^(i\s+need\s+to\s+)/i,
51
+ /^(can\s+you\s+)/i,
52
+ /^(research\s+and\s+find\s+all\s+of\s+my\s+)/i,
53
+ /^(find\s+and\s+)/i,
54
+ /^(go\s+and\s+)/i,
55
+ ];
56
+ let text = taskGoal.trim();
57
+ for (const filler of fillers) {
58
+ text = text.replace(filler, "");
59
+ }
60
+ // Remove trailing periods and extra whitespace
61
+ text = text
62
+ .replace(/\.\s*$/, "")
63
+ .replace(/\s+/g, " ")
64
+ .trim();
65
+ // Title case
66
+ text = text
67
+ .split(" ")
68
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
69
+ .join(" ");
70
+ // Truncate to 50 chars at word boundary
71
+ if (text.length > 50) {
72
+ text = text.substring(0, 50).replace(/\s\S*$/, "");
73
+ }
74
+ return text || "Untitled Session";
75
+ }
76
+ // ---- Session Registry ----
77
+ export class SessionRegistry {
78
+ sessions = new Map();
79
+ currentSessionKey = null;
80
+ tabGroupMutex = new Mutex();
81
+ async register(options = {}) {
82
+ const agentId = options.agentId || process.env.COMET_AGENT_ID || "claude-code";
83
+ const taskThreadId = options.taskThreadId || process.env.COMET_TASK_GROUP || `session-${Date.now()}`;
84
+ const url = options.url || "https://www.perplexity.ai/";
85
+ const tabGroupColor = options.tabGroupColor || "blue";
86
+ const port = options.port || 9222;
87
+ const sessionKey = `${agentId}:${taskThreadId}`;
88
+ const runtimeProfile = resolveAgentRuntimeProfile(options.profile);
89
+ const codexIdentity = deriveCodexSessionIdentity({
90
+ codexSessionId: options.codexSessionId,
91
+ projectThreadId: options.projectThreadId,
92
+ projectThreadFamily: options.projectThreadFamily,
93
+ worktreePath: options.worktreePath,
94
+ repoSlug: options.repoSlug,
95
+ branchName: options.branchName,
96
+ sessionKey: options.codexSessionKey,
97
+ role: options.codexSessionRole,
98
+ strict: options.strictCodexIdentity,
99
+ fallbackAgentId: agentId,
100
+ fallbackTaskThreadId: taskThreadId,
101
+ }, { cwd: process.cwd() });
102
+ // Reap orphaned sessions before creating new one (FR-015)
103
+ await this.reapOrphans();
104
+ // 1. Ensure browser is running (never kill, retry with backoff)
105
+ const startResult = await this.ensureBrowserRunning(port);
106
+ // 2. Create a dedicated top-display full-screen bounds window for this session.
107
+ const cdpClient = new CometCDPClient();
108
+ const initialUrl = `about:blank#comet-session-${encodeURIComponent(sessionKey)}-${Date.now()}`;
109
+ const windowTab = await tabGroupsClient.createTopDisplayFullscreenWindowWithTab(initialUrl);
110
+ const newTab = await cdpClient.waitForTargetUrl(initialUrl);
111
+ await new Promise((resolve) => setTimeout(resolve, 2000));
112
+ // 3. Connect to the new tab using its unique ID (FR-016)
113
+ await cdpClient.connect(newTab.id);
114
+ await cdpClient.navigate(url, true);
115
+ // 4. Position on top display
116
+ await cdpClient.positionOnTopDisplay(newTab.id);
117
+ // 5. Create tab group with advisory lock (FR-017)
118
+ let tabGroupId = null;
119
+ let chromeTabId = null;
120
+ let windowId = null;
121
+ const release = await this.tabGroupMutex.acquire();
122
+ try {
123
+ const result = await this.createTabGroup(cdpClient, newTab, taskThreadId, tabGroupColor, windowTab.tabId, windowTab.windowId);
124
+ tabGroupId = result.tabGroupId;
125
+ chromeTabId = result.chromeTabId;
126
+ windowId = result.windowId;
127
+ }
128
+ finally {
129
+ release();
130
+ }
131
+ windowId ??= windowTab.windowId ?? (await this.getWindowIdForTarget(newTab.id, port));
132
+ if (windowId === null) {
133
+ throw new Error(`Unable to resolve Comet window ID for target ${newTab.id}`);
134
+ }
135
+ // 6. Create CometAI bound to this session's CDP client (Spec 034, Phase 5)
136
+ const cometAI = new CometAI(cdpClient);
137
+ // Spec 037: Generate session name from task goal
138
+ const sessionName = options.taskGoal ? generateSessionName(options.taskGoal) : undefined;
139
+ // Spec 037 T024: Detect orchestrator/subagent hierarchy — if another session
140
+ // already owns this tab group, this session is a subagent of that orchestrator.
141
+ let role = options.role || "orchestrator";
142
+ let parentSessionId = null;
143
+ if (tabGroupId !== null) {
144
+ for (const [existingKey, existingSession] of this.sessions) {
145
+ if (existingKey !== sessionKey &&
146
+ existingSession.tabGroupId === tabGroupId &&
147
+ existingSession.role === "orchestrator") {
148
+ role = "subagent";
149
+ parentSessionId = existingKey;
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ const bindingWindowId = windowId ?? windowTab.windowId;
155
+ const bindingResult = await windowBindingStore.createOrReuse({
156
+ ...codexIdentity,
157
+ windowId: bindingWindowId,
158
+ tabGroupId,
159
+ targetId: newTab.id,
160
+ profileId: runtimeProfile.profileId,
161
+ profileAlias: runtimeProfile.profileAlias,
162
+ profileOwner: runtimeProfile.profileOwner,
163
+ });
164
+ const session = {
165
+ agentId,
166
+ taskThreadId,
167
+ sessionKey,
168
+ targetId: newTab.id,
169
+ chromeTabId,
170
+ tabGroupId,
171
+ tabGroupColor,
172
+ createdAt: Date.now(),
173
+ lastActivity: Date.now(),
174
+ status: "active",
175
+ role,
176
+ sessionName,
177
+ taskGoal: options.taskGoal,
178
+ orchestratorUrl: undefined,
179
+ parentSessionId,
180
+ codexIdentity,
181
+ codexBinding: bindingResult.binding,
182
+ profileId: runtimeProfile.profileId,
183
+ profileAlias: runtimeProfile.profileAlias,
184
+ profileOwner: runtimeProfile.profileOwner,
185
+ cdpClient,
186
+ cometAI,
187
+ };
188
+ this.sessions.set(sessionKey, session);
189
+ this.currentSessionKey = sessionKey;
190
+ this.persistManifest();
191
+ return session;
192
+ }
193
+ get(sessionKey) {
194
+ const session = this.sessions.get(sessionKey);
195
+ if (session) {
196
+ session.lastActivity = Date.now();
197
+ }
198
+ return session;
199
+ }
200
+ getCurrent() {
201
+ if (!this.currentSessionKey)
202
+ return undefined;
203
+ return this.get(this.currentSessionKey);
204
+ }
205
+ // Spec 037: Update orchestrator URL after first prompt creates the thread
206
+ updateSessionUrl(sessionKey, orchestratorUrl) {
207
+ const session = this.sessions.get(sessionKey);
208
+ if (session) {
209
+ session.orchestratorUrl = orchestratorUrl;
210
+ this.persistManifest();
211
+ }
212
+ }
213
+ // Spec 037: Get manifest entry for a taskThreadId (used by HTTP bridge)
214
+ getManifestEntryByThread(taskThreadId) {
215
+ const manifest = this.loadManifest();
216
+ if (!manifest)
217
+ return undefined;
218
+ return manifest.sessions.find((e) => e.taskThreadId === taskThreadId);
219
+ }
220
+ async release(sessionKey, options = {}) {
221
+ const session = this.sessions.get(sessionKey);
222
+ if (!session)
223
+ return;
224
+ session.status = "disconnecting";
225
+ // Completion-driven snapshot (Spec 016, FR-003, T029)
226
+ if (options.taskStatus) {
227
+ const reason = options.taskStatus === "success" ? "completion" : "manual";
228
+ try {
229
+ await captureTaskThreadSnapshot(session, reason, options.taskStatus);
230
+ }
231
+ catch (snapErr) {
232
+ console.warn(`[comet-bridge] Release snapshot failed for ${sessionKey}: ${snapErr instanceof Error ? snapErr.message : snapErr}`);
233
+ }
234
+ }
235
+ // Update tab group color on release
236
+ if (options.updateGroupColor && session.tabGroupId !== null) {
237
+ try {
238
+ const { tabGroupsClient } = await import("./tab-groups.js");
239
+ await tabGroupsClient.updateGroup({
240
+ groupId: session.tabGroupId,
241
+ color: options.updateGroupColor,
242
+ });
243
+ }
244
+ catch (extErr) {
245
+ console.warn(`[comet-bridge] Tab group color update failed for ${sessionKey}: ${extErr instanceof Error ? extErr.message : extErr}`);
246
+ }
247
+ }
248
+ // Optionally close only this agent's tabs (FR-014)
249
+ if (options.closeTabs && session.chromeTabId !== null) {
250
+ try {
251
+ await session.cdpClient.closeTab(session.targetId);
252
+ }
253
+ catch (closeErr) {
254
+ console.warn(`[comet-bridge] Tab close failed for ${sessionKey}: ${closeErr instanceof Error ? closeErr.message : closeErr}`);
255
+ }
256
+ }
257
+ // Disconnect CDP client — only this session's connection (FR-009)
258
+ try {
259
+ await session.cdpClient.disconnect();
260
+ }
261
+ catch (disconnErr) {
262
+ // Already disconnected — expected during cleanup
263
+ }
264
+ this.sessions.delete(sessionKey);
265
+ if (this.currentSessionKey === sessionKey) {
266
+ this.currentSessionKey = null;
267
+ }
268
+ this.persistManifest();
269
+ }
270
+ async reapOrphans() {
271
+ const manifest = this.loadManifest();
272
+ if (!manifest || manifest.sessions.length === 0)
273
+ return 0;
274
+ const now = Date.now();
275
+ let reaped = 0;
276
+ const surviving = [];
277
+ for (const entry of manifest.sessions) {
278
+ const age = now - entry.lastActivity;
279
+ const isOld = age > ORPHAN_THRESHOLD_MS;
280
+ const isOwnedByThisProcess = this.sessions.has(entry.sessionKey);
281
+ if (isOld && !isOwnedByThisProcess) {
282
+ // Snapshot before close (Spec 016, FR-003, T028)
283
+ const pseudoSession = {
284
+ agentId: entry.agentId,
285
+ taskThreadId: entry.taskThreadId,
286
+ sessionKey: entry.sessionKey,
287
+ targetId: entry.targetId,
288
+ chromeTabId: null,
289
+ tabGroupId: entry.tabGroupId,
290
+ tabGroupColor: "grey",
291
+ createdAt: entry.createdAt,
292
+ lastActivity: entry.lastActivity,
293
+ status: "orphaned",
294
+ role: null,
295
+ };
296
+ let snapshotPath = null;
297
+ try {
298
+ const snapshot = await captureTaskThreadSnapshot(pseudoSession, "orphan-cleanup");
299
+ snapshotPath = `${snapshot.agentId}_${snapshot.taskThreadId}_${snapshot.capturedAt.replace(/[:.]/g, "-")}.json`;
300
+ }
301
+ catch (snapshotErr) {
302
+ // Snapshot failed — skip reaping this session (fail safe, not fail silent)
303
+ dispatchAlert({
304
+ type: "ORPHAN_DETECTED",
305
+ message: `Task Thread ${entry.sessionKey} abandoned (${Math.round(age / 60000)}min idle) but snapshot failed — skipping cleanup.`,
306
+ consumerId: entry.agentId,
307
+ sessionKey: entry.sessionKey,
308
+ context: { age: Math.round(age / 60000), error: String(snapshotErr) },
309
+ });
310
+ surviving.push(entry);
311
+ continue;
312
+ }
313
+ // Verify the target still exists before reaping
314
+ try {
315
+ const response = await fetch(`http://127.0.0.1:9222/json/list`);
316
+ if (response.ok) {
317
+ const targets = (await response.json());
318
+ const targetExists = targets.some((t) => t.id === entry.targetId);
319
+ if (targetExists) {
320
+ // Close the orphaned tab
321
+ try {
322
+ await fetch(`http://127.0.0.1:9222/json/close/${entry.targetId}`);
323
+ }
324
+ catch (closeErr) {
325
+ console.warn(`[comet-bridge] Orphan tab close failed for ${entry.sessionKey}: ${closeErr instanceof Error ? closeErr.message : closeErr}`);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ catch (cdpErr) {
331
+ console.warn(`[comet-bridge] CDP not reachable during orphan reap for ${entry.sessionKey}: ${cdpErr instanceof Error ? cdpErr.message : cdpErr}`);
332
+ }
333
+ // Clean up tab group if possible
334
+ if (entry.tabGroupId !== null) {
335
+ try {
336
+ const { tabGroupsClient } = await import("./tab-groups.js");
337
+ await tabGroupsClient.updateGroup({
338
+ groupId: entry.tabGroupId,
339
+ collapsed: true,
340
+ title: `[reaped] ${entry.agentId}`,
341
+ });
342
+ }
343
+ catch (grpErr) {
344
+ console.warn(`[comet-bridge] Orphan tab group update failed for ${entry.sessionKey}: ${grpErr instanceof Error ? grpErr.message : grpErr}`);
345
+ }
346
+ }
347
+ // Emit ORPHAN_REAPED alert with snapshot path
348
+ dispatchAlert({
349
+ type: "ORPHAN_REAPED",
350
+ message: `Task Thread ${entry.sessionKey} snapshot-closed after ${Math.round(age / 60000)}min idle.`,
351
+ consumerId: entry.agentId,
352
+ sessionKey: entry.sessionKey,
353
+ context: { age: Math.round(age / 60000), snapshotPath },
354
+ });
355
+ try {
356
+ const runIds = entry.codexBinding?.runIds ?? [];
357
+ if (runIds.length > 0) {
358
+ await Promise.all(runIds.map((runId) => windowBindingStore.transitionByRunId(runId, "reaped")));
359
+ }
360
+ else if (entry.codexBinding?.bindingId) {
361
+ await windowBindingStore.transition(entry.codexBinding.bindingId, "reaped");
362
+ }
363
+ }
364
+ catch (bindingErr) {
365
+ console.warn(`[comet-bridge] Orphan binding transition failed for ${entry.sessionKey}: ${bindingErr instanceof Error ? bindingErr.message : bindingErr}`);
366
+ }
367
+ reaped++;
368
+ console.warn(`[comet-bridge] Reaped orphaned session: ${entry.sessionKey} (age: ${Math.round(age / 60000)}min, snapshot: ${snapshotPath})`);
369
+ }
370
+ else {
371
+ surviving.push(entry);
372
+ }
373
+ }
374
+ if (reaped > 0) {
375
+ this.writeManifest({ sessions: surviving, lastUpdated: now });
376
+ }
377
+ return reaped;
378
+ }
379
+ async ensureBrowserRunning(port = 9222) {
380
+ // Pre-restart snapshot: if there are active sessions, snapshot them before restart attempt (Spec 016, T035)
381
+ if (this.sessions.size > 0) {
382
+ for (const [, session] of this.sessions) {
383
+ if (session.status === "active") {
384
+ try {
385
+ await captureTaskThreadSnapshot(session, "crash", "in-progress");
386
+ }
387
+ catch (snapErr) {
388
+ console.error(`[comet-bridge] Pre-restart snapshot FAILED for ${session.sessionKey}: ${snapErr instanceof Error ? snapErr.message : snapErr}`);
389
+ dispatchAlert({
390
+ type: "BROWSER_CRASH",
391
+ message: `Pre-restart snapshot failed for ${session.sessionKey}. Session state may be lost.`,
392
+ sessionKey: session.sessionKey,
393
+ context: { error: String(snapErr) },
394
+ });
395
+ }
396
+ }
397
+ }
398
+ }
399
+ const cdpClient = new CometCDPClient();
400
+ return cdpClient.startComet(port);
401
+ }
402
+ // ---- Private Helpers ----
403
+ async createTabGroup(cdpClient, newTab, threadId, color, chromeTabIdHint, windowIdHint) {
404
+ try {
405
+ // Find the Chrome tab ID for our new tab by matching target URL
406
+ const allTabs = await tabGroupsClient.listTabs();
407
+ const matchedTab = allTabs.find((t) => t.url === newTab.url || t.url === newTab.url + "/");
408
+ // If URL match fails, use the tab with the highest ID (most recently created)
409
+ // This is still safe under the mutex lock — only one connect runs at a time
410
+ const fallbackTab = allTabs.length > 0
411
+ ? allTabs.reduce((latest, tab) => (tab.id > latest.id ? tab : latest), allTabs[0])
412
+ : null;
413
+ const chromeTabId = chromeTabIdHint || matchedTab?.id || fallbackTab?.id || null;
414
+ const windowId = windowIdHint ?? matchedTab?.windowId ?? fallbackTab?.windowId ?? null;
415
+ if (chromeTabId !== null && chromeTabId > 0) {
416
+ const groupTitle = threadId.slice(0, 50);
417
+ const result = await tabGroupsClient.createGroup({
418
+ tabIds: [chromeTabId],
419
+ title: groupTitle,
420
+ color: color,
421
+ });
422
+ return { tabGroupId: result.groupId, chromeTabId, windowId };
423
+ }
424
+ return { tabGroupId: null, chromeTabId: null, windowId: null };
425
+ }
426
+ catch (tgErr) {
427
+ console.warn(`[comet-bridge] Tab group creation failed for ${threadId}: ${tgErr instanceof Error ? tgErr.message : tgErr}`);
428
+ return { tabGroupId: null, chromeTabId: null, windowId: null };
429
+ }
430
+ }
431
+ async getWindowIdForTarget(targetId, port) {
432
+ const client = await CDP({ host: "127.0.0.1", port, target: targetId });
433
+ try {
434
+ const { windowId } = await client.Browser.getWindowForTarget();
435
+ return windowId;
436
+ }
437
+ finally {
438
+ try {
439
+ await client.close();
440
+ }
441
+ catch {
442
+ /* ignore */
443
+ }
444
+ }
445
+ }
446
+ loadManifest() {
447
+ try {
448
+ const data = readFileSync(MANIFEST_PATH, "utf-8");
449
+ return JSON.parse(data);
450
+ }
451
+ catch {
452
+ return null;
453
+ }
454
+ }
455
+ writeManifest(manifest) {
456
+ try {
457
+ mkdirSync(dirname(MANIFEST_PATH), { recursive: true });
458
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
459
+ }
460
+ catch (writeErr) {
461
+ console.error(`[comet-bridge] Manifest write failed: ${writeErr instanceof Error ? writeErr.message : writeErr}`);
462
+ }
463
+ }
464
+ persistManifest() {
465
+ const entries = [];
466
+ for (const [, session] of this.sessions) {
467
+ entries.push({
468
+ sessionKey: session.sessionKey,
469
+ agentId: session.agentId,
470
+ taskThreadId: session.taskThreadId,
471
+ targetId: session.targetId,
472
+ tabGroupId: session.tabGroupId,
473
+ createdAt: session.createdAt,
474
+ lastActivity: session.lastActivity,
475
+ pid: process.pid,
476
+ sessionName: session.sessionName,
477
+ taskGoal: session.taskGoal,
478
+ orchestratorUrl: session.orchestratorUrl,
479
+ role: session.role || "orchestrator",
480
+ parentSessionId: session.parentSessionId,
481
+ codexIdentity: session.codexIdentity,
482
+ codexBinding: session.codexBinding,
483
+ profileId: session.profileId,
484
+ profileAlias: session.profileAlias,
485
+ profileOwner: session.profileOwner,
486
+ });
487
+ }
488
+ // Merge with existing manifest (other processes' sessions)
489
+ const existing = this.loadManifest();
490
+ const otherSessions = existing
491
+ ? existing.sessions.filter((e) => !this.sessions.has(e.sessionKey))
492
+ : [];
493
+ this.writeManifest({
494
+ sessions: [...otherSessions, ...entries],
495
+ lastUpdated: Date.now(),
496
+ });
497
+ }
498
+ }
499
+ export const sessionRegistry = new SessionRegistry();
500
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1,49 @@
1
+ export type SidecarArtifactStatus = "submitted" | "working" | "completed" | "timed_out" | "failed";
2
+ export interface SidecarResultArtifact {
3
+ sidecarResultId: string;
4
+ sidecarContextKey: string;
5
+ bindingId: string;
6
+ sessionKey: string;
7
+ projectThreadId: string;
8
+ windowId: number;
9
+ targetId: string | null;
10
+ prompt: string;
11
+ status: SidecarArtifactStatus;
12
+ currentPageUrl: string;
13
+ response: string;
14
+ steps: string[];
15
+ currentStep: string;
16
+ error: string | null;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ }
20
+ export interface SidecarArtifactSnapshot {
21
+ version: 1;
22
+ artifacts: Record<string, SidecarResultArtifact>;
23
+ updatedAt: string;
24
+ }
25
+ export interface CreateSidecarArtifactInput {
26
+ sidecarContextKey: string;
27
+ bindingId: string;
28
+ sessionKey: string;
29
+ projectThreadId: string;
30
+ windowId: number;
31
+ targetId: string | null;
32
+ prompt: string;
33
+ currentPageUrl?: string;
34
+ }
35
+ export type SidecarArtifactPatch = Partial<Pick<SidecarResultArtifact, "status" | "currentPageUrl" | "response" | "steps" | "currentStep" | "error">>;
36
+ export declare class SidecarArtifactStore {
37
+ private readonly file;
38
+ private readonly dir;
39
+ private readonly lockDir;
40
+ constructor(storePath?: string);
41
+ get path(): string;
42
+ load(): Promise<SidecarArtifactSnapshot>;
43
+ create(input: CreateSidecarArtifactInput): Promise<SidecarResultArtifact>;
44
+ update(sidecarResultId: string, patch: SidecarArtifactPatch): Promise<SidecarResultArtifact>;
45
+ get(sidecarResultId: string): Promise<SidecarResultArtifact | null>;
46
+ latestForContext(sidecarContextKey: string): Promise<SidecarResultArtifact | null>;
47
+ }
48
+ export declare const sidecarArtifactStore: SidecarArtifactStore;
49
+ //# sourceMappingURL=sidecar-artifacts.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { randomUUID } from "crypto";
2
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "fs/promises";
3
+ import os from "os";
4
+ import path from "path";
5
+ const DEFAULT_DIR = path.join(os.homedir(), ".claude", "comet-browser");
6
+ const DEFAULT_FILE = path.join(DEFAULT_DIR, "sidecar-results.json");
7
+ const LOCK_RETRY_MS = 25;
8
+ const LOCK_TIMEOUT_MS = 5_000;
9
+ function nowIso() {
10
+ return new Date().toISOString();
11
+ }
12
+ function pathsFor(storePath) {
13
+ const file = storePath ?? DEFAULT_FILE;
14
+ return { file, dir: path.dirname(file), lockDir: `${file}.lock` };
15
+ }
16
+ function emptySnapshot() {
17
+ return { version: 1, artifacts: {}, updatedAt: nowIso() };
18
+ }
19
+ async function sleep(ms) {
20
+ await new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ async function withFileLock(lockDir, fn) {
23
+ const startedAt = Date.now();
24
+ while (true) {
25
+ try {
26
+ await mkdir(lockDir);
27
+ break;
28
+ }
29
+ catch (err) {
30
+ const code = err.code;
31
+ if (code !== "EEXIST" || Date.now() - startedAt > LOCK_TIMEOUT_MS)
32
+ throw err;
33
+ await sleep(LOCK_RETRY_MS);
34
+ }
35
+ }
36
+ try {
37
+ return await fn();
38
+ }
39
+ finally {
40
+ await rm(lockDir, { recursive: true, force: true });
41
+ }
42
+ }
43
+ async function readSnapshot(file) {
44
+ try {
45
+ const raw = await readFile(file, "utf-8");
46
+ const parsed = JSON.parse(raw);
47
+ return {
48
+ version: 1,
49
+ artifacts: parsed.artifacts ?? {},
50
+ updatedAt: parsed.updatedAt ?? nowIso(),
51
+ };
52
+ }
53
+ catch (err) {
54
+ const code = err.code;
55
+ if (code === "ENOENT")
56
+ return emptySnapshot();
57
+ throw err;
58
+ }
59
+ }
60
+ async function writeSnapshot(file, dir, snapshot) {
61
+ await mkdir(dir, { recursive: true });
62
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
63
+ try {
64
+ await writeFile(tmp, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
65
+ await rename(tmp, file);
66
+ }
67
+ catch (err) {
68
+ try {
69
+ await unlink(tmp);
70
+ }
71
+ catch {
72
+ /* best-effort cleanup */
73
+ }
74
+ throw err;
75
+ }
76
+ }
77
+ export class SidecarArtifactStore {
78
+ file;
79
+ dir;
80
+ lockDir;
81
+ constructor(storePath) {
82
+ const paths = pathsFor(storePath);
83
+ this.file = paths.file;
84
+ this.dir = paths.dir;
85
+ this.lockDir = paths.lockDir;
86
+ }
87
+ get path() {
88
+ return this.file;
89
+ }
90
+ async load() {
91
+ return readSnapshot(this.file);
92
+ }
93
+ async create(input) {
94
+ return withFileLock(this.lockDir, async () => {
95
+ const snapshot = await readSnapshot(this.file);
96
+ const timestamp = nowIso();
97
+ const artifact = {
98
+ sidecarResultId: randomUUID(),
99
+ sidecarContextKey: input.sidecarContextKey,
100
+ bindingId: input.bindingId,
101
+ sessionKey: input.sessionKey,
102
+ projectThreadId: input.projectThreadId,
103
+ windowId: input.windowId,
104
+ targetId: input.targetId,
105
+ prompt: input.prompt,
106
+ status: "submitted",
107
+ currentPageUrl: input.currentPageUrl ?? "unknown",
108
+ response: "",
109
+ steps: [],
110
+ currentStep: "",
111
+ error: null,
112
+ createdAt: timestamp,
113
+ updatedAt: timestamp,
114
+ };
115
+ snapshot.artifacts[artifact.sidecarResultId] = artifact;
116
+ snapshot.updatedAt = timestamp;
117
+ await writeSnapshot(this.file, this.dir, snapshot);
118
+ return artifact;
119
+ });
120
+ }
121
+ async update(sidecarResultId, patch) {
122
+ return withFileLock(this.lockDir, async () => {
123
+ const snapshot = await readSnapshot(this.file);
124
+ const artifact = snapshot.artifacts[sidecarResultId];
125
+ if (!artifact)
126
+ throw new Error(`Unknown sidecarResultId: ${sidecarResultId}`);
127
+ Object.assign(artifact, patch, { updatedAt: nowIso() });
128
+ snapshot.updatedAt = artifact.updatedAt;
129
+ await writeSnapshot(this.file, this.dir, snapshot);
130
+ return artifact;
131
+ });
132
+ }
133
+ async get(sidecarResultId) {
134
+ const snapshot = await readSnapshot(this.file);
135
+ return snapshot.artifacts[sidecarResultId] ?? null;
136
+ }
137
+ async latestForContext(sidecarContextKey) {
138
+ const snapshot = await readSnapshot(this.file);
139
+ const matches = Object.values(snapshot.artifacts)
140
+ .filter((artifact) => artifact.sidecarContextKey === sidecarContextKey)
141
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
142
+ return matches[0] ?? null;
143
+ }
144
+ }
145
+ export const sidecarArtifactStore = new SidecarArtifactStore();
146
+ //# sourceMappingURL=sidecar-artifacts.js.map