@shawnowen/comet-mcp 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -19
- package/dist/alert-dispatcher.d.ts +23 -0
- package/dist/alert-dispatcher.js +101 -0
- package/dist/bound-session.d.ts +23 -0
- package/dist/bound-session.js +119 -0
- package/dist/bridge-config.d.ts +6 -0
- package/dist/bridge-config.js +78 -0
- package/dist/cdp-client.d.ts +40 -4
- package/dist/cdp-client.js +502 -155
- package/dist/comet-ai.d.ts +15 -0
- package/dist/comet-ai.js +114 -38
- package/dist/delegate-binding.d.ts +19 -0
- package/dist/delegate-binding.js +73 -0
- package/dist/discovery/capability-entry.d.ts +215 -0
- package/dist/discovery/capability-entry.js +13 -0
- package/dist/discovery/description-template.d.ts +40 -0
- package/dist/discovery/description-template.js +61 -0
- package/dist/discovery/golden-queries.fixture.d.ts +22 -0
- package/dist/discovery/golden-queries.fixture.js +137 -0
- package/dist/discovery/mcp-source.d.ts +38 -0
- package/dist/discovery/mcp-source.js +70 -0
- package/dist/discovery/metadata-completeness.d.ts +48 -0
- package/dist/discovery/metadata-completeness.js +83 -0
- package/dist/discovery/registry.d.ts +35 -0
- package/dist/discovery/registry.js +35 -0
- package/dist/discovery/safety.d.ts +44 -0
- package/dist/discovery/safety.js +59 -0
- package/dist/discovery/schema-validator.d.ts +36 -0
- package/dist/discovery/schema-validator.js +257 -0
- package/dist/discovery/source-error.d.ts +47 -0
- package/dist/discovery/source-error.js +95 -0
- package/dist/discovery/tool-meta.d.ts +41 -0
- package/dist/discovery/tool-meta.js +229 -0
- package/dist/discovery/virtual-tools.d.ts +20 -0
- package/dist/discovery/virtual-tools.js +69 -0
- package/dist/http-server.js +2067 -47
- package/dist/index.js +3163 -710
- package/dist/observer.d.ts +47 -0
- package/dist/observer.js +516 -0
- package/dist/session-registry.d.ts +57 -0
- package/dist/session-registry.js +500 -0
- package/dist/sidecar-artifacts.d.ts +49 -0
- package/dist/sidecar-artifacts.js +146 -0
- package/dist/snapshot-capture.d.ts +3 -0
- package/dist/snapshot-capture.js +91 -0
- package/dist/tab-group-archive.js +3 -1
- package/dist/tab-groups.d.ts +7 -0
- package/dist/tab-groups.js +21 -3
- package/dist/task-thread-aggregator.d.ts +34 -0
- package/dist/task-thread-aggregator.js +480 -0
- package/dist/task-thread-canonical.d.ts +142 -0
- package/dist/task-thread-canonical.js +116 -0
- package/dist/types.d.ts +237 -0
- package/dist/window-bindings.d.ts +112 -0
- package/dist/window-bindings.js +476 -0
- package/extension/background.js +1556 -300
- package/extension/icons/icon.svg +9 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +19 -4
- package/extension/session-logic.js +2383 -0
- package/extension/session-manager.html +299 -0
- package/extension/sidepanel.css +5323 -528
- package/extension/sidepanel.html +282 -2
- package/extension/sidepanel.js +10075 -951
- package/extension/window-policy.js +162 -0
- package/package.json +10 -7
- package/vendor/lifecycle-mcp-adapter.mjs +103 -0
- package/vendor/lifecycle-metadata.mjs +252 -0
- package/vendor/readiness-report.mjs +742 -0
- package/dist/cdp-client.d.ts.map +0 -1
- package/dist/cdp-client.js.map +0 -1
- package/dist/comet-ai.d.ts.map +0 -1
- package/dist/comet-ai.js.map +0 -1
- package/dist/http-server.d.ts.map +0 -1
- package/dist/http-server.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tab-group-archive.d.ts.map +0 -1
- package/dist/tab-group-archive.js.map +0 -1
- package/dist/tab-groups.d.ts.map +0 -1
- package/dist/tab-groups.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/cdp-client.js
CHANGED
|
@@ -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() !==
|
|
15
|
+
if (platform() !== "linux")
|
|
13
16
|
return false;
|
|
14
17
|
try {
|
|
15
|
-
const release = execSync(
|
|
16
|
-
return release.includes(
|
|
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(
|
|
119
|
+
const net = await import("net");
|
|
65
120
|
return new Promise((resolve) => {
|
|
66
|
-
const client = net.createConnection({ port, host:
|
|
121
|
+
const client = net.createConnection({ port, host: "127.0.0.1" }, () => {
|
|
67
122
|
client.destroy();
|
|
68
123
|
resolve(true);
|
|
69
124
|
});
|
|
70
|
-
client.on(
|
|
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 =
|
|
158
|
+
async function windowsFetch(url, method = "GET") {
|
|
104
159
|
// Use native fetch on macOS/Linux (non-WSL)
|
|
105
|
-
if (platform() !==
|
|
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 ===
|
|
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:
|
|
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 () => {
|
|
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:
|
|
158
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(
|
|
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
|
-
|
|
197
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
333
|
+
catch {
|
|
334
|
+
/* target gone */
|
|
335
|
+
}
|
|
252
336
|
}
|
|
253
|
-
//
|
|
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 ===
|
|
256
|
-
targets.find(t => t.type ===
|
|
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(
|
|
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 ===
|
|
269
|
-
sidecar: targets.find(t => t.type ===
|
|
270
|
-
agentBrowsing: targets.find(t => t.type ===
|
|
271
|
-
!t.url.includes(
|
|
272
|
-
!t.url.includes(
|
|
273
|
-
!t.url.includes(
|
|
274
|
-
t.url !==
|
|
275
|
-
overlay: targets.find(t => t.url.includes(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
!t.url.includes(
|
|
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(
|
|
289
|
-
let output =
|
|
290
|
-
check.stdout?.on(
|
|
291
|
-
|
|
292
|
-
|
|
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(
|
|
390
|
+
check.on("error", () => resolve(false));
|
|
295
391
|
}
|
|
296
392
|
else {
|
|
297
393
|
// macOS/Linux: use pgrep
|
|
298
|
-
const check = spawn(
|
|
299
|
-
check.on(
|
|
300
|
-
check.on(
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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(
|
|
346
|
-
.trim()
|
|
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 =
|
|
351
|
-
|
|
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(
|
|
500
|
+
spawn("powershell.exe", ["-NoProfile", "-Command", psCommand], {
|
|
357
501
|
detached: true,
|
|
358
|
-
stdio:
|
|
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 {
|
|
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:
|
|
392
|
-
if (platform() ===
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
//
|
|
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 {
|
|
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:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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`, {
|
|
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 {
|
|
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:
|
|
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}` : ""}`,
|
|
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 {
|
|
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 ===
|
|
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 =
|
|
1017
|
+
const indent = " ".repeat(Math.min(depth, 10));
|
|
683
1018
|
let line = `${indent}[${role}`;
|
|
684
1019
|
// Add ref for interactive elements
|
|
685
|
-
const interactiveRoles = [
|
|
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 ===
|
|
1044
|
+
if (prop.name === "checked" && prop.value?.value)
|
|
700
1045
|
line += ` (checked)`;
|
|
701
|
-
if (prop.name ===
|
|
1046
|
+
if (prop.name === "expanded" && prop.value?.value === false)
|
|
702
1047
|
line += ` (collapsed)`;
|
|
703
|
-
if (prop.name ===
|
|
1048
|
+
if (prop.name === "disabled" && prop.value?.value)
|
|
704
1049
|
line += ` (disabled)`;
|
|
705
|
-
if (prop.name ===
|
|
1050
|
+
if (prop.name === "required" && prop.value?.value)
|
|
706
1051
|
line += ` (required)`;
|
|
707
|
-
if (prop.name ===
|
|
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(
|
|
1058
|
+
lines.push("... (truncated)");
|
|
714
1059
|
break;
|
|
715
1060
|
}
|
|
716
1061
|
lines.push(line);
|
|
717
1062
|
}
|
|
718
|
-
return lines.join(
|
|
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) +
|
|
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(
|
|
804
|
-
this.client.removeListener(
|
|
805
|
-
this.client.removeListener(
|
|
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(
|
|
852
|
-
this.client.on(
|
|
853
|
-
this.client.on(
|
|
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
|