surf-cli 2.14.0 → 2.15.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 +62 -33
- package/agents/gpt-pro.md +19 -0
- package/native/browser-scheduler.cjs +348 -0
- package/native/browser-session-store.cjs +271 -0
- package/native/cli.cjs +331 -60
- package/native/do-executor.cjs +5 -0
- package/native/host-helpers.cjs +19 -3
- package/native/host-sessions.cjs +8 -1
- package/native/host.cjs +766 -19
- package/native/playbook-cli.cjs +16 -3
- package/native/surf-error.cjs +47 -0
- package/native/tool-scope.cjs +107 -0
- package/native/workflow-definition.cjs +7 -0
- package/package.json +8 -2
- package/pi-extension/surf.ts +18 -4
- package/skills/surf/SKILL.md +48 -21
package/native/cli.cjs
CHANGED
|
@@ -24,7 +24,7 @@ const {
|
|
|
24
24
|
formatOracleOutput,
|
|
25
25
|
handleOracleCli,
|
|
26
26
|
} = require("./oracle-cli.cjs");
|
|
27
|
-
const { formatPlaybookOutput, handlePlaybookCli
|
|
27
|
+
const { formatPlaybookOutput, handlePlaybookCli } = require("./playbook-cli.cjs");
|
|
28
28
|
|
|
29
29
|
const IS_WIN = process.platform === "win32";
|
|
30
30
|
const { SURF_TMP, formatSocketError } = require("./socket-path.cjs");
|
|
@@ -32,6 +32,7 @@ const { acquireBrowserLock } = require("./browser-lock.cjs");
|
|
|
32
32
|
const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endpoint.cjs");
|
|
33
33
|
const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
|
|
34
34
|
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
35
|
+
const { classifyTool } = require("./tool-scope.cjs");
|
|
35
36
|
const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
36
37
|
const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
|
|
37
38
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
@@ -49,6 +50,46 @@ function parseBrowserLockOptions(noLockFlag) {
|
|
|
49
50
|
return { noLock, timeoutMs };
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function flagValue(argv, flag) {
|
|
54
|
+
const index = argv.indexOf(flag);
|
|
55
|
+
if (index === -1) return undefined;
|
|
56
|
+
const value = argv[index + 1];
|
|
57
|
+
if (!value || value.startsWith("--")) {
|
|
58
|
+
console.error(`Error: ${flag} requires a value`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function positiveIdFlag(argv, flag) {
|
|
65
|
+
const value = flagValue(argv, flag);
|
|
66
|
+
if (value === undefined) return undefined;
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
69
|
+
console.error(`Error: ${flag} must be a positive number`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
76
|
+
const explicitSession = flagValue(argv, "--session");
|
|
77
|
+
const tabId = positiveIdFlag(argv, "--tab-id");
|
|
78
|
+
const windowId = allowWindow ? positiveIdFlag(argv, "--window-id") : undefined;
|
|
79
|
+
if (explicitSession && (tabId || windowId)) {
|
|
80
|
+
console.error("Error: use either --session or --tab-id/--window-id, not both");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const environmentSession = process.env.SURF_SESSION;
|
|
84
|
+
const session = explicitSession || (!tabId && !windowId ? environmentSession : undefined);
|
|
85
|
+
return {
|
|
86
|
+
...(session ? { session, sessionSource: explicitSession ? "explicit" : "environment" } : {}),
|
|
87
|
+
...(tabId ? { tabId } : {}),
|
|
88
|
+
...(windowId ? { windowId } : {}),
|
|
89
|
+
...(argv.includes("--no-wait") ? { admission: { wait: false } } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
function installBrowserLock({ noLock, timeoutMs }, endpoint) {
|
|
53
94
|
let releaseBrowserLock = () => {};
|
|
54
95
|
if (!noLock) {
|
|
@@ -78,28 +119,6 @@ function installBrowserLock({ noLock, timeoutMs }, endpoint) {
|
|
|
78
119
|
});
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
async function runWithBrowserLock(lockOptions, endpoint, operation) {
|
|
82
|
-
let releaseBrowserLock = () => {};
|
|
83
|
-
if (!lockOptions.noLock) {
|
|
84
|
-
const lock = acquireBrowserLock(endpoint.key, SURF_TMP, {
|
|
85
|
-
timeoutMs: lockOptions.timeoutMs,
|
|
86
|
-
});
|
|
87
|
-
releaseBrowserLock = lock.release;
|
|
88
|
-
}
|
|
89
|
-
const release = () => {
|
|
90
|
-
const releaseCurrent = releaseBrowserLock;
|
|
91
|
-
releaseBrowserLock = () => {};
|
|
92
|
-
releaseCurrent();
|
|
93
|
-
};
|
|
94
|
-
process.once("exit", release);
|
|
95
|
-
try {
|
|
96
|
-
return await operation();
|
|
97
|
-
} finally {
|
|
98
|
-
process.removeListener("exit", release);
|
|
99
|
-
release();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
122
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
104
123
|
function resizeImage(filePath, maxSize) {
|
|
105
124
|
const platform = process.platform;
|
|
@@ -181,14 +200,13 @@ try {
|
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
if (args[0] === "oracle") {
|
|
203
|
+
if (args[1] === "ask" || args[1] === "follow") {
|
|
204
|
+
console.error("[surf] Oracle requires exclusive browser access while dispatching; other sessions will queue.");
|
|
205
|
+
}
|
|
184
206
|
handleOracleCli(args, {
|
|
185
207
|
endpoint,
|
|
186
208
|
cwd: process.cwd(),
|
|
187
|
-
withBrowserLock: (operation) =>
|
|
188
|
-
parseBrowserLockOptions(args.includes("--no-lock")),
|
|
189
|
-
endpoint,
|
|
190
|
-
operation,
|
|
191
|
-
),
|
|
209
|
+
withBrowserLock: (operation) => operation(),
|
|
192
210
|
})
|
|
193
211
|
.then((result) => {
|
|
194
212
|
if (!result.handled) throw new Error("Oracle command was not handled");
|
|
@@ -203,10 +221,8 @@ if (args[0] === "oracle") {
|
|
|
203
221
|
}
|
|
204
222
|
|
|
205
223
|
if (["playbook", "pb", "use"].includes(args[0])) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
handlePlaybookCli(args, { endpoint, cwd: process.cwd() })
|
|
224
|
+
const targetOptions = resolveEarlyTargetOptions(args, { allowWindow: false });
|
|
225
|
+
handlePlaybookCli(args, { endpoint, cwd: process.cwd(), ...targetOptions })
|
|
210
226
|
.then((result) => {
|
|
211
227
|
if (!result.handled) throw new Error("Playbook command was not handled");
|
|
212
228
|
if (result.value !== undefined) console.log(formatPlaybookOutput(result.value, result.json));
|
|
@@ -262,6 +278,66 @@ const REMOVED_COMMANDS = {
|
|
|
262
278
|
};
|
|
263
279
|
|
|
264
280
|
const TOOLS = {
|
|
281
|
+
session: {
|
|
282
|
+
desc: "Durable tab-bound browser sessions",
|
|
283
|
+
commands: {
|
|
284
|
+
"session.new": {
|
|
285
|
+
desc: "Create a named session in a separate unfocused window by default",
|
|
286
|
+
args: ["name", "url"],
|
|
287
|
+
opts: {
|
|
288
|
+
window: "Create a separate window (default)",
|
|
289
|
+
tab: "Create an inactive tab instead of a window",
|
|
290
|
+
focused: "Focus the new target",
|
|
291
|
+
"window-id": "Window for --tab mode",
|
|
292
|
+
},
|
|
293
|
+
examples: [
|
|
294
|
+
{ cmd: 'session.new research "https://example.com"', desc: "Create a window session" },
|
|
295
|
+
{ cmd: 'session.new research about:blank --tab', desc: "Create an inactive tab session" },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
"session.ensure": {
|
|
299
|
+
desc: "Idempotently create, reuse, or reopen a named session",
|
|
300
|
+
args: ["name", "url"],
|
|
301
|
+
opts: {
|
|
302
|
+
window: "Use a separate window (default)",
|
|
303
|
+
tab: "Use an inactive tab",
|
|
304
|
+
focused: "Focus a newly created target",
|
|
305
|
+
"window-id": "Window for --tab mode",
|
|
306
|
+
},
|
|
307
|
+
examples: [
|
|
308
|
+
{ cmd: 'session.ensure research about:blank', desc: "Safe first command for an agent" },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
"session.list": {
|
|
312
|
+
desc: "List sessions, status, and queue state",
|
|
313
|
+
args: [],
|
|
314
|
+
opts: { refresh: "Validate every binding against Chrome" },
|
|
315
|
+
},
|
|
316
|
+
"session.info": {
|
|
317
|
+
desc: "Show one session, target, and scheduler queue state",
|
|
318
|
+
args: ["name"],
|
|
319
|
+
opts: { refresh: "Validate the binding against Chrome" },
|
|
320
|
+
},
|
|
321
|
+
"session.close": {
|
|
322
|
+
desc: "Remove a session and close Surf-created targets by default",
|
|
323
|
+
args: ["name"],
|
|
324
|
+
opts: {
|
|
325
|
+
"keep-target": "Unbind without closing the target",
|
|
326
|
+
"close-target": "Close an adopted target too",
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
"session.rebind": {
|
|
330
|
+
desc: "Bind a stale or gone session to an explicit existing tab",
|
|
331
|
+
args: ["name"],
|
|
332
|
+
opts: { "tab-id": "Existing tab ID", replace: "Replace a live binding" },
|
|
333
|
+
},
|
|
334
|
+
"session.reopen": {
|
|
335
|
+
desc: "Create a replacement target using the stored or supplied URL",
|
|
336
|
+
args: ["name", "url"],
|
|
337
|
+
opts: { replace: "Replace a live target", tab: "Reopen as an inactive tab", window: "Reopen as a window" },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
265
341
|
ai: {
|
|
266
342
|
desc: "AI assistants (ChatGPT, Gemini)",
|
|
267
343
|
commands: {
|
|
@@ -1485,6 +1561,7 @@ Exclude text content:
|
|
|
1485
1561
|
};
|
|
1486
1562
|
|
|
1487
1563
|
const ALL_SOCKET_TOOLS = [
|
|
1564
|
+
"session.new", "session.ensure", "session.list", "session.info", "session.close", "session.rebind", "session.reopen",
|
|
1488
1565
|
"ai", "screenshot", "record", "animate-audit", "perf-audit", "navigate",
|
|
1489
1566
|
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1490
1567
|
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
@@ -1553,6 +1630,8 @@ const SEE_ALSO = {
|
|
|
1553
1630
|
"scroll.to": ["click", "page.read"],
|
|
1554
1631
|
"console": ["network", "perf.metrics"],
|
|
1555
1632
|
"network": ["console", "network.get"],
|
|
1633
|
+
"session.ensure": ["session.info", "session.list"],
|
|
1634
|
+
"session.info": ["session.reopen", "session.rebind", "session.list"],
|
|
1556
1635
|
};
|
|
1557
1636
|
|
|
1558
1637
|
const showBasicHelp = () => {
|
|
@@ -1561,6 +1640,7 @@ const showBasicHelp = () => {
|
|
|
1561
1640
|
Usage: surf <command> [args] [options]
|
|
1562
1641
|
|
|
1563
1642
|
Common Commands:
|
|
1643
|
+
session.ensure <name> [url] Idempotently create or reuse a tab-bound session
|
|
1564
1644
|
navigate <url> Go to URL (alias: go)
|
|
1565
1645
|
click <ref> Click element by ref or selector
|
|
1566
1646
|
type <text> Type text at cursor or into element
|
|
@@ -1577,6 +1657,8 @@ Common Commands:
|
|
|
1577
1657
|
wait <seconds> Wait N seconds
|
|
1578
1658
|
|
|
1579
1659
|
Quick Examples:
|
|
1660
|
+
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
|
|
1661
|
+
surf session.ensure "$SURF_SESSION" about:blank
|
|
1580
1662
|
surf go "https://example.com"
|
|
1581
1663
|
surf read
|
|
1582
1664
|
surf click e5
|
|
@@ -1587,6 +1669,8 @@ Quick Examples:
|
|
|
1587
1669
|
surf window.new "https://example.com" && surf --window-id 123 go "https://other.com"
|
|
1588
1670
|
|
|
1589
1671
|
More Help:
|
|
1672
|
+
--session <name> Target a durable named browser session
|
|
1673
|
+
--no-wait Return tab_busy/browser_busy instead of queueing
|
|
1590
1674
|
--remote <host>:<port> Route requests to a remote native host
|
|
1591
1675
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1592
1676
|
surf remote authorize <label> --output <path>
|
|
@@ -1621,8 +1705,10 @@ Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf sc
|
|
|
1621
1705
|
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1622
1706
|
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
1623
1707
|
Cookies: surf cookie list | surf cookie get "name" | surf cookie delete "name"
|
|
1624
|
-
|
|
1625
|
-
|
|
1708
|
+
Session targeting: surf --session research read | SURF_SESSION=research surf read
|
|
1709
|
+
Session status/queue: surf session.info research | surf session.list --refresh
|
|
1710
|
+
Recovery: run the exact command printed after Recovery: on tab_gone, session_epoch_stale, tab_busy, or browser_busy
|
|
1711
|
+
Concurrency: commands for different session tabs can overlap; each tab remains FIFO; provider flows are browser-exclusive
|
|
1626
1712
|
Doctor: surf doctor --browser all # native host/socket diagnostics
|
|
1627
1713
|
Workflow: surf do 'go "https://example.com" | wait 2 | read | click e5 | screenshot'
|
|
1628
1714
|
More help: surf --help-full | surf <command> --help | surf --help-topic refs | surf --find <query>`);
|
|
@@ -1656,12 +1742,14 @@ Playbooks:
|
|
|
1656
1742
|
Options:
|
|
1657
1743
|
--remote <host>:<port> Route requests to a remote native host
|
|
1658
1744
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1745
|
+
--session <name> Target a durable named session (or set SURF_SESSION)
|
|
1659
1746
|
--tab-id <id> Target specific tab
|
|
1660
|
-
--window-id <id> Target specific window
|
|
1661
|
-
--
|
|
1747
|
+
--window-id <id> Target specific window
|
|
1748
|
+
--no-wait Return immediately when the tab/browser is busy
|
|
1749
|
+
--json Output raw JSON including target metadata
|
|
1662
1750
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1663
1751
|
--soft-fail On error: warn and exit 0 (for non-critical commands)
|
|
1664
|
-
--no-lock Bypass the
|
|
1752
|
+
--no-lock Bypass the legacy lock for compound client-side commands
|
|
1665
1753
|
|
|
1666
1754
|
Remote Credentials (run on the browser host):
|
|
1667
1755
|
surf remote authorize <label> --output <credential-file>
|
|
@@ -2042,8 +2130,7 @@ if (args.includes("--script")) {
|
|
|
2042
2130
|
const dryRun = args.includes("--dry-run");
|
|
2043
2131
|
const stopOnError = args.includes("--stop-on-error");
|
|
2044
2132
|
|
|
2045
|
-
const
|
|
2046
|
-
const scriptTabId = tabIdIdx !== -1 ? args[tabIdIdx + 1] : undefined;
|
|
2133
|
+
const scriptTarget = resolveEarlyTargetOptions(args);
|
|
2047
2134
|
|
|
2048
2135
|
if (!scriptPath || scriptPath.startsWith("--")) {
|
|
2049
2136
|
console.error("Error: --script requires a file path");
|
|
@@ -2077,7 +2164,13 @@ if (args.includes("--script")) {
|
|
|
2077
2164
|
params: { tool: toolName, args: toolArgs },
|
|
2078
2165
|
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
2079
2166
|
};
|
|
2080
|
-
if (
|
|
2167
|
+
if (scriptTarget.tabId) req.tabId = scriptTarget.tabId;
|
|
2168
|
+
if (scriptTarget.windowId) req.windowId = scriptTarget.windowId;
|
|
2169
|
+
if (scriptTarget.session) {
|
|
2170
|
+
req.session = scriptTarget.session;
|
|
2171
|
+
req.sessionSource = scriptTarget.sessionSource;
|
|
2172
|
+
}
|
|
2173
|
+
if (scriptTarget.admission) req.admission = scriptTarget.admission;
|
|
2081
2174
|
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
2082
2175
|
req.params.args = prepared.args;
|
|
2083
2176
|
return scriptTransport.request(req, resolveRequestDeadlineMs(toolName, prepared.args), prepared);
|
|
@@ -2150,10 +2243,6 @@ if (args.includes("--script")) {
|
|
|
2150
2243
|
}
|
|
2151
2244
|
};
|
|
2152
2245
|
|
|
2153
|
-
if (!dryRun) {
|
|
2154
|
-
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
2246
|
runScript()
|
|
2158
2247
|
.then((code) => process.exit(code))
|
|
2159
2248
|
.catch((error) => {
|
|
@@ -2176,9 +2265,11 @@ if (args[0] === "do") {
|
|
|
2176
2265
|
let wantJson = false;
|
|
2177
2266
|
let tabId = undefined;
|
|
2178
2267
|
let windowId = undefined;
|
|
2268
|
+
let explicitSession = undefined;
|
|
2269
|
+
let noWait = false;
|
|
2179
2270
|
|
|
2180
2271
|
// Reserved flags that aren't workflow args
|
|
2181
|
-
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'no-lock'];
|
|
2272
|
+
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'session', 'no-lock', 'no-wait'];
|
|
2182
2273
|
|
|
2183
2274
|
// Workflow-specific args (collected for variable substitution)
|
|
2184
2275
|
const workflowArgs = {};
|
|
@@ -2208,6 +2299,16 @@ if (args[0] === "do") {
|
|
|
2208
2299
|
} else if (arg === "--window-id") {
|
|
2209
2300
|
windowId = parseInt(doArgs[i + 1], 10);
|
|
2210
2301
|
i++;
|
|
2302
|
+
} else if (arg === "--session") {
|
|
2303
|
+
const value = doArgs[i + 1];
|
|
2304
|
+
if (!value || value.startsWith("--")) {
|
|
2305
|
+
console.error("Error: --session requires a value");
|
|
2306
|
+
process.exit(1);
|
|
2307
|
+
}
|
|
2308
|
+
explicitSession = value;
|
|
2309
|
+
i++;
|
|
2310
|
+
} else if (arg === "--no-wait") {
|
|
2311
|
+
noWait = true;
|
|
2211
2312
|
} else if (arg.startsWith("--")) {
|
|
2212
2313
|
// Workflow-specific arg (e.g., --email, --password)
|
|
2213
2314
|
const key = arg.slice(2);
|
|
@@ -2231,6 +2332,22 @@ if (args[0] === "do") {
|
|
|
2231
2332
|
}
|
|
2232
2333
|
}
|
|
2233
2334
|
|
|
2335
|
+
if (tabId !== undefined && (!Number.isInteger(tabId) || tabId <= 0)) {
|
|
2336
|
+
console.error("Error: --tab-id must be a positive number");
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
2339
|
+
if (windowId !== undefined && (!Number.isInteger(windowId) || windowId <= 0)) {
|
|
2340
|
+
console.error("Error: --window-id must be a positive number");
|
|
2341
|
+
process.exit(1);
|
|
2342
|
+
}
|
|
2343
|
+
if (explicitSession && (tabId || windowId)) {
|
|
2344
|
+
console.error("Error: use either --session or --tab-id/--window-id, not both");
|
|
2345
|
+
process.exit(1);
|
|
2346
|
+
}
|
|
2347
|
+
const environmentSession = process.env.SURF_SESSION;
|
|
2348
|
+
const session = explicitSession || (!tabId && !windowId ? environmentSession : undefined);
|
|
2349
|
+
const sessionSource = explicitSession ? "explicit" : session ? "environment" : undefined;
|
|
2350
|
+
|
|
2234
2351
|
if (!commandsInput && !fileInput) {
|
|
2235
2352
|
console.error("Error: commands string, workflow name, or --file required");
|
|
2236
2353
|
console.error('Usage: surf do \'go "url" | click e5\'');
|
|
@@ -2336,8 +2453,6 @@ if (args[0] === "do") {
|
|
|
2336
2453
|
process.exit(0);
|
|
2337
2454
|
}
|
|
2338
2455
|
|
|
2339
|
-
installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")), endpoint);
|
|
2340
|
-
|
|
2341
2456
|
if (!wantJson) {
|
|
2342
2457
|
if (workflowName) {
|
|
2343
2458
|
console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
|
|
@@ -2359,6 +2474,9 @@ if (args[0] === "do") {
|
|
|
2359
2474
|
context: {
|
|
2360
2475
|
tabId,
|
|
2361
2476
|
windowId,
|
|
2477
|
+
session,
|
|
2478
|
+
sessionSource,
|
|
2479
|
+
admission: noWait ? { wait: false } : undefined,
|
|
2362
2480
|
endpoint,
|
|
2363
2481
|
transport,
|
|
2364
2482
|
},
|
|
@@ -2517,7 +2635,7 @@ if (args[0] === "workflow.validate") {
|
|
|
2517
2635
|
}
|
|
2518
2636
|
}
|
|
2519
2637
|
|
|
2520
|
-
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"];
|
|
2638
|
+
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", "no-wait", "window", "tab", "focused", "unfocused", "keep-target", "close-target", "replace", "refresh"];
|
|
2521
2639
|
|
|
2522
2640
|
const parseArgs = (rawArgs) => {
|
|
2523
2641
|
const result = { positional: [], options: {} };
|
|
@@ -2563,6 +2681,24 @@ let { positional, options } = parseArgs(args);
|
|
|
2563
2681
|
let tool = positional[0];
|
|
2564
2682
|
let firstArg = positional[1];
|
|
2565
2683
|
|
|
2684
|
+
if (tool === "session" && firstArg) {
|
|
2685
|
+
const sessionSubcommands = {
|
|
2686
|
+
new: "session.new",
|
|
2687
|
+
ensure: "session.ensure",
|
|
2688
|
+
list: "session.list",
|
|
2689
|
+
info: "session.info",
|
|
2690
|
+
close: "session.close",
|
|
2691
|
+
rebind: "session.rebind",
|
|
2692
|
+
reopen: "session.reopen",
|
|
2693
|
+
};
|
|
2694
|
+
const sessionTool = sessionSubcommands[firstArg];
|
|
2695
|
+
if (sessionTool) {
|
|
2696
|
+
tool = sessionTool;
|
|
2697
|
+
positional = [tool, ...positional.slice(2)];
|
|
2698
|
+
firstArg = positional[1];
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2566
2702
|
if (tool === "cookie" && firstArg) {
|
|
2567
2703
|
const cookieSubcommands = {
|
|
2568
2704
|
list: "cookie.list",
|
|
@@ -2678,6 +2814,12 @@ const PRIMARY_ARG_MAP = {
|
|
|
2678
2814
|
"window.new": "url",
|
|
2679
2815
|
"window.focus": "id",
|
|
2680
2816
|
"window.close": "id",
|
|
2817
|
+
"session.new": "name",
|
|
2818
|
+
"session.ensure": "name",
|
|
2819
|
+
"session.info": "name",
|
|
2820
|
+
"session.close": "name",
|
|
2821
|
+
"session.rebind": "name",
|
|
2822
|
+
"session.reopen": "name",
|
|
2681
2823
|
"locate.role": "role",
|
|
2682
2824
|
"locate.text": "text",
|
|
2683
2825
|
"locate.label": "label",
|
|
@@ -2748,6 +2890,10 @@ if (firstArg !== undefined) {
|
|
|
2748
2890
|
}
|
|
2749
2891
|
}
|
|
2750
2892
|
|
|
2893
|
+
if (["session.new", "session.ensure", "session.reopen"].includes(tool) && positional[2] !== undefined && toolArgs.url === undefined) {
|
|
2894
|
+
toolArgs.url = positional[2];
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2751
2897
|
if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
2752
2898
|
try {
|
|
2753
2899
|
toolArgs.code = fs.readFileSync(toolArgs.file, "utf8");
|
|
@@ -2767,6 +2913,13 @@ if (tool === "batch" && toolArgs.file) {
|
|
|
2767
2913
|
console.error(`Error: Failed to read batch file: ${e.message}`);
|
|
2768
2914
|
process.exit(1);
|
|
2769
2915
|
}
|
|
2916
|
+
} else if (tool === "batch" && typeof toolArgs.actions === "string") {
|
|
2917
|
+
try {
|
|
2918
|
+
toolArgs.actions = JSON.parse(toolArgs.actions);
|
|
2919
|
+
} catch (e) {
|
|
2920
|
+
console.error(`Error: Failed to parse batch actions: ${e.message}`);
|
|
2921
|
+
process.exit(1);
|
|
2922
|
+
}
|
|
2770
2923
|
}
|
|
2771
2924
|
|
|
2772
2925
|
// Handle select command: capture multiple values after selector
|
|
@@ -2786,22 +2939,30 @@ if (toolArgs.into && !toolArgs.selector) {
|
|
|
2786
2939
|
}
|
|
2787
2940
|
|
|
2788
2941
|
const globalOpts = {};
|
|
2942
|
+
const explicitSession = toolArgs.session;
|
|
2943
|
+
delete toolArgs.session;
|
|
2944
|
+
const environmentSession = process.env.SURF_SESSION;
|
|
2945
|
+
const noWait = toolArgs["no-wait"] === true;
|
|
2946
|
+
delete toolArgs["no-wait"];
|
|
2947
|
+
|
|
2789
2948
|
if (toolArgs["tab-id"] !== undefined) {
|
|
2790
2949
|
const tid = parseInt(toolArgs["tab-id"], 10);
|
|
2791
|
-
if (
|
|
2792
|
-
console.error("Error: --tab-id must be a number");
|
|
2950
|
+
if (!Number.isInteger(tid) || tid <= 0) {
|
|
2951
|
+
console.error("Error: --tab-id must be a positive number");
|
|
2793
2952
|
process.exit(1);
|
|
2794
2953
|
}
|
|
2795
|
-
|
|
2954
|
+
if (tool === "session.rebind") toolArgs.tabId = tid;
|
|
2955
|
+
else globalOpts.tabId = tid;
|
|
2796
2956
|
delete toolArgs["tab-id"];
|
|
2797
2957
|
}
|
|
2798
2958
|
if (toolArgs["window-id"] !== undefined) {
|
|
2799
2959
|
const wid = parseInt(toolArgs["window-id"], 10);
|
|
2800
|
-
if (
|
|
2801
|
-
console.error("Error: --window-id must be a number");
|
|
2960
|
+
if (!Number.isInteger(wid) || wid <= 0) {
|
|
2961
|
+
console.error("Error: --window-id must be a positive number");
|
|
2802
2962
|
process.exit(1);
|
|
2803
2963
|
}
|
|
2804
|
-
|
|
2964
|
+
if (["session.new", "session.ensure", "session.reopen"].includes(tool)) toolArgs.windowId = wid;
|
|
2965
|
+
else globalOpts.windowId = wid;
|
|
2805
2966
|
delete toolArgs["window-id"];
|
|
2806
2967
|
}
|
|
2807
2968
|
if (toolArgs["network-path"] !== undefined && typeof toolArgs["network-path"] !== "string") {
|
|
@@ -2912,6 +3073,39 @@ if (methodFlag === "js") {
|
|
|
2912
3073
|
}
|
|
2913
3074
|
}
|
|
2914
3075
|
|
|
3076
|
+
const finalClassification = classifyTool(finalTool, toolArgs);
|
|
3077
|
+
if (explicitSession !== undefined) {
|
|
3078
|
+
if (typeof explicitSession !== "string" || !explicitSession) {
|
|
3079
|
+
console.error("Error: --session requires a session name");
|
|
3080
|
+
process.exit(1);
|
|
3081
|
+
}
|
|
3082
|
+
if (finalClassification.targetUse !== "default-tab" || finalTool.startsWith("session.")) {
|
|
3083
|
+
console.error(`Error: --session does not apply to ${finalTool}`);
|
|
3084
|
+
process.exit(1);
|
|
3085
|
+
}
|
|
3086
|
+
if (globalOpts.tabId || globalOpts.windowId) {
|
|
3087
|
+
console.error("Error: use either --session or --tab-id/--window-id, not both");
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
globalOpts.session = explicitSession;
|
|
3091
|
+
globalOpts.sessionSource = "explicit";
|
|
3092
|
+
} else if (
|
|
3093
|
+
environmentSession &&
|
|
3094
|
+
finalClassification.targetUse === "default-tab" &&
|
|
3095
|
+
!globalOpts.tabId &&
|
|
3096
|
+
!globalOpts.windowId &&
|
|
3097
|
+
!finalTool.startsWith("session.")
|
|
3098
|
+
) {
|
|
3099
|
+
globalOpts.session = environmentSession;
|
|
3100
|
+
globalOpts.sessionSource = "environment";
|
|
3101
|
+
}
|
|
3102
|
+
if (noWait) globalOpts.admission = { wait: false };
|
|
3103
|
+
|
|
3104
|
+
if (finalClassification.scope === "provider") {
|
|
3105
|
+
const suffix = globalOpts.session ? ` Session ${globalOpts.session} remains selected for page context.` : "";
|
|
3106
|
+
console.error(`[surf] ${finalTool} requires exclusive browser access; other sessions will queue until it finishes.${suffix}`);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
2915
3109
|
if (streamMode && (tool === "console" || tool === "network")) {
|
|
2916
3110
|
const streamType = tool === "console" ? "STREAM_CONSOLE" : "STREAM_NETWORK";
|
|
2917
3111
|
const streamOpts = {
|
|
@@ -2967,7 +3161,8 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2967
3161
|
}
|
|
2968
3162
|
}
|
|
2969
3163
|
if (msg.error) {
|
|
2970
|
-
|
|
3164
|
+
const text = msg.error?.content?.[0]?.text || msg.error?.message || String(msg.error);
|
|
3165
|
+
console.error("Error:", text);
|
|
2971
3166
|
sock.end();
|
|
2972
3167
|
process.exit(1);
|
|
2973
3168
|
}
|
|
@@ -2976,7 +3171,11 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2976
3171
|
sock.end();
|
|
2977
3172
|
process.exit(1);
|
|
2978
3173
|
}
|
|
2979
|
-
if (msg.type === "stream_started")
|
|
3174
|
+
if (msg.type === "stream_started") {
|
|
3175
|
+
const context = formatTargetContext(msg.target);
|
|
3176
|
+
if (context) console.error(context);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
2980
3179
|
if (msg.type === "console_event") {
|
|
2981
3180
|
const { level, text, timestamp } = msg;
|
|
2982
3181
|
if (streamLevel && level !== streamLevel) return;
|
|
@@ -3227,7 +3426,6 @@ if (finalTool === "record") {
|
|
|
3227
3426
|
return;
|
|
3228
3427
|
}
|
|
3229
3428
|
|
|
3230
|
-
installBrowserLock(lockOptions, endpoint);
|
|
3231
3429
|
let socket;
|
|
3232
3430
|
let timeout;
|
|
3233
3431
|
|
|
@@ -3254,7 +3452,7 @@ socket = connectEndpoint(endpoint, () => {
|
|
|
3254
3452
|
writeFrame(socket, request).catch((error) => socket.destroy(error));
|
|
3255
3453
|
});
|
|
3256
3454
|
|
|
3257
|
-
const requestTimeout = resolveRequestDeadlineMs(
|
|
3455
|
+
const requestTimeout = resolveRequestDeadlineMs(finalTool, toolArgs);
|
|
3258
3456
|
timeout = setTimeout(() => {
|
|
3259
3457
|
console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
|
|
3260
3458
|
socket.destroy();
|
|
@@ -3295,8 +3493,45 @@ socket.on("close", () => {
|
|
|
3295
3493
|
clearTimeout(timeout);
|
|
3296
3494
|
});
|
|
3297
3495
|
|
|
3496
|
+
function formatTargetContext(target) {
|
|
3497
|
+
if (!target) return null;
|
|
3498
|
+
const fields = [];
|
|
3499
|
+
if (target.session) fields.push(`session=${target.session}`);
|
|
3500
|
+
if (target.tabId !== undefined) fields.push(`tab=${target.tabId}`);
|
|
3501
|
+
if (target.windowId !== undefined) fields.push(`window=${target.windowId}`);
|
|
3502
|
+
if (target.queuedMs > 0) fields.push(`queued=${target.queuedMs}ms`);
|
|
3503
|
+
return fields.length > 0 ? `[surf ${fields.join(" ")}]` : null;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function printResponseContext(response) {
|
|
3507
|
+
const context = formatTargetContext(response.target);
|
|
3508
|
+
if (context) console.error(context);
|
|
3509
|
+
if (response.notice && finalClassification.scope !== "provider") {
|
|
3510
|
+
console.error(`[surf] ${response.notice}`);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function queueSummary(queue) {
|
|
3515
|
+
if (!queue) return "unknown";
|
|
3516
|
+
const pieces = [
|
|
3517
|
+
`own-tab=${queue.active ? "active" : "idle"}`,
|
|
3518
|
+
`own-queued=${queue.queued || 0}`,
|
|
3519
|
+
];
|
|
3520
|
+
if (queue.blockedBy) pieces.push(`blocked-by=${queue.blockedBy}`);
|
|
3521
|
+
if (queue.browserWriter) {
|
|
3522
|
+
pieces.push(`writer=${queue.browserWriter.scope}${queue.browserWriter.session ? `:${queue.browserWriter.session}` : ""}`);
|
|
3523
|
+
} else if (queue.queuedBrowserWriters) {
|
|
3524
|
+
pieces.push(`writers-waiting=${queue.queuedBrowserWriters}`);
|
|
3525
|
+
}
|
|
3526
|
+
if (Array.isArray(queue.otherActiveTabLanes) && queue.otherActiveTabLanes.length > 0) {
|
|
3527
|
+
pieces.push(`other-tabs-active=${queue.otherActiveTabLanes.length}`);
|
|
3528
|
+
}
|
|
3529
|
+
return pieces.join(" ");
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3298
3532
|
async function handleResponse(response) {
|
|
3299
3533
|
clearTimeout(timeout);
|
|
3534
|
+
printResponseContext(response);
|
|
3300
3535
|
|
|
3301
3536
|
if (response.error) {
|
|
3302
3537
|
const errContent = response.error.content?.[0]?.text || JSON.stringify(response.error);
|
|
@@ -3355,12 +3590,48 @@ async function handleResponse(response) {
|
|
|
3355
3590
|
}
|
|
3356
3591
|
|
|
3357
3592
|
if (wantJson) {
|
|
3358
|
-
|
|
3593
|
+
const output = response.target || response.notice
|
|
3594
|
+
? { result: data ?? null, target: response.target || null, notice: response.notice || null }
|
|
3595
|
+
: data ?? null;
|
|
3596
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3359
3597
|
socket.end();
|
|
3360
3598
|
process.exit(0);
|
|
3361
3599
|
}
|
|
3362
3600
|
|
|
3363
|
-
if (
|
|
3601
|
+
if (finalTool === "session.list") {
|
|
3602
|
+
const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
3603
|
+
if (sessions.length === 0) {
|
|
3604
|
+
console.log("No browser sessions. Create one with: surf session.ensure <name> about:blank");
|
|
3605
|
+
} else {
|
|
3606
|
+
for (const entry of sessions) {
|
|
3607
|
+
console.log([
|
|
3608
|
+
entry.name,
|
|
3609
|
+
entry.status,
|
|
3610
|
+
`tab=${entry.tabId ?? "-"}`,
|
|
3611
|
+
`window=${entry.windowId ?? "-"}`,
|
|
3612
|
+
`mode=${entry.mode || "-"}`,
|
|
3613
|
+
queueSummary(entry.queue),
|
|
3614
|
+
entry.lastUrl || "",
|
|
3615
|
+
].join("\t"));
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
} else if (finalTool === "session.info" && data?.session) {
|
|
3619
|
+
const entry = data.session;
|
|
3620
|
+
console.log(`Session: ${entry.name}`);
|
|
3621
|
+
console.log(`Status: ${entry.status}`);
|
|
3622
|
+
console.log(`Target: tab=${entry.tabId ?? "-"} window=${entry.windowId ?? "-"} mode=${entry.mode || "-"}`);
|
|
3623
|
+
console.log(`Ownership: ${entry.ownership || "unknown"}`);
|
|
3624
|
+
console.log(`URL: ${entry.currentUrl || entry.lastUrl || "(unknown)"}`);
|
|
3625
|
+
console.log(`Queue: ${queueSummary(entry.queue)}`);
|
|
3626
|
+
if (data.sharedProfile) console.log(`Profile: ${data.sharedProfile}`);
|
|
3627
|
+
} else if (["session.new", "session.ensure", "session.rebind", "session.reopen"].includes(finalTool) && data?.session) {
|
|
3628
|
+
const entry = data.session;
|
|
3629
|
+
const action = data.created ? "created" : data.reopened ? "reopened" : data.rebound ? "rebound" : "ready";
|
|
3630
|
+
console.log(`Session ${entry.name} ${action}: tab=${entry.tabId} window=${entry.windowId} mode=${entry.mode} status=${entry.status}`);
|
|
3631
|
+
console.log(`Use: export SURF_SESSION=${entry.name}`);
|
|
3632
|
+
} else if (finalTool === "session.close" && data?.success) {
|
|
3633
|
+
console.log(`Session ${data.name} closed (${data.targetClosed ? "target closed" : "target kept"})`);
|
|
3634
|
+
} else if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3364
3635
|
const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
|
|
3365
3636
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
3366
3637
|
|
package/native/do-executor.cjs
CHANGED
|
@@ -13,6 +13,11 @@ function sendDoRequest(toolName, toolArgs, context = {}) {
|
|
|
13
13
|
};
|
|
14
14
|
if (context.tabId) request.tabId = context.tabId;
|
|
15
15
|
if (context.windowId) request.windowId = context.windowId;
|
|
16
|
+
if (context.session) {
|
|
17
|
+
request.session = context.session;
|
|
18
|
+
request.sessionSource = context.sessionSource || "environment";
|
|
19
|
+
}
|
|
20
|
+
if (context.admission) request.admission = context.admission;
|
|
16
21
|
const timeoutMs = context.timeoutMs || resolveRequestDeadlineMs(toolName, toolArgs);
|
|
17
22
|
const endpoint = context.endpoint || selectEndpoint([]).endpoint;
|
|
18
23
|
const prepared = endpoint.kind === "remote"
|