surf-cli 2.8.0 → 2.9.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.
Files changed (42) hide show
  1. package/README.md +98 -4
  2. package/dist/content/index.js +116 -0
  3. package/dist/content/index.js.map +1 -0
  4. package/dist/manifest.json +2 -11
  5. package/dist/options/options.js +3 -3
  6. package/dist/options/options.js.map +1 -1
  7. package/dist/service-worker/index.js +261 -61
  8. package/dist/service-worker/index.js.map +1 -1
  9. package/native/abort.cjs +65 -0
  10. package/native/ai-queue.cjs +64 -0
  11. package/native/aistudio-build.cjs +21 -13
  12. package/native/aistudio-client.cjs +40 -20
  13. package/native/browser-lock.cjs +2 -2
  14. package/native/chatgpt-client.cjs +47 -31
  15. package/native/cli.cjs +300 -204
  16. package/native/client-transport.cjs +168 -0
  17. package/native/do-executor.cjs +25 -44
  18. package/native/doctor.cjs +55 -5
  19. package/native/endpoint.cjs +174 -0
  20. package/native/file-transfer.cjs +734 -0
  21. package/native/gemini-client.cjs +156 -71
  22. package/native/grok-client.cjs +98 -89
  23. package/native/host-helpers.cjs +37 -12
  24. package/native/host-sessions.cjs +283 -0
  25. package/native/host.cjs +800 -620
  26. package/native/listener.cjs +20 -0
  27. package/native/mcp-server.cjs +60 -65
  28. package/native/network-export.cjs +113 -0
  29. package/native/perplexity-client.cjs +46 -17
  30. package/native/remote-auth.cjs +279 -0
  31. package/native/remote-transport.cjs +337 -0
  32. package/native/request-pending.cjs +148 -0
  33. package/native/socket-path.cjs +1 -1
  34. package/package.json +8 -6
  35. package/scripts/install-native-host.cjs +36 -5
  36. package/skills/README.md +11 -5
  37. package/skills/deep-x-research/SKILL.md +106 -0
  38. package/skills/surf/SKILL.md +31 -4
  39. package/dist/content/accessibility-tree.js +0 -11
  40. package/dist/content/accessibility-tree.js.map +0 -1
  41. package/dist/content/visual-indicator.js +0 -111
  42. package/dist/content/visual-indicator.js.map +0 -1
package/native/cli.cjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- const net = require("net");
3
2
  const fs = require("fs");
4
3
  const path = require("path");
5
4
  const os = require("os");
@@ -9,11 +8,17 @@ const networkFormatters = require("./formatters/network.cjs");
9
8
  const networkStore = require("./network-store.cjs");
10
9
  const { parseDoCommands } = require("./do-parser.cjs");
11
10
  const { executeDoSteps } = require("./do-executor.cjs");
11
+ const { openClientTransport } = require("./client-transport.cjs");
12
12
  const { version: VERSION } = require("../package.json");
13
13
 
14
14
  const IS_WIN = process.platform === "win32";
15
- const { SOCKET_PATH, SURF_TMP, formatSocketError } = require("./socket-path.cjs");
15
+ const { SURF_TMP, formatSocketError } = require("./socket-path.cjs");
16
16
  const { acquireBrowserLock } = require("./browser-lock.cjs");
17
+ const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endpoint.cjs");
18
+ const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
19
+ const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
20
+ const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
21
+ const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
17
22
  if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
18
23
 
