@shawnowen/comet-mcp 2.3.0

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/dist/index.js ADDED
@@ -0,0 +1,1174 @@
1
+ #!/usr/bin/env node
2
+ // Comet Browser MCP Server
3
+ // Claude Code ↔ Perplexity Comet bidirectional interaction
4
+ // 14 tools: 9 browsing + 1 tab groups + 4 lifecycle
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
+ import { cometClient } from "./cdp-client.js";
9
+ import { cometAI } from "./comet-ai.js";
10
+ // @ts-ignore — .mjs adapter for lifecycle event parity (Spec 078 AC-2)
11
+ import { emitLifecycleEvent, createMCPLifecycleEnvelope } from "../../scripts/lifecycle-mcp-adapter.mjs";
12
+ import { appendFileSync, readFileSync } from "fs";
13
+ import { execFile } from "child_process";
14
+ import { promisify } from "util";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ const execFileAsync = promisify(execFile);
18
+ // JSONL outbox for lifecycle events → orchestration layer
19
+ const OUTBOX_PATH = join(homedir(), "equabot", "agent-chat", "outbox-comet.jsonl");
20
+ const INBOX_PATH = join(homedir(), "equabot", "agent-chat", "inbox-comet.jsonl");
21
+ const MANIFEST_PATH = join(homedir(), ".claude", "comet-browser", "session-manifest.json");
22
+ function appendJsonl(path, obj) {
23
+ try {
24
+ appendFileSync(path, JSON.stringify(obj) + "\n");
25
+ }
26
+ catch { /* graceful degradation — file may not exist */ }
27
+ }
28
+ function readJsonSafe(path) {
29
+ try {
30
+ return JSON.parse(readFileSync(path, "utf-8"));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ const TOOLS = [
37
+ {
38
+ name: "comet_connect",
39
+ description: "Connect to Comet browser (auto-starts if needed)",
40
+ inputSchema: { type: "object", properties: {} },
41
+ },
42
+ {
43
+ 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.",
45
+ inputSchema: {
46
+ type: "object",
47
+ properties: {
48
+ prompt: { type: "string", description: "Question or task for Comet - focus on goals and context" },
49
+ newChat: { type: "boolean", description: "Start a fresh conversation (default: false)" },
50
+ timeout: { type: "number", description: "Max wait time in ms (default: 15000 = 15s)" },
51
+ },
52
+ required: ["prompt"],
53
+ },
54
+ },
55
+ {
56
+ name: "comet_poll",
57
+ description: "Check agent status and progress. Call repeatedly to monitor agentic tasks.",
58
+ inputSchema: { type: "object", properties: {} },
59
+ },
60
+ {
61
+ name: "comet_stop",
62
+ description: "Stop the current agent task if it's going off track",
63
+ inputSchema: { type: "object", properties: {} },
64
+ },
65
+ {
66
+ name: "comet_screenshot",
67
+ description: "Capture a screenshot of current page",
68
+ inputSchema: { type: "object", properties: {} },
69
+ },
70
+ {
71
+ name: "comet_mode",
72
+ description: "Switch Perplexity search mode. Modes: 'search' (basic), 'research' (deep research), 'labs' (analytics/visualization), 'learn' (educational). Call without mode to see current mode.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ mode: {
77
+ type: "string",
78
+ enum: ["search", "research", "labs", "learn"],
79
+ description: "Mode to switch to (optional - omit to see current mode)",
80
+ },
81
+ },
82
+ },
83
+ },
84
+ {
85
+ name: "comet_tab_groups",
86
+ description: "Manage Chrome tab groups in Comet browser. Requires the Comet Tab Groups Bridge extension (load unpacked from extension/ dir). " +
87
+ "Actions: list (all groups), list_tabs (all tabs with group info), create (new group from tab IDs), " +
88
+ "update (rename/recolor/collapse), move (reorder), ungroup (remove tabs from group), delete (ungroup all tabs in a group), " +
89
+ "save_group (persist tab URLs to archive), restore_group (reopen tabs from archive), archive_group (save + close tabs), " +
90
+ "list_archived (show all archived tab groups).",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ action: {
95
+ type: "string",
96
+ enum: ["list", "list_tabs", "create", "update", "move", "ungroup", "delete", "save_group", "restore_group", "archive_group", "list_archived"],
97
+ description: "The tab group operation to perform",
98
+ },
99
+ tabIds: {
100
+ type: "array",
101
+ items: { type: "number" },
102
+ description: "Tab IDs (for create, ungroup)",
103
+ },
104
+ groupId: {
105
+ type: "number",
106
+ description: "Group ID (for update, move, delete)",
107
+ },
108
+ title: {
109
+ type: "string",
110
+ description: "Group title (for create, update)",
111
+ },
112
+ color: {
113
+ type: "string",
114
+ enum: ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"],
115
+ description: "Group color (for create, update)",
116
+ },
117
+ collapsed: {
118
+ type: "boolean",
119
+ description: "Collapse/expand group (for update)",
120
+ },
121
+ index: {
122
+ type: "number",
123
+ description: "Position index (for move)",
124
+ },
125
+ taskThreadId: {
126
+ type: "string",
127
+ description: "Task thread ID (for save_group, restore_group, archive_group)",
128
+ },
129
+ closeTabs: {
130
+ type: "boolean",
131
+ description: "Close tabs after saving (for save_group; archive_group always closes)",
132
+ },
133
+ },
134
+ required: ["action"],
135
+ },
136
+ },
137
+ {
138
+ name: "comet_shortcut",
139
+ description: "Trigger a Comet Query Shortcut (e.g. /fact-check, /mailtodo). "
140
+ + "These are reusable AI prompts with pre-configured modes and sources. "
141
+ + "Type '/' to discover available shortcuts.",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ shortcut: {
146
+ type: "string",
147
+ description: "Shortcut name (e.g. 'fact-check', 'mailtodo', 'prep-next-meeting'). "
148
+ + "Omit the leading slash — it will be added automatically.",
149
+ },
150
+ context: {
151
+ type: "string",
152
+ description: "Optional additional context appended after the shortcut command.",
153
+ },
154
+ timeout: {
155
+ type: "number",
156
+ description: "Max wait time in ms (default 30000). Shortcuts often take longer than regular queries.",
157
+ },
158
+ },
159
+ required: ["shortcut"],
160
+ },
161
+ },
162
+ {
163
+ name: "comet_read_page",
164
+ description: "Extract content from the current page. Returns structured accessibility tree "
165
+ + "and/or clean text. Use 'tree' mode for interactive elements (buttons, links, forms), "
166
+ + "'text' mode for readable content, or 'both' for full extraction.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ mode: {
171
+ type: "string",
172
+ enum: ["tree", "text", "both"],
173
+ description: "Extraction mode: 'tree' = accessibility tree (roles, names, values), "
174
+ + "'text' = clean markdown-like text, 'both' = both formats. Default: 'text'.",
175
+ },
176
+ maxDepth: {
177
+ type: "number",
178
+ description: "Max tree depth for 'tree' mode (default: 5). Lower = less tokens.",
179
+ },
180
+ maxLength: {
181
+ type: "number",
182
+ description: "Max character length for output (default: 12000). Truncates if exceeded.",
183
+ },
184
+ },
185
+ },
186
+ },
187
+ {
188
+ name: "comet_wait_for_idle",
189
+ description: "Wait for the current page's network activity to settle (no pending requests "
190
+ + "for a specified duration). Use after navigation or triggering dynamic content loads. "
191
+ + "Returns a summary of network activity observed.",
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ idleTime: {
196
+ type: "number",
197
+ description: "Milliseconds of network silence required to consider 'idle' (default: 1500).",
198
+ },
199
+ timeout: {
200
+ type: "number",
201
+ description: "Max wait time in ms before giving up (default: 15000).",
202
+ },
203
+ },
204
+ },
205
+ },
206
+ {
207
+ name: "comet_lifecycle_start",
208
+ description: "Start (register) a new Comet browser run in the command-center-so lifecycle store. " +
209
+ "Returns the created CometRun record. Set deferred=true to start in pending state.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ runId: { type: "string", description: "Unique run identifier" },
214
+ taskThreadId: { type: "string", description: "Task-thread identifier" },
215
+ agentId: { type: "string", description: "Agent identity (optional for single-agent)" },
216
+ route: { type: "string", enum: ["mcp", "cli", "http"], description: "Execution channel" },
217
+ deferred: { type: "boolean", description: "Start in pending state (default: false)" },
218
+ },
219
+ required: ["runId", "taskThreadId"],
220
+ },
221
+ },
222
+ {
223
+ name: "comet_lifecycle_complete",
224
+ description: "Mark a Comet browser run as completed in the lifecycle store.",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ runId: { type: "string", description: "Run identifier to complete" },
229
+ },
230
+ required: ["runId"],
231
+ },
232
+ },
233
+ {
234
+ name: "comet_lifecycle_abort",
235
+ description: "Abort a Comet browser run in the lifecycle store.",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ runId: { type: "string", description: "Run identifier to abort" },
240
+ reason: { type: "string", description: "Reason for aborting (optional)" },
241
+ },
242
+ required: ["runId"],
243
+ },
244
+ },
245
+ {
246
+ name: "comet_lifecycle_update",
247
+ description: "Update metadata on a running Comet browser run (auditSessionId, tabGroupId, workflowId, or arbitrary metadata).",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ runId: { type: "string", description: "Run identifier to update" },
252
+ auditSessionId: { type: "string", description: "Audit session ID to attach" },
253
+ tabGroupId: { type: "string", description: "Tab group ID to associate" },
254
+ workflowId: { type: "string", description: "Workflow ID to associate" },
255
+ metadata: { type: "object", description: "Arbitrary metadata key-value pairs" },
256
+ },
257
+ required: ["runId"],
258
+ },
259
+ },
260
+ {
261
+ name: "comet_task_status",
262
+ description: "Get unified status for a Comet browser task. Combines session-manifest.json state, " +
263
+ "extension ring buffer events, and lifecycle metadata into one response. " +
264
+ "Query by groupId or threadId.",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ groupId: { type: "number", description: "Tab group ID to check" },
269
+ threadId: { type: "string", description: "Thread ID to check (returns all sessions for thread)" },
270
+ },
271
+ },
272
+ },
273
+ {
274
+ name: "comet_delegate",
275
+ description: "High-level delegation tool: dispatches a task to the Comet browser in one call. " +
276
+ "Creates a tab group (color-coded by priority), opens task URLs, registers in session manifest, " +
277
+ "starts lifecycle tracking, and writes to the Comet inbox. Returns groupId and status.",
278
+ inputSchema: {
279
+ type: "object",
280
+ properties: {
281
+ threadId: { type: "string", description: "Task thread identifier" },
282
+ instruction: { type: "string", description: "What the browser agent should do" },
283
+ priority: {
284
+ type: "string",
285
+ enum: ["P1", "P2", "P3", "P4"],
286
+ description: "Task priority (determines tab group color). Default: P3",
287
+ },
288
+ urls: {
289
+ type: "array",
290
+ items: { type: "string" },
291
+ description: "URLs to open in the tab group",
292
+ },
293
+ dependsOn: {
294
+ type: "array",
295
+ items: { type: "string" },
296
+ description: "Task IDs this task depends on",
297
+ },
298
+ agentId: { type: "string", description: "Agent identity for tracking" },
299
+ },
300
+ required: ["threadId", "instruction"],
301
+ },
302
+ },
303
+ ];
304
+ const CC_LIFECYCLE_URL = process.env.COMET_CC_LIFECYCLE_URL
305
+ || "http://localhost:3001/command-center/api/comet/lifecycle";
306
+ async function callLifecycleEndpoint(payload) {
307
+ const resp = await fetch(CC_LIFECYCLE_URL, {
308
+ method: "POST",
309
+ headers: { "Content-Type": "application/json" },
310
+ body: JSON.stringify(payload),
311
+ signal: AbortSignal.timeout(5000),
312
+ });
313
+ const data = await resp.text();
314
+ if (!resp.ok) {
315
+ throw new Error(`CC-SO lifecycle ${resp.status}: ${data.substring(0, 200)}`);
316
+ }
317
+ return data;
318
+ }
319
+ const server = new Server({ name: "comet-bridge", version: "2.4.0" }, { capabilities: { tools: {} } });
320
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
321
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
322
+ const { name, arguments: args } = request.params;
323
+ try {
324
+ switch (name) {
325
+ case "comet_connect": {
326
+ // Auto-start Comet with debug port (will restart if running without it)
327
+ const startResult = await cometClient.startComet(9222);
328
+ // Get all tabs and clean up - close all except one
329
+ const targets = await cometClient.listTargets();
330
+ const pageTabs = targets.filter(t => t.type === 'page');
331
+ // Close extra tabs, keep only one
332
+ if (pageTabs.length > 1) {
333
+ for (let i = 1; i < pageTabs.length; i++) {
334
+ try {
335
+ await cometClient.closeTab(pageTabs[i].id);
336
+ }
337
+ catch { /* ignore */ }
338
+ }
339
+ }
340
+ // Get fresh tab list
341
+ const freshTargets = await cometClient.listTargets();
342
+ const anyPage = freshTargets.find(t => t.type === 'page');
343
+ if (anyPage) {
344
+ await cometClient.connect(anyPage.id);
345
+ // Always navigate to Perplexity home for clean state
346
+ await cometClient.navigate("https://www.perplexity.ai/", true);
347
+ await new Promise(resolve => setTimeout(resolve, 1500));
348
+ return { content: [{ type: "text", text: `${startResult}\nConnected to Perplexity (cleaned ${pageTabs.length - 1} old tabs)` }] };
349
+ }
350
+ // No tabs at all - create a new one
351
+ const newTab = await cometClient.newTab("https://www.perplexity.ai/");
352
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for page load
353
+ await cometClient.connect(newTab.id);
354
+ return { content: [{ type: "text", text: `${startResult}\nCreated new tab and navigated to Perplexity` }] };
355
+ }
356
+ case "comet_ask": {
357
+ let prompt = args?.prompt;
358
+ const timeout = args?.timeout || 15000; // Default 15s, use poll for longer tasks
359
+ const newChat = args?.newChat || false;
360
+ // Validate prompt
361
+ if (!prompt || prompt.trim().length === 0) {
362
+ return { content: [{ type: "text", text: "Error: prompt cannot be empty" }] };
363
+ }
364
+ // Normalize prompt - convert markdown/bullets to natural text
365
+ prompt = prompt
366
+ .replace(/^[-*•]\s*/gm, '') // Remove bullet points
367
+ .replace(/\n+/g, ' ') // Collapse newlines to spaces
368
+ .replace(/\s+/g, ' ') // Collapse multiple spaces
369
+ .trim();
370
+ // For newChat: full reset (same as comet_connect) to handle post-agentic state
371
+ if (newChat) {
372
+ // Clean up extra tabs (fixes CDP state after agentic browsing)
373
+ const targets = await cometClient.listTargets();
374
+ const pageTabs = targets.filter(t => t.type === 'page');
375
+ if (pageTabs.length > 1) {
376
+ for (let i = 1; i < pageTabs.length; i++) {
377
+ try {
378
+ await cometClient.closeTab(pageTabs[i].id);
379
+ }
380
+ catch { /* ignore */ }
381
+ }
382
+ }
383
+ // Fresh connect to remaining tab
384
+ const freshTargets = await cometClient.listTargets();
385
+ const mainTab = freshTargets.find(t => t.type === 'page');
386
+ if (mainTab) {
387
+ await cometClient.connect(mainTab.id);
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) {
403
+ await cometClient.navigate("https://www.perplexity.ai/", true);
404
+ await new Promise(resolve => setTimeout(resolve, 2000));
405
+ }
406
+ }
407
+ // Capture old response state BEFORE sending prompt (for follow-up detection)
408
+ const oldStateResult = await cometClient.evaluate(`
409
+ (() => {
410
+ const proseEls = document.querySelectorAll('[class*="prose"]');
411
+ const lastProse = proseEls[proseEls.length - 1];
412
+ return {
413
+ count: proseEls.length,
414
+ lastText: lastProse ? lastProse.innerText.substring(0, 100) : ''
415
+ };
416
+ })()
417
+ `);
418
+ const oldState = oldStateResult.result.value;
419
+ // Send the prompt
420
+ await cometAI.sendPrompt(prompt);
421
+ // Wait for completion
422
+ const startTime = Date.now();
423
+ const stepsCollected = [];
424
+ let sawNewResponse = false;
425
+ while (Date.now() - startTime < timeout) {
426
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Poll every 2s
427
+ // Check if we have a NEW response (more prose elements or different text)
428
+ const currentStateResult = await cometClient.evaluate(`
429
+ (() => {
430
+ const proseEls = document.querySelectorAll('[class*="prose"]');
431
+ const lastProse = proseEls[proseEls.length - 1];
432
+ return {
433
+ count: proseEls.length,
434
+ lastText: lastProse ? lastProse.innerText.substring(0, 100) : ''
435
+ };
436
+ })()
437
+ `);
438
+ const currentState = currentStateResult.result.value;
439
+ // Detect new response
440
+ if (!sawNewResponse) {
441
+ if (currentState.count > oldState.count ||
442
+ (currentState.lastText && currentState.lastText !== oldState.lastText)) {
443
+ sawNewResponse = true;
444
+ }
445
+ }
446
+ const status = await cometAI.getAgentStatus();
447
+ // Collect steps
448
+ for (const step of status.steps) {
449
+ if (!stepsCollected.includes(step)) {
450
+ stepsCollected.push(step);
451
+ }
452
+ }
453
+ // Task completed - return result directly (but only if we saw a NEW response)
454
+ if (status.status === 'completed' && sawNewResponse) {
455
+ return { content: [{ type: "text", text: status.response || 'Task completed (no response text extracted)' }] };
456
+ }
457
+ }
458
+ // Still working after initial wait - return "in progress" (non-blocking)
459
+ const finalStatus = await cometAI.getAgentStatus();
460
+ let inProgressMsg = `Task in progress (${stepsCollected.length} steps so far).\n`;
461
+ inProgressMsg += `Status: ${finalStatus.status.toUpperCase()}\n`;
462
+ if (finalStatus.currentStep) {
463
+ inProgressMsg += `Current: ${finalStatus.currentStep}\n`;
464
+ }
465
+ if (finalStatus.agentBrowsingUrl) {
466
+ inProgressMsg += `Browsing: ${finalStatus.agentBrowsingUrl}\n`;
467
+ }
468
+ if (stepsCollected.length > 0) {
469
+ inProgressMsg += `\nSteps:\n${stepsCollected.map(s => ` • ${s}`).join('\n')}\n`;
470
+ }
471
+ inProgressMsg += `\nUse comet_poll to check progress or comet_stop to cancel.`;
472
+ return { content: [{ type: "text", text: inProgressMsg }] };
473
+ }
474
+ case "comet_poll": {
475
+ const status = await cometAI.getAgentStatus();
476
+ // If completed, return the response directly (most useful case)
477
+ if (status.status === 'completed' && status.response) {
478
+ return { content: [{ type: "text", text: status.response }] };
479
+ }
480
+ // Still working - return progress info
481
+ let output = `Status: ${status.status.toUpperCase()}\n`;
482
+ if (status.agentBrowsingUrl) {
483
+ output += `Browsing: ${status.agentBrowsingUrl}\n`;
484
+ }
485
+ if (status.currentStep) {
486
+ output += `Current: ${status.currentStep}\n`;
487
+ }
488
+ if (status.steps.length > 0) {
489
+ output += `\nSteps:\n${status.steps.map(s => ` • ${s}`).join('\n')}\n`;
490
+ }
491
+ if (status.status === 'working') {
492
+ output += `\n[Use comet_stop to interrupt, or comet_screenshot to see current page]`;
493
+ }
494
+ return { content: [{ type: "text", text: output }] };
495
+ }
496
+ case "comet_stop": {
497
+ const stopped = await cometAI.stopAgent();
498
+ return {
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(`
516
+ (() => {
517
+ // Try button group first (wide screen)
518
+ const modes = ['Search', 'Research', 'Labs', 'Learn'];
519
+ for (const mode of modes) {
520
+ const btn = document.querySelector('button[aria-label="' + mode + '"]');
521
+ if (btn && btn.getAttribute('data-state') === 'checked') {
522
+ return mode.toLowerCase();
523
+ }
524
+ }
525
+ // Try dropdown (narrow screen) - look for the mode selector button
526
+ const dropdownBtn = document.querySelector('button[class*="gap"]');
527
+ if (dropdownBtn) {
528
+ const text = dropdownBtn.innerText.toLowerCase();
529
+ if (text.includes('search')) return 'search';
530
+ if (text.includes('research')) return 'research';
531
+ if (text.includes('labs')) return 'labs';
532
+ if (text.includes('learn')) return 'learn';
533
+ }
534
+ return 'search';
535
+ })()
536
+ `);
537
+ const currentMode = result.result.value;
538
+ const descriptions = {
539
+ search: 'Basic web search',
540
+ research: 'Deep research with comprehensive analysis',
541
+ labs: 'Analytics, visualizations, and coding',
542
+ learn: 'Educational content and explanations'
543
+ };
544
+ let output = `Current mode: ${currentMode}\n\nAvailable modes:\n`;
545
+ for (const [m, desc] of Object.entries(descriptions)) {
546
+ const marker = m === currentMode ? "→" : " ";
547
+ output += `${marker} ${m}: ${desc}\n`;
548
+ }
549
+ return { content: [{ type: "text", text: output }] };
550
+ }
551
+ // Switch mode
552
+ const modeMap = {
553
+ search: "Search",
554
+ research: "Research",
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,
563
+ };
564
+ }
565
+ // Navigate to Perplexity first if not there
566
+ const state = cometClient.currentState;
567
+ if (!state.currentUrl?.includes("perplexity.ai")) {
568
+ await cometClient.navigate("https://www.perplexity.ai/", true);
569
+ }
570
+ // Try both UI patterns: button group (wide) and dropdown (narrow)
571
+ const result = await cometClient.evaluate(`
572
+ (() => {
573
+ // Strategy 1: Direct button (wide screen)
574
+ const btn = document.querySelector('button[aria-label="${ariaLabel}"]');
575
+ if (btn) {
576
+ btn.click();
577
+ return { success: true, method: 'button' };
578
+ }
579
+
580
+ // Strategy 2: Dropdown menu (narrow screen)
581
+ // Find and click the dropdown trigger (button with current mode text)
582
+ const allButtons = document.querySelectorAll('button');
583
+ for (const b of allButtons) {
584
+ const text = b.innerText.toLowerCase();
585
+ if ((text.includes('search') || text.includes('research') ||
586
+ text.includes('labs') || text.includes('learn')) &&
587
+ b.querySelector('svg')) {
588
+ b.click();
589
+ return { success: true, method: 'dropdown-open', needsSelect: true };
590
+ }
591
+ }
592
+
593
+ return { success: false, error: "Mode selector not found" };
594
+ })()
595
+ `);
596
+ const clickResult = result.result.value;
597
+ if (clickResult.success && clickResult.needsSelect) {
598
+ // Wait for dropdown to open, then select the mode
599
+ await new Promise(resolve => setTimeout(resolve, 300));
600
+ const selectResult = await cometClient.evaluate(`
601
+ (() => {
602
+ // Look for dropdown menu items
603
+ const items = document.querySelectorAll('[role="menuitem"], [role="option"], button');
604
+ for (const item of items) {
605
+ if (item.innerText.toLowerCase().includes('${mode}')) {
606
+ item.click();
607
+ return { success: true };
608
+ }
609
+ }
610
+ return { success: false, error: "Mode option not found in dropdown" };
611
+ })()
612
+ `);
613
+ const selectRes = selectResult.result.value;
614
+ if (selectRes.success) {
615
+ return { content: [{ type: "text", text: `Switched to ${mode} mode` }] };
616
+ }
617
+ else {
618
+ return { content: [{ type: "text", text: `Failed: ${selectRes.error}` }], isError: true };
619
+ }
620
+ }
621
+ if (clickResult.success) {
622
+ return { content: [{ type: "text", text: `Switched to ${mode} mode` }] };
623
+ }
624
+ else {
625
+ return {
626
+ content: [{ type: "text", text: `Failed to switch mode: ${clickResult.error}` }],
627
+ isError: true,
628
+ };
629
+ }
630
+ }
631
+ case "comet_shortcut": {
632
+ const shortcut = (args?.shortcut).replace(/^\//, "");
633
+ const context = args?.context;
634
+ const timeout = args?.timeout || 30000;
635
+ // Ensure we're on Perplexity
636
+ const tabs = await cometClient.listTabsCategorized();
637
+ if (tabs.main) {
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(`
648
+ (() => {
649
+ const proseEls = document.querySelectorAll('[class*="prose"]');
650
+ const lastProse = proseEls[proseEls.length - 1];
651
+ return {
652
+ count: proseEls.length,
653
+ lastText: lastProse ? lastProse.innerText.substring(0, 100) : ''
654
+ };
655
+ })()
656
+ `);
657
+ const oldState = oldStateResult.result.value;
658
+ // Send the shortcut
659
+ await cometAI.sendShortcut(shortcut, context);
660
+ // Poll for response with timeout
661
+ const startTime = Date.now();
662
+ let sawNewResponse = false;
663
+ while (Date.now() - startTime < timeout) {
664
+ await new Promise(resolve => setTimeout(resolve, 2000));
665
+ const currentStateResult = await cometClient.evaluate(`
666
+ (() => {
667
+ const proseEls = document.querySelectorAll('[class*="prose"]');
668
+ const lastProse = proseEls[proseEls.length - 1];
669
+ return {
670
+ count: proseEls.length,
671
+ lastText: lastProse ? lastProse.innerText.substring(0, 100) : ''
672
+ };
673
+ })()
674
+ `);
675
+ const currentState = currentStateResult.result.value;
676
+ if (!sawNewResponse) {
677
+ if (currentState.count > oldState.count ||
678
+ (currentState.lastText && currentState.lastText !== oldState.lastText)) {
679
+ sawNewResponse = true;
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." }] };
733
+ }
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
+ }
737
+ case "list_tabs": {
738
+ const tabs = await tabGroupsClient.listTabs();
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
+ });
752
+ return {
753
+ content: [{
754
+ type: "text",
755
+ text: `Created group ${result.groupId}: "${result.group.title || "(untitled)"}" (${result.group.color})`,
756
+ }],
757
+ };
758
+ }
759
+ case "update": {
760
+ const groupId = args?.groupId;
761
+ if (groupId === undefined) {
762
+ return { content: [{ type: "text", text: "Error: groupId required for update" }], isError: true };
763
+ }
764
+ const group = await tabGroupsClient.updateGroup({
765
+ groupId,
766
+ title: args?.title,
767
+ color: args?.color,
768
+ collapsed: args?.collapsed,
769
+ });
770
+ return {
771
+ content: [{
772
+ type: "text",
773
+ text: `Updated group ${group.id}: "${group.title || "(untitled)"}" (${group.color}${group.collapsed ? ", collapsed" : ""})`,
774
+ }],
775
+ };
776
+ }
777
+ case "move": {
778
+ const groupId = args?.groupId;
779
+ const index = args?.index;
780
+ if (groupId === undefined || index === undefined) {
781
+ return { content: [{ type: "text", text: "Error: groupId and index required for move" }], isError: true };
782
+ }
783
+ const group = await tabGroupsClient.moveGroup(groupId, index);
784
+ return { content: [{ type: "text", text: `Moved group ${group.id} to index ${index}` }] };
785
+ }
786
+ case "ungroup": {
787
+ const tabIds = args?.tabIds;
788
+ if (!tabIds || tabIds.length === 0) {
789
+ return { content: [{ type: "text", text: "Error: tabIds required for ungroup" }], isError: true };
790
+ }
791
+ await tabGroupsClient.ungroupTabs(tabIds);
792
+ return { content: [{ type: "text", text: `Ungrouped ${tabIds.length} tab(s)` }] };
793
+ }
794
+ case "delete": {
795
+ const groupId = args?.groupId;
796
+ if (groupId === undefined) {
797
+ return { content: [{ type: "text", text: "Error: groupId required for delete" }], isError: true };
798
+ }
799
+ const tabs = await tabGroupsClient.listTabs();
800
+ const groupTabs = tabs.filter((t) => t.groupId === groupId);
801
+ if (groupTabs.length === 0) {
802
+ return { content: [{ type: "text", text: `No tabs found in group ${groupId}` }] };
803
+ }
804
+ await tabGroupsClient.ungroupTabs(groupTabs.map((t) => t.id));
805
+ return {
806
+ content: [{
807
+ type: "text",
808
+ text: `Deleted group ${groupId} (ungrouped ${groupTabs.length} tab(s))`,
809
+ }],
810
+ };
811
+ }
812
+ case "save_group": {
813
+ const { archiveStore } = await import("./tab-group-archive.js");
814
+ const groupId = args?.groupId;
815
+ const taskThreadId = args?.taskThreadId;
816
+ const closeTabs = args?.closeTabs;
817
+ if (groupId === undefined || !taskThreadId) {
818
+ return { content: [{ type: "text", text: "Error: groupId and taskThreadId required for save_group" }], isError: true };
819
+ }
820
+ const group = await tabGroupsClient.getGroup(groupId);
821
+ const allTabs = await tabGroupsClient.listTabs();
822
+ const groupTabs = allTabs.filter((t) => t.groupId === groupId);
823
+ const entry = {
824
+ taskThreadId,
825
+ title: group.title,
826
+ color: group.color,
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 */ }
839
+ }
840
+ }
841
+ return { content: [{ type: "text", text: `Saved ${groupTabs.length} tab(s) for thread ${taskThreadId} (status: ${entry.status})` }] };
842
+ }
843
+ case "restore_group": {
844
+ const { archiveStore } = await import("./tab-group-archive.js");
845
+ const taskThreadId = args?.taskThreadId;
846
+ if (!taskThreadId) {
847
+ return { content: [{ type: "text", text: "Error: taskThreadId required for restore_group" }], isError: true };
848
+ }
849
+ const entry = await archiveStore.restore(taskThreadId);
850
+ if (!entry) {
851
+ return { content: [{ type: "text", text: `No archive found for thread ${taskThreadId}` }], isError: true };
852
+ }
853
+ const tabIds = [];
854
+ for (const u of entry.urls) {
855
+ const tabId = await tabGroupsClient.createTab(u.url, false);
856
+ if (typeof tabId === "number")
857
+ tabIds.push(tabId);
858
+ }
859
+ if (tabIds.length > 0) {
860
+ await tabGroupsClient.createGroup({
861
+ tabIds,
862
+ title: entry.title,
863
+ color: entry.color,
864
+ });
865
+ }
866
+ return { content: [{ type: "text", text: `Restored ${tabIds.length} tab(s) for thread ${taskThreadId}` }] };
867
+ }
868
+ case "archive_group": {
869
+ const { archiveStore } = await import("./tab-group-archive.js");
870
+ const groupId = args?.groupId;
871
+ const taskThreadId = args?.taskThreadId;
872
+ if (groupId === undefined || !taskThreadId) {
873
+ return { content: [{ type: "text", text: "Error: groupId and taskThreadId required for archive_group" }], isError: true };
874
+ }
875
+ const group = await tabGroupsClient.getGroup(groupId);
876
+ const allTabs = await tabGroupsClient.listTabs();
877
+ const groupTabs = allTabs.filter((t) => t.groupId === groupId);
878
+ const entry = {
879
+ taskThreadId,
880
+ title: group.title,
881
+ color: group.color,
882
+ collapsed: group.collapsed,
883
+ urls: groupTabs.map((t) => ({ url: t.url, title: t.title })),
884
+ archivedAt: new Date().toISOString(),
885
+ status: "archived",
886
+ };
887
+ await archiveStore.save(entry);
888
+ const tabIdsToClose = groupTabs.map((t) => t.id);
889
+ if (tabIdsToClose.length > 0) {
890
+ await tabGroupsClient.closeTabs(tabIdsToClose);
891
+ }
892
+ return { content: [{ type: "text", text: `Archived ${groupTabs.length} tab(s) for thread ${taskThreadId} (tabs closed)` }] };
893
+ }
894
+ case "list_archived": {
895
+ const { archiveStore } = await import("./tab-group-archive.js");
896
+ const entries = await archiveStore.loadAll();
897
+ if (entries.length === 0) {
898
+ return { content: [{ type: "text", text: "No archived tab groups." }] };
899
+ }
900
+ const lines = entries.map((e) => {
901
+ const date = e.archivedAt ? new Date(e.archivedAt).toLocaleDateString() : "unknown";
902
+ return `[${e.taskThreadId}] "${e.title || "(untitled)"}" (${e.urls.length} tabs, ${e.status}, archived ${date})`;
903
+ });
904
+ return { content: [{ type: "text", text: `Archived tab groups (${entries.length}):\n${lines.join("\n")}` }] };
905
+ }
906
+ default:
907
+ return {
908
+ content: [{ type: "text", text: `Unknown action: ${action}. Use: list, list_tabs, create, update, move, ungroup, delete, save_group, restore_group, archive_group, list_archived` }],
909
+ isError: true,
910
+ };
911
+ }
912
+ }
913
+ catch (tgError) {
914
+ const msg = tgError instanceof Error ? tgError.message : String(tgError);
915
+ if (msg.includes("extension") || msg.includes("service worker") || msg.includes("Bridge")) {
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({
944
+ runId: args?.runId,
945
+ taskThreadId: args?.taskThreadId,
946
+ agentId: args?.agentId,
947
+ toolName: "comet_lifecycle_start",
948
+ });
949
+ emitLifecycleEvent("start", env, { persist: true });
950
+ }
951
+ catch { /* graceful degradation — HTTP result already persisted */ }
952
+ // Bridge to JSONL outbox for orchestration
953
+ appendJsonl(OUTBOX_PATH, {
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({
972
+ runId: args?.runId,
973
+ toolName: "comet_lifecycle_complete",
974
+ });
975
+ emitLifecycleEvent("complete", env, { persist: true });
976
+ }
977
+ catch { /* graceful degradation */ }
978
+ // Bridge to JSONL outbox
979
+ appendJsonl(OUTBOX_PATH, {
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({
998
+ runId: args?.runId,
999
+ toolName: "comet_lifecycle_abort",
1000
+ });
1001
+ emitLifecycleEvent("abort", env, { persist: true });
1002
+ }
1003
+ catch { /* graceful degradation */ }
1004
+ // Bridge to JSONL outbox
1005
+ appendJsonl(OUTBOX_PATH, {
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({
1027
+ runId: args?.runId,
1028
+ toolName: "comet_lifecycle_update",
1029
+ });
1030
+ emitLifecycleEvent("update", env, { persist: true });
1031
+ }
1032
+ catch { /* graceful degradation */ }
1033
+ return { content: [{ type: "text", text: result }] };
1034
+ }
1035
+ case "comet_task_status": {
1036
+ const groupId = args?.groupId;
1037
+ const threadId = args?.threadId;
1038
+ if (!groupId && !threadId) {
1039
+ return { content: [{ type: "text", text: "Error: provide groupId or threadId" }], isError: true };
1040
+ }
1041
+ const manifest = readJsonSafe(MANIFEST_PATH);
1042
+ const sessions = manifest?.sessions || [];
1043
+ // Filter sessions
1044
+ const matched = sessions.filter((s) => {
1045
+ if (groupId !== undefined && s.groupId === groupId)
1046
+ return true;
1047
+ if (threadId && s.threadId === threadId)
1048
+ return true;
1049
+ return false;
1050
+ });
1051
+ if (matched.length === 0) {
1052
+ return { content: [{ type: "text", text: `No sessions found for ${groupId ? `groupId=${groupId}` : `threadId=${threadId}`}` }] };
1053
+ }
1054
+ // Try to get recent ring buffer events via CDP
1055
+ let recentEvents = [];
1056
+ try {
1057
+ const { tabGroupsClient } = await import("./tab-groups.js");
1058
+ // Reuse the tab groups client's CDP path to find service worker
1059
+ const response = await fetch(`http://127.0.0.1:9222/json/list`);
1060
+ const targets = (await response.json());
1061
+ const sw = targets.find((t) => t.type === "service_worker" && t.webSocketDebuggerUrl);
1062
+ if (sw) {
1063
+ // We can't easily eval here without ws, so skip ring buffer in MCP context
1064
+ // Ring buffer events are aggregated by comet-event-aggregator.py instead
1065
+ }
1066
+ }
1067
+ catch { /* CDP not available */ }
1068
+ const result = matched.map((s) => ({
1069
+ groupId: s.groupId,
1070
+ groupName: s.groupName,
1071
+ groupColor: s.groupColor,
1072
+ threadId: s.threadId,
1073
+ status: s.agent?.status || "unknown",
1074
+ agentId: s.agent?.id || null,
1075
+ tabs: s.tabs?.length || 0,
1076
+ metrics: s.metrics || {},
1077
+ links: s.links || {},
1078
+ lastActivity: s.metrics?.lastActivityAt || null,
1079
+ }));
1080
+ return { content: [{ type: "text", text: JSON.stringify(result.length === 1 ? result[0] : result, null, 2) }] };
1081
+ }
1082
+ case "comet_delegate": {
1083
+ const threadId = args?.threadId;
1084
+ const instruction = args?.instruction;
1085
+ const priority = args?.priority || "P3";
1086
+ const urls = args?.urls || [];
1087
+ const dependsOn = args?.dependsOn || [];
1088
+ const agentId = args?.agentId || "comet-mcp";
1089
+ if (!threadId || !instruction) {
1090
+ return { content: [{ type: "text", text: "Error: threadId and instruction are required" }], isError: true };
1091
+ }
1092
+ const taskId = `comet-${Date.now()}`;
1093
+ const priorityColors = { P1: "red", P2: "yellow", P3: "blue", P4: "grey" };
1094
+ const color = priorityColors[priority] || "blue";
1095
+ // 1. Write to inbox-comet.jsonl
1096
+ appendJsonl(INBOX_PATH, {
1097
+ ts: Math.floor(Date.now() / 1000),
1098
+ from: "comet-delegate",
1099
+ to: "comet-browser",
1100
+ task: taskId,
1101
+ type: "instruction",
1102
+ thread: threadId,
1103
+ priority,
1104
+ msg: instruction,
1105
+ browser_task: {
1106
+ thread_id: threadId,
1107
+ group_name: threadId.slice(0, 50),
1108
+ color,
1109
+ priority,
1110
+ urls,
1111
+ depends_on: dependsOn,
1112
+ },
1113
+ });
1114
+ // 2. Start lifecycle tracking
1115
+ let lifecycleResult = "skipped";
1116
+ try {
1117
+ lifecycleResult = await callLifecycleEndpoint({
1118
+ action: "start",
1119
+ runId: taskId,
1120
+ taskThreadId: threadId,
1121
+ agentId,
1122
+ route: "mcp",
1123
+ });
1124
+ }
1125
+ catch { /* CC-SO may not be running */ }
1126
+ // 3. Dispatch via session-controller.mjs
1127
+ let dispatchResult = "skipped";
1128
+ const controllerPath = join(homedir(), ".claude", "comet-browser", "session-controller.mjs");
1129
+ try {
1130
+ const dispatchArgs = ["dispatch", "--thread", threadId];
1131
+ if (agentId)
1132
+ dispatchArgs.push("--agent", agentId);
1133
+ const { stdout } = await execFileAsync("node", [controllerPath, ...dispatchArgs], { timeout: 15000 });
1134
+ dispatchResult = stdout.trim();
1135
+ }
1136
+ catch (e) {
1137
+ dispatchResult = `failed: ${e instanceof Error ? e.message : e}`;
1138
+ }
1139
+ // 4. Bridge to outbox
1140
+ appendJsonl(OUTBOX_PATH, {
1141
+ ts: Math.floor(Date.now() / 1000),
1142
+ from: "comet-browser",
1143
+ to: "orchestration",
1144
+ type: "update",
1145
+ task: taskId,
1146
+ thread: threadId,
1147
+ msg: `Delegated: ${instruction.slice(0, 100)}`,
1148
+ lifecycle: { action: "start", runId: taskId, status: "dispatched" },
1149
+ });
1150
+ const summary = [
1151
+ `Task delegated successfully.`,
1152
+ ` Task ID: ${taskId}`,
1153
+ ` Thread: ${threadId}`,
1154
+ ` Priority: ${priority} (color: ${color})`,
1155
+ ` URLs: ${urls.length > 0 ? urls.join(", ") : "(from thread metadata)"}`,
1156
+ ` Lifecycle: ${lifecycleResult === "skipped" ? "skipped (CC-SO unavailable)" : "started"}`,
1157
+ ` Dispatch: ${dispatchResult}`,
1158
+ ].join("\n");
1159
+ return { content: [{ type: "text", text: summary }] };
1160
+ }
1161
+ default:
1162
+ throw new Error(`Unknown tool: ${name}`);
1163
+ }
1164
+ }
1165
+ catch (error) {
1166
+ return {
1167
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : error}` }],
1168
+ isError: true,
1169
+ };
1170
+ }
1171
+ });
1172
+ const transport = new StdioServerTransport();
1173
+ server.connect(transport);
1174
+ //# sourceMappingURL=index.js.map