@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.
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
@@ -1,19 +1,22 @@
1
1
  // CDP Client wrapper for Comet browser control
2
2
  // Supports macOS, Windows, and WSL
3
3
  import CDP from "chrome-remote-interface";
4
- import { spawn, execSync } from "child_process";
5
- import { platform } from "os";
6
- import { existsSync } from "fs";
4
+ import { spawn, execSync, execFileSync } from "child_process";
5
+ import { platform, homedir } from "os";
6
+ import { existsSync, appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { join, dirname } from "path";
8
+ import { loadBridgeConfig } from "./bridge-config.js";
9
+ import { dispatchAlert } from "./alert-dispatcher.js";
7
10
  // ============ PLATFORM DETECTION ============
8
11
  /**
9
12
  * Detect if running in WSL (Windows Subsystem for Linux)
10
13
  */
11
14
  function isWSL() {
12
- if (platform() !== 'linux')
15
+ if (platform() !== "linux")
13
16
  return false;
14
17
  try {
15
- const release = execSync('uname -r', { encoding: 'utf8' }).toLowerCase();
16
- return release.includes('microsoft') || release.includes('wsl');
18
+ const release = execSync("uname -r", { encoding: "utf8" }).toLowerCase();
19
+ return release.includes("microsoft") || release.includes("wsl");
17
20
  }
18
21
  catch {
19
22
  return false;
@@ -54,6 +57,58 @@ function getCometPath() {
54
57
  }
55
58
  const COMET_PATH = getCometPath();
56
59
  const DEFAULT_PORT = 9222;
60
+ const CRASH_LOG_PATH = join(homedir(), ".local", "log", "comet-crashes.log");
61
+ const EXTENSION_PATH = join(homedir(), "Documents", "repos", "Comet-Bridge", "comet-mcp", "extension");
62
+ function canonicalCometSessionTargetUrl(url) {
63
+ return url.startsWith("about:blank#comet-session-") ? url.toLowerCase() : url;
64
+ }
65
+ /**
66
+ * CDP windowState="normal" does not reliably exit native macOS fullscreen Spaces.
67
+ * Managed top-display Comet windows must stay windowed so control-left/right can
68
+ * switch normal Spaces and browser chrome remains visible.
69
+ */
70
+ function enforceWindowedTopDisplayOnMac() {
71
+ if (platform() !== "darwin")
72
+ return;
73
+ const script = `
74
+ tell application "System Events"
75
+ if exists process "Comet" then
76
+ tell process "Comet"
77
+ repeat with managedWindow in windows
78
+ try
79
+ set {xPos, yPos} to position of managedWindow
80
+ set {wWidth, wHeight} to size of managedWindow
81
+ if yPos < 0 and wWidth is greater than or equal to 800 then
82
+ if value of attribute "AXFullScreen" of managedWindow is true then
83
+ set value of attribute "AXFullScreen" of managedWindow to false
84
+ delay 0.5
85
+ end if
86
+ set position of managedWindow to {0, -1440}
87
+ set size of managedWindow to {2560, 1440}
88
+ end if
89
+ end try
90
+ end repeat
91
+ end tell
92
+ end if
93
+ end tell
94
+ `;
95
+ try {
96
+ execFileSync("osascript", ["-e", script], { stdio: "ignore", timeout: 5000 });
97
+ }
98
+ catch {
99
+ // Best-effort only; CDP bounds still keep the session usable if accessibility is unavailable.
100
+ }
101
+ }
102
+ function logCrashEvent(reason) {
103
+ try {
104
+ mkdirSync(join(homedir(), ".local", "log"), { recursive: true });
105
+ const ts = new Date().toISOString();
106
+ appendFileSync(CRASH_LOG_PATH, `${ts} ${reason}\n`);
107
+ }
108
+ catch {
109
+ /* best-effort logging */
110
+ }
111
+ }
57
112
  // ============ WSL NETWORK HELPERS ============
58
113
  /**
59
114
  * Check if WSL can directly connect to Windows localhost (mirrored networking)
@@ -61,13 +116,13 @@ const DEFAULT_PORT = 9222;
61
116
  async function canConnectToWindowsLocalhost(port) {
62
117
  if (!IS_WSL)
63
118
  return true;
64
- const net = await import('net');
119
+ const net = await import("net");
65
120
  return new Promise((resolve) => {
66
- const client = net.createConnection({ port, host: '127.0.0.1' }, () => {
121
+ const client = net.createConnection({ port, host: "127.0.0.1" }, () => {
67
122
  client.destroy();
68
123
  resolve(true);
69
124
  });
70
- client.on('error', () => {
125
+ client.on("error", () => {
71
126
  resolve(false);
72
127
  });
73
128
  client.setTimeout(2000, () => {
@@ -100,33 +155,35 @@ async function getWSLConnectPort(targetPort) {
100
155
  * Windows/WSL-compatible fetch using PowerShell
101
156
  * On WSL, native fetch connects to WSL's localhost, not Windows where Comet runs
102
157
  */
103
- async function windowsFetch(url, method = 'GET') {
158
+ async function windowsFetch(url, method = "GET") {
104
159
  // Use native fetch on macOS/Linux (non-WSL)
105
- if (platform() !== 'win32' && !IS_WSL) {
160
+ if (platform() !== "win32" && !IS_WSL) {
106
161
  const response = await fetch(url, { method });
107
162
  return response;
108
163
  }
109
164
  // On Windows or WSL, use PowerShell to reach Windows localhost
110
165
  try {
111
- const psCommand = method === 'PUT'
166
+ const psCommand = method === "PUT"
112
167
  ? `Invoke-WebRequest -Uri '${url}' -Method PUT -UseBasicParsing | Select-Object -ExpandProperty Content`
113
168
  : `Invoke-WebRequest -Uri '${url}' -UseBasicParsing | Select-Object -ExpandProperty Content`;
114
169
  const result = execSync(`powershell.exe -NoProfile -Command "${psCommand}"`, {
115
- encoding: 'utf8',
170
+ encoding: "utf8",
116
171
  timeout: 10000,
117
172
  windowsHide: true,
118
173
  });
119
174
  return {
120
175
  ok: true,
121
176
  status: 200,
122
- json: async () => JSON.parse(result.trim())
177
+ json: async () => JSON.parse(result.trim()),
123
178
  };
124
179
  }
125
180
  catch (error) {
126
181
  return {
127
182
  ok: false,
128
183
  status: 0,
129
- json: async () => { throw error; }
184
+ json: async () => {
185
+ throw error;
186
+ },
130
187
  };
131
188
  }
132
189
  }
@@ -136,6 +193,8 @@ export class CometCDPClient {
136
193
  state = {
137
194
  connected: false,
138
195
  port: DEFAULT_PORT,
196
+ profileId: "agent",
197
+ profileOwner: "agent",
139
198
  };
140
199
  lastTargetId;
141
200
  reconnectAttempts = 0;
@@ -144,6 +203,17 @@ export class CometCDPClient {
144
203
  get isConnected() {
145
204
  return this.state.connected && this.client !== null;
146
205
  }
206
+ /**
207
+ * Access the raw CDP protocol client for direct domain calls
208
+ * (e.g., Page.printToPDF, Network.enable, Input.dispatchKeyEvent).
209
+ * Throws if not connected — always check isConnected or call connect() first.
210
+ */
211
+ get protocol() {
212
+ if (!this.client) {
213
+ throw new Error("CDP client not connected. Call connect() first.");
214
+ }
215
+ return this.client;
216
+ }
147
217
  /**
148
218
  * Health check - verify connection is actually alive (not just "connected" in state)
149
219
  * This catches cases where WebSocket died silently
@@ -154,8 +224,8 @@ export class CometCDPClient {
154
224
  try {
155
225
  // Simple evaluation that should always work if connected
156
226
  const result = await Promise.race([
157
- this.client.Runtime.evaluate({ expression: '1+1', returnByValue: true }),
158
- new Promise((_, reject) => setTimeout(() => reject(new Error('Health check timeout')), 3000))
227
+ this.client.Runtime.evaluate({ expression: "1+1", returnByValue: true }),
228
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 3000)),
159
229
  ]);
160
230
  return result?.result?.value === 2;
161
231
  }
@@ -183,7 +253,7 @@ export class CometCDPClient {
183
253
  */
184
254
  async withAutoReconnect(operation) {
185
255
  if (this.isReconnecting) {
186
- await new Promise(resolve => setTimeout(resolve, 1000));
256
+ await new Promise((resolve) => setTimeout(resolve, 1000));
187
257
  }
188
258
  try {
189
259
  const result = await operation();
@@ -193,16 +263,23 @@ export class CometCDPClient {
193
263
  catch (error) {
194
264
  const errorMessage = error instanceof Error ? error.message : String(error);
195
265
  const connectionErrors = [
196
- 'WebSocket', 'CLOSED', 'not open', 'disconnected',
197
- 'ECONNREFUSED', 'ECONNRESET', 'Protocol error', 'Target closed', 'Session closed'
266
+ "WebSocket",
267
+ "CLOSED",
268
+ "not open",
269
+ "disconnected",
270
+ "ECONNREFUSED",
271
+ "ECONNRESET",
272
+ "Protocol error",
273
+ "Target closed",
274
+ "Session closed",
198
275
  ];
199
- if (connectionErrors.some(e => errorMessage.includes(e)) &&
276
+ if (connectionErrors.some((e) => errorMessage.includes(e)) &&
200
277
  this.reconnectAttempts < this.maxReconnectAttempts) {
201
278
  this.reconnectAttempts++;
202
279
  this.isReconnecting = true;
203
280
  try {
204
281
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 5000);
205
- await new Promise(resolve => setTimeout(resolve, delay));
282
+ await new Promise((resolve) => setTimeout(resolve, delay));
206
283
  await this.reconnect();
207
284
  this.isReconnecting = false;
208
285
  return await operation();
@@ -223,7 +300,9 @@ export class CometCDPClient {
223
300
  try {
224
301
  await this.client.close();
225
302
  }
226
- catch { /* ignore */ }
303
+ catch {
304
+ /* ignore */
305
+ }
227
306
  }
228
307
  this.state.connected = false;
229
308
  this.client = null;
@@ -232,32 +311,46 @@ export class CometCDPClient {
232
311
  await this.getVersion();
233
312
  }
234
313
  catch {
314
+ // Comet process is dead — log crash event and restart
315
+ logCrashEvent(`RECONNECT_RESTART: Comet not responding on port ${this.state.port}, attempting restart`);
235
316
  try {
236
317
  await this.startComet(this.state.port);
237
- await new Promise(resolve => setTimeout(resolve, 2000));
318
+ await new Promise((resolve) => setTimeout(resolve, 2000));
238
319
  }
239
320
  catch {
240
- throw new Error('Cannot connect to Comet. Ensure Comet is running with --remote-debugging-port=9222');
321
+ logCrashEvent("RECONNECT_FAILED: Could not restart Comet");
322
+ throw new Error("Cannot connect to Comet. Ensure Comet is running with --remote-debugging-port=9222");
241
323
  }
242
324
  }
243
- // Try to reconnect to last target
325
+ // Try to reconnect to last target (session-specific — never grab another agent's tab)
244
326
  if (this.lastTargetId) {
245
327
  try {
246
328
  const targets = await this.listTargets();
247
- if (targets.find(t => t.id === this.lastTargetId)) {
329
+ if (targets.find((t) => t.id === this.lastTargetId)) {
248
330
  return await this.connect(this.lastTargetId);
249
331
  }
250
332
  }
251
- catch { /* target gone */ }
333
+ catch {
334
+ /* target gone */
335
+ }
252
336
  }
253
- // Find best target
337
+ // Spec 034: Do NOT fall back to grabbing any Perplexity tab.
338
+ // In multi-agent mode, picking a random tab would hijack another agent's session.
339
+ // If we had a lastTargetId and it's gone, fail explicitly so the caller
340
+ // can re-register via comet_connect.
341
+ if (this.lastTargetId) {
342
+ throw new Error(`Session target ${this.lastTargetId} no longer exists. ` +
343
+ `Call comet_connect to establish a new session.`);
344
+ }
345
+ // No lastTargetId means this is a legacy/global client without session isolation.
346
+ // Fall back to finding a tab (backward compat for non-session usage).
254
347
  const targets = await this.listTargets();
255
- const target = targets.find(t => t.type === 'page' && t.url.includes('perplexity.ai')) ||
256
- targets.find(t => t.type === 'page' && t.url !== 'about:blank');
348
+ const target = targets.find((t) => t.type === "page" && t.url.includes("perplexity.ai")) ||
349
+ targets.find((t) => t.type === "page" && t.url !== "about:blank");
257
350
  if (target) {
258
351
  return await this.connect(target.id);
259
352
  }
260
- throw new Error('No suitable tab found for reconnection');
353
+ throw new Error("No suitable tab found for reconnection. Call comet_connect first.");
261
354
  }
262
355
  /**
263
356
  * List tabs with categorization
@@ -265,17 +358,18 @@ export class CometCDPClient {
265
358
  async listTabsCategorized() {
266
359
  const targets = await this.listTargets();
267
360
  return {
268
- main: targets.find(t => t.type === 'page' && t.url.includes('perplexity.ai') && !t.url.includes('sidecar')) || null,
269
- sidecar: targets.find(t => t.type === 'page' && t.url.includes('sidecar')) || null,
270
- agentBrowsing: targets.find(t => t.type === 'page' &&
271
- !t.url.includes('perplexity.ai') &&
272
- !t.url.includes('chrome-extension') &&
273
- !t.url.includes('chrome://') &&
274
- t.url !== 'about:blank') || null,
275
- overlay: targets.find(t => t.url.includes('chrome-extension') && t.url.includes('overlay')) || null,
276
- others: targets.filter(t => t.type === 'page' &&
277
- !t.url.includes('perplexity.ai') &&
278
- !t.url.includes('chrome-extension')),
361
+ main: targets.find((t) => t.type === "page" && t.url.includes("perplexity.ai") && !t.url.includes("sidecar")) || null,
362
+ sidecar: targets.find((t) => t.type === "page" && t.url.includes("sidecar")) || null,
363
+ agentBrowsing: targets.find((t) => t.type === "page" &&
364
+ !t.url.includes("perplexity.ai") &&
365
+ !t.url.includes("chrome-extension") &&
366
+ !t.url.includes("chrome://") &&
367
+ t.url !== "about:blank") || null,
368
+ overlay: targets.find((t) => t.url.includes("chrome-extension") && t.url.includes("overlay")) ||
369
+ null,
370
+ others: targets.filter((t) => t.type === "page" &&
371
+ !t.url.includes("perplexity.ai") &&
372
+ !t.url.includes("chrome-extension")),
279
373
  };
280
374
  }
281
375
  /**
@@ -285,46 +379,93 @@ export class CometCDPClient {
285
379
  return new Promise((resolve) => {
286
380
  if (IS_WINDOWS) {
287
381
  // Windows: use tasklist to check for comet.exe
288
- const check = spawn('tasklist', ['/FI', 'IMAGENAME eq comet.exe', '/NH']);
289
- let output = '';
290
- check.stdout?.on('data', (data) => { output += data.toString(); });
291
- check.on('close', () => {
292
- resolve(output.toLowerCase().includes('comet.exe'));
382
+ const check = spawn("tasklist", ["/FI", "IMAGENAME eq comet.exe", "/NH"]);
383
+ let output = "";
384
+ check.stdout?.on("data", (data) => {
385
+ output += data.toString();
386
+ });
387
+ check.on("close", () => {
388
+ resolve(output.toLowerCase().includes("comet.exe"));
293
389
  });
294
- check.on('error', () => resolve(false));
390
+ check.on("error", () => resolve(false));
295
391
  }
296
392
  else {
297
393
  // macOS/Linux: use pgrep
298
- const check = spawn('pgrep', ['-f', 'Comet.app']);
299
- check.on('close', (code) => resolve(code === 0));
300
- check.on('error', () => resolve(false));
394
+ const check = spawn("pgrep", ["-f", "Comet.app"]);
395
+ check.on("close", (code) => resolve(code === 0));
396
+ check.on("error", () => resolve(false));
301
397
  }
302
398
  });
303
399
  }
304
- /**
305
- * Kill any running Comet process
306
- */
307
- async killComet() {
308
- return new Promise((resolve) => {
309
- if (IS_WINDOWS) {
310
- // Windows: use taskkill to kill comet.exe
311
- const kill = spawn('taskkill', ['/F', '/IM', 'comet.exe']);
312
- kill.on('close', () => setTimeout(resolve, 1000));
313
- kill.on('error', () => setTimeout(resolve, 1000));
400
+ // ---- Crash Loop Protection (Spec 016, FR-005, T032-T033) ----
401
+ readCrashHistory() {
402
+ try {
403
+ const config = loadBridgeConfig();
404
+ const data = readFileSync(config.crashRecovery.crashHistoryPath, "utf-8");
405
+ return JSON.parse(data);
406
+ }
407
+ catch (err) {
408
+ // ENOENT is expected on first run only log actual errors
409
+ if (err?.code !== "ENOENT") {
410
+ console.warn(`[comet-bridge] Crash history read failed: ${err instanceof Error ? err.message : err}`);
314
411
  }
315
- else {
316
- // macOS/Linux: use pkill
317
- const kill = spawn('pkill', ['-f', 'Comet.app']);
318
- kill.on('close', () => setTimeout(resolve, 1000));
319
- kill.on('error', () => setTimeout(resolve, 1000));
412
+ return [];
413
+ }
414
+ }
415
+ writeCrashHistory(timestamps) {
416
+ try {
417
+ const config = loadBridgeConfig();
418
+ mkdirSync(dirname(config.crashRecovery.crashHistoryPath), { recursive: true });
419
+ writeFileSync(config.crashRecovery.crashHistoryPath, JSON.stringify(timestamps));
420
+ }
421
+ catch (err) {
422
+ console.error(`[comet-bridge] Crash history write failed: ${err instanceof Error ? err.message : err}`);
423
+ }
424
+ }
425
+ recordCrash() {
426
+ const timestamps = this.readCrashHistory();
427
+ timestamps.push(new Date().toISOString());
428
+ this.writeCrashHistory(timestamps);
429
+ }
430
+ checkCrashLoopCap() {
431
+ try {
432
+ const config = loadBridgeConfig();
433
+ const maxRestarts = config.crashRecovery.maxRestarts;
434
+ const windowMs = config.crashRecovery.windowMinutes * 60 * 1000;
435
+ const cutoff = Date.now() - windowMs;
436
+ const history = this.readCrashHistory();
437
+ const recent = history.filter((ts) => new Date(ts).getTime() > cutoff);
438
+ // Clean up old entries
439
+ if (recent.length !== history.length) {
440
+ this.writeCrashHistory(recent);
320
441
  }
321
- });
442
+ return {
443
+ blocked: recent.length >= maxRestarts,
444
+ count: recent.length,
445
+ windowMinutes: config.crashRecovery.windowMinutes,
446
+ };
447
+ }
448
+ catch (err) {
449
+ console.error(`[comet-bridge] Crash loop cap check failed: ${err instanceof Error ? err.message : err}. Failing closed (blocking restart).`);
450
+ return { blocked: true, count: -1, windowMinutes: 10 };
451
+ }
322
452
  }
323
453
  /**
324
454
  * Start Comet browser with remote debugging enabled
325
455
  * Handles macOS, Windows, and WSL environments
326
456
  */
327
457
  async startComet(port = DEFAULT_PORT) {
458
+ // Crash loop guard (Spec 016, FR-005, T033)
459
+ const crashCheck = this.checkCrashLoopCap();
460
+ if (crashCheck.blocked) {
461
+ dispatchAlert({
462
+ type: "CRASH_LOOP_CAP",
463
+ message: `Browser restart blocked: ${crashCheck.count} crashes in ${crashCheck.windowMinutes} minutes. Manual intervention required.`,
464
+ context: { crashCount: crashCheck.count, windowMinutes: crashCheck.windowMinutes },
465
+ });
466
+ throw new Error(`Crash loop detected: ${crashCheck.count} crashes in ${crashCheck.windowMinutes} minutes. ` +
467
+ `Automatic restart halted. Manual intervention required.`);
468
+ }
328
469
  this.state.port = port;
329
470
  // ========== WSL: Use PowerShell to communicate with Windows ==========
330
471
  if (IS_WSL) {
@@ -332,7 +473,7 @@ export class CometCDPClient {
332
473
  try {
333
474
  const response = await windowsFetch(`http://127.0.0.1:${port}/json/version`);
334
475
  if (response.ok) {
335
- const version = await response.json();
476
+ const version = (await response.json());
336
477
  return `Comet already running on Windows host, port: ${port} (${version.Browser})`;
337
478
  }
338
479
  }
@@ -340,22 +481,25 @@ export class CometCDPClient {
340
481
  // Comet not accessible, need to launch
341
482
  }
342
483
  // Get Windows LOCALAPPDATA path and construct Comet path
343
- let cometPath = '';
484
+ let cometPath = "";
344
485
  try {
345
- const localAppData = execSync('cmd.exe /c echo %LOCALAPPDATA%', { encoding: 'utf8' })
346
- .trim().replace(/\r?\n/g, '');
486
+ const localAppData = execSync("cmd.exe /c echo %LOCALAPPDATA%", { encoding: "utf8" })
487
+ .trim()
488
+ .replace(/\r?\n/g, "");
347
489
  cometPath = `${localAppData}\\Perplexity\\Comet\\Application\\Comet.exe`;
348
490
  }
349
491
  catch {
350
- cometPath = 'C:\\Users\\' + (process.env.USER || 'user') +
351
- '\\AppData\\Local\\Perplexity\\Comet\\Application\\Comet.exe';
492
+ cometPath =
493
+ "C:\\Users\\" +
494
+ (process.env.USER || "user") +
495
+ "\\AppData\\Local\\Perplexity\\Comet\\Application\\Comet.exe";
352
496
  }
353
497
  try {
354
498
  // Launch Comet via PowerShell (Set-Location avoids UNC path issues)
355
499
  const psCommand = `Set-Location C:\\; Start-Process -FilePath '${cometPath}' -ArgumentList '--remote-debugging-port=${port}'`;
356
- spawn('powershell.exe', ['-NoProfile', '-Command', psCommand], {
500
+ spawn("powershell.exe", ["-NoProfile", "-Command", psCommand], {
357
501
  detached: true,
358
- stdio: 'ignore',
502
+ stdio: "ignore",
359
503
  }).unref();
360
504
  // Wait for Comet to start
361
505
  return new Promise((resolve, reject) => {
@@ -370,7 +514,9 @@ export class CometCDPClient {
370
514
  return;
371
515
  }
372
516
  }
373
- catch { /* keep trying */ }
517
+ catch {
518
+ /* keep trying */
519
+ }
374
520
  if (attempts < maxAttempts) {
375
521
  setTimeout(checkReady, 500);
376
522
  }
@@ -388,22 +534,31 @@ export class CometCDPClient {
388
534
  `Error: ${launchError instanceof Error ? launchError.message : String(launchError)}`);
389
535
  }
390
536
  }
391
- // ========== Native Windows: Use windowsFetch for HTTP ==========
392
- if (platform() === 'win32') {
393
- try {
394
- const response = await windowsFetch(`http://127.0.0.1:${port}/json/version`);
395
- if (response.ok) {
396
- const version = await response.json();
397
- return `Comet already running with debug port: ${version.Browser}`;
537
+ // ========== Native Windows: Retry with exponential backoff (Spec 034, FR-005) ==========
538
+ if (platform() === "win32") {
539
+ const WIN_RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000];
540
+ for (let attempt = 0; attempt <= WIN_RETRY_DELAYS.length; attempt++) {
541
+ try {
542
+ const response = await windowsFetch(`http://127.0.0.1:${port}/json/version`);
543
+ if (response.ok) {
544
+ const version = (await response.json());
545
+ return `Comet already running with debug port: ${version.Browser}`;
546
+ }
398
547
  }
399
- }
400
- catch {
401
- const isRunning = await this.isCometProcessRunning();
402
- if (isRunning) {
403
- await this.killComet();
548
+ catch {
549
+ if (attempt < WIN_RETRY_DELAYS.length) {
550
+ console.warn(`[comet-bridge] CDP health check failed (attempt ${attempt + 1}/${WIN_RETRY_DELAYS.length + 1}), retrying in ${WIN_RETRY_DELAYS[attempt]}ms...`);
551
+ await new Promise((resolve) => setTimeout(resolve, WIN_RETRY_DELAYS[attempt]));
552
+ }
404
553
  }
405
554
  }
406
- // Start Comet on Windows
555
+ // All retries exhausted — check if Comet process exists (FR-006, FR-007)
556
+ const winIsRunning = await this.isCometProcessRunning();
557
+ if (winIsRunning) {
558
+ throw new Error(`Comet is running but CDP port ${port} is unreachable after retries.\n` +
559
+ `Restart Comet with: "${COMET_PATH}" --remote-debugging-port=${port}`);
560
+ }
561
+ // No Comet process — safe to start
407
562
  return new Promise((resolve, reject) => {
408
563
  this.cometProcess = spawn(COMET_PATH, [`--remote-debugging-port=${port}`], {
409
564
  detached: true,
@@ -421,7 +576,9 @@ export class CometCDPClient {
421
576
  return;
422
577
  }
423
578
  }
424
- catch { /* keep trying */ }
579
+ catch {
580
+ /* keep trying */
581
+ }
425
582
  if (attempts < maxAttempts) {
426
583
  setTimeout(checkReady, 500);
427
584
  }
@@ -432,28 +589,54 @@ export class CometCDPClient {
432
589
  setTimeout(checkReady, 1500);
433
590
  });
434
591
  }
435
- // ========== macOS/Linux: Original approach ==========
436
- try {
437
- const controller = new AbortController();
438
- const timeoutId = setTimeout(() => controller.abort(), 2000);
439
- const response = await fetch(`http://localhost:${port}/json/version`, { signal: controller.signal });
440
- clearTimeout(timeoutId);
441
- if (response.ok) {
442
- const version = await response.json();
443
- return `Comet already running with debug port: ${version.Browser}`;
592
+ // ========== macOS/Linux: Retry with exponential backoff (Spec 034, FR-005) ==========
593
+ const RETRY_DELAYS = [1000, 2000, 4000, 8000, 16000]; // ~31s total
594
+ for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
595
+ try {
596
+ const controller = new AbortController();
597
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
598
+ const response = await fetch(`http://localhost:${port}/json/version`, {
599
+ signal: controller.signal,
600
+ });
601
+ clearTimeout(timeoutId);
602
+ if (response.ok) {
603
+ const version = (await response.json());
604
+ return `Comet already running with debug port: ${version.Browser}`;
605
+ }
444
606
  }
445
- }
446
- catch {
447
- const isRunning = await this.isCometProcessRunning();
448
- if (isRunning) {
449
- await this.killComet();
607
+ catch {
608
+ if (attempt < RETRY_DELAYS.length) {
609
+ console.warn(`[comet-bridge] CDP health check failed (attempt ${attempt + 1}/${RETRY_DELAYS.length + 1}), retrying in ${RETRY_DELAYS[attempt]}ms...`);
610
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAYS[attempt]));
611
+ }
450
612
  }
451
613
  }
452
- // Start Comet on macOS/Linux
614
+ // All retries exhausted — check if Comet process exists (FR-006, FR-007)
615
+ const isRunning = await this.isCometProcessRunning();
616
+ if (isRunning) {
617
+ // NEVER kill — report diagnostic error instead (FR-001)
618
+ throw new Error(`Comet is running but CDP port ${port} is unreachable after ${RETRY_DELAYS.length + 1} attempts (~31s).\n` +
619
+ `Restart Comet with: ${COMET_PATH} --remote-debugging-port=${port}\n` +
620
+ `Or check if another process is using port ${port}.`);
621
+ }
622
+ // No Comet process at all — safe to start a new one (FR-007)
453
623
  return new Promise((resolve, reject) => {
454
- this.cometProcess = spawn(COMET_PATH, [`--remote-debugging-port=${port}`], {
624
+ const args = [
625
+ `--remote-debugging-port=${port}`,
626
+ "--no-first-run",
627
+ "--no-default-browser-check",
628
+ "--disable-background-timer-throttling",
629
+ "--disable-backgrounding-occluded-windows",
630
+ "--disable-renderer-backgrounding",
631
+ ];
632
+ // Load tab groups extension if available
633
+ if (existsSync(EXTENSION_PATH)) {
634
+ args.push(`--load-extension=${EXTENSION_PATH}`);
635
+ }
636
+ this.cometProcess = spawn(COMET_PATH, args, {
455
637
  detached: true,
456
638
  stdio: "ignore",
639
+ env: { ...process.env, CHROME_HEADLESS: "1" },
457
640
  });
458
641
  this.cometProcess.unref();
459
642
  const maxAttempts = 40;
@@ -463,15 +646,19 @@ export class CometCDPClient {
463
646
  try {
464
647
  const controller = new AbortController();
465
648
  const timeoutId = setTimeout(() => controller.abort(), 2000);
466
- const response = await fetch(`http://localhost:${port}/json/version`, { signal: controller.signal });
649
+ const response = await fetch(`http://localhost:${port}/json/version`, {
650
+ signal: controller.signal,
651
+ });
467
652
  clearTimeout(timeoutId);
468
653
  if (response.ok) {
469
- const version = await response.json();
654
+ const version = (await response.json());
470
655
  resolve(`Comet started with debug port ${port}: ${version.Browser}`);
471
656
  return;
472
657
  }
473
658
  }
474
- catch { /* keep trying */ }
659
+ catch {
660
+ /* keep trying */
661
+ }
475
662
  if (attempts < maxAttempts) {
476
663
  setTimeout(checkReady, 500);
477
664
  }
@@ -482,6 +669,65 @@ export class CometCDPClient {
482
669
  setTimeout(checkReady, 1500);
483
670
  });
484
671
  }
672
+ /**
673
+ * Position the Comet browser window on the top display using full-screen bounds.
674
+ * Top display staging bounds: origin (0, -1440), size 2560x1440.
675
+ * Keep the native window state normal so browser tabs and toolbar remain visible.
676
+ * Uses CDP Browser.getWindowForTarget + Browser.setWindowBounds.
677
+ */
678
+ async positionOnTopDisplay(targetId) {
679
+ if (platform() !== "darwin")
680
+ return; // Only applies to this Mac Studio setup
681
+ try {
682
+ // Use targetId if provided, otherwise find any page target
683
+ let pageTarget;
684
+ if (targetId) {
685
+ pageTarget = { id: targetId };
686
+ }
687
+ else {
688
+ let targets = await this.listTargets();
689
+ pageTarget = targets.find((t) => t.type === "page");
690
+ // Retry once if no targets (Comet 145.x quirk where /json/list returns empty)
691
+ if (!pageTarget) {
692
+ await new Promise((r) => setTimeout(r, 500));
693
+ targets = await this.listTargets();
694
+ pageTarget = targets.find((t) => t.type === "page");
695
+ if (!pageTarget)
696
+ return;
697
+ }
698
+ }
699
+ // Temporarily connect to get the window ID
700
+ const tempClient = await CDP({
701
+ host: "127.0.0.1",
702
+ port: this.state.port,
703
+ target: pageTarget.webSocketDebuggerUrl || pageTarget.id,
704
+ });
705
+ try {
706
+ const { windowId } = await tempClient.Browser.getWindowForTarget();
707
+ // First set to normal state (can't set bounds while maximized/fullscreen)
708
+ await tempClient.Browser.setWindowBounds({
709
+ windowId,
710
+ bounds: { windowState: "normal" },
711
+ });
712
+ await tempClient.Browser.setWindowBounds({
713
+ windowId,
714
+ bounds: { left: 0, top: -1440, width: 2560, height: 1440 },
715
+ });
716
+ enforceWindowedTopDisplayOnMac();
717
+ }
718
+ finally {
719
+ try {
720
+ await tempClient.close();
721
+ }
722
+ catch {
723
+ /* ignore */
724
+ }
725
+ }
726
+ }
727
+ catch {
728
+ // Best-effort positioning — browser still works without it
729
+ }
730
+ }
485
731
  /**
486
732
  * Get CDP version info
487
733
  */
@@ -499,7 +745,7 @@ export class CometCDPClient {
499
745
  // so fall back to /json which is equivalent but more reliable.
500
746
  const response = await windowsFetch(`http://127.0.0.1:${this.state.port}/json/list`);
501
747
  if (response.ok) {
502
- const targets = await response.json();
748
+ const targets = (await response.json());
503
749
  if (targets.length > 0)
504
750
  return targets;
505
751
  }
@@ -508,6 +754,51 @@ export class CometCDPClient {
508
754
  throw new Error(`Failed to list targets: ${fallback.status}`);
509
755
  return fallback.json();
510
756
  }
757
+ async waitForTargetUrl(url, timeoutMs = 10_000) {
758
+ const startedAt = Date.now();
759
+ const canonicalTargetUrl = canonicalCometSessionTargetUrl(url);
760
+ while (Date.now() - startedAt < timeoutMs) {
761
+ const targets = await this.listTargets();
762
+ const target = targets.find((t) => t.type === "page" &&
763
+ (t.url === url || canonicalCometSessionTargetUrl(t.url) === canonicalTargetUrl));
764
+ if (target)
765
+ return target;
766
+ await new Promise((resolve) => setTimeout(resolve, 250));
767
+ }
768
+ throw new Error(`Timed out waiting for Comet target URL: ${url}`);
769
+ }
770
+ async getWindowIdForTarget(target) {
771
+ const tempClient = await CDP({
772
+ host: "127.0.0.1",
773
+ port: this.state.port,
774
+ target: target.webSocketDebuggerUrl || target.id,
775
+ });
776
+ try {
777
+ const { windowId } = await tempClient.Browser.getWindowForTarget();
778
+ return typeof windowId === "number" ? windowId : null;
779
+ }
780
+ finally {
781
+ try {
782
+ await tempClient.close();
783
+ }
784
+ catch {
785
+ /* ignore */
786
+ }
787
+ }
788
+ }
789
+ async findSidecarTargetForWindow(windowId) {
790
+ const sidecarTargets = (await this.listTargets()).filter((target) => target.type === "page" && target.url.includes("sidecar"));
791
+ for (const target of sidecarTargets) {
792
+ try {
793
+ if ((await this.getWindowIdForTarget(target)) === windowId)
794
+ return target;
795
+ }
796
+ catch {
797
+ /* target may have disappeared between /json/list and attach */
798
+ }
799
+ }
800
+ return null;
801
+ }
511
802
  /**
512
803
  * Connect to a specific tab
513
804
  */
@@ -517,10 +808,42 @@ export class CometCDPClient {
517
808
  }
518
809
  // On WSL, verify mirrored networking is available for WebSocket connection
519
810
  const connectPort = await getWSLConnectPort(this.state.port);
520
- const options = { port: connectPort, host: '127.0.0.1' };
811
+ const options = { port: connectPort, host: "127.0.0.1" };
521
812
  if (targetId)
522
813
  options.target = targetId;
523
814
  this.client = await CDP(options);
815
+ // Proactive crash detection: listen for WebSocket disconnects
816
+ this.client.on("disconnect", () => {
817
+ const wasConnected = this.state.connected;
818
+ this.state.connected = false;
819
+ this.client = null;
820
+ if (wasConnected) {
821
+ // Crash error classification (Spec 016, FR-004, T034)
822
+ this.isCometProcessRunning()
823
+ .then((running) => {
824
+ if (running) {
825
+ logCrashEvent("CDP_DISCONNECT: WebSocket connection lost — browser still running");
826
+ dispatchAlert({
827
+ type: "CDP_DISCONNECT",
828
+ message: "CDP WebSocket connection lost, but browser process is still running.",
829
+ });
830
+ }
831
+ else {
832
+ logCrashEvent("BROWSER_CRASH: Browser process terminated unexpectedly");
833
+ this.recordCrash();
834
+ dispatchAlert({
835
+ type: "BROWSER_CRASH",
836
+ message: "Browser process terminated unexpectedly. Automatic restart will be attempted on next connect.",
837
+ severity: "critical",
838
+ });
839
+ }
840
+ })
841
+ .catch(() => {
842
+ logCrashEvent("CDP_DISCONNECT: WebSocket connection lost — could not check process");
843
+ });
844
+ console.error("[comet-bridge] CDP connection lost");
845
+ }
846
+ });
524
847
  await Promise.all([
525
848
  this.client.Page.enable(),
526
849
  this.client.Runtime.enable(),
@@ -528,23 +851,6 @@ export class CometCDPClient {
528
851
  this.client.Network.enable(),
529
852
  this.client.Accessibility.enable(),
530
853
  ]);
531
- // Position window fullscreen on top display (agents workspace)
532
- // See: ~/.claude/workflows/display-browser-config.md
533
- try {
534
- const { windowId } = await this.client.Browser.getWindowForTarget({ targetId });
535
- await this.client.Browser.setWindowBounds({
536
- windowId,
537
- bounds: { left: 0, top: -1080, width: 1920, height: 1080, windowState: 'normal' },
538
- });
539
- }
540
- catch {
541
- try {
542
- await this.client.Emulation.setDeviceMetricsOverride({
543
- width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false,
544
- });
545
- }
546
- catch { /* continue */ }
547
- }
548
854
  this.state.connected = true;
549
855
  this.state.activeTabId = targetId;
550
856
  this.lastTargetId = targetId;
@@ -621,11 +927,38 @@ export class CometCDPClient {
621
927
  await this.client.Input.dispatchKeyEvent({ type: "keyDown", key });
622
928
  await this.client.Input.dispatchKeyEvent({ type: "keyUp", key });
623
929
  }
930
+ /**
931
+ * Return the existing Perplexity sidecar CDP target without activating Comet
932
+ * or sending OS-level keyboard shortcuts.
933
+ */
934
+ async ensureSidecarOpen(options = {}) {
935
+ if (options.windowId !== undefined) {
936
+ const existingBoundSidecar = await this.findSidecarTargetForWindow(options.windowId);
937
+ if (existingBoundSidecar)
938
+ return existingBoundSidecar;
939
+ }
940
+ // Check if sidecar is already open
941
+ const tabs = await this.listTabsCategorized();
942
+ if (tabs.sidecar && options.windowId === undefined)
943
+ return tabs.sidecar;
944
+ return null;
945
+ }
946
+ /**
947
+ * Connect a separate CDP client to the sidecar target for isolated interaction.
948
+ */
949
+ async connectToSidecar(options = {}) {
950
+ const sidecarTarget = await this.ensureSidecarOpen(options);
951
+ if (!sidecarTarget)
952
+ return null;
953
+ const sidecarClient = new CometCDPClient();
954
+ await sidecarClient.connect(sidecarTarget.id);
955
+ return sidecarClient;
956
+ }
624
957
  /**
625
958
  * Create a new tab
626
959
  */
627
960
  async newTab(url) {
628
- const response = await windowsFetch(`http://127.0.0.1:${this.state.port}/json/new${url ? `?${url}` : ""}`, 'PUT');
961
+ const response = await windowsFetch(`http://127.0.0.1:${this.state.port}/json/new${url ? `?${url}` : ""}`, "PUT");
629
962
  if (!response.ok)
630
963
  throw new Error(`Failed to create new tab: ${response.status}`);
631
964
  return response.json();
@@ -640,7 +973,9 @@ export class CometCDPClient {
640
973
  return result.success;
641
974
  }
642
975
  }
643
- catch { /* fallback to HTTP */ }
976
+ catch {
977
+ /* fallback to HTTP */
978
+ }
644
979
  try {
645
980
  const response = await windowsFetch(`http://127.0.0.1:${this.state.port}/json/close/${targetId}`);
646
981
  return response.ok;
@@ -661,12 +996,12 @@ export class CometCDPClient {
661
996
  const node = nodes[i];
662
997
  if (node.ignored)
663
998
  continue;
664
- const role = node.role?.value || '';
665
- if (role === 'none' || role === 'generic' || role === 'InlineTextBox')
999
+ const role = node.role?.value || "";
1000
+ if (role === "none" || role === "generic" || role === "InlineTextBox")
666
1001
  continue;
667
- const name = node.name?.value || '';
668
- const value = node.value?.value || '';
669
- const description = node.description?.value || '';
1002
+ const name = node.name?.value || "";
1003
+ const value = node.value?.value || "";
1004
+ const description = node.description?.value || "";
670
1005
  // Calculate depth from parent chain
671
1006
  let depth = 0;
672
1007
  let parentId = node.parentId;
@@ -679,14 +1014,24 @@ export class CometCDPClient {
679
1014
  depth++;
680
1015
  parentId = parent.parentId;
681
1016
  }
682
- const indent = ' '.repeat(Math.min(depth, 10));
1017
+ const indent = " ".repeat(Math.min(depth, 10));
683
1018
  let line = `${indent}[${role}`;
684
1019
  // Add ref for interactive elements
685
- const interactiveRoles = ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'menuitem', 'tab', 'switch'];
1020
+ const interactiveRoles = [
1021
+ "button",
1022
+ "link",
1023
+ "textbox",
1024
+ "checkbox",
1025
+ "radio",
1026
+ "combobox",
1027
+ "menuitem",
1028
+ "tab",
1029
+ "switch",
1030
+ ];
686
1031
  if (interactiveRoles.includes(role)) {
687
1032
  line += ` ref_${i}`;
688
1033
  }
689
- line += ']';
1034
+ line += "]";
690
1035
  if (name)
691
1036
  line += ` "${name}"`;
692
1037
  if (value)
@@ -696,26 +1041,26 @@ export class CometCDPClient {
696
1041
  // Add relevant properties
697
1042
  if (node.properties) {
698
1043
  for (const prop of node.properties) {
699
- if (prop.name === 'checked' && prop.value?.value)
1044
+ if (prop.name === "checked" && prop.value?.value)
700
1045
  line += ` (checked)`;
701
- if (prop.name === 'expanded' && prop.value?.value === false)
1046
+ if (prop.name === "expanded" && prop.value?.value === false)
702
1047
  line += ` (collapsed)`;
703
- if (prop.name === 'disabled' && prop.value?.value)
1048
+ if (prop.name === "disabled" && prop.value?.value)
704
1049
  line += ` (disabled)`;
705
- if (prop.name === 'required' && prop.value?.value)
1050
+ if (prop.name === "required" && prop.value?.value)
706
1051
  line += ` (required)`;
707
- if (prop.name === 'selected' && prop.value?.value)
1052
+ if (prop.name === "selected" && prop.value?.value)
708
1053
  line += ` (selected)`;
709
1054
  }
710
1055
  }
711
1056
  totalLength += line.length + 1;
712
1057
  if (totalLength > maxLength) {
713
- lines.push('... (truncated)');
1058
+ lines.push("... (truncated)");
714
1059
  break;
715
1060
  }
716
1061
  lines.push(line);
717
1062
  }
718
- return lines.join('\n');
1063
+ return lines.join("\n");
719
1064
  }
720
1065
  /**
721
1066
  * Extract clean readable text from the current page
@@ -772,9 +1117,9 @@ export class CometCDPClient {
772
1117
  return cleaned;
773
1118
  })()
774
1119
  `);
775
- let text = result.result.value || '';
1120
+ let text = result.result.value || "";
776
1121
  if (text.length > maxLength) {
777
- text = text.substring(0, maxLength) + '\n... (truncated)';
1122
+ text = text.substring(0, maxLength) + "\n... (truncated)";
778
1123
  }
779
1124
  return text;
780
1125
  }
@@ -800,11 +1145,13 @@ export class CometCDPClient {
800
1145
  if (timeoutTimer)
801
1146
  clearTimeout(timeoutTimer);
802
1147
  try {
803
- this.client.removeListener('Network.requestWillBeSent', onRequest);
804
- this.client.removeListener('Network.loadingFinished', onFinished);
805
- this.client.removeListener('Network.loadingFailed', onFailed);
1148
+ this.client.removeListener("Network.requestWillBeSent", onRequest);
1149
+ this.client.removeListener("Network.loadingFinished", onFinished);
1150
+ this.client.removeListener("Network.loadingFailed", onFailed);
1151
+ }
1152
+ catch {
1153
+ /* ignore listener removal errors */
806
1154
  }
807
- catch { /* ignore listener removal errors */ }
808
1155
  };
809
1156
  const finish = (idle) => {
810
1157
  if (resolved)
@@ -848,9 +1195,9 @@ export class CometCDPClient {
848
1195
  resetIdleTimer();
849
1196
  }
850
1197
  };
851
- this.client.on('Network.requestWillBeSent', onRequest);
852
- this.client.on('Network.loadingFinished', onFinished);
853
- this.client.on('Network.loadingFailed', onFailed);
1198
+ this.client.on("Network.requestWillBeSent", onRequest);
1199
+ this.client.on("Network.loadingFinished", onFinished);
1200
+ this.client.on("Network.loadingFailed", onFailed);
854
1201
  // Start idle timer immediately (page may already be idle)
855
1202
  resetIdleTimer();
856
1203
  // Overall timeout