19
24
  function parseBrowserLockOptions(noLockFlag) {
@@ -29,11 +34,11 @@ function parseBrowserLockOptions(noLockFlag) {
29
34
  return { noLock, timeoutMs };
30
35
  }
31
36
 
32
- function installBrowserLock({ noLock, timeoutMs }) {
37
+ function installBrowserLock({ noLock, timeoutMs }, endpoint) {
33
38
  let releaseBrowserLock = () => {};
34
39
  if (!noLock) {
35
40
  try {
36
- const lock = acquireBrowserLock(SOCKET_PATH, SURF_TMP, { timeoutMs });
41
+ const lock = acquireBrowserLock(endpoint.key, SURF_TMP, { timeoutMs });
37
42
  releaseBrowserLock = lock.release;
38
43
  } catch (error) {
39
44
  console.error("Error:", error && error.message ? error.message : String(error));
@@ -336,7 +341,50 @@ function resizeImage(filePath, maxSize) {
336
341
  return { success: false, error: e.message };
337
342
  }
338
343
  }
339
- const args = process.argv.slice(2);
344
+ let args = process.argv.slice(2);
345
+ if (args[0] === "remote") {
346
+ const remoteArgs = args.slice(1);
347
+ const subcommand = remoteArgs[0];
348
+ const stateDir = getStateDir();
349
+ try {
350
+ if (subcommand === "authorize") {
351
+ const label = remoteArgs[1];
352
+ const outputIndex = remoteArgs.indexOf("--output");
353
+ const output = outputIndex === -1 ? undefined : remoteArgs[outputIndex + 1];
354
+ if (!label || !output || output.startsWith("--")) throw new Error("Usage: surf remote authorize <label> --output <credential-file>");
355
+ const client = authorizeClient(label, output, stateDir);
356
+ console.log(`Authorized remote client: ${client.label}`);
357
+ console.log(`Credential: ${client.output}`);
358
+ process.exit(0);
359
+ }
360
+ if (subcommand === "list") {
361
+ const clients = listClients(stateDir);
362
+ if (clients.length === 0) console.log("No authorized remote clients.");
363
+ else for (const client of clients) console.log(`${client.label}\t${client.id}\t${client.createdAt}`);
364
+ process.exit(0);
365
+ }
366
+ if (subcommand === "revoke") {
367
+ const label = remoteArgs[1];
368
+ if (!label || label.startsWith("--")) throw new Error("Usage: surf remote revoke <label>");
369
+ revokeClient(label, stateDir);
370
+ console.log(`Revoked remote client: ${label}`);
371
+ process.exit(0);
372
+ }
373
+ console.error("Usage: surf remote authorize <label> --output <credential-file> | list | revoke <label>");
374
+ process.exit(1);
375
+ } catch (error) {
376
+ console.error(`Error: ${error.message}`);
377
+ process.exit(1);
378
+ }
379
+ }
380
+
381
+ let endpoint;
382
+ try {
383
+ ({ args, endpoint } = selectEndpoint(args));
384
+ } catch (error) {
385
+ console.error(`Error: ${error.message}`);
386
+ process.exit(1);
387
+ }
340
388
 
341
389
  const ALIASES = {
342
390
  snap: "screenshot",
@@ -405,7 +453,7 @@ const TOOLS = {
405
453
  args: ["query"],
406
454
  opts: {
407
455
  "with-page": "Include current page context",
408
- model: "Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash",
456
+ model: "Model: gemini-3.1-pro (default), gemini-3.5-flash, gemini-3.1-flash-lite",
409
457
  file: "Attach file to analyze",
410
458
  "generate-image": "Generate image and save to path",
411
459
  "edit-image": "Edit existing image (use with --output)",
@@ -528,6 +576,12 @@ const TOOLS = {
528
576
  opts: { ids: "Close multiple tabs" },
529
577
  examples: [{ cmd: "tab.close 123", desc: "Close tab" }]
530
578
  },
579
+ "tab.move": {
580
+ desc: "Move tab to another window",
581
+ args: ["id"],
582
+ opts: { ids: "Move multiple tabs", "to-window": "Destination window ID", index: "Destination index" },
583
+ examples: [{ cmd: "tab.move 123 --to-window 456", desc: "Move tab to window" }]
584
+ },
531
585
  "tab.name": {
532
586
  desc: "Register current tab with a name",
533
587
  args: ["name"],
@@ -675,6 +729,7 @@ const TOOLS = {
675
729
  "no-text": "Exclude visible text content",
676
730
  depth: "Maximum tree depth (default: unlimited)",
677
731
  compact: "Remove empty structural elements",
732
+ "max-bytes": "Maximum visible text bytes",
678
733
  },
679
734
  examples: [
680
735
  { cmd: "page.read", desc: "Interactive elements + text content" },
@@ -682,7 +737,7 @@ const TOOLS = {
682
737
  { cmd: "page.read --no-text", desc: "Interactive elements only (no text)" },
683
738
  { cmd: "page.read --depth 3", desc: "Limit to 3 levels deep" },
684
739
  { cmd: "page.read --compact", desc: "Skip empty containers" },
685
- { cmd: "page.read --depth 3 --compact", desc: "Shallow + compact (60% smaller)" },
740
+ { cmd: "page.read --depth 3 --compact --max-bytes 2000", desc: "Shallow + compact output" },
686
741
  { cmd: "read", desc: "Alias" },
687
742
  ]
688
743
  },
@@ -823,7 +878,7 @@ const TOOLS = {
823
878
  ref: "Element ref (uses JS DOM method, more reliable for modals)",
824
879
  submit: "Press enter after",
825
880
  clear: "Clear first",
826
- method: "cdp|js (default: cdp, but ref uses JS automatically)"
881
+ method: "cdp|js (cursor typing uses CDP; selector/ref targets use JS)"
827
882
  },
828
883
  examples: [
829
884
  { cmd: 'type "hello world"', desc: "Type at cursor (CDP events)" },
@@ -1567,7 +1622,7 @@ const ALL_SOCKET_TOOLS = [
1567
1622
  "computer",
1568
1623
  "page.read", "page.text", "page.state",
1569
1624
  "locate.role", "locate.text", "locate.label",
1570
- "tab.list", "tab.new", "tab.switch", "tab.close", "tab.name", "tab.unname", "tab.named",
1625
+ "tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
1571
1626
  "tab.group", "tab.ungroup", "tab.groups", "tab.reload",
1572
1627
  "scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
1573
1628
  "wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
@@ -1659,6 +1714,10 @@ Quick Examples:
1659
1714
  surf window.new "https://example.com" && surf --window-id 123 go "https://other.com"
1660
1715
 
1661
1716
  More Help:
1717
+ --remote <host>:<port> Route requests to a remote native host
1718
+ --remote-credential <path> Use a mode-0600 Ed25519 remote credential file
1719
+ surf remote authorize <label> --output <path>
1720
+ surf remote list | surf remote revoke <label>
1662
1721
  surf --help-full All commands
1663
1722
  surf --llm-context Compact reference for AI agents
1664
1723
  surf --help-topic <topic> Topic guide (refs, semantic, frames, devices...)
@@ -1715,6 +1774,8 @@ Usage: surf <command> [args] [options]
1715
1774
  console.log(`Aliases: snap -> screenshot, read -> page.read, find -> search, go -> navigate
1716
1775
 
1717
1776
  Options:
1777
+ --remote <host>:<port> Route requests to a remote native host
1778
+ --remote-credential <path> Use a mode-0600 Ed25519 remote credential file
1718
1779
  --tab-id <id> Target specific tab
1719
1780
  --window-id <id> Target specific window (isolate from your browsing)
1720
1781
  --json Output raw JSON
@@ -1722,6 +1783,11 @@ Options:
1722
1783
  --soft-fail On error: warn and exit 0 (for non-critical commands)
1723
1784
  --no-lock Bypass the per-socket browser request lock
1724
1785
 
1786
+ Remote Credentials (run on the browser host):
1787
+ surf remote authorize <label> --output <credential-file>
1788
+ surf remote list
1789
+ surf remote revoke <label>
1790
+
1725
1791
  Script Mode:
1726
1792
  surf --script <file> Run workflow from JSON
1727
1793
  surf --script <file> --dry-run
@@ -1937,7 +2003,7 @@ if (args[0] === "server") {
1937
2003
  process.exit(0);
1938
2004
  }
1939
2005
  const { PiChromeMcpServer } = require("./mcp-server.cjs");
1940
- const server = new PiChromeMcpServer();
2006
+ const server = new PiChromeMcpServer(endpoint);
1941
2007
  server.start().catch((err) => {
1942
2008
  console.error("MCP Server error:", err.message);
1943
2009
  process.exit(1);
@@ -1953,7 +2019,7 @@ if (args[0] === "extension-path" || args[0] === "path") {
1953
2019
 
1954
2020
  if (args[0] === "doctor") {
1955
2021
  const { runDoctorCli } = require("./doctor.cjs");
1956
- runDoctorCli(args.slice(1)).then((code) => process.exit(code));
2022
+ runDoctorCli(args.slice(1), endpoint).then((code) => process.exit(code));
1957
2023
  return;
1958
2024
  }
1959
2025
 
@@ -1978,6 +2044,8 @@ Options:
1978
2044
  Multiple: --browser chrome,brave
1979
2045
  --target Install target: auto, linux, windows
1980
2046
  On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
2047
+ --listen <tailscale-ip>:<port>
2048
+ Requires surf remote authorize <label> --output <path> first.
1981
2049
 
1982
2050
  Examples:
1983
2051
  surf install hnfbepgmaoklhekckbpjnleifhahkcpl
@@ -2121,44 +2189,24 @@ if (args.includes("--script")) {
2121
2189
  process.exit(1);
2122
2190
  }
2123
2191
 
2192
+ let scriptTransport;
2124
2193
  const sendScriptRequest = (toolName, toolArgs = {}) => {
2125
- return new Promise((resolve, reject) => {
2126
- const sock = net.createConnection(SOCKET_PATH, () => {
2127
- const req = {
2128
- type: "tool_request",
2129
- method: "execute_tool",
2130
- params: { tool: toolName, args: toolArgs },
2131
- id: "cli-" + Date.now() + "-" + Math.random(),
2132
- };
2133
- if (scriptTabId) req.tabId = parseInt(scriptTabId, 10);
2134
- sock.write(JSON.stringify(req) + "\n");
2135
- });
2136
- let buf = "";
2137
- sock.on("data", (d) => {
2138
- buf += d.toString();
2139
- const lines = buf.split("\n");
2140
- buf = lines.pop();
2141
- for (const line of lines) {
2142
- if (!line.trim()) continue;
2143
- try {
2144
- const resp = JSON.parse(line);
2145
- sock.end();
2146
- resolve(resp);
2147
- } catch {
2148
- sock.end();
2149
- reject(new Error("Invalid JSON"));
2150
- }
2151
- }
2152
- });
2153
- sock.on("error", (e) => reject(new Error(formatSocketError(e))));
2154
- let timeoutId;
2155
- timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 30000);
2156
- sock.on("close", () => clearTimeout(timeoutId));
2157
- });
2194
+ const req = {
2195
+ type: "tool_request",
2196
+ method: "execute_tool",
2197
+ params: { tool: toolName, args: toolArgs },
2198
+ id: "cli-" + Date.now() + "-" + Math.random(),
2199
+ };
2200
+ if (scriptTabId) req.tabId = parseInt(scriptTabId, 10);
2201
+ const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
2202
+ req.params.args = prepared.args;
2203
+ return scriptTransport.request(req, resolveRequestDeadlineMs(toolName, prepared.args), prepared);
2158
2204
  };
2159
2205
 
2160
2206
  const runScript = async () => {
2161
- const total = script.steps.length;
2207
+ try {
2208
+ if (!dryRun) scriptTransport = await openClientTransport(endpoint);
2209
+ const total = script.steps.length;
2162
2210
  const results = [];
2163
2211
  let failed = 0;
2164
2212
 
@@ -2216,14 +2264,22 @@ if (args.includes("--script")) {
2216
2264
  console.log(`Summary: ${passed} passed, ${failed} failed, ${total} total`);
2217
2265
  }
2218
2266
 
2219
- process.exit(failed > 0 ? 1 : 0);
2267
+ return failed > 0 ? 1 : 0;
2268
+ } finally {
2269
+ await scriptTransport?.close();
2270
+ }
2220
2271
  };
2221
2272
 
2222
2273
  if (!dryRun) {
2223
- installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")));
2274
+ installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
2224
2275
  }
2225
2276
 
2226
- runScript();
2277
+ runScript()
2278
+ .then((code) => process.exit(code))
2279
+ .catch((error) => {
2280
+ console.error(`Error: ${error.message}`);
2281
+ process.exit(1);
2282
+ });
2227
2283
  return;
2228
2284
  }
2229
2285
 
@@ -2425,7 +2481,7 @@ if (args[0] === "do") {
2425
2481
  process.exit(0);
2426
2482
  }
2427
2483
 
2428
- installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")));
2484
+ installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")), endpoint);
2429
2485
 
2430
2486
  if (!wantJson) {
2431
2487
  if (workflowName) {
@@ -2436,7 +2492,10 @@ if (args[0] === "do") {
2436
2492
  }
2437
2493
 
2438
2494
  const runWorkflow = async () => {
2439
- const result = await executeDoSteps(steps, {
2495
+ let transport;
2496
+ try {
2497
+ transport = await openClientTransport(endpoint);
2498
+ const result = await executeDoSteps(steps, {
2440
2499
  onError,
2441
2500
  autoWait: !noAutoWait,
2442
2501
  stepDelay,
@@ -2445,30 +2504,40 @@ if (args[0] === "do") {
2445
2504
  context: {
2446
2505
  tabId,
2447
2506
  windowId,
2507
+ endpoint,
2508
+ transport,
2448
2509
  },
2449
- });
2510
+ });
2450
2511
 
2451
2512
  // Print summary
2452
2513
  if (wantJson) {
2453
2514
  console.log(JSON.stringify(result, null, 2));
2454
- process.exit(result.status === "completed" ? 0 : 1);
2515
+ return result.status === "completed" ? 0 : 1;
2455
2516
  }
2456
2517
 
2457
2518
  console.log("");
2458
2519
  if (result.status === "completed") {
2459
2520
  console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
2460
- process.exit(0);
2521
+ return 0;
2461
2522
  } else if (result.status === "partial") {
2462
2523
  console.log(`Partial: ${result.completedSteps}/${result.totalSteps} steps completed, ${result.failed} failed`);
2463
- process.exit(1);
2524
+ return 1;
2464
2525
  } else {
2465
2526
  console.error(`Failed: ${result.completedSteps}/${result.totalSteps} steps completed`);
2466
2527
  if (result.error) console.error(`Error: ${result.error}`);
2467
- process.exit(1);
2528
+ return 1;
2529
+ }
2530
+ } finally {
2531
+ transport?.close();
2468
2532
  }
2469
2533
  };
2470
2534
 
2471
- runWorkflow();
2535
+ runWorkflow()
2536
+ .then((code) => process.exit(code))
2537
+ .catch((error) => {
2538
+ console.error(`Error: ${error.message}`);
2539
+ process.exit(1);
2540
+ });
2472
2541
  return;
2473
2542
  }
2474
2543
 
@@ -2595,8 +2664,6 @@ if (args[0] === "workflow.validate") {
2595
2664
 
2596
2665
  const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "full-page", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait", "no-lock"];
2597
2666
 
2598
- const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];
2599
-
2600
2667
  const parseArgs = (rawArgs) => {
2601
2668
  const result = { positional: [], options: {} };
2602
2669
  for (let i = 0; i < rawArgs.length; i++) {
@@ -2732,6 +2799,7 @@ const PRIMARY_ARG_MAP = {
2732
2799
  "tab.switch": "id",
2733
2800
  close_tab: "tab_id",
2734
2801
  "tab.close": "id",
2802
+ "tab.move": "id",
2735
2803
  "tab.name": "name",
2736
2804
  "tab.unname": "name",
2737
2805
  scroll_to_position: "position",
@@ -2763,7 +2831,7 @@ const PRIMARY_ARG_MAP = {
2763
2831
  "select": "selector",
2764
2832
  };
2765
2833
 
2766
- const toolArgs = { ...options };
2834
+ let toolArgs = { ...options };
2767
2835
 
2768
2836
  if (tool === "scroll" && firstArg) {
2769
2837
  if (firstArg === "top" || firstArg === "bottom") {
@@ -2824,7 +2892,7 @@ if (firstArg !== undefined) {
2824
2892
  }
2825
2893
  }
2826
2894
 
2827
- if (tool === "js" && toolArgs.file) {
2895
+ if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
2828
2896
  try {
2829
2897
  toolArgs.code = fs.readFileSync(toolArgs.file, "utf8");
2830
2898
  delete toolArgs.file;
@@ -2834,6 +2902,17 @@ if (tool === "js" && toolArgs.file) {
2834
2902
  }
2835
2903
  }
2836
2904
 
2905
+ if (tool === "batch" && toolArgs.file) {
2906
+ try {
2907
+ const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
2908
+ toolArgs.actions = parsed;
2909
+ delete toolArgs.file;
2910
+ } catch (e) {
2911
+ console.error(`Error: Failed to read batch file: ${e.message}`);
2912
+ process.exit(1);
2913
+ }
2914
+ }
2915
+
2837
2916
  // Handle select command: capture multiple values after selector
2838
2917
  if (tool === "select" && positional.length > 2) {
2839
2918
  const values = positional.slice(2); // All args after "select <selector>"
@@ -2898,23 +2977,18 @@ if (tool === "aistudio.build" && outputPath) {
2898
2977
  toolArgs.output = path.resolve(outputPath);
2899
2978
  }
2900
2979
  if (tool === "gemini") {
2901
- if (outputPath) toolArgs.output = path.resolve(outputPath);
2902
- if (toolArgs["generate-image"] && typeof toolArgs["generate-image"] === "string") {
2903
- toolArgs["generate-image"] = path.resolve(toolArgs["generate-image"]);
2904
- }
2905
- if (toolArgs["edit-image"] && typeof toolArgs["edit-image"] === "string") {
2906
- toolArgs["edit-image"] = path.resolve(toolArgs["edit-image"]);
2907
- }
2908
- if (toolArgs.file && typeof toolArgs.file === "string") {
2909
- toolArgs.file = path.resolve(toolArgs.file);
2980
+ if (outputPath !== undefined) toolArgs.output = outputPath;
2981
+ if (toolArgs.model) {
2982
+ const known = ["gemini-3.1-pro", "gemini-3.5-flash", "gemini-3.1-flash-lite"];
2983
+ if (!known.includes(toolArgs.model)) {
2984
+ process.stderr.write(
2985
+ `warning: unknown Gemini model "${toolArgs.model}"; using "gemini-3.1-pro". Available: ${known.join(", ")}\n`,
2986
+ );
2987
+ }
2910
2988
  }
2911
2989
  }
2912
- if (tool === "chatgpt" && toolArgs.file) {
2913
- if (Array.isArray(toolArgs.file)) {
2914
- toolArgs.file = toolArgs.file.map((filePath) => path.resolve(filePath));
2915
- } else if (typeof toolArgs.file === "string") {
2916
- toolArgs.file = path.resolve(toolArgs.file);
2917
- }
2990
+ if (tool === "network.export" && outputPath !== undefined) {
2991
+ toolArgs.output = outputPath;
2918
2992
  }
2919
2993
 
2920
2994
  if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
@@ -2938,7 +3012,9 @@ const streamMode = toolArgs.stream === true;
2938
3012
  delete toolArgs.stream;
2939
3013
 
2940
3014
  const streamLevel = toolArgs.level;
2941
- delete toolArgs.level;
3015
+ if (tool === "console" || tool === "network") {
3016
+ delete toolArgs.level;
3017
+ }
2942
3018
 
2943
3019
  const streamFilter = toolArgs.filter;
2944
3020
  delete toolArgs.filter;
@@ -2946,11 +3022,15 @@ delete toolArgs.filter;
2946
3022
  let finalTool = tool;
2947
3023
  if (methodFlag === "js") {
2948
3024
  if (tool === "type") {
2949
- if (!toolArgs.selector) {
2950
- console.error("Error: --selector or --into required for type with --method js");
2951
- process.exit(1);
3025
+ if (toolArgs.ref) {
3026
+ finalTool = "type";
3027
+ } else {
3028
+ if (!toolArgs.selector) {
3029
+ console.error("Error: --selector, --into, or --ref required for type with --method js");
3030
+ process.exit(1);
3031
+ }
3032
+ finalTool = "smart_type";
2952
3033
  }
2953
- finalTool = "smart_type";
2954
3034
  } else if (tool === "click") {
2955
3035
  if (!toolArgs.selector) {
2956
3036
  console.error("Error: --selector required for click with --method js");
@@ -2961,8 +3041,13 @@ if (methodFlag === "js") {
2961
3041
  finalTool = "js";
2962
3042
  }
2963
3043
  } else if (methodFlag === "cdp") {
3044
+ if (tool === "type" && (toolArgs.selector || toolArgs.ref)) {
3045
+ console.error("Error: --method cdp types at the current focus and cannot be combined with --into, --selector, or --ref");
3046
+ process.exit(1);
3047
+ }
2964
3048
  if (tool === "smart_type") {
2965
- finalTool = "type";
3049
+ console.error("Error: smart_type uses the JS input path and cannot be combined with --method cdp");
3050
+ process.exit(1);
2966
3051
  }
2967
3052
  }
2968
3053
 
@@ -2980,8 +3065,10 @@ if (streamMode && (tool === "console" || tool === "network")) {
2980
3065
 
2981
3066
  let connectionTimeout = null;
2982
3067
  let receivedData = false;
3068
+ let streamWriter;
2983
3069
 
2984
- const sock = net.createConnection(SOCKET_PATH, () => {
3070
+ const sock = connectEndpoint(endpoint, () => {
3071
+ streamWriter = createSocketWriter(sock, { onOverflow: ({ error }) => sock.destroy(error) });
2985
3072
  const req = {
2986
3073
  type: "stream_request",
2987
3074
  streamType,
@@ -2989,7 +3076,11 @@ if (streamMode && (tool === "console" || tool === "network")) {
2989
3076
  id: "cli-stream-" + Date.now(),
2990
3077
  ...globalOpts,
2991
3078
  };
2992
- sock.write(JSON.stringify(req) + "\n");
3079
+ streamWriter.send(req).catch((error) => sock.destroy(error));
3080
+ if (connectionTimeout) {
3081
+ clearTimeout(connectionTimeout);
3082
+ connectionTimeout = null;
3083
+ }
2993
3084
  connectionTimeout = setTimeout(() => {
2994
3085
  if (!receivedData) {
2995
3086
  console.error("Error: Stream connection timeout (10s) - no data received");
@@ -2999,57 +3090,62 @@ if (streamMode && (tool === "console" || tool === "network")) {
2999
3090
  }, 10000);
3000
3091
  });
3001
3092
 
3002
- let buf = "";
3003
- sock.on("data", (d) => {
3004
- if (!receivedData) {
3005
- receivedData = true;
3006
- if (connectionTimeout) {
3007
- clearTimeout(connectionTimeout);
3008
- connectionTimeout = null;
3009
- }
3010
- }
3011
- buf += d.toString();
3012
- const lines = buf.split("\n");
3013
- buf = lines.pop();
3014
- for (const line of lines) {
3015
- if (!line.trim()) continue;
3016
- try {
3017
- const msg = JSON.parse(line);
3018
- if (msg.error) {
3019
- console.error("Error:", msg.error);
3020
- sock.end();
3021
- process.exit(1);
3022
- }
3023
- if (msg.type === "extension_disconnected") {
3024
- console.error(msg.message);
3025
- sock.end();
3026
- process.exit(1);
3027
- }
3028
- if (msg.type === "stream_started") {
3029
- continue;
3030
- }
3031
- if (msg.type === "console_event") {
3032
- const { level, text, timestamp } = msg;
3033
- if (streamLevel && level !== streamLevel) continue;
3034
- console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
3035
- } else if (msg.type === "network_event") {
3036
- const { method, url, status, duration } = msg;
3037
- if (streamFilter && !url.includes(streamFilter)) continue;
3038
- const statusStr = status !== undefined ? status : "...";
3039
- const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
3040
- console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
3093
+ connectionTimeout = setTimeout(() => {
3094
+ console.error(`Error: Stream connection timeout (10s) - could not connect to ${endpoint.display}`);
3095
+ sock.destroy();
3096
+ process.exit(1);
3097
+ }, 10000);
3098
+
3099
+ const parser = createFrameParser({
3100
+ onFrame(msg) {
3101
+ if (!receivedData) {
3102
+ receivedData = true;
3103
+ if (connectionTimeout) {
3104
+ clearTimeout(connectionTimeout);
3105
+ connectionTimeout = null;
3041
3106
  }
3042
- } catch {}
3043
- }
3107
+ }
3108
+ if (msg.error) {
3109
+ console.error("Error:", msg.error);
3110
+ sock.end();
3111
+ process.exit(1);
3112
+ }
3113
+ if (msg.type === "extension_disconnected") {
3114
+ console.error(msg.message);
3115
+ sock.end();
3116
+ process.exit(1);
3117
+ }
3118
+ if (msg.type === "stream_started") return;
3119
+ if (msg.type === "console_event") {
3120
+ const { level, text, timestamp } = msg;
3121
+ if (streamLevel && level !== streamLevel) return;
3122
+ console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
3123
+ } else if (msg.type === "network_event") {
3124
+ const { method, url, status, duration } = msg;
3125
+ if (streamFilter && !url.includes(streamFilter)) return;
3126
+ const statusStr = status !== undefined ? status : "...";
3127
+ const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
3128
+ console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
3129
+ }
3130
+ },
3131
+ onError(error) {
3132
+ if (connectionTimeout) clearTimeout(connectionTimeout);
3133
+ console.error("Error:", error.message);
3134
+ sock.destroy();
3135
+ process.exit(1);
3136
+ },
3044
3137
  });
3138
+ sock.on("data", (data) => parser.push(data));
3045
3139
 
3046
3140
  sock.on("error", (e) => {
3047
- console.error("Error:", formatSocketError(e));
3141
+ if (connectionTimeout) clearTimeout(connectionTimeout);
3142
+ console.error("Error:", formatEndpointError(e, endpoint, formatSocketError));
3048
3143
  process.exit(1);
3049
3144
  });
3050
3145
 
3051
3146
  process.on("SIGINT", () => {
3052
- sock.write(JSON.stringify({ type: "stream_stop" }) + "\n");
3147
+ if (connectionTimeout) clearTimeout(connectionTimeout);
3148
+ streamWriter?.send({ type: "stream_stop" }).catch(() => {});
3053
3149
  sock.end();
3054
3150
  process.exit(0);
3055
3151
  });
@@ -3057,6 +3153,17 @@ if (streamMode && (tool === "console" || tool === "network")) {
3057
3153
  return;
3058
3154
  }
3059
3155
 
3156
+ let transferPlan;
3157
+ try {
3158
+ transferPlan = endpoint.kind === "remote" ? prepareRemoteTool(finalTool, toolArgs) : (() => { const args = validateLocalToolPaths(finalTool, toolArgs); return { args, uploads: [], downloads: [] }; })();
3159
+ } catch (error) {
3160
+ const message = finalTool === "record" && endpoint.kind === "remote"
3161
+ ? `record is not supported with remote endpoint ${endpoint.display}`
3162
+ : error.message;
3163
+ console.error(`Error: ${message}`);
3164
+ process.exit(1);
3165
+ }
3166
+ toolArgs = transferPlan.args;
3060
3167
  const request = {
3061
3168
  type: "tool_request",
3062
3169
  method: "execute_tool",
@@ -3065,45 +3172,20 @@ const request = {
3065
3172
  ...globalOpts,
3066
3173
  };
3067
3174
 
3068
- const sendRequest = (toolName, toolArgs = {}, timeoutMs = 5000) => {
3069
- return new Promise((resolve, reject) => {
3070
- const sock = net.createConnection(SOCKET_PATH, () => {
3071
- const req = {
3072
- type: "tool_request",
3073
- method: "execute_tool",
3074
- params: { tool: toolName, args: toolArgs },
3075
- id: "cli-" + Date.now() + "-" + Math.random(),
3076
- ...globalOpts,
3077
- };
3078
- sock.write(JSON.stringify(req) + "\n");
3079
- });
3080
- let buf = "";
3081
- sock.on("data", (d) => {
3082
- buf += d.toString();
3083
- const lines = buf.split("\n");
3084
- buf = lines.pop();
3085
- for (const line of lines) {
3086
- if (!line.trim()) continue;
3087
- try {
3088
- const resp = JSON.parse(line);
3089
- if (resp.type === "extension_disconnected") {
3090
- sock.end();
3091
- reject(new Error(resp.message));
3092
- return;
3093
- }
3094
- sock.end();
3095
- resolve(resp);
3096
- } catch {
3097
- sock.end();
3098
- reject(new Error("Invalid JSON"));
3099
- }
3100
- }
3101
- });
3102
- sock.on("error", (e) => reject(new Error(formatSocketError(e))));
3103
- let timeoutId;
3104
- timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, timeoutMs);
3105
- sock.on("close", () => clearTimeout(timeoutId));
3106
- });
3175
+ const sendRequest = async (toolName, toolArgs = {}, timeoutMs = 5000) => {
3176
+ const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
3177
+ try {
3178
+ const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
3179
+ return await transport.request({
3180
+ type: "tool_request",
3181
+ method: "execute_tool",
3182
+ params: { tool: toolName, args: prepared.args },
3183
+ id: "cli-" + Date.now() + "-" + Math.random(),
3184
+ ...globalOpts,
3185
+ }, timeoutMs, prepared);
3186
+ } finally {
3187
+ await transport.close();
3188
+ }
3107
3189
  };
3108
3190
 
3109
3191
  function parseRecordNumber(value, fallback, name, min, max) {
@@ -3270,7 +3352,11 @@ const performAutoCapture = async () => {
3270
3352
  };
3271
3353
 
3272
3354
  if (finalTool === "record") {
3273
- installBrowserLock(lockOptions);
3355
+ if (endpoint.kind === "remote") {
3356
+ console.error(`Error: record is not supported with remote endpoint ${endpoint.display}`);
3357
+ process.exit(1);
3358
+ }
3359
+ installBrowserLock(lockOptions, endpoint);
3274
3360
  runRecord()
3275
3361
  .then(() => process.exit(0))
3276
3362
  .catch((error) => {
@@ -3280,57 +3366,67 @@ if (finalTool === "record") {
3280
3366
  return;
3281
3367
  }
3282
3368
 
3283
- installBrowserLock(lockOptions);
3369
+ installBrowserLock(lockOptions, endpoint);
3370
+ let socket;
3371
+ let timeout;
3372
+
3373
+ if (endpoint.kind === "remote") {
3374
+ socket = { end() {}, destroy() {} };
3375
+ const requestTimeout = resolveRequestDeadlineMs(tool, toolArgs);
3376
+ openClientTransport(endpoint, { requestTimeoutMs: requestTimeout })
3377
+ .then(async (transport) => {
3378
+ try {
3379
+ const response = await transport.request(request, requestTimeout, transferPlan);
3380
+ await handleResponse(response);
3381
+ } finally {
3382
+ await transport.close();
3383
+ }
3384
+ })
3385
+ .catch((error) => {
3386
+ console.error(`Error: ${error.message}`);
3387
+ process.exit(1);
3388
+ });
3389
+ return;
3390
+ }
3284
3391
 
3285
- const socket = net.createConnection(SOCKET_PATH, () => {
3286
- socket.write(JSON.stringify(request) + "\n");
3392
+ socket = connectEndpoint(endpoint, () => {
3393
+ writeFrame(socket, request).catch((error) => socket.destroy(error));
3287
3394
  });
3288
3395
 
3289
- const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "aistudio", "aistudio.build", "ai"];
3290
- let requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
3291
- if (tool === "aistudio.build") {
3292
- const userTimeoutSec = parseInt(options.timeout || "600", 10);
3293
- requestTimeout = (userTimeoutSec * 1000) + 60000;
3294
- }
3295
- const timeout = setTimeout(() => {
3396
+ const requestTimeout = resolveRequestDeadlineMs(tool, options);
3397
+ timeout = setTimeout(() => {
3296
3398
  console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
3297
3399
  socket.destroy();
3298
3400
  process.exit(1);
3299
3401
  }, requestTimeout);
3300
3402
 
3301
- let buffer = "";
3302
-
3303
- socket.on("data", (data) => {
3304
- buffer += data.toString();
3305
- const lines = buffer.split("\n");
3306
- buffer = lines.pop();
3307
-
3308
- for (const line of lines) {
3309
- if (!line.trim()) continue;
3310
- try {
3311
- const msg = JSON.parse(line);
3312
-
3313
- if (msg.type === "extension_disconnected") {
3314
- clearTimeout(timeout);
3315
- console.error(msg.message);
3316
- socket.end();
3317
- process.exit(1);
3318
- }
3319
-
3320
- handleResponse(msg).catch((err) => {
3321
- console.error("Handler error:", err.message);
3322
- process.exit(1);
3323
- });
3324
- } catch (e) {
3325
- console.error("Invalid JSON response:", line);
3403
+ const responseParser = createFrameParser({
3404
+ onFrame(msg) {
3405
+ if (msg.type === "extension_disconnected") {
3406
+ clearTimeout(timeout);
3407
+ console.error(msg.message);
3408
+ socket.end();
3326
3409
  process.exit(1);
3327
3410
  }
3328
- }
3411
+ if (msg.id !== request.id) return;
3412
+ handleResponse(msg).catch((err) => {
3413
+ console.error("Handler error:", err.message);
3414
+ process.exit(1);
3415
+ });
3416
+ },
3417
+ onError(error) {
3418
+ clearTimeout(timeout);
3419
+ console.error("Invalid response frame:", error.message);
3420
+ socket.destroy();
3421
+ process.exit(1);
3422
+ },
3329
3423
  });
3330
3424
 
3425
+ socket.on("data", (data) => responseParser.push(data));
3426
+
3331
3427
  socket.on("error", (err) => {
3332
3428
  clearTimeout(timeout);
3333
- console.error("Error:", formatSocketError(err));
3429
+ console.error("Error:", formatEndpointError(err, endpoint, formatSocketError));
3334
3430
  process.exit(1);
3335
3431
  });
3336
3432
 
@@ -3389,7 +3485,7 @@ async function handleResponse(response) {
3389
3485
  }
3390
3486
 
3391
3487
  if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
3392
- const saveTo = outputPath || toolArgs.savePath;
3488
+ const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
3393
3489
  fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
3394
3490
 
3395
3491
  const skipResize = options.full || toolArgs.full;