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/README.md +34 -6
- package/dist/content/accessibility-tree.js +8 -8
- package/dist/content/accessibility-tree.js.map +1 -1
- package/dist/service-worker/index.js +5 -5
- package/dist/service-worker/index.js.map +1 -1
- package/native/cli.cjs +65 -13
- package/native/config.cjs +3 -0
- package/native/host-helpers.cjs +62 -2
- package/native/host.cjs +3 -1
- package/package.json +4 -4
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-
|
|
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
|
|
250
|
-
{ cmd: "screenshot --
|
|
251
|
-
{ cmd: "screenshot --
|
|
252
|
-
{ cmd: "screenshot --annotate
|
|
253
|
-
{ cmd: "snap", desc: "
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
},
|
package/native/host-helpers.cjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
67
|
+
"vite": "^7.3.1",
|
|
68
68
|
"vitest": "^4.0.16"
|
|
69
69
|
}
|
|
70
70
|
}
|