surf-cli 2.16.1 → 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 +30 -6
- 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 +626 -197
- package/native/chatgpt-client.cjs +65 -6
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +10 -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 +68 -7
- package/native/oracle-host.cjs +46 -2
- package/native/oracle-jobs.cjs +5 -3
- 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/pi-extension/surf.ts +31 -2
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +11 -7
|
@@ -11,10 +11,14 @@ const {
|
|
|
11
11
|
resolveChatGPTEffortMenuOption,
|
|
12
12
|
resolveChatGPTModelMenuOption,
|
|
13
13
|
selectEffort,
|
|
14
|
+
selectChatTab,
|
|
15
|
+
selectGitHubTool,
|
|
14
16
|
selectModel,
|
|
17
|
+
verifyCurrentModel,
|
|
15
18
|
typePrompt,
|
|
16
19
|
verifyChatGPTEffortSelection,
|
|
17
20
|
verifyChatGPTModelSelection,
|
|
21
|
+
waitForChatGPTAttachment,
|
|
18
22
|
waitForPageLoad,
|
|
19
23
|
waitForPromptReady,
|
|
20
24
|
} = require("./chatgpt-client-ui.cjs");
|
|
@@ -32,6 +36,12 @@ const {
|
|
|
32
36
|
|
|
33
37
|
const CHATGPT_URL = "https://chatgpt.com/";
|
|
34
38
|
const RESPONSE_STARTED_AT = Symbol("responseStartedAt");
|
|
39
|
+
const ATTACHMENT_ERROR_CODES = new Set([
|
|
40
|
+
"attachment_chooser_interception",
|
|
41
|
+
"attachment_file_access",
|
|
42
|
+
"attachment_processing",
|
|
43
|
+
"attachment_selector_drift",
|
|
44
|
+
]);
|
|
35
45
|
|
|
36
46
|
function hasRequiredCookies(cookies) {
|
|
37
47
|
if (!cookies || !Array.isArray(cookies)) return false;
|
|
@@ -93,6 +103,7 @@ async function dispatch(options) {
|
|
|
93
103
|
cdpEvaluate,
|
|
94
104
|
cdpCommand,
|
|
95
105
|
uploadFile,
|
|
106
|
+
github = false,
|
|
96
107
|
beforeSubmit,
|
|
97
108
|
afterSubmit,
|
|
98
109
|
startUrl,
|
|
@@ -141,6 +152,12 @@ async function dispatch(options) {
|
|
|
141
152
|
throw codedError("ChatGPT login required", "auth");
|
|
142
153
|
}
|
|
143
154
|
log("Login verified");
|
|
155
|
+
if (github) {
|
|
156
|
+
await selectChatTab(cdp, 10000, signal);
|
|
157
|
+
log("Verified Chat tab");
|
|
158
|
+
await selectGitHubTool(cdp, 10000, signal);
|
|
159
|
+
log("Verified GitHub tool");
|
|
160
|
+
}
|
|
144
161
|
const promptReady = await waitForPromptReady(cdp, 30000, signal);
|
|
145
162
|
if (!promptReady) {
|
|
146
163
|
throw new Error("Prompt textarea not ready");
|
|
@@ -149,34 +166,62 @@ async function dispatch(options) {
|
|
|
149
166
|
let modelVerified = null;
|
|
150
167
|
let effortVerified = null;
|
|
151
168
|
if (model) {
|
|
152
|
-
modelVerified = await selectModel(cdp, model, 8000, signal);
|
|
169
|
+
modelVerified = await selectModel(cdp, inputCdp, model, 8000, signal);
|
|
153
170
|
log(`Verified model: ${modelVerified}`);
|
|
154
171
|
}
|
|
155
172
|
if (file) {
|
|
156
173
|
if (!uploadFile) {
|
|
157
|
-
throw
|
|
174
|
+
throw codedError(
|
|
158
175
|
"ChatGPT file upload unavailable: native host did not provide upload callback",
|
|
176
|
+
"attachment_chooser_interception",
|
|
159
177
|
);
|
|
160
178
|
}
|
|
161
179
|
const files = Array.isArray(file) ? file : [file];
|
|
162
180
|
const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
|
|
163
181
|
log(`Uploading ${absFiles.length} file(s) to ChatGPT...`);
|
|
164
|
-
|
|
182
|
+
let uploadResult;
|
|
183
|
+
try {
|
|
184
|
+
uploadResult = await guardedUploadFile(tabId, absFiles);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw classifyError(
|
|
187
|
+
error,
|
|
188
|
+
"attachment_chooser_interception",
|
|
189
|
+
["attachment_file_access", "attachment_processing", "attachment_selector_drift", "attachment_chooser_interception"],
|
|
190
|
+
);
|
|
191
|
+
}
|
|
165
192
|
if (uploadResult?.error) {
|
|
166
|
-
|
|
193
|
+
const uploadCode = ATTACHMENT_ERROR_CODES.has(uploadResult.errorCode)
|
|
194
|
+
? uploadResult.errorCode
|
|
195
|
+
: "attachment_chooser_interception";
|
|
196
|
+
throw codedError(
|
|
197
|
+
`ChatGPT file upload failed: ${uploadResult.error}`,
|
|
198
|
+
uploadCode,
|
|
199
|
+
);
|
|
167
200
|
}
|
|
168
201
|
if (!uploadResult?.success) {
|
|
169
|
-
throw
|
|
202
|
+
throw codedError(
|
|
203
|
+
"ChatGPT attachment processing failed: upload did not report success",
|
|
204
|
+
"attachment_processing",
|
|
205
|
+
);
|
|
170
206
|
}
|
|
171
207
|
log("File uploaded, waiting for ChatGPT attachment processing...");
|
|
172
208
|
await delay(1500, signal);
|
|
209
|
+
try {
|
|
210
|
+
await waitForChatGPTAttachment(cdp, absFiles, 30000, signal);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw classifyError(error, "attachment_processing", ["attachment_processing"]);
|
|
213
|
+
}
|
|
173
214
|
}
|
|
174
215
|
await typePrompt(cdp, inputCdp, prompt, signal);
|
|
175
216
|
log("Prompt typed");
|
|
176
217
|
if (effort) {
|
|
177
|
-
effortVerified = await selectEffort(cdp, effort, 8000, signal);
|
|
218
|
+
effortVerified = await selectEffort(cdp, inputCdp, effort, 8000, signal);
|
|
178
219
|
log(`Verified effort: ${effortVerified}`);
|
|
179
220
|
}
|
|
221
|
+
if (model) {
|
|
222
|
+
modelVerified = await verifyCurrentModel(cdp, inputCdp, model, 8000, signal);
|
|
223
|
+
log(`Reverified model before submit: ${modelVerified}`);
|
|
224
|
+
}
|
|
180
225
|
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
181
226
|
if (beforeSubmit) await raceAbort(beforeSubmit, signal);
|
|
182
227
|
await clickSend(cdp, inputCdp, signal);
|
|
@@ -201,6 +246,17 @@ async function dispatch(options) {
|
|
|
201
246
|
throw classifyError(error, "dispatch_failed", [
|
|
202
247
|
"auth",
|
|
203
248
|
"cloudflare",
|
|
249
|
+
"attachment_chooser_interception",
|
|
250
|
+
"attachment_file_access",
|
|
251
|
+
"attachment_processing",
|
|
252
|
+
"attachment_selector_drift",
|
|
253
|
+
"chat_mode_selection_failed",
|
|
254
|
+
"chat_mode_selector_drift",
|
|
255
|
+
"chat_mode_unavailable",
|
|
256
|
+
"github_tool_disconnected",
|
|
257
|
+
"github_tool_missing",
|
|
258
|
+
"github_tool_selection_failed",
|
|
259
|
+
"github_tool_selector_drift",
|
|
204
260
|
"model_verification_failed",
|
|
205
261
|
]);
|
|
206
262
|
}
|
|
@@ -353,5 +409,8 @@ module.exports = {
|
|
|
353
409
|
extractConversationUrl,
|
|
354
410
|
verifyChatGPTEffortSelection,
|
|
355
411
|
verifyChatGPTModelSelection,
|
|
412
|
+
selectChatTab,
|
|
413
|
+
selectGitHubTool,
|
|
414
|
+
waitForChatGPTAttachment,
|
|
356
415
|
CHATGPT_URL,
|
|
357
416
|
};
|
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
|
@@ -270,10 +270,18 @@ function validateLocalToolPaths(tool, args = {}) {
|
|
|
270
270
|
? args.files.map((value) => normalize("files", value))
|
|
271
271
|
: String(args.files).split(",").map((value) => normalize("files", value.trim())).join(",");
|
|
272
272
|
}
|
|
273
|
+
if (tool === "oracle.ask" && args.file !== undefined) {
|
|
274
|
+
normalized.file = Array.isArray(args.file)
|
|
275
|
+
? args.file.map((value) => normalize("file", value))
|
|
276
|
+
: normalize("file", args.file);
|
|
277
|
+
}
|
|
273
278
|
if (tool === "screenshot") {
|
|
274
279
|
if (args.savePath !== undefined && args.output !== undefined) throw transferError("screenshot accepts only one output path", "SURF_PATH_FIELD");
|
|
275
280
|
for (const field of ["savePath", "output"]) if (args[field] !== undefined) normalized[field] = normalize(field, args[field]);
|
|
276
281
|
}
|
|
282
|
+
if (tool === "video.start" || tool === "video.restart") {
|
|
283
|
+
if (args.output !== undefined) normalized.output = normalize("output", args.output);
|
|
284
|
+
}
|
|
277
285
|
if (tool === "network.export") {
|
|
278
286
|
if (args.har !== undefined && typeof args.har !== "boolean") throw transferError("network.export har must be boolean", "SURF_PATH_FIELD");
|
|
279
287
|
if (args.jsonl !== undefined && typeof args.jsonl !== "boolean") throw transferError("network.export jsonl must be boolean", "SURF_PATH_FIELD");
|
|
@@ -351,6 +359,7 @@ function prepareRemoteTool(tool, args = {}) {
|
|
|
351
359
|
if (args.autoScreenshot === true && !AUTO_SCREENSHOT_TOOLS.includes(tool)) throw transferError(`autoScreenshot is not supported for ${tool}`, "SURF_PATH_DESCRIPTOR");
|
|
352
360
|
if (args.autoScreenshotOutput !== undefined) throw transferError("autoScreenshotOutput is internal", "SURF_PATH_DESCRIPTOR");
|
|
353
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");
|
|
354
363
|
if (tool === "aistudio.build") throw transferError("aistudio.build is not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
355
364
|
if (tool === "smoke" && args.screenshot !== undefined) throw transferError("smoke screenshots are not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
356
365
|
if (tool === "network.export") {
|
|
@@ -432,7 +441,7 @@ async function materializeRemoteTool({ tool, args: rawArgs = {}, metadata = null
|
|
|
432
441
|
const uploads = meta.uploads === undefined ? [] : meta.uploads;
|
|
433
442
|
const downloads = meta.downloads === undefined ? [] : meta.downloads;
|
|
434
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");
|
|
435
|
-
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");
|
|
436
445
|
if (tool === "smoke" && args.screenshot !== undefined) throw transferError("smoke screenshots are not supported for remote connections", "SURF_REMOTE_UNSUPPORTED");
|
|
437
446
|
if (tool === "network.export") {
|
|
438
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
|
|