@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/index.js
CHANGED
|
@@ -1,20 +1,252 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Comet Browser MCP Server
|
|
3
3
|
// Claude Code ↔ Perplexity Comet bidirectional interaction
|
|
4
|
-
//
|
|
4
|
+
// 25 tools: 9 browsing + 2 direct interaction + 1 tab groups + 4 lifecycle + 2 orchestration + 5 parity + 2 safe observe (observe + peek)
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { cometClient } from "./cdp-client.js";
|
|
9
|
-
import { cometAI } from "./comet-ai.js";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
8
|
+
import { cometClient as globalCometClient } from "./cdp-client.js";
|
|
9
|
+
import { CometAI, cometAI as globalCometAI } from "./comet-ai.js";
|
|
10
|
+
import { sessionRegistry } from "./session-registry.js";
|
|
11
|
+
import { drainMcpAlertQueue, formatAlertsForResponse, dispatchAlert } from "./alert-dispatcher.js";
|
|
12
|
+
import { loadBridgeConfig } from "./bridge-config.js";
|
|
13
|
+
import { BoundSessionError, resolveBoundSession } from "./bound-session.js";
|
|
14
|
+
import { sidecarArtifactStore } from "./sidecar-artifacts.js";
|
|
15
|
+
import { windowBindingStore } from "./window-bindings.js";
|
|
16
|
+
import { createOrReuseDelegateBinding } from "./delegate-binding.js";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the current session's CDP client and CometAI, or fall back to globals.
|
|
19
|
+
* This ensures each agent uses its own isolated tab connection.
|
|
20
|
+
* Also runs a health check and reconnects if the WebSocket is dead.
|
|
21
|
+
*/
|
|
22
|
+
async function getSessionClients() {
|
|
23
|
+
const session = sessionRegistry.getCurrent();
|
|
24
|
+
const client = session?.cdpClient ?? globalCometClient;
|
|
25
|
+
const ai = session?.cometAI ?? globalCometAI;
|
|
26
|
+
// Proactive health check — catches dead WebSockets before tool calls fail
|
|
27
|
+
if (client.isConnected) {
|
|
28
|
+
await client.ensureHealthyConnection();
|
|
29
|
+
}
|
|
30
|
+
return { client, ai };
|
|
31
|
+
}
|
|
32
|
+
async function disconnectCdpClientsAfterTool() {
|
|
33
|
+
const clients = new Set();
|
|
34
|
+
const session = sessionRegistry.getCurrent();
|
|
35
|
+
if (session?.cdpClient)
|
|
36
|
+
clients.add(session.cdpClient);
|
|
37
|
+
clients.add(globalCometClient);
|
|
38
|
+
for (const client of clients) {
|
|
39
|
+
if (!client.isConnected)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
await client.disconnect();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.warn(`[comet-bridge] CDP post-tool disconnect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Protocol enforcement (Spec 016, FR-006, T009-T012)
|
|
50
|
+
// Tool safety classification (Spec 034 — MCP Tool Parity, session safety)
|
|
51
|
+
// SAFE_OBSERVE: No session required, read-only, zero side effects
|
|
52
|
+
// BROWSING: Requires active session (comet_connect first), may modify browser state
|
|
53
|
+
// LIFECYCLE/ORCHESTRATION: No session required, infrastructure-level
|
|
54
|
+
const SAFE_OBSERVE_TOOLS = new Set([
|
|
55
|
+
"comet_observe", // Read-only: /json/list + extension bridge + agent-registry
|
|
56
|
+
]);
|
|
57
|
+
const BROWSING_TOOLS = new Set([
|
|
58
|
+
"comet_ask",
|
|
59
|
+
"comet_poll",
|
|
60
|
+
"comet_stop",
|
|
61
|
+
"comet_screenshot",
|
|
62
|
+
"comet_mode",
|
|
63
|
+
"comet_shortcut",
|
|
64
|
+
"comet_read_page",
|
|
65
|
+
"comet_wait_for_idle",
|
|
66
|
+
"comet_interact",
|
|
67
|
+
"comet_navigate",
|
|
68
|
+
"comet_pdf",
|
|
69
|
+
"comet_scrape",
|
|
70
|
+
"comet_network",
|
|
71
|
+
"comet_automate",
|
|
72
|
+
"comet_domain",
|
|
73
|
+
]);
|
|
74
|
+
const BOUND_ROUTING_TOOLS = new Set([
|
|
75
|
+
"comet_ask",
|
|
76
|
+
"comet_poll",
|
|
77
|
+
"comet_stop",
|
|
78
|
+
"comet_screenshot",
|
|
79
|
+
"comet_mode",
|
|
80
|
+
"comet_shortcut",
|
|
81
|
+
"comet_read_page",
|
|
82
|
+
"comet_wait_for_idle",
|
|
83
|
+
"comet_interact",
|
|
84
|
+
"comet_navigate",
|
|
85
|
+
"comet_task_status",
|
|
86
|
+
"comet_peek",
|
|
87
|
+
"comet_pdf",
|
|
88
|
+
"comet_scrape",
|
|
89
|
+
"comet_network",
|
|
90
|
+
"comet_automate",
|
|
91
|
+
"comet_domain",
|
|
92
|
+
]);
|
|
93
|
+
// Tools with DESTRUCTIVE side effects — these are flagged in documentation
|
|
94
|
+
// as "use with caution" because they modify browser state beyond the calling agent's scope
|
|
95
|
+
const DESTRUCTIVE_ACTIONS = new Set([
|
|
96
|
+
"comet_stop", // Terminates running agent tasks
|
|
97
|
+
"comet_navigate", // Changes the active tab's URL
|
|
98
|
+
]);
|
|
99
|
+
const tabGroupWarnedSessions = new Set();
|
|
100
|
+
function requireSession(toolName, args) {
|
|
101
|
+
const config = loadBridgeConfig();
|
|
102
|
+
if (!config.protocolEnforcement.requireConnectBeforeBrowse)
|
|
103
|
+
return null;
|
|
104
|
+
// Safe observe tools never require session
|
|
105
|
+
if (SAFE_OBSERVE_TOOLS.has(toolName))
|
|
106
|
+
return null;
|
|
107
|
+
// comet_tab_groups with action="list" or "list_tabs" is read-only — no session needed
|
|
108
|
+
if (toolName === "comet_tab_groups") {
|
|
109
|
+
const action = args?.action;
|
|
110
|
+
if (action === "list" || action === "list_tabs" || action === "list_archived") {
|
|
111
|
+
return null; // Read-only tab group queries bypass session requirement
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!BROWSING_TOOLS.has(toolName))
|
|
115
|
+
return null;
|
|
116
|
+
const session = sessionRegistry.getCurrent();
|
|
117
|
+
if (!session) {
|
|
118
|
+
dispatchAlert({
|
|
119
|
+
type: "PROTOCOL_VIOLATION",
|
|
120
|
+
message: `Tool "${toolName}" called without active session. Call comet_connect first.`,
|
|
121
|
+
context: { toolName },
|
|
122
|
+
});
|
|
123
|
+
// Suggest safe alternatives
|
|
124
|
+
const safeAlternative = toolName === "comet_screenshot" || toolName === "comet_read_page"
|
|
125
|
+
? `\n\nSafe alternative: Use comet_peek(targetId) to read/screenshot any tab without a session.\nOr use comet_observe(action='snapshot') to see all tabs and groups.`
|
|
126
|
+
: toolName === "comet_tab_groups"
|
|
127
|
+
? `\n\nSafe alternative: comet_tab_groups(action='list') works without a session for read-only queries.`
|
|
128
|
+
: "";
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Protocol error: No active connection. Call comet_connect first.\n\nRequired sequence:\n1. comet_connect (establish CDP session with agent identity)\n2. ${toolName} (your requested tool)\n\nSuggested action: Call comet_connect with agentId and taskThreadId parameters.${safeAlternative}`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
async function requireBoundRouting(toolName, args) {
|
|
142
|
+
if (!BOUND_ROUTING_TOOLS.has(toolName))
|
|
143
|
+
return null;
|
|
144
|
+
try {
|
|
145
|
+
const resolved = await resolveBoundSession(sessionRegistry.getCurrent(), args);
|
|
146
|
+
const session = sessionRegistry.getCurrent();
|
|
147
|
+
if (resolved.binding.targetId && session) {
|
|
148
|
+
await session?.cdpClient.connect(resolved.binding.targetId);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof BoundSessionError) {
|
|
154
|
+
dispatchAlert({
|
|
155
|
+
type: "PROTOCOL_VIOLATION",
|
|
156
|
+
message: err.message,
|
|
157
|
+
context: { toolName, code: err.code, repairAction: err.repairAction },
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: err.toMcpText(toolName) }],
|
|
161
|
+
isError: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function getTabGroupWarning(toolName) {
|
|
168
|
+
const config = loadBridgeConfig();
|
|
169
|
+
if (!config.protocolEnforcement.warnMissingTabGroup)
|
|
170
|
+
return "";
|
|
171
|
+
const session = sessionRegistry.getCurrent();
|
|
172
|
+
if (!session || session.tabGroupId !== null)
|
|
173
|
+
return "";
|
|
174
|
+
if (tabGroupWarnedSessions.has(session.sessionKey))
|
|
175
|
+
return "";
|
|
176
|
+
tabGroupWarnedSessions.add(session.sessionKey);
|
|
177
|
+
return `\n\n⚠️ Warning: No tab group assigned for this session. For multi-agent safety, create a tab group with comet_tab_groups(action='create', title='${session.taskThreadId}').`;
|
|
178
|
+
}
|
|
179
|
+
function assertAgentRuntimeProfileArg(profile) {
|
|
180
|
+
if (profile === undefined ||
|
|
181
|
+
profile === null ||
|
|
182
|
+
profile === "" ||
|
|
183
|
+
profile === "agent" ||
|
|
184
|
+
profile === "oe") {
|
|
185
|
+
return profile === "agent" ? "oe" : profile;
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`PROFILE_OWNERSHIP_VIOLATION: profile ${String(profile)} is not an agent-owned Comet runtime profile`);
|
|
188
|
+
}
|
|
189
|
+
// Safe CSS selector escaping — prevents injection via user-supplied selectors (Spec 034 review)
|
|
190
|
+
function safeSelector(sel) {
|
|
191
|
+
return JSON.stringify(sel);
|
|
192
|
+
}
|
|
193
|
+
// Validate variable name is a safe JS identifier (Spec 034 review — prevents variable name injection)
|
|
194
|
+
function isValidIdentifier(name) {
|
|
195
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
196
|
+
}
|
|
197
|
+
// prettier-ignore
|
|
198
|
+
// @ts-ignore — .mjs adapter for lifecycle event parity (Spec 078 AC-2).
|
|
199
|
+
// Vendored from ../../scripts/ into ./vendor/ by the prebuild step so the
|
|
200
|
+
// published npm package is self-contained (see package.json "prebuild").
|
|
201
|
+
import { emitLifecycleEvent, createMCPLifecycleEnvelope } from "../vendor/lifecycle-mcp-adapter.mjs";
|
|
202
|
+
import { appendFileSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
15
203
|
import { homedir } from "os";
|
|
16
|
-
import { join } from "path";
|
|
17
|
-
|
|
204
|
+
import { join, dirname } from "path";
|
|
205
|
+
// Duplicate process detection (Spec 016, FR-010, T005)
|
|
206
|
+
const PID_FILE_PATH = join(homedir(), ".claude", "comet-browser", "comet-mcp.pid");
|
|
207
|
+
function checkDuplicateProcess() {
|
|
208
|
+
try {
|
|
209
|
+
const existingPid = readFileSync(PID_FILE_PATH, "utf-8").trim();
|
|
210
|
+
if (existingPid) {
|
|
211
|
+
const existingPidNumber = parseInt(existingPid, 10);
|
|
212
|
+
if (existingPidNumber === process.pid)
|
|
213
|
+
return;
|
|
214
|
+
// Check if the process is still alive
|
|
215
|
+
let processAlive = false;
|
|
216
|
+
try {
|
|
217
|
+
process.kill(existingPidNumber, 0); // signal 0 = existence check
|
|
218
|
+
processAlive = true;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Process is dead — stale PID file, safe to overwrite
|
|
222
|
+
}
|
|
223
|
+
if (processAlive) {
|
|
224
|
+
// Process is alive — fail closed so multiple controllers cannot compete.
|
|
225
|
+
dispatchAlert({
|
|
226
|
+
type: "DUPLICATE_PROCESS",
|
|
227
|
+
message: `Another comet-mcp process is running (PID ${existingPidNumber}). Current PID: ${process.pid}. Refusing to start a competing controller.`,
|
|
228
|
+
context: { existingPid: existingPidNumber, currentPid: process.pid },
|
|
229
|
+
});
|
|
230
|
+
throw new Error(`DUPLICATE_PROCESS: comet-mcp PID ${existingPidNumber} already owns ${PID_FILE_PATH}; refusing to start PID ${process.pid}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof Error && err.message.startsWith("DUPLICATE_PROCESS:")) {
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
// No PID file exists — first run
|
|
239
|
+
}
|
|
240
|
+
// Write our PID
|
|
241
|
+
try {
|
|
242
|
+
mkdirSync(dirname(PID_FILE_PATH), { recursive: true });
|
|
243
|
+
writeFileSync(PID_FILE_PATH, String(process.pid));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Best effort
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
checkDuplicateProcess();
|
|
18
250
|
// JSONL outbox for lifecycle events → orchestration layer
|
|
19
251
|
const OUTBOX_PATH = join(homedir(), "equabot", "agent-chat", "outbox-comet.jsonl");
|
|
20
252
|
const INBOX_PATH = join(homedir(), "equabot", "agent-chat", "inbox-comet.jsonl");
|
|
@@ -23,31 +255,138 @@ function appendJsonl(path, obj) {
|
|
|
23
255
|
try {
|
|
24
256
|
appendFileSync(path, JSON.stringify(obj) + "\n");
|
|
25
257
|
}
|
|
26
|
-
catch {
|
|
258
|
+
catch (err) {
|
|
259
|
+
console.warn(`[comet-bridge] JSONL write failed to ${path}: ${err instanceof Error ? err.message : err}`);
|
|
260
|
+
}
|
|
27
261
|
}
|
|
28
262
|
function readJsonSafe(path) {
|
|
29
263
|
try {
|
|
30
264
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
31
265
|
}
|
|
32
|
-
catch {
|
|
266
|
+
catch (err) {
|
|
267
|
+
if (err?.code !== "ENOENT") {
|
|
268
|
+
console.warn(`[comet-bridge] JSON read/parse failed for ${path}: ${err instanceof Error ? err.message : err}`);
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function attachRunIdToCurrentBinding(runId, bindingIdArg) {
|
|
274
|
+
if (!runId)
|
|
33
275
|
return null;
|
|
276
|
+
const session = sessionRegistry.getCurrent();
|
|
277
|
+
const bindingId = bindingIdArg ?? session?.codexBinding?.bindingId;
|
|
278
|
+
if (!bindingId)
|
|
279
|
+
return null;
|
|
280
|
+
const binding = await windowBindingStore.addRunId(bindingId, runId);
|
|
281
|
+
if (session?.codexBinding?.bindingId === binding.bindingId) {
|
|
282
|
+
session.codexBinding = binding;
|
|
34
283
|
}
|
|
284
|
+
return binding;
|
|
285
|
+
}
|
|
286
|
+
async function transitionBindingByRunId(runId, status) {
|
|
287
|
+
if (!runId)
|
|
288
|
+
return null;
|
|
289
|
+
return windowBindingStore.transitionByRunId(runId, status);
|
|
35
290
|
}
|
|
36
291
|
const TOOLS = [
|
|
37
292
|
{
|
|
38
293
|
name: "comet_connect",
|
|
39
|
-
description: "Connect to Comet browser (auto-starts if needed)"
|
|
40
|
-
|
|
294
|
+
description: "Connect to Comet browser (auto-starts if needed). Creates a NEW tab and tab group " +
|
|
295
|
+
"for this agent session — never closes existing tabs or interferes with other agents. " +
|
|
296
|
+
"Always positions browser at full-screen bounds on the top display (agents workspace) while keeping browser tabs visible.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
taskGoal: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "REQUIRED. Describe the purpose of this browser session. Used to name the session and initialize the orchestrator thread.",
|
|
303
|
+
},
|
|
304
|
+
agentId: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Agent identity for tracking (e.g. 'claude-code-main'). Used in tab group name.",
|
|
307
|
+
},
|
|
308
|
+
taskThreadId: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Task thread identifier. Tab group is named after this.",
|
|
311
|
+
},
|
|
312
|
+
codexSessionId: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Codex runtime/session identifier for durable Comet window binding.",
|
|
315
|
+
},
|
|
316
|
+
projectThreadId: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description: "Codex project-thread identifier that owns one active Comet window binding.",
|
|
319
|
+
},
|
|
320
|
+
projectThreadFamily: {
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "Optional worktree/project-thread family for worktree orchestrator scope.",
|
|
323
|
+
},
|
|
324
|
+
worktreePath: {
|
|
325
|
+
type: "string",
|
|
326
|
+
description: "Absolute Codex worktree path used for binding ownership and scope checks.",
|
|
327
|
+
},
|
|
328
|
+
repoSlug: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "Repository slug for the Codex session, for example EQUAStart/equa-comet-browser-control.",
|
|
331
|
+
},
|
|
332
|
+
branchName: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "Git branch name for the Codex session.",
|
|
335
|
+
},
|
|
336
|
+
codexSessionRole: {
|
|
337
|
+
type: "string",
|
|
338
|
+
enum: ["session_agent", "worktree_orchestrator", "fleet_orchestrator"],
|
|
339
|
+
description: "Binding authorization role for this Codex session.",
|
|
340
|
+
},
|
|
341
|
+
codexSessionKey: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "Optional explicit Codex binding session key. Defaults to codexSessionId:projectThreadId.",
|
|
344
|
+
},
|
|
345
|
+
strictCodexIdentity: {
|
|
346
|
+
type: "boolean",
|
|
347
|
+
description: "Fail closed when required Codex identity fields cannot be supplied or derived.",
|
|
348
|
+
},
|
|
349
|
+
profile: {
|
|
350
|
+
type: "string",
|
|
351
|
+
enum: ["agent", "oe"],
|
|
352
|
+
description: "Agent-owned Comet runtime browser profile. Human-owned profiles such as moon are never valid mutation targets.",
|
|
353
|
+
},
|
|
354
|
+
tabGroupColor: {
|
|
355
|
+
type: "string",
|
|
356
|
+
enum: ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"],
|
|
357
|
+
description: "Color for the new tab group (default: blue)",
|
|
358
|
+
},
|
|
359
|
+
url: {
|
|
360
|
+
type: "string",
|
|
361
|
+
description: "Initial URL to open (default: https://www.perplexity.ai/)",
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
required: ["taskGoal"],
|
|
365
|
+
},
|
|
41
366
|
},
|
|
42
367
|
{
|
|
43
368
|
name: "comet_ask",
|
|
44
|
-
description: "Send a prompt to Comet/Perplexity and wait for the complete response (blocking). Ideal for tasks requiring real browser interaction (login walls, dynamic content, filling forms) or deep research with agentic browsing."
|
|
369
|
+
description: "Send a prompt to Comet/Perplexity and wait for the complete response (blocking). Ideal for tasks requiring real browser interaction (login walls, dynamic content, filling forms) or deep research with agentic browsing. " +
|
|
370
|
+
"Use sidecar: true to keep the current page visible and ask via the Perplexity Assistant sidebar — best for interacting with the active tab (clicking, form filling, reading page content).",
|
|
45
371
|
inputSchema: {
|
|
46
372
|
type: "object",
|
|
47
373
|
properties: {
|
|
48
|
-
prompt: {
|
|
374
|
+
prompt: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Question or task for Comet - focus on goals and context",
|
|
377
|
+
},
|
|
49
378
|
newChat: { type: "boolean", description: "Start a fresh conversation (default: false)" },
|
|
50
379
|
timeout: { type: "number", description: "Max wait time in ms (default: 15000 = 15s)" },
|
|
380
|
+
sidecar: {
|
|
381
|
+
type: "boolean",
|
|
382
|
+
description: "Use the Perplexity Assistant sidecar instead of navigating to perplexity.ai. " +
|
|
383
|
+
"Uses an already-open assistant sidebar without activating Comet or sending OS keyboard shortcuts. " +
|
|
384
|
+
"Best for tasks that interact with the active tab: clicking buttons, filling forms, reading page content. (default: false)",
|
|
385
|
+
},
|
|
386
|
+
sidecarResultId: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "Optional existing sidecar result artifact ID to refresh or correlate.",
|
|
389
|
+
},
|
|
51
390
|
},
|
|
52
391
|
required: ["prompt"],
|
|
53
392
|
},
|
|
@@ -55,7 +394,19 @@ const TOOLS = [
|
|
|
55
394
|
{
|
|
56
395
|
name: "comet_poll",
|
|
57
396
|
description: "Check agent status and progress. Call repeatedly to monitor agentic tasks.",
|
|
58
|
-
inputSchema: {
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
sidecarResultId: {
|
|
401
|
+
type: "string",
|
|
402
|
+
description: "Return a persisted sidecar result artifact by ID.",
|
|
403
|
+
},
|
|
404
|
+
sidecarContextKey: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Return the latest persisted sidecar result for this binding context.",
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
59
410
|
},
|
|
60
411
|
{
|
|
61
412
|
name: "comet_stop",
|
|
@@ -93,7 +444,19 @@ const TOOLS = [
|
|
|
93
444
|
properties: {
|
|
94
445
|
action: {
|
|
95
446
|
type: "string",
|
|
96
|
-
enum: [
|
|
447
|
+
enum: [
|
|
448
|
+
"list",
|
|
449
|
+
"list_tabs",
|
|
450
|
+
"create",
|
|
451
|
+
"update",
|
|
452
|
+
"move",
|
|
453
|
+
"ungroup",
|
|
454
|
+
"delete",
|
|
455
|
+
"save_group",
|
|
456
|
+
"restore_group",
|
|
457
|
+
"archive_group",
|
|
458
|
+
"list_archived",
|
|
459
|
+
],
|
|
97
460
|
description: "The tab group operation to perform",
|
|
98
461
|
},
|
|
99
462
|
tabIds: {
|
|
@@ -136,16 +499,16 @@ const TOOLS = [
|
|
|
136
499
|
},
|
|
137
500
|
{
|
|
138
501
|
name: "comet_shortcut",
|
|
139
|
-
description: "Trigger a Comet Query Shortcut (e.g. /fact-check, /mailtodo). "
|
|
140
|
-
|
|
141
|
-
|
|
502
|
+
description: "Trigger a Comet Query Shortcut (e.g. /fact-check, /mailtodo). " +
|
|
503
|
+
"These are reusable AI prompts with pre-configured modes and sources. " +
|
|
504
|
+
"Type '/' to discover available shortcuts.",
|
|
142
505
|
inputSchema: {
|
|
143
506
|
type: "object",
|
|
144
507
|
properties: {
|
|
145
508
|
shortcut: {
|
|
146
509
|
type: "string",
|
|
147
|
-
description: "Shortcut name (e.g. 'fact-check', 'mailtodo', 'prep-next-meeting'). "
|
|
148
|
-
|
|
510
|
+
description: "Shortcut name (e.g. 'fact-check', 'mailtodo', 'prep-next-meeting'). " +
|
|
511
|
+
"Omit the leading slash — it will be added automatically.",
|
|
149
512
|
},
|
|
150
513
|
context: {
|
|
151
514
|
type: "string",
|
|
@@ -161,17 +524,17 @@ const TOOLS = [
|
|
|
161
524
|
},
|
|
162
525
|
{
|
|
163
526
|
name: "comet_read_page",
|
|
164
|
-
description: "Extract content from the current page. Returns structured accessibility tree "
|
|
165
|
-
|
|
166
|
-
|
|
527
|
+
description: "Extract content from the current page. Returns structured accessibility tree " +
|
|
528
|
+
"and/or clean text. Use 'tree' mode for interactive elements (buttons, links, forms), " +
|
|
529
|
+
"'text' mode for readable content, or 'both' for full extraction.",
|
|
167
530
|
inputSchema: {
|
|
168
531
|
type: "object",
|
|
169
532
|
properties: {
|
|
170
533
|
mode: {
|
|
171
534
|
type: "string",
|
|
172
535
|
enum: ["tree", "text", "both"],
|
|
173
|
-
description: "Extraction mode: 'tree' = accessibility tree (roles, names, values), "
|
|
174
|
-
|
|
536
|
+
description: "Extraction mode: 'tree' = accessibility tree (roles, names, values), " +
|
|
537
|
+
"'text' = clean markdown-like text, 'both' = both formats. Default: 'text'.",
|
|
175
538
|
},
|
|
176
539
|
maxDepth: {
|
|
177
540
|
type: "number",
|
|
@@ -184,11 +547,105 @@ const TOOLS = [
|
|
|
184
547
|
},
|
|
185
548
|
},
|
|
186
549
|
},
|
|
550
|
+
{
|
|
551
|
+
name: "comet_interact",
|
|
552
|
+
description: "Directly interact with the current page — click, type, check/uncheck, select, scroll, extract text, or run JavaScript. " +
|
|
553
|
+
"NO Perplexity AI involved. Executes actions via CDP on the active tab. " +
|
|
554
|
+
"Use comet_read_page first to find selectors, then comet_interact to act on them. " +
|
|
555
|
+
"Supports multiple sequential actions in one call.",
|
|
556
|
+
inputSchema: {
|
|
557
|
+
type: "object",
|
|
558
|
+
properties: {
|
|
559
|
+
actions: {
|
|
560
|
+
type: "array",
|
|
561
|
+
description: "Ordered list of actions to execute sequentially on the current page.",
|
|
562
|
+
items: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
action: {
|
|
566
|
+
type: "string",
|
|
567
|
+
enum: [
|
|
568
|
+
"click",
|
|
569
|
+
"type",
|
|
570
|
+
"fill",
|
|
571
|
+
"press",
|
|
572
|
+
"check",
|
|
573
|
+
"uncheck",
|
|
574
|
+
"select",
|
|
575
|
+
"scroll",
|
|
576
|
+
"wait",
|
|
577
|
+
"extract",
|
|
578
|
+
"evaluate",
|
|
579
|
+
],
|
|
580
|
+
description: "click: click element | type: keystroke-by-keystroke | fill: clear + set value | " +
|
|
581
|
+
"press: keyboard key (Enter, Tab, Escape, etc.) | check/uncheck: checkbox toggle | " +
|
|
582
|
+
"select: dropdown option | scroll: scroll page | wait: pause (ms) or wait for selector | " +
|
|
583
|
+
"extract: get element text | evaluate: run arbitrary JS",
|
|
584
|
+
},
|
|
585
|
+
selector: {
|
|
586
|
+
type: "string",
|
|
587
|
+
description: "CSS selector for the target element. Supports standard CSS (e.g. '#id', '.class', " +
|
|
588
|
+
"'button[aria-label=\"Submit\"]', 'tr:nth-child(3) td.amount'). " +
|
|
589
|
+
"Use comet_read_page mode='tree' to discover selectors.",
|
|
590
|
+
},
|
|
591
|
+
value: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "Value for type/fill/select actions. For select, use the option value or visible text.",
|
|
594
|
+
},
|
|
595
|
+
key: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description: "Key name for press action (e.g. 'Enter', 'Tab', 'Escape', 'ArrowDown').",
|
|
598
|
+
},
|
|
599
|
+
script: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "JavaScript to execute for evaluate action. Return value is captured.",
|
|
602
|
+
},
|
|
603
|
+
direction: {
|
|
604
|
+
type: "string",
|
|
605
|
+
enum: ["up", "down", "left", "right"],
|
|
606
|
+
description: "Scroll direction (default: 'down').",
|
|
607
|
+
},
|
|
608
|
+
amount: {
|
|
609
|
+
type: "number",
|
|
610
|
+
description: "Scroll amount in pixels (default: 500).",
|
|
611
|
+
},
|
|
612
|
+
ms: {
|
|
613
|
+
type: "number",
|
|
614
|
+
description: "Wait duration in milliseconds (for wait action without selector).",
|
|
615
|
+
},
|
|
616
|
+
optional: {
|
|
617
|
+
type: "boolean",
|
|
618
|
+
description: "If true, failure of this action won't abort the remaining actions (default: false).",
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
required: ["action"],
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
required: ["actions"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "comet_navigate",
|
|
630
|
+
description: "Navigate the current tab to a URL. Does NOT open Perplexity — navigates the active tab directly. " +
|
|
631
|
+
"Use this to go to QBO, Mercury, Google Drive, or any URL before using comet_interact or comet_read_page.",
|
|
632
|
+
inputSchema: {
|
|
633
|
+
type: "object",
|
|
634
|
+
properties: {
|
|
635
|
+
url: { type: "string", description: "URL to navigate to." },
|
|
636
|
+
waitForIdle: {
|
|
637
|
+
type: "boolean",
|
|
638
|
+
description: "Wait for network idle after navigation (default: true).",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["url"],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
187
644
|
{
|
|
188
645
|
name: "comet_wait_for_idle",
|
|
189
|
-
description: "Wait for the current page's network activity to settle (no pending requests "
|
|
190
|
-
|
|
191
|
-
|
|
646
|
+
description: "Wait for the current page's network activity to settle (no pending requests " +
|
|
647
|
+
"for a specified duration). Use after navigation or triggering dynamic content loads. " +
|
|
648
|
+
"Returns a summary of network activity observed.",
|
|
192
649
|
inputSchema: {
|
|
193
650
|
type: "object",
|
|
194
651
|
properties: {
|
|
@@ -213,6 +670,7 @@ const TOOLS = [
|
|
|
213
670
|
runId: { type: "string", description: "Unique run identifier" },
|
|
214
671
|
taskThreadId: { type: "string", description: "Task-thread identifier" },
|
|
215
672
|
agentId: { type: "string", description: "Agent identity (optional for single-agent)" },
|
|
673
|
+
bindingId: { type: "string", description: "Codex window binding to attach this run to" },
|
|
216
674
|
route: { type: "string", enum: ["mcp", "cli", "http"], description: "Execution channel" },
|
|
217
675
|
deferred: { type: "boolean", description: "Start in pending state (default: false)" },
|
|
218
676
|
},
|
|
@@ -261,12 +719,27 @@ const TOOLS = [
|
|
|
261
719
|
name: "comet_task_status",
|
|
262
720
|
description: "Get unified status for a Comet browser task. Combines session-manifest.json state, " +
|
|
263
721
|
"extension ring buffer events, and lifecycle metadata into one response. " +
|
|
264
|
-
"Query by groupId or threadId.",
|
|
722
|
+
"Query by bindingId, sessionKey, projectThreadId, runId, groupId, or threadId.",
|
|
265
723
|
inputSchema: {
|
|
266
724
|
type: "object",
|
|
267
725
|
properties: {
|
|
726
|
+
bindingId: { type: "string", description: "Durable Codex window binding ID to check" },
|
|
727
|
+
sessionKey: { type: "string", description: "Codex session key to check" },
|
|
728
|
+
projectThreadId: { type: "string", description: "Codex project thread ID to check" },
|
|
729
|
+
runId: { type: "string", description: "Lifecycle run ID to check" },
|
|
730
|
+
sidecarResultId: {
|
|
731
|
+
type: "string",
|
|
732
|
+
description: "Persisted sidecar result artifact ID to include",
|
|
733
|
+
},
|
|
734
|
+
sidecarContextKey: {
|
|
735
|
+
type: "string",
|
|
736
|
+
description: "Sidecar binding context key to include latest result for",
|
|
737
|
+
},
|
|
268
738
|
groupId: { type: "number", description: "Tab group ID to check" },
|
|
269
|
-
threadId: {
|
|
739
|
+
threadId: {
|
|
740
|
+
type: "string",
|
|
741
|
+
description: "Legacy task-thread ID to check (returns sessions in this binding scope)",
|
|
742
|
+
},
|
|
270
743
|
},
|
|
271
744
|
},
|
|
272
745
|
},
|
|
@@ -296,13 +769,268 @@ const TOOLS = [
|
|
|
296
769
|
description: "Task IDs this task depends on",
|
|
297
770
|
},
|
|
298
771
|
agentId: { type: "string", description: "Agent identity for tracking" },
|
|
772
|
+
bindingId: { type: "string", description: "Existing binding to reuse for dispatch" },
|
|
773
|
+
windowId: { type: "number", description: "Explicit Comet window ID for direct binding" },
|
|
774
|
+
tabGroupId: { type: "number", description: "Explicit tab group ID for direct binding" },
|
|
775
|
+
groupId: { type: "number", description: "Legacy alias for tabGroupId" },
|
|
776
|
+
targetId: { type: "string", description: "Explicit CDP target ID for direct binding" },
|
|
777
|
+
codexSessionId: { type: "string", description: "Codex caller session ID" },
|
|
778
|
+
projectThreadId: { type: "string", description: "Codex project thread ID" },
|
|
779
|
+
worktreePath: { type: "string", description: "Codex worktree path" },
|
|
780
|
+
repoSlug: { type: "string", description: "Repository slug" },
|
|
781
|
+
branchName: { type: "string", description: "Branch name" },
|
|
782
|
+
sessionKey: { type: "string", description: "Codex session key" },
|
|
783
|
+
codexSessionRole: {
|
|
784
|
+
type: "string",
|
|
785
|
+
enum: ["session_agent", "worktree_orchestrator", "fleet_orchestrator"],
|
|
786
|
+
description: "Codex binding role",
|
|
787
|
+
},
|
|
299
788
|
},
|
|
300
789
|
required: ["threadId", "instruction"],
|
|
301
790
|
},
|
|
302
791
|
},
|
|
792
|
+
{
|
|
793
|
+
name: "comet_observe",
|
|
794
|
+
description: "Passively observe browser state without disrupting active agents. Returns tab groups, " +
|
|
795
|
+
"agent ownership, activity status, and optionally per-tab thumbnails. " +
|
|
796
|
+
"Actions: snapshot (full browser state), status (compact ownership table), " +
|
|
797
|
+
"detail (drill into one group), health (lightweight check).",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
action: {
|
|
802
|
+
type: "string",
|
|
803
|
+
enum: ["snapshot", "status", "detail", "health"],
|
|
804
|
+
description: "snapshot: full browser state (all tabs, groups, agents). " +
|
|
805
|
+
"status: tab group ownership and activity summary. " +
|
|
806
|
+
"detail: deep info on one specific tab group. " +
|
|
807
|
+
"health: lightweight browser health check.",
|
|
808
|
+
},
|
|
809
|
+
group: {
|
|
810
|
+
type: "string",
|
|
811
|
+
description: "Tab group name — required for 'detail' action, optional filter for 'snapshot' and 'status'.",
|
|
812
|
+
},
|
|
813
|
+
agentId: {
|
|
814
|
+
type: "string",
|
|
815
|
+
description: "Filter results to a specific agent ID. Optional for 'snapshot' and 'status'.",
|
|
816
|
+
},
|
|
817
|
+
urlPattern: {
|
|
818
|
+
type: "string",
|
|
819
|
+
description: "Filter tabs by URL pattern (substring match). Optional for 'snapshot' and 'detail'.",
|
|
820
|
+
},
|
|
821
|
+
thumbnails: {
|
|
822
|
+
type: "boolean",
|
|
823
|
+
description: "Include per-tab viewport screenshots. Default false. Adds ~100-200ms per tab.",
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
required: ["action"],
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
// ── Safe observation tool (no session required) ──
|
|
830
|
+
{
|
|
831
|
+
name: "comet_peek",
|
|
832
|
+
description: "Binding-scoped read-only observation of the caller's own bound tab. " +
|
|
833
|
+
"SAFE: Does not create tabs, close tabs, navigate, or modify any state. " +
|
|
834
|
+
"Rejects target IDs outside the caller's authorized Codex window binding.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: "object",
|
|
837
|
+
properties: {
|
|
838
|
+
targetId: {
|
|
839
|
+
type: "string",
|
|
840
|
+
description: "CDP target ID of the tab to peek at. Get this from comet_observe(action='snapshot').",
|
|
841
|
+
},
|
|
842
|
+
action: {
|
|
843
|
+
type: "string",
|
|
844
|
+
enum: ["screenshot", "read", "info"],
|
|
845
|
+
description: "screenshot: capture PNG of the tab. read: extract page text/accessibility tree. info: return URL, title, and load state.",
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
required: ["targetId", "action"],
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
// ── Parity tools (close gaps with CLI scripts) ──
|
|
852
|
+
{
|
|
853
|
+
name: "comet_pdf",
|
|
854
|
+
description: "Generate a PDF from the current page or a URL. Navigates if a URL is provided, " +
|
|
855
|
+
"then uses CDP Page.printToPDF. Returns the file path of the saved PDF.",
|
|
856
|
+
inputSchema: {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {
|
|
859
|
+
url: {
|
|
860
|
+
type: "string",
|
|
861
|
+
description: "URL to navigate to before printing. If omitted, prints the current page.",
|
|
862
|
+
},
|
|
863
|
+
name: {
|
|
864
|
+
type: "string",
|
|
865
|
+
description: "Output filename (without .pdf extension). Defaults to page title or timestamp.",
|
|
866
|
+
},
|
|
867
|
+
format: {
|
|
868
|
+
type: "string",
|
|
869
|
+
enum: ["Letter", "Legal", "A4", "A3", "Tabloid"],
|
|
870
|
+
description: "Paper size. Default: Letter.",
|
|
871
|
+
},
|
|
872
|
+
landscape: { type: "boolean", description: "Landscape orientation. Default: false." },
|
|
873
|
+
margin: { type: "number", description: "Margin in inches. Default: 0.5." },
|
|
874
|
+
scale: { type: "number", description: "Scale factor (0.1–2.0). Default: 1." },
|
|
875
|
+
printBackground: {
|
|
876
|
+
type: "boolean",
|
|
877
|
+
description: "Include background graphics. Default: true.",
|
|
878
|
+
},
|
|
879
|
+
hideSelectors: {
|
|
880
|
+
type: "string",
|
|
881
|
+
description: "Comma-separated CSS selectors to hide before printing.",
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
name: "comet_scrape",
|
|
888
|
+
description: "Extract structured data from the current page or a URL. Supports CSS selector extraction, " +
|
|
889
|
+
"table parsing, JSON-LD extraction, list items, and attribute extraction. " +
|
|
890
|
+
"Optionally auto-scrolls to load lazy content.",
|
|
891
|
+
inputSchema: {
|
|
892
|
+
type: "object",
|
|
893
|
+
properties: {
|
|
894
|
+
url: {
|
|
895
|
+
type: "string",
|
|
896
|
+
description: "URL to navigate to before scraping. If omitted, scrapes the current page.",
|
|
897
|
+
},
|
|
898
|
+
selector: { type: "string", description: "CSS selector to extract content from." },
|
|
899
|
+
mode: {
|
|
900
|
+
type: "string",
|
|
901
|
+
enum: ["text", "table", "json-ld", "list", "attr", "multi"],
|
|
902
|
+
description: "Extraction mode. text: innerText (default). table: parse <table> as JSON rows. json-ld: extract JSON-LD. list: extract <li> items. attr: extract a specific attribute. multi: all matching elements.",
|
|
903
|
+
},
|
|
904
|
+
attr: { type: "string", description: "Attribute name to extract (when mode is 'attr')." },
|
|
905
|
+
scroll: {
|
|
906
|
+
type: "boolean",
|
|
907
|
+
description: "Auto-scroll page to load lazy content before scraping. Default: false.",
|
|
908
|
+
},
|
|
909
|
+
waitFor: { type: "string", description: "CSS selector to wait for before scraping." },
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: "comet_network",
|
|
915
|
+
description: "Capture and analyze network traffic on the current page. Enables CDP Network domain, " +
|
|
916
|
+
"records requests/responses, and optionally intercepts or blocks requests. " +
|
|
917
|
+
"Actions: capture (record traffic), block (block URL patterns), intercept (mock responses).",
|
|
918
|
+
inputSchema: {
|
|
919
|
+
type: "object",
|
|
920
|
+
properties: {
|
|
921
|
+
action: {
|
|
922
|
+
type: "string",
|
|
923
|
+
enum: ["capture", "block", "intercept"],
|
|
924
|
+
description: "capture: record traffic for a duration. block: block URL patterns. intercept: mock responses for URL patterns.",
|
|
925
|
+
},
|
|
926
|
+
url: {
|
|
927
|
+
type: "string",
|
|
928
|
+
description: "URL to navigate to before capturing. If omitted, captures on the current page.",
|
|
929
|
+
},
|
|
930
|
+
duration: { type: "number", description: "Capture duration in ms. Default: 10000." },
|
|
931
|
+
filter: { type: "string", description: "URL substring to filter captured requests." },
|
|
932
|
+
resourceType: {
|
|
933
|
+
type: "string",
|
|
934
|
+
description: "Filter by resource type: XHR, Fetch, Document, Stylesheet, Image, Script, Font.",
|
|
935
|
+
},
|
|
936
|
+
pattern: {
|
|
937
|
+
type: "string",
|
|
938
|
+
description: "URL pattern to block or intercept (substring match).",
|
|
939
|
+
},
|
|
940
|
+
mockResponse: {
|
|
941
|
+
type: "string",
|
|
942
|
+
description: "JSON string to return for intercepted requests.",
|
|
943
|
+
},
|
|
944
|
+
mockStatus: {
|
|
945
|
+
type: "number",
|
|
946
|
+
description: "HTTP status code for mock responses. Default: 200.",
|
|
947
|
+
},
|
|
948
|
+
includeHeaders: {
|
|
949
|
+
type: "boolean",
|
|
950
|
+
description: "Include request/response headers in capture output. Default: false.",
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
required: ["action"],
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: "comet_automate",
|
|
958
|
+
description: "Execute a multi-step browser workflow. Each step is a tool+args object. Supports: " +
|
|
959
|
+
"navigate, click, fill, type, press, select, wait, screenshot, extract (with variable storage), " +
|
|
960
|
+
"assert (verify content), evaluate (run JS), and conditional logic (if/loop). " +
|
|
961
|
+
"Steps execute sequentially; aborts on first failure unless step has optional:true.",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
steps: {
|
|
966
|
+
type: "array",
|
|
967
|
+
items: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
tool: {
|
|
971
|
+
type: "string",
|
|
972
|
+
description: "Step type: navigate, click, fill, type, press, select, wait, screenshot, extract, assert, evaluate, if, loop",
|
|
973
|
+
},
|
|
974
|
+
selector: { type: "string" },
|
|
975
|
+
url: { type: "string" },
|
|
976
|
+
value: { type: "string" },
|
|
977
|
+
variable: {
|
|
978
|
+
type: "string",
|
|
979
|
+
description: "Store extracted value in this variable name.",
|
|
980
|
+
},
|
|
981
|
+
contains: { type: "string", description: "Assert text contains this string." },
|
|
982
|
+
expression: { type: "string", description: "JS expression to evaluate." },
|
|
983
|
+
condition: { type: "string", description: "JS condition for if/loop." },
|
|
984
|
+
then: { type: "array", description: "Steps to run if condition is true." },
|
|
985
|
+
items: {
|
|
986
|
+
type: "string",
|
|
987
|
+
description: "Variable name containing array to loop over.",
|
|
988
|
+
},
|
|
989
|
+
each: { type: "array", description: "Steps to run for each item." },
|
|
990
|
+
optional: {
|
|
991
|
+
type: "boolean",
|
|
992
|
+
description: "If true, failure doesn't abort the workflow.",
|
|
993
|
+
},
|
|
994
|
+
name: { type: "string", description: "Screenshot name." },
|
|
995
|
+
},
|
|
996
|
+
required: ["tool"],
|
|
997
|
+
},
|
|
998
|
+
description: "Array of workflow steps to execute sequentially.",
|
|
999
|
+
},
|
|
1000
|
+
verbose: { type: "boolean", description: "Show detailed output per step. Default: false." },
|
|
1001
|
+
},
|
|
1002
|
+
required: ["steps"],
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: "comet_domain",
|
|
1007
|
+
description: "Route to a domain-specific playbook for authenticated sites. Handles login checks, " +
|
|
1008
|
+
"navigation patterns, and domain-aware interaction for: QBO (QuickBooks Online), " +
|
|
1009
|
+
"Mercury (banking), GitHub, Google (Drive/Sheets/Docs), and SALT (tax). " +
|
|
1010
|
+
"Checks auth status and provides domain-specific guidance.",
|
|
1011
|
+
inputSchema: {
|
|
1012
|
+
type: "object",
|
|
1013
|
+
properties: {
|
|
1014
|
+
domain: {
|
|
1015
|
+
type: "string",
|
|
1016
|
+
enum: ["qbo", "mercury", "github", "google", "salt"],
|
|
1017
|
+
description: "Target domain playbook.",
|
|
1018
|
+
},
|
|
1019
|
+
action: {
|
|
1020
|
+
type: "string",
|
|
1021
|
+
enum: ["check-auth", "navigate", "status"],
|
|
1022
|
+
description: "check-auth: verify logged in. navigate: go to domain home. status: domain session status.",
|
|
1023
|
+
},
|
|
1024
|
+
path: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
description: "Specific path within the domain (e.g., '/reports' for QBO).",
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
required: ["domain"],
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
303
1032
|
];
|
|
304
|
-
const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL
|
|
305
|
-
|| "http://localhost:3001/command-center/api/comet/lifecycle";
|
|
1033
|
+
const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL || "http://localhost:3001/command-center/api/comet/lifecycle";
|
|
306
1034
|
async function callLifecycleEndpoint(payload) {
|
|
307
1035
|
const resp = await fetch(CC_LIFECYCLE_URL, {
|
|
308
1036
|
method: "POST",
|
|
@@ -320,92 +1048,261 @@ const server = new Server({ name: "comet-bridge", version: "2.4.0" }, { capabili
|
|
|
320
1048
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
321
1049
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
322
1050
|
const { name, arguments: args } = request.params;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1051
|
+
// Resolve session-scoped clients for isolation (Spec 034)
|
|
1052
|
+
// Health check runs here — reconnects dead WebSockets before any tool executes
|
|
1053
|
+
const { client: cometClient, ai: cometAI } = await getSessionClients();
|
|
1054
|
+
// Execute handler, then append any queued alerts to the response (Spec 016, FR-011)
|
|
1055
|
+
const result = await (async () => {
|
|
1056
|
+
// Protocol enforcement — check prerequisites before browsing tools (Spec 016, FR-006, T009-T010)
|
|
1057
|
+
const protocolError = requireSession(name, args);
|
|
1058
|
+
if (protocolError)
|
|
1059
|
+
return protocolError;
|
|
1060
|
+
const bindingError = await requireBoundRouting(name, args);
|
|
1061
|
+
if (bindingError)
|
|
1062
|
+
return bindingError;
|
|
1063
|
+
try {
|
|
1064
|
+
switch (name) {
|
|
1065
|
+
case "comet_connect": {
|
|
1066
|
+
// Spec 037: Require taskGoal for session startup gate
|
|
1067
|
+
const taskGoal = args?.taskGoal;
|
|
1068
|
+
if (!taskGoal || taskGoal.trim().length === 0) {
|
|
1069
|
+
return {
|
|
1070
|
+
content: [
|
|
1071
|
+
{
|
|
1072
|
+
type: "text",
|
|
1073
|
+
text: 'Error: taskGoal is required. Describe what this browser session is for.\n\nExample: comet_connect({ taskGoal: "Research Google Drive documents for OTB accounting project" })',
|
|
1074
|
+
},
|
|
1075
|
+
],
|
|
1076
|
+
isError: true,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
// Spec 034: Use SessionRegistry for isolated, safe connections.
|
|
1080
|
+
// NEVER closes existing tabs. NEVER kills the browser.
|
|
1081
|
+
const session = await sessionRegistry.register({
|
|
1082
|
+
agentId: args?.agentId || undefined,
|
|
1083
|
+
taskThreadId: args?.taskThreadId || undefined,
|
|
1084
|
+
url: args?.url || undefined,
|
|
1085
|
+
tabGroupColor: args?.tabGroupColor || undefined,
|
|
1086
|
+
port: 9222,
|
|
1087
|
+
taskGoal: taskGoal.trim(),
|
|
1088
|
+
codexSessionId: args?.codexSessionId || undefined,
|
|
1089
|
+
projectThreadId: args?.projectThreadId || undefined,
|
|
1090
|
+
projectThreadFamily: args?.projectThreadFamily || undefined,
|
|
1091
|
+
worktreePath: args?.worktreePath || undefined,
|
|
1092
|
+
repoSlug: args?.repoSlug || undefined,
|
|
1093
|
+
branchName: args?.branchName || undefined,
|
|
1094
|
+
codexSessionRole: args?.codexSessionRole || undefined,
|
|
1095
|
+
codexSessionKey: args?.codexSessionKey || undefined,
|
|
1096
|
+
strictCodexIdentity: args?.strictCodexIdentity || undefined,
|
|
1097
|
+
profile: assertAgentRuntimeProfileArg(args?.profile),
|
|
1098
|
+
});
|
|
1099
|
+
const groupInfo = session.tabGroupId !== null
|
|
1100
|
+
? `tab group: "${session.taskThreadId.slice(0, 50)}" (id: ${session.tabGroupId}, color: ${session.tabGroupColor})`
|
|
1101
|
+
: "tab group: skipped (extension not available)";
|
|
1102
|
+
// Spec 037: Send task goal as first prompt to create orchestrator thread
|
|
1103
|
+
let orchestratorUrl;
|
|
1104
|
+
try {
|
|
1105
|
+
const orchestratorPrompt = `Take control of the browser. ${taskGoal.trim()}`;
|
|
1106
|
+
await session.cometAI.sendPrompt(orchestratorPrompt);
|
|
1107
|
+
// Poll for URL change (max 10 attempts, 1s apart) instead of hardcoded delay
|
|
1108
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1110
|
+
const evalResult = await session.cdpClient.safeEvaluate("window.location.href");
|
|
1111
|
+
const currentUrl = evalResult?.result?.value;
|
|
1112
|
+
if (currentUrl &&
|
|
1113
|
+
!currentUrl.includes("/b/home") &&
|
|
1114
|
+
currentUrl.includes("perplexity.ai/search/")) {
|
|
1115
|
+
orchestratorUrl = currentUrl;
|
|
1116
|
+
session.orchestratorUrl = orchestratorUrl;
|
|
1117
|
+
sessionRegistry.updateSessionUrl(session.sessionKey, orchestratorUrl);
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
336
1120
|
}
|
|
337
|
-
catch { /* ignore */ }
|
|
338
1121
|
}
|
|
1122
|
+
catch (goalErr) {
|
|
1123
|
+
// Non-fatal: session is usable without orchestrator URL
|
|
1124
|
+
console.warn(`[comet-bridge] Orchestrator thread creation failed: ${goalErr instanceof Error ? goalErr.message : goalErr}`);
|
|
1125
|
+
}
|
|
1126
|
+
// Log to orchestration outbox
|
|
1127
|
+
appendJsonl(OUTBOX_PATH, {
|
|
1128
|
+
ts: Math.floor(Date.now() / 1000),
|
|
1129
|
+
from: session.agentId,
|
|
1130
|
+
to: "orchestration",
|
|
1131
|
+
type: "update",
|
|
1132
|
+
task: session.taskThreadId,
|
|
1133
|
+
msg: `Connected: agent=${session.agentId}, ${groupInfo}, goal="${session.sessionName}"`,
|
|
1134
|
+
});
|
|
1135
|
+
const summary = [
|
|
1136
|
+
`Agent: ${session.agentId}`,
|
|
1137
|
+
`Session: ${session.sessionName || session.taskThreadId}`,
|
|
1138
|
+
`Goal: ${taskGoal.trim()}`,
|
|
1139
|
+
`Thread: ${session.taskThreadId}`,
|
|
1140
|
+
orchestratorUrl ? `Orchestrator: ${orchestratorUrl}` : "Orchestrator: initializing...",
|
|
1141
|
+
groupInfo,
|
|
1142
|
+
`Display: top (agents workspace, full-screen bounds)`,
|
|
1143
|
+
].join("\n");
|
|
1144
|
+
return { content: [{ type: "text", text: summary }] };
|
|
339
1145
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1146
|
+
case "comet_ask": {
|
|
1147
|
+
let prompt = args?.prompt;
|
|
1148
|
+
const timeout = args?.timeout || 15000; // Default 15s, use poll for longer tasks
|
|
1149
|
+
const newChat = args?.newChat || false;
|
|
1150
|
+
// Validate prompt
|
|
1151
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
1152
|
+
return { content: [{ type: "text", text: "Error: prompt cannot be empty" }] };
|
|
1153
|
+
}
|
|
1154
|
+
// Normalize prompt - convert markdown/bullets to natural text
|
|
1155
|
+
prompt = prompt
|
|
1156
|
+
.replace(/^[-*•]\s*/gm, "") // Remove bullet points
|
|
1157
|
+
.replace(/\n+/g, " ") // Collapse newlines to spaces
|
|
1158
|
+
.replace(/\s+/g, " ") // Collapse multiple spaces
|
|
1159
|
+
.trim();
|
|
1160
|
+
const useSidecar = args?.sidecar || false;
|
|
1161
|
+
// ── Sidecar mode: keep current page, open assistant sidebar, ask there ──
|
|
1162
|
+
if (useSidecar) {
|
|
1163
|
+
const boundResolution = await resolveBoundSession(sessionRegistry.getCurrent(), args);
|
|
1164
|
+
// Capture current page URL for context
|
|
1165
|
+
let currentPageUrl = "unknown";
|
|
1166
|
+
try {
|
|
1167
|
+
const urlRes = await cometClient.evaluate("window.location.href");
|
|
1168
|
+
currentPageUrl = urlRes.result.value || "unknown";
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
/* continue */
|
|
1172
|
+
}
|
|
1173
|
+
const sidecarArtifact = await sidecarArtifactStore.create({
|
|
1174
|
+
sidecarContextKey: boundResolution.binding.sidecarContextKey,
|
|
1175
|
+
bindingId: boundResolution.binding.bindingId,
|
|
1176
|
+
sessionKey: boundResolution.binding.sessionKey,
|
|
1177
|
+
projectThreadId: boundResolution.binding.projectThreadId,
|
|
1178
|
+
windowId: boundResolution.binding.windowId,
|
|
1179
|
+
targetId: boundResolution.binding.targetId,
|
|
1180
|
+
prompt,
|
|
1181
|
+
currentPageUrl,
|
|
1182
|
+
});
|
|
1183
|
+
// Attach to an existing sidecar and get a CDP client connected to it.
|
|
1184
|
+
const sidecarClient = await cometClient.connectToSidecar({
|
|
1185
|
+
windowId: boundResolution.binding.windowId,
|
|
1186
|
+
targetId: boundResolution.binding.targetId ?? undefined,
|
|
1187
|
+
});
|
|
1188
|
+
if (!sidecarClient) {
|
|
1189
|
+
await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1190
|
+
status: "failed",
|
|
1191
|
+
error: "Perplexity sidecar is not already open. Focus-safe automation will not activate Comet or send Option+A.",
|
|
1192
|
+
});
|
|
1193
|
+
return {
|
|
1194
|
+
content: [
|
|
1195
|
+
{
|
|
1196
|
+
type: "text",
|
|
1197
|
+
text: "Error: Perplexity sidecar is not already open. Focus-safe automation will not activate Comet or send Option+A through the operator keyboard.",
|
|
1198
|
+
},
|
|
1199
|
+
],
|
|
1200
|
+
isError: true,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
// Create a CometAI instance bound to the sidecar's CDP client
|
|
1205
|
+
const sidecarAI = new CometAI(sidecarClient);
|
|
1206
|
+
// Send prompt to the sidecar input
|
|
1207
|
+
await sidecarAI.sendPrompt(prompt);
|
|
1208
|
+
await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1209
|
+
status: "working",
|
|
1210
|
+
});
|
|
1211
|
+
// Poll sidecar for response completion
|
|
1212
|
+
const startTime = Date.now();
|
|
1213
|
+
const stepsCollected = [];
|
|
1214
|
+
while (Date.now() - startTime < timeout) {
|
|
1215
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1216
|
+
const status = await sidecarAI.getAgentStatus();
|
|
1217
|
+
for (const step of status.steps) {
|
|
1218
|
+
if (!stepsCollected.includes(step)) {
|
|
1219
|
+
stepsCollected.push(step);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (status.status === "completed" && status.response) {
|
|
1223
|
+
await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1224
|
+
status: "completed",
|
|
1225
|
+
response: status.response,
|
|
1226
|
+
steps: stepsCollected,
|
|
1227
|
+
currentStep: status.currentStep,
|
|
1228
|
+
});
|
|
1229
|
+
return {
|
|
1230
|
+
content: [
|
|
1231
|
+
{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: [
|
|
1234
|
+
`[sidecar → ${currentPageUrl}]`,
|
|
1235
|
+
`sidecarContextKey: ${boundResolution.binding.sidecarContextKey}`,
|
|
1236
|
+
`sidecarResultId: ${sidecarArtifact.sidecarResultId}`,
|
|
1237
|
+
"",
|
|
1238
|
+
status.response,
|
|
1239
|
+
].join("\n"),
|
|
1240
|
+
},
|
|
1241
|
+
],
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1245
|
+
status: "working",
|
|
1246
|
+
steps: stepsCollected,
|
|
1247
|
+
currentStep: status.currentStep,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
// Timed out — return in-progress status
|
|
1251
|
+
const finalStatus = await sidecarAI.getAgentStatus();
|
|
1252
|
+
const timedOutArtifact = await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1253
|
+
status: "timed_out",
|
|
1254
|
+
response: finalStatus.response,
|
|
1255
|
+
steps: stepsCollected,
|
|
1256
|
+
currentStep: finalStatus.currentStep,
|
|
1257
|
+
});
|
|
1258
|
+
let msg = `Sidecar task in progress on ${currentPageUrl}\n`;
|
|
1259
|
+
msg += `sidecarContextKey: ${timedOutArtifact.sidecarContextKey}\n`;
|
|
1260
|
+
msg += `sidecarResultId: ${timedOutArtifact.sidecarResultId}\n`;
|
|
1261
|
+
msg += `Status: ${finalStatus.status.toUpperCase()}\n`;
|
|
1262
|
+
if (finalStatus.currentStep)
|
|
1263
|
+
msg += `Current: ${finalStatus.currentStep}\n`;
|
|
1264
|
+
if (stepsCollected.length > 0) {
|
|
1265
|
+
msg += `\nSteps:\n${stepsCollected.map((s) => ` • ${s}`).join("\n")}\n`;
|
|
1266
|
+
}
|
|
1267
|
+
msg += `\nUse comet_poll with sidecarResultId to check progress.`;
|
|
1268
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1269
|
+
}
|
|
1270
|
+
catch (err) {
|
|
1271
|
+
await sidecarArtifactStore.update(sidecarArtifact.sidecarResultId, {
|
|
1272
|
+
status: "failed",
|
|
1273
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1274
|
+
});
|
|
1275
|
+
throw err;
|
|
1276
|
+
}
|
|
1277
|
+
finally {
|
|
377
1278
|
try {
|
|
378
|
-
await
|
|
1279
|
+
await sidecarClient.disconnect();
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
/* already disconnected */
|
|
379
1283
|
}
|
|
380
|
-
catch { /* ignore */ }
|
|
381
1284
|
}
|
|
382
1285
|
}
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
// Navigate to Perplexity home
|
|
390
|
-
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
391
|
-
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
// Not newChat - just ensure we're on Perplexity
|
|
395
|
-
const tabs = await cometClient.listTabsCategorized();
|
|
396
|
-
if (tabs.main) {
|
|
397
|
-
await cometClient.connect(tabs.main.id);
|
|
398
|
-
}
|
|
399
|
-
const urlResult = await cometClient.evaluate('window.location.href');
|
|
400
|
-
const currentUrl = urlResult.result.value;
|
|
401
|
-
const isOnPerplexity = currentUrl?.includes('perplexity.ai');
|
|
402
|
-
if (!isOnPerplexity) {
|
|
1286
|
+
// ── Standard mode: navigate to Perplexity and ask there ──
|
|
1287
|
+
// For newChat: navigate the CURRENT connected tab to Perplexity home
|
|
1288
|
+
// NEVER close other tabs — other agents may be working in them
|
|
1289
|
+
if (newChat) {
|
|
1290
|
+
// Navigate the currently connected tab to Perplexity home
|
|
403
1291
|
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
404
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1292
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
405
1293
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1294
|
+
else {
|
|
1295
|
+
// Not newChat - stay on the bound tab and only navigate that tab if needed.
|
|
1296
|
+
const urlResult = await cometClient.evaluate("window.location.href");
|
|
1297
|
+
const currentUrl = urlResult.result.value;
|
|
1298
|
+
const isOnPerplexity = currentUrl?.includes("perplexity.ai");
|
|
1299
|
+
if (!isOnPerplexity) {
|
|
1300
|
+
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
1301
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
// Capture old response state BEFORE sending prompt (for follow-up detection)
|
|
1305
|
+
const oldStateResult = await cometClient.evaluate(`
|
|
409
1306
|
(() => {
|
|
410
1307
|
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
411
1308
|
const lastProse = proseEls[proseEls.length - 1];
|
|
@@ -415,17 +1312,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
415
1312
|
};
|
|
416
1313
|
})()
|
|
417
1314
|
`);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1315
|
+
const oldState = oldStateResult.result.value;
|
|
1316
|
+
// Send the prompt
|
|
1317
|
+
await cometAI.sendPrompt(prompt);
|
|
1318
|
+
// Wait for completion
|
|
1319
|
+
const startTime = Date.now();
|
|
1320
|
+
const stepsCollected = [];
|
|
1321
|
+
let sawNewResponse = false;
|
|
1322
|
+
while (Date.now() - startTime < timeout) {
|
|
1323
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)); // Poll every 2s
|
|
1324
|
+
// Check if we have a NEW response (more prose elements or different text)
|
|
1325
|
+
const currentStateResult = await cometClient.evaluate(`
|
|
429
1326
|
(() => {
|
|
430
1327
|
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
431
1328
|
const lastProse = proseEls[proseEls.length - 1];
|
|
@@ -435,84 +1332,215 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
435
1332
|
};
|
|
436
1333
|
})()
|
|
437
1334
|
`);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1335
|
+
const currentState = currentStateResult.result.value;
|
|
1336
|
+
// Detect new response
|
|
1337
|
+
if (!sawNewResponse) {
|
|
1338
|
+
if (currentState.count > oldState.count ||
|
|
1339
|
+
(currentState.lastText && currentState.lastText !== oldState.lastText)) {
|
|
1340
|
+
sawNewResponse = true;
|
|
1341
|
+
}
|
|
444
1342
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1343
|
+
const status = await cometAI.getAgentStatus();
|
|
1344
|
+
// Failure detection heuristics (Spec 016, FR-002, T017)
|
|
1345
|
+
if (await cometAI.detectVoiceMode()) {
|
|
1346
|
+
dispatchAlert({
|
|
1347
|
+
type: "PERPLEXITY_VOICE_MODE",
|
|
1348
|
+
message: "Perplexity entered voice/listening mode instead of processing the text prompt.",
|
|
1349
|
+
consumerId: sessionRegistry.getCurrent()?.agentId ?? null,
|
|
1350
|
+
sessionKey: sessionRegistry.getCurrent()?.sessionKey ?? null,
|
|
1351
|
+
});
|
|
1352
|
+
return {
|
|
1353
|
+
content: [
|
|
1354
|
+
{
|
|
1355
|
+
type: "text",
|
|
1356
|
+
text: "Error: Perplexity entered voice/listening mode instead of processing your prompt.\n\nSuggested action: Retry with newChat: true to get a fresh Perplexity session.",
|
|
1357
|
+
},
|
|
1358
|
+
],
|
|
1359
|
+
isError: true,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (status.status === "idle" && (await cometAI.detectIdleNoNavigation())) {
|
|
1363
|
+
dispatchAlert({
|
|
1364
|
+
type: "PERPLEXITY_IDLE_NO_NAV",
|
|
1365
|
+
message: "Task went IDLE without navigating — Perplexity did not process the request.",
|
|
1366
|
+
consumerId: sessionRegistry.getCurrent()?.agentId ?? null,
|
|
1367
|
+
sessionKey: sessionRegistry.getCurrent()?.sessionKey ?? null,
|
|
1368
|
+
});
|
|
1369
|
+
return {
|
|
1370
|
+
content: [
|
|
1371
|
+
{
|
|
1372
|
+
type: "text",
|
|
1373
|
+
text: "Error: Task went IDLE without navigating. Perplexity did not process the request.\n\nSuggested action: Retry the task. If persistent, use comet_stop then retry with newChat: true.",
|
|
1374
|
+
},
|
|
1375
|
+
],
|
|
1376
|
+
isError: true,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
// Collect steps
|
|
1380
|
+
for (const step of status.steps) {
|
|
1381
|
+
if (!stepsCollected.includes(step)) {
|
|
1382
|
+
stepsCollected.push(step);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// Task completed - return result directly (but only if we saw a NEW response)
|
|
1386
|
+
if (status.status === "completed" && sawNewResponse) {
|
|
1387
|
+
// Context bleed detection (Spec 016, FR-002, T017)
|
|
1388
|
+
const urlMatch = prompt.match(/https?:\/\/[^\s"')]+/);
|
|
1389
|
+
if (urlMatch && (await cometAI.detectContextBleed(urlMatch[0]))) {
|
|
1390
|
+
dispatchAlert({
|
|
1391
|
+
type: "PERPLEXITY_CONTEXT_BLEED",
|
|
1392
|
+
message: `Response content appears to be from a different URL than requested (${urlMatch[0]}).`,
|
|
1393
|
+
consumerId: sessionRegistry.getCurrent()?.agentId ?? null,
|
|
1394
|
+
sessionKey: sessionRegistry.getCurrent()?.sessionKey ?? null,
|
|
1395
|
+
context: { requestedUrl: urlMatch[0], actualUrl: status.agentBrowsingUrl },
|
|
1396
|
+
});
|
|
1397
|
+
return {
|
|
1398
|
+
content: [
|
|
1399
|
+
{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: `Error: Context bleed detected — response content appears to be from a different URL than requested.\nRequested: ${urlMatch[0]}\nActual browsing: ${status.agentBrowsingUrl || "unknown"}\n\nSuggested action: Use newChat: true to prevent context from prior conversation bleeding into this task.`,
|
|
1402
|
+
},
|
|
1403
|
+
],
|
|
1404
|
+
isError: true,
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
content: [
|
|
1409
|
+
{
|
|
1410
|
+
type: "text",
|
|
1411
|
+
text: status.response || "Task completed (no response text extracted)",
|
|
1412
|
+
},
|
|
1413
|
+
],
|
|
1414
|
+
};
|
|
451
1415
|
}
|
|
452
1416
|
}
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
1417
|
+
// Still working after initial wait - return "in progress" (non-blocking)
|
|
1418
|
+
const finalStatus = await cometAI.getAgentStatus();
|
|
1419
|
+
let inProgressMsg = `Task in progress (${stepsCollected.length} steps so far).\n`;
|
|
1420
|
+
inProgressMsg += `Status: ${finalStatus.status.toUpperCase()}\n`;
|
|
1421
|
+
if (finalStatus.currentStep) {
|
|
1422
|
+
inProgressMsg += `Current: ${finalStatus.currentStep}\n`;
|
|
456
1423
|
}
|
|
1424
|
+
if (finalStatus.agentBrowsingUrl) {
|
|
1425
|
+
inProgressMsg += `Browsing: ${finalStatus.agentBrowsingUrl}\n`;
|
|
1426
|
+
}
|
|
1427
|
+
if (stepsCollected.length > 0) {
|
|
1428
|
+
inProgressMsg += `\nSteps:\n${stepsCollected.map((s) => ` • ${s}`).join("\n")}\n`;
|
|
1429
|
+
}
|
|
1430
|
+
inProgressMsg += `\nUse comet_poll to check progress or comet_stop to cancel.`;
|
|
1431
|
+
return { content: [{ type: "text", text: inProgressMsg }] };
|
|
457
1432
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
1433
|
+
case "comet_poll": {
|
|
1434
|
+
const sidecarResultId = args?.sidecarResultId;
|
|
1435
|
+
const sidecarContextKey = args?.sidecarContextKey;
|
|
1436
|
+
if (sidecarResultId || sidecarContextKey) {
|
|
1437
|
+
const artifact = sidecarResultId
|
|
1438
|
+
? await sidecarArtifactStore.get(sidecarResultId)
|
|
1439
|
+
: await sidecarArtifactStore.latestForContext(sidecarContextKey);
|
|
1440
|
+
if (!artifact) {
|
|
1441
|
+
return {
|
|
1442
|
+
content: [
|
|
1443
|
+
{
|
|
1444
|
+
type: "text",
|
|
1445
|
+
text: `No sidecar result artifact found for ${JSON.stringify({ sidecarResultId, sidecarContextKey })}`,
|
|
1446
|
+
},
|
|
1447
|
+
],
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
const resolved = await resolveBoundSession(sessionRegistry.getCurrent(), args);
|
|
1451
|
+
if (artifact.bindingId !== resolved.binding.bindingId) {
|
|
1452
|
+
return {
|
|
1453
|
+
content: [
|
|
1454
|
+
{
|
|
1455
|
+
type: "text",
|
|
1456
|
+
text: `Binding error (OWNERSHIP_VIOLATION) before comet_poll: sidecar result ${artifact.sidecarResultId} belongs to binding ${artifact.bindingId}.`,
|
|
1457
|
+
},
|
|
1458
|
+
],
|
|
1459
|
+
isError: true,
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
return {
|
|
1463
|
+
content: [{ type: "text", text: JSON.stringify(artifact, null, 2) }],
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
const status = await cometAI.getAgentStatus();
|
|
1467
|
+
// Failure detection heuristics on poll (Spec 016, FR-002, T018)
|
|
1468
|
+
if (await cometAI.detectVoiceMode()) {
|
|
1469
|
+
dispatchAlert({
|
|
1470
|
+
type: "PERPLEXITY_VOICE_MODE",
|
|
1471
|
+
message: "Perplexity entered voice/listening mode.",
|
|
1472
|
+
consumerId: sessionRegistry.getCurrent()?.agentId ?? null,
|
|
1473
|
+
sessionKey: sessionRegistry.getCurrent()?.sessionKey ?? null,
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
content: [
|
|
1477
|
+
{
|
|
1478
|
+
type: "text",
|
|
1479
|
+
text: "Error: Perplexity entered voice/listening mode.\n\nSuggested action: Retry with newChat: true.",
|
|
1480
|
+
},
|
|
1481
|
+
],
|
|
1482
|
+
isError: true,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
if (status.status === "idle" && (await cometAI.detectIdleNoNavigation())) {
|
|
1486
|
+
dispatchAlert({
|
|
1487
|
+
type: "PERPLEXITY_IDLE_NO_NAV",
|
|
1488
|
+
message: "Task went IDLE without navigating.",
|
|
1489
|
+
consumerId: sessionRegistry.getCurrent()?.agentId ?? null,
|
|
1490
|
+
sessionKey: sessionRegistry.getCurrent()?.sessionKey ?? null,
|
|
1491
|
+
});
|
|
1492
|
+
return {
|
|
1493
|
+
content: [
|
|
1494
|
+
{
|
|
1495
|
+
type: "text",
|
|
1496
|
+
text: "Error: Task went IDLE without navigating.\n\nSuggested action: Retry the task, or use comet_stop then retry with newChat: true.",
|
|
1497
|
+
},
|
|
1498
|
+
],
|
|
1499
|
+
isError: true,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
// If completed, return the response directly (most useful case)
|
|
1503
|
+
if (status.status === "completed" && status.response) {
|
|
1504
|
+
return { content: [{ type: "text", text: status.response }] };
|
|
1505
|
+
}
|
|
1506
|
+
// Still working - return progress info
|
|
1507
|
+
let output = `Status: ${status.status.toUpperCase()}\n`;
|
|
1508
|
+
if (status.agentBrowsingUrl) {
|
|
1509
|
+
output += `Browsing: ${status.agentBrowsingUrl}\n`;
|
|
1510
|
+
}
|
|
1511
|
+
if (status.currentStep) {
|
|
1512
|
+
output += `Current: ${status.currentStep}\n`;
|
|
1513
|
+
}
|
|
1514
|
+
if (status.steps.length > 0) {
|
|
1515
|
+
output += `\nSteps:\n${status.steps.map((s) => ` • ${s}`).join("\n")}\n`;
|
|
1516
|
+
}
|
|
1517
|
+
if (status.status === "working") {
|
|
1518
|
+
output += `\n[Use comet_stop to interrupt, or comet_screenshot to see current page]`;
|
|
1519
|
+
}
|
|
1520
|
+
return { content: [{ type: "text", text: output }] };
|
|
487
1521
|
}
|
|
488
|
-
|
|
489
|
-
|
|
1522
|
+
case "comet_stop": {
|
|
1523
|
+
const stopped = await cometAI.stopAgent();
|
|
1524
|
+
return {
|
|
1525
|
+
content: [
|
|
1526
|
+
{
|
|
1527
|
+
type: "text",
|
|
1528
|
+
text: stopped ? "Agent stopped" : "No active agent to stop",
|
|
1529
|
+
},
|
|
1530
|
+
],
|
|
1531
|
+
};
|
|
490
1532
|
}
|
|
491
|
-
|
|
492
|
-
|
|
1533
|
+
case "comet_screenshot": {
|
|
1534
|
+
const result = await cometClient.screenshot("png");
|
|
1535
|
+
return {
|
|
1536
|
+
content: [{ type: "image", data: result.data, mimeType: "image/png" }],
|
|
1537
|
+
};
|
|
493
1538
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
content: [{
|
|
500
|
-
type: "text",
|
|
501
|
-
text: stopped ? "Agent stopped" : "No active agent to stop",
|
|
502
|
-
}],
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
case "comet_screenshot": {
|
|
506
|
-
const result = await cometClient.screenshot("png");
|
|
507
|
-
return {
|
|
508
|
-
content: [{ type: "image", data: result.data, mimeType: "image/png" }],
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
case "comet_mode": {
|
|
512
|
-
const mode = args?.mode;
|
|
513
|
-
// If no mode provided, show current mode
|
|
514
|
-
if (!mode) {
|
|
515
|
-
const result = await cometClient.evaluate(`
|
|
1539
|
+
case "comet_mode": {
|
|
1540
|
+
const mode = args?.mode;
|
|
1541
|
+
// If no mode provided, show current mode
|
|
1542
|
+
if (!mode) {
|
|
1543
|
+
const result = await cometClient.evaluate(`
|
|
516
1544
|
(() => {
|
|
517
1545
|
// Try button group first (wide screen)
|
|
518
1546
|
const modes = ['Search', 'Research', 'Labs', 'Learn'];
|
|
@@ -534,41 +1562,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
534
1562
|
return 'search';
|
|
535
1563
|
})()
|
|
536
1564
|
`);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1565
|
+
const currentMode = result.result.value;
|
|
1566
|
+
const descriptions = {
|
|
1567
|
+
search: "Basic web search",
|
|
1568
|
+
research: "Deep research with comprehensive analysis",
|
|
1569
|
+
labs: "Analytics, visualizations, and coding",
|
|
1570
|
+
learn: "Educational content and explanations",
|
|
1571
|
+
};
|
|
1572
|
+
let output = `Current mode: ${currentMode}\n\nAvailable modes:\n`;
|
|
1573
|
+
for (const [m, desc] of Object.entries(descriptions)) {
|
|
1574
|
+
const marker = m === currentMode ? "→" : " ";
|
|
1575
|
+
output += `${marker} ${m}: ${desc}\n`;
|
|
1576
|
+
}
|
|
1577
|
+
return { content: [{ type: "text", text: output }] };
|
|
548
1578
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
labs: "Labs",
|
|
556
|
-
learn: "Learn",
|
|
557
|
-
};
|
|
558
|
-
const ariaLabel = modeMap[mode];
|
|
559
|
-
if (!ariaLabel) {
|
|
560
|
-
return {
|
|
561
|
-
content: [{ type: "text", text: `Invalid mode: ${mode}. Use: search, research, labs, learn` }],
|
|
562
|
-
isError: true,
|
|
1579
|
+
// Switch mode
|
|
1580
|
+
const modeMap = {
|
|
1581
|
+
search: "Search",
|
|
1582
|
+
research: "Research",
|
|
1583
|
+
labs: "Labs",
|
|
1584
|
+
learn: "Learn",
|
|
563
1585
|
};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1586
|
+
const ariaLabel = modeMap[mode];
|
|
1587
|
+
if (!ariaLabel) {
|
|
1588
|
+
return {
|
|
1589
|
+
content: [
|
|
1590
|
+
{ type: "text", text: `Invalid mode: ${mode}. Use: search, research, labs, learn` },
|
|
1591
|
+
],
|
|
1592
|
+
isError: true,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
// Navigate to Perplexity first if not there
|
|
1596
|
+
const state = cometClient.currentState;
|
|
1597
|
+
if (!state.currentUrl?.includes("perplexity.ai")) {
|
|
1598
|
+
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
1599
|
+
}
|
|
1600
|
+
// Try both UI patterns: button group (wide) and dropdown (narrow)
|
|
1601
|
+
const result = await cometClient.evaluate(`
|
|
572
1602
|
(() => {
|
|
573
1603
|
// Strategy 1: Direct button (wide screen)
|
|
574
1604
|
const btn = document.querySelector('button[aria-label="${ariaLabel}"]');
|
|
@@ -593,11 +1623,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
593
1623
|
return { success: false, error: "Mode selector not found" };
|
|
594
1624
|
})()
|
|
595
1625
|
`);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1626
|
+
const clickResult = result.result.value;
|
|
1627
|
+
if (clickResult.success && clickResult.needsSelect) {
|
|
1628
|
+
// Wait for dropdown to open, then select the mode
|
|
1629
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1630
|
+
const selectResult = await cometClient.evaluate(`
|
|
601
1631
|
(() => {
|
|
602
1632
|
// Look for dropdown menu items
|
|
603
1633
|
const items = document.querySelectorAll('[role="menuitem"], [role="option"], button');
|
|
@@ -610,41 +1640,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
610
1640
|
return { success: false, error: "Mode option not found in dropdown" };
|
|
611
1641
|
})()
|
|
612
1642
|
`);
|
|
613
|
-
|
|
614
|
-
|
|
1643
|
+
const selectRes = selectResult.result.value;
|
|
1644
|
+
if (selectRes.success) {
|
|
1645
|
+
return { content: [{ type: "text", text: `Switched to ${mode} mode` }] };
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
return {
|
|
1649
|
+
content: [{ type: "text", text: `Failed: ${selectRes.error}` }],
|
|
1650
|
+
isError: true,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (clickResult.success) {
|
|
615
1655
|
return { content: [{ type: "text", text: `Switched to ${mode} mode` }] };
|
|
616
1656
|
}
|
|
617
1657
|
else {
|
|
618
|
-
return {
|
|
1658
|
+
return {
|
|
1659
|
+
content: [{ type: "text", text: `Failed to switch mode: ${clickResult.error}` }],
|
|
1660
|
+
isError: true,
|
|
1661
|
+
};
|
|
619
1662
|
}
|
|
620
1663
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
await cometClient.connect(tabs.main.id);
|
|
639
|
-
}
|
|
640
|
-
const urlResult = await cometClient.evaluate('window.location.href');
|
|
641
|
-
const currentUrl = urlResult.result.value;
|
|
642
|
-
if (!currentUrl?.includes('perplexity.ai')) {
|
|
643
|
-
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
644
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
645
|
-
}
|
|
646
|
-
// Capture old state for response detection
|
|
647
|
-
const oldStateResult = await cometClient.evaluate(`
|
|
1664
|
+
case "comet_shortcut": {
|
|
1665
|
+
const shortcut = (args?.shortcut).replace(/^\//, "");
|
|
1666
|
+
const context = args?.context;
|
|
1667
|
+
const timeout = args?.timeout || 30000;
|
|
1668
|
+
// Ensure we're on Perplexity
|
|
1669
|
+
const tabs = await cometClient.listTabsCategorized();
|
|
1670
|
+
if (tabs.main) {
|
|
1671
|
+
await cometClient.connect(tabs.main.id);
|
|
1672
|
+
}
|
|
1673
|
+
const urlResult = await cometClient.evaluate("window.location.href");
|
|
1674
|
+
const currentUrl = urlResult.result.value;
|
|
1675
|
+
if (!currentUrl?.includes("perplexity.ai")) {
|
|
1676
|
+
await cometClient.navigate("https://www.perplexity.ai/", true);
|
|
1677
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1678
|
+
}
|
|
1679
|
+
// Capture old state for response detection
|
|
1680
|
+
const oldStateResult = await cometClient.evaluate(`
|
|
648
1681
|
(() => {
|
|
649
1682
|
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
650
1683
|
const lastProse = proseEls[proseEls.length - 1];
|
|
@@ -654,15 +1687,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
654
1687
|
};
|
|
655
1688
|
})()
|
|
656
1689
|
`);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1690
|
+
const oldState = oldStateResult.result.value;
|
|
1691
|
+
// Send the shortcut
|
|
1692
|
+
await cometAI.sendShortcut(shortcut, context);
|
|
1693
|
+
// Poll for response with timeout
|
|
1694
|
+
const startTime = Date.now();
|
|
1695
|
+
let sawNewResponse = false;
|
|
1696
|
+
while (Date.now() - startTime < timeout) {
|
|
1697
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1698
|
+
const currentStateResult = await cometClient.evaluate(`
|
|
666
1699
|
(() => {
|
|
667
1700
|
const proseEls = document.querySelectorAll('[class*="prose"]');
|
|
668
1701
|
const lastProse = proseEls[proseEls.length - 1];
|
|
@@ -672,502 +1705,1922 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
672
1705
|
};
|
|
673
1706
|
})()
|
|
674
1707
|
`);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const status = await cometAI.getAgentStatus();
|
|
683
|
-
if (status.status === 'completed' && sawNewResponse) {
|
|
684
|
-
return { content: [{ type: "text", text: status.response || 'Shortcut completed (no response text extracted)' }] };
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
// Timed out — return status so user can poll
|
|
688
|
-
const finalStatus = await cometAI.getAgentStatus();
|
|
689
|
-
let msg = `Shortcut /${shortcut} in progress (timed out after ${timeout}ms).\n`;
|
|
690
|
-
msg += `Status: ${finalStatus.status.toUpperCase()}\n`;
|
|
691
|
-
if (finalStatus.currentStep)
|
|
692
|
-
msg += `Current: ${finalStatus.currentStep}\n`;
|
|
693
|
-
msg += `\nUse comet_poll to check progress.`;
|
|
694
|
-
return { content: [{ type: "text", text: msg }] };
|
|
695
|
-
}
|
|
696
|
-
case "comet_read_page": {
|
|
697
|
-
const mode = args?.mode || "text";
|
|
698
|
-
const maxDepth = args?.maxDepth || 5;
|
|
699
|
-
const maxLength = args?.maxLength || 12000;
|
|
700
|
-
const parts = [];
|
|
701
|
-
if (mode === "tree" || mode === "both") {
|
|
702
|
-
const tree = await cometClient.getAccessibilityTree(maxDepth, maxLength);
|
|
703
|
-
parts.push("## Accessibility Tree\n" + tree);
|
|
704
|
-
}
|
|
705
|
-
if (mode === "text" || mode === "both") {
|
|
706
|
-
const text = await cometClient.getPageText(maxLength);
|
|
707
|
-
parts.push("## Page Text\n" + text);
|
|
708
|
-
}
|
|
709
|
-
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
710
|
-
}
|
|
711
|
-
case "comet_wait_for_idle": {
|
|
712
|
-
const idleTime = args?.idleTime || 1500;
|
|
713
|
-
const timeout = args?.timeout || 15000;
|
|
714
|
-
const result = await cometClient.waitForNetworkIdle({ idleTime, timeout });
|
|
715
|
-
const status = result.idle ? "Network idle reached" : "Timeout — network still active";
|
|
716
|
-
const summary = [
|
|
717
|
-
status,
|
|
718
|
-
`Waited: ${result.waitedMs}ms`,
|
|
719
|
-
`Requests: ${result.totalRequests} total, ${result.totalCompleted} completed, ${result.totalFailed} failed`,
|
|
720
|
-
result.pendingRequests > 0 ? `Still pending: ${result.pendingRequests}` : "",
|
|
721
|
-
].filter(Boolean).join("\n");
|
|
722
|
-
return { content: [{ type: "text", text: summary }] };
|
|
723
|
-
}
|
|
724
|
-
case "comet_tab_groups": {
|
|
725
|
-
const { tabGroupsClient } = await import("./tab-groups.js");
|
|
726
|
-
const action = args?.action;
|
|
727
|
-
try {
|
|
728
|
-
switch (action) {
|
|
729
|
-
case "list": {
|
|
730
|
-
const groups = await tabGroupsClient.listGroups();
|
|
731
|
-
if (groups.length === 0) {
|
|
732
|
-
return { content: [{ type: "text", text: "No tab groups found." }] };
|
|
1708
|
+
const currentState = currentStateResult.result.value;
|
|
1709
|
+
if (!sawNewResponse) {
|
|
1710
|
+
if (currentState.count > oldState.count ||
|
|
1711
|
+
(currentState.lastText && currentState.lastText !== oldState.lastText)) {
|
|
1712
|
+
sawNewResponse = true;
|
|
733
1713
|
}
|
|
734
|
-
const lines = groups.map((g) => `[${g.id}] "${g.title || "(untitled)"}" (${g.color}${g.collapsed ? ", collapsed" : ""})`);
|
|
735
|
-
return { content: [{ type: "text", text: `Tab groups:\n${lines.join("\n")}` }] };
|
|
736
1714
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const lines = tabs.map((t) => `[tab:${t.id}] group:${t.groupId === -1 ? "none" : t.groupId} "${t.title}" ${t.url}`);
|
|
740
|
-
return { content: [{ type: "text", text: `Tabs (${tabs.length}):\n${lines.join("\n")}` }] };
|
|
741
|
-
}
|
|
742
|
-
case "create": {
|
|
743
|
-
const tabIds = args?.tabIds;
|
|
744
|
-
if (!tabIds || tabIds.length === 0) {
|
|
745
|
-
return { content: [{ type: "text", text: "Error: tabIds required for create" }], isError: true };
|
|
746
|
-
}
|
|
747
|
-
const result = await tabGroupsClient.createGroup({
|
|
748
|
-
tabIds,
|
|
749
|
-
title: args?.title,
|
|
750
|
-
color: args?.color,
|
|
751
|
-
});
|
|
1715
|
+
const status = await cometAI.getAgentStatus();
|
|
1716
|
+
if (status.status === "completed" && sawNewResponse) {
|
|
752
1717
|
return {
|
|
753
|
-
content: [
|
|
1718
|
+
content: [
|
|
1719
|
+
{
|
|
754
1720
|
type: "text",
|
|
755
|
-
text:
|
|
756
|
-
}
|
|
1721
|
+
text: status.response || "Shortcut completed (no response text extracted)",
|
|
1722
|
+
},
|
|
1723
|
+
],
|
|
757
1724
|
};
|
|
758
1725
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1726
|
+
}
|
|
1727
|
+
// Timed out — return status so user can poll
|
|
1728
|
+
const finalStatus = await cometAI.getAgentStatus();
|
|
1729
|
+
let msg = `Shortcut /${shortcut} in progress (timed out after ${timeout}ms).\n`;
|
|
1730
|
+
msg += `Status: ${finalStatus.status.toUpperCase()}\n`;
|
|
1731
|
+
if (finalStatus.currentStep)
|
|
1732
|
+
msg += `Current: ${finalStatus.currentStep}\n`;
|
|
1733
|
+
msg += `\nUse comet_poll to check progress.`;
|
|
1734
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1735
|
+
}
|
|
1736
|
+
case "comet_read_page": {
|
|
1737
|
+
const mode = args?.mode || "text";
|
|
1738
|
+
const maxDepth = args?.maxDepth || 5;
|
|
1739
|
+
const maxLength = args?.maxLength || 12000;
|
|
1740
|
+
const parts = [];
|
|
1741
|
+
if (mode === "tree" || mode === "both") {
|
|
1742
|
+
const tree = await cometClient.getAccessibilityTree(maxDepth, maxLength);
|
|
1743
|
+
parts.push("## Accessibility Tree\n" + tree);
|
|
1744
|
+
}
|
|
1745
|
+
if (mode === "text" || mode === "both") {
|
|
1746
|
+
const text = await cometClient.getPageText(maxLength);
|
|
1747
|
+
parts.push("## Page Text\n" + text);
|
|
1748
|
+
}
|
|
1749
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
1750
|
+
}
|
|
1751
|
+
case "comet_interact": {
|
|
1752
|
+
const actions = args?.actions;
|
|
1753
|
+
if (!actions || actions.length === 0) {
|
|
1754
|
+
return {
|
|
1755
|
+
content: [
|
|
1756
|
+
{ type: "text", text: "Error: actions array is required and must not be empty." },
|
|
1757
|
+
],
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
const results = [];
|
|
1761
|
+
for (const act of actions) {
|
|
1762
|
+
try {
|
|
1763
|
+
switch (act.action) {
|
|
1764
|
+
case "click": {
|
|
1765
|
+
if (!act.selector)
|
|
1766
|
+
throw new Error("click requires a selector");
|
|
1767
|
+
const clicked = await cometClient.safeEvaluate(`
|
|
1768
|
+
(() => {
|
|
1769
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1770
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found: ${act.selector.replace(/'/g, "\\'")}' });
|
|
1771
|
+
el.scrollIntoView({ block: 'center' });
|
|
1772
|
+
el.click();
|
|
1773
|
+
return JSON.stringify({ ok: true, tag: el.tagName, text: (el.textContent || '').trim().substring(0, 80) });
|
|
1774
|
+
})()
|
|
1775
|
+
`);
|
|
1776
|
+
const clickRes = JSON.parse(clicked.result.value);
|
|
1777
|
+
if (!clickRes.ok)
|
|
1778
|
+
throw new Error(clickRes.error);
|
|
1779
|
+
results.push({
|
|
1780
|
+
action: "click",
|
|
1781
|
+
success: true,
|
|
1782
|
+
result: `Clicked <${clickRes.tag}> "${clickRes.text}"`,
|
|
1783
|
+
});
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case "type": {
|
|
1787
|
+
if (!act.selector)
|
|
1788
|
+
throw new Error("type requires a selector");
|
|
1789
|
+
if (!act.value)
|
|
1790
|
+
throw new Error("type requires a value");
|
|
1791
|
+
// Focus the element first
|
|
1792
|
+
await cometClient.safeEvaluate(`
|
|
1793
|
+
(() => {
|
|
1794
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1795
|
+
if (!el) throw new Error('Element not found');
|
|
1796
|
+
el.focus();
|
|
1797
|
+
})()
|
|
1798
|
+
`);
|
|
1799
|
+
// Type character by character using CDP Input events
|
|
1800
|
+
for (const char of act.value) {
|
|
1801
|
+
await cometClient.pressKey(char);
|
|
1802
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1803
|
+
}
|
|
1804
|
+
results.push({
|
|
1805
|
+
action: "type",
|
|
1806
|
+
success: true,
|
|
1807
|
+
result: `Typed ${act.value.length} chars`,
|
|
1808
|
+
});
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
case "fill": {
|
|
1812
|
+
if (!act.selector)
|
|
1813
|
+
throw new Error("fill requires a selector");
|
|
1814
|
+
const fillRes = await cometClient.safeEvaluate(`
|
|
1815
|
+
(() => {
|
|
1816
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1817
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
1818
|
+
el.focus();
|
|
1819
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
1820
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
1821
|
+
el.tagName === 'INPUT' ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype, 'value'
|
|
1822
|
+
)?.set;
|
|
1823
|
+
if (nativeSetter) nativeSetter.call(el, ${JSON.stringify(act.value || "")});
|
|
1824
|
+
else el.value = ${JSON.stringify(act.value || "")};
|
|
1825
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1826
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1827
|
+
} else if (el.contentEditable === 'true') {
|
|
1828
|
+
el.focus();
|
|
1829
|
+
document.execCommand('selectAll', false, null);
|
|
1830
|
+
document.execCommand('insertText', false, ${JSON.stringify(act.value || "")});
|
|
1831
|
+
}
|
|
1832
|
+
return JSON.stringify({ ok: true });
|
|
1833
|
+
})()
|
|
1834
|
+
`);
|
|
1835
|
+
const fRes = JSON.parse(fillRes.result.value);
|
|
1836
|
+
if (!fRes.ok)
|
|
1837
|
+
throw new Error(fRes.error);
|
|
1838
|
+
results.push({
|
|
1839
|
+
action: "fill",
|
|
1840
|
+
success: true,
|
|
1841
|
+
result: `Filled with "${(act.value || "").substring(0, 40)}"`,
|
|
1842
|
+
});
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
case "press": {
|
|
1846
|
+
const key = act.key || act.value || "Enter";
|
|
1847
|
+
if (act.selector) {
|
|
1848
|
+
await cometClient.safeEvaluate(`
|
|
1849
|
+
(() => {
|
|
1850
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1851
|
+
if (el) el.focus();
|
|
1852
|
+
})()
|
|
1853
|
+
`);
|
|
1854
|
+
}
|
|
1855
|
+
await cometClient.pressKey(key);
|
|
1856
|
+
results.push({ action: "press", success: true, result: `Pressed ${key}` });
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
case "check":
|
|
1860
|
+
case "uncheck": {
|
|
1861
|
+
if (!act.selector)
|
|
1862
|
+
throw new Error(`${act.action} requires a selector`);
|
|
1863
|
+
const shouldCheck = act.action === "check";
|
|
1864
|
+
const checkRes = await cometClient.safeEvaluate(`
|
|
1865
|
+
(() => {
|
|
1866
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1867
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
1868
|
+
const isCheckbox = el.type === 'checkbox' || el.role === 'checkbox' || el.getAttribute('role') === 'checkbox';
|
|
1869
|
+
if (isCheckbox) {
|
|
1870
|
+
if (el.checked !== ${shouldCheck}) {
|
|
1871
|
+
el.click();
|
|
1872
|
+
}
|
|
1873
|
+
return JSON.stringify({ ok: true, checked: ${shouldCheck} });
|
|
1874
|
+
}
|
|
1875
|
+
// Fallback: just click it (toggle behavior)
|
|
1876
|
+
el.scrollIntoView({ block: 'center' });
|
|
1877
|
+
el.click();
|
|
1878
|
+
return JSON.stringify({ ok: true, clicked: true });
|
|
1879
|
+
})()
|
|
1880
|
+
`);
|
|
1881
|
+
const cRes = JSON.parse(checkRes.result.value);
|
|
1882
|
+
if (!cRes.ok)
|
|
1883
|
+
throw new Error(cRes.error);
|
|
1884
|
+
results.push({
|
|
1885
|
+
action: act.action,
|
|
1886
|
+
success: true,
|
|
1887
|
+
result: `${act.action}ed: ${act.selector}`,
|
|
1888
|
+
});
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
case "select": {
|
|
1892
|
+
if (!act.selector)
|
|
1893
|
+
throw new Error("select requires a selector");
|
|
1894
|
+
if (!act.value)
|
|
1895
|
+
throw new Error("select requires a value");
|
|
1896
|
+
const selRes = await cometClient.safeEvaluate(`
|
|
1897
|
+
(() => {
|
|
1898
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1899
|
+
if (!el || el.tagName !== 'SELECT') return JSON.stringify({ ok: false, error: 'SELECT element not found' });
|
|
1900
|
+
// Try by value first, then by text
|
|
1901
|
+
let found = false;
|
|
1902
|
+
for (const opt of el.options) {
|
|
1903
|
+
if (opt.value === ${JSON.stringify(act.value)} || opt.text === ${JSON.stringify(act.value)}) {
|
|
1904
|
+
el.value = opt.value;
|
|
1905
|
+
found = true;
|
|
1906
|
+
break;
|
|
776
1907
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1908
|
+
}
|
|
1909
|
+
if (!found) return JSON.stringify({ ok: false, error: 'Option not found: ${act.value}' });
|
|
1910
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1911
|
+
return JSON.stringify({ ok: true, selected: el.value });
|
|
1912
|
+
})()
|
|
1913
|
+
`);
|
|
1914
|
+
const sRes = JSON.parse(selRes.result.value);
|
|
1915
|
+
if (!sRes.ok)
|
|
1916
|
+
throw new Error(sRes.error);
|
|
1917
|
+
results.push({
|
|
1918
|
+
action: "select",
|
|
1919
|
+
success: true,
|
|
1920
|
+
result: `Selected: ${sRes.selected}`,
|
|
1921
|
+
});
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
case "scroll": {
|
|
1925
|
+
const dir = act.direction || "down";
|
|
1926
|
+
const amount = act.amount || 500;
|
|
1927
|
+
const dx = dir === "right" ? amount : dir === "left" ? -amount : 0;
|
|
1928
|
+
const dy = dir === "down" ? amount : dir === "up" ? -amount : 0;
|
|
1929
|
+
if (act.selector) {
|
|
1930
|
+
await cometClient.safeEvaluate(`
|
|
1931
|
+
(() => {
|
|
1932
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1933
|
+
if (el) el.scrollBy(${dx}, ${dy});
|
|
1934
|
+
})()
|
|
1935
|
+
`);
|
|
1936
|
+
}
|
|
1937
|
+
else {
|
|
1938
|
+
await cometClient.safeEvaluate(`window.scrollBy(${dx}, ${dy})`);
|
|
1939
|
+
}
|
|
1940
|
+
results.push({
|
|
1941
|
+
action: "scroll",
|
|
1942
|
+
success: true,
|
|
1943
|
+
result: `Scrolled ${dir} ${amount}px`,
|
|
1944
|
+
});
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
case "wait": {
|
|
1948
|
+
if (act.selector) {
|
|
1949
|
+
// Wait for element to appear
|
|
1950
|
+
const waitTimeout = act.ms || 10000;
|
|
1951
|
+
const start = Date.now();
|
|
1952
|
+
let found = false;
|
|
1953
|
+
while (Date.now() - start < waitTimeout) {
|
|
1954
|
+
const exists = await cometClient.safeEvaluate(`document.querySelector(${JSON.stringify(act.selector)}) !== null`);
|
|
1955
|
+
if (exists.result.value === true) {
|
|
1956
|
+
found = true;
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1960
|
+
}
|
|
1961
|
+
if (!found)
|
|
1962
|
+
throw new Error(`Timeout waiting for ${act.selector}`);
|
|
1963
|
+
results.push({
|
|
1964
|
+
action: "wait",
|
|
1965
|
+
success: true,
|
|
1966
|
+
result: `Found: ${act.selector}`,
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
const ms = act.ms || 1000;
|
|
1971
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
1972
|
+
results.push({ action: "wait", success: true, result: `Waited ${ms}ms` });
|
|
1973
|
+
}
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1976
|
+
case "extract": {
|
|
1977
|
+
if (!act.selector)
|
|
1978
|
+
throw new Error("extract requires a selector");
|
|
1979
|
+
const extRes = await cometClient.safeEvaluate(`
|
|
1980
|
+
(() => {
|
|
1981
|
+
const el = document.querySelector(${JSON.stringify(act.selector)});
|
|
1982
|
+
if (!el) return JSON.stringify({ ok: false, error: 'Element not found' });
|
|
1983
|
+
return JSON.stringify({
|
|
1984
|
+
ok: true,
|
|
1985
|
+
text: el.innerText?.trim() || '',
|
|
1986
|
+
value: el.value || '',
|
|
1987
|
+
tag: el.tagName,
|
|
1988
|
+
checked: el.checked ?? null,
|
|
1989
|
+
href: el.href || null,
|
|
1990
|
+
});
|
|
1991
|
+
})()
|
|
1992
|
+
`);
|
|
1993
|
+
const eRes = JSON.parse(extRes.result.value);
|
|
1994
|
+
if (!eRes.ok)
|
|
1995
|
+
throw new Error(eRes.error);
|
|
1996
|
+
const extracted = eRes.text || eRes.value || `<${eRes.tag}>`;
|
|
1997
|
+
results.push({
|
|
1998
|
+
action: "extract",
|
|
1999
|
+
success: true,
|
|
2000
|
+
result: extracted.substring(0, 2000),
|
|
2001
|
+
});
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
case "evaluate": {
|
|
2005
|
+
if (!act.script)
|
|
2006
|
+
throw new Error("evaluate requires a script");
|
|
2007
|
+
const evalRes = await cometClient.safeEvaluate(`
|
|
2008
|
+
(() => {
|
|
2009
|
+
try {
|
|
2010
|
+
const result = (function() { ${act.script} })();
|
|
2011
|
+
return JSON.stringify({ ok: true, result: String(result ?? 'undefined').substring(0, 4000) });
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
return JSON.stringify({ ok: false, error: e.message });
|
|
2014
|
+
}
|
|
2015
|
+
})()
|
|
2016
|
+
`);
|
|
2017
|
+
const evRes = JSON.parse(evalRes.result.value);
|
|
2018
|
+
if (!evRes.ok)
|
|
2019
|
+
throw new Error(evRes.error);
|
|
2020
|
+
results.push({ action: "evaluate", success: true, result: evRes.result });
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
default:
|
|
2024
|
+
throw new Error(`Unknown action: ${act.action}`);
|
|
782
2025
|
}
|
|
783
|
-
|
|
784
|
-
|
|
2026
|
+
// Small pause between actions for DOM to settle
|
|
2027
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
785
2028
|
}
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
2029
|
+
catch (err) {
|
|
2030
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2031
|
+
results.push({ action: act.action, success: false, error: errorMsg });
|
|
2032
|
+
if (!act.optional) {
|
|
2033
|
+
// Abort remaining actions
|
|
2034
|
+
results.push({
|
|
2035
|
+
action: "ABORTED",
|
|
2036
|
+
success: false,
|
|
2037
|
+
error: `Stopped after ${act.action} failed`,
|
|
2038
|
+
});
|
|
2039
|
+
break;
|
|
790
2040
|
}
|
|
791
|
-
await tabGroupsClient.ungroupTabs(tabIds);
|
|
792
|
-
return { content: [{ type: "text", text: `Ungrouped ${tabIds.length} tab(s)` }] };
|
|
793
2041
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
2042
|
+
}
|
|
2043
|
+
const output = results
|
|
2044
|
+
.map((r) => `${r.success ? "✓" : "✗"} ${r.action}: ${r.result || r.error || ""}`)
|
|
2045
|
+
.join("\n");
|
|
2046
|
+
const allSucceeded = results.every((r) => r.success);
|
|
2047
|
+
return {
|
|
2048
|
+
content: [{ type: "text", text: output }],
|
|
2049
|
+
isError: !allSucceeded,
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
case "comet_navigate": {
|
|
2053
|
+
const url = args?.url;
|
|
2054
|
+
if (!url) {
|
|
2055
|
+
return { content: [{ type: "text", text: "Error: url is required" }] };
|
|
2056
|
+
}
|
|
2057
|
+
const waitForIdle = args?.waitForIdle !== false; // default true
|
|
2058
|
+
const navResult = await cometClient.navigate(url, true, waitForIdle);
|
|
2059
|
+
// Get final URL after any redirects
|
|
2060
|
+
const finalUrl = await cometClient.safeEvaluate("window.location.href");
|
|
2061
|
+
const finalUrlStr = finalUrl.result.value || url;
|
|
2062
|
+
const summary = [
|
|
2063
|
+
`Navigated to: ${finalUrlStr}`,
|
|
2064
|
+
navResult.networkIdle !== undefined
|
|
2065
|
+
? `Network: ${navResult.networkIdle ? "idle" : "still loading"}`
|
|
2066
|
+
: "",
|
|
2067
|
+
]
|
|
2068
|
+
.filter(Boolean)
|
|
2069
|
+
.join("\n");
|
|
2070
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2071
|
+
}
|
|
2072
|
+
case "comet_wait_for_idle": {
|
|
2073
|
+
const idleTime = args?.idleTime || 1500;
|
|
2074
|
+
const timeout = args?.timeout || 15000;
|
|
2075
|
+
const result = await cometClient.waitForNetworkIdle({ idleTime, timeout });
|
|
2076
|
+
const status = result.idle ? "Network idle reached" : "Timeout — network still active";
|
|
2077
|
+
const summary = [
|
|
2078
|
+
status,
|
|
2079
|
+
`Waited: ${result.waitedMs}ms`,
|
|
2080
|
+
`Requests: ${result.totalRequests} total, ${result.totalCompleted} completed, ${result.totalFailed} failed`,
|
|
2081
|
+
result.pendingRequests > 0 ? `Still pending: ${result.pendingRequests}` : "",
|
|
2082
|
+
]
|
|
2083
|
+
.filter(Boolean)
|
|
2084
|
+
.join("\n");
|
|
2085
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2086
|
+
}
|
|
2087
|
+
case "comet_tab_groups": {
|
|
2088
|
+
const { tabGroupsClient } = await import("./tab-groups.js");
|
|
2089
|
+
const action = args?.action;
|
|
2090
|
+
try {
|
|
2091
|
+
switch (action) {
|
|
2092
|
+
case "list": {
|
|
2093
|
+
const groups = await tabGroupsClient.listGroups();
|
|
2094
|
+
if (groups.length === 0) {
|
|
2095
|
+
return { content: [{ type: "text", text: "No tab groups found." }] };
|
|
2096
|
+
}
|
|
2097
|
+
const lines = groups.map((g) => `[${g.id}] "${g.title || "(untitled)"}" (${g.color}${g.collapsed ? ", collapsed" : ""})`);
|
|
2098
|
+
return { content: [{ type: "text", text: `Tab groups:\n${lines.join("\n")}` }] };
|
|
798
2099
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
return {
|
|
2100
|
+
case "list_tabs": {
|
|
2101
|
+
const tabs = await tabGroupsClient.listTabs();
|
|
2102
|
+
const lines = tabs.map((t) => `[tab:${t.id}] group:${t.groupId === -1 ? "none" : t.groupId} "${t.title}" ${t.url}`);
|
|
2103
|
+
return {
|
|
2104
|
+
content: [{ type: "text", text: `Tabs (${tabs.length}):\n${lines.join("\n")}` }],
|
|
2105
|
+
};
|
|
803
2106
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
text:
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
2107
|
+
case "create": {
|
|
2108
|
+
const tabIds = args?.tabIds;
|
|
2109
|
+
if (!tabIds || tabIds.length === 0) {
|
|
2110
|
+
return {
|
|
2111
|
+
content: [{ type: "text", text: "Error: tabIds required for create" }],
|
|
2112
|
+
isError: true,
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
const result = await tabGroupsClient.createGroup({
|
|
2116
|
+
tabIds,
|
|
2117
|
+
title: args?.title,
|
|
2118
|
+
color: args?.color,
|
|
2119
|
+
});
|
|
2120
|
+
return {
|
|
2121
|
+
content: [
|
|
2122
|
+
{
|
|
2123
|
+
type: "text",
|
|
2124
|
+
text: `Created group ${result.groupId}: "${result.group.title || "(untitled)"}" (${result.group.color})`,
|
|
2125
|
+
},
|
|
2126
|
+
],
|
|
2127
|
+
};
|
|
819
2128
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
collapsed: group.collapsed,
|
|
828
|
-
urls: groupTabs.map((t) => ({ url: t.url, title: t.title })),
|
|
829
|
-
archivedAt: new Date().toISOString(),
|
|
830
|
-
status: closeTabs ? "archived" : "saved",
|
|
831
|
-
};
|
|
832
|
-
await archiveStore.save(entry);
|
|
833
|
-
if (closeTabs) {
|
|
834
|
-
for (const t of groupTabs) {
|
|
835
|
-
try {
|
|
836
|
-
await tabGroupsClient.ungroupTabs([t.id]);
|
|
837
|
-
}
|
|
838
|
-
catch { /* tab may already be closed */ }
|
|
2129
|
+
case "update": {
|
|
2130
|
+
const groupId = args?.groupId;
|
|
2131
|
+
if (groupId === undefined) {
|
|
2132
|
+
return {
|
|
2133
|
+
content: [{ type: "text", text: "Error: groupId required for update" }],
|
|
2134
|
+
isError: true,
|
|
2135
|
+
};
|
|
839
2136
|
}
|
|
2137
|
+
const group = await tabGroupsClient.updateGroup({
|
|
2138
|
+
groupId,
|
|
2139
|
+
title: args?.title,
|
|
2140
|
+
color: args?.color,
|
|
2141
|
+
collapsed: args?.collapsed,
|
|
2142
|
+
});
|
|
2143
|
+
return {
|
|
2144
|
+
content: [
|
|
2145
|
+
{
|
|
2146
|
+
type: "text",
|
|
2147
|
+
text: `Updated group ${group.id}: "${group.title || "(untitled)"}" (${group.color}${group.collapsed ? ", collapsed" : ""})`,
|
|
2148
|
+
},
|
|
2149
|
+
],
|
|
2150
|
+
};
|
|
840
2151
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
2152
|
+
case "move": {
|
|
2153
|
+
const groupId = args?.groupId;
|
|
2154
|
+
const index = args?.index;
|
|
2155
|
+
if (groupId === undefined || index === undefined) {
|
|
2156
|
+
return {
|
|
2157
|
+
content: [{ type: "text", text: "Error: groupId and index required for move" }],
|
|
2158
|
+
isError: true,
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
const group = await tabGroupsClient.moveGroup(groupId, index);
|
|
2162
|
+
return {
|
|
2163
|
+
content: [{ type: "text", text: `Moved group ${group.id} to index ${index}` }],
|
|
2164
|
+
};
|
|
848
2165
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
2166
|
+
case "ungroup": {
|
|
2167
|
+
const tabIds = args?.tabIds;
|
|
2168
|
+
if (!tabIds || tabIds.length === 0) {
|
|
2169
|
+
return {
|
|
2170
|
+
content: [{ type: "text", text: "Error: tabIds required for ungroup" }],
|
|
2171
|
+
isError: true,
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
await tabGroupsClient.ungroupTabs(tabIds);
|
|
2175
|
+
return { content: [{ type: "text", text: `Ungrouped ${tabIds.length} tab(s)` }] };
|
|
852
2176
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
2177
|
+
case "delete": {
|
|
2178
|
+
const groupId = args?.groupId;
|
|
2179
|
+
if (groupId === undefined) {
|
|
2180
|
+
return {
|
|
2181
|
+
content: [{ type: "text", text: "Error: groupId required for delete" }],
|
|
2182
|
+
isError: true,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
const tabs = await tabGroupsClient.listTabs();
|
|
2186
|
+
const groupTabs = tabs.filter((t) => t.groupId === groupId);
|
|
2187
|
+
if (groupTabs.length === 0) {
|
|
2188
|
+
return { content: [{ type: "text", text: `No tabs found in group ${groupId}` }] };
|
|
2189
|
+
}
|
|
2190
|
+
await tabGroupsClient.ungroupTabs(groupTabs.map((t) => t.id));
|
|
2191
|
+
return {
|
|
2192
|
+
content: [
|
|
2193
|
+
{
|
|
2194
|
+
type: "text",
|
|
2195
|
+
text: `Deleted group ${groupId} (ungrouped ${groupTabs.length} tab(s))`,
|
|
2196
|
+
},
|
|
2197
|
+
],
|
|
2198
|
+
};
|
|
858
2199
|
}
|
|
859
|
-
|
|
860
|
-
await
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
2200
|
+
case "save_group": {
|
|
2201
|
+
const { archiveStore } = await import("./tab-group-archive.js");
|
|
2202
|
+
const groupId = args?.groupId;
|
|
2203
|
+
const taskThreadId = args?.taskThreadId;
|
|
2204
|
+
const closeTabs = args?.closeTabs;
|
|
2205
|
+
if (groupId === undefined || !taskThreadId) {
|
|
2206
|
+
return {
|
|
2207
|
+
content: [
|
|
2208
|
+
{
|
|
2209
|
+
type: "text",
|
|
2210
|
+
text: "Error: groupId and taskThreadId required for save_group",
|
|
2211
|
+
},
|
|
2212
|
+
],
|
|
2213
|
+
isError: true,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const group = await tabGroupsClient.getGroup(groupId);
|
|
2217
|
+
const allTabs = await tabGroupsClient.listTabs();
|
|
2218
|
+
const groupTabs = allTabs.filter((t) => t.groupId === groupId);
|
|
2219
|
+
const entry = {
|
|
2220
|
+
taskThreadId,
|
|
2221
|
+
title: group.title,
|
|
2222
|
+
color: group.color,
|
|
2223
|
+
collapsed: group.collapsed,
|
|
2224
|
+
urls: groupTabs.map((t) => ({ url: t.url, title: t.title })),
|
|
2225
|
+
archivedAt: new Date().toISOString(),
|
|
2226
|
+
status: closeTabs ? "archived" : "saved",
|
|
2227
|
+
};
|
|
2228
|
+
await archiveStore.save(entry);
|
|
2229
|
+
if (closeTabs) {
|
|
2230
|
+
for (const t of groupTabs) {
|
|
2231
|
+
try {
|
|
2232
|
+
await tabGroupsClient.ungroupTabs([t.id]);
|
|
2233
|
+
}
|
|
2234
|
+
catch {
|
|
2235
|
+
/* tab may already be closed */
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return {
|
|
2240
|
+
content: [
|
|
2241
|
+
{
|
|
2242
|
+
type: "text",
|
|
2243
|
+
text: `Saved ${groupTabs.length} tab(s) for thread ${taskThreadId} (status: ${entry.status})`,
|
|
2244
|
+
},
|
|
2245
|
+
],
|
|
2246
|
+
};
|
|
865
2247
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
2248
|
+
case "restore_group": {
|
|
2249
|
+
const { archiveStore } = await import("./tab-group-archive.js");
|
|
2250
|
+
const taskThreadId = args?.taskThreadId;
|
|
2251
|
+
if (!taskThreadId) {
|
|
2252
|
+
return {
|
|
2253
|
+
content: [
|
|
2254
|
+
{ type: "text", text: "Error: taskThreadId required for restore_group" },
|
|
2255
|
+
],
|
|
2256
|
+
isError: true,
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const entry = await archiveStore.restore(taskThreadId);
|
|
2260
|
+
if (!entry) {
|
|
2261
|
+
return {
|
|
2262
|
+
content: [
|
|
2263
|
+
{ type: "text", text: `No archive found for thread ${taskThreadId}` },
|
|
2264
|
+
],
|
|
2265
|
+
isError: true,
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
const tabIds = [];
|
|
2269
|
+
for (const u of entry.urls) {
|
|
2270
|
+
const tabId = await tabGroupsClient.createTab(u.url, false);
|
|
2271
|
+
if (typeof tabId === "number")
|
|
2272
|
+
tabIds.push(tabId);
|
|
2273
|
+
}
|
|
2274
|
+
if (tabIds.length > 0) {
|
|
2275
|
+
await tabGroupsClient.createGroup({
|
|
2276
|
+
tabIds,
|
|
2277
|
+
title: entry.title,
|
|
2278
|
+
color: entry.color,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
content: [
|
|
2283
|
+
{
|
|
2284
|
+
type: "text",
|
|
2285
|
+
text: `Restored ${tabIds.length} tab(s) for thread ${taskThreadId}`,
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
2288
|
+
};
|
|
874
2289
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
taskThreadId
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
await tabGroupsClient.
|
|
2290
|
+
case "archive_group": {
|
|
2291
|
+
const { archiveStore } = await import("./tab-group-archive.js");
|
|
2292
|
+
const groupId = args?.groupId;
|
|
2293
|
+
const taskThreadId = args?.taskThreadId;
|
|
2294
|
+
if (groupId === undefined || !taskThreadId) {
|
|
2295
|
+
return {
|
|
2296
|
+
content: [
|
|
2297
|
+
{
|
|
2298
|
+
type: "text",
|
|
2299
|
+
text: "Error: groupId and taskThreadId required for archive_group",
|
|
2300
|
+
},
|
|
2301
|
+
],
|
|
2302
|
+
isError: true,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
const group = await tabGroupsClient.getGroup(groupId);
|
|
2306
|
+
const allTabs = await tabGroupsClient.listTabs();
|
|
2307
|
+
const groupTabs = allTabs.filter((t) => t.groupId === groupId);
|
|
2308
|
+
const entry = {
|
|
2309
|
+
taskThreadId,
|
|
2310
|
+
title: group.title,
|
|
2311
|
+
color: group.color,
|
|
2312
|
+
collapsed: group.collapsed,
|
|
2313
|
+
urls: groupTabs.map((t) => ({ url: t.url, title: t.title })),
|
|
2314
|
+
archivedAt: new Date().toISOString(),
|
|
2315
|
+
status: "archived",
|
|
2316
|
+
};
|
|
2317
|
+
await archiveStore.save(entry);
|
|
2318
|
+
const tabIdsToClose = groupTabs.map((t) => t.id);
|
|
2319
|
+
if (tabIdsToClose.length > 0) {
|
|
2320
|
+
await tabGroupsClient.closeTabs(tabIdsToClose);
|
|
2321
|
+
}
|
|
2322
|
+
return {
|
|
2323
|
+
content: [
|
|
2324
|
+
{
|
|
2325
|
+
type: "text",
|
|
2326
|
+
text: `Archived ${groupTabs.length} tab(s) for thread ${taskThreadId} (tabs closed)`,
|
|
2327
|
+
},
|
|
2328
|
+
],
|
|
2329
|
+
};
|
|
891
2330
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
2331
|
+
case "list_archived": {
|
|
2332
|
+
const { archiveStore } = await import("./tab-group-archive.js");
|
|
2333
|
+
const entries = await archiveStore.loadAll();
|
|
2334
|
+
if (entries.length === 0) {
|
|
2335
|
+
return { content: [{ type: "text", text: "No archived tab groups." }] };
|
|
2336
|
+
}
|
|
2337
|
+
const lines = entries.map((e) => {
|
|
2338
|
+
const date = e.archivedAt
|
|
2339
|
+
? new Date(e.archivedAt).toLocaleDateString()
|
|
2340
|
+
: "unknown";
|
|
2341
|
+
return `[${e.taskThreadId}] "${e.title || "(untitled)"}" (${e.urls.length} tabs, ${e.status}, archived ${date})`;
|
|
2342
|
+
});
|
|
2343
|
+
return {
|
|
2344
|
+
content: [
|
|
2345
|
+
{
|
|
2346
|
+
type: "text",
|
|
2347
|
+
text: `Archived tab groups (${entries.length}):\n${lines.join("\n")}`,
|
|
2348
|
+
},
|
|
2349
|
+
],
|
|
2350
|
+
};
|
|
899
2351
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
2352
|
+
default:
|
|
2353
|
+
return {
|
|
2354
|
+
content: [
|
|
2355
|
+
{
|
|
2356
|
+
type: "text",
|
|
2357
|
+
text: `Unknown action: ${action}. Use: list, list_tabs, create, update, move, ungroup, delete, save_group, restore_group, archive_group, list_archived`,
|
|
2358
|
+
},
|
|
2359
|
+
],
|
|
2360
|
+
isError: true,
|
|
2361
|
+
};
|
|
905
2362
|
}
|
|
906
|
-
|
|
2363
|
+
}
|
|
2364
|
+
catch (tgError) {
|
|
2365
|
+
const msg = tgError instanceof Error ? tgError.message : String(tgError);
|
|
2366
|
+
if (msg.includes("extension") ||
|
|
2367
|
+
msg.includes("service worker") ||
|
|
2368
|
+
msg.includes("Bridge")) {
|
|
907
2369
|
return {
|
|
908
|
-
content: [
|
|
2370
|
+
content: [
|
|
2371
|
+
{
|
|
2372
|
+
type: "text",
|
|
2373
|
+
text: `Tab Groups Bridge extension not connected.\n\n` +
|
|
2374
|
+
`To use tab groups:\n` +
|
|
2375
|
+
`1. Open comet://extensions in Comet\n` +
|
|
2376
|
+
`2. Enable "Developer mode"\n` +
|
|
2377
|
+
`3. Click "Load unpacked" and select the extension/ folder from comet-mcp\n` +
|
|
2378
|
+
`4. Try again\n\n` +
|
|
2379
|
+
`Error: ${msg}`,
|
|
2380
|
+
},
|
|
2381
|
+
],
|
|
909
2382
|
isError: true,
|
|
910
2383
|
};
|
|
2384
|
+
}
|
|
2385
|
+
throw tgError;
|
|
911
2386
|
}
|
|
912
2387
|
}
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
return {
|
|
917
|
-
content: [{
|
|
918
|
-
type: "text",
|
|
919
|
-
text: `Tab Groups Bridge extension not connected.\n\n` +
|
|
920
|
-
`To use tab groups:\n` +
|
|
921
|
-
`1. Open comet://extensions in Comet\n` +
|
|
922
|
-
`2. Enable "Developer mode"\n` +
|
|
923
|
-
`3. Click "Load unpacked" and select the extension/ folder from comet-mcp\n` +
|
|
924
|
-
`4. Try again\n\n` +
|
|
925
|
-
`Error: ${msg}`,
|
|
926
|
-
}],
|
|
927
|
-
isError: true,
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
throw tgError;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
case "comet_lifecycle_start": {
|
|
934
|
-
const result = await callLifecycleEndpoint({
|
|
935
|
-
action: "start",
|
|
936
|
-
runId: args?.runId,
|
|
937
|
-
taskThreadId: args?.taskThreadId,
|
|
938
|
-
agentId: args?.agentId,
|
|
939
|
-
route: args?.route || "mcp",
|
|
940
|
-
deferred: args?.deferred,
|
|
941
|
-
});
|
|
942
|
-
try {
|
|
943
|
-
const env = createMCPLifecycleEnvelope({
|
|
2388
|
+
case "comet_lifecycle_start": {
|
|
2389
|
+
const result = await callLifecycleEndpoint({
|
|
2390
|
+
action: "start",
|
|
944
2391
|
runId: args?.runId,
|
|
945
2392
|
taskThreadId: args?.taskThreadId,
|
|
946
2393
|
agentId: args?.agentId,
|
|
947
|
-
|
|
2394
|
+
route: args?.route || "mcp",
|
|
2395
|
+
deferred: args?.deferred,
|
|
948
2396
|
});
|
|
949
|
-
|
|
2397
|
+
const binding = await attachRunIdToCurrentBinding(args?.runId, args?.bindingId);
|
|
2398
|
+
try {
|
|
2399
|
+
const env = createMCPLifecycleEnvelope({
|
|
2400
|
+
runId: args?.runId,
|
|
2401
|
+
taskThreadId: args?.taskThreadId,
|
|
2402
|
+
agentId: args?.agentId,
|
|
2403
|
+
toolName: "comet_lifecycle_start",
|
|
2404
|
+
});
|
|
2405
|
+
emitLifecycleEvent("start", env, { persist: true });
|
|
2406
|
+
}
|
|
2407
|
+
catch {
|
|
2408
|
+
/* graceful degradation — HTTP result already persisted */
|
|
2409
|
+
}
|
|
2410
|
+
// Bridge to JSONL outbox for orchestration
|
|
2411
|
+
appendJsonl(OUTBOX_PATH, {
|
|
2412
|
+
ts: Math.floor(Date.now() / 1000),
|
|
2413
|
+
from: "comet-browser",
|
|
2414
|
+
to: "orchestration",
|
|
2415
|
+
type: "update",
|
|
2416
|
+
task: args?.runId,
|
|
2417
|
+
thread: args?.taskThreadId,
|
|
2418
|
+
msg: `Lifecycle started: run=${args?.runId}`,
|
|
2419
|
+
lifecycle: {
|
|
2420
|
+
action: "start",
|
|
2421
|
+
runId: args?.runId,
|
|
2422
|
+
status: "started",
|
|
2423
|
+
bindingId: binding?.bindingId ?? null,
|
|
2424
|
+
},
|
|
2425
|
+
});
|
|
2426
|
+
return {
|
|
2427
|
+
content: [
|
|
2428
|
+
{
|
|
2429
|
+
type: "text",
|
|
2430
|
+
text: binding
|
|
2431
|
+
? `${result}\nBinding attached: ${binding.bindingId}`
|
|
2432
|
+
: `${result}\nBinding attached: none`,
|
|
2433
|
+
},
|
|
2434
|
+
],
|
|
2435
|
+
};
|
|
950
2436
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
ts: Math.floor(Date.now() / 1000),
|
|
955
|
-
from: "comet-browser",
|
|
956
|
-
to: "orchestration",
|
|
957
|
-
type: "update",
|
|
958
|
-
task: args?.runId,
|
|
959
|
-
thread: args?.taskThreadId,
|
|
960
|
-
msg: `Lifecycle started: run=${args?.runId}`,
|
|
961
|
-
lifecycle: { action: "start", runId: args?.runId, status: "started" },
|
|
962
|
-
});
|
|
963
|
-
return { content: [{ type: "text", text: result }] };
|
|
964
|
-
}
|
|
965
|
-
case "comet_lifecycle_complete": {
|
|
966
|
-
const result = await callLifecycleEndpoint({
|
|
967
|
-
action: "complete",
|
|
968
|
-
runId: args?.runId,
|
|
969
|
-
});
|
|
970
|
-
try {
|
|
971
|
-
const env = createMCPLifecycleEnvelope({
|
|
2437
|
+
case "comet_lifecycle_complete": {
|
|
2438
|
+
const result = await callLifecycleEndpoint({
|
|
2439
|
+
action: "complete",
|
|
972
2440
|
runId: args?.runId,
|
|
973
|
-
toolName: "comet_lifecycle_complete",
|
|
974
2441
|
});
|
|
975
|
-
|
|
2442
|
+
const binding = await transitionBindingByRunId(args?.runId, "completed");
|
|
2443
|
+
try {
|
|
2444
|
+
const env = createMCPLifecycleEnvelope({
|
|
2445
|
+
runId: args?.runId,
|
|
2446
|
+
toolName: "comet_lifecycle_complete",
|
|
2447
|
+
});
|
|
2448
|
+
emitLifecycleEvent("complete", env, { persist: true });
|
|
2449
|
+
}
|
|
2450
|
+
catch {
|
|
2451
|
+
/* graceful degradation */
|
|
2452
|
+
}
|
|
2453
|
+
// Bridge to JSONL outbox
|
|
2454
|
+
appendJsonl(OUTBOX_PATH, {
|
|
2455
|
+
ts: Math.floor(Date.now() / 1000),
|
|
2456
|
+
from: "comet-browser",
|
|
2457
|
+
to: "orchestration",
|
|
2458
|
+
type: "complete",
|
|
2459
|
+
task: args?.runId,
|
|
2460
|
+
msg: `Lifecycle completed: run=${args?.runId}`,
|
|
2461
|
+
lifecycle: {
|
|
2462
|
+
action: "complete",
|
|
2463
|
+
runId: args?.runId,
|
|
2464
|
+
status: "completed",
|
|
2465
|
+
bindingId: binding?.bindingId ?? null,
|
|
2466
|
+
},
|
|
2467
|
+
});
|
|
2468
|
+
return {
|
|
2469
|
+
content: [
|
|
2470
|
+
{
|
|
2471
|
+
type: "text",
|
|
2472
|
+
text: binding
|
|
2473
|
+
? `${result}\nBinding transitioned: ${binding.bindingId} -> completed`
|
|
2474
|
+
: `${result}\nBinding transitioned: none`,
|
|
2475
|
+
},
|
|
2476
|
+
],
|
|
2477
|
+
};
|
|
976
2478
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
ts: Math.floor(Date.now() / 1000),
|
|
981
|
-
from: "comet-browser",
|
|
982
|
-
to: "orchestration",
|
|
983
|
-
type: "complete",
|
|
984
|
-
task: args?.runId,
|
|
985
|
-
msg: `Lifecycle completed: run=${args?.runId}`,
|
|
986
|
-
lifecycle: { action: "complete", runId: args?.runId, status: "completed" },
|
|
987
|
-
});
|
|
988
|
-
return { content: [{ type: "text", text: result }] };
|
|
989
|
-
}
|
|
990
|
-
case "comet_lifecycle_abort": {
|
|
991
|
-
const result = await callLifecycleEndpoint({
|
|
992
|
-
action: "abort",
|
|
993
|
-
runId: args?.runId,
|
|
994
|
-
reason: args?.reason,
|
|
995
|
-
});
|
|
996
|
-
try {
|
|
997
|
-
const env = createMCPLifecycleEnvelope({
|
|
2479
|
+
case "comet_lifecycle_abort": {
|
|
2480
|
+
const result = await callLifecycleEndpoint({
|
|
2481
|
+
action: "abort",
|
|
998
2482
|
runId: args?.runId,
|
|
999
|
-
|
|
2483
|
+
reason: args?.reason,
|
|
2484
|
+
});
|
|
2485
|
+
const binding = await transitionBindingByRunId(args?.runId, "stale");
|
|
2486
|
+
try {
|
|
2487
|
+
const env = createMCPLifecycleEnvelope({
|
|
2488
|
+
runId: args?.runId,
|
|
2489
|
+
toolName: "comet_lifecycle_abort",
|
|
2490
|
+
});
|
|
2491
|
+
emitLifecycleEvent("abort", env, { persist: true });
|
|
2492
|
+
}
|
|
2493
|
+
catch {
|
|
2494
|
+
/* graceful degradation */
|
|
2495
|
+
}
|
|
2496
|
+
// Bridge to JSONL outbox
|
|
2497
|
+
appendJsonl(OUTBOX_PATH, {
|
|
2498
|
+
ts: Math.floor(Date.now() / 1000),
|
|
2499
|
+
from: "comet-browser",
|
|
2500
|
+
to: "orchestration",
|
|
2501
|
+
type: "blocked",
|
|
2502
|
+
task: args?.runId,
|
|
2503
|
+
msg: `Lifecycle aborted: run=${args?.runId}${args?.reason ? ` reason=${args.reason}` : ""}`,
|
|
2504
|
+
lifecycle: {
|
|
2505
|
+
action: "abort",
|
|
2506
|
+
runId: args?.runId,
|
|
2507
|
+
status: "aborted",
|
|
2508
|
+
reason: args?.reason,
|
|
2509
|
+
bindingId: binding?.bindingId ?? null,
|
|
2510
|
+
},
|
|
1000
2511
|
});
|
|
1001
|
-
|
|
2512
|
+
return {
|
|
2513
|
+
content: [
|
|
2514
|
+
{
|
|
2515
|
+
type: "text",
|
|
2516
|
+
text: binding
|
|
2517
|
+
? `${result}\nBinding transitioned: ${binding.bindingId} -> stale`
|
|
2518
|
+
: `${result}\nBinding transitioned: none`,
|
|
2519
|
+
},
|
|
2520
|
+
],
|
|
2521
|
+
};
|
|
1002
2522
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
ts: Math.floor(Date.now() / 1000),
|
|
1007
|
-
from: "comet-browser",
|
|
1008
|
-
to: "orchestration",
|
|
1009
|
-
type: "blocked",
|
|
1010
|
-
task: args?.runId,
|
|
1011
|
-
msg: `Lifecycle aborted: run=${args?.runId}${args?.reason ? ` reason=${args.reason}` : ""}`,
|
|
1012
|
-
lifecycle: { action: "abort", runId: args?.runId, status: "aborted", reason: args?.reason },
|
|
1013
|
-
});
|
|
1014
|
-
return { content: [{ type: "text", text: result }] };
|
|
1015
|
-
}
|
|
1016
|
-
case "comet_lifecycle_update": {
|
|
1017
|
-
const result = await callLifecycleEndpoint({
|
|
1018
|
-
action: "update",
|
|
1019
|
-
runId: args?.runId,
|
|
1020
|
-
auditSessionId: args?.auditSessionId,
|
|
1021
|
-
tabGroupId: args?.tabGroupId,
|
|
1022
|
-
workflowId: args?.workflowId,
|
|
1023
|
-
metadata: args?.metadata,
|
|
1024
|
-
});
|
|
1025
|
-
try {
|
|
1026
|
-
const env = createMCPLifecycleEnvelope({
|
|
2523
|
+
case "comet_lifecycle_update": {
|
|
2524
|
+
const result = await callLifecycleEndpoint({
|
|
2525
|
+
action: "update",
|
|
1027
2526
|
runId: args?.runId,
|
|
1028
|
-
|
|
2527
|
+
auditSessionId: args?.auditSessionId,
|
|
2528
|
+
tabGroupId: args?.tabGroupId,
|
|
2529
|
+
workflowId: args?.workflowId,
|
|
2530
|
+
metadata: args?.metadata,
|
|
1029
2531
|
});
|
|
1030
|
-
|
|
2532
|
+
try {
|
|
2533
|
+
const env = createMCPLifecycleEnvelope({
|
|
2534
|
+
runId: args?.runId,
|
|
2535
|
+
toolName: "comet_lifecycle_update",
|
|
2536
|
+
});
|
|
2537
|
+
emitLifecycleEvent("update", env, { persist: true });
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
/* graceful degradation */
|
|
2541
|
+
}
|
|
2542
|
+
return { content: [{ type: "text", text: result }] };
|
|
1031
2543
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
2544
|
+
case "comet_task_status": {
|
|
2545
|
+
const bindingId = args?.bindingId;
|
|
2546
|
+
const sessionKey = args?.sessionKey;
|
|
2547
|
+
const projectThreadId = args?.projectThreadId;
|
|
2548
|
+
const runId = args?.runId;
|
|
2549
|
+
const sidecarResultId = args?.sidecarResultId;
|
|
2550
|
+
const sidecarContextKey = args?.sidecarContextKey;
|
|
2551
|
+
const groupId = args?.groupId;
|
|
2552
|
+
const threadId = args?.threadId;
|
|
2553
|
+
if (!bindingId &&
|
|
2554
|
+
!sessionKey &&
|
|
2555
|
+
!projectThreadId &&
|
|
2556
|
+
!runId &&
|
|
2557
|
+
!sidecarResultId &&
|
|
2558
|
+
!sidecarContextKey &&
|
|
2559
|
+
!groupId &&
|
|
2560
|
+
!threadId) {
|
|
2561
|
+
return {
|
|
2562
|
+
content: [
|
|
2563
|
+
{
|
|
2564
|
+
type: "text",
|
|
2565
|
+
text: "Error: provide bindingId, sessionKey, projectThreadId, runId, sidecarResultId, sidecarContextKey, groupId, or threadId",
|
|
2566
|
+
},
|
|
2567
|
+
],
|
|
2568
|
+
isError: true,
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
const sidecarArtifact = sidecarResultId
|
|
2572
|
+
? await sidecarArtifactStore.get(sidecarResultId)
|
|
2573
|
+
: sidecarContextKey
|
|
2574
|
+
? await sidecarArtifactStore.latestForContext(sidecarContextKey)
|
|
2575
|
+
: null;
|
|
2576
|
+
const authorizedBinding = sidecarArtifact
|
|
2577
|
+
? await resolveBoundSession(sessionRegistry.getCurrent(), {
|
|
2578
|
+
bindingId: sidecarArtifact.bindingId,
|
|
2579
|
+
})
|
|
2580
|
+
: null;
|
|
2581
|
+
const manifest = readJsonSafe(MANIFEST_PATH);
|
|
2582
|
+
const sessions = manifest?.sessions || [];
|
|
2583
|
+
let liveTabCountsByGroupId = new Map();
|
|
2584
|
+
try {
|
|
2585
|
+
const { tabGroupsClient } = await import("./tab-groups.js");
|
|
2586
|
+
const liveTabs = await tabGroupsClient.listTabs();
|
|
2587
|
+
liveTabCountsByGroupId = liveTabs.reduce((counts, tab) => {
|
|
2588
|
+
if (typeof tab.groupId === "number" && tab.groupId !== -1) {
|
|
2589
|
+
counts.set(tab.groupId, (counts.get(tab.groupId) ?? 0) + 1);
|
|
2590
|
+
}
|
|
2591
|
+
return counts;
|
|
2592
|
+
}, new Map());
|
|
2593
|
+
}
|
|
2594
|
+
catch {
|
|
2595
|
+
/* Extension data unavailable; fall back to manifest-only status. */
|
|
2596
|
+
}
|
|
2597
|
+
// Filter sessions. Multiple query keys narrow the result; they do not broaden it.
|
|
2598
|
+
const matched = sessions.filter((s) => {
|
|
2599
|
+
const checks = [];
|
|
2600
|
+
if (bindingId)
|
|
2601
|
+
checks.push(s.codexBinding?.bindingId === bindingId);
|
|
2602
|
+
if (sessionKey)
|
|
2603
|
+
checks.push(s.sessionKey === sessionKey);
|
|
2604
|
+
if (projectThreadId)
|
|
2605
|
+
checks.push(s.codexBinding?.projectThreadId === projectThreadId);
|
|
2606
|
+
if (runId) {
|
|
2607
|
+
checks.push(Array.isArray(s.codexBinding?.runIds) && s.codexBinding.runIds.includes(runId));
|
|
2608
|
+
}
|
|
2609
|
+
if (groupId !== undefined)
|
|
2610
|
+
checks.push(s.tabGroupId === groupId);
|
|
2611
|
+
if (threadId)
|
|
2612
|
+
checks.push(s.taskThreadId === threadId);
|
|
2613
|
+
return checks.length > 0 && checks.every(Boolean);
|
|
2614
|
+
});
|
|
2615
|
+
if (matched.length === 0) {
|
|
2616
|
+
if (sidecarArtifact) {
|
|
2617
|
+
return {
|
|
2618
|
+
content: [
|
|
2619
|
+
{
|
|
2620
|
+
type: "text",
|
|
2621
|
+
text: JSON.stringify({
|
|
2622
|
+
bindingId: authorizedBinding?.binding.bindingId,
|
|
2623
|
+
sidecarArtifact,
|
|
2624
|
+
}, null, 2),
|
|
2625
|
+
},
|
|
2626
|
+
],
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
content: [
|
|
2631
|
+
{
|
|
2632
|
+
type: "text",
|
|
2633
|
+
text: `No sessions found for binding-scoped query ${JSON.stringify({ bindingId, sessionKey, projectThreadId, runId, sidecarResultId, sidecarContextKey, groupId, threadId })}`,
|
|
2634
|
+
},
|
|
2635
|
+
],
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
// Try to get recent ring buffer events via CDP
|
|
2639
|
+
let recentEvents = [];
|
|
2640
|
+
try {
|
|
2641
|
+
const { tabGroupsClient } = await import("./tab-groups.js");
|
|
2642
|
+
// Reuse the tab groups client's CDP path to find service worker
|
|
2643
|
+
const response = await fetch(`http://127.0.0.1:9222/json/list`);
|
|
2644
|
+
const targets = (await response.json());
|
|
2645
|
+
const sw = targets.find((t) => t.type === "service_worker" && t.webSocketDebuggerUrl);
|
|
2646
|
+
if (sw) {
|
|
2647
|
+
// We can't easily eval here without ws, so skip ring buffer in MCP context
|
|
2648
|
+
// Ring buffer events are aggregated by comet-event-aggregator.py instead
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
catch {
|
|
2652
|
+
/* CDP not available */
|
|
2653
|
+
}
|
|
2654
|
+
const result = matched.map((s) => ({
|
|
2655
|
+
bindingId: s.codexBinding?.bindingId ?? null,
|
|
2656
|
+
sessionKey: s.sessionKey,
|
|
2657
|
+
groupId: s.tabGroupId,
|
|
2658
|
+
threadId: s.taskThreadId,
|
|
2659
|
+
status: s.agent?.status || "unknown",
|
|
2660
|
+
agentId: s.agentId || s.agent?.id || null,
|
|
2661
|
+
tabs: typeof s.tabGroupId === "number" && liveTabCountsByGroupId.has(s.tabGroupId)
|
|
2662
|
+
? liveTabCountsByGroupId.get(s.tabGroupId)
|
|
2663
|
+
: s.tabs?.length || 0,
|
|
2664
|
+
metrics: s.metrics || {},
|
|
2665
|
+
links: s.links || {},
|
|
2666
|
+
sidecarArtifact: sidecarArtifact?.bindingId === s.codexBinding?.bindingId ? sidecarArtifact : null,
|
|
2667
|
+
lastActivity: s.metrics?.lastActivityAt || null,
|
|
2668
|
+
}));
|
|
2669
|
+
return {
|
|
2670
|
+
content: [
|
|
2671
|
+
{
|
|
2672
|
+
type: "text",
|
|
2673
|
+
text: JSON.stringify(result.length === 1 ? result[0] : result, null, 2),
|
|
2674
|
+
},
|
|
2675
|
+
],
|
|
2676
|
+
};
|
|
1040
2677
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2678
|
+
case "comet_delegate": {
|
|
2679
|
+
const threadId = args?.threadId;
|
|
2680
|
+
const instruction = args?.instruction;
|
|
2681
|
+
const priority = args?.priority || "P3";
|
|
2682
|
+
const urls = args?.urls || [];
|
|
2683
|
+
const dependsOn = args?.dependsOn || [];
|
|
2684
|
+
const agentId = args?.agentId || "comet-mcp";
|
|
2685
|
+
if (!threadId || !instruction) {
|
|
2686
|
+
return {
|
|
2687
|
+
content: [{ type: "text", text: "Error: threadId and instruction are required" }],
|
|
2688
|
+
isError: true,
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
const taskId = `comet-${Date.now()}`;
|
|
2692
|
+
const priorityColors = {
|
|
2693
|
+
P1: "red",
|
|
2694
|
+
P2: "yellow",
|
|
2695
|
+
P3: "blue",
|
|
2696
|
+
P4: "grey",
|
|
2697
|
+
};
|
|
2698
|
+
const color = priorityColors[priority] || "blue";
|
|
2699
|
+
// 1. Write to inbox-comet.jsonl
|
|
2700
|
+
appendJsonl(INBOX_PATH, {
|
|
2701
|
+
ts: Math.floor(Date.now() / 1000),
|
|
2702
|
+
from: "comet-delegate",
|
|
2703
|
+
to: "comet-browser",
|
|
2704
|
+
task: taskId,
|
|
2705
|
+
type: "instruction",
|
|
2706
|
+
thread: threadId,
|
|
2707
|
+
priority,
|
|
2708
|
+
msg: instruction,
|
|
2709
|
+
browser_task: {
|
|
2710
|
+
thread_id: threadId,
|
|
2711
|
+
group_name: threadId.slice(0, 50),
|
|
2712
|
+
color,
|
|
2713
|
+
priority,
|
|
2714
|
+
urls,
|
|
2715
|
+
depends_on: dependsOn,
|
|
2716
|
+
},
|
|
2717
|
+
});
|
|
2718
|
+
// 2. Start lifecycle tracking
|
|
2719
|
+
let lifecycleResult = "skipped";
|
|
2720
|
+
try {
|
|
2721
|
+
lifecycleResult = await callLifecycleEndpoint({
|
|
2722
|
+
action: "start",
|
|
2723
|
+
runId: taskId,
|
|
2724
|
+
taskThreadId: threadId,
|
|
2725
|
+
agentId,
|
|
2726
|
+
route: "mcp",
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
catch {
|
|
2730
|
+
/* CC-SO may not be running */
|
|
2731
|
+
}
|
|
2732
|
+
// 3. Create/reuse the Codex window binding directly.
|
|
2733
|
+
const currentSession = sessionRegistry.getCurrent();
|
|
2734
|
+
const bindingDispatch = await createOrReuseDelegateBinding({
|
|
2735
|
+
taskId,
|
|
2736
|
+
threadId,
|
|
2737
|
+
agentId,
|
|
2738
|
+
currentBinding: currentSession?.codexBinding ?? null,
|
|
2739
|
+
bindingId: args?.bindingId,
|
|
2740
|
+
codexSessionId: args?.codexSessionId ??
|
|
2741
|
+
currentSession?.codexIdentity?.codexSessionId,
|
|
2742
|
+
projectThreadId: args?.projectThreadId ?? threadId,
|
|
2743
|
+
projectThreadFamily: args?.projectThreadFamily ??
|
|
2744
|
+
currentSession?.codexIdentity?.projectThreadFamily,
|
|
2745
|
+
worktreePath: args?.worktreePath ??
|
|
2746
|
+
currentSession?.codexIdentity?.worktreePath,
|
|
2747
|
+
repoSlug: args?.repoSlug ?? currentSession?.codexIdentity?.repoSlug,
|
|
2748
|
+
branchName: args?.branchName ?? currentSession?.codexIdentity?.branchName,
|
|
2749
|
+
sessionKey: args?.sessionKey ?? currentSession?.codexIdentity?.sessionKey,
|
|
2750
|
+
role: args?.codexSessionRole ?? currentSession?.codexIdentity?.role,
|
|
2751
|
+
windowId: args?.windowId,
|
|
2752
|
+
tabGroupId: args?.tabGroupId ?? args?.groupId,
|
|
2753
|
+
targetId: args?.targetId,
|
|
2754
|
+
});
|
|
2755
|
+
// 4. Bridge to outbox
|
|
2756
|
+
appendJsonl(OUTBOX_PATH, {
|
|
2757
|
+
ts: Math.floor(Date.now() / 1000),
|
|
2758
|
+
from: "comet-browser",
|
|
2759
|
+
to: "orchestration",
|
|
2760
|
+
type: "update",
|
|
2761
|
+
task: taskId,
|
|
2762
|
+
thread: threadId,
|
|
2763
|
+
msg: `Delegated: ${instruction.slice(0, 100)}`,
|
|
2764
|
+
lifecycle: { action: "start", runId: taskId, status: "dispatched" },
|
|
2765
|
+
});
|
|
2766
|
+
const summary = [
|
|
2767
|
+
`Task delegated successfully.`,
|
|
2768
|
+
` Task ID: ${taskId}`,
|
|
2769
|
+
` Thread: ${threadId}`,
|
|
2770
|
+
` Priority: ${priority} (color: ${color})`,
|
|
2771
|
+
` URLs: ${urls.length > 0 ? urls.join(", ") : "(from thread metadata)"}`,
|
|
2772
|
+
` Lifecycle: ${lifecycleResult === "skipped" ? "skipped (CC-SO unavailable)" : "started"}`,
|
|
2773
|
+
` Binding ID: ${bindingDispatch.bindingId ?? "none"}`,
|
|
2774
|
+
` Window ID: ${bindingDispatch.windowId ?? "none"}`,
|
|
2775
|
+
` Tab Group ID: ${bindingDispatch.tabGroupId ?? "none"}`,
|
|
2776
|
+
` Dispatch: ${bindingDispatch.dispatchStatus}`,
|
|
2777
|
+
].join("\n");
|
|
2778
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1053
2779
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
2780
|
+
case "comet_observe": {
|
|
2781
|
+
const { getHealth, getSnapshot, getStatus, getDetail, formatHealth, formatSnapshot } = await import("./observer.js");
|
|
2782
|
+
const observeAction = args?.action;
|
|
2783
|
+
const observeIdentity = sessionRegistry.getCurrent()?.codexIdentity;
|
|
2784
|
+
const observeFilters = {
|
|
2785
|
+
group: args?.group,
|
|
2786
|
+
agentId: args?.agentId,
|
|
2787
|
+
urlPattern: args?.urlPattern,
|
|
2788
|
+
thumbnails: args?.thumbnails || false,
|
|
2789
|
+
codexIdentity: observeIdentity,
|
|
2790
|
+
};
|
|
2791
|
+
switch (observeAction) {
|
|
2792
|
+
case "health": {
|
|
2793
|
+
const health = await getHealth();
|
|
2794
|
+
const isErr = !health.running;
|
|
2795
|
+
return {
|
|
2796
|
+
content: [{ type: "text", text: formatHealth(health) }],
|
|
2797
|
+
...(isErr ? { isError: true } : {}),
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
case "snapshot": {
|
|
2801
|
+
const snapshot = await getSnapshot(observeFilters);
|
|
2802
|
+
if (!snapshot.browser.cdpConnected) {
|
|
2803
|
+
return {
|
|
2804
|
+
content: [
|
|
2805
|
+
{
|
|
2806
|
+
type: "text",
|
|
2807
|
+
text: "Comet browser is not running. Start with: node scripts/session.mjs start --profile oe",
|
|
2808
|
+
},
|
|
2809
|
+
],
|
|
2810
|
+
isError: true,
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
return { content: [{ type: "text", text: formatSnapshot(snapshot) }] };
|
|
2814
|
+
}
|
|
2815
|
+
case "status": {
|
|
2816
|
+
const statusText = await getStatus(observeFilters);
|
|
2817
|
+
const isStatusErr = statusText.includes("not running");
|
|
2818
|
+
return {
|
|
2819
|
+
content: [{ type: "text", text: statusText }],
|
|
2820
|
+
...(isStatusErr ? { isError: true } : {}),
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
case "detail": {
|
|
2824
|
+
if (!observeFilters.group) {
|
|
2825
|
+
return {
|
|
2826
|
+
content: [
|
|
2827
|
+
{
|
|
2828
|
+
type: "text",
|
|
2829
|
+
text: "The 'detail' action requires a 'group' parameter (tab group name).",
|
|
2830
|
+
},
|
|
2831
|
+
],
|
|
2832
|
+
isError: true,
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
const detailText = await getDetail(observeFilters.group, observeFilters);
|
|
2836
|
+
const isDetailErr = detailText.includes("not found") || detailText.includes("not running");
|
|
2837
|
+
return {
|
|
2838
|
+
content: [{ type: "text", text: detailText }],
|
|
2839
|
+
...(isDetailErr ? { isError: true } : {}),
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
default:
|
|
2843
|
+
return {
|
|
2844
|
+
content: [
|
|
2845
|
+
{
|
|
2846
|
+
type: "text",
|
|
2847
|
+
text: `Unknown observe action: ${observeAction}. Use: snapshot, status, detail, or health.`,
|
|
2848
|
+
},
|
|
2849
|
+
],
|
|
2850
|
+
isError: true,
|
|
2851
|
+
};
|
|
1065
2852
|
}
|
|
1066
2853
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
2854
|
+
// ── Safe observation (no session required) ──
|
|
2855
|
+
case "comet_peek": {
|
|
2856
|
+
const peekTargetId = args?.targetId;
|
|
2857
|
+
const peekAction = args?.action;
|
|
2858
|
+
if (!peekTargetId || !peekAction) {
|
|
2859
|
+
return {
|
|
2860
|
+
content: [
|
|
2861
|
+
{
|
|
2862
|
+
type: "text",
|
|
2863
|
+
text: "Error: targetId and action are required. Use comet_observe(action='snapshot') to find target IDs.",
|
|
2864
|
+
},
|
|
2865
|
+
],
|
|
2866
|
+
isError: true,
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
// Resolve the WebSocket URL for this target — raw HTTP, no session needed
|
|
2870
|
+
const peekTargetsResp = await fetch(`http://127.0.0.1:9222/json/list`);
|
|
2871
|
+
if (!peekTargetsResp.ok) {
|
|
2872
|
+
return {
|
|
2873
|
+
content: [
|
|
2874
|
+
{
|
|
2875
|
+
type: "text",
|
|
2876
|
+
text: "Error: Cannot connect to CDP on port 9222. Is Comet browser running?",
|
|
2877
|
+
},
|
|
2878
|
+
],
|
|
2879
|
+
isError: true,
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
const peekTargets = (await peekTargetsResp.json());
|
|
2883
|
+
const peekTarget = peekTargets.find((t) => t.id === peekTargetId);
|
|
2884
|
+
if (!peekTarget) {
|
|
2885
|
+
return {
|
|
2886
|
+
content: [
|
|
2887
|
+
{
|
|
2888
|
+
type: "text",
|
|
2889
|
+
text: `Error: Target ID '${peekTargetId}' not found. Use comet_observe(action='snapshot') to list current targets.`,
|
|
2890
|
+
},
|
|
2891
|
+
],
|
|
2892
|
+
isError: true,
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
if (peekAction === "info") {
|
|
2896
|
+
return {
|
|
2897
|
+
content: [
|
|
2898
|
+
{
|
|
2899
|
+
type: "text",
|
|
2900
|
+
text: `Tab Info (read-only peek)\nTitle: ${peekTarget.title}\nURL: ${peekTarget.url}\nType: ${peekTarget.type}\nTarget ID: ${peekTarget.id}`,
|
|
2901
|
+
},
|
|
2902
|
+
],
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
// Temporarily attach to target for screenshot/read — then immediately disconnect
|
|
2906
|
+
const peekCDP = await (await import("chrome-remote-interface")).default({
|
|
2907
|
+
target: peekTarget.webSocketDebuggerUrl,
|
|
2908
|
+
});
|
|
2909
|
+
try {
|
|
2910
|
+
if (peekAction === "screenshot") {
|
|
2911
|
+
await peekCDP.Page.enable();
|
|
2912
|
+
const ssResult = (await Promise.race([
|
|
2913
|
+
peekCDP.Page.captureScreenshot({ format: "png" }),
|
|
2914
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Screenshot timeout (3s)")), 3000)),
|
|
2915
|
+
]));
|
|
2916
|
+
const peekOutputDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
2917
|
+
mkdirSync(peekOutputDir, { recursive: true });
|
|
2918
|
+
const peekPath = join(peekOutputDir, `peek-${Date.now()}.png`);
|
|
2919
|
+
writeFileSync(peekPath, Buffer.from(ssResult.data, "base64"));
|
|
2920
|
+
return {
|
|
2921
|
+
content: [
|
|
2922
|
+
{
|
|
2923
|
+
type: "text",
|
|
2924
|
+
text: `Screenshot saved: ${peekPath}\nTab: ${peekTarget.title}\nURL: ${peekTarget.url}`,
|
|
2925
|
+
},
|
|
2926
|
+
],
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
if (peekAction === "read") {
|
|
2930
|
+
await peekCDP.Runtime.enable();
|
|
2931
|
+
const readResult = await peekCDP.Runtime.evaluate({
|
|
2932
|
+
expression: `(() => {
|
|
2933
|
+
const title = document.title;
|
|
2934
|
+
const url = window.location.href;
|
|
2935
|
+
const text = document.body?.innerText?.substring(0, 10000) || '';
|
|
2936
|
+
const links = Array.from(document.querySelectorAll('a[href]')).slice(0, 20).map(a => ({ text: a.innerText.trim().substring(0, 50), href: a.href }));
|
|
2937
|
+
return JSON.stringify({ title, url, text, links });
|
|
2938
|
+
})()`,
|
|
2939
|
+
returnByValue: true,
|
|
2940
|
+
});
|
|
2941
|
+
const pageData = JSON.parse(readResult.result.value);
|
|
2942
|
+
return {
|
|
2943
|
+
content: [
|
|
2944
|
+
{
|
|
2945
|
+
type: "text",
|
|
2946
|
+
text: `Page Content (read-only peek)\nTitle: ${pageData.title}\nURL: ${pageData.url}\n\n${pageData.text.substring(0, 5000)}${pageData.text.length > 5000 ? "\n\n... (truncated)" : ""}`,
|
|
2947
|
+
},
|
|
2948
|
+
],
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
return {
|
|
2952
|
+
content: [
|
|
2953
|
+
{
|
|
2954
|
+
type: "text",
|
|
2955
|
+
text: `Unknown peek action: ${peekAction}. Use: screenshot, read, or info.`,
|
|
2956
|
+
},
|
|
2957
|
+
],
|
|
2958
|
+
isError: true,
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
finally {
|
|
2962
|
+
try {
|
|
2963
|
+
await peekCDP.close();
|
|
2964
|
+
}
|
|
2965
|
+
catch {
|
|
2966
|
+
/* ignore */
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
1091
2969
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
2970
|
+
// ── Parity tools ──
|
|
2971
|
+
case "comet_pdf": {
|
|
2972
|
+
const pdfUrl = args?.url;
|
|
2973
|
+
const pdfName = args?.name;
|
|
2974
|
+
const pdfFormat = args?.format || "Letter";
|
|
2975
|
+
const pdfLandscape = args?.landscape || false;
|
|
2976
|
+
const pdfMargin = args?.margin ?? 0.5;
|
|
2977
|
+
const pdfScale = args?.scale || 1;
|
|
2978
|
+
const pdfPrintBg = args?.printBackground !== false;
|
|
2979
|
+
const pdfHideSelectors = args?.hideSelectors;
|
|
2980
|
+
// Navigate if URL provided
|
|
2981
|
+
if (pdfUrl) {
|
|
2982
|
+
await cometClient.navigate(pdfUrl, true, true);
|
|
2983
|
+
}
|
|
2984
|
+
// Hide elements if requested
|
|
2985
|
+
if (pdfHideSelectors) {
|
|
2986
|
+
const selectors = pdfHideSelectors.split(",").map((s) => s.trim());
|
|
2987
|
+
for (const sel of selectors) {
|
|
2988
|
+
await cometClient.evaluate(`document.querySelectorAll(${safeSelector(sel)}).forEach(el => el.style.display = 'none')`);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
// Paper dimensions in inches
|
|
2992
|
+
const paperSizes = {
|
|
2993
|
+
Letter: { width: 8.5, height: 11 },
|
|
2994
|
+
Legal: { width: 8.5, height: 14 },
|
|
2995
|
+
A4: { width: 8.27, height: 11.69 },
|
|
2996
|
+
A3: { width: 11.69, height: 16.54 },
|
|
2997
|
+
Tabloid: { width: 11, height: 17 },
|
|
2998
|
+
};
|
|
2999
|
+
const paper = paperSizes[pdfFormat] || paperSizes.Letter;
|
|
3000
|
+
// Use CDP Page.printToPDF
|
|
3001
|
+
const cdp = cometClient.protocol;
|
|
3002
|
+
const pdfResult = await cdp.send("Page.printToPDF", {
|
|
3003
|
+
landscape: pdfLandscape,
|
|
3004
|
+
printBackground: pdfPrintBg,
|
|
3005
|
+
scale: pdfScale,
|
|
3006
|
+
paperWidth: paper.width,
|
|
3007
|
+
paperHeight: paper.height,
|
|
3008
|
+
marginTop: pdfMargin,
|
|
3009
|
+
marginBottom: pdfMargin,
|
|
3010
|
+
marginLeft: pdfMargin,
|
|
3011
|
+
marginRight: pdfMargin,
|
|
1123
3012
|
});
|
|
3013
|
+
// Save to output directory
|
|
3014
|
+
const outputDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
3015
|
+
mkdirSync(outputDir, { recursive: true });
|
|
3016
|
+
const titleResult = await cometClient.evaluate("document.title");
|
|
3017
|
+
const pageTitle = titleResult.result.value || "page";
|
|
3018
|
+
const safeName = pdfName || pageTitle.replace(/[^a-zA-Z0-9-_]/g, "_").substring(0, 60);
|
|
3019
|
+
const outputPath = join(outputDir, `${safeName}-${Date.now()}.pdf`);
|
|
3020
|
+
const pdfBuffer = Buffer.from(pdfResult.data, "base64");
|
|
3021
|
+
writeFileSync(outputPath, pdfBuffer);
|
|
3022
|
+
return {
|
|
3023
|
+
content: [
|
|
3024
|
+
{
|
|
3025
|
+
type: "text",
|
|
3026
|
+
text: `PDF saved: ${outputPath}\nFormat: ${pdfFormat}${pdfLandscape ? " (landscape)" : ""}\nSize: ${(pdfBuffer.length / 1024).toFixed(1)} KB`,
|
|
3027
|
+
},
|
|
3028
|
+
],
|
|
3029
|
+
};
|
|
1124
3030
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
3031
|
+
case "comet_scrape": {
|
|
3032
|
+
const scrapeUrl = args?.url;
|
|
3033
|
+
const scrapeSelector = args?.selector;
|
|
3034
|
+
const scrapeMode = args?.mode || "text";
|
|
3035
|
+
const scrapeAttr = args?.attr;
|
|
3036
|
+
const scrapeScroll = args?.scroll || false;
|
|
3037
|
+
const scrapeWaitFor = args?.waitFor;
|
|
3038
|
+
// Navigate if URL provided
|
|
3039
|
+
if (scrapeUrl) {
|
|
3040
|
+
await cometClient.navigate(scrapeUrl, true, true);
|
|
3041
|
+
}
|
|
3042
|
+
// Wait for selector if specified
|
|
3043
|
+
if (scrapeWaitFor) {
|
|
3044
|
+
const waitStart = Date.now();
|
|
3045
|
+
while (Date.now() - waitStart < 10000) {
|
|
3046
|
+
const found = await cometClient.evaluate(`!!document.querySelector(${safeSelector(scrapeWaitFor)})`);
|
|
3047
|
+
if (found.result.value)
|
|
3048
|
+
break;
|
|
3049
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
// Auto-scroll for lazy loading
|
|
3053
|
+
if (scrapeScroll) {
|
|
3054
|
+
await cometClient.evaluate(`
|
|
3055
|
+
(async () => {
|
|
3056
|
+
let totalHeight = 0;
|
|
3057
|
+
const distance = 500;
|
|
3058
|
+
while (totalHeight < document.body.scrollHeight && totalHeight < 50000) {
|
|
3059
|
+
window.scrollBy(0, distance);
|
|
3060
|
+
totalHeight += distance;
|
|
3061
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1135
3062
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
3063
|
+
window.scrollTo(0, 0);
|
|
3064
|
+
})()
|
|
3065
|
+
`);
|
|
3066
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
3067
|
+
}
|
|
3068
|
+
// Safe selector strings for browser evaluation
|
|
3069
|
+
const safeSel = safeSelector(scrapeSelector || "body");
|
|
3070
|
+
const safeAttrName = safeSelector(scrapeAttr || "href");
|
|
3071
|
+
let extractionScript;
|
|
3072
|
+
switch (scrapeMode) {
|
|
3073
|
+
case "table":
|
|
3074
|
+
extractionScript = `
|
|
3075
|
+
(() => {
|
|
3076
|
+
const table = document.querySelector(${safeSelector(scrapeSelector || "table")});
|
|
3077
|
+
if (!table) return { error: 'No table found' };
|
|
3078
|
+
const rows = [...table.querySelectorAll('tr')];
|
|
3079
|
+
const headers = [...rows[0]?.querySelectorAll('th, td')].map(c => c.innerText.trim());
|
|
3080
|
+
const data = rows.slice(1).map(row => {
|
|
3081
|
+
const cells = [...row.querySelectorAll('td, th')].map(c => c.innerText.trim());
|
|
3082
|
+
return headers.length ? Object.fromEntries(headers.map((h, i) => [h, cells[i] || ''])) : cells;
|
|
3083
|
+
});
|
|
3084
|
+
return { rows: data.length, headers, data };
|
|
3085
|
+
})()`;
|
|
3086
|
+
break;
|
|
3087
|
+
case "json-ld":
|
|
3088
|
+
extractionScript = `
|
|
3089
|
+
(() => {
|
|
3090
|
+
const scripts = [...document.querySelectorAll('script[type="application/ld+json"]')];
|
|
3091
|
+
const data = scripts.map(s => { try { return JSON.parse(s.textContent); } catch { return null; } }).filter(Boolean);
|
|
3092
|
+
return { count: data.length, data };
|
|
3093
|
+
})()`;
|
|
3094
|
+
break;
|
|
3095
|
+
case "list":
|
|
3096
|
+
extractionScript = `
|
|
3097
|
+
(() => {
|
|
3098
|
+
const sel = ${safeSelector(scrapeSelector || "ul li, ol li")};
|
|
3099
|
+
const items = [...document.querySelectorAll(sel)].map(el => el.innerText.trim());
|
|
3100
|
+
return { count: items.length, items };
|
|
3101
|
+
})()`;
|
|
3102
|
+
break;
|
|
3103
|
+
case "attr":
|
|
3104
|
+
extractionScript = `
|
|
3105
|
+
(() => {
|
|
3106
|
+
const sel = ${safeSel};
|
|
3107
|
+
const attr = ${safeAttrName};
|
|
3108
|
+
const els = [...document.querySelectorAll(sel)];
|
|
3109
|
+
const values = els.map(el => el.getAttribute(attr)).filter(Boolean);
|
|
3110
|
+
return { count: values.length, attribute: attr, values };
|
|
3111
|
+
})()`;
|
|
3112
|
+
break;
|
|
3113
|
+
case "multi":
|
|
3114
|
+
extractionScript = `
|
|
3115
|
+
(() => {
|
|
3116
|
+
const sel = ${safeSelector(scrapeSelector || "p")};
|
|
3117
|
+
const els = [...document.querySelectorAll(sel)];
|
|
3118
|
+
const items = els.map(el => ({ tag: el.tagName, text: el.innerText.trim().substring(0, 500) }));
|
|
3119
|
+
return { count: items.length, items };
|
|
3120
|
+
})()`;
|
|
3121
|
+
break;
|
|
3122
|
+
default: // text
|
|
3123
|
+
extractionScript = `
|
|
3124
|
+
(() => {
|
|
3125
|
+
const sel = ${safeSel};
|
|
3126
|
+
const el = document.querySelector(sel);
|
|
3127
|
+
if (!el) return { error: 'Selector not found: ' + sel };
|
|
3128
|
+
return { tag: el.tagName, text: el.innerText.trim().substring(0, 10000) };
|
|
3129
|
+
})()`;
|
|
3130
|
+
}
|
|
3131
|
+
const scrapeResult = await cometClient.evaluate(extractionScript);
|
|
3132
|
+
const scrapeData = scrapeResult.result.value;
|
|
3133
|
+
return {
|
|
3134
|
+
content: [
|
|
3135
|
+
{
|
|
3136
|
+
type: "text",
|
|
3137
|
+
text: typeof scrapeData === "string" ? scrapeData : JSON.stringify(scrapeData, null, 2),
|
|
3138
|
+
},
|
|
3139
|
+
],
|
|
3140
|
+
};
|
|
1138
3141
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
3142
|
+
case "comet_network": {
|
|
3143
|
+
const netAction = args?.action || "capture";
|
|
3144
|
+
const netUrl = args?.url;
|
|
3145
|
+
const netDuration = args?.duration || 10000;
|
|
3146
|
+
const netFilter = args?.filter;
|
|
3147
|
+
const netResourceType = args?.resourceType;
|
|
3148
|
+
const netPattern = args?.pattern;
|
|
3149
|
+
const netMockResponse = args?.mockResponse;
|
|
3150
|
+
const netMockStatus = args?.mockStatus || 200;
|
|
3151
|
+
const netIncludeHeaders = args?.includeHeaders || false;
|
|
3152
|
+
// CDP.Client extends EventEmitter at runtime but types don't expose on/once/removeListener.
|
|
3153
|
+
// Scoped cast for event methods — only used in this network handler.
|
|
3154
|
+
const cdp = cometClient.protocol;
|
|
3155
|
+
if (netAction === "block") {
|
|
3156
|
+
if (!netPattern) {
|
|
3157
|
+
return {
|
|
3158
|
+
content: [{ type: "text", text: "Error: 'pattern' is required for block action." }],
|
|
3159
|
+
isError: true,
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
await cdp.send("Network.enable");
|
|
3163
|
+
await cdp.send("Network.setBlockedURLs", { urls: [netPattern] });
|
|
3164
|
+
return {
|
|
3165
|
+
content: [
|
|
3166
|
+
{
|
|
3167
|
+
type: "text",
|
|
3168
|
+
text: `Blocking requests matching: ${netPattern}\nUse comet_network(action='block', pattern='') to clear.`,
|
|
3169
|
+
},
|
|
3170
|
+
],
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
if (netAction === "intercept") {
|
|
3174
|
+
if (!netPattern) {
|
|
3175
|
+
return {
|
|
3176
|
+
content: [
|
|
3177
|
+
{ type: "text", text: "Error: 'pattern' is required for intercept action." },
|
|
3178
|
+
],
|
|
3179
|
+
isError: true,
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
await cdp.send("Fetch.enable", {
|
|
3183
|
+
patterns: [{ urlPattern: `*${netPattern}*`, requestStage: "Response" }],
|
|
3184
|
+
});
|
|
3185
|
+
// Set up a one-shot interceptor
|
|
3186
|
+
const interceptPromise = new Promise((resolve) => {
|
|
3187
|
+
const timeout = setTimeout(() => resolve("No matching request within 30s"), 30000);
|
|
3188
|
+
cdp.once("Fetch.requestPaused", async (params) => {
|
|
3189
|
+
clearTimeout(timeout);
|
|
3190
|
+
const body = netMockResponse || '{"mocked":true}';
|
|
3191
|
+
const headers = [
|
|
3192
|
+
{ name: "Content-Type", value: "application/json" },
|
|
3193
|
+
{ name: "Access-Control-Allow-Origin", value: "*" },
|
|
3194
|
+
];
|
|
3195
|
+
await cdp.send("Fetch.fulfillRequest", {
|
|
3196
|
+
requestId: params.requestId,
|
|
3197
|
+
responseCode: netMockStatus,
|
|
3198
|
+
responseHeaders: headers,
|
|
3199
|
+
body: Buffer.from(body).toString("base64"),
|
|
3200
|
+
});
|
|
3201
|
+
resolve(`Intercepted: ${params.request.url}\nResponded with status ${netMockStatus}`);
|
|
3202
|
+
});
|
|
3203
|
+
});
|
|
3204
|
+
const interceptResult = await interceptPromise;
|
|
3205
|
+
await cdp.send("Fetch.disable");
|
|
3206
|
+
return { content: [{ type: "text", text: interceptResult }] };
|
|
3207
|
+
}
|
|
3208
|
+
// capture action
|
|
3209
|
+
if (netUrl) {
|
|
3210
|
+
await cometClient.navigate(netUrl, true, true);
|
|
3211
|
+
}
|
|
3212
|
+
await cdp.send("Network.enable");
|
|
3213
|
+
const captured = [];
|
|
3214
|
+
const responseMap = new Map();
|
|
3215
|
+
const onRequest = (params) => {
|
|
3216
|
+
if (netFilter && !params.request.url.includes(netFilter))
|
|
3217
|
+
return;
|
|
3218
|
+
if (netResourceType && params.type?.toLowerCase() !== netResourceType.toLowerCase())
|
|
3219
|
+
return;
|
|
3220
|
+
captured.push({
|
|
3221
|
+
requestId: params.requestId,
|
|
3222
|
+
method: params.request.method,
|
|
3223
|
+
url: params.request.url,
|
|
3224
|
+
type: params.type,
|
|
3225
|
+
...(netIncludeHeaders ? { requestHeaders: params.request.headers } : {}),
|
|
3226
|
+
});
|
|
3227
|
+
};
|
|
3228
|
+
const onResponse = (params) => {
|
|
3229
|
+
responseMap.set(params.requestId, {
|
|
3230
|
+
status: params.response.status,
|
|
3231
|
+
mimeType: params.response.mimeType,
|
|
3232
|
+
...(netIncludeHeaders ? { responseHeaders: params.response.headers } : {}),
|
|
3233
|
+
});
|
|
3234
|
+
};
|
|
3235
|
+
cdp.on("Network.requestWillBeSent", onRequest);
|
|
3236
|
+
cdp.on("Network.responseReceived", onResponse);
|
|
3237
|
+
await new Promise((r) => setTimeout(r, netDuration));
|
|
3238
|
+
cdp.removeListener("Network.requestWillBeSent", onRequest);
|
|
3239
|
+
cdp.removeListener("Network.responseReceived", onResponse);
|
|
3240
|
+
await cdp.send("Network.disable"); // Clean up CDP domain state after capture
|
|
3241
|
+
// Merge responses into captured requests
|
|
3242
|
+
for (const req of captured) {
|
|
3243
|
+
const resp = responseMap.get(req.requestId);
|
|
3244
|
+
if (resp) {
|
|
3245
|
+
req.status = resp.status;
|
|
3246
|
+
req.mimeType = resp.mimeType;
|
|
3247
|
+
if (resp.responseHeaders)
|
|
3248
|
+
req.responseHeaders = resp.responseHeaders;
|
|
3249
|
+
}
|
|
3250
|
+
delete req.requestId;
|
|
3251
|
+
}
|
|
3252
|
+
const summary = `Captured ${captured.length} requests over ${netDuration}ms${netFilter ? ` (filter: ${netFilter})` : ""}`;
|
|
3253
|
+
return {
|
|
3254
|
+
content: [
|
|
3255
|
+
{
|
|
3256
|
+
type: "text",
|
|
3257
|
+
text: `${summary}\n\n${JSON.stringify(captured.slice(0, 50), null, 2)}${captured.length > 50 ? `\n\n... and ${captured.length - 50} more` : ""}`,
|
|
3258
|
+
},
|
|
3259
|
+
],
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
case "comet_automate": {
|
|
3263
|
+
const autoSteps = args?.steps;
|
|
3264
|
+
const autoVerbose = args?.verbose || false;
|
|
3265
|
+
if (!autoSteps || autoSteps.length === 0) {
|
|
3266
|
+
return {
|
|
3267
|
+
content: [
|
|
3268
|
+
{ type: "text", text: "Error: 'steps' array is required and must not be empty." },
|
|
3269
|
+
],
|
|
3270
|
+
isError: true,
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
const variables = {};
|
|
3274
|
+
const results = [];
|
|
3275
|
+
async function executeStep(step, index) {
|
|
3276
|
+
const prefix = `Step ${index + 1} [${step.tool}]`;
|
|
3277
|
+
try {
|
|
3278
|
+
switch (step.tool) {
|
|
3279
|
+
case "navigate":
|
|
3280
|
+
await cometClient.navigate(step.url, true, true);
|
|
3281
|
+
results.push(`✓ ${prefix}: navigated to ${step.url}`);
|
|
3282
|
+
break;
|
|
3283
|
+
case "click":
|
|
3284
|
+
await cometClient.evaluate(`
|
|
3285
|
+
(() => {
|
|
3286
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
3287
|
+
if (!el) throw new Error('Element not found: ' + ${safeSelector(step.selector)});
|
|
3288
|
+
el.scrollIntoView({ block: 'center' });
|
|
3289
|
+
el.click();
|
|
3290
|
+
return el.innerText?.substring(0, 50) || el.tagName;
|
|
3291
|
+
})()
|
|
3292
|
+
`);
|
|
3293
|
+
results.push(`✓ ${prefix}: clicked ${step.selector}`);
|
|
3294
|
+
break;
|
|
3295
|
+
case "fill": {
|
|
3296
|
+
const safeFillVal = JSON.stringify(step.value || "");
|
|
3297
|
+
await cometClient.evaluate(`
|
|
3298
|
+
(() => {
|
|
3299
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
3300
|
+
if (!el) throw new Error('Element not found: ' + ${safeSelector(step.selector)});
|
|
3301
|
+
const val = ${safeFillVal};
|
|
3302
|
+
const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
|
|
3303
|
+
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
3304
|
+
if (nativeSet) nativeSet.call(el, val);
|
|
3305
|
+
else el.value = val;
|
|
3306
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3307
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3308
|
+
})()
|
|
3309
|
+
`);
|
|
3310
|
+
results.push(`✓ ${prefix}: filled ${step.selector}`);
|
|
3311
|
+
break;
|
|
3312
|
+
}
|
|
3313
|
+
case "type":
|
|
3314
|
+
for (const char of step.value || "") {
|
|
3315
|
+
const cdpInner = cometClient.protocol;
|
|
3316
|
+
await cdpInner.send("Input.dispatchKeyEvent", {
|
|
3317
|
+
type: "keyDown",
|
|
3318
|
+
text: char,
|
|
3319
|
+
});
|
|
3320
|
+
await cdpInner.send("Input.dispatchKeyEvent", {
|
|
3321
|
+
type: "keyUp",
|
|
3322
|
+
text: char,
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
results.push(`✓ ${prefix}: typed ${(step.value || "").length} chars`);
|
|
3326
|
+
break;
|
|
3327
|
+
case "press": {
|
|
3328
|
+
const cdpPress = cometClient.protocol;
|
|
3329
|
+
await cdpPress.send("Input.dispatchKeyEvent", {
|
|
3330
|
+
type: "keyDown",
|
|
3331
|
+
key: step.value,
|
|
3332
|
+
code: `Key${step.value?.toUpperCase()}`,
|
|
3333
|
+
});
|
|
3334
|
+
await cdpPress.send("Input.dispatchKeyEvent", {
|
|
3335
|
+
type: "keyUp",
|
|
3336
|
+
key: step.value,
|
|
3337
|
+
code: `Key${step.value?.toUpperCase()}`,
|
|
3338
|
+
});
|
|
3339
|
+
results.push(`✓ ${prefix}: pressed ${step.value}`);
|
|
3340
|
+
break;
|
|
3341
|
+
}
|
|
3342
|
+
case "select":
|
|
3343
|
+
await cometClient.evaluate(`
|
|
3344
|
+
(() => {
|
|
3345
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
3346
|
+
if (!el) throw new Error('Element not found');
|
|
3347
|
+
el.value = ${JSON.stringify(step.value || "")};
|
|
3348
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3349
|
+
})()
|
|
3350
|
+
`);
|
|
3351
|
+
results.push(`✓ ${prefix}: selected ${step.value}`);
|
|
3352
|
+
break;
|
|
3353
|
+
case "wait":
|
|
3354
|
+
if (step.selector) {
|
|
3355
|
+
const waitMs = Date.now();
|
|
3356
|
+
while (Date.now() - waitMs < 10000) {
|
|
3357
|
+
const found = await cometClient.evaluate(`!!document.querySelector(${safeSelector(step.selector)})`);
|
|
3358
|
+
if (found.result.value)
|
|
3359
|
+
break;
|
|
3360
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
else if (step.value) {
|
|
3364
|
+
await new Promise((r) => setTimeout(r, parseInt(step.value)));
|
|
3365
|
+
}
|
|
3366
|
+
results.push(`✓ ${prefix}: waited for ${step.selector || step.value + "ms"}`);
|
|
3367
|
+
break;
|
|
3368
|
+
case "screenshot": {
|
|
3369
|
+
const cdpScreenshot = cometClient.protocol;
|
|
3370
|
+
const ssResult = await cdpScreenshot.send("Page.captureScreenshot", {
|
|
3371
|
+
format: "png",
|
|
3372
|
+
});
|
|
3373
|
+
const ssDir = join(homedir(), ".claude", "comet-browser", "output");
|
|
3374
|
+
mkdirSync(ssDir, { recursive: true });
|
|
3375
|
+
const ssPath = join(ssDir, `${step.name || "step-" + (index + 1)}-${Date.now()}.png`);
|
|
3376
|
+
writeFileSync(ssPath, Buffer.from(ssResult.data, "base64"));
|
|
3377
|
+
results.push(`✓ ${prefix}: screenshot → ${ssPath}`);
|
|
3378
|
+
break;
|
|
3379
|
+
}
|
|
3380
|
+
case "extract": {
|
|
3381
|
+
const extractResult = await cometClient.evaluate(`
|
|
3382
|
+
(() => {
|
|
3383
|
+
const el = document.querySelector(${safeSelector(step.selector)});
|
|
3384
|
+
if (!el) return null;
|
|
3385
|
+
return el.innerText?.trim() || el.value || '';
|
|
3386
|
+
})()
|
|
3387
|
+
`);
|
|
3388
|
+
const extractedValue = extractResult.result.value;
|
|
3389
|
+
if (step.variable && isValidIdentifier(step.variable))
|
|
3390
|
+
variables[step.variable] = extractedValue;
|
|
3391
|
+
results.push(`✓ ${prefix}: extracted ${String(extractedValue).substring(0, 100)}${step.variable ? ` → $${step.variable}` : ""}`);
|
|
3392
|
+
break;
|
|
3393
|
+
}
|
|
3394
|
+
case "assert": {
|
|
3395
|
+
const assertResult = await cometClient.evaluate(`
|
|
3396
|
+
document.querySelector(${safeSelector(step.selector)})?.innerText || ''
|
|
3397
|
+
`);
|
|
3398
|
+
const assertText = assertResult.result.value;
|
|
3399
|
+
if (step.contains && !assertText.includes(step.contains)) {
|
|
3400
|
+
throw new Error(`Assertion failed: "${step.selector}" does not contain "${step.contains}". Got: "${assertText.substring(0, 200)}"`);
|
|
3401
|
+
}
|
|
3402
|
+
results.push(`✓ ${prefix}: assertion passed`);
|
|
3403
|
+
break;
|
|
3404
|
+
}
|
|
3405
|
+
case "evaluate": {
|
|
3406
|
+
const evalResult = await cometClient.evaluate(step.expression);
|
|
3407
|
+
const evalValue = evalResult.result.value;
|
|
3408
|
+
if (step.variable && isValidIdentifier(step.variable))
|
|
3409
|
+
variables[step.variable] = evalValue;
|
|
3410
|
+
results.push(`✓ ${prefix}: ${JSON.stringify(evalValue).substring(0, 200)}${step.variable ? ` → $${step.variable}` : ""}`);
|
|
3411
|
+
break;
|
|
3412
|
+
}
|
|
3413
|
+
case "if": {
|
|
3414
|
+
// Evaluate condition in browser context (sandboxed) with variable injection
|
|
3415
|
+
const varInjection = Object.entries(variables)
|
|
3416
|
+
.filter(([k]) => isValidIdentifier(k))
|
|
3417
|
+
.map(([k, v]) => `const ${k} = ${JSON.stringify(v)};`)
|
|
3418
|
+
.join(" ");
|
|
3419
|
+
const condEvalResult = await cometClient.evaluate(`(() => { ${varInjection} return !!(${step.condition}); })()`);
|
|
3420
|
+
const condResult = condEvalResult.result.value;
|
|
3421
|
+
if (condResult && step.then) {
|
|
3422
|
+
for (let i = 0; i < step.then.length; i++) {
|
|
3423
|
+
const ok = await executeStep(step.then[i], i);
|
|
3424
|
+
if (!ok)
|
|
3425
|
+
return false;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
results.push(`✓ ${prefix}: condition ${condResult ? "true" : "false"}`);
|
|
3429
|
+
break;
|
|
3430
|
+
}
|
|
3431
|
+
case "loop": {
|
|
3432
|
+
const loopItems = variables[step.items];
|
|
3433
|
+
if (!Array.isArray(loopItems)) {
|
|
3434
|
+
throw new Error(`Variable "${step.items}" is not an array`);
|
|
3435
|
+
}
|
|
3436
|
+
for (let li = 0; li < loopItems.length; li++) {
|
|
3437
|
+
variables["_item"] = loopItems[li];
|
|
3438
|
+
variables["_index"] = li;
|
|
3439
|
+
for (let si = 0; si < (step.each || []).length; si++) {
|
|
3440
|
+
const ok = await executeStep(step.each[si], si);
|
|
3441
|
+
if (!ok)
|
|
3442
|
+
return false;
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
results.push(`✓ ${prefix}: looped ${loopItems.length} items`);
|
|
3446
|
+
break;
|
|
3447
|
+
}
|
|
3448
|
+
default:
|
|
3449
|
+
throw new Error(`Unknown step tool: ${step.tool}`);
|
|
3450
|
+
}
|
|
3451
|
+
// Small pause between steps for DOM settling
|
|
3452
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3453
|
+
return true;
|
|
3454
|
+
}
|
|
3455
|
+
catch (err) {
|
|
3456
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3457
|
+
results.push(`✗ ${prefix}: ${errMsg}`);
|
|
3458
|
+
if (step.optional)
|
|
3459
|
+
return true;
|
|
3460
|
+
return false;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
for (let i = 0; i < autoSteps.length; i++) {
|
|
3464
|
+
const ok = await executeStep(autoSteps[i], i);
|
|
3465
|
+
if (!ok) {
|
|
3466
|
+
results.push(`\n⚠ Workflow aborted at step ${i + 1}`);
|
|
3467
|
+
break;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
const varSummary = Object.keys(variables).length > 0
|
|
3471
|
+
? `\n\nVariables:\n${Object.entries(variables)
|
|
3472
|
+
.filter(([k]) => !k.startsWith("_"))
|
|
3473
|
+
.map(([k, v]) => ` ${k}: ${JSON.stringify(v).substring(0, 200)}`)
|
|
3474
|
+
.join("\n")}`
|
|
3475
|
+
: "";
|
|
3476
|
+
return {
|
|
3477
|
+
content: [
|
|
3478
|
+
{
|
|
3479
|
+
type: "text",
|
|
3480
|
+
text: `Workflow: ${autoSteps.length} steps\n\n${results.join("\n")}${varSummary}`,
|
|
3481
|
+
},
|
|
3482
|
+
],
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
case "comet_domain": {
|
|
3486
|
+
const domainName = args?.domain;
|
|
3487
|
+
const domainAction = args?.action || "check-auth";
|
|
3488
|
+
const domainPath = args?.path;
|
|
3489
|
+
const domainUrls = {
|
|
3490
|
+
qbo: {
|
|
3491
|
+
home: "https://app.qbo.intuit.com/app/homepage",
|
|
3492
|
+
authCheck: "app.qbo.intuit.com",
|
|
3493
|
+
name: "QuickBooks Online",
|
|
3494
|
+
},
|
|
3495
|
+
mercury: {
|
|
3496
|
+
home: "https://app.mercury.com/dashboard",
|
|
3497
|
+
authCheck: "app.mercury.com",
|
|
3498
|
+
name: "Mercury Banking",
|
|
3499
|
+
},
|
|
3500
|
+
github: {
|
|
3501
|
+
home: "https://github.com",
|
|
3502
|
+
authCheck: "github.com",
|
|
3503
|
+
name: "GitHub",
|
|
3504
|
+
},
|
|
3505
|
+
google: {
|
|
3506
|
+
home: "https://drive.google.com",
|
|
3507
|
+
authCheck: "accounts.google.com/SignOut",
|
|
3508
|
+
name: "Google Workspace",
|
|
3509
|
+
},
|
|
3510
|
+
salt: {
|
|
3511
|
+
home: "https://app.salt.dev",
|
|
3512
|
+
authCheck: "app.salt.dev",
|
|
3513
|
+
name: "SALT Tax",
|
|
3514
|
+
},
|
|
3515
|
+
};
|
|
3516
|
+
const domainConfig = domainUrls[domainName];
|
|
3517
|
+
if (!domainConfig) {
|
|
3518
|
+
return {
|
|
3519
|
+
content: [
|
|
3520
|
+
{
|
|
3521
|
+
type: "text",
|
|
3522
|
+
text: `Unknown domain: ${domainName}. Use: qbo, mercury, github, google, salt`,
|
|
3523
|
+
},
|
|
3524
|
+
],
|
|
3525
|
+
isError: true,
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
if (domainAction === "navigate" || domainAction === "status") {
|
|
3529
|
+
const targetUrl = domainPath
|
|
3530
|
+
? new URL(domainPath, domainConfig.home).href
|
|
3531
|
+
: domainConfig.home;
|
|
3532
|
+
await cometClient.navigate(targetUrl, true, true);
|
|
3533
|
+
const finalUrlResult = await cometClient.evaluate("window.location.href");
|
|
3534
|
+
const finalUrl = finalUrlResult.result.value;
|
|
3535
|
+
const titleResult = await cometClient.evaluate("document.title");
|
|
3536
|
+
const title = titleResult.result.value;
|
|
3537
|
+
// Check if we got redirected to login
|
|
3538
|
+
const isLoginPage = finalUrl.includes("login") ||
|
|
3539
|
+
finalUrl.includes("signin") ||
|
|
3540
|
+
finalUrl.includes("auth") ||
|
|
3541
|
+
finalUrl.includes("accounts.google.com/v3/signin");
|
|
3542
|
+
return {
|
|
3543
|
+
content: [
|
|
3544
|
+
{
|
|
3545
|
+
type: "text",
|
|
3546
|
+
text: [
|
|
3547
|
+
`Domain: ${domainConfig.name}`,
|
|
3548
|
+
`URL: ${finalUrl}`,
|
|
3549
|
+
`Title: ${title}`,
|
|
3550
|
+
`Auth: ${isLoginPage ? "❌ NOT LOGGED IN — redirected to login page" : "✓ Authenticated"}`,
|
|
3551
|
+
isLoginPage
|
|
3552
|
+
? `\nAction needed: Log in to ${domainConfig.name} in the Comet browser (profile: oe) before proceeding.`
|
|
3553
|
+
: "",
|
|
3554
|
+
]
|
|
3555
|
+
.filter(Boolean)
|
|
3556
|
+
.join("\n"),
|
|
3557
|
+
},
|
|
3558
|
+
],
|
|
3559
|
+
...(isLoginPage ? { isError: true } : {}),
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
// check-auth: just check current URL without navigating
|
|
3563
|
+
const currentUrlResult = await cometClient.evaluate("window.location.href");
|
|
3564
|
+
const currentUrl = currentUrlResult.result.value;
|
|
3565
|
+
const isOnDomain = currentUrl.includes(domainConfig.authCheck);
|
|
3566
|
+
if (!isOnDomain) {
|
|
3567
|
+
// Navigate to domain to check
|
|
3568
|
+
await cometClient.navigate(domainConfig.home, true, true);
|
|
3569
|
+
const checkUrlResult = await cometClient.evaluate("window.location.href");
|
|
3570
|
+
const checkUrl = checkUrlResult.result.value;
|
|
3571
|
+
const isLoginRedirect = checkUrl.includes("login") ||
|
|
3572
|
+
checkUrl.includes("signin") ||
|
|
3573
|
+
checkUrl.includes("auth");
|
|
3574
|
+
return {
|
|
3575
|
+
content: [
|
|
3576
|
+
{
|
|
3577
|
+
type: "text",
|
|
3578
|
+
text: `${domainConfig.name}: ${isLoginRedirect ? "❌ Not authenticated" : "✓ Authenticated"}\nURL: ${checkUrl}`,
|
|
3579
|
+
},
|
|
3580
|
+
],
|
|
3581
|
+
...(isLoginRedirect ? { isError: true } : {}),
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
return {
|
|
3585
|
+
content: [
|
|
3586
|
+
{
|
|
3587
|
+
type: "text",
|
|
3588
|
+
text: `${domainConfig.name}: ✓ Currently on domain\nURL: ${currentUrl}`,
|
|
3589
|
+
},
|
|
3590
|
+
],
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
default:
|
|
3594
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1160
3595
|
}
|
|
1161
|
-
default:
|
|
1162
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1163
3596
|
}
|
|
3597
|
+
catch (error) {
|
|
3598
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3599
|
+
console.error(`[comet-bridge] Tool "${name}" failed: ${errorMsg}`);
|
|
3600
|
+
return {
|
|
3601
|
+
content: [{ type: "text", text: `Error in ${name}: ${errorMsg}` }],
|
|
3602
|
+
isError: true,
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
})();
|
|
3606
|
+
// Drain queued alerts and append to response (Spec 016, FR-011, T004)
|
|
3607
|
+
const queuedAlerts = drainMcpAlertQueue();
|
|
3608
|
+
if (queuedAlerts.length > 0 && result?.content) {
|
|
3609
|
+
const alertText = formatAlertsForResponse(queuedAlerts);
|
|
3610
|
+
result.content.push({ type: "text", text: alertText });
|
|
1164
3611
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
3612
|
+
// Tab group warning — once per session (Spec 016, T011)
|
|
3613
|
+
if (result?.content && !result.isError) {
|
|
3614
|
+
const tabGroupWarn = getTabGroupWarning(name);
|
|
3615
|
+
if (tabGroupWarn) {
|
|
3616
|
+
const lastContent = result.content[result.content.length - 1];
|
|
3617
|
+
if (lastContent?.type === "text") {
|
|
3618
|
+
lastContent.text += tabGroupWarn;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
1170
3621
|
}
|
|
3622
|
+
await disconnectCdpClientsAfterTool();
|
|
3623
|
+
return result;
|
|
1171
3624
|
});
|
|
1172
3625
|
const transport = new StdioServerTransport();
|
|
1173
3626
|
server.connect(transport);
|