@shawnowen/comet-mcp 2.4.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 (46) hide show
  1. package/README.md +12 -1
  2. package/dist/binding-reaper.d.ts +46 -0
  3. package/dist/binding-reaper.js +73 -0
  4. package/dist/http-server.js +121 -0
  5. package/dist/index.js +310 -6
  6. package/dist/project-config.d.ts +46 -0
  7. package/dist/project-config.js +166 -0
  8. package/dist/tab-groups.d.ts +21 -1
  9. package/dist/tab-groups.js +184 -0
  10. package/dist/window-bindings.d.ts +48 -0
  11. package/dist/window-bindings.js +85 -0
  12. package/extension/background.js +38 -17
  13. package/extension/manifest.json +16 -1
  14. package/extension/perplexity-capability-manifest.json +1181 -0
  15. package/extension/perplexity-capability-manifest.schema.json +142 -0
  16. package/extension/session-logic.js +696 -25
  17. package/extension/session-manager.html +13 -1
  18. package/extension/sidepanel.css +21 -6
  19. package/extension/sidepanel.js +598 -68
  20. package/package.json +1 -1
  21. package/dist/discovery/capability-entry.d.ts +0 -215
  22. package/dist/discovery/capability-entry.js +0 -13
  23. package/dist/discovery/description-template.d.ts +0 -40
  24. package/dist/discovery/description-template.js +0 -61
  25. package/dist/discovery/golden-queries.fixture.d.ts +0 -22
  26. package/dist/discovery/golden-queries.fixture.js +0 -137
  27. package/dist/discovery/mcp-source.d.ts +0 -38
  28. package/dist/discovery/mcp-source.js +0 -70
  29. package/dist/discovery/metadata-completeness.d.ts +0 -48
  30. package/dist/discovery/metadata-completeness.js +0 -83
  31. package/dist/discovery/registry.d.ts +0 -35
  32. package/dist/discovery/registry.js +0 -35
  33. package/dist/discovery/safety.d.ts +0 -44
  34. package/dist/discovery/safety.js +0 -59
  35. package/dist/discovery/schema-validator.d.ts +0 -36
  36. package/dist/discovery/schema-validator.js +0 -257
  37. package/dist/discovery/source-error.d.ts +0 -47
  38. package/dist/discovery/source-error.js +0 -95
  39. package/dist/discovery/tool-meta.d.ts +0 -41
  40. package/dist/discovery/tool-meta.js +0 -229
  41. package/dist/discovery/virtual-tools.d.ts +0 -20
  42. package/dist/discovery/virtual-tools.js +0 -69
  43. package/dist/task-thread-aggregator.d.ts +0 -34
  44. package/dist/task-thread-aggregator.js +0 -480
  45. package/dist/task-thread-canonical.d.ts +0 -142
  46. package/dist/task-thread-canonical.js +0 -116
package/README.md CHANGED
@@ -62,7 +62,7 @@ You: "Log into my GitHub and check my notifications"
62
62
  Claude: [Comet handles the login flow and navigation]
