surf-cli 2.0.0 → 2.2.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/native/cli.cjs CHANGED
@@ -234,7 +234,7 @@ const TOOLS = {
234
234
  examples: [{ cmd: "forward", desc: "Browser forward" }]
235
235
  },
236
236
  "screenshot": {
237
- desc: "Capture screenshot (auto-resized for LLM by default)",
237
+ desc: "Capture screenshot (auto-saves to /tmp by default)",
238
238
  args: [],
239
239
  opts: {
240
240
  output: "Save to file",
@@ -243,14 +243,15 @@ const TOOLS = {
243
243
  fullpage: "Capture full page",
244
244
  "max-height": "Max height for fullpage (default: 4000)",
245
245
  full: "Skip resize, save at full resolution",
246
- "max-size": "Max dimension in px (default: 1200)"
246
+ "max-size": "Max dimension in px (default: 1200)",
247
+ "no-save": "Don't auto-save, return base64 + ID (saves context)"
247
248
  },
248
249
  examples: [
249
- { cmd: "screenshot --output /tmp/shot.png", desc: "Save to file (auto-resized)" },
250
- { cmd: "screenshot --full --output /tmp/shot.png", desc: "Full resolution" },
251
- { cmd: "screenshot --max-size 800 --output /tmp/small.png", desc: "Custom max size" },
252
- { cmd: "screenshot --annotate --output /tmp/annotated.png", desc: "With element labels" },
253
- { cmd: "snap", desc: "Auto-save to /tmp (resized)" },
250
+ { cmd: "screenshot", desc: "Auto-save to /tmp (default)" },
251
+ { cmd: "screenshot --output /tmp/shot.png", desc: "Save to specific file" },
252
+ { cmd: "screenshot --no-save", desc: "Return base64 without saving" },
253
+ { cmd: "screenshot --annotate", desc: "With element labels" },
254
+ { cmd: "snap", desc: "Alias for screenshot" },
254
255
  ]
255
256
  },
256
257
  "snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
@@ -348,6 +349,37 @@ const TOOLS = {
348
349
  },
349
350
  }
350
351
  },
