surf-cli 2.0.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.
@@ -0,0 +1,989 @@
1
+ const fs = require("fs");
2
+ const networkFormatters = require("./formatters/network.cjs");
3
+ const networkStore = require("./network-store.cjs");
4
+
5
+ /**
6
+ * Format tool result content for MCP response
7
+ * @param {*} result - The result object from the extension
8
+ * @param {Function} log - Logging function (defaults to no-op for testing)
9
+ * @returns {Array} Array of content objects with type and text/data
10
+ */
11
+ function formatToolContent(result, log = () => {}) {
12
+ const text = (s) => [{ type: "text", text: s }];
13
+
14
+ if (!result) return text("OK");
15
+
16
+ if (result.aiResult) {
17
+ if (result.mode === "find") {
18
+ return text(result.ref || "NOT_FOUND");
19
+ }
20
+ return text(result.content);
21
+ }
22
+
23
+ // Handle ChatGPT/Gemini responses
24
+ if (result.response !== undefined && result.model !== undefined && result.tookMs !== undefined) {
25
+ let output = result.response;
26
+ if (result.imagePath) {
27
+ output += `\n\n*Image saved to: ${result.imagePath}*`;
28
+ }
29
+ return text(output);
30
+ }
31
+
32
+ if (result.messages && Array.isArray(result.messages)) {
33
+ const formatted = result.messages.map(m => {
34
+ let loc = "";
35
+ if (m.url) loc = m.line !== undefined ? ` (${m.url}:${m.line})` : ` (${m.url})`;
36
+ return `[${m.type}] ${m.text}${loc}`;
37
+ }).join("\n");
38
+ return text(formatted || "No console messages");
39
+ }
40
+
41
+ // Handle both requests (basic) and entries (full) formats
42
+ const items = result.requests || result.entries;
43
+ if (items && Array.isArray(items)) {
44
+ // Persist entries with full data to disk
45
+ if (result.entries && items.length > 0) {
46
+ (async () => {
47
+ for (const entry of items) {
48
+ try {
49
+ await networkStore.appendEntry(entry);
50
+ } catch (err) {
51
+ log(`Failed to persist network entry: ${err.message}`);
52
+ }
53
+ }
54
+ })();
55
+ }
56
+
57
+ if (items.length === 0) {
58
+ return text("No network requests captured");
59
+ }
60
+ let formatted;
61
+ if (result.format === 'curl') {
62
+ formatted = networkFormatters.formatCurlBatch(items);
63
+ } else if (result.format === 'urls') {
64
+ formatted = networkFormatters.formatUrls(items);
65
+ } else if (result.format === 'raw') {
66
+ formatted = networkFormatters.formatRaw(items);
67
+ } else if (result.verbose > 0) {
68
+ formatted = networkFormatters.formatVerbose(items, result.verbose);
69
+ } else if (result.entries) {
70
+ // entries format means full data was requested - use verbose level 1
71
+ formatted = networkFormatters.formatVerbose(items, 1);
72
+ } else {
73
+ formatted = items.map(r => {
74
+ const status = String(r.status || '-').padStart(3);
75
+ const method = (r.method || 'GET').padEnd(7);
76
+ const type = (r.type || '').padEnd(10);
77
+ return `${status} ${method} ${type} ${r.url}`;
78
+ }).join("\n");
79
+ }
80
+ return text(formatted);
81
+ }
82
+
83
+ if (result.output !== undefined) {
84
+ return text(result.output);
85
+ }
86
+
87
+ if (result.screenshotId) {
88
+ const dims = result.width && result.height
89
+ ? `${result.width}x${result.height}`
90
+ : "unknown dimensions";
91
+ return [
92
+ { type: "text", text: `Screenshot captured (${dims}) - ID: ${result.screenshotId}` },
93
+ { type: "image", data: result.base64, mimeType: "image/png" }
94
+ ];
95
+ }
96
+
97
+ if (result.base64) {
98
+ const dims = result.width && result.height
99
+ ? `${result.width}x${result.height}`
100
+ : "unknown dimensions";
101
+ return [
102
+ { type: "text", text: `Screenshot (${dims})` },
103
+ { type: "image", data: result.base64, mimeType: "image/png" }
104
+ ];
105
+ }
106
+
107
+ if (result.pageContent !== undefined) {
108
+ const content = result.pageContent || "No content";
109
+ let output = '';
110
+
111
+ if (result.waited !== undefined) {
112
+ output += `[Waited ${result.waited}ms]\n\n`;
113
+ }
114
+
115
+ output += content;
116
+
117
+ if (result.isIncremental && result.diff) {
118
+ output += `\n--- Diff from previous snapshot ---\n${result.diff}`;
119
+ }
120
+
121
+ if (result.modalStates && result.modalStates.length > 0) {
122
+ output += `\n\n[ACTION REQUIRED] Modal blocking page - dismiss before proceeding:`;
123
+ output += `\n -> Press Escape key: computer(action="key", text="Escape")`;
124
+ for (const modal of result.modalStates) {
125
+ output += `\n - ${modal.description}`;
126
+ }
127
+ }
128
+
129
+ if (result.error) {
130
+ return text(`Error: ${result.error}\n\n${output}`);
131
+ }
132
+
133
+ // Include page text content if requested via --text flag
134
+ if (result.text) {
135
+ output += `\n\n--- Page Text ---\n${result.text}`;
136
+ }
137
+
138
+ if (result.screenshot && result.screenshot.base64) {
139
+ const dims = result.screenshot.width && result.screenshot.height
140
+ ? `${result.screenshot.width}x${result.screenshot.height}`
141
+ : "unknown";
142
+ return [
143
+ { type: "text", text: output },
144
+ { type: "text", text: `\n[Screenshot included (${dims})]` },
145
+ { type: "image", data: result.screenshot.base64, mimeType: "image/png" }
146
+ ];
147
+ }
148
+
149
+ return text(output);
150
+ }
151
+
152
+ // Window commands - check before generic tabs check
153
+ if (result.windowId !== undefined && result.success) {
154
+ // window.new, window.focus, window.close, window.resize
155
+ let msg = `Window ${result.windowId}`;
156
+ if (result.tabId) msg += ` (tab ${result.tabId})`;
157
+ if (result.width && result.height) msg += ` ${result.width}x${result.height}`;
158
+ if (result.hint) msg += `\n${result.hint}`;
159
+ return text(msg);
160
+ }
161
+
162
+ if (result.windows) {
163
+ // window.list - preserve structure for CLI formatting (exclude internal id)
164
+ return text(JSON.stringify({ windows: result.windows }, null, 2));
165
+ }
166
+
167
+ if (result.tabs) {
168
+ return text(JSON.stringify(result.tabs, null, 2));
169
+ }
170
+
171
+ if (result.cookies && Array.isArray(result.cookies)) {
172
+ return text(JSON.stringify(result.cookies, null, 2));
173
+ }
174
+
175
+ if (result.cookie) {
176
+ return text(JSON.stringify(result.cookie, null, 2));
177
+ }
178
+
179
+ if (result.cleared !== undefined) {
180
+ if (typeof result.cleared === "number") {
181
+ return text(`Cleared ${result.cleared} cookies`);
182
+ }
183
+ return text(`Cleared cookie: ${result.cleared}`);
184
+ }
185
+
186
+ if (result.query !== undefined && result.matches) {
187
+ const header = `Found ${result.count} matches for "${result.query}":`;
188
+ if (result.matches.length === 0) return text(header);
189
+ const matchList = result.matches.map(m =>
190
+ ` ${m.ref}: "${m.text}" in "...${m.context}..."${m.elementRef ? ` [${m.elementRef}]` : ""}`
191
+ ).join("\n");
192
+ return text(`${header}\n${matchList}`);
193
+ }
194
+
195
+ if (result.groupId !== undefined && result.name !== undefined) {
196
+ return text(`Tab group "${result.name}" (id: ${result.groupId}) with tabs: ${(result.tabIds || []).join(", ")}`);
197
+ }
198
+
199
+ if (result.ungrouped) {
200
+ return text(`Ungrouped tabs: ${result.ungrouped.join(", ")}`);
201
+ }
202
+
203
+ if (result.groups && Array.isArray(result.groups)) {
204
+ if (result.groups.length === 0) return text("No tab groups");
205
+ const formatted = result.groups.map(g => {
206
+ const tabList = g.tabs.map(t => ` ${t.id}: ${t.title}`).join("\n");
207
+ return `${g.name} (${g.color}, ${g.tabs.length} tabs):\n${tabList}`;
208
+ }).join("\n\n");
209
+ return text(formatted);
210
+ }
211
+
212
+ if (result.completedActions !== undefined && result.totalActions !== undefined) {
213
+ const status = result.success ? "SUCCESS" : "FAILED";
214
+ const header = `Batch ${status}: ${result.completedActions}/${result.totalActions} actions completed`;
215
+ if (result.results && result.results.length > 0) {
216
+ const details = result.results.map(r =>
217
+ ` [${r.index}] ${r.type}: ${r.success ? "OK" : "FAILED"}${r.error ? ` - ${r.error}` : ""}`
218
+ ).join("\n");
219
+ return text(`${header}\n${details}${result.error ? `\n\nError: ${result.error}` : ""}`);
220
+ }
221
+ return text(header);
222
+ }
223
+
224
+ if (result.zoom !== undefined) {
225
+ return text(`Zoom: ${Math.round(result.zoom * 100)}%`);
226
+ }
227
+
228
+ if (result.bookmarks && Array.isArray(result.bookmarks)) {
229
+ if (result.bookmarks.length === 0) return text("No bookmarks");
230
+ const formatted = result.bookmarks.map(b =>
231
+ `${b.title}\n ${b.url}`
232
+ ).join("\n\n");
233
+ return text(formatted);
234
+ }
235
+
236
+ if (result.bookmark && result.bookmark.id) {
237
+ return text(`Bookmarked: ${result.bookmark.title}\n ${result.bookmark.url}`);
238
+ }
239
+
240
+ if (result.history && Array.isArray(result.history)) {
241
+ if (result.history.length === 0) return text("No history");
242
+ const formatted = result.history.map(h => {
243
+ const date = h.lastVisitTime ? new Date(h.lastVisitTime).toLocaleString() : "unknown";
244
+ return `${h.title || "(no title)"}\n ${h.url}\n Last visited: ${date}`;
245
+ }).join("\n\n");
246
+ return text(formatted);
247
+ }
248
+
249
+ if (result.text !== undefined) {
250
+ const textContent = result.text || "No text content";
251
+ let output = "";
252
+ if (result.title) output += `Title: ${result.title}\n`;
253
+ if (result.url) output += `URL: ${result.url}\n`;
254
+ if (output) output += "\n";
255
+ output += textContent;
256
+ if (result.error) {
257
+ return text(`Error: ${result.error}\n\n${output}`);
258
+ }
259
+ return text(output);
260
+ }
261
+
262
+ if (result.success && result.name && result.tabId !== undefined) {
263
+ return text(`Registered tab ${result.tabId} as "${result.name}"`);
264
+ }
265
+
266
+ if (result.success && result.tabId && result.title !== undefined) {
267
+ return text(`Switched to tab ${result.tabId}: ${result.title}`);
268
+ }
269
+
270
+ if (result.success && result.tabId && result.url) {
271
+ return text(`Created tab ${result.tabId}: ${result.url}`);
272
+ }
273
+
274
+ if (result.success && result.closed) {
275
+ return text(`Closed ${result.closed.length} tabs: ${result.closed.join(", ")}`);
276
+ }
277
+
278
+ if (result.success && result.tabId && !result.url) {
279
+ return text(`Closed tab ${result.tabId}`);
280
+ }
281
+
282
+ if (result.success && result.width && result.height) {
283
+ return text(`Resized window to ${result.width}x${result.height}`);
284
+ }
285
+
286
+ if (result.autoScreenshot) {
287
+ const { path: ssPath, width, height } = result.autoScreenshot;
288
+ try {
289
+ const imgData = fs.readFileSync(ssPath);
290
+ const base64 = imgData.toString("base64");
291
+ const dims = width && height ? `${width}x${height}` : "unknown";
292
+ return [
293
+ { type: "text", text: `OK\nScreenshot (${dims}): ${ssPath}` },
294
+ { type: "image", data: base64, mimeType: "image/png" }
295
+ ];
296
+ } catch {
297
+ return text(`OK\nScreenshot saved: ${ssPath}`);
298
+ }
299
+ }
300
+
301
+ if (result.autoScreenshotError) {
302
+ return text(`OK\n[Screenshot failed: ${result.autoScreenshotError}]`);
303
+ }
304
+
305
+ // Bug fix: Handle success with metrics/frames/readyState/hint in one block
306
+ if (result.success) {
307
+ if (result.metrics) {
308
+ return text(JSON.stringify(result.metrics, null, 2));
309
+ }
310
+ if (result.frames) {
311
+ return text(JSON.stringify(result.frames, null, 2));
312
+ }
313
+ if (result.readyState) {
314
+ return text(`Page loaded (readyState: ${result.readyState})`);
315
+ }
316
+ // Include _hint handling here instead of unreachable code below
317
+ let msg = "OK";
318
+ if (result._hint) msg += `\n[hint] ${result._hint}`;
319
+ return text(msg);
320
+ }
321
+
322
+ if (result.value !== undefined) {
323
+ return text(typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2));
324
+ }
325
+
326
+ // Strip internal fields before JSON output
327
+ const { _resolvedTabId, _hint, ...cleanResult } = result;
328
+ if (_hint) {
329
+ return text(JSON.stringify(cleanResult) + `\n[hint] ${_hint}`);
330
+ }
331
+ return text(JSON.stringify(cleanResult));
332
+ }
333
+
334
+ /**
335
+ * Map computer action to extension message
336
+ */
337
+ function mapComputerAction(args, tabId) {
338
+ const a = args || {};
339
+ const { action, text, scroll_direction, scroll_amount,
340
+ start_coordinate, ref, duration, modifiers } = a;
341
+ const coordinate = a.coordinate || (a.x !== undefined && a.y !== undefined ? [a.x, a.y] : undefined);
342
+ const baseMsg = { tabId };
343
+
344
+ if (!action) {
345
+ return { type: "UNSUPPORTED_ACTION", action: null, message: "No action specified for computer tool" };
346
+ }
347
+
348
+ switch (action) {
349
+ case "screenshot":
350
+ return { type: "EXECUTE_SCREENSHOT", ...baseMsg };
351
+
352
+ case "left_click":
353
+ if (ref) return { type: "CLICK_REF", ref, button: "left", ...baseMsg };
354
+ if (a.selector) return { type: "CLICK_SELECTOR", selector: a.selector, index: a.index || 0, button: "left", ...baseMsg };
355
+ return { type: "EXECUTE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
356
+
357
+ case "right_click":
358
+ if (ref) return { type: "CLICK_REF", ref, button: "right", ...baseMsg };
359
+ return { type: "EXECUTE_RIGHT_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
360
+
361
+ case "double_click":
362
+ if (ref) return { type: "CLICK_REF", ref, button: "double", ...baseMsg };
363
+ return { type: "EXECUTE_DOUBLE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
364
+
365
+ case "triple_click":
366
+ if (ref) return { type: "CLICK_REF", ref, button: "triple", ...baseMsg };
367
+ return { type: "EXECUTE_TRIPLE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
368
+
369
+ case "type":
370
+ if (ref) {
371
+ return { type: "FORM_FILL", data: [{ ref, value: text }], ...baseMsg };
372
+ }
373
+ return { type: "EXECUTE_TYPE", text, ...baseMsg };
374
+
375
+ case "key": {
376
+ const keyValue = a.key || text;
377
+ const repeatCount = Math.min(100, Math.max(1, a.repeat || 1));
378
+ if (repeatCount > 1) {
379
+ return { type: "EXECUTE_KEY_REPEAT", key: keyValue, repeat: repeatCount, tabId };
380
+ }
381
+ return { type: "EXECUTE_KEY", key: keyValue, ...baseMsg };
382
+ }
383
+
384
+ case "type_submit":
385
+ return { type: "TYPE_SUBMIT", text, submitKey: a.submitKey || "Enter", ...baseMsg };
386
+
387
+ case "click_type":
388
+ return { type: "CLICK_TYPE", text, ref, coordinate, ...baseMsg };
389
+
390
+ case "click_type_submit":
391
+ return { type: "CLICK_TYPE_SUBMIT", text, ref, coordinate, submitKey: a.submitKey || "Enter", ...baseMsg };
392
+
393
+ case "find_and_type":
394
+ return { type: "FIND_AND_TYPE", text, submit: a.submit ?? false, submitKey: a.submitKey || "Enter", ...baseMsg };
395
+
396
+ case "scroll": {
397
+ const amount = (scroll_amount || 3) * 100;
398
+ const deltas = {
399
+ up: { deltaX: 0, deltaY: -amount },
400
+ down: { deltaX: 0, deltaY: amount },
401
+ left: { deltaX: -amount, deltaY: 0 },
402
+ right: { deltaX: amount, deltaY: 0 },
403
+ };
404
+ const { deltaX, deltaY } = deltas[scroll_direction] || { deltaX: 0, deltaY: 0 };
405
+ return { type: "EXECUTE_SCROLL", deltaX, deltaY, x: coordinate?.[0], y: coordinate?.[1], ...baseMsg };
406
+ }
407
+
408
+ case "scroll_to":
409
+ return { type: "SCROLL_TO_ELEMENT", ref, ...baseMsg };
410
+
411
+ case "hover":
412
+ if (ref) return { type: "HOVER_REF", ref, ...baseMsg };
413
+ return { type: "EXECUTE_HOVER", x: coordinate?.[0], y: coordinate?.[1], ...baseMsg };
414
+
415
+ case "left_click_drag":
416
+ case "drag":
417
+ return {
418
+ type: "EXECUTE_DRAG",
419
+ startX: start_coordinate?.[0],
420
+ startY: start_coordinate?.[1],
421
+ endX: coordinate?.[0],
422
+ endY: coordinate?.[1],
423
+ modifiers,
424
+ ...baseMsg
425
+ };
426
+
427
+ case "wait":
428
+ return { type: "LOCAL_WAIT", seconds: Math.min(30, duration || 1) };
429
+
430
+ case "zoom":
431
+ if (a.reset) return { type: "ZOOM_RESET", tabId };
432
+ if (a.level !== undefined) return { type: "ZOOM_SET", level: parseFloat(a.level), tabId };
433
+ return { type: "ZOOM_GET", tabId };
434
+
435
+ default:
436
+ return { type: "UNSUPPORTED_ACTION", action, message: `Unknown computer action: ${action}` };
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Map tool name and args to extension message
442
+ */
443
+ function mapToolToMessage(tool, args, tabId) {
444
+ const baseMsg = { tabId };
445
+ const a = args || {};
446
+
447
+ switch (tool) {
448
+ case "computer":
449
+ return mapComputerAction(args, tabId);
450
+ case "navigate":
451
+ return { type: "EXECUTE_NAVIGATE", url: a.url, ...baseMsg };
452
+ case "read_page":
453
+ return {
454
+ type: "READ_PAGE",
455
+ options: {
456
+ filter: a.filter || "interactive",
457
+ depth: a.depth,
458
+ refId: a.ref_id,
459
+ format: a.format,
460
+ forceFullSnapshot: a.forceFullSnapshot ?? false,
461
+ includeScreenshot: a.includeScreenshot ?? false
462
+ },
463
+ ...baseMsg
464
+ };
465
+ case "get_page_text":
466
+ return { type: "GET_PAGE_TEXT", ...baseMsg };
467
+ case "form_input":
468
+ return { type: "FORM_INPUT", ref: a.ref, value: a.value, ...baseMsg };
469
+ case "eval":
470
+ return { type: "EVAL_IN_PAGE", code: a.code, ...baseMsg };
471
+ case "find_and_type":
472
+ return { type: "FIND_AND_TYPE", text: a.text, submit: a.submit ?? false, submitKey: a.submitKey || "Enter", ...baseMsg };
473
+ case "autocomplete":
474
+ return { type: "AUTOCOMPLETE_SELECT", text: a.text, ref: a.ref, coordinate: a.coordinate, index: a.index ?? 0, waitMs: a.waitMs ?? 500, ...baseMsg };
475
+ case "set_value":
476
+ return { type: "SET_INPUT_VALUE", selector: a.selector, ref: a.ref, value: a.value, ...baseMsg };
477
+ case "smart_type":
478
+ return { type: "SMART_TYPE", selector: a.selector, text: a.text, clear: a.clear ?? true, submit: a.submit ?? false, ...baseMsg };
479
+ case "scroll_to_position":
480
+ return { type: "SCROLL_TO_POSITION", position: a.position, selector: a.selector, ...baseMsg };
481
+ case "get_scroll_info":
482
+ return { type: "GET_SCROLL_INFO", selector: a.selector, ...baseMsg };
483
+ case "close_dialogs":
484
+ return { type: "CLOSE_DIALOGS", maxAttempts: a.maxAttempts ?? 3, ...baseMsg };
485
+ case "page_state":
486
+ return { type: "PAGE_STATE", ...baseMsg };
487
+ case "tabs_context":
488
+ return { type: "GET_TABS" };
489
+ case "screenshot":
490
+ return {
491
+ type: "EXECUTE_SCREENSHOT",
492
+ savePath: a.savePath,
493
+ annotate: a.annotate || false,
494
+ fullpage: a.fullpage || false,
495
+ maxHeight: a["max-height"] || 4000,
496
+ fullRes: a.full || false,
497
+ maxSize: a["max-size"] || 1200,
498
+ ...baseMsg
499
+ };
500
+ case "javascript_tool":
501
+ return { type: "EXECUTE_JAVASCRIPT", code: a.code, ...baseMsg };
502
+ case "wait_for_element":
503
+ return {
504
+ type: "WAIT_FOR_ELEMENT",
505
+ selector: a.selector,
506
+ state: a.state || "visible",
507
+ timeout: a.timeout || 20000,
508
+ ...baseMsg
509
+ };
510
+ case "wait_for_url":
511
+ return {
512
+ type: "WAIT_FOR_URL",
513
+ pattern: a.pattern || a.url || a.urlContains,
514
+ timeout: a.timeout || 20000,
515
+ ...baseMsg
516
+ };
517
+ case "wait_for_network_idle":
518
+ return {
519
+ type: "WAIT_FOR_NETWORK_IDLE",
520
+ timeout: a.timeout || 10000,
521
+ ...baseMsg
522
+ };
523
+ case "console":
524
+ case "read_console_messages":
525
+ return {
526
+ type: "READ_CONSOLE_MESSAGES",
527
+ onlyErrors: a.only_errors,
528
+ pattern: a.pattern,
529
+ limit: a.limit,
530
+ clear: a.clear,
531
+ ...baseMsg
532
+ };
533
+ case "network":
534
+ case "get_network_entries":
535
+ return {
536
+ type: "READ_NETWORK_REQUESTS",
537
+ full: a.v || a.vv || a.format === 'curl' || a.format === 'verbose' || a.format === 'raw',
538
+ urlPattern: a.filter || a.url_pattern || a.origin,
539
+ method: a.method,
540
+ status: a.status,
541
+ contentType: a.type,
542
+ limit: a.limit || a.last,
543
+ format: a.format,
544
+ verbose: a.v ? 1 : (a.vv ? 2 : 0),
545
+ ...baseMsg
546
+ };
547
+
548
+ case "network.get":
549
+ case "get_network_entry":
550
+ return {
551
+ type: "GET_NETWORK_ENTRY",
552
+ requestId: a.id || args[0],
553
+ ...baseMsg
554
+ };
555
+
556
+ case "network.body":
557
+ return {
558
+ type: "GET_RESPONSE_BODY",
559
+ requestId: a.id || args[0],
560
+ isRequest: a.request,
561
+ ...baseMsg
562
+ };
563
+
564
+ case "network.curl":
565
+ return {
566
+ type: "GET_NETWORK_ENTRY",
567
+ requestId: a.id || args[0],
568
+ formatAsCurl: true,
569
+ ...baseMsg
570
+ };
571
+
572
+ case "network.origins":
573
+ return {
574
+ type: "GET_NETWORK_ORIGINS",
575
+ byTab: a["by-tab"] || a.byTab,
576
+ ...baseMsg
577
+ };
578
+
579
+ case "network.clear":
580
+ return {
581
+ type: "CLEAR_NETWORK_REQUESTS",
582
+ before: a.before,
583
+ origin: a.origin,
584
+ ...baseMsg
585
+ };
586
+
587
+ case "network.stats":
588
+ return {
589
+ type: "GET_NETWORK_STATS",
590
+ ...baseMsg
591
+ };
592
+
593
+ case "network.export":
594
+ return {
595
+ type: "EXPORT_NETWORK_REQUESTS",
596
+ har: a.har,
597
+ jsonl: a.jsonl,
598
+ output: a.output,
599
+ ...baseMsg
600
+ };
601
+
602
+ case "network.path":
603
+ return {
604
+ type: "GET_NETWORK_PATHS",
605
+ requestId: a.id || args[0],
606
+ ...baseMsg
607
+ };
608
+
609
+ case "read_network_requests":
610
+ return {
611
+ type: "READ_NETWORK_REQUESTS",
612
+ urlPattern: a.url_pattern,
613
+ limit: a.limit,
614
+ clear: a.clear,
615
+ ...baseMsg
616
+ };
617
+ case "upload_image":
618
+ return {
619
+ type: "UPLOAD_IMAGE",
620
+ screenshotId: a.screenshot_id,
621
+ ref: a.ref,
622
+ coordinate: a.coordinate,
623
+ filename: a.filename,
624
+ ...baseMsg
625
+ };
626
+ case "resize_window":
627
+ return {
628
+ type: "RESIZE_WINDOW",
629
+ width: a.width,
630
+ height: a.height,
631
+ ...baseMsg
632
+ };
633
+ case "tabs_create":
634
+ return { type: "TABS_CREATE", url: a.url, ...baseMsg };
635
+ case "tabs_register":
636
+ return { type: "TABS_REGISTER", name: a.name, ...baseMsg };
637
+ case "tabs_get_by_name":
638
+ return { type: "TABS_GET_BY_NAME", name: a.name };
639
+ case "tabs_list_named":
640
+ return { type: "TABS_LIST_NAMED" };
641
+ case "tabs_unregister":
642
+ return { type: "TABS_UNREGISTER", name: a.name };
643
+ case "list_tabs":
644
+ return { type: "LIST_TABS" };
645
+ case "new_tab":
646
+ return { type: "NEW_TAB", url: a.url, urls: a.urls };
647
+ case "switch_tab":
648
+ return { type: "SWITCH_TAB", tabId: a.tab_id || a.tabId };
649
+ case "close_tab":
650
+ return { type: "CLOSE_TAB", tabId: a.tab_id || a.tabId, tabIds: a.tab_ids || a.tabIds };
651
+ case "tab.list":
652
+ return { type: "LIST_TABS" };
653
+ case "tab.new":
654
+ return { type: "NEW_TAB", url: a.url, urls: a.urls };
655
+ case "tab.switch": {
656
+ const id = a.id || a.tab_id || a.tabId;
657
+ if (typeof id === "string" && !/^\d+$/.test(id)) {
658
+ return { type: "NAMED_TAB_SWITCH", name: id };
659
+ }
660
+ return { type: "SWITCH_TAB", tabId: id };
661
+ }
662
+ case "tab.close": {
663
+ const id = a.id || a.tab_id || a.tabId;
664
+ const ids = a.ids || a.tab_ids || a.tabIds;
665
+ if (typeof id === "string" && !/^\d+$/.test(id)) {
666
+ return { type: "NAMED_TAB_CLOSE", name: id };
667
+ }
668
+ return { type: "CLOSE_TAB", tabId: id, tabIds: ids };
669
+ }
670
+ case "tab.name":
671
+ return { type: "TABS_REGISTER", name: a.name, ...baseMsg };
672
+ case "tab.unname":
673
+ return { type: "TABS_UNREGISTER", name: a.name };
674
+ case "tab.named":
675
+ return { type: "TABS_LIST_NAMED" };
676
+ case "js":
677
+ return { type: "EXECUTE_JAVASCRIPT", code: a.code, ...baseMsg };
678
+ case "scroll.top":
679
+ return { type: "SCROLL_TO_POSITION", position: "top", selector: a.selector, ...baseMsg };
680
+ case "scroll.bottom":
681
+ return { type: "SCROLL_TO_POSITION", position: "bottom", selector: a.selector, ...baseMsg };
682
+ case "scroll.info":
683
+ return { type: "GET_SCROLL_INFO", selector: a.selector, ...baseMsg };
684
+ case "scroll.to":
685
+ return { type: "SCROLL_TO_ELEMENT", ref: a.ref, ...baseMsg };
686
+ case "wait.element":
687
+ return { type: "WAIT_FOR_ELEMENT", selector: a.selector, timeout: a.timeout, ...baseMsg };
688
+ case "wait.network":
689
+ return { type: "WAIT_FOR_NETWORK_IDLE", timeout: a.timeout, ...baseMsg };
690
+ case "wait.url":
691
+ return { type: "WAIT_FOR_URL", pattern: a.pattern || a.url, timeout: a.timeout, ...baseMsg };
692
+ case "wait.dom":
693
+ return { type: "WAIT_FOR_DOM_STABLE", stable: a.stable || 100, timeout: a.timeout || 5000, ...baseMsg };
694
+ case "wait.load":
695
+ return { type: "WAIT_FOR_LOAD", timeout: a.timeout || 30000, ...baseMsg };
696
+ case "frame.list":
697
+ return { type: "GET_FRAMES", ...baseMsg };
698
+ case "frame.switch":
699
+ return {
700
+ type: "FRAME_SWITCH",
701
+ selector: a.selector,
702
+ name: a.name,
703
+ index: a.index !== undefined ? parseInt(a.index, 10) : undefined,
704
+ ...baseMsg
705
+ };
706
+ case "frame.main":
707
+ return { type: "FRAME_MAIN", ...baseMsg };
708
+ case "frame.js":
709
+ return { type: "EVALUATE_IN_FRAME", frameId: a.id, code: a.code, ...baseMsg };
710
+ case "dialog.accept":
711
+ return { type: "DIALOG_ACCEPT", text: a.text, ...baseMsg };
712
+ case "dialog.dismiss":
713
+ if (a.all) return { type: "CLOSE_DIALOGS", maxAttempts: a.maxAttempts || 3, ...baseMsg };
714
+ return { type: "DIALOG_DISMISS", ...baseMsg };
715
+ case "dialog.info":
716
+ return { type: "DIALOG_INFO", ...baseMsg };
717
+ case "emulate.network":
718
+ return { type: "EMULATE_NETWORK", preset: a.preset, ...baseMsg };
719
+ case "emulate.cpu":
720
+ const cpuRate = parseFloat(a.rate);
721
+ return { type: "EMULATE_CPU", rate: isNaN(cpuRate) ? 1 : cpuRate, ...baseMsg };
722
+ case "emulate.geo":
723
+ if (a.clear) {
724
+ return { type: "EMULATE_GEO", clear: true, ...baseMsg };
725
+ }
726
+ if (a.lat === undefined || a.lon === undefined) {
727
+ throw new Error("--lat and --lon required");
728
+ }
729
+ return { type: "EMULATE_GEO", latitude: parseFloat(a.lat), longitude: parseFloat(a.lon), accuracy: parseFloat(a.accuracy) || 100, ...baseMsg };
730
+ case "emulate.device":
731
+ if (a.list) {
732
+ return { type: "EMULATE_DEVICE_LIST" };
733
+ }
734
+ if (!a.device) throw new Error("device name required");
735
+ return { type: "EMULATE_DEVICE", device: a.device, ...baseMsg };
736
+ case "emulate.viewport":
737
+ return {
738
+ type: "EMULATE_VIEWPORT",
739
+ width: a.width ? parseInt(a.width, 10) : undefined,
740
+ height: a.height ? parseInt(a.height, 10) : undefined,
741
+ deviceScaleFactor: a.scale ? parseFloat(a.scale) : undefined,
742
+ mobile: a.mobile,
743
+ ...baseMsg
744
+ };
745
+ case "emulate.touch":
746
+ return { type: "EMULATE_TOUCH", enabled: a.enabled !== false, ...baseMsg };
747
+ case "form.fill":
748
+ let fillData = a.data;
749
+ if (typeof fillData === "string") {
750
+ try { fillData = JSON.parse(fillData); } catch (e) { throw new Error("invalid --data JSON"); }
751
+ }
752
+ return { type: "FORM_FILL", data: fillData, ...baseMsg };
753
+ case "perf.start":
754
+ return { type: "PERF_START", categories: a.categories ? a.categories.split(",") : undefined, ...baseMsg };
755
+ case "perf.stop":
756
+ return { type: "PERF_STOP", ...baseMsg };
757
+ case "perf.metrics":
758
+ return { type: "PERF_METRICS", ...baseMsg };
759
+ case "upload":
760
+ const files = a.files ? (typeof a.files === "string" ? a.files.split(",").map(f => f.trim()) : a.files) : [];
761
+ return { type: "UPLOAD_FILE", ref: a.ref, files, ...baseMsg };
762
+ case "page.read":
763
+ return {
764
+ type: "READ_PAGE",
765
+ options: {
766
+ filter: a.filter || "interactive",
767
+ refId: a.ref,
768
+ includeText: a["no-text"] !== true,
769
+ depth: a.depth !== undefined ? parseInt(a.depth, 10) : undefined,
770
+ compact: a.compact || false,
771
+ },
772
+ ...baseMsg
773
+ };
774
+ case "page.text":
775
+ return { type: "GET_PAGE_TEXT", ...baseMsg };
776
+ case "page.state":
777
+ return { type: "PAGE_STATE", ...baseMsg };
778
+ case "locate.role":
779
+ if (!a.role) throw new Error("role argument required");
780
+ return {
781
+ type: "LOCATE_ROLE",
782
+ role: a.role,
783
+ name: a.name,
784
+ action: a.action,
785
+ value: a.value,
786
+ all: a.all || false,
787
+ ...baseMsg
788
+ };
789
+ case "locate.text":
790
+ if (!a.text) throw new Error("text argument required");
791
+ return {
792
+ type: "LOCATE_TEXT",
793
+ text: a.text,
794
+ exact: a.exact || false,
795
+ action: a.action,
796
+ value: a.value,
797
+ ...baseMsg
798
+ };
799
+ case "locate.label":
800
+ if (!a.label) throw new Error("label argument required");
801
+ return {
802
+ type: "LOCATE_LABEL",
803
+ label: a.label,
804
+ action: a.action,
805
+ value: a.value,
806
+ ...baseMsg
807
+ };
808
+ case "ai":
809
+ return { type: "AI_ANALYZE", query: a.query, act: a.act, mode: a.mode, ...baseMsg };
810
+ case "wait":
811
+ return { type: "LOCAL_WAIT", seconds: Math.min(30, a.duration || a.seconds || 1) };
812
+ case "health":
813
+ if (a.url) {
814
+ return { type: "HEALTH_CHECK_URL", url: a.url, expect: a.expect || 200, timeout: a.timeout || 30000 };
815
+ } else if (a.selector) {
816
+ return { type: "WAIT_FOR_ELEMENT", selector: a.selector, timeout: a.timeout || 30000, ...baseMsg };
817
+ }
818
+ return { type: "ERROR", error: "--url or --selector required" };
819
+ case "smoke":
820
+ return {
821
+ type: "SMOKE_TEST",
822
+ urls: a.urls || [],
823
+ routes: a.routes,
824
+ savePath: a.screenshot,
825
+ failFast: a["fail-fast"] || false,
826
+ ...baseMsg
827
+ };
828
+ case "type":
829
+ case "left_click":
830
+ case "right_click":
831
+ case "double_click":
832
+ case "triple_click":
833
+ case "key":
834
+ case "hover":
835
+ case "drag":
836
+ case "scroll":
837
+ return mapComputerAction({ ...a, action: tool }, tabId);
838
+ case "click":
839
+ return mapComputerAction({ ...a, action: "left_click" }, tabId);
840
+ case "cookie.list":
841
+ return { type: "COOKIE_LIST", ...baseMsg };
842
+ case "cookie.get":
843
+ if (!a.name) throw new Error("--name required");
844
+ return { type: "COOKIE_GET", name: a.name, ...baseMsg };
845
+ case "cookie.set":
846
+ if (!a.name) throw new Error("--name required");
847
+ if (a.value === undefined) throw new Error("--value required");
848
+ return { type: "COOKIE_SET", name: a.name, value: a.value, expires: a.expires, ...baseMsg };
849
+ case "cookie.clear":
850
+ if (a.all) return { type: "COOKIE_CLEAR_ALL", ...baseMsg };
851
+ if (!a.name) throw new Error("--name or --all required");
852
+ return { type: "COOKIE_CLEAR", name: a.name, ...baseMsg };
853
+ case "search":
854
+ if (!a.term) throw new Error("search term required");
855
+ return { type: "SEARCH_PAGE", term: a.term, caseSensitive: a["case-sensitive"] || false, limit: a.limit || 10, ...baseMsg };
856
+ case "tab.group": {
857
+ const tabIds = a.tabs ? String(a.tabs).split(",").map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)) : [];
858
+ return { type: "TAB_GROUP_CREATE", name: a.name, tabIds, color: a.color || "blue", ...baseMsg };
859
+ }
860
+ case "tab.ungroup": {
861
+ const tabIds = a.tabs ? String(a.tabs).split(",").map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)) : [];
862
+ return { type: "TAB_GROUP_REMOVE", tabIds, ...baseMsg };
863
+ }
864
+ case "tab.groups":
865
+ return { type: "TAB_GROUPS_LIST" };
866
+ case "batch": {
867
+ let actions = a.actions;
868
+
869
+ if (a.file) {
870
+ if (!fs.existsSync(a.file)) {
871
+ throw new Error(`file not found: ${a.file}`);
872
+ }
873
+ const content = fs.readFileSync(a.file, "utf8");
874
+ try {
875
+ actions = JSON.parse(content);
876
+ } catch (e) {
877
+ throw new Error(`invalid JSON in ${a.file}`);
878
+ }
879
+ }
880
+
881
+ if (typeof actions === "string") {
882
+ try {
883
+ actions = JSON.parse(actions);
884
+ } catch (e) {
885
+ throw new Error("invalid --actions JSON");
886
+ }
887
+ }
888
+
889
+ if (!Array.isArray(actions)) {
890
+ throw new Error("actions must be array");
891
+ }
892
+
893
+ return { type: "BATCH_EXECUTE", actions, ...baseMsg };
894
+ }
895
+ case "back":
896
+ return { type: "EXECUTE_JAVASCRIPT", code: "history.back()", ...baseMsg };
897
+ case "forward":
898
+ return { type: "EXECUTE_JAVASCRIPT", code: "history.forward()", ...baseMsg };
899
+ case "tab.reload":
900
+ return { type: "TAB_RELOAD", hard: a.hard || false, ...baseMsg };
901
+ case "zoom":
902
+ if (a.reset) return { type: "ZOOM_RESET", ...baseMsg };
903
+ if (a.level !== undefined) return { type: "ZOOM_SET", level: parseFloat(a.level), ...baseMsg };
904
+ return { type: "ZOOM_GET", ...baseMsg };
905
+ case "resize":
906
+ return { type: "RESIZE_WINDOW", width: a.width, height: a.height, ...baseMsg };
907
+ case "bookmark.add":
908
+ return { type: "BOOKMARK_ADD", folder: a.folder, ...baseMsg };
909
+ case "bookmark.remove":
910
+ return { type: "BOOKMARK_REMOVE", ...baseMsg };
911
+ case "bookmark.list":
912
+ return { type: "BOOKMARK_LIST", folder: a.folder, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 50 };
913
+ case "history.list":
914
+ return { type: "HISTORY_LIST", limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 };
915
+ case "history.search":
916
+ if (!a.query) throw new Error("query required");
917
+ return { type: "HISTORY_SEARCH", query: a.query, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 };
918
+ case "chatgpt":
919
+ if (!a.query) throw new Error("query required");
920
+ return {
921
+ type: "CHATGPT_QUERY",
922
+ query: a.query,
923
+ model: a.model,
924
+ withPage: a["with-page"],
925
+ file: a.file,
926
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 2700000,
927
+ ...baseMsg
928
+ };
929
+ case "gemini":
930
+ if (!a.query && !a["generate-image"]) throw new Error("query required");
931
+ return {
932
+ type: "GEMINI_QUERY",
933
+ query: a.query,
934
+ model: a.model || "gemini-3-pro",
935
+ withPage: a["with-page"],
936
+ file: a.file,
937
+ generateImage: a["generate-image"],
938
+ editImage: a["edit-image"],
939
+ output: a.output,
940
+ youtube: a.youtube,
941
+ aspectRatio: a["aspect-ratio"],
942
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
943
+ ...baseMsg
944
+ };
945
+ case "perplexity":
946
+ if (!a.query) throw new Error("query required");
947
+ return {
948
+ type: "PERPLEXITY_QUERY",
949
+ query: a.query,
950
+ mode: a.mode || "search",
951
+ model: a.model,
952
+ withPage: a["with-page"],
953
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 120000,
954
+ ...baseMsg
955
+ };
956
+ case "window.new":
957
+ return {
958
+ type: "WINDOW_NEW",
959
+ url: a.url,
960
+ width: a.width ? parseInt(a.width, 10) : undefined,
961
+ height: a.height ? parseInt(a.height, 10) : undefined,
962
+ incognito: a.incognito || false,
963
+ focused: a.unfocused ? false : true,
964
+ };
965
+ case "window.list":
966
+ return { type: "WINDOW_LIST", includeTabs: a.tabs || false };
967
+ case "window.focus":
968
+ if (!a.id) throw new Error("window id required");
969
+ return { type: "WINDOW_FOCUS", windowId: parseInt(a.id, 10) };
970
+ case "window.close":
971
+ if (!a.id) throw new Error("window id required");
972
+ return { type: "WINDOW_CLOSE", windowId: parseInt(a.id, 10) };
973
+ case "window.resize":
974
+ if (!a.id) throw new Error("--id required");
975
+ return {
976
+ type: "WINDOW_RESIZE",
977
+ windowId: parseInt(a.id, 10),
978
+ width: a.width ? parseInt(a.width, 10) : undefined,
979
+ height: a.height ? parseInt(a.height, 10) : undefined,
980
+ left: a.left !== undefined ? parseInt(a.left, 10) : undefined,
981
+ top: a.top !== undefined ? parseInt(a.top, 10) : undefined,
982
+ state: a.state,
983
+ };
984
+ default:
985
+ return null;
986
+ }
987
+ }
988
+
989
+ module.exports = { mapToolToMessage, mapComputerAction, formatToolContent };