@shawnowen/comet-mcp 2.3.1 → 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.
- package/README.md +86 -19
- package/dist/alert-dispatcher.d.ts +23 -0
- package/dist/alert-dispatcher.js +101 -0
- package/dist/bound-session.d.ts +23 -0
- package/dist/bound-session.js +119 -0
- package/dist/bridge-config.d.ts +6 -0
- package/dist/bridge-config.js +78 -0
- package/dist/cdp-client.d.ts +40 -4
- package/dist/cdp-client.js +502 -155
- package/dist/comet-ai.d.ts +15 -0
- package/dist/comet-ai.js +114 -38
- package/dist/delegate-binding.d.ts +19 -0
- package/dist/delegate-binding.js +73 -0
- package/dist/discovery/capability-entry.d.ts +215 -0
- package/dist/discovery/capability-entry.js +13 -0
- package/dist/discovery/description-template.d.ts +40 -0
- package/dist/discovery/description-template.js +61 -0
- package/dist/discovery/golden-queries.fixture.d.ts +22 -0
- package/dist/discovery/golden-queries.fixture.js +137 -0
- package/dist/discovery/mcp-source.d.ts +38 -0
- package/dist/discovery/mcp-source.js +70 -0
- package/dist/discovery/metadata-completeness.d.ts +48 -0
- package/dist/discovery/metadata-completeness.js +83 -0
- package/dist/discovery/registry.d.ts +35 -0
- package/dist/discovery/registry.js +35 -0
- package/dist/discovery/safety.d.ts +44 -0
- package/dist/discovery/safety.js +59 -0
- package/dist/discovery/schema-validator.d.ts +36 -0
- package/dist/discovery/schema-validator.js +257 -0
- package/dist/discovery/source-error.d.ts +47 -0
- package/dist/discovery/source-error.js +95 -0
- package/dist/discovery/tool-meta.d.ts +41 -0
- package/dist/discovery/tool-meta.js +229 -0
- package/dist/discovery/virtual-tools.d.ts +20 -0
- package/dist/discovery/virtual-tools.js +69 -0
- package/dist/http-server.js +2067 -47
- package/dist/index.js +3163 -710
- package/dist/observer.d.ts +47 -0
- package/dist/observer.js +516 -0
- package/dist/session-registry.d.ts +57 -0
- package/dist/session-registry.js +500 -0
- package/dist/sidecar-artifacts.d.ts +49 -0
- package/dist/sidecar-artifacts.js +146 -0
- package/dist/snapshot-capture.d.ts +3 -0
- package/dist/snapshot-capture.js +91 -0
- package/dist/tab-group-archive.js +3 -1
- package/dist/tab-groups.d.ts +7 -0
- package/dist/tab-groups.js +21 -3
- package/dist/task-thread-aggregator.d.ts +34 -0
- package/dist/task-thread-aggregator.js +480 -0
- package/dist/task-thread-canonical.d.ts +142 -0
- package/dist/task-thread-canonical.js +116 -0
- package/dist/types.d.ts +237 -0
- package/dist/window-bindings.d.ts +112 -0
- package/dist/window-bindings.js +476 -0
- package/extension/background.js +1556 -300
- package/extension/icons/icon.svg +9 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +19 -4
- package/extension/session-logic.js +2383 -0
- package/extension/session-manager.html +299 -0
- package/extension/sidepanel.css +5323 -528
- package/extension/sidepanel.html +282 -2
- package/extension/sidepanel.js +10075 -951
- package/extension/window-policy.js +162 -0
- package/package.json +10 -7
- package/vendor/lifecycle-mcp-adapter.mjs +103 -0
- package/vendor/lifecycle-metadata.mjs +252 -0
- package/vendor/readiness-report.mjs +742 -0
- package/dist/cdp-client.d.ts.map +0 -1
- package/dist/cdp-client.js.map +0 -1
- package/dist/comet-ai.d.ts.map +0 -1
- package/dist/comet-ai.js.map +0 -1
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tab-group-archive.d.ts.map +0 -1
- package/dist/tab-group-archive.js.map +0 -1
- package/dist/tab-groups.d.ts.map +0 -1
- package/dist/tab-groups.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- 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
|
package/dist/observer.js
ADDED
|
@@ -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
|