surf-cli 2.17.0 → 2.18.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 +28 -4
- package/agents/gpt-pro.md +2 -2
- package/native/browser-session-store.cjs +30 -2
- package/native/chatgpt-client-selection.cjs +79 -35
- package/native/chatgpt-client-ui.cjs +313 -198
- package/native/chatgpt-client.cjs +7 -2
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +5 -1
- package/native/host-helpers.cjs +1 -1
- package/native/host.cjs +424 -7
- package/native/mcp-server.cjs +1 -1
- package/native/oracle-cli.cjs +2 -2
- package/native/socket-permissions.cjs +114 -0
- package/native/tool-scope.cjs +6 -2
- package/native/video-recorder.cjs +444 -0
- package/native/workflow-definition.cjs +5 -0
- package/package.json +3 -3
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +7 -3
package/native/cli.cjs
CHANGED
|
@@ -33,6 +33,7 @@ const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endp
|
|
|
33
33
|
const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
|
|
34
34
|
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
35
35
|
const { classifyTool } = require("./tool-scope.cjs");
|
|
36
|
+
const { parseVideoFps, validateVideoOutputPath } = require("./video-recorder.cjs");
|
|
36
37
|
const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
37
38
|
const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
|
|
38
39
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
@@ -313,6 +314,14 @@ const TOOLS = {
|
|
|
313
314
|
args: [],
|
|
314
315
|
opts: { refresh: "Validate every binding against Chrome" },
|
|
315
316
|
},
|
|
317
|
+
"session.cleanup": {
|
|
318
|
+
desc: "Remove idle session bindings and close only Surf-created targets",
|
|
319
|
+
args: [],
|
|
320
|
+
opts: {
|
|
321
|
+
"idle-after": "Required threshold such as 30s, 5m, 1h, or 1d",
|
|
322
|
+
"dry-run": "Report matches without removing bindings or closing targets",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
316
325
|
"session.info": {
|
|
317
326
|
desc: "Show one session, target, and scheduler queue state",
|
|
318
327
|
args: ["name"],
|
|
@@ -346,7 +355,7 @@ const TOOLS = {
|
|
|
346
355
|
args: ["query"],
|
|
347
356
|
opts: {
|
|
348
357
|
"with-page": "Include current page context",
|
|
349
|
-
model: "Model:
|
|
358
|
+
model: "Model: gpt-6-astra, latest, gpt-5.6-sol, gpt-5.5",
|
|
350
359
|
file: "Attach file",
|
|
351
360
|
timeout: "Timeout in seconds (default: 2700 = 45min)"
|
|
352
361
|
},
|
|
@@ -619,6 +628,37 @@ const TOOLS = {
|
|
|
619
628
|
"snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
|
|
620
629
|
}
|
|
621
630
|
},
|
|
631
|
+
video: {
|
|
632
|
+
desc: "Local WebM recording",
|
|
633
|
+
commands: {
|
|
634
|
+
"video.start": {
|
|
635
|
+
desc: "Start local WebM recording",
|
|
636
|
+
args: ["output"],
|
|
637
|
+
opts: {
|
|
638
|
+
fps: "Frames per second (default: 30, max: 60)",
|
|
639
|
+
},
|
|
640
|
+
examples: [{ cmd: "video start ./demo.webm --fps 30", desc: "Start recording" }],
|
|
641
|
+
},
|
|
642
|
+
"video.stop": {
|
|
643
|
+
desc: "Stop local WebM recording",
|
|
644
|
+
args: [],
|
|
645
|
+
examples: [{ cmd: "video stop", desc: "Stop recording" }],
|
|
646
|
+
},
|
|
647
|
+
"video.status": {
|
|
648
|
+
desc: "Show local WebM recording status",
|
|
649
|
+
args: [],
|
|
650
|
+
examples: [{ cmd: "video status", desc: "Show status" }],
|
|
651
|
+
},
|
|
652
|
+
"video.restart": {
|
|
653
|
+
desc: "Restart local WebM recording",
|
|
654
|
+
args: ["output"],
|
|
655
|
+
opts: {
|
|
656
|
+
fps: "Frames per second (default: 30, max: 60)",
|
|
657
|
+
},
|
|
658
|
+
examples: [{ cmd: "video restart ./take2.webm --fps 60", desc: "Restart recording" }],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
},
|
|
622
662
|
scroll: {
|
|
623
663
|
desc: "Scrolling",
|
|
624
664
|
commands: {
|
|
@@ -1561,8 +1601,8 @@ Exclude text content:
|
|
|
1561
1601
|
};
|
|
1562
1602
|
|
|
1563
1603
|
const ALL_SOCKET_TOOLS = [
|
|
1564
|
-
"session.new", "session.ensure", "session.list", "session.info", "session.close", "session.rebind", "session.reopen",
|
|
1565
|
-
"ai", "screenshot", "record", "animate-audit", "perf-audit", "navigate",
|
|
1604
|
+
"session.new", "session.ensure", "session.list", "session.cleanup", "session.info", "session.close", "session.rebind", "session.reopen",
|
|
1605
|
+
"ai", "screenshot", "record", "video.start", "video.stop", "video.status", "video.restart", "animate-audit", "perf-audit", "navigate",
|
|
1566
1606
|
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1567
1607
|
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
1568
1608
|
"javascript_tool", "health", "smoke",
|
|
@@ -1621,6 +1661,10 @@ const SEE_ALSO = {
|
|
|
1621
1661
|
"navigate": ["wait.load", "page.read"],
|
|
1622
1662
|
"screenshot": ["page.read", "scroll.bottom for fullpage"],
|
|
1623
1663
|
"record": ["screenshot", "animate-audit", "perf-audit"],
|
|
1664
|
+
"video.start": ["video.stop", "video.status"],
|
|
1665
|
+
"video.stop": ["video.status", "video.restart"],
|
|
1666
|
+
"video.status": ["video.start", "video.stop"],
|
|
1667
|
+
"video.restart": ["video.stop", "video.status"],
|
|
1624
1668
|
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1625
1669
|
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1626
1670
|
"search": ["locate.text", "page.read"],
|
|
@@ -1641,11 +1685,16 @@ Usage: surf <command> [args] [options]
|
|
|
1641
1685
|
|
|
1642
1686
|
Common Commands:
|
|
1643
1687
|
session.ensure <name> [url] Idempotently create or reuse a tab-bound session
|
|
1688
|
+
session.cleanup --idle-after <duration> Remove idle sessions (opt-in; use --dry-run first)
|
|
1644
1689
|
navigate <url> Go to URL (alias: go)
|
|
1645
1690
|
click <ref> Click element by ref or selector
|
|
1646
1691
|
type <text> Type text at cursor or into element
|
|
1647
1692
|
screenshot Capture screenshot (alias: snap)
|
|
1648
1693
|
record Capture screenshot frames into an animated GIF
|
|
1694
|
+
video start <path> Start a long-running local WebM recording
|
|
1695
|
+
video stop Stop the active WebM recording
|
|
1696
|
+
video status Show WebM recording status
|
|
1697
|
+
video restart <path> Replace the active WebM recording
|
|
1649
1698
|
animate-audit JSON timeline of element animation/style samples
|
|
1650
1699
|
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1651
1700
|
page.read Get page accessibility tree (alias: read)
|
|
@@ -1698,6 +1747,7 @@ Type: surf type "text" --submit # use --ref e5 to target a fiel
|
|
|
1698
1747
|
Screenshot: surf screenshot /tmp/shot.png # auto-saves to /tmp if no path
|
|
1699
1748
|
Full page screenshot: surf screenshot --full-page /tmp/full.png
|
|
1700
1749
|
Record animation: surf record --duration 2000 --fps 10 --output /tmp/anim.gif
|
|
1750
|
+
Video recording: surf video start ./demo.webm --fps 30; surf video stop
|
|
1701
1751
|
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1702
1752
|
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1703
1753
|
JavaScript: surf js "return document.title"
|
|
@@ -1707,6 +1757,7 @@ Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
|
1707
1757
|
Cookies: surf cookie list | surf cookie get "name" | surf cookie delete "name"
|
|
1708
1758
|
Session targeting: surf --session research read | SURF_SESSION=research surf read
|
|
1709
1759
|
Session status/queue: surf session.info research | surf session.list --refresh
|
|
1760
|
+
Session cleanup: surf session.cleanup --idle-after 1h [--dry-run]
|
|
1710
1761
|
Recovery: run the exact command printed after Recovery: on tab_gone, session_epoch_stale, tab_busy, or browser_busy
|
|
1711
1762
|
Concurrency: commands for different session tabs can overlap; each tab remains FIFO; provider flows are browser-exclusive
|
|
1712
1763
|
Doctor: surf doctor --browser all # native host/socket diagnostics
|
|
@@ -2014,12 +2065,17 @@ Options:
|
|
|
2014
2065
|
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
2015
2066
|
--listen <tailscale-ip>:<port>
|
|
2016
2067
|
Requires surf remote authorize <label> --output <path> first.
|
|
2068
|
+
--socket-mode <600|660>
|
|
2069
|
+
Persist the local POSIX socket mode (default: 600); 660 requires --socket-group.
|
|
2070
|
+
--socket-group <group-or-gid>
|
|
2071
|
+
Persist the local POSIX socket group for mode 660.
|
|
2017
2072
|
|
|
2018
2073
|
Examples:
|
|
2019
2074
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
2020
2075
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser brave
|
|
2021
2076
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser all
|
|
2022
2077
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --target linux
|
|
2078
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --socket-mode 660 --socket-group surf
|
|
2023
2079
|
`);
|
|
2024
2080
|
process.exit(0);
|
|
2025
2081
|
}
|
|
@@ -2686,6 +2742,7 @@ if (tool === "session" && firstArg) {
|
|
|
2686
2742
|
new: "session.new",
|
|
2687
2743
|
ensure: "session.ensure",
|
|
2688
2744
|
list: "session.list",
|
|
2745
|
+
cleanup: "session.cleanup",
|
|
2689
2746
|
info: "session.info",
|
|
2690
2747
|
close: "session.close",
|
|
2691
2748
|
rebind: "session.rebind",
|
|
@@ -2715,6 +2772,21 @@ if (tool === "cookie" && firstArg) {
|
|
|
2715
2772
|
}
|
|
2716
2773
|
}
|
|
2717
2774
|
|
|
2775
|
+
if (tool === "video" && firstArg) {
|
|
2776
|
+
const videoSubcommands = {
|
|
2777
|
+
start: "video.start",
|
|
2778
|
+
stop: "video.stop",
|
|
2779
|
+
status: "video.status",
|
|
2780
|
+
restart: "video.restart",
|
|
2781
|
+
};
|
|
2782
|
+
const videoTool = videoSubcommands[firstArg];
|
|
2783
|
+
if (videoTool) {
|
|
2784
|
+
tool = videoTool;
|
|
2785
|
+
positional = [tool, ...positional.slice(2)];
|
|
2786
|
+
firstArg = positional[1];
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2718
2790
|
if (!tool) {
|
|
2719
2791
|
console.error("Error: No command specified");
|
|
2720
2792
|
process.exit(1);
|
|
@@ -2879,6 +2951,11 @@ if (tool === "record" && firstArg !== undefined && toolArgs.output === undefined
|
|
|
2879
2951
|
firstArg = undefined;
|
|
2880
2952
|
}
|
|
2881
2953
|
|
|
2954
|
+
if ((tool === "video.start" || tool === "video.restart") && firstArg !== undefined && toolArgs.output === undefined) {
|
|
2955
|
+
toolArgs.output = firstArg;
|
|
2956
|
+
firstArg = undefined;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2882
2959
|
if (firstArg !== undefined) {
|
|
2883
2960
|
const primaryKey = PRIMARY_ARG_MAP[tool];
|
|
2884
2961
|
if (primaryKey && toolArgs[primaryKey] === undefined) {
|
|
@@ -3007,8 +3084,11 @@ if (tool === "gemini") {
|
|
|
3007
3084
|
if (tool === "network.export" && outputPath !== undefined) {
|
|
3008
3085
|
toolArgs.output = outputPath;
|
|
3009
3086
|
}
|
|
3087
|
+
if ((tool === "video.start" || tool === "video.restart") && outputPath !== undefined) {
|
|
3088
|
+
toolArgs.output = outputPath;
|
|
3089
|
+
}
|
|
3010
3090
|
|
|
3011
|
-
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save") && outputPath && typeof outputPath !== "string") {
|
|
3091
|
+
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save" || tool === "video.start" || tool === "video.restart") && outputPath && typeof outputPath !== "string") {
|
|
3012
3092
|
console.error("Error: --output requires a file path");
|
|
3013
3093
|
process.exit(1);
|
|
3014
3094
|
}
|
|
@@ -3018,6 +3098,16 @@ if (tool === "page.save" && !outputPath) {
|
|
|
3018
3098
|
process.exit(1);
|
|
3019
3099
|
}
|
|
3020
3100
|
|
|
3101
|
+
if (tool === "video.start" || tool === "video.restart") {
|
|
3102
|
+
try {
|
|
3103
|
+
parseVideoFps(toolArgs.fps);
|
|
3104
|
+
validateVideoOutputPath(toolArgs.output, { createParent: false });
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
console.error(`Error: ${error.message}`);
|
|
3107
|
+
process.exit(1);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3021
3111
|
if (tool === "screenshot" && outputPath) {
|
|
3022
3112
|
toolArgs.savePath = outputPath;
|
|
3023
3113
|
if (options.full) toolArgs.full = true;
|
|
@@ -3615,6 +3705,21 @@ async function handleResponse(response) {
|
|
|
3615
3705
|
].join("\t"));
|
|
3616
3706
|
}
|
|
3617
3707
|
}
|
|
3708
|
+
} else if (finalTool === "session.cleanup" && data?.success) {
|
|
3709
|
+
const removed = Array.isArray(data.removed) ? data.removed : [];
|
|
3710
|
+
if (removed.length === 0) {
|
|
3711
|
+
console.log("No browser sessions matched the idle cleanup threshold.");
|
|
3712
|
+
} else {
|
|
3713
|
+
const verb = data.dryRun ? "Would remove" : "Removed";
|
|
3714
|
+
for (const entry of removed) {
|
|
3715
|
+
const target = entry.targetAction === "close"
|
|
3716
|
+
? "target closed"
|
|
3717
|
+
: entry.targetAction === "keep"
|
|
3718
|
+
? "target kept"
|
|
3719
|
+
: "target already gone";
|
|
3720
|
+
console.log(`${verb} session ${entry.name} tab=${entry.tabId ?? "-"} (${target})`);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3618
3723
|
} else if (finalTool === "session.info" && data?.session) {
|
|
3619
3724
|
const entry = data.session;
|
|
3620
3725
|
console.log(`Session: ${entry.name}`);
|
|
@@ -3631,6 +3736,18 @@ async function handleResponse(response) {
|
|
|
3631
3736
|
console.log(`Use: export SURF_SESSION=${entry.name}`);
|
|
3632
3737
|
} else if (finalTool === "session.close" && data?.success) {
|
|
3633
3738
|
console.log(`Session ${data.name} closed (${data.targetClosed ? "target closed" : "target kept"})`);
|
|
3739
|
+
} else if (finalTool === "video.start" && data?.status === "active") {
|
|
3740
|
+
console.log(`Video recording started: ${data.path} (${data.fps}fps)`);
|
|
3741
|
+
} else if (finalTool === "video.restart" && data?.status === "active") {
|
|
3742
|
+
console.log(`Video recording restarted: ${data.path} (${data.fps}fps)`);
|
|
3743
|
+
} else if (finalTool === "video.stop" && data?.status === "stopped") {
|
|
3744
|
+
console.log(`Saved video recording to ${data.path} (${data.frames} frames, ${data.capturedFrames} captured frames @ ${data.fps}fps)`);
|
|
3745
|
+
} else if (finalTool === "video.status") {
|
|
3746
|
+
if (data?.status === "active") {
|
|
3747
|
+
console.log(`Video recording active: ${data.path} (${data.fps}fps, ${data.frames} frames, ${data.capturedFrames} captured frames)`);
|
|
3748
|
+
} else {
|
|
3749
|
+
console.log("No active video recording");
|
|
3750
|
+
}
|
|
3634
3751
|
} else if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3635
3752
|
const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
|
|
3636
3753
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
package/native/file-transfer.cjs
CHANGED
|
@@ -279,6 +279,9 @@ function validateLocalToolPaths(tool, args = {}) {
|
|
|
279
279
|
if (args.savePath !== undefined && args.output !== undefined) throw transferError("screenshot accepts only one output path", "SURF_PATH_FIELD");
|
|
280
280
|
for (const field of ["savePath", "output"]) if (args[field] !== undefined) normalized[field] = normalize(field, args[field]);
|
|
281
281
|
}
|
|
282
|
+
if (tool === "video.start" || tool === "video.restart") {
|
|
283
|
+
if (args.output !== undefined) normalized.output = normalize("output", args.output);
|
|
284
|
+
}
|
|
282
285
|
if (tool === "network.export") {
|
|
283
286
|
if (args.har !== undefined && typeof args.har !== "boolean") throw transferError("network.export har must be boolean", "SURF_PATH_FIELD");
|
|
284
287
|
if (args.jsonl !== undefined && typeof args.jsonl !== "boolean") throw transferError("network.export jsonl must be boolean", "SURF_PATH_FIELD");
|
|
@@ -356,6 +359,7 @@ function prepareRemoteTool(tool, args = {}) {
|
|
|
356
359
|
if (args.autoScreenshot === true && !AUTO_SCREENSHOT_TOOLS.includes(tool)) throw transferError(`autoScreenshot is not supported for ${tool}`, "SURF_PATH_DESCRIPTOR");
|
|
357
360
|
if (args.autoScreenshotOutput !== undefined) throw transferError("autoScreenshotOutput is internal", "SURF_PATH_DESCRIPTOR");
|
|
358
361
|
if (tool === "record") throw transferError("record is not supported with remote endpoint", "SURF_REMOTE_UNSUPPORTED");
|
|
362
|
+
if (typeof tool === "string" && tool.startsWith("video.")) throw transferError("video recording is not supported with remote endpoint", "SURF_REMOTE_UNSUPPORTED");
|
|
359
363
|
if (tool === "aistudio.build") throw transferError("aistudio.build is not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
360
364
|
if (tool === "smoke" && args.screenshot !== undefined) throw transferError("smoke screenshots are not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
361
365
|
if (tool === "network.export") {
|
|
@@ -437,7 +441,7 @@ async function materializeRemoteTool({ tool, args: rawArgs = {}, metadata = null
|
|
|
437
441
|
const uploads = meta.uploads === undefined ? [] : meta.uploads;
|
|
438
442
|
const downloads = meta.downloads === undefined ? [] : meta.downloads;
|
|
439
443
|
if (!Array.isArray(pathRefs) || !Array.isArray(uploads) || !Array.isArray(downloads) || uploads.length > 1 || downloads.length > 1) throw transferError("invalid transfer metadata", "SURF_PATH_DESCRIPTOR");
|
|
440
|
-
if (tool === "record" || tool === "aistudio.build") throw transferError(`${tool} is not supported for remote connections`, "SURF_REMOTE_UNSUPPORTED");
|
|
444
|
+
if (tool === "record" || (typeof tool === "string" && tool.startsWith("video.")) || tool === "aistudio.build") throw transferError(`${tool} is not supported for remote connections`, "SURF_REMOTE_UNSUPPORTED");
|
|
441
445
|
if (tool === "smoke" && args.screenshot !== undefined) throw transferError("smoke screenshots are not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
442
446
|
if (tool === "network.export") {
|
|
443
447
|
if (args.har !== undefined && typeof args.har !== "boolean") throw transferError("network.export har must be boolean", "SURF_PATH_FIELD");
|
package/native/host-helpers.cjs
CHANGED
|
@@ -51,7 +51,7 @@ function formatToolContent(result, log = () => {}, options = {}) {
|
|
|
51
51
|
|
|
52
52
|
if (!result) return text("OK");
|
|
53
53
|
|
|
54
|
-
if (result.session || Array.isArray(result.sessions) || Object.hasOwn(result, "targetClosed")) {
|
|
54
|
+
if (result.session || Array.isArray(result.sessions) || Array.isArray(result.removed) || Object.hasOwn(result, "targetClosed")) {
|
|
55
55
|
return text(JSON.stringify(result, null, 2));
|
|
56
56
|
}
|
|
57
57
|
|