@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/http-server.js
CHANGED
|
@@ -6,10 +6,173 @@
|
|
|
6
6
|
// Architecture:
|
|
7
7
|
// Cowork VM -> Claude-in-Chrome MCP -> Chrome fetch('localhost:3456') -> this server -> CDP -> Comet
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
|
+
import { readFileSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
9
12
|
import { cometClient } from "./cdp-client.js";
|
|
10
13
|
import { cometAI } from "./comet-ai.js";
|
|
11
14
|
import { tabGroupsClient } from "./tab-groups.js";
|
|
15
|
+
import { BoundSessionError, resolveHttpBoundSession } from "./bound-session.js";
|
|
16
|
+
import { createOrReuseDelegateBinding } from "./delegate-binding.js";
|
|
17
|
+
import { deriveCodexSessionIdentity, windowBindingStore, } from "./window-bindings.js";
|
|
18
|
+
import { readAgentRegistry, classifyAgentStatus, getHealth, getSnapshot, getStatus, getDetail, formatHealth, formatSnapshot, } from "./observer.js";
|
|
12
19
|
const PORT = parseInt(process.env.COMET_HTTP_PORT || "3456", 10);
|
|
20
|
+
// Lifecycle + orchestration paths (mirrored from index.ts)
|
|
21
|
+
const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL || "http://localhost:3001/command-center/api/comet/lifecycle";
|
|
22
|
+
const OUTBOX_PATH = join(homedir(), "equabot", "agent-chat", "outbox-comet.jsonl");
|
|
23
|
+
const INBOX_PATH = join(homedir(), "equabot", "agent-chat", "inbox-comet.jsonl");
|
|
24
|
+
const MANIFEST_PATH = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
|
|
25
|
+
const EQUA_SERVER_URL = process.env.EQUA_SERVER_URL || "http://localhost:3000";
|
|
26
|
+
const EQUANAUT_GATEWAY_ASK_URL = process.env.EQUANAUT_GATEWAY_ASK_URL || "";
|
|
27
|
+
const EQUANAUT_GATEWAY_STATUS_URLS = [
|
|
28
|
+
process.env.EQUANAUT_GATEWAY_STATUS_URL,
|
|
29
|
+
"http://localhost:3001/api/v1/agent/gateway/status",
|
|
30
|
+
"http://localhost:3001/command-center/api/v1/agent/gateway/status",
|
|
31
|
+
"http://localhost:3000/api/v1/agent/gateway/status",
|
|
32
|
+
].filter(Boolean);
|
|
33
|
+
const EQUANAUT_OLLAMA_BASE = process.env.EQUANAUT_OLLAMA_BASE || "http://localhost:11434";
|
|
34
|
+
// ---- Helper utilities ----
|
|
35
|
+
/** Safely quote a CSS selector for use in browser evaluate() expressions */
|
|
36
|
+
function safeSelector(sel) {
|
|
37
|
+
return JSON.stringify(sel);
|
|
38
|
+
}
|
|
39
|
+
/** Append a JSON line to a JSONL file (non-fatal if directory missing) */
|
|
40
|
+
function appendJsonl(filePath, obj) {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
43
|
+
appendFileSync(filePath, JSON.stringify(obj) + "\n", "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* non-fatal — outbox may not be configured */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Read and parse a JSON file safely, returning null on any error */
|
|
50
|
+
function readJsonSafe(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** POST to Command Center lifecycle API */
|
|
59
|
+
async function callLifecycleEndpoint(payload) {
|
|
60
|
+
const resp = await fetch(CC_LIFECYCLE_URL, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(payload),
|
|
64
|
+
signal: AbortSignal.timeout(5000),
|
|
65
|
+
});
|
|
66
|
+
const data = await resp.text();
|
|
67
|
+
if (!resp.ok) {
|
|
68
|
+
throw new Error(`CC-SO lifecycle ${resp.status}: ${data.substring(0, 200)}`);
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Session guard for browsing endpoints.
|
|
74
|
+
* Returns true (and sends HTTP 409) if no active session; returns false if OK to proceed.
|
|
75
|
+
*/
|
|
76
|
+
function requireSession(res) {
|
|
77
|
+
if (!cometClient.isConnected) {
|
|
78
|
+
errorJson(res, "No active Comet session. Call POST /api/connect first.", 409);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function paramsToRecord(params) {
|
|
84
|
+
const args = {};
|
|
85
|
+
for (const [key, value] of params.entries()) {
|
|
86
|
+
if (["windowId", "tabGroupId", "groupId"].includes(key)) {
|
|
87
|
+
const parsed = Number(value);
|
|
88
|
+
args[key] = Number.isFinite(parsed) ? parsed : value;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
args[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return args;
|
|
95
|
+
}
|
|
96
|
+
function identityInputFromArgs(args = {}) {
|
|
97
|
+
return {
|
|
98
|
+
codexSessionId: args.codexSessionId,
|
|
99
|
+
projectThreadId: args.projectThreadId,
|
|
100
|
+
projectThreadFamily: args.projectThreadFamily,
|
|
101
|
+
worktreePath: args.worktreePath,
|
|
102
|
+
repoSlug: args.repoSlug,
|
|
103
|
+
branchName: args.branchName,
|
|
104
|
+
sessionKey: args.sessionKey,
|
|
105
|
+
role: args.codexSessionRole,
|
|
106
|
+
strict: true,
|
|
107
|
+
fallbackAgentId: args.agentId,
|
|
108
|
+
fallbackTaskThreadId: args.taskThreadId,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function assertAgentRuntimeProfileArg(profile) {
|
|
112
|
+
if (profile === undefined ||
|
|
113
|
+
profile === null ||
|
|
114
|
+
profile === "" ||
|
|
115
|
+
profile === "agent" ||
|
|
116
|
+
profile === "oe") {
|
|
117
|
+
return profile === "agent" ? "oe" : profile;
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`PROFILE_OWNERSHIP_VIOLATION: profile ${String(profile)} is not an agent-owned Comet runtime profile`);
|
|
120
|
+
}
|
|
121
|
+
async function getWindowIdForTarget(targetId) {
|
|
122
|
+
const CDP = (await import("chrome-remote-interface")).default;
|
|
123
|
+
const client = await CDP({ host: "127.0.0.1", port: 9222, target: targetId });
|
|
124
|
+
try {
|
|
125
|
+
const { windowId } = await client.Browser.getWindowForTarget();
|
|
126
|
+
return windowId;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
try {
|
|
130
|
+
await client.close();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* ignore */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function writeBoundError(res, err) {
|
|
138
|
+
if (err instanceof BoundSessionError) {
|
|
139
|
+
errorJson(res, `${err.code}: ${err.message}. ${err.repairAction}`, err.status);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
async function requireHttpBinding(res, args) {
|
|
145
|
+
try {
|
|
146
|
+
const resolved = await resolveHttpBoundSession(identityInputFromArgs(args), args);
|
|
147
|
+
if (resolved.binding.targetId) {
|
|
148
|
+
await cometClient.connect(resolved.binding.targetId);
|
|
149
|
+
}
|
|
150
|
+
return resolved.binding;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (!writeBoundError(res, err))
|
|
154
|
+
throw err;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function attachRunIdToHttpBinding(runId, args) {
|
|
159
|
+
if (!runId)
|
|
160
|
+
return null;
|
|
161
|
+
const bindingId = typeof args.bindingId === "string" ? args.bindingId : undefined;
|
|
162
|
+
if (bindingId) {
|
|
163
|
+
return windowBindingStore.addRunId(bindingId, runId);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const identity = deriveCodexSessionIdentity(identityInputFromArgs(args));
|
|
167
|
+
const binding = await windowBindingStore.findActiveByIdentity(identity);
|
|
168
|
+
if (!binding)
|
|
169
|
+
return null;
|
|
170
|
+
return windowBindingStore.addRunId(binding.bindingId, runId);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
13
176
|
// Simple mutex to prevent concurrent CDP operations
|
|
14
177
|
let busy = false;
|
|
15
178
|
function json(res, data, status = 200) {
|
|
@@ -40,6 +203,138 @@ async function readBody(req) {
|
|
|
40
203
|
});
|
|
41
204
|
});
|
|
42
205
|
}
|
|
206
|
+
async function fetchJsonWithTimeout(url, init = {}, timeoutMs = 1500) {
|
|
207
|
+
const resp = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
208
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
209
|
+
const data = contentType.includes("application/json") ? await resp.json() : await resp.text();
|
|
210
|
+
return { resp, data };
|
|
211
|
+
}
|
|
212
|
+
function routerCapability(reachable, extra = {}) {
|
|
213
|
+
return { reachable, fresh: reachable, ...extra };
|
|
214
|
+
}
|
|
215
|
+
function routerCapabilityUsable(capability) {
|
|
216
|
+
return !!(capability &&
|
|
217
|
+
capability.reachable === true &&
|
|
218
|
+
capability.fresh !== false &&
|
|
219
|
+
capability.connected !== false &&
|
|
220
|
+
capability.usable !== false);
|
|
221
|
+
}
|
|
222
|
+
function selectRouterRoute(capabilities) {
|
|
223
|
+
if (routerCapabilityUsable(capabilities.equaGateway)) {
|
|
224
|
+
return { route: "gateway", degraded: false };
|
|
225
|
+
}
|
|
226
|
+
if (routerCapabilityUsable(capabilities.equaApi)) {
|
|
227
|
+
return {
|
|
228
|
+
route: "equa_api_enriched",
|
|
229
|
+
degraded: true,
|
|
230
|
+
degradedReason: "gateway unavailable; using Equa API context enrichment",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (routerCapabilityUsable(capabilities.cometBridgeAsk)) {
|
|
234
|
+
return {
|
|
235
|
+
route: "comet_bridge",
|
|
236
|
+
degraded: true,
|
|
237
|
+
degradedReason: "gateway and Equa API unavailable; using Comet bridge fallback",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (routerCapabilityUsable(capabilities.localModel)) {
|
|
241
|
+
return {
|
|
242
|
+
route: "local_model",
|
|
243
|
+
degraded: true,
|
|
244
|
+
degradedReason: "gateway, Equa API, and Comet bridge unavailable; using local model fallback",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { route: "unavailable", degraded: true, degradedReason: "no router backend reachable" };
|
|
248
|
+
}
|
|
249
|
+
function mergeRouterCapability(probedCapability, envelopeCapability) {
|
|
250
|
+
if (!probedCapability)
|
|
251
|
+
return envelopeCapability || {};
|
|
252
|
+
if (!envelopeCapability)
|
|
253
|
+
return probedCapability;
|
|
254
|
+
const probedUsable = routerCapabilityUsable(probedCapability);
|
|
255
|
+
const envelopeUsable = routerCapabilityUsable(envelopeCapability);
|
|
256
|
+
const preferred = probedUsable || !envelopeUsable ? probedCapability : envelopeCapability;
|
|
257
|
+
return {
|
|
258
|
+
...envelopeCapability,
|
|
259
|
+
...probedCapability,
|
|
260
|
+
...preferred,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function mergeRouterCapabilities(probedCapabilities, envelopeCapabilities) {
|
|
264
|
+
const merged = { ...envelopeCapabilities, ...probedCapabilities };
|
|
265
|
+
for (const key of new Set([
|
|
266
|
+
...Object.keys(envelopeCapabilities || {}),
|
|
267
|
+
...Object.keys(probedCapabilities || {}),
|
|
268
|
+
])) {
|
|
269
|
+
merged[key] = mergeRouterCapability(probedCapabilities?.[key], envelopeCapabilities?.[key]);
|
|
270
|
+
}
|
|
271
|
+
return merged;
|
|
272
|
+
}
|
|
273
|
+
async function probeGatewayCapability() {
|
|
274
|
+
for (const url of EQUANAUT_GATEWAY_STATUS_URLS) {
|
|
275
|
+
try {
|
|
276
|
+
const { resp, data } = await fetchJsonWithTimeout(url, { method: "GET", headers: { Accept: "application/json" } }, 1200);
|
|
277
|
+
if (!resp.ok || typeof data !== "object" || data === null)
|
|
278
|
+
continue;
|
|
279
|
+
const payload = data;
|
|
280
|
+
if (payload.connected) {
|
|
281
|
+
return routerCapability(true, {
|
|
282
|
+
connected: true,
|
|
283
|
+
modelId: payload.modelId || payload.model?.id || payload.model || null,
|
|
284
|
+
threadId: payload.threadId || payload.sessionKey || null,
|
|
285
|
+
statusUrl: url,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
/* try next status URL */
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return routerCapability(false, { connected: false });
|
|
294
|
+
}
|
|
295
|
+
async function probeEquaApiCapability() {
|
|
296
|
+
try {
|
|
297
|
+
const { resp } = await fetchJsonWithTimeout(`${EQUA_SERVER_URL}/api/equanaut/threads`, { method: "GET", headers: { Accept: "application/json" } }, 1200);
|
|
298
|
+
// Auth failures still prove the API surface is reachable. The router can
|
|
299
|
+
// report reachability while leaving authenticated data access to configured
|
|
300
|
+
// local credentials.
|
|
301
|
+
if (resp.ok || resp.status === 401 || resp.status === 403) {
|
|
302
|
+
return routerCapability(true, { status: resp.status, baseUrl: EQUA_SERVER_URL });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* unavailable */
|
|
307
|
+
}
|
|
308
|
+
return routerCapability(false, { baseUrl: EQUA_SERVER_URL });
|
|
309
|
+
}
|
|
310
|
+
async function probeLocalModelCapability() {
|
|
311
|
+
try {
|
|
312
|
+
const { resp, data } = await fetchJsonWithTimeout(`${EQUANAUT_OLLAMA_BASE}/api/tags`, { method: "GET", headers: { Accept: "application/json" } }, 1200);
|
|
313
|
+
if (resp.ok) {
|
|
314
|
+
const models = data?.models;
|
|
315
|
+
const firstModel = Array.isArray(models) && models[0] ? models[0].name || models[0].model : null;
|
|
316
|
+
return routerCapability(true, { modelId: firstModel || null });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* unavailable */
|
|
321
|
+
}
|
|
322
|
+
return routerCapability(false);
|
|
323
|
+
}
|
|
324
|
+
async function getEquanautRouterCapabilities() {
|
|
325
|
+
const [gateway, equaApi, localModel] = await Promise.all([
|
|
326
|
+
probeGatewayCapability(),
|
|
327
|
+
probeEquaApiCapability(),
|
|
328
|
+
probeLocalModelCapability(),
|
|
329
|
+
]);
|
|
330
|
+
return {
|
|
331
|
+
cometMcpRouter: routerCapability(true, { port: PORT }),
|
|
332
|
+
equaGateway: gateway,
|
|
333
|
+
equaApi,
|
|
334
|
+
cometBridgeAsk: routerCapability(true, { path: "/api/ask" }),
|
|
335
|
+
localModel,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
43
338
|
async function withMutex(res, fn) {
|
|
44
339
|
if (busy) {
|
|
45
340
|
errorJson(res, "Server busy — another operation is in progress. Try again shortly.", 429);
|
|
@@ -54,37 +349,58 @@ async function withMutex(res, fn) {
|
|
|
54
349
|
}
|
|
55
350
|
}
|
|
56
351
|
// ---- Route handlers (mirrored from index.ts MCP tool handlers) ----
|
|
57
|
-
async function handleConnect(res) {
|
|
352
|
+
async function handleConnect(res, body) {
|
|
353
|
+
const identity = deriveCodexSessionIdentity(identityInputFromArgs(body));
|
|
58
354
|
const result = await withMutex(res, async () => {
|
|
355
|
+
const profileAlias = assertAgentRuntimeProfileArg(body.profile);
|
|
59
356
|
const startResult = await cometClient.startComet(9222);
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
for (let i = 1; i < pageTabs.length; i++) {
|
|
64
|
-
try {
|
|
65
|
-
await cometClient.closeTab(pageTabs[i].id);
|
|
66
|
-
}
|
|
67
|
-
catch { /* ignore */ }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const freshTargets = await cometClient.listTargets();
|
|
71
|
-
const anyPage = freshTargets.find((t) => t.type === "page");
|
|
72
|
-
if (anyPage) {
|
|
73
|
-
await cometClient.connect(anyPage.id);
|
|
74
|
-
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
75
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
76
|
-
return { message: `${startResult}\nConnected to Perplexity (cleaned ${pageTabs.length - 1} old tabs)` };
|
|
77
|
-
}
|
|
78
|
-
const newTab = await cometClient.newTab("https://www.perplexity.ai/");
|
|
357
|
+
const initialUrl = `about:blank#comet-http-session-${Date.now()}`;
|
|
358
|
+
const windowTab = await tabGroupsClient.createTopDisplayFullscreenWindowWithTab(initialUrl);
|
|
359
|
+
const newTab = await cometClient.waitForTargetUrl(initialUrl);
|
|
79
360
|
await new Promise((r) => setTimeout(r, 2000));
|
|
80
361
|
await cometClient.connect(newTab.id);
|
|
81
|
-
|
|
362
|
+
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
363
|
+
await cometClient.positionOnTopDisplay(newTab.id);
|
|
364
|
+
const targetId = newTab.id;
|
|
365
|
+
let tabGroupId = null;
|
|
366
|
+
// Create tab group for the new agent session (FR-007, spec 010)
|
|
367
|
+
// Mirrors MCP tool path: session-registry.ts creates a dedicated window and group.
|
|
368
|
+
try {
|
|
369
|
+
const group = await tabGroupsClient.createGroup({
|
|
370
|
+
tabIds: [windowTab.tabId],
|
|
371
|
+
title: `session-${Date.now()}`,
|
|
372
|
+
color: "blue",
|
|
373
|
+
});
|
|
374
|
+
tabGroupId = group.groupId;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Tab group creation is advisory — do not fail the connection
|
|
378
|
+
}
|
|
379
|
+
const windowId = windowTab.windowId || (await getWindowIdForTarget(targetId));
|
|
380
|
+
const bindingResult = await windowBindingStore.createOrReuse({
|
|
381
|
+
...identity,
|
|
382
|
+
windowId,
|
|
383
|
+
tabGroupId,
|
|
384
|
+
targetId,
|
|
385
|
+
runIds: typeof body.runId === "string" ? [body.runId] : [],
|
|
386
|
+
profileId: "agent",
|
|
387
|
+
profileAlias: profileAlias || "oe",
|
|
388
|
+
profileOwner: "agent",
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
message: `${startResult}\nCreated dedicated top-display full-screen bounds window and tab group`,
|
|
392
|
+
binding: bindingResult.binding,
|
|
393
|
+
bindingAction: bindingResult.action,
|
|
394
|
+
};
|
|
82
395
|
});
|
|
83
396
|
if (result)
|
|
84
397
|
json(res, result);
|
|
85
398
|
}
|
|
86
399
|
async function handleAsk(res, body) {
|
|
87
400
|
const result = await withMutex(res, async () => {
|
|
401
|
+
const binding = await requireHttpBinding(res, body);
|
|
402
|
+
if (!binding)
|
|
403
|
+
return null;
|
|
88
404
|
let prompt = body.prompt;
|
|
89
405
|
const timeout = body.timeout || 15000;
|
|
90
406
|
const newChat = body.newChat || false;
|
|
@@ -97,20 +413,10 @@ async function handleAsk(res, body) {
|
|
|
97
413
|
.replace(/\n+/g, " ")
|
|
98
414
|
.replace(/\s+/g, " ")
|
|
99
415
|
.trim();
|
|
100
|
-
// newChat:
|
|
416
|
+
// newChat: navigate to fresh Perplexity page (without closing other agents' tabs)
|
|
101
417
|
if (newChat) {
|
|
102
418
|
const targets = await cometClient.listTargets();
|
|
103
|
-
const
|
|
104
|
-
if (pageTabs.length > 1) {
|
|
105
|
-
for (let i = 1; i < pageTabs.length; i++) {
|
|
106
|
-
try {
|
|
107
|
-
await cometClient.closeTab(pageTabs[i].id);
|
|
108
|
-
}
|
|
109
|
-
catch { /* ignore */ }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const freshTargets = await cometClient.listTargets();
|
|
113
|
-
const mainTab = freshTargets.find((t) => t.type === "page");
|
|
419
|
+
const mainTab = targets.find((t) => t.type === "page");
|
|
114
420
|
if (mainTab)
|
|
115
421
|
await cometClient.connect(mainTab.id);
|
|
116
422
|
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
@@ -170,7 +476,10 @@ async function handleAsk(res, body) {
|
|
|
170
476
|
stepsCollected.push(step);
|
|
171
477
|
}
|
|
172
478
|
if (status.status === "completed" && sawNewResponse) {
|
|
173
|
-
return {
|
|
479
|
+
return {
|
|
480
|
+
status: "completed",
|
|
481
|
+
response: status.response || "Task completed (no response text extracted)",
|
|
482
|
+
};
|
|
174
483
|
}
|
|
175
484
|
}
|
|
176
485
|
// Timeout — return in-progress status
|
|
@@ -192,8 +501,79 @@ async function handleAsk(res, body) {
|
|
|
192
501
|
}
|
|
193
502
|
}
|
|
194
503
|
}
|
|
195
|
-
async function
|
|
504
|
+
async function handleEquanautRouterStatus(res) {
|
|
505
|
+
const capabilities = await getEquanautRouterCapabilities();
|
|
506
|
+
json(res, {
|
|
507
|
+
capabilities,
|
|
508
|
+
decision: selectRouterRoute(capabilities),
|
|
509
|
+
timestamp: new Date().toISOString(),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
async function handleEquanautRouterAsk(res, body) {
|
|
513
|
+
const prompt = body.prompt;
|
|
514
|
+
const envelope = body.envelope || {};
|
|
515
|
+
if (!prompt || !prompt.trim()) {
|
|
516
|
+
errorJson(res, "prompt is required", 400);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const probedCapabilities = await getEquanautRouterCapabilities();
|
|
520
|
+
const envelopeCapabilities = envelope.capabilities && typeof envelope.capabilities === "object" ? envelope.capabilities : {};
|
|
521
|
+
const liveCapabilities = mergeRouterCapabilities(probedCapabilities, envelopeCapabilities);
|
|
522
|
+
const decision = selectRouterRoute(liveCapabilities);
|
|
523
|
+
if (decision.route === "gateway" && EQUANAUT_GATEWAY_ASK_URL) {
|
|
524
|
+
try {
|
|
525
|
+
const { resp, data } = await fetchJsonWithTimeout(EQUANAUT_GATEWAY_ASK_URL, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
528
|
+
body: JSON.stringify({ prompt, envelope, timeout: body.timeout || 60000 }),
|
|
529
|
+
}, Number(body.timeout || 60000));
|
|
530
|
+
if (!resp.ok) {
|
|
531
|
+
json(res, {
|
|
532
|
+
...decision,
|
|
533
|
+
error: `gateway ask ${resp.status}`,
|
|
534
|
+
details: typeof data === "string" ? data.slice(0, 200) : data,
|
|
535
|
+
}, 502);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const payload = typeof data === "object" && data !== null ? data : {};
|
|
539
|
+
json(res, {
|
|
540
|
+
...decision,
|
|
541
|
+
response: payload.response || payload.answer || payload.text || "",
|
|
542
|
+
telemetry: {
|
|
543
|
+
route: decision.route,
|
|
544
|
+
degraded: decision.degraded,
|
|
545
|
+
activeWindowId: envelope.telemetry?.activeWindowId || null,
|
|
546
|
+
completedAt: new Date().toISOString(),
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
json(res, {
|
|
553
|
+
...decision,
|
|
554
|
+
error: `gateway ask unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
555
|
+
}, 502);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
json(res, {
|
|
560
|
+
...decision,
|
|
561
|
+
error: decision.route === "gateway"
|
|
562
|
+
? "gateway route selected but EQUANAUT_GATEWAY_ASK_URL is not configured"
|
|
563
|
+
: "router has no stronger configured answer route; caller should use existing fallback",
|
|
564
|
+
telemetry: {
|
|
565
|
+
route: decision.route,
|
|
566
|
+
degraded: decision.degraded,
|
|
567
|
+
activeWindowId: envelope.telemetry?.activeWindowId || null,
|
|
568
|
+
completedAt: new Date().toISOString(),
|
|
569
|
+
},
|
|
570
|
+
}, 503);
|
|
571
|
+
}
|
|
572
|
+
async function handlePoll(res, args) {
|
|
196
573
|
const result = await withMutex(res, async () => {
|
|
574
|
+
const binding = await requireHttpBinding(res, args);
|
|
575
|
+
if (!binding)
|
|
576
|
+
return null;
|
|
197
577
|
const status = await cometAI.getAgentStatus();
|
|
198
578
|
if (status.status === "completed" && status.response) {
|
|
199
579
|
return { status: "completed", response: status.response };
|
|
@@ -216,8 +596,11 @@ async function handleStop(res) {
|
|
|
216
596
|
if (result)
|
|
217
597
|
json(res, result);
|
|
218
598
|
}
|
|
219
|
-
async function handleScreenshot(res) {
|
|
599
|
+
async function handleScreenshot(res, args) {
|
|
220
600
|
const result = await withMutex(res, async () => {
|
|
601
|
+
const binding = await requireHttpBinding(res, args);
|
|
602
|
+
if (!binding)
|
|
603
|
+
return null;
|
|
221
604
|
const screenshot = await cometClient.screenshot("png");
|
|
222
605
|
return { data: screenshot.data, mimeType: "image/png" };
|
|
223
606
|
});
|
|
@@ -249,7 +632,12 @@ async function handleMode(res, body) {
|
|
|
249
632
|
`);
|
|
250
633
|
return { currentMode: modeResult.result.value };
|
|
251
634
|
}
|
|
252
|
-
const modeMap = {
|
|
635
|
+
const modeMap = {
|
|
636
|
+
search: "Search",
|
|
637
|
+
research: "Research",
|
|
638
|
+
labs: "Labs",
|
|
639
|
+
learn: "Learn",
|
|
640
|
+
};
|
|
253
641
|
const ariaLabel = modeMap[mode];
|
|
254
642
|
if (!ariaLabel) {
|
|
255
643
|
return { error: `Invalid mode: ${mode}. Use: search, research, labs, learn` };
|
|
@@ -307,6 +695,1486 @@ async function handleMode(res, body) {
|
|
|
307
695
|
}
|
|
308
696
|
}
|
|
309
697
|
}
|
|
698
|
+
// ---- Navigation & Interaction handlers (require session) ----
|
|
699
|
+
async function handleNavigate(res, body) {
|
|
700
|
+
const result = await withMutex(res, async () => {
|
|
701
|
+
const binding = await requireHttpBinding(res, body);
|
|
702
|
+
if (!binding)
|
|
703
|
+
return null;
|
|
704
|
+
const url = body.url;
|
|
705
|
+
if (!url || url.trim().length === 0) {
|
|
706
|
+
return { error: "url is required" };
|
|
707
|
+
}
|
|
708
|
+
const waitForIdle = body.waitForIdle !== false; // default true
|
|
709
|
+
const navResult = await cometClient.navigate(url, true, waitForIdle);
|
|
710
|
+
const finalUrlResult = await cometClient.safeEvaluate("window.location.href");
|
|
711
|
+
const finalUrl = finalUrlResult.result.value || url;
|
|
712
|
+
const titleResult = await cometClient.safeEvaluate("document.title");
|
|
713
|
+
const title = titleResult.result.value || "";
|
|
714
|
+
return {
|
|
715
|
+
url: finalUrl,
|
|
716
|
+
title,
|
|
717
|
+
networkIdle: navResult.networkIdle !== undefined ? navResult.networkIdle : null,
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
if (result !== null) {
|
|
721
|
+
if (result && "error" in result)
|
|
722
|
+
errorJson(res, result.error, 400);
|
|
723
|
+
else if (result)
|
|
724
|
+
json(res, result);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function handleInteract(res, body) {
|
|
728
|
+
const result = await withMutex(res, async () => {
|
|
729
|
+
const binding = await requireHttpBinding(res, body);
|
|
730
|
+
if (!binding)
|
|
731
|
+
return null;
|
|
732
|
+
const actions = body.actions;
|
|
733
|
+
if (!actions || actions.length === 0) {
|
|
734
|
+
return { error: "actions array is required and cannot be empty" };
|
|
735
|
+
}
|
|
736
|
+
const results = [];
|
|
737
|
+
for (const act of actions) {
|
|
738
|
+
try {
|
|
739
|
+
switch (act.action) {
|
|
740
|
+
case "click": {
|
|
741
|
+
if (!act.selector)
|
|
742
|
+
throw new Error("click requires a selector");
|
|
743
|
+
const clicked = await cometClient.safeEvaluate(`
|
|
744
|
+
(() => {
|
|
745
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
746
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found: ${act.selector.replace(/'/g, "\\'")}' });
|
|
747
|
+
el.scrollIntoView({ block: 'center' });
|
|
748
|
+
el.click();
|
|
749
|
+
return JSON.stringify({ ok: true, tag: el.tagName, text: (el.textContent || '').trim().substring(0, 80) });
|
|
750
|
+
})()
|
|
751
|
+
`);
|
|
752
|
+
const clickRes = JSON.parse(clicked.result.value);
|
|
753
|
+
if (!clickRes.ok)
|
|
754
|
+
throw new Error(clickRes.error);
|
|
755
|
+
results.push({
|
|
756
|
+
action: "click",
|
|
757
|
+
success: true,
|
|
758
|
+
result: `Clicked <${clickRes.tag}> "${clickRes.text}"`,
|
|
759
|
+
});
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case "type": {
|
|
763
|
+
if (!act.selector)
|
|
764
|
+
throw new Error("type requires a selector");
|
|
765
|
+
if (!act.value)
|
|
766
|
+
throw new Error("type requires a value");
|
|
767
|
+
await cometClient.safeEvaluate(`
|
|
768
|
+
(() => {
|
|
769
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
770
|
+
if (!el) throw new Error('Element not found');
|
|
771
|
+
el.focus();
|
|
772
|
+
})()
|
|
773
|
+
`);
|
|
774
|
+
for (const char of act.value) {
|
|
775
|
+
await cometClient.pressKey(char);
|
|
776
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
777
|
+
}
|
|
778
|
+
results.push({
|
|
779
|
+
action: "type",
|
|
780
|
+
success: true,
|
|
781
|
+
result: `Typed ${act.value.length} chars`,
|
|
782
|
+
});
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "fill": {
|
|
786
|
+
if (!act.selector)
|
|
787
|
+
throw new Error("fill requires a selector");
|
|
788
|
+
const fillRes = await cometClient.safeEvaluate(`
|
|
789
|
+
(() => {
|
|
790
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
791
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
792
|
+
el.focus();
|
|
793
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
794
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
795
|
+
el.tagName === 'INPUT' ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype, 'value'
|
|
796
|
+
)?.set;
|
|
797
|
+
if (nativeSetter) nativeSetter.call(el, ${JSON.stringify(act.value || "")});
|
|
798
|
+
else el.value = ${JSON.stringify(act.value || "")};
|
|
799
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
800
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
801
|
+
} else if (el.contentEditable === 'true') {
|
|
802
|
+
el.focus();
|
|
803
|
+
document.execCommand('selectAll', false, null);
|
|
804
|
+
document.execCommand('insertText', false, ${JSON.stringify(act.value || "")});
|
|
805
|
+
}
|
|
806
|
+
return JSON.stringify({ ok: true });
|
|
807
|
+
})()
|
|
808
|
+
`);
|
|
809
|
+
const fRes = JSON.parse(fillRes.result.value);
|
|
810
|
+
if (!fRes.ok)
|
|
811
|
+
throw new Error(fRes.error);
|
|
812
|
+
results.push({
|
|
813
|
+
action: "fill",
|
|
814
|
+
success: true,
|
|
815
|
+
result: `Filled with "${(act.value || "").substring(0, 40)}"`,
|
|
816
|
+
});
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case "press": {
|
|
820
|
+
const key = act.key || act.value || "Enter";
|
|
821
|
+
if (act.selector) {
|
|
822
|
+
await cometClient.safeEvaluate(`
|
|
823
|
+
(() => {
|
|
824
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
825
|
+
if (el) el.focus();
|
|
826
|
+
})()
|
|
827
|
+
`);
|
|
828
|
+
}
|
|
829
|
+
await cometClient.pressKey(key);
|
|
830
|
+
results.push({ action: "press", success: true, result: `Pressed ${key}` });
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case "check":
|
|
834
|
+
case "uncheck": {
|
|
835
|
+
if (!act.selector)
|
|
836
|
+
throw new Error(`${act.action} requires a selector`);
|
|
837
|
+
const shouldCheck = act.action === "check";
|
|
838
|
+
const checkRes = await cometClient.safeEvaluate(`
|
|
839
|
+
(() => {
|
|
840
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
841
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
842
|
+
const isCheckbox = el.type === 'checkbox' || el.getAttribute('role') === 'checkbox';
|
|
843
|
+
if (isCheckbox) {
|
|
844
|
+
if (el.checked !== ${shouldCheck}) el.click();
|
|
845
|
+
return JSON.stringify({ ok: true, checked: ${shouldCheck} });
|
|
846
|
+
}
|
|
847
|
+
el.scrollIntoView({ block: 'center' });
|
|
848
|
+
el.click();
|
|
849
|
+
return JSON.stringify({ ok: true, clicked: true });
|
|
850
|
+
})()
|
|
851
|
+
`);
|
|
852
|
+
const cRes = JSON.parse(checkRes.result.value);
|
|
853
|
+
if (!cRes.ok)
|
|
854
|
+
throw new Error(cRes.error);
|
|
855
|
+
results.push({
|
|
856
|
+
action: act.action,
|
|
857
|
+
success: true,
|
|
858
|
+
result: `${act.action}ed: ${act.selector}`,
|
|
859
|
+
});
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case "select": {
|
|
863
|
+
if (!act.selector)
|
|
864
|
+
throw new Error("select requires a selector");
|
|
865
|
+
if (!act.value)
|
|
866
|
+
throw new Error("select requires a value");
|
|
867
|
+
const selRes = await cometClient.safeEvaluate(`
|
|
868
|
+
(() => {
|
|
869
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
870
|
+
if (!el || el.tagName !== 'SELECT') return JSON.stringify({ ok: false, error: 'SELECT element not found' });
|
|
871
|
+
let found = false;
|
|
872
|
+
for (const opt of el.options) {
|
|
873
|
+
if (opt.value === ${JSON.stringify(act.value)} || opt.text === ${JSON.stringify(act.value)}) {
|
|
874
|
+
el.value = opt.value;
|
|
875
|
+
found = true;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (!found) return JSON.stringify({ ok: false, error: 'Option not found' });
|
|
880
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
881
|
+
return JSON.stringify({ ok: true, selected: el.value });
|
|
882
|
+
})()
|
|
883
|
+
`);
|
|
884
|
+
const sRes = JSON.parse(selRes.result.value);
|
|
885
|
+
if (!sRes.ok)
|
|
886
|
+
throw new Error(sRes.error);
|
|
887
|
+
results.push({ action: "select", success: true, result: `Selected: ${sRes.selected}` });
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
case "scroll": {
|
|
891
|
+
const dir = act.direction || "down";
|
|
892
|
+
const amount = act.amount || 500;
|
|
893
|
+
const dx = dir === "right" ? amount : dir === "left" ? -amount : 0;
|
|
894
|
+
const dy = dir === "down" ? amount : dir === "up" ? -amount : 0;
|
|
895
|
+
if (act.selector) {
|
|
896
|
+
await cometClient.safeEvaluate(`
|
|
897
|
+
(() => {
|
|
898
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
899
|
+
if (el) el.scrollBy(${dx}, ${dy});
|
|
900
|
+
})()
|
|
901
|
+
`);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
await cometClient.safeEvaluate(`window.scrollBy(${dx}, ${dy})`);
|
|
905
|
+
}
|
|
906
|
+
results.push({
|
|
907
|
+
action: "scroll",
|
|
908
|
+
success: true,
|
|
909
|
+
result: `Scrolled ${dir} ${amount}px`,
|
|
910
|
+
});
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "wait": {
|
|
914
|
+
if (act.selector) {
|
|
915
|
+
const waitTimeout = act.ms || 10000;
|
|
916
|
+
const start = Date.now();
|
|
917
|
+
let found = false;
|
|
918
|
+
while (Date.now() - start < waitTimeout) {
|
|
919
|
+
const exists = await cometClient.safeEvaluate(`document.querySelector(${safeSelector(act.selector)}) !== null`);
|
|
920
|
+
if (exists.result.value === true) {
|
|
921
|
+
found = true;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
925
|
+
}
|
|
926
|
+
if (!found)
|
|
927
|
+
throw new Error(`Timeout waiting for ${act.selector}`);
|
|
928
|
+
results.push({ action: "wait", success: true, result: `Found: ${act.selector}` });
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
const ms = act.ms || 1000;
|
|
932
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
933
|
+
results.push({ action: "wait", success: true, result: `Waited ${ms}ms` });
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
case "extract": {
|
|
938
|
+
if (!act.selector)
|
|
939
|
+
throw new Error("extract requires a selector");
|
|
940
|
+
const extRes = await cometClient.safeEvaluate(`
|
|
941
|
+
(() => {
|
|
942
|
+
const el = document.querySelector(${safeSelector(act.selector)});
|
|
943
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
944
|
+
return JSON.stringify({
|
|
945
|
+
ok: true,
|
|
946
|
+
text: el.innerText?.trim() || '',
|
|
947
|
+
value: el.value || '',
|
|
948
|
+
tag: el.tagName,
|
|
949
|
+
});
|
|
950
|
+
})()
|
|
951
|
+
`);
|
|
952
|
+
const eRes = JSON.parse(extRes.result.value);
|
|
953
|
+
if (!eRes.ok)
|
|
954
|
+
throw new Error(eRes.error);
|
|
955
|
+
const extracted = eRes.text || eRes.value || `<${eRes.tag}>`;
|
|
956
|
+
results.push({
|
|
957
|
+
action: "extract",
|
|
958
|
+
success: true,
|
|
959
|
+
result: extracted.substring(0, 2000),
|
|
960
|
+
});
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
case "evaluate": {
|
|
964
|
+
if (!act.script)
|
|
965
|
+
throw new Error("evaluate requires a script");
|
|
966
|
+
const evalRes = await cometClient.safeEvaluate(`
|
|
967
|
+
(() => {
|
|
968
|
+
try {
|
|
969
|
+
const result = (function() { ${act.script} })();
|
|
970
|
+
return JSON.stringify({ ok: true, result: String(result ?? 'undefined').substring(0, 4000) });
|
|
971
|
+
} catch (e) {
|
|
972
|
+
return JSON.stringify({ ok: false, error: e.message });
|
|
973
|
+
}
|
|
974
|
+
})()
|
|
975
|
+
`);
|
|
976
|
+
const evRes = JSON.parse(evalRes.result.value);
|
|
977
|
+
if (!evRes.ok)
|
|
978
|
+
throw new Error(evRes.error);
|
|
979
|
+
results.push({ action: "evaluate", success: true, result: evRes.result });
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
default:
|
|
983
|
+
throw new Error(`Unknown action: ${act.action}`);
|
|
984
|
+
}
|
|
985
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
989
|
+
results.push({ action: act.action, success: false, error: errorMsg });
|
|
990
|
+
if (!act.optional) {
|
|
991
|
+
results.push({
|
|
992
|
+
action: "ABORTED",
|
|
993
|
+
success: false,
|
|
994
|
+
error: `Stopped after ${act.action} failed`,
|
|
995
|
+
});
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const allSucceeded = results.every((r) => r.success);
|
|
1001
|
+
return { results, allSucceeded };
|
|
1002
|
+
});
|
|
1003
|
+
if (result !== null) {
|
|
1004
|
+
if (result && "error" in result)
|
|
1005
|
+
errorJson(res, result.error, 400);
|
|
1006
|
+
else if (result)
|
|
1007
|
+
json(res, result);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function handleReadPage(res, params) {
|
|
1011
|
+
const result = await withMutex(res, async () => {
|
|
1012
|
+
const args = paramsToRecord(params);
|
|
1013
|
+
const binding = await requireHttpBinding(res, args);
|
|
1014
|
+
if (!binding)
|
|
1015
|
+
return null;
|
|
1016
|
+
const mode = params.get("mode") || "text";
|
|
1017
|
+
const maxDepth = parseInt(params.get("maxDepth") || "5", 10);
|
|
1018
|
+
const maxLength = parseInt(params.get("maxLength") || "12000", 10);
|
|
1019
|
+
const parts = [];
|
|
1020
|
+
if (mode === "tree" || mode === "both") {
|
|
1021
|
+
const tree = await cometClient.getAccessibilityTree(maxDepth, maxLength);
|
|
1022
|
+
parts.push("## Accessibility Tree\n" + tree);
|
|
1023
|
+
}
|
|
1024
|
+
if (mode === "text" || mode === "both") {
|
|
1025
|
+
const text = await cometClient.getPageText(maxLength);
|
|
1026
|
+
parts.push("## Page Text\n" + text);
|
|
1027
|
+
}
|
|
1028
|
+
if (parts.length === 0) {
|
|
1029
|
+
return { error: `Invalid mode: ${mode}. Use: text, tree, or both` };
|
|
1030
|
+
}
|
|
1031
|
+
return { mode, content: parts.join("\n\n") };
|
|
1032
|
+
});
|
|
1033
|
+
if (result !== null) {
|
|
1034
|
+
if (result && "error" in result)
|
|
1035
|
+
errorJson(res, result.error, 400);
|
|
1036
|
+
else if (result)
|
|
1037
|
+
json(res, result);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function handleShortcut(res, body) {
|
|
1041
|
+
const result = await withMutex(res, async () => {
|
|
1042
|
+
if (requireSession(res))
|
|
1043
|
+
return null;
|
|
1044
|
+
let shortcut = body.shortcut;
|
|
1045
|
+
if (!shortcut || shortcut.trim().length === 0) {
|
|
1046
|
+
return { error: "shortcut is required" };
|
|
1047
|
+
}
|
|
1048
|
+
shortcut = shortcut.replace(/^\//, "");
|
|
1049
|
+
const context = body.context;
|
|
1050
|
+
const timeout = body.timeout || 30000;
|
|
1051
|
+
// Ensure on Perplexity
|
|
1052
|
+
const tabs = await cometClient.listTabsCategorized();
|
|
1053
|
+
if (tabs.main)
|
|
1054
|
+
await cometClient.connect(tabs.main.id);
|
|
1055
|
+
const urlResult = await cometClient.evaluate("window.location.href");
|
|
1056
|
+
const currentUrl = urlResult.result.value;
|
|
1057
|
+
if (!currentUrl?.includes("perplexity.ai")) {
|
|
1058
|
+
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
1059
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1060
|
+
}
|
|
1061
|
+
// Capture old state
|
|
1062
|
+
const oldStateResult = await cometClient.evaluate(`
|
|
1063
|
+
(() => {
|
|
1064
|
+
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
1065
|
+
const lastProse = proseEls[proseEls.length - 1];
|
|
1066
|
+
return { count: proseEls.length, lastText: lastProse ? lastProse.innerText.substring(0, 100) : '' };
|
|
1067
|
+
})()
|
|
1068
|
+
`);
|
|
1069
|
+
const oldState = oldStateResult.result.value;
|
|
1070
|
+
await cometAI.sendShortcut(shortcut, context);
|
|
1071
|
+
const startTime = Date.now();
|
|
1072
|
+
let sawNewResponse = false;
|
|
1073
|
+
while (Date.now() - startTime < timeout) {
|
|
1074
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1075
|
+
const currentStateResult = await cometClient.evaluate(`
|
|
1076
|
+
(() => {
|
|
1077
|
+
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
1078
|
+
const lastProse = proseEls[proseEls.length - 1];
|
|
1079
|
+
return { count: proseEls.length, lastText: lastProse ? lastProse.innerText.substring(0, 100) : '' };
|
|
1080
|
+
})()
|
|
1081
|
+
`);
|
|
1082
|
+
const currentState = currentStateResult.result.value;
|
|
1083
|
+
if (!sawNewResponse) {
|
|
1084
|
+
if (currentState.count > oldState.count ||
|
|
1085
|
+
(currentState.lastText && currentState.lastText !== oldState.lastText)) {
|
|
1086
|
+
sawNewResponse = true;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const status = await cometAI.getAgentStatus();
|
|
1090
|
+
if (status.status === "completed" && sawNewResponse) {
|
|
1091
|
+
return {
|
|
1092
|
+
status: "completed",
|
|
1093
|
+
response: status.response || "Shortcut completed (no response text extracted)",
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const finalStatus = await cometAI.getAgentStatus();
|
|
1098
|
+
return {
|
|
1099
|
+
status: "in_progress",
|
|
1100
|
+
currentStep: finalStatus.currentStep || null,
|
|
1101
|
+
message: `Shortcut /${shortcut} in progress (timed out after ${timeout}ms). Use GET /api/poll to check progress.`,
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
if (result !== null) {
|
|
1105
|
+
if (result && "error" in result)
|
|
1106
|
+
errorJson(res, result.error, 400);
|
|
1107
|
+
else if (result)
|
|
1108
|
+
json(res, result);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function handleWaitForIdle(res, body) {
|
|
1112
|
+
const result = await withMutex(res, async () => {
|
|
1113
|
+
if (requireSession(res))
|
|
1114
|
+
return null;
|
|
1115
|
+
const idleTime = body.idleTime || 1500;
|
|
1116
|
+
const timeout = body.timeout || 15000;
|
|
1117
|
+
const idleResult = await cometClient.waitForNetworkIdle({ idleTime, timeout });
|
|
1118
|
+
return {
|
|
1119
|
+
idle: idleResult.idle,
|
|
1120
|
+
waitedMs: idleResult.waitedMs,
|
|
1121
|
+
totalRequests: idleResult.totalRequests,
|
|
1122
|
+
totalCompleted: idleResult.totalCompleted,
|
|
1123
|
+
totalFailed: idleResult.totalFailed,
|
|
1124
|
+
pendingRequests: idleResult.pendingRequests,
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
if (result !== null) {
|
|
1128
|
+
if (result)
|
|
1129
|
+
json(res, result);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
// ---- Safe Observation handlers (no session, no mutex) ----
|
|
1133
|
+
async function handleObserve(res, params) {
|
|
1134
|
+
const action = params.get("action") || "health";
|
|
1135
|
+
const filters = {
|
|
1136
|
+
group: params.get("group") || undefined,
|
|
1137
|
+
agentId: params.get("agentId") || undefined,
|
|
1138
|
+
urlPattern: params.get("urlPattern") || undefined,
|
|
1139
|
+
thumbnails: params.get("thumbnails") === "true",
|
|
1140
|
+
codexIdentity: (() => {
|
|
1141
|
+
try {
|
|
1142
|
+
return deriveCodexSessionIdentity(identityInputFromArgs(paramsToRecord(params)));
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
})(),
|
|
1148
|
+
};
|
|
1149
|
+
try {
|
|
1150
|
+
switch (action) {
|
|
1151
|
+
case "health": {
|
|
1152
|
+
const health = await getHealth();
|
|
1153
|
+
json(res, { action: "health", formatted: formatHealth(health), data: health });
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
case "snapshot": {
|
|
1157
|
+
const snapshot = await getSnapshot(filters);
|
|
1158
|
+
json(res, { action: "snapshot", formatted: formatSnapshot(snapshot), data: snapshot });
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
case "status": {
|
|
1162
|
+
const statusText = await getStatus(filters);
|
|
1163
|
+
json(res, { action: "status", formatted: statusText });
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
case "detail": {
|
|
1167
|
+
if (!filters.group) {
|
|
1168
|
+
errorJson(res, "The 'detail' action requires a 'group' query parameter (tab group name).", 400);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const detailText = await getDetail(filters.group, filters);
|
|
1172
|
+
json(res, { action: "detail", group: filters.group, formatted: detailText });
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
default:
|
|
1176
|
+
errorJson(res, `Unknown observe action: ${action}. Use: health, snapshot, status, or detail.`, 400);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
catch (err) {
|
|
1180
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1181
|
+
errorJson(res, message);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function handlePeek(res, params) {
|
|
1185
|
+
const args = paramsToRecord(params);
|
|
1186
|
+
const targetId = params.get("targetId");
|
|
1187
|
+
const action = params.get("action") || "info";
|
|
1188
|
+
const binding = await requireHttpBinding(res, args);
|
|
1189
|
+
if (!binding)
|
|
1190
|
+
return;
|
|
1191
|
+
if (!targetId) {
|
|
1192
|
+
errorJson(res, "targetId query parameter is required. Use GET /api/observe?action=snapshot to find target IDs.", 400);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
// Resolve CDP target list (no session, no mutex needed for info)
|
|
1196
|
+
let peekTargets;
|
|
1197
|
+
try {
|
|
1198
|
+
const resp = await fetch(`http://127.0.0.1:9222/json/list`);
|
|
1199
|
+
if (!resp.ok) {
|
|
1200
|
+
errorJson(res, "Cannot connect to CDP on port 9222. Is Comet browser running?", 503);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
peekTargets = (await resp.json());
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
errorJson(res, "Cannot reach CDP on port 9222. Is Comet browser running?", 503);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const peekTarget = peekTargets.find((t) => t.id === targetId);
|
|
1210
|
+
if (!peekTarget) {
|
|
1211
|
+
errorJson(res, `Target ID '${targetId}' not found. Use GET /api/observe?action=snapshot to list current targets.`, 404);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (action === "info") {
|
|
1215
|
+
json(res, {
|
|
1216
|
+
targetId: peekTarget.id,
|
|
1217
|
+
url: peekTarget.url,
|
|
1218
|
+
title: peekTarget.title,
|
|
1219
|
+
type: peekTarget.type,
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
// screenshot and read require temporary CDP attachment — use mutex
|
|
1224
|
+
const mutexResult = await withMutex(res, async () => {
|
|
1225
|
+
if (!peekTarget.webSocketDebuggerUrl) {
|
|
1226
|
+
return { error: "Target has no WebSocket debugger URL" };
|
|
1227
|
+
}
|
|
1228
|
+
const peekCDP = await (await import("chrome-remote-interface")).default({
|
|
1229
|
+
target: peekTarget.webSocketDebuggerUrl,
|
|
1230
|
+
});
|
|
1231
|
+
try {
|
|
1232
|
+
if (action === "screenshot") {
|
|
1233
|
+
await peekCDP.Page.enable();
|
|
1234
|
+
const ssResult = (await Promise.race([
|
|
1235
|
+
peekCDP.Page.captureScreenshot({ format: "png" }),
|
|
1236
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Screenshot timeout (3s)")), 3000)),
|
|
1237
|
+
]));
|
|
1238
|
+
const outputDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
1239
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1240
|
+
const outputPath = join(outputDir, `peek-${Date.now()}.png`);
|
|
1241
|
+
writeFileSync(outputPath, Buffer.from(ssResult.data, "base64"));
|
|
1242
|
+
return { filePath: outputPath, url: peekTarget.url, title: peekTarget.title };
|
|
1243
|
+
}
|
|
1244
|
+
if (action === "read") {
|
|
1245
|
+
await peekCDP.Runtime.enable();
|
|
1246
|
+
const readResult = await peekCDP.Runtime.evaluate({
|
|
1247
|
+
expression: `(() => {
|
|
1248
|
+
const title = document.title;
|
|
1249
|
+
const url = window.location.href;
|
|
1250
|
+
const text = document.body?.innerText?.substring(0, 10000) || '';
|
|
1251
|
+
const links = Array.from(document.querySelectorAll('a[href]')).slice(0, 20).map(a => ({ text: a.innerText.trim().substring(0, 50), href: a.href }));
|
|
1252
|
+
return JSON.stringify({ title, url, text, links });
|
|
1253
|
+
})()`,
|
|
1254
|
+
returnByValue: true,
|
|
1255
|
+
});
|
|
1256
|
+
const pageData = JSON.parse(readResult.result.value);
|
|
1257
|
+
return {
|
|
1258
|
+
title: pageData.title,
|
|
1259
|
+
url: pageData.url,
|
|
1260
|
+
text: pageData.text.substring(0, 5000),
|
|
1261
|
+
links: pageData.links,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return { error: `Unknown peek action: ${action}. Use: info, screenshot, or read.` };
|
|
1265
|
+
}
|
|
1266
|
+
finally {
|
|
1267
|
+
try {
|
|
1268
|
+
await peekCDP.close();
|
|
1269
|
+
}
|
|
1270
|
+
catch {
|
|
1271
|
+
/* ignore */
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
if (mutexResult !== null) {
|
|
1276
|
+
if (mutexResult && "error" in mutexResult)
|
|
1277
|
+
errorJson(res, mutexResult.error, 400);
|
|
1278
|
+
else if (mutexResult)
|
|
1279
|
+
json(res, mutexResult);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// ---- Lifecycle handlers (no session, no mutex, forward to CC-SO) ----
|
|
1283
|
+
async function handleLifecycleStart(res, body) {
|
|
1284
|
+
const runId = body.runId;
|
|
1285
|
+
const taskThreadId = body.taskThreadId;
|
|
1286
|
+
if (!runId) {
|
|
1287
|
+
errorJson(res, "runId is required", 400);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
if (!taskThreadId) {
|
|
1291
|
+
errorJson(res, "taskThreadId is required", 400);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
let result = "skipped";
|
|
1295
|
+
let warning;
|
|
1296
|
+
try {
|
|
1297
|
+
result = await callLifecycleEndpoint({
|
|
1298
|
+
action: "start",
|
|
1299
|
+
runId,
|
|
1300
|
+
taskThreadId,
|
|
1301
|
+
agentId: body.agentId,
|
|
1302
|
+
route: body.route || "http",
|
|
1303
|
+
deferred: body.deferred,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
warning = "CC-SO unavailable — lifecycle event not persisted";
|
|
1308
|
+
}
|
|
1309
|
+
const binding = await attachRunIdToHttpBinding(runId, body);
|
|
1310
|
+
appendJsonl(OUTBOX_PATH, {
|
|
1311
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1312
|
+
from: "comet-http",
|
|
1313
|
+
to: "orchestration",
|
|
1314
|
+
type: "update",
|
|
1315
|
+
task: runId,
|
|
1316
|
+
thread: taskThreadId,
|
|
1317
|
+
msg: `Lifecycle started: run=${runId}`,
|
|
1318
|
+
lifecycle: { action: "start", runId, status: "started", bindingId: binding?.bindingId ?? null },
|
|
1319
|
+
});
|
|
1320
|
+
json(res, {
|
|
1321
|
+
success: true,
|
|
1322
|
+
runId,
|
|
1323
|
+
result,
|
|
1324
|
+
bindingId: binding?.bindingId ?? null,
|
|
1325
|
+
...(warning ? { warning } : {}),
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
async function handleLifecycleComplete(res, body) {
|
|
1329
|
+
const runId = body.runId;
|
|
1330
|
+
if (!runId) {
|
|
1331
|
+
errorJson(res, "runId is required", 400);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
let result = "skipped";
|
|
1335
|
+
let warning;
|
|
1336
|
+
try {
|
|
1337
|
+
result = await callLifecycleEndpoint({ action: "complete", runId });
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
warning = "CC-SO unavailable — lifecycle event not persisted";
|
|
1341
|
+
}
|
|
1342
|
+
const binding = await windowBindingStore.transitionByRunId(runId, "completed");
|
|
1343
|
+
appendJsonl(OUTBOX_PATH, {
|
|
1344
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1345
|
+
from: "comet-http",
|
|
1346
|
+
to: "orchestration",
|
|
1347
|
+
type: "complete",
|
|
1348
|
+
task: runId,
|
|
1349
|
+
msg: `Lifecycle completed: run=${runId}`,
|
|
1350
|
+
lifecycle: {
|
|
1351
|
+
action: "complete",
|
|
1352
|
+
runId,
|
|
1353
|
+
status: "completed",
|
|
1354
|
+
bindingId: binding?.bindingId ?? null,
|
|
1355
|
+
},
|
|
1356
|
+
});
|
|
1357
|
+
json(res, {
|
|
1358
|
+
success: true,
|
|
1359
|
+
runId,
|
|
1360
|
+
result,
|
|
1361
|
+
bindingId: binding?.bindingId ?? null,
|
|
1362
|
+
...(warning ? { warning } : {}),
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
async function handleLifecycleAbort(res, body) {
|
|
1366
|
+
const runId = body.runId;
|
|
1367
|
+
if (!runId) {
|
|
1368
|
+
errorJson(res, "runId is required", 400);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
let result = "skipped";
|
|
1372
|
+
let warning;
|
|
1373
|
+
try {
|
|
1374
|
+
result = await callLifecycleEndpoint({
|
|
1375
|
+
action: "abort",
|
|
1376
|
+
runId,
|
|
1377
|
+
reason: body.reason,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
warning = "CC-SO unavailable — lifecycle event not persisted";
|
|
1382
|
+
}
|
|
1383
|
+
const binding = await windowBindingStore.transitionByRunId(runId, "stale");
|
|
1384
|
+
appendJsonl(OUTBOX_PATH, {
|
|
1385
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1386
|
+
from: "comet-http",
|
|
1387
|
+
to: "orchestration",
|
|
1388
|
+
type: "blocked",
|
|
1389
|
+
task: runId,
|
|
1390
|
+
msg: `Lifecycle aborted: run=${runId}${body.reason ? ` reason=${body.reason}` : ""}`,
|
|
1391
|
+
lifecycle: {
|
|
1392
|
+
action: "abort",
|
|
1393
|
+
runId,
|
|
1394
|
+
status: "aborted",
|
|
1395
|
+
reason: body.reason,
|
|
1396
|
+
bindingId: binding?.bindingId ?? null,
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
json(res, {
|
|
1400
|
+
success: true,
|
|
1401
|
+
runId,
|
|
1402
|
+
result,
|
|
1403
|
+
bindingId: binding?.bindingId ?? null,
|
|
1404
|
+
...(warning ? { warning } : {}),
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
async function handleLifecycleUpdate(res, body) {
|
|
1408
|
+
const runId = body.runId;
|
|
1409
|
+
if (!runId) {
|
|
1410
|
+
errorJson(res, "runId is required", 400);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
let result = "skipped";
|
|
1414
|
+
let warning;
|
|
1415
|
+
try {
|
|
1416
|
+
result = await callLifecycleEndpoint({
|
|
1417
|
+
action: "update",
|
|
1418
|
+
runId,
|
|
1419
|
+
auditSessionId: body.auditSessionId,
|
|
1420
|
+
tabGroupId: body.tabGroupId,
|
|
1421
|
+
workflowId: body.workflowId,
|
|
1422
|
+
metadata: body.metadata,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
catch {
|
|
1426
|
+
warning = "CC-SO unavailable — lifecycle event not persisted";
|
|
1427
|
+
}
|
|
1428
|
+
json(res, { success: true, runId, result, ...(warning ? { warning } : {}) });
|
|
1429
|
+
}
|
|
1430
|
+
// ---- Orchestration handlers ----
|
|
1431
|
+
async function handleTaskStatus(res, params) {
|
|
1432
|
+
const args = paramsToRecord(params);
|
|
1433
|
+
const binding = await requireHttpBinding(res, args);
|
|
1434
|
+
if (!binding)
|
|
1435
|
+
return;
|
|
1436
|
+
const groupId = typeof args.groupId === "number" ? args.groupId : undefined;
|
|
1437
|
+
const threadId = args.projectThreadId ?? args.threadId;
|
|
1438
|
+
const bindingId = args.bindingId;
|
|
1439
|
+
const sessionKey = args.sessionKey;
|
|
1440
|
+
const runId = args.runId;
|
|
1441
|
+
const manifest = readJsonSafe(MANIFEST_PATH);
|
|
1442
|
+
const sessions = manifest?.sessions || [];
|
|
1443
|
+
const matched = sessions.filter((s) => {
|
|
1444
|
+
const codexBinding = s.codexBinding;
|
|
1445
|
+
if (codexBinding?.bindingId !== binding.bindingId)
|
|
1446
|
+
return false;
|
|
1447
|
+
const checks = [];
|
|
1448
|
+
if (bindingId)
|
|
1449
|
+
checks.push(codexBinding.bindingId === bindingId);
|
|
1450
|
+
if (sessionKey)
|
|
1451
|
+
checks.push(s.sessionKey === sessionKey);
|
|
1452
|
+
if (groupId !== undefined)
|
|
1453
|
+
checks.push(s.tabGroupId === groupId);
|
|
1454
|
+
if (threadId)
|
|
1455
|
+
checks.push(s.taskThreadId === threadId || codexBinding.projectThreadId === threadId);
|
|
1456
|
+
if (runId)
|
|
1457
|
+
checks.push(Array.isArray(codexBinding.runIds) && codexBinding.runIds.includes(runId));
|
|
1458
|
+
return checks.every(Boolean);
|
|
1459
|
+
});
|
|
1460
|
+
if (matched.length === 0) {
|
|
1461
|
+
errorJson(res, `No sessions found for binding-scoped query ${JSON.stringify({ bindingId, sessionKey, projectThreadId: threadId, runId, groupId })}`, 404);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const result = matched.map((s) => ({
|
|
1465
|
+
bindingId: s.codexBinding?.bindingId ?? null,
|
|
1466
|
+
sessionKey: s.sessionKey,
|
|
1467
|
+
groupId: s.tabGroupId,
|
|
1468
|
+
threadId: s.taskThreadId,
|
|
1469
|
+
status: s.agent?.status || "unknown",
|
|
1470
|
+
agentId: s.agentId ?? s.agent?.id ?? null,
|
|
1471
|
+
tabs: Array.isArray(s.tabs) ? s.tabs.length : 0,
|
|
1472
|
+
metrics: s.metrics || {},
|
|
1473
|
+
links: s.links || {},
|
|
1474
|
+
lastActivity: s.metrics?.lastActivityAt || null,
|
|
1475
|
+
}));
|
|
1476
|
+
json(res, { sessions: result });
|
|
1477
|
+
}
|
|
1478
|
+
async function handleDelegate(res, body) {
|
|
1479
|
+
const delegateResult = await withMutex(res, async () => {
|
|
1480
|
+
const threadId = body.threadId;
|
|
1481
|
+
const instruction = body.instruction;
|
|
1482
|
+
if (!threadId || !instruction) {
|
|
1483
|
+
return { error: "threadId and instruction are required" };
|
|
1484
|
+
}
|
|
1485
|
+
const priority = body.priority || "P3";
|
|
1486
|
+
const urls = body.urls || [];
|
|
1487
|
+
const dependsOn = body.dependsOn || [];
|
|
1488
|
+
const agentId = body.agentId || "comet-http";
|
|
1489
|
+
const taskId = `comet-${Date.now()}`;
|
|
1490
|
+
const priorityColors = {
|
|
1491
|
+
P1: "red",
|
|
1492
|
+
P2: "yellow",
|
|
1493
|
+
P3: "blue",
|
|
1494
|
+
P4: "grey",
|
|
1495
|
+
};
|
|
1496
|
+
const color = priorityColors[priority] || "blue";
|
|
1497
|
+
// 1. Write to inbox
|
|
1498
|
+
appendJsonl(INBOX_PATH, {
|
|
1499
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1500
|
+
from: "comet-delegate",
|
|
1501
|
+
to: "comet-browser",
|
|
1502
|
+
task: taskId,
|
|
1503
|
+
type: "instruction",
|
|
1504
|
+
thread: threadId,
|
|
1505
|
+
priority,
|
|
1506
|
+
msg: instruction,
|
|
1507
|
+
browser_task: {
|
|
1508
|
+
thread_id: threadId,
|
|
1509
|
+
group_name: threadId.slice(0, 50),
|
|
1510
|
+
color,
|
|
1511
|
+
priority,
|
|
1512
|
+
urls,
|
|
1513
|
+
depends_on: dependsOn,
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
// 2. Start lifecycle (graceful)
|
|
1517
|
+
let lifecycleResult = "skipped";
|
|
1518
|
+
try {
|
|
1519
|
+
lifecycleResult = await callLifecycleEndpoint({
|
|
1520
|
+
action: "start",
|
|
1521
|
+
runId: taskId,
|
|
1522
|
+
taskThreadId: threadId,
|
|
1523
|
+
agentId,
|
|
1524
|
+
route: "http",
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
/* CC-SO may not be running */
|
|
1529
|
+
}
|
|
1530
|
+
// 3. Create/reuse the Codex window binding directly (no legacy session-controller dispatch).
|
|
1531
|
+
let currentBinding = null;
|
|
1532
|
+
try {
|
|
1533
|
+
const identity = deriveCodexSessionIdentity(identityInputFromArgs(body));
|
|
1534
|
+
currentBinding = await windowBindingStore.findActiveByIdentity(identity);
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
currentBinding = null;
|
|
1538
|
+
}
|
|
1539
|
+
const bindingDispatch = await createOrReuseDelegateBinding({
|
|
1540
|
+
...identityInputFromArgs(body),
|
|
1541
|
+
strict: false,
|
|
1542
|
+
taskId,
|
|
1543
|
+
threadId,
|
|
1544
|
+
agentId,
|
|
1545
|
+
currentBinding,
|
|
1546
|
+
bindingId: body.bindingId,
|
|
1547
|
+
projectThreadId: body.projectThreadId ?? threadId,
|
|
1548
|
+
fallbackAgentId: agentId,
|
|
1549
|
+
fallbackTaskThreadId: threadId,
|
|
1550
|
+
windowId: body.windowId,
|
|
1551
|
+
tabGroupId: body.tabGroupId ?? body.groupId ?? null,
|
|
1552
|
+
targetId: body.targetId,
|
|
1553
|
+
});
|
|
1554
|
+
// 4. Write to outbox
|
|
1555
|
+
appendJsonl(OUTBOX_PATH, {
|
|
1556
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1557
|
+
from: "comet-http",
|
|
1558
|
+
to: "orchestration",
|
|
1559
|
+
type: "update",
|
|
1560
|
+
task: taskId,
|
|
1561
|
+
thread: threadId,
|
|
1562
|
+
msg: `Delegated: ${instruction.slice(0, 100)}`,
|
|
1563
|
+
lifecycle: { action: "start", runId: taskId, status: "dispatched" },
|
|
1564
|
+
});
|
|
1565
|
+
return {
|
|
1566
|
+
taskId,
|
|
1567
|
+
threadId,
|
|
1568
|
+
priority,
|
|
1569
|
+
color,
|
|
1570
|
+
lifecycleResult,
|
|
1571
|
+
bindingId: bindingDispatch.bindingId,
|
|
1572
|
+
windowId: bindingDispatch.windowId,
|
|
1573
|
+
tabGroupId: bindingDispatch.tabGroupId,
|
|
1574
|
+
dispatchStatus: bindingDispatch.dispatchStatus,
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
if (delegateResult !== null) {
|
|
1578
|
+
if (delegateResult && "error" in delegateResult)
|
|
1579
|
+
errorJson(res, delegateResult.error, 400);
|
|
1580
|
+
else if (delegateResult)
|
|
1581
|
+
json(res, delegateResult);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
// ---- Parity tool handlers (require session) ----
|
|
1585
|
+
async function handlePdf(res, body) {
|
|
1586
|
+
const result = await withMutex(res, async () => {
|
|
1587
|
+
if (requireSession(res))
|
|
1588
|
+
return null;
|
|
1589
|
+
const pdfUrl = body.url;
|
|
1590
|
+
const pdfName = body.name;
|
|
1591
|
+
const pdfFormat = body.format || "Letter";
|
|
1592
|
+
const pdfLandscape = body.landscape || false;
|
|
1593
|
+
const pdfMargin = body.margin ?? 0.5;
|
|
1594
|
+
const pdfScale = body.scale || 1;
|
|
1595
|
+
const pdfPrintBg = body.printBackground !== false;
|
|
1596
|
+
const pdfHideSelectors = body.hideSelectors;
|
|
1597
|
+
if (pdfUrl) {
|
|
1598
|
+
await cometClient.navigate(pdfUrl, true, true);
|
|
1599
|
+
}
|
|
1600
|
+
if (pdfHideSelectors) {
|
|
1601
|
+
const selectors = pdfHideSelectors.split(",").map((s) => s.trim());
|
|
1602
|
+
for (const sel of selectors) {
|
|
1603
|
+
await cometClient.evaluate(`document.querySelectorAll(${safeSelector(sel)}).forEach(el => el.style.display = 'none')`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const paperSizes = {
|
|
1607
|
+
Letter: { width: 8.5, height: 11 },
|
|
1608
|
+
Legal: { width: 8.5, height: 14 },
|
|
1609
|
+
A4: { width: 8.27, height: 11.69 },
|
|
1610
|
+
A3: { width: 11.69, height: 16.54 },
|
|
1611
|
+
Tabloid: { width: 11, height: 17 },
|
|
1612
|
+
};
|
|
1613
|
+
const paper = paperSizes[pdfFormat] || paperSizes.Letter;
|
|
1614
|
+
const cdp = cometClient.protocol;
|
|
1615
|
+
const pdfResult = await cdp.send("Page.printToPDF", {
|
|
1616
|
+
landscape: pdfLandscape,
|
|
1617
|
+
printBackground: pdfPrintBg,
|
|
1618
|
+
scale: pdfScale,
|
|
1619
|
+
paperWidth: paper.width,
|
|
1620
|
+
paperHeight: paper.height,
|
|
1621
|
+
marginTop: pdfMargin,
|
|
1622
|
+
marginBottom: pdfMargin,
|
|
1623
|
+
marginLeft: pdfMargin,
|
|
1624
|
+
marginRight: pdfMargin,
|
|
1625
|
+
});
|
|
1626
|
+
const outputDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
1627
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1628
|
+
const titleResult = await cometClient.evaluate("document.title");
|
|
1629
|
+
const pageTitle = titleResult.result.value || "page";
|
|
1630
|
+
const safeName = pdfName || pageTitle.replace(/[^a-zA-Z0-9-_]/g, "_").substring(0, 60);
|
|
1631
|
+
const outputPath = join(outputDir, `${safeName}-${Date.now()}.pdf`);
|
|
1632
|
+
const pdfBuffer = Buffer.from(pdfResult.data, "base64");
|
|
1633
|
+
writeFileSync(outputPath, pdfBuffer);
|
|
1634
|
+
return {
|
|
1635
|
+
filePath: outputPath,
|
|
1636
|
+
sizeKB: (pdfBuffer.length / 1024).toFixed(1),
|
|
1637
|
+
format: pdfFormat,
|
|
1638
|
+
landscape: pdfLandscape,
|
|
1639
|
+
};
|
|
1640
|
+
});
|
|
1641
|
+
if (result !== null) {
|
|
1642
|
+
if (result)
|
|
1643
|
+
json(res, result);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
async function handleScrape(res, body) {
|
|
1647
|
+
const result = await withMutex(res, async () => {
|
|
1648
|
+
if (requireSession(res))
|
|
1649
|
+
return null;
|
|
1650
|
+
const scrapeUrl = body.url;
|
|
1651
|
+
const scrapeSelector = body.selector;
|
|
1652
|
+
const scrapeMode = body.mode || "text";
|
|
1653
|
+
const scrapeAttr = body.attr;
|
|
1654
|
+
const scrapeScroll = body.scroll || false;
|
|
1655
|
+
const scrapeWaitFor = body.waitFor;
|
|
1656
|
+
if (scrapeUrl) {
|
|
1657
|
+
await cometClient.navigate(scrapeUrl, true, true);
|
|
1658
|
+
}
|
|
1659
|
+
if (scrapeWaitFor) {
|
|
1660
|
+
const waitStart = Date.now();
|
|
1661
|
+
while (Date.now() - waitStart < 10000) {
|
|
1662
|
+
const found = await cometClient.evaluate(`!!document.querySelector(${safeSelector(scrapeWaitFor)})`);
|
|
1663
|
+
if (found.result.value)
|
|
1664
|
+
break;
|
|
1665
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (scrapeScroll) {
|
|
1669
|
+
await cometClient.evaluate(`
|
|
1670
|
+
(async () => {
|
|
1671
|
+
let totalHeight = 0;
|
|
1672
|
+
const distance = 500;
|
|
1673
|
+
while (totalHeight < document.body.scrollHeight && totalHeight < 50000) {
|
|
1674
|
+
window.scrollBy(0, distance);
|
|
1675
|
+
totalHeight += distance;
|
|
1676
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1677
|
+
}
|
|
1678
|
+
window.scrollTo(0, 0);
|
|
1679
|
+
})()
|
|
1680
|
+
`);
|
|
1681
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1682
|
+
}
|
|
1683
|
+
const safeSel = safeSelector(scrapeSelector || "body");
|
|
1684
|
+
const safeAttrName = safeSelector(scrapeAttr || "href");
|
|
1685
|
+
let extractionScript;
|
|
1686
|
+
switch (scrapeMode) {
|
|
1687
|
+
case "table":
|
|
1688
|
+
extractionScript = `
|
|
1689
|
+
(() => {
|
|
1690
|
+
const table = document.querySelector(${safeSelector(scrapeSelector || "table")});
|
|
1691
|
+
if (!table) return { error: 'No table found' };
|
|
1692
|
+
const rows = [...table.querySelectorAll('tr')];
|
|
1693
|
+
const headers = [...rows[0]?.querySelectorAll('th, td')].map(c => c.innerText.trim());
|
|
1694
|
+
const data = rows.slice(1).map(row => {
|
|
1695
|
+
const cells = [...row.querySelectorAll('td, th')].map(c => c.innerText.trim());
|
|
1696
|
+
return headers.length ? Object.fromEntries(headers.map((h, i) => [h, cells[i] || ''])) : cells;
|
|
1697
|
+
});
|
|
1698
|
+
return { rows: data.length, headers, data };
|
|
1699
|
+
})()`;
|
|
1700
|
+
break;
|
|
1701
|
+
case "json-ld":
|
|
1702
|
+
extractionScript = `
|
|
1703
|
+
(() => {
|
|
1704
|
+
const scripts = [...document.querySelectorAll('script[type="application/ld+json"]')];
|
|
1705
|
+
const data = scripts.map(s => { try { return JSON.parse(s.textContent); } catch { return null; } }).filter(Boolean);
|
|
1706
|
+
return { count: data.length, data };
|
|
1707
|
+
})()`;
|
|
1708
|
+
break;
|
|
1709
|
+
case "list":
|
|
1710
|
+
extractionScript = `
|
|
1711
|
+
(() => {
|
|
1712
|
+
const sel = ${safeSelector(scrapeSelector || "ul li, ol li")};
|
|
1713
|
+
const items = [...document.querySelectorAll(sel)].map(el => el.innerText.trim());
|
|
1714
|
+
return { count: items.length, items };
|
|
1715
|
+
})()`;
|
|
1716
|
+
break;
|
|
1717
|
+
case "attr":
|
|
1718
|
+
extractionScript = `
|
|
1719
|
+
(() => {
|
|
1720
|
+
const sel = ${safeSel};
|
|
1721
|
+
const attr = ${safeAttrName};
|
|
1722
|
+
const els = [...document.querySelectorAll(sel)];
|
|
1723
|
+
const values = els.map(el => el.getAttribute(attr)).filter(Boolean);
|
|
1724
|
+
return { count: values.length, attribute: attr, values };
|
|
1725
|
+
})()`;
|
|
1726
|
+
break;
|
|
1727
|
+
case "multi":
|
|
1728
|
+
extractionScript = `
|
|
1729
|
+
(() => {
|
|
1730
|
+
const sel = ${safeSelector(scrapeSelector || "p")};
|
|
1731
|
+
const els = [...document.querySelectorAll(sel)];
|
|
1732
|
+
const items = els.map(el => ({ tag: el.tagName, text: el.innerText.trim().substring(0, 500) }));
|
|
1733
|
+
return { count: items.length, items };
|
|
1734
|
+
})()`;
|
|
1735
|
+
break;
|
|
1736
|
+
default: // text
|
|
1737
|
+
extractionScript = `
|
|
1738
|
+
(() => {
|
|
1739
|
+
const sel = ${safeSel};
|
|
1740
|
+
const el = document.querySelector(sel);
|
|
1741
|
+
if (!el) return { error: 'Selector not found: ' + sel };
|
|
1742
|
+
return { tag: el.tagName, text: el.innerText.trim().substring(0, 10000) };
|
|
1743
|
+
})()`;
|
|
1744
|
+
}
|
|
1745
|
+
const scrapeResult = await cometClient.evaluate(extractionScript);
|
|
1746
|
+
return { data: scrapeResult.result.value };
|
|
1747
|
+
});
|
|
1748
|
+
if (result !== null) {
|
|
1749
|
+
if (result)
|
|
1750
|
+
json(res, result);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function handleNetwork(res, body) {
|
|
1754
|
+
const result = await withMutex(res, async () => {
|
|
1755
|
+
if (requireSession(res))
|
|
1756
|
+
return null;
|
|
1757
|
+
const netAction = body.action || "capture";
|
|
1758
|
+
const netUrl = body.url;
|
|
1759
|
+
const netDuration = body.duration || 10000;
|
|
1760
|
+
const netFilter = body.filter;
|
|
1761
|
+
const netResourceType = body.resourceType;
|
|
1762
|
+
const netPattern = body.pattern;
|
|
1763
|
+
const netMockResponse = body.mockResponse;
|
|
1764
|
+
const netMockStatus = body.mockStatus || 200;
|
|
1765
|
+
const netIncludeHeaders = body.includeHeaders || false;
|
|
1766
|
+
// CDP.Client extends EventEmitter at runtime but types don't expose on/once/removeListener
|
|
1767
|
+
const cdp = cometClient.protocol;
|
|
1768
|
+
if (netAction === "block") {
|
|
1769
|
+
if (!netPattern)
|
|
1770
|
+
return { error: "'pattern' is required for block action" };
|
|
1771
|
+
await cdp.send("Network.enable");
|
|
1772
|
+
await cdp.send("Network.setBlockedURLs", { urls: [netPattern] });
|
|
1773
|
+
return {
|
|
1774
|
+
action: "block",
|
|
1775
|
+
pattern: netPattern,
|
|
1776
|
+
message: `Blocking requests matching: ${netPattern}`,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
if (netAction === "intercept") {
|
|
1780
|
+
if (!netPattern)
|
|
1781
|
+
return { error: "'pattern' is required for intercept action" };
|
|
1782
|
+
await cdp.send("Fetch.enable", {
|
|
1783
|
+
patterns: [{ urlPattern: `*${netPattern}*`, requestStage: "Response" }],
|
|
1784
|
+
});
|
|
1785
|
+
const interceptResult = await new Promise((resolve) => {
|
|
1786
|
+
const timeout = setTimeout(() => resolve("No matching request within 30s"), 30000);
|
|
1787
|
+
cdp.once("Fetch.requestPaused", async (params) => {
|
|
1788
|
+
clearTimeout(timeout);
|
|
1789
|
+
const body = netMockResponse || '{"mocked":true}';
|
|
1790
|
+
const headers = [
|
|
1791
|
+
{ name: "Content-Type", value: "application/json" },
|
|
1792
|
+
{ name: "Access-Control-Allow-Origin", value: "*" },
|
|
1793
|
+
];
|
|
1794
|
+
await cdp.send("Fetch.fulfillRequest", {
|
|
1795
|
+
requestId: params.requestId,
|
|
1796
|
+
responseCode: netMockStatus,
|
|
1797
|
+
responseHeaders: headers,
|
|
1798
|
+
body: Buffer.from(body).toString("base64"),
|
|
1799
|
+
});
|
|
1800
|
+
resolve(`Intercepted: ${params.request.url}\nResponded with status ${netMockStatus}`);
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
await cdp.send("Fetch.disable");
|
|
1804
|
+
return { action: "intercept", pattern: netPattern, result: interceptResult };
|
|
1805
|
+
}
|
|
1806
|
+
// capture action
|
|
1807
|
+
if (netUrl)
|
|
1808
|
+
await cometClient.navigate(netUrl, true, true);
|
|
1809
|
+
await cdp.send("Network.enable");
|
|
1810
|
+
const captured = [];
|
|
1811
|
+
const responseMap = new Map();
|
|
1812
|
+
const onRequest = (params) => {
|
|
1813
|
+
if (netFilter && !params.request.url.includes(netFilter))
|
|
1814
|
+
return;
|
|
1815
|
+
if (netResourceType && params.type?.toLowerCase() !== netResourceType.toLowerCase())
|
|
1816
|
+
return;
|
|
1817
|
+
captured.push({
|
|
1818
|
+
requestId: params.requestId,
|
|
1819
|
+
method: params.request.method,
|
|
1820
|
+
url: params.request.url,
|
|
1821
|
+
type: params.type,
|
|
1822
|
+
...(netIncludeHeaders ? { requestHeaders: params.request.headers } : {}),
|
|
1823
|
+
});
|
|
1824
|
+
};
|
|
1825
|
+
const onResponse = (params) => {
|
|
1826
|
+
responseMap.set(params.requestId, {
|
|
1827
|
+
status: params.response.status,
|
|
1828
|
+
mimeType: params.response.mimeType,
|
|
1829
|
+
...(netIncludeHeaders ? { responseHeaders: params.response.headers } : {}),
|
|
1830
|
+
});
|
|
1831
|
+
};
|
|
1832
|
+
cdp.on("Network.requestWillBeSent", onRequest);
|
|
1833
|
+
cdp.on("Network.responseReceived", onResponse);
|
|
1834
|
+
await new Promise((r) => setTimeout(r, netDuration));
|
|
1835
|
+
cdp.removeListener("Network.requestWillBeSent", onRequest);
|
|
1836
|
+
cdp.removeListener("Network.responseReceived", onResponse);
|
|
1837
|
+
await cdp.send("Network.disable");
|
|
1838
|
+
for (const req of captured) {
|
|
1839
|
+
const resp = responseMap.get(req.requestId);
|
|
1840
|
+
if (resp) {
|
|
1841
|
+
req.status = resp.status;
|
|
1842
|
+
req.mimeType = resp.mimeType;
|
|
1843
|
+
if (resp.responseHeaders)
|
|
1844
|
+
req.responseHeaders = resp.responseHeaders;
|
|
1845
|
+
}
|
|
1846
|
+
delete req.requestId;
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
action: "capture",
|
|
1850
|
+
summary: `Captured ${captured.length} requests over ${netDuration}ms${netFilter ? ` (filter: ${netFilter})` : ""}`,
|
|
1851
|
+
captured: captured.slice(0, 50),
|
|
1852
|
+
overflow: captured.length > 50 ? captured.length - 50 : 0,
|
|
1853
|
+
};
|
|
1854
|
+
});
|
|
1855
|
+
if (result !== null) {
|
|
1856
|
+
if (result && "error" in result)
|
|
1857
|
+
errorJson(res, result.error, 400);
|
|
1858
|
+
else if (result)
|
|
1859
|
+
json(res, result);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async function handleAutomate(res, body) {
|
|
1863
|
+
const result = await withMutex(res, async () => {
|
|
1864
|
+
if (requireSession(res))
|
|
1865
|
+
return null;
|
|
1866
|
+
const autoSteps = body.steps;
|
|
1867
|
+
if (!autoSteps || autoSteps.length === 0) {
|
|
1868
|
+
return { error: "'steps' array is required and must not be empty" };
|
|
1869
|
+
}
|
|
1870
|
+
const variables = {};
|
|
1871
|
+
const results = [];
|
|
1872
|
+
function isValidIdentifier(name) {
|
|
1873
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
1874
|
+
}
|
|
1875
|
+
function isSafeConditionExpression(expression) {
|
|
1876
|
+
if (typeof expression !== "string" || expression.length === 0 || expression.length > 200)
|
|
1877
|
+
return false;
|
|
1878
|
+
if (/[;{}\\`]/.test(expression))
|
|
1879
|
+
return false;
|
|
1880
|
+
if (/\b(?:function|new|this|window|document|globalThis|constructor|__proto__|prototype|eval|import|return)\b/.test(expression)) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
return /^[\w\s.$'"!?<>=&|()+\-*/%:[\],]+$/.test(expression);
|
|
1884
|
+
}
|
|
1885
|
+
async function executeStep(step, index) {
|
|
1886
|
+
const prefix = `Step ${index + 1} [${step.tool}]`;
|
|
1887
|
+
try {
|
|
1888
|
+
switch (step.tool) {
|
|
1889
|
+
case "navigate":
|
|
1890
|
+
await cometClient.navigate(step.url, true, true);
|
|
1891
|
+
results.push(`✓ ${prefix}: navigated to ${step.url}`);
|
|
1892
|
+
break;
|
|
1893
|
+
case "click":
|
|
1894
|
+
await cometClient.evaluate(`
|
|
1895
|
+
(() => {
|
|
1896
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
1897
|
+
if (!el) throw new Error('Element not found: ' + ${safeSelector(step.selector)});
|
|
1898
|
+
el.scrollIntoView({ block: 'center' });
|
|
1899
|
+
el.click();
|
|
1900
|
+
})()
|
|
1901
|
+
`);
|
|
1902
|
+
results.push(`✓ ${prefix}: clicked ${step.selector}`);
|
|
1903
|
+
break;
|
|
1904
|
+
case "fill": {
|
|
1905
|
+
const safeFillVal = JSON.stringify(step.value || "");
|
|
1906
|
+
await cometClient.evaluate(`
|
|
1907
|
+
(() => {
|
|
1908
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
1909
|
+
if (!el) throw new Error('Element not found: ' + ${safeSelector(step.selector)});
|
|
1910
|
+
const val = ${safeFillVal};
|
|
1911
|
+
const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
|
1912
|
+
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
1913
|
+
if (nativeSet) nativeSet.call(el, val);
|
|
1914
|
+
else el.value = val;
|
|
1915
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1916
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1917
|
+
})()
|
|
1918
|
+
`);
|
|
1919
|
+
results.push(`✓ ${prefix}: filled ${step.selector}`);
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
case "type":
|
|
1923
|
+
for (const char of step.value || "") {
|
|
1924
|
+
await cometClient.protocol.send("Input.dispatchKeyEvent", {
|
|
1925
|
+
type: "keyDown",
|
|
1926
|
+
text: char,
|
|
1927
|
+
});
|
|
1928
|
+
await cometClient.protocol.send("Input.dispatchKeyEvent", {
|
|
1929
|
+
type: "keyUp",
|
|
1930
|
+
text: char,
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
results.push(`✓ ${prefix}: typed ${(step.value || "").length} chars`);
|
|
1934
|
+
break;
|
|
1935
|
+
case "press":
|
|
1936
|
+
await cometClient.protocol.send("Input.dispatchKeyEvent", {
|
|
1937
|
+
type: "keyDown",
|
|
1938
|
+
key: step.value,
|
|
1939
|
+
code: `Key${step.value?.toUpperCase()}`,
|
|
1940
|
+
});
|
|
1941
|
+
await cometClient.protocol.send("Input.dispatchKeyEvent", {
|
|
1942
|
+
type: "keyUp",
|
|
1943
|
+
key: step.value,
|
|
1944
|
+
code: `Key${step.value?.toUpperCase()}`,
|
|
1945
|
+
});
|
|
1946
|
+
results.push(`✓ ${prefix}: pressed ${step.value}`);
|
|
1947
|
+
break;
|
|
1948
|
+
case "select":
|
|
1949
|
+
await cometClient.evaluate(`
|
|
1950
|
+
(() => {
|
|
1951
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
1952
|
+
if (!el) throw new Error('Element not found');
|
|
1953
|
+
el.value = ${JSON.stringify(step.value || "")};
|
|
1954
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1955
|
+
})()
|
|
1956
|
+
`);
|
|
1957
|
+
results.push(`✓ ${prefix}: selected ${step.value}`);
|
|
1958
|
+
break;
|
|
1959
|
+
case "wait":
|
|
1960
|
+
if (step.selector) {
|
|
1961
|
+
const waitMs = Date.now();
|
|
1962
|
+
while (Date.now() - waitMs < 10000) {
|
|
1963
|
+
const found = await cometClient.evaluate(`!!document.querySelector(${safeSelector(step.selector)})`);
|
|
1964
|
+
if (found.result.value)
|
|
1965
|
+
break;
|
|
1966
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
else if (step.value) {
|
|
1970
|
+
await new Promise((r) => setTimeout(r, parseInt(step.value)));
|
|
1971
|
+
}
|
|
1972
|
+
results.push(`✓ ${prefix}: waited for ${step.selector || step.value + "ms"}`);
|
|
1973
|
+
break;
|
|
1974
|
+
case "screenshot": {
|
|
1975
|
+
const ssResult = await cometClient.protocol.send("Page.captureScreenshot", {
|
|
1976
|
+
format: "png",
|
|
1977
|
+
});
|
|
1978
|
+
const ssDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
1979
|
+
mkdirSync(ssDir, { recursive: true });
|
|
1980
|
+
const ssPath = join(ssDir, `${step.name || "step-" + (index + 1)}-${Date.now()}.png`);
|
|
1981
|
+
writeFileSync(ssPath, Buffer.from(ssResult.data, "base64"));
|
|
1982
|
+
results.push(`✓ ${prefix}: screenshot → ${ssPath}`);
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
case "extract": {
|
|
1986
|
+
const extractResult = await cometClient.evaluate(`
|
|
1987
|
+
(() => {
|
|
1988
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
1989
|
+
if (!el) return null;
|
|
1990
|
+
return el.innerText?.trim() || el.value || '';
|
|
1991
|
+
})()
|
|
1992
|
+
`);
|
|
1993
|
+
const extractedValue = extractResult.result.value;
|
|
1994
|
+
if (step.variable && isValidIdentifier(step.variable))
|
|
1995
|
+
variables[step.variable] = extractedValue;
|
|
1996
|
+
results.push(`✓ ${prefix}: extracted ${String(extractedValue).substring(0, 100)}${step.variable ? ` → $${step.variable}` : ""}`);
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
case "assert": {
|
|
2000
|
+
const assertResult = await cometClient.evaluate(`document.querySelector(${safeSelector(step.selector)})?.innerText || ''`);
|
|
2001
|
+
const assertText = assertResult.result.value;
|
|
2002
|
+
if (step.contains && !assertText.includes(step.contains)) {
|
|
2003
|
+
throw new Error(`Assertion failed: "${step.selector}" does not contain "${step.contains}". Got: "${assertText.substring(0, 200)}"`);
|
|
2004
|
+
}
|
|
2005
|
+
results.push(`✓ ${prefix}: assertion passed`);
|
|
2006
|
+
break;
|
|
2007
|
+
}
|
|
2008
|
+
case "evaluate": {
|
|
2009
|
+
const evalResult = await cometClient.evaluate(step.expression);
|
|
2010
|
+
const evalValue = evalResult.result.value;
|
|
2011
|
+
if (step.variable && isValidIdentifier(step.variable))
|
|
2012
|
+
variables[step.variable] = evalValue;
|
|
2013
|
+
results.push(`✓ ${prefix}: ${JSON.stringify(evalValue).substring(0, 200)}${step.variable ? ` → $${step.variable}` : ""}`);
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
case "if": {
|
|
2017
|
+
if (!isSafeConditionExpression(step.condition)) {
|
|
2018
|
+
throw new Error("Unsafe condition expression in if step");
|
|
2019
|
+
}
|
|
2020
|
+
const varInjection = Object.entries(variables)
|
|
2021
|
+
.filter(([k]) => isValidIdentifier(k))
|
|
2022
|
+
.map(([k, v]) => `const ${k} = ${JSON.stringify(v)};`)
|
|
2023
|
+
.join(" ");
|
|
2024
|
+
const condEvalResult = await cometClient.evaluate(`(() => { ${varInjection} return !!(${step.condition}); })()`);
|
|
2025
|
+
const condResult = condEvalResult.result.value;
|
|
2026
|
+
if (condResult && step.then) {
|
|
2027
|
+
for (let i = 0; i < step.then.length; i++) {
|
|
2028
|
+
const ok = await executeStep(step.then[i], i);
|
|
2029
|
+
if (!ok)
|
|
2030
|
+
return false;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
results.push(`✓ ${prefix}: condition ${condResult ? "true" : "false"}`);
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
case "loop": {
|
|
2037
|
+
const loopItems = variables[step.items];
|
|
2038
|
+
if (!Array.isArray(loopItems))
|
|
2039
|
+
throw new Error(`Variable "${step.items}" is not an array`);
|
|
2040
|
+
for (let li = 0; li < loopItems.length; li++) {
|
|
2041
|
+
variables["_item"] = loopItems[li];
|
|
2042
|
+
variables["_index"] = li;
|
|
2043
|
+
for (let si = 0; si < (step.each || []).length; si++) {
|
|
2044
|
+
const ok = await executeStep(step.each[si], si);
|
|
2045
|
+
if (!ok)
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
results.push(`✓ ${prefix}: looped ${loopItems.length} items`);
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
default:
|
|
2053
|
+
throw new Error(`Unknown step tool: ${step.tool}`);
|
|
2054
|
+
}
|
|
2055
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2056
|
+
return true;
|
|
2057
|
+
}
|
|
2058
|
+
catch (err) {
|
|
2059
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2060
|
+
results.push(`✗ ${prefix}: ${errMsg}`);
|
|
2061
|
+
if (step.optional)
|
|
2062
|
+
return true;
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
let aborted = false;
|
|
2067
|
+
for (let i = 0; i < autoSteps.length; i++) {
|
|
2068
|
+
const ok = await executeStep(autoSteps[i], i);
|
|
2069
|
+
if (!ok) {
|
|
2070
|
+
results.push(`\n⚠ Workflow aborted at step ${i + 1}`);
|
|
2071
|
+
aborted = true;
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const publicVars = Object.fromEntries(Object.entries(variables).filter(([k]) => !k.startsWith("_")));
|
|
2076
|
+
return {
|
|
2077
|
+
stepsTotal: autoSteps.length,
|
|
2078
|
+
stepsCompleted: results.filter((r) => r.startsWith("✓")).length,
|
|
2079
|
+
results,
|
|
2080
|
+
variables: publicVars,
|
|
2081
|
+
aborted,
|
|
2082
|
+
};
|
|
2083
|
+
});
|
|
2084
|
+
if (result !== null) {
|
|
2085
|
+
if (result && "error" in result)
|
|
2086
|
+
errorJson(res, result.error, 400);
|
|
2087
|
+
else if (result)
|
|
2088
|
+
json(res, result);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async function handleDomain(res, body) {
|
|
2092
|
+
const result = await withMutex(res, async () => {
|
|
2093
|
+
if (requireSession(res))
|
|
2094
|
+
return null;
|
|
2095
|
+
const domainName = body.domain;
|
|
2096
|
+
const domainAction = body.action || "check-auth";
|
|
2097
|
+
const domainPath = body.path;
|
|
2098
|
+
if (!domainName) {
|
|
2099
|
+
return { error: "domain is required. Use: qbo, mercury, github, google, salt" };
|
|
2100
|
+
}
|
|
2101
|
+
const domainConfigs = {
|
|
2102
|
+
qbo: {
|
|
2103
|
+
home: "https://app.qbo.intuit.com/app/homepage",
|
|
2104
|
+
authCheck: "app.qbo.intuit.com",
|
|
2105
|
+
name: "QuickBooks Online",
|
|
2106
|
+
},
|
|
2107
|
+
mercury: {
|
|
2108
|
+
home: "https://app.mercury.com/dashboard",
|
|
2109
|
+
authCheck: "app.mercury.com",
|
|
2110
|
+
name: "Mercury Banking",
|
|
2111
|
+
},
|
|
2112
|
+
github: { home: "https://github.com", authCheck: "github.com", name: "GitHub" },
|
|
2113
|
+
google: {
|
|
2114
|
+
home: "https://drive.google.com",
|
|
2115
|
+
authCheck: "accounts.google.com/SignOut",
|
|
2116
|
+
name: "Google Workspace",
|
|
2117
|
+
},
|
|
2118
|
+
salt: { home: "https://app.salt.dev", authCheck: "app.salt.dev", name: "SALT Tax" },
|
|
2119
|
+
};
|
|
2120
|
+
const domainConfig = domainConfigs[domainName];
|
|
2121
|
+
if (!domainConfig) {
|
|
2122
|
+
return { error: `Unknown domain: ${domainName}. Use: qbo, mercury, github, google, salt` };
|
|
2123
|
+
}
|
|
2124
|
+
if (domainAction === "navigate" || domainAction === "status") {
|
|
2125
|
+
const targetUrl = domainPath
|
|
2126
|
+
? new URL(domainPath, domainConfig.home).href
|
|
2127
|
+
: domainConfig.home;
|
|
2128
|
+
await cometClient.navigate(targetUrl, true, true);
|
|
2129
|
+
const finalUrlResult = await cometClient.evaluate("window.location.href");
|
|
2130
|
+
const finalUrl = finalUrlResult.result.value;
|
|
2131
|
+
const titleResult = await cometClient.evaluate("document.title");
|
|
2132
|
+
const title = titleResult.result.value;
|
|
2133
|
+
const isLoginPage = finalUrl.includes("login") ||
|
|
2134
|
+
finalUrl.includes("signin") ||
|
|
2135
|
+
finalUrl.includes("auth") ||
|
|
2136
|
+
finalUrl.includes("accounts.google.com/v3/signin");
|
|
2137
|
+
return {
|
|
2138
|
+
domain: domainName,
|
|
2139
|
+
name: domainConfig.name,
|
|
2140
|
+
action: domainAction,
|
|
2141
|
+
url: finalUrl,
|
|
2142
|
+
title,
|
|
2143
|
+
authenticated: !isLoginPage,
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
// check-auth: check current URL, navigate to verify if not on domain
|
|
2147
|
+
const currentUrlResult = await cometClient.evaluate("window.location.href");
|
|
2148
|
+
const currentUrl = currentUrlResult.result.value;
|
|
2149
|
+
const isOnDomain = currentUrl.includes(domainConfig.authCheck);
|
|
2150
|
+
if (!isOnDomain) {
|
|
2151
|
+
await cometClient.navigate(domainConfig.home, true, true);
|
|
2152
|
+
const checkUrlResult = await cometClient.evaluate("window.location.href");
|
|
2153
|
+
const checkUrl = checkUrlResult.result.value;
|
|
2154
|
+
const isLoginRedirect = checkUrl.includes("login") || checkUrl.includes("signin") || checkUrl.includes("auth");
|
|
2155
|
+
return {
|
|
2156
|
+
domain: domainName,
|
|
2157
|
+
name: domainConfig.name,
|
|
2158
|
+
action: "check-auth",
|
|
2159
|
+
url: checkUrl,
|
|
2160
|
+
authenticated: !isLoginRedirect,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
return {
|
|
2164
|
+
domain: domainName,
|
|
2165
|
+
name: domainConfig.name,
|
|
2166
|
+
action: "check-auth",
|
|
2167
|
+
url: currentUrl,
|
|
2168
|
+
authenticated: true,
|
|
2169
|
+
};
|
|
2170
|
+
});
|
|
2171
|
+
if (result !== null) {
|
|
2172
|
+
if (result && "error" in result)
|
|
2173
|
+
errorJson(res, result.error, 400);
|
|
2174
|
+
else if (result)
|
|
2175
|
+
json(res, result);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
310
2178
|
// ---- Tab Group route handlers ----
|
|
311
2179
|
async function handleTabGroupsList(res) {
|
|
312
2180
|
const result = await withMutex(res, async () => {
|
|
@@ -378,6 +2246,37 @@ async function handleTabGroupsDelete(res, body) {
|
|
|
378
2246
|
json(res, result);
|
|
379
2247
|
}
|
|
380
2248
|
}
|
|
2249
|
+
/// Spec 037: Session lookup endpoint — returns manifest entry for a taskThreadId
|
|
2250
|
+
function handleSessionLookup(res, taskThreadId) {
|
|
2251
|
+
if (!taskThreadId) {
|
|
2252
|
+
errorJson(res, "taskThreadId is required", 400);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
try {
|
|
2256
|
+
const manifestPath = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
|
|
2257
|
+
const data = readFileSync(manifestPath, "utf-8");
|
|
2258
|
+
const manifest = JSON.parse(data);
|
|
2259
|
+
const entry = (manifest.sessions || []).find((e) => e.taskThreadId === taskThreadId);
|
|
2260
|
+
if (entry) {
|
|
2261
|
+
json(res, entry);
|
|
2262
|
+
}
|
|
2263
|
+
else {
|
|
2264
|
+
errorJson(res, `No session found for taskThreadId: ${taskThreadId}`, 404);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
catch {
|
|
2268
|
+
errorJson(res, "Session manifest not available", 404);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
// T110b: Agent registry endpoint — sync file read, no mutex needed
|
|
2272
|
+
function handleAgents(res) {
|
|
2273
|
+
const registry = readAgentRegistry();
|
|
2274
|
+
const agents = {};
|
|
2275
|
+
for (const [id, entry] of Object.entries(registry)) {
|
|
2276
|
+
agents[id] = classifyAgentStatus(entry);
|
|
2277
|
+
}
|
|
2278
|
+
json(res, { agents });
|
|
2279
|
+
}
|
|
381
2280
|
// ---- HTTP Server ----
|
|
382
2281
|
const server = createServer(async (req, res) => {
|
|
383
2282
|
// CORS preflight
|
|
@@ -397,20 +2296,28 @@ const server = createServer(async (req, res) => {
|
|
|
397
2296
|
json(res, { status: "ok", port: PORT, timestamp: new Date().toISOString() });
|
|
398
2297
|
}
|
|
399
2298
|
else if (path === "/api/connect" && req.method === "POST") {
|
|
400
|
-
await
|
|
2299
|
+
const body = await readBody(req);
|
|
2300
|
+
await handleConnect(res, body);
|
|
401
2301
|
}
|
|
402
2302
|
else if (path === "/api/ask" && req.method === "POST") {
|
|
403
2303
|
const body = await readBody(req);
|
|
404
2304
|
await handleAsk(res, body);
|
|
405
2305
|
}
|
|
2306
|
+
else if (path === "/api/equanaut/router/status" && req.method === "GET") {
|
|
2307
|
+
await handleEquanautRouterStatus(res);
|
|
2308
|
+
}
|
|
2309
|
+
else if (path === "/api/equanaut/router/ask" && req.method === "POST") {
|
|
2310
|
+
const body = await readBody(req);
|
|
2311
|
+
await handleEquanautRouterAsk(res, body);
|
|
2312
|
+
}
|
|
406
2313
|
else if (path === "/api/poll" && req.method === "GET") {
|
|
407
|
-
await handlePoll(res);
|
|
2314
|
+
await handlePoll(res, paramsToRecord(url.searchParams));
|
|
408
2315
|
}
|
|
409
2316
|
else if (path === "/api/stop" && req.method === "POST") {
|
|
410
2317
|
await handleStop(res);
|
|
411
2318
|
}
|
|
412
2319
|
else if (path === "/api/screenshot" && req.method === "GET") {
|
|
413
|
-
await handleScreenshot(res);
|
|
2320
|
+
await handleScreenshot(res, paramsToRecord(url.searchParams));
|
|
414
2321
|
}
|
|
415
2322
|
else if (path === "/api/mode" && req.method === "POST") {
|
|
416
2323
|
const body = await readBody(req);
|
|
@@ -434,11 +2341,95 @@ const server = createServer(async (req, res) => {
|
|
|
434
2341
|
const body = await readBody(req);
|
|
435
2342
|
await handleTabGroupsDelete(res, body);
|
|
436
2343
|
}
|
|
2344
|
+
else if (path === "/api/agents" && req.method === "GET") {
|
|
2345
|
+
handleAgents(res);
|
|
2346
|
+
}
|
|
2347
|
+
else if (path.startsWith("/api/session/") && req.method === "GET") {
|
|
2348
|
+
// Spec 037: Return session manifest entry by taskThreadId
|
|
2349
|
+
const taskThreadId = path.replace("/api/session/", "");
|
|
2350
|
+
handleSessionLookup(res, taskThreadId);
|
|
2351
|
+
// ---- Spec 042: New endpoints ----
|
|
2352
|
+
// Safe observation (no session required)
|
|
2353
|
+
}
|
|
2354
|
+
else if (path === "/api/observe" && req.method === "GET") {
|
|
2355
|
+
await handleObserve(res, url.searchParams);
|
|
2356
|
+
}
|
|
2357
|
+
else if (path === "/api/peek" && req.method === "GET") {
|
|
2358
|
+
await handlePeek(res, url.searchParams);
|
|
2359
|
+
// Lifecycle (no session required)
|
|
2360
|
+
}
|
|
2361
|
+
else if (path === "/api/lifecycle/start" && req.method === "POST") {
|
|
2362
|
+
const body = await readBody(req);
|
|
2363
|
+
await handleLifecycleStart(res, body);
|
|
2364
|
+
}
|
|
2365
|
+
else if (path === "/api/lifecycle/complete" && req.method === "POST") {
|
|
2366
|
+
const body = await readBody(req);
|
|
2367
|
+
await handleLifecycleComplete(res, body);
|
|
2368
|
+
}
|
|
2369
|
+
else if (path === "/api/lifecycle/abort" && req.method === "POST") {
|
|
2370
|
+
const body = await readBody(req);
|
|
2371
|
+
await handleLifecycleAbort(res, body);
|
|
2372
|
+
}
|
|
2373
|
+
else if (path === "/api/lifecycle/update" && req.method === "POST") {
|
|
2374
|
+
const body = await readBody(req);
|
|
2375
|
+
await handleLifecycleUpdate(res, body);
|
|
2376
|
+
// Orchestration
|
|
2377
|
+
}
|
|
2378
|
+
else if (path === "/api/task-status" && req.method === "GET") {
|
|
2379
|
+
await handleTaskStatus(res, url.searchParams);
|
|
2380
|
+
}
|
|
2381
|
+
else if (path === "/api/delegate" && req.method === "POST") {
|
|
2382
|
+
const body = await readBody(req);
|
|
2383
|
+
await handleDelegate(res, body);
|
|
2384
|
+
// Navigation & interaction (require session)
|
|
2385
|
+
}
|
|
2386
|
+
else if (path === "/api/navigate" && req.method === "POST") {
|
|
2387
|
+
const body = await readBody(req);
|
|
2388
|
+
await handleNavigate(res, body);
|
|
2389
|
+
}
|
|
2390
|
+
else if (path === "/api/interact" && req.method === "POST") {
|
|
2391
|
+
const body = await readBody(req);
|
|
2392
|
+
await handleInteract(res, body);
|
|
2393
|
+
}
|
|
2394
|
+
else if (path === "/api/read-page" && req.method === "GET") {
|
|
2395
|
+
await handleReadPage(res, url.searchParams);
|
|
2396
|
+
}
|
|
2397
|
+
else if (path === "/api/shortcut" && req.method === "POST") {
|
|
2398
|
+
const body = await readBody(req);
|
|
2399
|
+
await handleShortcut(res, body);
|
|
2400
|
+
}
|
|
2401
|
+
else if (path === "/api/wait-for-idle" && req.method === "POST") {
|
|
2402
|
+
const body = await readBody(req);
|
|
2403
|
+
await handleWaitForIdle(res, body);
|
|
2404
|
+
// Parity tools (require session)
|
|
2405
|
+
}
|
|
2406
|
+
else if (path === "/api/pdf" && req.method === "POST") {
|
|
2407
|
+
const body = await readBody(req);
|
|
2408
|
+
await handlePdf(res, body);
|
|
2409
|
+
}
|
|
2410
|
+
else if (path === "/api/scrape" && req.method === "POST") {
|
|
2411
|
+
const body = await readBody(req);
|
|
2412
|
+
await handleScrape(res, body);
|
|
2413
|
+
}
|
|
2414
|
+
else if (path === "/api/network" && req.method === "POST") {
|
|
2415
|
+
const body = await readBody(req);
|
|
2416
|
+
await handleNetwork(res, body);
|
|
2417
|
+
}
|
|
2418
|
+
else if (path === "/api/automate" && req.method === "POST") {
|
|
2419
|
+
const body = await readBody(req);
|
|
2420
|
+
await handleAutomate(res, body);
|
|
2421
|
+
}
|
|
2422
|
+
else if (path === "/api/domain" && req.method === "POST") {
|
|
2423
|
+
const body = await readBody(req);
|
|
2424
|
+
await handleDomain(res, body);
|
|
2425
|
+
}
|
|
437
2426
|
else {
|
|
438
2427
|
errorJson(res, `Not found: ${req.method} ${path}`, 404);
|
|
439
2428
|
}
|
|
440
2429
|
}
|
|
441
2430
|
catch (err) {
|
|
2431
|
+
if (writeBoundError(res, err))
|
|
2432
|
+
return;
|
|
442
2433
|
const message = err instanceof Error ? err.message : String(err);
|
|
443
2434
|
console.error(`[${new Date().toISOString()}] Error on ${req.method} ${path}:`, message);
|
|
444
2435
|
errorJson(res, message, 500);
|
|
@@ -447,13 +2438,42 @@ const server = createServer(async (req, res) => {
|
|
|
447
2438
|
server.listen(PORT, () => {
|
|
448
2439
|
console.log(`Comet Bridge HTTP API listening on port ${PORT}`);
|
|
449
2440
|
console.log(`Health check: http://localhost:${PORT}/api/health`);
|
|
450
|
-
console.log(`\nEndpoints:`);
|
|
451
|
-
console.log(
|
|
452
|
-
console.log(` POST /api/
|
|
453
|
-
console.log(`
|
|
454
|
-
console.log(`
|
|
455
|
-
console.log(`
|
|
456
|
-
console.log(`
|
|
2441
|
+
console.log(`\nEndpoints (31 total):`);
|
|
2442
|
+
console.log(`\n --- Connection & Querying ---`);
|
|
2443
|
+
console.log(` POST /api/connect - Start Comet & connect {taskGoal?}`);
|
|
2444
|
+
console.log(` POST /api/ask - Send prompt {prompt, newChat?, timeout?}`);
|
|
2445
|
+
console.log(` GET /api/equanaut/router/status - Probe Equanaut router capabilities`);
|
|
2446
|
+
console.log(` POST /api/equanaut/router/ask - Route sidepanel envelope through gateway`);
|
|
2447
|
+
console.log(` GET /api/poll - Check agent status`);
|
|
2448
|
+
console.log(` POST /api/stop - Stop current agent`);
|
|
2449
|
+
console.log(` GET /api/screenshot - Capture page screenshot`);
|
|
2450
|
+
console.log(` POST /api/mode - Get/set Perplexity mode {mode?}`);
|
|
2451
|
+
console.log(` POST /api/shortcut - Trigger query shortcut {shortcut, context?, timeout?}`);
|
|
2452
|
+
console.log(`\n --- Navigation & Interaction (require session) ---`);
|
|
2453
|
+
console.log(` POST /api/navigate - Navigate to URL {url, waitForIdle?}`);
|
|
2454
|
+
console.log(` POST /api/interact - CDP interactions {actions: Action[]}`);
|
|
2455
|
+
console.log(` GET /api/read-page - Read page content ?mode=text|tree|both`);
|
|
2456
|
+
console.log(` POST /api/wait-for-idle - Wait for network idle {idleTime?, timeout?}`);
|
|
2457
|
+
console.log(`\n --- Safe Observation (no session required) ---`);
|
|
2458
|
+
console.log(` GET /api/observe - Browser state ?action=health|snapshot|status|detail`);
|
|
2459
|
+
console.log(` GET /api/peek - Read tab by target ID ?targetId=&action=info|screenshot|read`);
|
|
2460
|
+
console.log(` GET /api/agents - Agent registry snapshot`);
|
|
2461
|
+
console.log(`\n --- Lifecycle (no session required) ---`);
|
|
2462
|
+
console.log(` POST /api/lifecycle/start - Register run {runId, taskThreadId, agentId?, route?}`);
|
|
2463
|
+
console.log(` POST /api/lifecycle/complete - Complete run {runId}`);
|
|
2464
|
+
console.log(` POST /api/lifecycle/abort - Abort run {runId, reason?}`);
|
|
2465
|
+
console.log(` POST /api/lifecycle/update - Update run metadata {runId, tabGroupId?, workflowId?}`);
|
|
2466
|
+
console.log(`\n --- Orchestration ---`);
|
|
2467
|
+
console.log(` GET /api/task-status - Unified task status ?groupId=|threadId=`);
|
|
2468
|
+
console.log(` POST /api/delegate - Dispatch browser task {threadId, instruction, priority?}`);
|
|
2469
|
+
console.log(` GET /api/session/:id - Session manifest lookup by taskThreadId`);
|
|
2470
|
+
console.log(`\n --- Parity Tools (require session) ---`);
|
|
2471
|
+
console.log(` POST /api/pdf - Generate PDF {url?, format?, landscape?, ...}`);
|
|
2472
|
+
console.log(` POST /api/scrape - Extract structured data {mode?, selector?, ...}`);
|
|
2473
|
+
console.log(` POST /api/network - Network traffic {action?, duration?, filter?, ...}`);
|
|
2474
|
+
console.log(` POST /api/automate - Multi-step workflow {steps: Step[]}`);
|
|
2475
|
+
console.log(` POST /api/domain - Domain playbook {domain, action?, path?}`);
|
|
2476
|
+
console.log(`\n --- Tab Groups ---`);
|
|
457
2477
|
console.log(` GET /api/tab-groups - List all tab groups`);
|
|
458
2478
|
console.log(` GET /api/tab-groups/tabs - List all tabs with group info`);
|
|
459
2479
|
console.log(` POST /api/tab-groups - Create group {tabIds, title?, color?}`);
|