352
+ element: {
353
+ desc: "Element inspection",
354
+ commands: {
355
+ "element.styles": {
356
+ desc: "Get computed styles from element(s)",
357
+ args: ["ref_or_selector"],
358
+ examples: [
359
+ { cmd: "element.styles e5", desc: "Get styles by ref" },
360
+ { cmd: 'element.styles ".header"', desc: "Get styles by selector (can return multiple)" },
361
+ ]
362
+ },
363
+ }
364
+ },
365
+ forms: {
366
+ desc: "Form interactions",
367
+ commands: {
368
+ "select": {
369
+ desc: "Select option(s) in dropdown",
370
+ args: ["ref_or_selector", "values..."],
371
+ opts: {
372
+ by: "Match by: value (default), label, index"
373
+ },
374
+ examples: [
375
+ { cmd: 'select e5 "US"', desc: "Select by value" },
376
+ { cmd: 'select e5 "opt1" "opt2"', desc: "Multi-select" },
377
+ { cmd: 'select e5 --by label "United States"', desc: "Select by visible text" },
378
+ { cmd: 'select e5 --by index 0', desc: "Select first option" },
379
+ ]
380
+ },
381
+ }
382
+ },
351
383
  wait: {
352
384
  desc: "Waiting",
353
385
  commands: {
@@ -1672,7 +1704,7 @@ if (args.includes("--script")) {
1672
1704
  return;
1673
1705
  }
1674
1706
 
1675
- const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl"];
1707
+ const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save"];
1676
1708
 
1677
1709
  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"];
1678
1710
 
@@ -1727,10 +1759,14 @@ if (REMOVED_COMMANDS[tool]) {
1727
1759
  process.exit(1);
1728
1760
  }
1729
1761
 
1730
- const wasSnap = tool === "snap";
1731
1762
  tool = ALIASES[tool] || tool;
1732
1763
 
1733
- if (wasSnap && !options.output && !options.savePath) {
1764
+ // Auto-save screenshots to temp file when no --output specified
1765
+ // This ensures agents always get a usable file path, not just an in-memory ID
1766
+ // Can be disabled with --no-save flag or autoSaveScreenshots: false in surf.json
1767
+ const config = loadConfig();
1768
+ const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
1769
+ if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
1734
1770
  options.savePath = `/tmp/surf-snap-${Date.now()}.png`;
1735
1771
  }
1736
1772
 
@@ -1804,6 +1840,8 @@ const PRIMARY_ARG_MAP = {
1804
1840
  "locate.label": "label",
1805
1841
  "emulate.device": "device",
1806
1842
  "frame.js": "code",
1843
+ "element.styles": "selector",
1844
+ "select": "selector",
1807
1845
  };
1808
1846
 
1809
1847
  const toolArgs = { ...options };
@@ -1840,6 +1878,17 @@ if (tool === "js" && toolArgs.file) {
1840
1878
  }
1841
1879
  }
1842
1880
 
1881
+ // Handle select command: capture multiple values after selector
1882
+ if (tool === "select" && positional.length > 2) {
1883
+ const values = positional.slice(2); // All args after "select <selector>"
1884
+ toolArgs.values = values.length === 1 ? values[0] : values;
1885
+ } else if (tool === "select" && positional.length === 2) {
1886
+ // Only selector provided, no values
1887
+ console.error("Error: select requires at least one value");
1888
+ console.error("Usage: surf select <selector> <value...>");
1889
+ process.exit(1);
1890
+ }
1891
+
1843
1892
  if (toolArgs.into && !toolArgs.selector) {
1844
1893
  toolArgs.selector = toolArgs.into;
1845
1894
  delete toolArgs.into;
@@ -1887,7 +1936,7 @@ if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
1887
1936
  const outputPath = toolArgs.output;
1888
1937
  delete toolArgs.output;
1889
1938
 
1890
- if ((tool === "screenshot" || tool === "snap") && outputPath) {
1939
+ if (tool === "screenshot" && outputPath) {
1891
1940
  if (typeof outputPath !== "string") {
1892
1941
  console.error("Error: --output requires a file path");
1893
1942
  process.exit(1);
@@ -2208,7 +2257,7 @@ async function handleResponse(response) {
2208
2257
  process.exit(0);
2209
2258
  }
2210
2259
 
2211
- if ((tool === "screenshot" || tool === "snap") && data?.base64 && (outputPath || toolArgs.savePath)) {
2260
+ if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
2212
2261
  const saveTo = outputPath || toolArgs.savePath;
2213
2262
  fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
2214
2263
 
@@ -2227,8 +2276,11 @@ async function handleResponse(response) {
2227
2276
  } else {
2228
2277
  console.log(`Saved to ${saveTo} (${origWidth}x${origHeight})`);
2229
2278
  }
2230
- } else if ((tool === "screenshot" || tool === "snap") && data?.message) {
2279
+ } else if (tool === "screenshot" && data?.message) {
2231
2280
  console.log(data.message);
2281
+ if (data.screenshotId) {
2282
+ console.log(`[Screenshot ID: ${data.screenshotId}]`);
2283
+ }
2232
2284
  } else if (tool === "tab.list") {
2233
2285
  const tabs = data?.tabs || data || [];
2234
2286
  if (Array.isArray(tabs)) {
package/native/config.cjs CHANGED
@@ -8,6 +8,9 @@ let cachedConfig = null;
8
8
  let cachedConfigPath = null;
9
9
 
10
10
  const STARTER_CONFIG = {
11
+ // Set to false to disable auto-saving screenshots to /tmp
12
+ // When disabled, screenshots return base64 + ID instead of file path
13
+ autoSaveScreenshots: true,
11
14
  routes: {
12
15
  main: ["http://localhost:3000"]
13
16
  },
@@ -20,6 +20,37 @@ function formatToolContent(result, log = () => {}) {
20
20
  return text(result.content);
21
21
  }
22
22
 
23
+ // Handle element.styles response
24
+ if (result.styles && Array.isArray(result.styles)) {
25
+ const output = result.styles.map(el => {
26
+ const lines = [`<${el.tag}>${el.text ? ` "${el.text.slice(0, 50)}${el.text.length > 50 ? '...' : ''}"` : ''}`];
27
+ if (el.box) lines.push(` box: ${el.box.x},${el.box.y} ${el.box.width}x${el.box.height}`);
28
+ const s = el.styles;
29
+ if (s) {
30
+ if (s.fontSize) lines.push(` font: ${s.fontSize} ${s.fontWeight} ${s.fontFamily}`);
31
+ if (s.color) lines.push(` color: ${s.color}`);
32
+ if (s.backgroundColor && s.backgroundColor !== 'rgba(0, 0, 0, 0)') lines.push(` bg: ${s.backgroundColor}`);
33
+ if (s.borderRadius && s.borderRadius !== '0px') lines.push(` radius: ${s.borderRadius}`);
34
+ if (s.border) lines.push(` border: ${s.border}`);
35
+ if (s.boxShadow) lines.push(` shadow: ${s.boxShadow}`);
36
+ if (s.padding && s.padding !== '0px') lines.push(` padding: ${s.padding}`);
37
+ }
38
+ return lines.join('\n');
39
+ }).join('\n\n');
40
+ return text(output || "No elements found");
41
+ }
42
+
43
+ // Handle select response
44
+ if (result.selected !== undefined) {
45
+ let output = Array.isArray(result.selected)
46
+ ? `Selected: ${result.selected.join(', ')}`
47
+ : `Selected: ${result.selected}`;
48
+ if (result.warning) {
49
+ output += `\n[Warning: ${result.warning}]`;
50
+ }
51
+ return text(output);
52
+ }
53
+
23
54
  // Handle ChatGPT/Gemini responses
24
55
  if (result.response !== undefined && result.model !== undefined && result.tookMs !== undefined) {
25
56
  let output = result.response;
@@ -84,7 +115,17 @@ function formatToolContent(result, log = () => {}) {
84
115
  return text(result.output);
85
116
  }
86
117
 
87
- if (result.screenshotId) {
118
+ // Screenshot saved to file (has path but no base64)
119
+ if (result.path && result.message && !result.base64) {
120
+ let msg = result.message;
121
+ if (result.screenshotId) {
122
+ msg += `\n[Screenshot ID: ${result.screenshotId} - use with upload_image]`;
123
+ }
124
+ return text(msg);
125
+ }
126
+
127
+ // Screenshot with inline base64 (MCP flow or no savePath)
128
+ if (result.screenshotId && result.base64) {
88
129
  const dims = result.width && result.height
89
130
  ? `${result.width}x${result.height}`
90
131
  : "unknown dimensions";
@@ -489,7 +530,7 @@ function mapToolToMessage(tool, args, tabId) {
489
530
  case "screenshot":
490
531
  return {
491
532
  type: "EXECUTE_SCREENSHOT",
492
- savePath: a.savePath,
533
+ savePath: a.savePath || a.output, // Accept both savePath (CLI) and output (MCP)
493
534
  annotate: a.annotate || false,
494
535
  fullpage: a.fullpage || false,
495
536
  maxHeight: a["max-height"] || 4000,
@@ -805,6 +846,25 @@ function mapToolToMessage(tool, args, tabId) {
805
846
  value: a.value,
806
847
  ...baseMsg
807
848
  };
849
+ case "element.styles":
850
+ if (!a.selector) throw new Error("selector argument required");
851
+ return {
852
+ type: "GET_ELEMENT_STYLES",
853
+ selector: a.selector,
854
+ ...baseMsg
855
+ };
856
+ case "select": {
857
+ if (!a.selector) throw new Error("selector argument required");
858
+ const values = Array.isArray(a.values) ? a.values : (a.values ? [a.values] : []);
859
+ if (values.length === 0) throw new Error("at least one value required");
860
+ return {
861
+ type: "SELECT_OPTION",
862
+ selector: a.selector,
863
+ values,
864
+ by: a.by || "value", // value, label, or index
865
+ ...baseMsg
866
+ };
867
+ }
808
868
  case "ai":
809
869
  return { type: "AI_ANALYZE", query: a.query, act: a.act, mode: a.mode, ...baseMsg };
810
870
  case "wait":
package/native/host.cjs CHANGED
@@ -996,7 +996,9 @@ function processInput() {
996
996
  }
997
997
  }
998
998
  sendToolResponse(socket, originalId, {
999
- message: `Saved to ${savePath} (${finalDims})`
999
+ message: `Saved to ${savePath} (${finalDims})`,
1000
+ path: savePath,
1001
+ screenshotId: msg.screenshotId, // Preserve for upload_image workflow
1000
1002
  }, null);
1001
1003
  } catch (e) {
1002
1004
  sendToolResponse(socket, originalId, null, `Failed to save: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -49,14 +49,14 @@
49
49
  "uninstall:native": "node scripts/uninstall-native-host.cjs"
50
50
  },
51
51
  "dependencies": {
52
- "@google/generative-ai": "^0.21.0",
52
+ "@google/generative-ai": "^0.24.1",
53
53
  "@modelcontextprotocol/sdk": "^1.7.0",
54
54
  "buffer": "^6.0.3",
55
55
  "crypto-browserify": "^3.12.1",
56
56
  "events": "^3.3.0",
57
57
  "stream-browserify": "^3.0.0",
58
58
  "vite-plugin-node-polyfills": "^0.24.0",
59
- "zod": "^3.24.0"
59
+ "zod": "^4.3.5"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@biomejs/biome": "^2.3.11",
@@ -64,7 +64,7 @@
64
64
  "@vitest/coverage-v8": "^4.0.16",
65
65
  "@vitest/ui": "^4.0.16",
66
66
  "typescript": "^5.7.2",
67
- "vite": "^6.0.0",
67
+ "vite": "^7.3.1",
68
68
  "vitest": "^4.0.16"
69
69
  }
70
70
  }