@shawnowen/comet-mcp 2.3.0 → 2.4.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.
Files changed (85) hide show
  1. package/README.md +86 -19
  2. package/dist/alert-dispatcher.d.ts +23 -0
  3. package/dist/alert-dispatcher.js +101 -0
  4. package/dist/bound-session.d.ts +23 -0
  5. package/dist/bound-session.js +119 -0
  6. package/dist/bridge-config.d.ts +6 -0
  7. package/dist/bridge-config.js +78 -0
  8. package/dist/cdp-client.d.ts +40 -4
  9. package/dist/cdp-client.js +502 -155
  10. package/dist/comet-ai.d.ts +15 -0
  11. package/dist/comet-ai.js +114 -38
  12. package/dist/delegate-binding.d.ts +19 -0
  13. package/dist/delegate-binding.js +73 -0
  14. package/dist/discovery/capability-entry.d.ts +215 -0
  15. package/dist/discovery/capability-entry.js +13 -0
  16. package/dist/discovery/description-template.d.ts +40 -0
  17. package/dist/discovery/description-template.js +61 -0
  18. package/dist/discovery/golden-queries.fixture.d.ts +22 -0
  19. package/dist/discovery/golden-queries.fixture.js +137 -0
  20. package/dist/discovery/mcp-source.d.ts +38 -0
  21. package/dist/discovery/mcp-source.js +70 -0
  22. package/dist/discovery/metadata-completeness.d.ts +48 -0
  23. package/dist/discovery/metadata-completeness.js +83 -0
  24. package/dist/discovery/registry.d.ts +35 -0
  25. package/dist/discovery/registry.js +35 -0
  26. package/dist/discovery/safety.d.ts +44 -0
  27. package/dist/discovery/safety.js +59 -0
  28. package/dist/discovery/schema-validator.d.ts +36 -0
  29. package/dist/discovery/schema-validator.js +257 -0
  30. package/dist/discovery/source-error.d.ts +47 -0
  31. package/dist/discovery/source-error.js +95 -0
  32. package/dist/discovery/tool-meta.d.ts +41 -0
  33. package/dist/discovery/tool-meta.js +229 -0
  34. package/dist/discovery/virtual-tools.d.ts +20 -0
  35. package/dist/discovery/virtual-tools.js +69 -0
  36. package/dist/http-server.js +2067 -47
  37. package/dist/index.js +3163 -710
  38. package/dist/observer.d.ts +47 -0
  39. package/dist/observer.js +516 -0
  40. package/dist/session-registry.d.ts +57 -0
  41. package/dist/session-registry.js +500 -0
  42. package/dist/sidecar-artifacts.d.ts +49 -0
  43. package/dist/sidecar-artifacts.js +146 -0
  44. package/dist/snapshot-capture.d.ts +3 -0
  45. package/dist/snapshot-capture.js +91 -0
  46. package/dist/tab-group-archive.js +3 -1
  47. package/dist/tab-groups.d.ts +7 -0
  48. package/dist/tab-groups.js +21 -3
  49. package/dist/task-thread-aggregator.d.ts +34 -0
  50. package/dist/task-thread-aggregator.js +480 -0
  51. package/dist/task-thread-canonical.d.ts +142 -0
  52. package/dist/task-thread-canonical.js +116 -0
  53. package/dist/types.d.ts +237 -0
  54. package/dist/window-bindings.d.ts +112 -0
  55. package/dist/window-bindings.js +476 -0
  56. package/extension/background.js +1556 -300
  57. package/extension/icons/icon.svg +9 -0
  58. package/extension/icons/icon128.png +0 -0
  59. package/extension/icons/icon16.png +0 -0
  60. package/extension/icons/icon48.png +0 -0
  61. package/extension/manifest.json +19 -4
  62. package/extension/session-logic.js +2383 -0
  63. package/extension/session-manager.html +299 -0
  64. package/extension/sidepanel.css +5323 -528
  65. package/extension/sidepanel.html +282 -2
  66. package/extension/sidepanel.js +10075 -951
  67. package/extension/window-policy.js +162 -0
  68. package/package.json +10 -7
  69. package/vendor/lifecycle-mcp-adapter.mjs +103 -0
  70. package/vendor/lifecycle-metadata.mjs +252 -0
  71. package/vendor/readiness-report.mjs +742 -0
  72. package/dist/cdp-client.d.ts.map +0 -1
  73. package/dist/cdp-client.js.map +0 -1
  74. package/dist/comet-ai.d.ts.map +0 -1
  75. package/dist/comet-ai.js.map +0 -1
  76. package/dist/http-server.d.ts.map +0 -1
  77. package/dist/http-server.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/tab-group-archive.d.ts.map +0 -1
  81. package/dist/tab-group-archive.js.map +0 -1
  82. package/dist/tab-groups.d.ts.map +0 -1
  83. package/dist/tab-groups.js.map +0 -1
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,47 @@
1
+ import type { BrowserHealth, BrowserSnapshot, ObserverTabInfo, ObserverTabGroupInfo, ObserverAgentStatus, ObserverBindingInfo, ObserverFilters, ObserverWindowBindingInfo, AgentRegistryEntry } from "./types.js";
2
+ import { type CodexWindowBinding } from "./window-bindings.js";
3
+ export declare function readAgentRegistry(): Record<string, AgentRegistryEntry>;
4
+ export declare function readSessionManifestAgents(): Record<string, AgentRegistryEntry>;
5
+ export declare function classifyAgentStatus(agent: AgentRegistryEntry): ObserverAgentStatus;
6
+ export declare function listCDPTargets(): Promise<ObserverTabInfo[]>;
7
+ interface ExtTabGroup {
8
+ id: number;
9
+ collapsed: boolean;
10
+ color: string;
11
+ title: string;
12
+ windowId: number;
13
+ }
14
+ interface ExtTab {
15
+ id: number;
16
+ groupId: number;
17
+ windowId: number;
18
+ index: number;
19
+ title: string;
20
+ url: string;
21
+ active: boolean;
22
+ }
23
+ export declare function queryTabGroups(): Promise<{
24
+ groups: ExtTabGroup[];
25
+ tabs: ExtTab[];
26
+ } | null>;
27
+ export declare function matchTabsToGroups(cdpTargets: ObserverTabInfo[], extData: {
28
+ groups: ExtTabGroup[];
29
+ tabs: ExtTab[];
30
+ } | null, agents: Record<string, AgentRegistryEntry>): {
31
+ groups: ObserverTabGroupInfo[];
32
+ ungroupedTabs: ObserverTabInfo[];
33
+ };
34
+ export declare function attachBindingMetadata(groups: ObserverTabGroupInfo[], bindings: CodexWindowBinding[]): {
35
+ groups: ObserverTabGroupInfo[];
36
+ windows: ObserverWindowBindingInfo[];
37
+ bindings: ObserverBindingInfo[];
38
+ };
39
+ export declare function getHealth(): Promise<BrowserHealth>;
40
+ export declare function getSnapshot(filters?: ObserverFilters): Promise<BrowserSnapshot>;
41
+ export declare function getStatus(filters?: ObserverFilters): Promise<string>;
42
+ export declare function getDetail(groupName: string, filters?: ObserverFilters): Promise<string>;
43
+ export declare function captureThumbnail(targetId: string): Promise<string | null>;
44
+ export declare function formatHealth(health: BrowserHealth): string;
45
+ export declare function formatSnapshot(snapshot: BrowserSnapshot): string;
46
+ export {};
47
+ //# sourceMappingURL=observer.d.ts.map
@@ -0,0 +1,516 @@
1
+ // Observer module — non-disruptive browser state observation (Spec 030)
2
+ // Read-only: CDP /json/list + extension bridge + agent-registry.json
3
+ import { readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ const __filename_local = fileURLToPath(import.meta.url);
8
+ const __dirname_local = dirname(__filename_local);
9
+ import CDP from "chrome-remote-interface";
10
+ import { canReadBinding, windowBindingStore } from "./window-bindings.js";
11
+ const CDP_PORT = 9222;
12
+ const IDLE_THRESHOLD_MS = 60_000; // 60 seconds → idle
13
+ const ORPHAN_THRESHOLD_MS = 300_000; // 5 minutes → orphaned
14
+ const MANIFEST_PATH = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
15
+ // ---- Agent Registry ----
16
+ export function readAgentRegistry() {
17
+ // Look for agent-registry.json relative to repo root
18
+ // The MCP server runs from comet-mcp/, so go up one level
19
+ const paths = [
20
+ join(process.cwd(), "scripts", "agent-registry.json"),
21
+ join(process.cwd(), "..", "scripts", "agent-registry.json"),
22
+ join(__dirname_local, "..", "..", "scripts", "agent-registry.json"),
23
+ ];
24
+ for (const p of paths) {
25
+ try {
26
+ const raw = readFileSync(p, "utf-8");
27
+ const registry = JSON.parse(raw);
28
+ return registry.agents ?? {};
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ }
34
+ return {};
35
+ }
36
+ export function readSessionManifestAgents() {
37
+ try {
38
+ const raw = readFileSync(MANIFEST_PATH, "utf-8");
39
+ const manifest = JSON.parse(raw);
40
+ const agents = {};
41
+ for (const session of manifest.sessions ?? []) {
42
+ const lastHeartbeat = new Date(session.lastActivity || Date.now()).toISOString();
43
+ agents[`manifest:${session.sessionKey}`] = {
44
+ agentId: session.agentId,
45
+ agentType: "codex",
46
+ label: session.sessionName ?? session.agentId,
47
+ lane: 0,
48
+ status: "active",
49
+ taskGroup: session.taskThreadId.slice(0, 50),
50
+ tabGroupId: session.tabGroupId,
51
+ tabGroupColor: null,
52
+ currentUrl: session.orchestratorUrl ?? null,
53
+ taskDescription: session.taskGoal ?? null,
54
+ startedAt: new Date(session.createdAt || Date.now()).toISOString(),
55
+ lastHeartbeat,
56
+ completedAt: null,
57
+ errorMessage: null,
58
+ };
59
+ }
60
+ return agents;
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ export function classifyAgentStatus(agent) {
67
+ const now = Date.now();
68
+ const hb = new Date(agent.lastHeartbeat).getTime();
69
+ const elapsed = now - hb;
70
+ let status;
71
+ let idleSince = null;
72
+ if (elapsed <= IDLE_THRESHOLD_MS) {
73
+ status = "active";
74
+ }
75
+ else if (elapsed <= ORPHAN_THRESHOLD_MS) {
76
+ status = "idle";
77
+ idleSince = agent.lastHeartbeat;
78
+ }
79
+ else {
80
+ status = "orphaned";
81
+ idleSince = agent.lastHeartbeat;
82
+ }
83
+ return {
84
+ agentId: agent.agentId,
85
+ agentType: agent.agentType ?? "unknown",
86
+ label: agent.label ?? agent.agentId,
87
+ status,
88
+ lastHeartbeat: agent.lastHeartbeat,
89
+ lastActivity: agent.taskDescription ?? null,
90
+ idleSince,
91
+ };
92
+ }
93
+ // ---- CDP Targets ----
94
+ export async function listCDPTargets() {
95
+ const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/list`);
96
+ if (!response.ok) {
97
+ // Try fallback
98
+ const fallback = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
99
+ if (!fallback.ok) {
100
+ throw new Error(`Cannot connect to CDP on port ${CDP_PORT}. Check browser status.`);
101
+ }
102
+ const targets = (await fallback.json());
103
+ return targets.filter((t) => t.type === "page").map(cdpToTabInfo);
104
+ }
105
+ const targets = (await response.json());
106
+ // If /json/list returns empty, try /json fallback (Comet 145.x)
107
+ if (targets.length === 0) {
108
+ const fallback = await fetch(`http://127.0.0.1:${CDP_PORT}/json`);
109
+ if (fallback.ok) {
110
+ const fbTargets = (await fallback.json());
111
+ return fbTargets.filter((t) => t.type === "page").map(cdpToTabInfo);
112
+ }
113
+ }
114
+ return targets.filter((t) => t.type === "page").map(cdpToTabInfo);
115
+ }
116
+ function cdpToTabInfo(t) {
117
+ return {
118
+ id: t.id,
119
+ url: t.url,
120
+ title: t.title,
121
+ loadState: "complete", // CDP /json doesn't report load state; default to complete
122
+ thumbnail: null,
123
+ };
124
+ }
125
+ export async function queryTabGroups() {
126
+ try {
127
+ const { tabGroupsClient } = await import("./tab-groups.js");
128
+ const groups = await tabGroupsClient.listGroups();
129
+ const tabs = await tabGroupsClient.listTabs();
130
+ return { groups, tabs };
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ }
136
+ // ---- Matching & Assembly ----
137
+ export function matchTabsToGroups(cdpTargets, extData, agents) {
138
+ if (!extData) {
139
+ // No extension data — all tabs are ungrouped
140
+ return { groups: [], ungroupedTabs: cdpTargets };
141
+ }
142
+ const { groups: extGroups, tabs: extTabs } = extData;
143
+ // Build a map of extension tab URL+title → groupId for cross-referencing
144
+ const extTabByUrl = new Map();
145
+ for (const t of extTabs) {
146
+ extTabByUrl.set(`${t.url}|${t.title}`, t);
147
+ }
148
+ // Build a map from groupId → agent entry (match by taskGroup === group title)
149
+ const agentByTaskGroup = new Map();
150
+ const agentByTabGroupId = new Map();
151
+ for (const agent of Object.values(agents)) {
152
+ if (agent.taskGroup) {
153
+ agentByTaskGroup.set(agent.taskGroup, agent);
154
+ }
155
+ if (typeof agent.tabGroupId === "number" && agent.tabGroupId !== null) {
156
+ agentByTabGroupId.set(agent.tabGroupId, agent);
157
+ }
158
+ }
159
+ // Build group structures
160
+ const groupMap = new Map();
161
+ for (const g of extGroups) {
162
+ const agent = agentByTabGroupId.get(g.id) ?? agentByTaskGroup.get(g.title) ?? null;
163
+ groupMap.set(g.id, {
164
+ id: g.id,
165
+ title: g.title,
166
+ color: g.color,
167
+ collapsed: g.collapsed,
168
+ windowId: g.windowId,
169
+ tabs: [],
170
+ tabCount: 0,
171
+ agent: agent ? classifyAgentStatus(agent) : null,
172
+ binding: null,
173
+ bindingStatus: "unbound",
174
+ });
175
+ }
176
+ // Assign CDP targets to groups
177
+ const ungrouped = [];
178
+ for (const cdpTab of cdpTargets) {
179
+ const key = `${cdpTab.url}|${cdpTab.title}`;
180
+ const extTab = extTabByUrl.get(key);
181
+ if (extTab && extTab.groupId !== -1 && groupMap.has(extTab.groupId)) {
182
+ groupMap.get(extTab.groupId).tabs.push(cdpTab);
183
+ }
184
+ else {
185
+ ungrouped.push(cdpTab);
186
+ }
187
+ }
188
+ // Update tab counts
189
+ for (const g of groupMap.values()) {
190
+ g.tabCount = g.tabs.length;
191
+ }
192
+ return {
193
+ groups: Array.from(groupMap.values()),
194
+ ungroupedTabs: ungrouped,
195
+ };
196
+ }
197
+ function toObserverBindingInfo(binding) {
198
+ return {
199
+ bindingId: binding.bindingId,
200
+ codexSessionId: binding.codexSessionId,
201
+ projectThreadId: binding.projectThreadId,
202
+ repoSlug: binding.repoSlug,
203
+ worktreePath: binding.worktreePath,
204
+ branchName: binding.branchName,
205
+ sessionKey: binding.sessionKey,
206
+ role: binding.role,
207
+ sidecarContextKey: binding.sidecarContextKey,
208
+ status: binding.status,
209
+ windowId: binding.windowId,
210
+ tabGroupId: binding.tabGroupId,
211
+ targetId: binding.targetId,
212
+ runIds: binding.runIds,
213
+ updatedAt: binding.updatedAt,
214
+ };
215
+ }
216
+ export function attachBindingMetadata(groups, bindings) {
217
+ const observerBindings = bindings.map(toObserverBindingInfo);
218
+ const bindingByWindow = new Map();
219
+ for (const binding of observerBindings) {
220
+ const current = bindingByWindow.get(binding.windowId);
221
+ if (!current || binding.updatedAt > current.updatedAt) {
222
+ bindingByWindow.set(binding.windowId, binding);
223
+ }
224
+ }
225
+ const liveWindowIds = new Set();
226
+ const groupsWithBindings = groups.map((group) => {
227
+ liveWindowIds.add(group.windowId);
228
+ const binding = bindingByWindow.get(group.windowId) ??
229
+ observerBindings.find((candidate) => candidate.tabGroupId === group.id) ??
230
+ null;
231
+ const bindingStatus = binding?.status ?? "unbound";
232
+ return {
233
+ ...group,
234
+ binding,
235
+ bindingStatus,
236
+ };
237
+ });
238
+ for (const binding of observerBindings) {
239
+ liveWindowIds.add(binding.windowId);
240
+ }
241
+ const windows = [...liveWindowIds]
242
+ .sort((a, b) => a - b)
243
+ .map((windowId) => {
244
+ const binding = bindingByWindow.get(windowId) ?? null;
245
+ const bindingStatus = binding?.status ?? "unbound";
246
+ return {
247
+ windowId,
248
+ binding,
249
+ bindingStatus,
250
+ };
251
+ });
252
+ return { groups: groupsWithBindings, windows, bindings: observerBindings };
253
+ }
254
+ // ---- Health Check ----
255
+ export async function getHealth() {
256
+ let running = false;
257
+ let cdpConnected = false;
258
+ let extensionAvailable = false;
259
+ let tabCount = 0;
260
+ let groupCount = 0;
261
+ // Check CDP
262
+ try {
263
+ const resp = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`, {
264
+ signal: AbortSignal.timeout(3000),
265
+ });
266
+ if (resp.ok) {
267
+ running = true;
268
+ cdpConnected = true;
269
+ }
270
+ }
271
+ catch {
272
+ // Not running or not reachable
273
+ }
274
+ if (cdpConnected) {
275
+ try {
276
+ const targets = await listCDPTargets();
277
+ tabCount = targets.length;
278
+ }
279
+ catch {
280
+ /* already counted as 0 */
281
+ }
282
+ try {
283
+ const extData = await queryTabGroups();
284
+ if (extData) {
285
+ extensionAvailable = true;
286
+ groupCount = extData.groups.length;
287
+ }
288
+ }
289
+ catch {
290
+ /* extension not available */
291
+ }
292
+ }
293
+ return { running, cdpConnected, extensionAvailable, tabCount, groupCount };
294
+ }
295
+ // ---- Snapshot ----
296
+ export async function getSnapshot(filters) {
297
+ const health = await getHealth();
298
+ if (!health.cdpConnected) {
299
+ return {
300
+ timestamp: new Date().toISOString(),
301
+ browser: health,
302
+ groups: [],
303
+ ungroupedTabs: [],
304
+ windows: [],
305
+ bindings: [],
306
+ totalTabs: 0,
307
+ totalGroups: 0,
308
+ };
309
+ }
310
+ const [cdpTargets, extData, agents, bindings] = await Promise.all([
311
+ listCDPTargets(),
312
+ queryTabGroups(),
313
+ Promise.resolve({ ...readSessionManifestAgents(), ...readAgentRegistry() }),
314
+ windowBindingStore.list().catch(() => []),
315
+ ]);
316
+ let { groups, ungroupedTabs } = matchTabsToGroups(cdpTargets, extData, agents);
317
+ const scopedBindings = filters?.codexIdentity
318
+ ? bindings.filter((binding) => canReadBinding(filters.codexIdentity, binding))
319
+ : bindings;
320
+ if (filters?.codexIdentity) {
321
+ const allowedWindowIds = new Set(scopedBindings.map((binding) => binding.windowId));
322
+ const allowedTabGroupIds = new Set(scopedBindings
323
+ .map((binding) => binding.tabGroupId)
324
+ .filter((tabGroupId) => typeof tabGroupId === "number"));
325
+ groups = groups.filter((group) => allowedWindowIds.has(group.windowId) || allowedTabGroupIds.has(group.id));
326
+ ungroupedTabs = [];
327
+ }
328
+ let bindingMetadata = attachBindingMetadata(groups, scopedBindings);
329
+ groups = bindingMetadata.groups;
330
+ // Apply filters
331
+ if (filters?.group) {
332
+ groups = groups.filter((g) => g.title.toLowerCase().includes(filters.group.toLowerCase()));
333
+ }
334
+ if (filters?.agentId) {
335
+ groups = groups.filter((g) => g.agent?.agentId === filters.agentId);
336
+ }
337
+ if (filters?.urlPattern) {
338
+ const pattern = filters.urlPattern.toLowerCase();
339
+ groups = groups
340
+ .map((g) => ({
341
+ ...g,
342
+ tabs: g.tabs.filter((t) => t.url.toLowerCase().includes(pattern)),
343
+ tabCount: g.tabs.filter((t) => t.url.toLowerCase().includes(pattern)).length,
344
+ }))
345
+ .filter((g) => g.tabCount > 0);
346
+ ungroupedTabs = ungroupedTabs.filter((t) => t.url.toLowerCase().includes(pattern));
347
+ }
348
+ bindingMetadata = attachBindingMetadata(groups, scopedBindings);
349
+ groups = bindingMetadata.groups;
350
+ // Capture thumbnails if requested
351
+ if (filters?.thumbnails) {
352
+ for (const g of groups) {
353
+ for (const tab of g.tabs) {
354
+ tab.thumbnail = await captureThumbnail(tab.id);
355
+ }
356
+ }
357
+ for (const tab of ungroupedTabs) {
358
+ tab.thumbnail = await captureThumbnail(tab.id);
359
+ }
360
+ }
361
+ const totalTabs = groups.reduce((sum, g) => sum + g.tabCount, 0) + ungroupedTabs.length;
362
+ return {
363
+ timestamp: new Date().toISOString(),
364
+ browser: health,
365
+ groups,
366
+ ungroupedTabs,
367
+ windows: bindingMetadata.windows,
368
+ bindings: bindingMetadata.bindings,
369
+ totalTabs,
370
+ totalGroups: groups.length,
371
+ };
372
+ }
373
+ // ---- Status (compact table) ----
374
+ export async function getStatus(filters) {
375
+ const snapshot = await getSnapshot(filters);
376
+ if (!snapshot.browser.cdpConnected) {
377
+ return "Comet browser is not running. Start with: node scripts/session.mjs start --profile oe";
378
+ }
379
+ if (snapshot.groups.length === 0 && !snapshot.browser.extensionAvailable) {
380
+ return ("Tab group/agent data unavailable — extension not detected. Showing tabs only.\n\n" +
381
+ `${snapshot.ungroupedTabs.length} ungrouped tabs found.`);
382
+ }
383
+ const lines = ["Tab Group Status Overview\n"];
384
+ lines.push("| Group | Color | Agent | Binding | Status | Tabs | Last Activity |");
385
+ lines.push("|-------|-------|-------|---------|--------|------|---------------|");
386
+ for (const g of snapshot.groups) {
387
+ const agent = g.agent;
388
+ const agentName = agent?.agentId ?? "unknown";
389
+ const status = agent?.status?.toUpperCase() ?? "UNKNOWN";
390
+ const binding = g.binding
391
+ ? `${g.binding.status}:${g.binding.codexSessionId}`
392
+ : g.bindingStatus.toUpperCase();
393
+ const lastActivity = agent?.lastHeartbeat
394
+ ? new Date(agent.lastHeartbeat).toLocaleTimeString("en-US", { hour12: false })
395
+ : "—";
396
+ lines.push(`| ${g.title} | ${g.color} | ${agentName} | ${binding} | ${status} | ${g.tabCount} | ${lastActivity} |`);
397
+ }
398
+ if (snapshot.ungroupedTabs.length > 0) {
399
+ lines.push(`\n${snapshot.ungroupedTabs.length} ungrouped tab(s) not shown in table.`);
400
+ }
401
+ return lines.join("\n");
402
+ }
403
+ // ---- Detail (single group) ----
404
+ export async function getDetail(groupName, filters) {
405
+ const snapshot = await getSnapshot(filters);
406
+ if (!snapshot.browser.cdpConnected) {
407
+ return "Comet browser is not running. Start with: node scripts/session.mjs start --profile oe";
408
+ }
409
+ const group = snapshot.groups.find((g) => g.title.toLowerCase() === groupName.toLowerCase());
410
+ if (!group) {
411
+ const available = snapshot.groups.map((g) => g.title).join(", ");
412
+ return `Tab group '${groupName}' not found.\n\nAvailable groups: ${available || "(none)"}`;
413
+ }
414
+ const agent = group.agent;
415
+ const binding = group.binding
416
+ ? `${group.binding.status}:${group.binding.bindingId} (${group.binding.codexSessionId})`
417
+ : "unbound";
418
+ const lines = [
419
+ `Tab Group Detail: ${group.title}`,
420
+ `Color: ${group.color} | Agent: ${agent?.agentId ?? "unknown"} | Binding: ${binding} | Status: ${agent?.status?.toUpperCase() ?? "UNKNOWN"} | Last heartbeat: ${agent?.lastHeartbeat ? new Date(agent.lastHeartbeat).toLocaleTimeString("en-US", { hour12: false }) : "—"}`,
421
+ "",
422
+ `Tabs (${group.tabCount}):`,
423
+ ];
424
+ for (let i = 0; i < group.tabs.length; i++) {
425
+ const tab = group.tabs[i];
426
+ lines.push(` ${i + 1}. ${tab.title}`);
427
+ lines.push(` URL: ${tab.url}`);
428
+ lines.push(` Load: ${tab.loadState}`);
429
+ if (tab.thumbnail) {
430
+ lines.push(` Thumbnail: captured (${Math.round((tab.thumbnail.length * 0.75) / 1024)}KB)`);
431
+ }
432
+ }
433
+ return lines.join("\n");
434
+ }
435
+ // ---- Thumbnail Capture ----
436
+ export async function captureThumbnail(targetId) {
437
+ try {
438
+ // Get the WebSocket URL for this target
439
+ const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/list`);
440
+ if (!response.ok)
441
+ return null;
442
+ const targets = (await response.json());
443
+ const target = targets.find((t) => t.id === targetId);
444
+ if (!target?.webSocketDebuggerUrl)
445
+ return null;
446
+ // Skip extension pages
447
+ if (target.url.startsWith("chrome-extension://") || target.url.startsWith("chrome://")) {
448
+ return null;
449
+ }
450
+ // Temporarily attach to capture screenshot
451
+ const client = await CDP({ target: target.webSocketDebuggerUrl });
452
+ try {
453
+ await client.Page.enable();
454
+ const result = await Promise.race([
455
+ client.Page.captureScreenshot({ format: "png", quality: 50 }),
456
+ new Promise((_, reject) => setTimeout(() => reject(new Error("thumbnail timeout")), 2000)),
457
+ ]);
458
+ return result.data;
459
+ }
460
+ finally {
461
+ try {
462
+ await client.close();
463
+ }
464
+ catch {
465
+ /* ignore */
466
+ }
467
+ }
468
+ }
469
+ catch {
470
+ return null;
471
+ }
472
+ }
473
+ // ---- Formatters ----
474
+ export function formatHealth(health) {
475
+ return [
476
+ `Browser: ${health.running ? "running" : "not running"}`,
477
+ `CDP: ${health.cdpConnected ? "connected" : "disconnected"}`,
478
+ `Extension: ${health.extensionAvailable ? "available" : "unavailable"}`,
479
+ `Tabs: ${health.tabCount}`,
480
+ `Groups: ${health.groupCount}`,
481
+ ].join(" | ");
482
+ }
483
+ export function formatSnapshot(snapshot) {
484
+ if (!snapshot.browser.cdpConnected) {
485
+ return "Comet browser is not running. Start with: node scripts/session.mjs start --profile oe";
486
+ }
487
+ const lines = [
488
+ `Browser Snapshot (${snapshot.timestamp})`,
489
+ `${snapshot.totalTabs} tabs across ${snapshot.totalGroups} groups`,
490
+ ];
491
+ if (!snapshot.browser.extensionAvailable && snapshot.groups.length === 0) {
492
+ lines.push("\nWarning: Tab group/agent data unavailable — extension not detected. Showing tabs only.");
493
+ }
494
+ for (const g of snapshot.groups) {
495
+ const agentStr = g.agent
496
+ ? `agent: ${g.agent.agentId} [${g.agent.status.toUpperCase()}${g.agent.status === "idle" ? ` since ${new Date(g.agent.idleSince).toLocaleTimeString("en-US", { hour12: false })}` : ""}]`
497
+ : "agent: unknown";
498
+ const bindingStr = g.binding
499
+ ? `binding: ${g.binding.status}:${g.binding.bindingId} owner=${g.binding.codexSessionId}`
500
+ : `binding: ${g.bindingStatus}`;
501
+ lines.push(`\n## ${g.title} (${g.color}) — ${agentStr} — ${bindingStr}`);
502
+ for (let i = 0; i < g.tabs.length; i++) {
503
+ const t = g.tabs[i];
504
+ lines.push(` ${i + 1}. ${t.title} | ${t.url} | ${t.loadState}`);
505
+ }
506
+ }
507
+ if (snapshot.ungroupedTabs.length > 0) {
508
+ lines.push("\n## Ungrouped");
509
+ for (let i = 0; i < snapshot.ungroupedTabs.length; i++) {
510
+ const t = snapshot.ungroupedTabs[i];
511
+ lines.push(` ${i + 1}. ${t.title} | ${t.url} | ${t.loadState}`);
512
+ }
513
+ }
514
+ return lines.join("\n");
515
+ }
516
+ //# sourceMappingURL=observer.js.map
@@ -0,0 +1,57 @@
1
+ import { CometCDPClient } from "./cdp-client.js";
2
+ import { CometAI } from "./comet-ai.js";
3
+ import type { AgentSession, ManifestEntry, TabGroupColor, ConsumerRole } from "./types.js";
4
+ export interface RegisterOptions {
5
+ agentId?: string;
6
+ taskThreadId?: string;
7
+ url?: string;
8
+ tabGroupColor?: TabGroupColor;
9
+ port?: number;
10
+ role?: ConsumerRole;
11
+ taskGoal?: string;
12
+ codexSessionId?: string;
13
+ projectThreadId?: string;
14
+ projectThreadFamily?: string;
15
+ worktreePath?: string;
16
+ repoSlug?: string;
17
+ branchName?: string;
18
+ codexSessionRole?: string;
19
+ codexSessionKey?: string;
20
+ strictCodexIdentity?: boolean;
21
+ profile?: string;
22
+ }
23
+ export declare function generateSessionName(taskGoal: string): string;
24
+ export interface ReleaseOptions {
25
+ closeTabs?: boolean;
26
+ updateGroupColor?: TabGroupColor;
27
+ taskStatus?: "success" | "failed" | "abandoned" | "in-progress";
28
+ }
29
+ export declare class SessionRegistry {
30
+ private sessions;
31
+ private currentSessionKey;
32
+ private tabGroupMutex;
33
+ register(options?: RegisterOptions): Promise<AgentSession & {
34
+ cdpClient: CometCDPClient;
35
+ cometAI: CometAI;
36
+ }>;
37
+ get(sessionKey: string): (AgentSession & {
38
+ cdpClient: CometCDPClient;
39
+ cometAI: CometAI;
40
+ }) | undefined;
41
+ getCurrent(): (AgentSession & {
42
+ cdpClient: CometCDPClient;
43
+ cometAI: CometAI;
44
+ }) | undefined;
45
+ updateSessionUrl(sessionKey: string, orchestratorUrl: string): void;
46
+ getManifestEntryByThread(taskThreadId: string): ManifestEntry | undefined;
47
+ release(sessionKey: string, options?: ReleaseOptions): Promise<void>;
48
+ reapOrphans(): Promise<number>;
49
+ ensureBrowserRunning(port?: number): Promise<string>;
50
+ private createTabGroup;
51
+ private getWindowIdForTarget;
52
+ private loadManifest;
53
+ private writeManifest;
54
+ private persistManifest;
55
+ }
56
+ export declare const sessionRegistry: SessionRegistry;
57
+ //# sourceMappingURL=session-registry.d.ts.map