@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
package/README.md CHANGED
@@ -1,10 +1,6 @@
1
1
  # comet-mcp
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/comet-mcp.svg)](https://www.npmjs.com/package/comet-mcp)
4
-
5
- <a href="https://glama.ai/mcp/servers/@hanzili/comet-mcp">
6
- <img width="380" height="200" src="https://glama.ai/mcp/servers/@hanzili/comet-mcp/badge" />
7
- </a>
3
+ [![npm version](https://img.shields.io/npm/v/@shawnowen/comet-mcp.svg)](https://www.npmjs.com/package/@shawnowen/comet-mcp)
8
4
 
9
5
  **Give Claude Code a browser that thinks.**
10
6
 
@@ -22,25 +18,29 @@ Return static text. No interaction, no login, no dynamic content. Great for quic
22
18
  ### 2. Browser Automation (browser-use, Puppeteer MCP, Playwright MCP)
23
19
  Can interact with pages, but use a **one-agent-do-all** approach: the same reasoning model that's writing your code is also deciding where to click, what to type, and how to navigate. This overwhelms the context window and fragments focus.
24
20
 
25
- ### 3. Comet MCP: Multi-Agent Delegation
26
- **Comet MCP takes a different approach.** Instead of Claude controlling a browser directly, it delegates to [Perplexity Comet](https://www.perplexity.ai/comet) - an AI purpose-built for web research and browsing.
21
+ ### 3. Comet MCP: Direct Control + AI Delegation
22
+ **Comet MCP gives you both.** Direct DOM interaction for deterministic tasks (clicking, typing, form filling) AND delegation to [Perplexity Comet](https://www.perplexity.ai/comet) for AI-powered research and complex browsing.
23
+
24
+ - **Direct mode** — `comet_interact` and `comet_navigate` give Claude instant, precise control over any web page via CDP. No AI middleman. Click buttons, fill forms, read content in milliseconds
25
+ - **AI mode** — `comet_ask` delegates to Perplexity for research, summarization, and complex browsing decisions
26
+ - **Sidecar mode** — `comet_ask` with `sidecar: true` keeps the current page visible and uses the Perplexity Assistant sidebar for page-aware Q&A
27
+ - **Result**: Deterministic precision when you need it, AI intelligence when you want it
27
28
 
28
- - **Claude** stays focused on your coding task
29
- - **Comet** handles the browsing: navigation, login walls, dynamic content, deep research
30
- - **Result**: Claude's coding intelligence + Perplexity's web intelligence, working together
29
+ > **For full toolkit installation, see the [canonical install guide](../docs/INSTALLATION.md).**
30
+ > Run `npx comet-browser-automation install` from any compatible repo to set up all configuration automatically.
31
31
 
32
32
  ## Quick Start
33
33
 
34
34
  ### 1. Configure Claude Code
35
35
 
36
- Add to `~/.claude.json` or `.mcp.json`:
36
+ Add to `~/.claude/settings.json` or `.mcp.json`:
37
37
 
38
38
  ```json
39
39
  {
40
40
  "mcpServers": {
41
41
  "comet-bridge": {
42
42
  "command": "npx",
43
- "args": ["-y", "comet-mcp"]
43
+ "args": ["-y", "@shawnowen/comet-mcp"]
44
44
  }
45
45
  }
46
46
  }
@@ -62,17 +62,90 @@ 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
65
+ ## Tools (26)
66
+
67
+ ### Direct Interaction (2) — No AI, pure CDP
68
+
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `comet_interact` | Click, type, fill, check/uncheck, select, scroll, extract text, or run JS on the active page. Supports chaining multiple actions in one call |
72
+ | `comet_navigate` | Navigate the active tab to any URL (QBO, Mercury, GitHub, etc.) without opening Perplexity |
73
+
74
+ **`comet_interact` actions:** `click`, `type`, `fill`, `press`, `check`, `uncheck`, `select`, `scroll`, `wait`, `extract`, `evaluate`
75
+
76
+ ### AI-Assisted Browsing (9)
66
77
 
67
78
  | Tool | Description |
68
79
  |------|-------------|
69
80
  | `comet_connect` | Connect to Comet (auto-starts if needed) |
70
- | `comet_ask` | Send a task and wait for response |
81
+ | `comet_ask` | Send a task to Perplexity and wait for response. Use `sidecar: true` to keep current page and ask via the assistant sidebar |
71
82
  | `comet_poll` | Check progress on long-running tasks |
72
83
  | `comet_stop` | Stop current task |
73
84
  | `comet_screenshot` | Capture current page |
74
85
  | `comet_mode` | Switch modes: search, research, labs, learn |
75
- | `comet_tab_groups` | Manage Chrome tab groups (list, create, update, delete) |
86
+ | `comet_shortcut` | Trigger a Comet Query Shortcut |
87
+ | `comet_read_page` | Extract page as accessibility tree + clean text |
88
+ | `comet_wait_for_idle` | Wait for network activity to settle |
89
+
90
+ ### Tab Groups (1)
91
+
92
+ | Tool | Description |
93
+ |------|-------------|
94
+ | `comet_tab_groups` | Manage Chrome tab groups (list, create, update, delete, archive, restore) |
95
+
96
+ ### Lifecycle (4)
97
+
98
+ | Tool | Description |
99
+ |------|-------------|
100
+ | `comet_lifecycle_start` | Register a new lifecycle run |
101
+ | `comet_lifecycle_complete` | Mark a run as completed |
102
+ | `comet_lifecycle_abort` | Abort a run with optional reason |
103
+ | `comet_lifecycle_update` | Update run metadata |
104
+
105
+ Lifecycle tools are binding-aware: `start` attaches the run ID to the active or supplied Codex window binding, `complete` transitions the binding to `completed`, and `abort` transitions it to `stale`.
106
+
107
+ ### Orchestration (3)
108
+
109
+ | Tool | Description |
110
+ |------|-------------|
111
+ | `comet_task_status` | Get unified status (session manifest + extension events + lifecycle) |
112
+ | `comet_delegate` | High-level task dispatch with direct Codex binding creation/reuse, URL, and lifecycle setup |
113
+ | `comet_observe` | Passively observe browser state, binding owners, and stale/conflict/unbound windows without disrupting active agents |
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
+
126
+ ## Codex Window Bindings
127
+
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.
129
+
130
+ Binding rules:
131
+
132
+ - Normal session agents can operate only on their own binding.
133
+ - Worktree and fleet orchestrators can observe wider scopes, but cross-session mutation requires explicit target binding, reason, and audit.
134
+ - `comet_task_status`, `comet_peek`, sidecar artifacts, lifecycle transitions, and delegation all resolve through the binding index instead of the currently focused window.
135
+ - `comet_delegate` no longer shells out to the legacy session controller. It creates or reuses a binding directly and returns `bindingId`, `windowId`, `tabGroupId`, and `dispatchStatus`.
136
+
137
+ Focus-independent routing means browser actions use the resolved binding's `targetId`, `windowId`, and `tabGroupId`, not whatever Comet window happens to be frontmost. Orchestrator-only cross-session actions must carry the target binding and audit reason so ordinary agents cannot accidentally mutate another agent's window.
138
+
139
+ ### Choosing the Right Tool
140
+
141
+ | Task | Use | Why |
142
+ |------|-----|-----|
143
+ | Click buttons, fill forms | `comet_interact` | Direct CDP, milliseconds |
144
+ | Navigate to a URL | `comet_navigate` | Immediate, no AI |
145
+ | Read page content | `comet_read_page` | Structured a11y tree |
146
+ | Research a topic | `comet_ask` | Perplexity's strength |
147
+ | "Summarize this page" | `comet_ask(sidecar: true)` | Sidecar sees active tab |
148
+ | Take a screenshot | `comet_screenshot` | Direct CDP capture |
76
149
 
77
150
  ## Tab Groups
78
151
 
@@ -110,7 +183,8 @@ Claude Code → MCP Server → CDP → Extension Service Worker → chrome.tabGr
110
183
  For environments that can't use MCP directly (e.g., sandboxed VMs), an HTTP server exposes all tools as REST endpoints:
111
184
 
112
185
  ```bash
113
- npm run build && npm run http
186
+ npx @shawnowen/comet-mcp --http
187
+ # Or if installed locally: npm run http
114
188
  # Starts on http://localhost:3456
115
189
  ```
116
190
 
@@ -123,7 +197,11 @@ Claude Code → MCP Server → CDP → Comet Browser → Perplexity AI
123
197
  (reasoning) (bridge) (web browsing)
124
198
  ```
125
199
 
126
- Claude sends high-level goals ("research X", "log into Y"). Comet figures out the clicks, scrolls, and searches. Results flow back to Claude.
200
+ **Direct mode**: Claude sends DOM actions (`comet_interact`, `comet_navigate`) CDP executes them instantly on the active page.
201
+
202
+ **AI mode**: Claude sends high-level goals (`comet_ask`) → Comet/Perplexity figures out the clicks, scrolls, and searches → results flow back.
203
+
204
+ **Sidecar mode**: Claude asks about the current page (`comet_ask` with `sidecar: true`) → Perplexity Assistant sees and interacts with the active tab → results flow back.
127
205
 
128
206
  ## Requirements
129
207
 
@@ -164,7 +242,7 @@ If Comet is installed in a non-standard location:
164
242
  "mcpServers": {
165
243
  "comet-bridge": {
166
244
  "command": "npx",
167
- "args": ["-y", "comet-mcp"],
245
+ "args": ["-y", "@shawnowen/comet-mcp"],
168
246
  "env": {
169
247
  "COMET_PATH": "/path/to/your/Comet"
170
248
  }
@@ -193,4 +271,4 @@ MIT
193
271
 
194
272
  ---
195
273
 
196
- [Report Issues](https://github.com/hanzili/comet-mcp/issues) · [Contribute](https://github.com/hanzili/comet-mcp)
274
+ [Report Issues](https://github.com/EQUAStart/equa-comet-browser-control/issues) · [Contribute](https://github.com/EQUAStart/equa-comet-browser-control)
@@ -0,0 +1,23 @@
1
+ import type { ErrorEvent, ErrorEventType, ErrorSeverity } from "./types.js";
2
+ declare const mcpErrorQueue: ErrorEvent[];
3
+ export interface DispatchOptions {
4
+ type: ErrorEventType;
5
+ message: string;
6
+ consumerId?: string | null;
7
+ sessionKey?: string | null;
8
+ context?: Record<string, unknown>;
9
+ suggestedAction?: string;
10
+ severity?: ErrorSeverity;
11
+ }
12
+ export declare function dispatchAlert(options: DispatchOptions): ErrorEvent;
13
+ /**
14
+ * Drain the MCP error queue — call on each tool response to append alerts.
15
+ * Returns queued alerts and clears the queue.
16
+ */
17
+ export declare function drainMcpAlertQueue(): ErrorEvent[];
18
+ /**
19
+ * Format drained alerts as text to append to MCP tool responses.
20
+ */
21
+ export declare function formatAlertsForResponse(events: ErrorEvent[]): string;
22
+ export { mcpErrorQueue };
23
+ //# sourceMappingURL=alert-dispatcher.d.ts.map
@@ -0,0 +1,101 @@
1
+ // Alert Dispatcher — three-channel alert system (Spec 016, FR-011)
2
+ // Channels: macOS notification, JSONL log file, in-memory MCP error queue
3
+ // Routes alerts by severity and consumer role per BridgeConfig.alertRouting
4
+ import { appendFileSync, mkdirSync } from "fs";
5
+ import { execFile } from "child_process";
6
+ import { dirname } from "path";
7
+ import { randomUUID } from "crypto";
8
+ import { loadBridgeConfig } from "./bridge-config.js";
9
+ // In-memory MCP error queue — drained on next tool response
10
+ const mcpErrorQueue = [];
11
+ // Suggested actions by error type
12
+ const SUGGESTED_ACTIONS = {
13
+ PERPLEXITY_VOICE_MODE: "Start a fresh Perplexity session by calling comet_connect with a new taskThreadId",
14
+ PERPLEXITY_IDLE_NO_NAV: "Retry the task. If persistent, use comet_stop then start a fresh session via comet_connect",
15
+ PERPLEXITY_CONTEXT_BLEED: "Start a fresh Perplexity session to prevent context from prior conversation bleeding into this task",
16
+ BROWSER_CRASH: "Browser is restarting automatically. Reconnect with comet_connect after restart completes",
17
+ CRASH_LOOP_CAP: "Automatic restart halted after 3 attempts in 10 minutes. Manual intervention required",
18
+ CDP_DISCONNECT: "Connection lost. Call comet_connect to re-establish",
19
+ ORPHAN_DETECTED: "Task Thread will be snapshot-closed at threshold",
20
+ ORPHAN_REAPED: "Task Thread snapshot-closed. Check snapshots directory for recovery",
21
+ DUPLICATE_PROCESS: "Multiple comet-mcp processes detected. Review and terminate extras",
22
+ PROTOCOL_VIOLATION: "Missing prerequisite step. Check error message for required sequence",
23
+ };
24
+ // Default severity by error type
25
+ const DEFAULT_SEVERITY = {
26
+ BROWSER_CRASH: "critical",
27
+ CRASH_LOOP_CAP: "critical",
28
+ DUPLICATE_PROCESS: "operational",
29
+ ORPHAN_DETECTED: "operational",
30
+ ORPHAN_REAPED: "operational",
31
+ PERPLEXITY_VOICE_MODE: "warning",
32
+ PERPLEXITY_IDLE_NO_NAV: "warning",
33
+ PERPLEXITY_CONTEXT_BLEED: "warning",
34
+ CDP_DISCONNECT: "warning",
35
+ PROTOCOL_VIOLATION: "warning",
36
+ };
37
+ export function dispatchAlert(options) {
38
+ const config = loadBridgeConfig();
39
+ const severity = options.severity || DEFAULT_SEVERITY[options.type] || "warning";
40
+ const event = {
41
+ id: randomUUID(),
42
+ timestamp: new Date().toISOString(),
43
+ type: options.type,
44
+ severity,
45
+ consumerId: options.consumerId ?? null,
46
+ sessionKey: options.sessionKey ?? null,
47
+ message: options.message,
48
+ context: options.context || {},
49
+ suggestedAction: options.suggestedAction || SUGGESTED_ACTIONS[options.type] || "",
50
+ delivered: { macosNotification: false, logFile: false, mcpQueue: false },
51
+ };
52
+ // Channel 1: JSONL log (always — durable record)
53
+ try {
54
+ mkdirSync(dirname(config.alerts.logPath), { recursive: true });
55
+ appendFileSync(config.alerts.logPath, JSON.stringify(event) + "\n");
56
+ event.delivered.logFile = true;
57
+ }
58
+ catch (logErr) {
59
+ console.error(`[comet-bridge] ALERT LOG FAILED: Could not write to ${config.alerts.logPath}: ${logErr instanceof Error ? logErr.message : logErr}`);
60
+ }
61
+ // Channel 2: macOS notification (if enabled and severity warrants)
62
+ if (config.alerts.macosNotifications && severity !== "warning") {
63
+ const title = `Comet Bridge [${severity.toUpperCase()}]`;
64
+ const msg = event.message.substring(0, 200);
65
+ // execFile is safe — no shell injection (arguments are array, not interpolated)
66
+ execFile("osascript", ["-e", `display notification "${msg.replace(/"/g, '\\"')}" with title "${title}"`], (err) => {
67
+ if (!err) {
68
+ event.delivered.macosNotification = true;
69
+ }
70
+ else if (severity === "critical") {
71
+ console.error(`[comet-bridge] macOS notification failed for ${severity} alert: ${err.message}`);
72
+ }
73
+ });
74
+ }
75
+ // Channel 3: MCP error queue (if enabled)
76
+ if (config.alerts.mcpErrorQueue) {
77
+ mcpErrorQueue.push(event);
78
+ event.delivered.mcpQueue = true;
79
+ }
80
+ return event;
81
+ }
82
+ /**
83
+ * Drain the MCP error queue — call on each tool response to append alerts.
84
+ * Returns queued alerts and clears the queue.
85
+ */
86
+ export function drainMcpAlertQueue() {
87
+ const events = [...mcpErrorQueue];
88
+ mcpErrorQueue.length = 0;
89
+ return events;
90
+ }
91
+ /**
92
+ * Format drained alerts as text to append to MCP tool responses.
93
+ */
94
+ export function formatAlertsForResponse(events) {
95
+ if (events.length === 0)
96
+ return "";
97
+ const lines = events.map((e) => `⚠️ [${e.severity.toUpperCase()}] ${e.type}: ${e.message}`);
98
+ return "\n\n--- Queued Alerts ---\n" + lines.join("\n");
99
+ }
100
+ export { mcpErrorQueue };
101
+ //# sourceMappingURL=alert-dispatcher.js.map
@@ -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
@@ -0,0 +1,23 @@
1
+ import type { AgentSession } from "./types.js";
2
+ import { type CodexIdentityInput, type CodexSessionIdentity, type CodexWindowBinding, type CodexWindowBindingStore } from "./window-bindings.js";
3
+ export type BoundSessionErrorCode = "MISSING_SESSION" | "MISSING_IDENTITY" | "MISSING_BINDING" | "STALE_BINDING" | "CONFLICT_BINDING" | "OWNERSHIP_VIOLATION";
4
+ export declare class BoundSessionError extends Error {
5
+ readonly code: BoundSessionErrorCode;
6
+ readonly repairAction: string;
7
+ readonly status: number;
8
+ constructor(code: BoundSessionErrorCode, message: string, repairAction: string, status?: number);
9
+ toMcpText(toolName: string): string;
10
+ }
11
+ export interface BoundSessionResolution {
12
+ session: AgentSession;
13
+ identity: CodexSessionIdentity;
14
+ binding: CodexWindowBinding;
15
+ }
16
+ export interface BoundHttpResolution {
17
+ identity: CodexSessionIdentity;
18
+ binding: CodexWindowBinding;
19
+ }
20
+ export declare function validateBoundTargetHints(binding: CodexWindowBinding, args?: Record<string, unknown>): void;
21
+ export declare function resolveBoundSession(session: AgentSession | undefined, args?: Record<string, unknown>, store?: CodexWindowBindingStore): Promise<BoundSessionResolution>;
22
+ export declare function resolveHttpBoundSession(input: CodexIdentityInput, args?: Record<string, unknown>, store?: CodexWindowBindingStore): Promise<BoundHttpResolution>;
23
+ //# sourceMappingURL=bound-session.d.ts.map
@@ -0,0 +1,119 @@
1
+ import { assertBindingProfileAllowed, deriveCodexSessionIdentity, isBindingInWorktreeScope, windowBindingStore, } from "./window-bindings.js";
2
+ export class BoundSessionError extends Error {
3
+ code;
4
+ repairAction;
5
+ status;
6
+ constructor(code, message, repairAction, status = 409) {
7
+ super(message);
8
+ this.code = code;
9
+ this.repairAction = repairAction;
10
+ this.status = status;
11
+ this.name = "BoundSessionError";
12
+ }
13
+ toMcpText(toolName) {
14
+ return [
15
+ `Binding error (${this.code}) before ${toolName}: ${this.message}`,
16
+ `Safe repair action: ${this.repairAction}`,
17
+ ].join("\n\n");
18
+ }
19
+ }
20
+ const TARGET_HINT_KEYS = ["targetId"];
21
+ const WINDOW_HINT_KEYS = ["windowId"];
22
+ const TAB_GROUP_HINT_KEYS = ["tabGroupId", "groupId"];
23
+ function stringValue(value) {
24
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
25
+ }
26
+ function numberValue(value) {
27
+ if (typeof value === "number" && Number.isFinite(value))
28
+ return value;
29
+ if (typeof value === "string" && value.trim().length > 0) {
30
+ const parsed = Number(value);
31
+ if (Number.isFinite(parsed))
32
+ return parsed;
33
+ }
34
+ return undefined;
35
+ }
36
+ function getHintArgs(args) {
37
+ return args ?? {};
38
+ }
39
+ export function validateBoundTargetHints(binding, args) {
40
+ const hints = getHintArgs(args);
41
+ for (const key of TARGET_HINT_KEYS) {
42
+ const targetId = stringValue(hints[key]);
43
+ if (targetId && targetId !== binding.targetId) {
44
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `${key}=${targetId} is outside binding ${binding.bindingId}`, binding.targetId
45
+ ? `Use the bound targetId ${binding.targetId} or reconnect with comet_connect.`
46
+ : "Reconnect with comet_connect so the binding has a targetId before targeting a page.");
47
+ }
48
+ }
49
+ for (const key of WINDOW_HINT_KEYS) {
50
+ const windowId = numberValue(hints[key]);
51
+ if (windowId !== undefined && windowId !== binding.windowId) {
52
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `${key}=${windowId} is outside binding ${binding.bindingId}`, `Use the bound windowId ${binding.windowId} or reconnect with comet_connect.`);
53
+ }
54
+ }
55
+ for (const key of TAB_GROUP_HINT_KEYS) {
56
+ const tabGroupId = numberValue(hints[key]);
57
+ if (tabGroupId !== undefined && tabGroupId !== binding.tabGroupId) {
58
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `${key}=${tabGroupId} is outside binding ${binding.bindingId}`, binding.tabGroupId !== null
59
+ ? `Use the bound tabGroupId ${binding.tabGroupId} or reconnect with comet_connect.`
60
+ : "Reconnect with comet_connect so the binding has a tabGroupId before targeting a group.");
61
+ }
62
+ }
63
+ const sessionKey = stringValue(hints.sessionKey);
64
+ if (sessionKey && sessionKey !== binding.sessionKey) {
65
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `sessionKey=${sessionKey} is outside binding ${binding.bindingId}`, `Use the bound sessionKey ${binding.sessionKey}.`);
66
+ }
67
+ const projectThreadId = stringValue(hints.projectThreadId) ?? stringValue(hints.threadId);
68
+ if (projectThreadId && projectThreadId !== binding.projectThreadId) {
69
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `projectThreadId=${projectThreadId} is outside binding ${binding.bindingId}`, `Use the bound projectThreadId ${binding.projectThreadId}.`);
70
+ }
71
+ }
72
+ async function resolveBindingByArgs(store, identity, args) {
73
+ const bindingId = stringValue(args?.bindingId);
74
+ if (bindingId)
75
+ return store.get(bindingId);
76
+ const runId = stringValue(args?.runId);
77
+ if (runId)
78
+ return store.findByRunId(runId);
79
+ return store.findActiveByIdentity(identity);
80
+ }
81
+ function assertBindingUsable(identity, binding) {
82
+ if (!binding) {
83
+ throw new BoundSessionError("MISSING_BINDING", "No active Codex window binding exists for this caller.", "Call comet_connect with Codex identity fields to create or repair the binding.");
84
+ }
85
+ if (binding.status === "stale" || binding.status === "reaped" || binding.status === "completed") {
86
+ throw new BoundSessionError("STALE_BINDING", `Binding ${binding.bindingId} is ${binding.status}.`, "Call comet_connect to repair or replace the stale binding.");
87
+ }
88
+ if (binding.status === "conflict") {
89
+ throw new BoundSessionError("CONFLICT_BINDING", `Binding ${binding.bindingId} is in conflict.`, "Resolve the conflicting window ownership before using browser tools.");
90
+ }
91
+ if (!isBindingInWorktreeScope(identity, binding)) {
92
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", `Caller ${identity.sessionKey} is not authorized for binding ${binding.bindingId}.`, "Use the caller's own binding or an authorized orchestrator identity.");
93
+ }
94
+ try {
95
+ assertBindingProfileAllowed(identity, binding);
96
+ }
97
+ catch (err) {
98
+ throw new BoundSessionError("OWNERSHIP_VIOLATION", err instanceof Error ? err.message : String(err), "Use an agent-owned Comet runtime profile or reconnect with comet_connect.", 403);
99
+ }
100
+ return binding;
101
+ }
102
+ export async function resolveBoundSession(session, args, store = windowBindingStore) {
103
+ if (!session) {
104
+ throw new BoundSessionError("MISSING_SESSION", "No active session is registered.", "Call comet_connect before using bound browser tools.");
105
+ }
106
+ if (!session.codexIdentity) {
107
+ throw new BoundSessionError("MISSING_IDENTITY", `Session ${session.sessionKey} has no Codex identity metadata.`, "Reconnect with comet_connect so Codex identity can be derived and persisted.");
108
+ }
109
+ const binding = assertBindingUsable(session.codexIdentity, await resolveBindingByArgs(store, session.codexIdentity, args));
110
+ validateBoundTargetHints(binding, args);
111
+ return { session, identity: session.codexIdentity, binding };
112
+ }
113
+ export async function resolveHttpBoundSession(input, args, store = windowBindingStore) {
114
+ const identity = deriveCodexSessionIdentity({ ...input, strict: true });
115
+ const binding = assertBindingUsable(identity, await resolveBindingByArgs(store, identity, args));
116
+ validateBoundTargetHints(binding, args);
117
+ return { identity, binding };
118
+ }
119
+ //# sourceMappingURL=bound-session.js.map
@@ -0,0 +1,6 @@
1
+ import type { BridgeConfig } from "./types.js";
2
+ declare const CONFIG_PATH: string;
3
+ declare const DEFAULTS: BridgeConfig;
4
+ export declare function loadBridgeConfig(): BridgeConfig;
5
+ export { CONFIG_PATH, DEFAULTS };
6
+ //# sourceMappingURL=bridge-config.d.ts.map
@@ -0,0 +1,78 @@
1
+ // Bridge Configuration — unified config for MCP server, extension, and scripts (Spec 016, FR-013)
2
+ // Reads from ~/.claude/comet-browser/bridge-config.json with typed defaults.
3
+ // Re-reads on each access — no caching, so config changes take effect on next tool call.
4
+ import { readFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ const CONFIG_PATH = join(homedir(), ".claude", "comet-browser", "bridge-config.json");
8
+ const DEFAULTS = {
9
+ version: "1.0.0",
10
+ cleanup: {
11
+ orphanThresholdMinutes: 120,
12
+ snapshotBeforeClose: true,
13
+ snapshotDir: join(homedir(), ".claude", "comet-browser", "snapshots"),
14
+ },
15
+ crashRecovery: {
16
+ maxRestarts: 3,
17
+ windowMinutes: 10,
18
+ autoRestart: true,
19
+ crashHistoryPath: join(homedir(), ".claude", "comet-browser", "crash-history.json"),
20
+ },
21
+ alerts: {
22
+ logPath: join(homedir(), ".local", "log", "comet-alerts.log"),
23
+ macosNotifications: true,
24
+ mcpErrorQueue: true,
25
+ routing: {
26
+ critical: "operator-direct",
27
+ operational: "orchestrator-first",
28
+ warning: "orchestrator-only",
29
+ },
30
+ },
31
+ roles: {
32
+ operatorId: "operator",
33
+ orchestratorAgentId: null,
34
+ },
35
+ protocolEnforcement: {
36
+ requireConnectBeforeBrowse: true,
37
+ warnMissingTabGroup: true,
38
+ },
39
+ };
40
+ export function loadBridgeConfig() {
41
+ try {
42
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
43
+ try {
44
+ const parsed = JSON.parse(raw);
45
+ return deepMerge(DEFAULTS, parsed);
46
+ }
47
+ catch (parseErr) {
48
+ console.error(`[comet-bridge] CONFIG PARSE ERROR in ${CONFIG_PATH}: ${parseErr instanceof Error ? parseErr.message : parseErr}. Using defaults.`);
49
+ return { ...DEFAULTS };
50
+ }
51
+ }
52
+ catch (readErr) {
53
+ if (readErr?.code !== "ENOENT") {
54
+ console.error(`[comet-bridge] CONFIG READ ERROR: ${readErr?.message}. Using defaults.`);
55
+ }
56
+ return { ...DEFAULTS };
57
+ }
58
+ }
59
+ function deepMerge(defaults, overrides) {
60
+ const result = { ...defaults };
61
+ for (const key of Object.keys(overrides)) {
62
+ const val = overrides[key];
63
+ if (val !== undefined && val !== null) {
64
+ if (typeof val === "object" &&
65
+ !Array.isArray(val) &&
66
+ typeof result[key] === "object" &&
67
+ result[key] !== null) {
68
+ result[key] = deepMerge(result[key], val);
69
+ }
70
+ else {
71
+ result[key] = val;
72
+ }
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ export { CONFIG_PATH, DEFAULTS };
78
+ //# sourceMappingURL=bridge-config.js.map