63
63
  ```
64
64
 
65
- ## Tools (25)
65
+ ## Tools (26)
66
66
 
67
67
  ### Direct Interaction (2) — No AI, pure CDP
68
68
 
@@ -112,6 +112,17 @@ Lifecycle tools are binding-aware: `start` attaches the run ID to the active or
112
112
  | `comet_delegate` | High-level task dispatch with direct Codex binding creation/reuse, URL, and lifecycle setup |
113
113
  | `comet_observe` | Passively observe browser state, binding owners, and stale/conflict/unbound windows without disrupting active agents |
114
114
 
115
+ ### Parity and Workflow Tools (6)
116
+
117
+ | Tool | Description |
118
+ |------|-------------|
119
+ | `comet_pdf` | Generate PDF from current page or URL |
120
+ | `comet_scrape` | Extract structured data: text, tables, JSON-LD, lists, attributes, multi-element content |
121
+ | `comet_network` | Capture network traffic, block URL patterns, intercept and mock API responses |
122
+ | `comet_monitor` | Monitor current page or URL for content changes with baselines, diffs, screenshots, and notifications |
123
+ | `comet_automate` | Multi-step browser workflows with assertions and variables |
124
+ | `comet_domain` | Domain playbooks for QBO, Mercury, GitHub, Google, and SALT |
125
+
115
126
  ## Codex Window Bindings
116
127
 
117
128
  Comet MCP routes multi-agent browser work through Codex window bindings. Each binding records the owning Codex session, repo/worktree, branch, `windowId`, `tabGroupId`, `targetId`, sidecar context key, lifecycle run IDs, and status.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Window-binding TTL reaper (Spec 084).
3
+ *
4
+ * Schedules periodic reconciliation of persisted window bindings against the live
5
+ * CDP window set and prunes records whose window has been missing beyond a
6
+ * configurable TTL. Dependency-injected so it is deterministically unit-testable
7
+ * (fake live-window source, fake clock, fake archive) and free of CDP/timer setup.
8
+ *
9
+ * Wired into the long-lived bridge process (http-server.ts) after `server.listen`.
10
+ */
11
+ import { type CodexWindowBinding, type ReapCycleResult, type ReapOptions } from "./window-bindings.js";
12
+ export interface ReaperConfig {
13
+ enabled: boolean;
14
+ ttlMs: number;
15
+ intervalMs: number;
16
+ }
17
+ /** Minimal store surface the reaper depends on (injectable for tests). */
18
+ export interface ReapableStore {
19
+ reapExpiredBindings(opts: ReapOptions): Promise<ReapCycleResult>;
20
+ }
21
+ export interface ReaperDeps {
22
+ /** Resolve the distinct live window ids. Return `null` when unknown (CDP down) → no-op cycle. */
23
+ getLiveWindowIds: () => Promise<number[] | null>;
24
+ /** Archive a binding (recovery snapshot + alert) before deletion. */
25
+ archive?: (binding: CodexWindowBinding) => Promise<void>;
26
+ /** Injectable clock. Defaults to Date.now. */
27
+ now?: () => number;
28
+ /** Structured logger. Defaults to console.log. */
29
+ log?: (msg: string) => void;
30
+ /** Binding store. Defaults to the shared `windowBindingStore` singleton. */
31
+ store?: ReapableStore;
32
+ }
33
+ /** Parse reaper configuration from the environment with safe defaults. */
34
+ export declare function readReaperConfig(env?: NodeJS.ProcessEnv): ReaperConfig;
35
+ /**
36
+ * Run a single reap cycle. Returns the cycle result, or `null` when the live
37
+ * window set could not be determined (in which case nothing is mutated).
38
+ */
39
+ export declare function runReapCycle(cfg: ReaperConfig, deps: ReaperDeps): Promise<ReapCycleResult | null>;
40
+ /**
41
+ * Start the reaper: one cycle on startup, then on the configured interval.
42
+ * Returns a `stop()` that clears the timer. When disabled, no cycle runs and no
43
+ * timer is scheduled, and `stop()` is a no-op.
44
+ */
45
+ export declare function startBindingReaper(cfg: ReaperConfig, deps: ReaperDeps): () => void;
46
+ //# sourceMappingURL=binding-reaper.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Window-binding TTL reaper (Spec 084).
3
+ *
4
+ * Schedules periodic reconciliation of persisted window bindings against the live
5
+ * CDP window set and prunes records whose window has been missing beyond a
6
+ * configurable TTL. Dependency-injected so it is deterministically unit-testable
7
+ * (fake live-window source, fake clock, fake archive) and free of CDP/timer setup.
8
+ *
9
+ * Wired into the long-lived bridge process (http-server.ts) after `server.listen`.
10
+ */
11
+ import { windowBindingStore, } from "./window-bindings.js";
12
+ const DEFAULT_TTL_HOURS = 6;
13
+ const DEFAULT_INTERVAL_SEC = 1800;
14
+ function positiveNumber(raw, fallback) {
15
+ const n = Number(raw);
16
+ return Number.isFinite(n) && n > 0 ? n : fallback;
17
+ }
18
+ /** Parse reaper configuration from the environment with safe defaults. */
19
+ export function readReaperConfig(env = process.env) {
20
+ const enabled = (env.COMET_BINDING_REAP_ENABLED ?? "1") !== "0";
21
+ const ttlHours = positiveNumber(env.COMET_BINDING_REAP_TTL_HOURS, DEFAULT_TTL_HOURS);
22
+ const intervalSec = positiveNumber(env.COMET_BINDING_REAP_INTERVAL_SEC, DEFAULT_INTERVAL_SEC);
23
+ return {
24
+ enabled,
25
+ ttlMs: ttlHours * 3_600_000,
26
+ intervalMs: intervalSec * 1000,
27
+ };
28
+ }
29
+ /**
30
+ * Run a single reap cycle. Returns the cycle result, or `null` when the live
31
+ * window set could not be determined (in which case nothing is mutated).
32
+ */
33
+ export async function runReapCycle(cfg, deps) {
34
+ const log = deps.log ?? ((m) => console.log(m));
35
+ const liveIds = await deps.getLiveWindowIds();
36
+ if (liveIds == null) {
37
+ log("[binding-reaper] live window set unknown — cycle skipped (no mutation)");
38
+ return null;
39
+ }
40
+ const store = deps.store ?? windowBindingStore;
41
+ const result = await store.reapExpiredBindings({
42
+ liveWindowIds: liveIds,
43
+ ttlMs: cfg.ttlMs,
44
+ now: deps.now ? deps.now() : undefined,
45
+ archive: deps.archive,
46
+ });
47
+ log(`[binding-reaper] evaluated=${result.evaluated} newlyMissing=${result.newlyMissing} ` +
48
+ `retainedLive=${result.retainedLive} reaped=${result.reaped} skippedOwned=${result.skippedOwned}`);
49
+ return result;
50
+ }
51
+ /**
52
+ * Start the reaper: one cycle on startup, then on the configured interval.
53
+ * Returns a `stop()` that clears the timer. When disabled, no cycle runs and no
54
+ * timer is scheduled, and `stop()` is a no-op.
55
+ */
56
+ export function startBindingReaper(cfg, deps) {
57
+ const log = deps.log ?? ((m) => console.log(m));
58
+ if (!cfg.enabled) {
59
+ log("[binding-reaper] disabled (COMET_BINDING_REAP_ENABLED=0)");
60
+ return () => { };
61
+ }
62
+ log(`[binding-reaper] enabled — ttl=${Math.round(cfg.ttlMs / 3_600_000)}h interval=${Math.round(cfg.intervalMs / 1000)}s`);
63
+ // Startup cycle (fire-and-forget; errors are swallowed per-cycle).
64
+ void runReapCycle(cfg, deps).catch((err) => log(`[binding-reaper] startup cycle error: ${err instanceof Error ? err.message : err}`));
65
+ const timer = setInterval(() => {
66
+ void runReapCycle(cfg, deps).catch((err) => log(`[binding-reaper] cycle error: ${err instanceof Error ? err.message : err}`));
67
+ }, cfg.intervalMs);
68
+ // Never hold the process open solely for the reaper.
69
+ if (typeof timer.unref === "function")
70
+ timer.unref();
71
+ return () => clearInterval(timer);
72
+ }
73
+ //# sourceMappingURL=binding-reaper.js.map
@@ -12,10 +12,12 @@ import { homedir } from "node:os";
12
12
  import { cometClient } from "./cdp-client.js";
13
13
  import { cometAI } from "./comet-ai.js";
14
14
  import { tabGroupsClient } from "./tab-groups.js";
15
+ import { startBindingReaper, readReaperConfig } from "./binding-reaper.js";
15
16
  import { BoundSessionError, resolveHttpBoundSession } from "./bound-session.js";
16
17
  import { createOrReuseDelegateBinding } from "./delegate-binding.js";
17
18
  import { deriveCodexSessionIdentity, windowBindingStore, } from "./window-bindings.js";
18
19
  import { readAgentRegistry, classifyAgentStatus, getHealth, getSnapshot, getStatus, getDetail, formatHealth, formatSnapshot, } from "./observer.js";
20
+ import { dispatchAlert } from "./alert-dispatcher.js";
19
21
  const PORT = parseInt(process.env.COMET_HTTP_PORT || "3456", 10);
20
22
  // Lifecycle + orchestration paths (mirrored from index.ts)
21
23
  const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL || "http://localhost:3001/command-center/api/comet/lifecycle";
@@ -134,6 +136,67 @@ async function getWindowIdForTarget(targetId) {
134
136
  }
135
137
  }
136
138
  }
139
+ /**
140
+ * Resolve the distinct live window ids for the window-binding reaper (Spec 084).
141
+ * Returns `null` when the live set cannot be determined (CDP unreachable, or page
142
+ * targets exist but none resolved) so the reaper performs a safe no-op instead of
143
+ * treating every binding as missing.
144
+ */
145
+ async function getLiveWindowIds() {
146
+ try {
147
+ const resp = await fetch("http://127.0.0.1:9222/json", {
148
+ signal: AbortSignal.timeout(5000),
149
+ });
150
+ const targets = (await resp.json());
151
+ const pages = targets.filter((t) => t.type === "page" && typeof t.id === "string");
152
+ const ids = new Set();
153
+ for (const t of pages) {
154
+ try {
155
+ ids.add(await getWindowIdForTarget(t.id));
156
+ }
157
+ catch {
158
+ /* per-target resolution failure — skip this target */
159
+ }
160
+ }
161
+ // Page targets existed but none resolved → treat live set as unknown (no-op).
162
+ if (pages.length > 0 && ids.size === 0)
163
+ return null;
164
+ return [...ids];
165
+ }
166
+ catch {
167
+ return null; // CDP unreachable → unknown
168
+ }
169
+ }
170
+ /**
171
+ * Archive a recovery snapshot of a binding before the reaper deletes it
172
+ * (Spec 084 FR-006). Writes a JSON record into the snapshots dir; throwing here
173
+ * causes the reaper to skip the delete (fail-safe).
174
+ */
175
+ async function archiveBindingForReap(binding) {
176
+ const dir = join(homedir(), ".claude", "comet-browser", "snapshots");
177
+ mkdirSync(dir, { recursive: true });
178
+ const safeId = binding.bindingId.replace(/[^a-zA-Z0-9_-]/g, "");
179
+ const file = join(dir, `reaped-binding-${safeId}-${Date.now()}.json`);
180
+ writeFileSync(file, JSON.stringify({ reapedAt: new Date().toISOString(), reason: "ttl-reaper", binding }, null, 2));
181
+ dispatchAlert({
182
+ type: "ORPHAN_REAPED",
183
+ message: `Window binding ${binding.bindingId} reaped by TTL reaper after recovery snapshot.`,
184
+ consumerId: binding.codexSessionId,
185
+ sessionKey: binding.sessionKey,
186
+ context: {
187
+ bindingId: binding.bindingId,
188
+ runIds: binding.runIds,
189
+ windowId: binding.windowId,
190
+ tabGroupId: binding.tabGroupId,
191
+ targetId: binding.targetId,
192
+ repoSlug: binding.repoSlug,
193
+ branchName: binding.branchName,
194
+ snapshotPath: file,
195
+ reaperReason: "ttl-reaper",
196
+ },
197
+ });
198
+ console.log(`[binding-reaper] archived recovery snapshot -> ${file}`);
199
+ }
137
200
  function writeBoundError(res, err) {
138
201
  if (err instanceof BoundSessionError) {
139
202
  errorJson(res, `${err.code}: ${err.message}. ${err.repairAction}`, err.status);
@@ -2190,6 +2253,13 @@ async function handleTabGroupsListTabs(res) {
2190
2253
  if (result)
2191
2254
  json(res, result);
2192
2255
  }
2256
+ async function handleTabGroupsMetadataList(res) {
2257
+ const result = await withMutex(res, async () => {
2258
+ return await tabGroupsClient.listExtensionMetadata();
2259
+ });
2260
+ if (result)
2261
+ json(res, result);
2262
+ }
2193
2263
  async function handleTabGroupsCreate(res, body) {
2194
2264
  const result = await withMutex(res, async () => {
2195
2265
  const tabIds = body.tabIds;
@@ -2227,6 +2297,26 @@ async function handleTabGroupsUpdate(res, body) {
2227
2297
  json(res, result);
2228
2298
  }
2229
2299
  }
2300
+ async function handleTabGroupsMetadataUpdate(res, body) {
2301
+ const result = await withMutex(res, async () => {
2302
+ return await tabGroupsClient.updateExtensionMetadata({
2303
+ entityType: body.entityType,
2304
+ entityId: body.entityId,
2305
+ windowId: body.windowId,
2306
+ activeWindowId: body.activeWindowId,
2307
+ taskThreadId: body.taskThreadId,
2308
+ tabIndex: body.tabIndex,
2309
+ sourceFamily: body.sourceFamily,
2310
+ policyTier: body.policyTier,
2311
+ description: body.description,
2312
+ title: body.title,
2313
+ label: body.label,
2314
+ status: body.status,
2315
+ });
2316
+ });
2317
+ if (result)
2318
+ json(res, result);
2319
+ }
2230
2320
  async function handleTabGroupsDelete(res, body) {
2231
2321
  const result = await withMutex(res, async () => {
2232
2322
  const groupId = body.groupId;
@@ -2329,6 +2419,9 @@ const server = createServer(async (req, res) => {
2329
2419
  else if (path === "/api/tab-groups/tabs" && req.method === "GET") {
2330
2420
  await handleTabGroupsListTabs(res);
2331
2421
  }
2422
+ else if (path === "/api/tab-groups/metadata" && req.method === "GET") {
2423
+ await handleTabGroupsMetadataList(res);
2424
+ }
2332
2425
  else if (path === "/api/tab-groups" && req.method === "POST") {
2333
2426
  const body = await readBody(req);
2334
2427
  await handleTabGroupsCreate(res, body);
@@ -2337,6 +2430,10 @@ const server = createServer(async (req, res) => {
2337
2430
  const body = await readBody(req);
2338
2431
  await handleTabGroupsUpdate(res, body);
2339
2432
  }
2433
+ else if (path === "/api/tab-groups/metadata/update" && req.method === "POST") {
2434
+ const body = await readBody(req);
2435
+ await handleTabGroupsMetadataUpdate(res, body);
2436
+ }
2340
2437
  else if (path === "/api/tab-groups/delete" && req.method === "POST") {
2341
2438
  const body = await readBody(req);
2342
2439
  await handleTabGroupsDelete(res, body);
@@ -2476,8 +2573,32 @@ server.listen(PORT, () => {
2476
2573
  console.log(`\n --- Tab Groups ---`);
2477
2574
  console.log(` GET /api/tab-groups - List all tab groups`);
2478
2575
  console.log(` GET /api/tab-groups/tabs - List all tabs with group info`);
2576
+ console.log(` GET /api/tab-groups/metadata - List extension metadata`);
2479
2577
  console.log(` POST /api/tab-groups - Create group {tabIds, title?, color?}`);
2480
2578
  console.log(` POST /api/tab-groups/update - Update group {groupId, title?, color?, collapsed?}`);
2579
+ console.log(` POST /api/tab-groups/metadata/update - Update extension metadata`);
2481
2580
  console.log(` POST /api/tab-groups/delete - Delete group {groupId}`);
2581
+ // --- Window-binding TTL reaper (Spec 084) ---
2582
+ // Periodically prune binding records whose Comet window has been gone past the
2583
+ // TTL, so the Session Manager / mirror stop accumulating "Display unknown" phantoms.
2584
+ const reaperConfig = readReaperConfig();
2585
+ const stopReaper = startBindingReaper(reaperConfig, {
2586
+ getLiveWindowIds,
2587
+ archive: archiveBindingForReap,
2588
+ log: (m) => console.log(m),
2589
+ });
2590
+ for (const sig of ["SIGINT", "SIGTERM"]) {
2591
+ process.once(sig, () => {
2592
+ try {
2593
+ stopReaper();
2594
+ }
2595
+ catch {
2596
+ /* ignore */
2597
+ }
2598
+ const exitCode = sig === "SIGINT" ? 130 : 143;
2599
+ server.close(() => process.exit(exitCode));
2600
+ server.closeAllConnections?.();
2601
+ });
2602
+ }
2482
2603
  });
2483
2604
  //# sourceMappingURL=http-server.js.map