surf-cli 2.17.0 → 2.19.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 +135 -9
- 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 +443 -13
- package/native/doctor.cjs +11 -2
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/file-transfer.cjs +5 -1
- package/native/host-helpers.cjs +40 -3
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +451 -26
- package/native/mcp-server.cjs +26 -1
- package/native/oracle-cli.cjs +2 -2
- package/native/script-options.cjs +33 -0
- package/native/socket-permissions.cjs +114 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +8 -4
- package/native/video-recorder.cjs +444 -0
- package/native/workflow-definition.cjs +5 -0
- package/package.json +6 -6
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +25 -3
package/native/cli.cjs
CHANGED
|
@@ -16,7 +16,9 @@ const {
|
|
|
16
16
|
validateWorkflowArgs,
|
|
17
17
|
validateWorkflowFile,
|
|
18
18
|
} = require("./workflow-definition.cjs");
|
|
19
|
-
const { executeDoSteps } = require("./do-executor.cjs");
|
|
19
|
+
const { executeDoSteps, sendDoRequest } = require("./do-executor.cjs");
|
|
20
|
+
const { runExtraction, renderExtractionMarkdown } = require("./extract.cjs");
|
|
21
|
+
const { applyOptionsPrelude, parseScriptOptions } = require("./script-options.cjs");
|
|
20
22
|
const { openClientTransport } = require("./client-transport.cjs");
|
|
21
23
|
const { version: VERSION } = require("../package.json");
|
|
22
24
|
const {
|
|
@@ -33,6 +35,7 @@ const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endp
|
|
|
33
35
|
const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
|
|
34
36
|
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
35
37
|
const { classifyTool } = require("./tool-scope.cjs");
|
|
38
|
+
const { parseVideoFps, validateVideoOutputPath } = require("./video-recorder.cjs");
|
|
36
39
|
const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
37
40
|
const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
|
|
38
41
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
@@ -72,7 +75,7 @@ function positiveIdFlag(argv, flag) {
|
|
|
72
75
|
return parsed;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
78
|
+
function resolveEarlyTargetOptions(argv, { allowWindow = true, allowEnvironmentSession = true } = {}) {
|
|
76
79
|
const explicitSession = flagValue(argv, "--session");
|
|
77
80
|
const tabId = positiveIdFlag(argv, "--tab-id");
|
|
78
81
|
const windowId = allowWindow ? positiveIdFlag(argv, "--window-id") : undefined;
|
|
@@ -81,7 +84,7 @@ function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
|
81
84
|
process.exit(1);
|
|
82
85
|
}
|
|
83
86
|
const environmentSession = process.env.SURF_SESSION;
|
|
84
|
-
const session = explicitSession || (!tabId && !windowId ? environmentSession : undefined);
|
|
87
|
+
const session = explicitSession || (allowEnvironmentSession && !tabId && !windowId ? environmentSession : undefined);
|
|
85
88
|
return {
|
|
86
89
|
...(session ? { session, sessionSource: explicitSession ? "explicit" : "environment" } : {}),
|
|
87
90
|
...(tabId ? { tabId } : {}),
|
|
@@ -313,6 +316,14 @@ const TOOLS = {
|
|
|
313
316
|
args: [],
|
|
314
317
|
opts: { refresh: "Validate every binding against Chrome" },
|
|
315
318
|
},
|
|
319
|
+
"session.cleanup": {
|
|
320
|
+
desc: "Remove idle session bindings and close only Surf-created targets",
|
|
321
|
+
args: [],
|
|
322
|
+
opts: {
|
|
323
|
+
"idle-after": "Required threshold such as 30s, 5m, 1h, or 1d",
|
|
324
|
+
"dry-run": "Report matches without removing bindings or closing targets",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
316
327
|
"session.info": {
|
|
317
328
|
desc: "Show one session, target, and scheduler queue state",
|
|
318
329
|
args: ["name"],
|
|
@@ -346,7 +357,7 @@ const TOOLS = {
|
|
|
346
357
|
args: ["query"],
|
|
347
358
|
opts: {
|
|
348
359
|
"with-page": "Include current page context",
|
|
349
|
-
model: "Model:
|
|
360
|
+
model: "Model: gpt-6-astra, latest, gpt-5.6-sol, gpt-5.5",
|
|
350
361
|
file: "Attach file",
|
|
351
362
|
timeout: "Timeout in seconds (default: 2700 = 45min)"
|
|
352
363
|
},
|
|
@@ -619,6 +630,37 @@ const TOOLS = {
|
|
|
619
630
|
"snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
|
|
620
631
|
}
|
|
621
632
|
},
|
|
633
|
+
video: {
|
|
634
|
+
desc: "Local WebM recording",
|
|
635
|
+
commands: {
|
|
636
|
+
"video.start": {
|
|
637
|
+
desc: "Start local WebM recording",
|
|
638
|
+
args: ["output"],
|
|
639
|
+
opts: {
|
|
640
|
+
fps: "Frames per second (default: 30, max: 60)",
|
|
641
|
+
},
|
|
642
|
+
examples: [{ cmd: "video start ./demo.webm --fps 30", desc: "Start recording" }],
|
|
643
|
+
},
|
|
644
|
+
"video.stop": {
|
|
645
|
+
desc: "Stop local WebM recording",
|
|
646
|
+
args: [],
|
|
647
|
+
examples: [{ cmd: "video stop", desc: "Stop recording" }],
|
|
648
|
+
},
|
|
649
|
+
"video.status": {
|
|
650
|
+
desc: "Show local WebM recording status",
|
|
651
|
+
args: [],
|
|
652
|
+
examples: [{ cmd: "video status", desc: "Show status" }],
|
|
653
|
+
},
|
|
654
|
+
"video.restart": {
|
|
655
|
+
desc: "Restart local WebM recording",
|
|
656
|
+
args: ["output"],
|
|
657
|
+
opts: {
|
|
658
|
+
fps: "Frames per second (default: 30, max: 60)",
|
|
659
|
+
},
|
|
660
|
+
examples: [{ cmd: "video restart ./take2.webm --fps 60", desc: "Restart recording" }],
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
622
664
|
scroll: {
|
|
623
665
|
desc: "Scrolling",
|
|
624
666
|
commands: {
|
|
@@ -681,6 +723,17 @@ const TOOLS = {
|
|
|
681
723
|
examples: [{ cmd: "page.save --output page.html", desc: "Save current document HTML" }],
|
|
682
724
|
},
|
|
683
725
|
"page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
|
|
726
|
+
"page.readiness": {
|
|
727
|
+
desc: "Classify the page once: ready, empty, loading, login, challenge, not-found, error",
|
|
728
|
+
args: [],
|
|
729
|
+
opts: {
|
|
730
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
731
|
+
text: "Page text that marks a ready page",
|
|
732
|
+
"url-prefix": "Expected URL prefix",
|
|
733
|
+
"empty-text": "Text of an explicit no-results render",
|
|
734
|
+
},
|
|
735
|
+
examples: [{ cmd: "page.readiness --json", desc: "State plus evidence as JSON" }]
|
|
736
|
+
},
|
|
684
737
|
}
|
|
685
738
|
},
|
|
686
739
|
locate: {
|
|
@@ -784,6 +837,24 @@ const TOOLS = {
|
|
|
784
837
|
},
|
|
785
838
|
"wait.dom": { desc: "Wait for DOM to stabilize", args: [], opts: { stable: "Stability window in ms (default: 100)", timeout: "Max wait time in ms" } },
|
|
786
839
|
"wait.load": { desc: "Wait for page to fully load", args: [], opts: { timeout: "Max wait time in ms (default: 30000)" } },
|
|
840
|
+
"wait.ready": {
|
|
841
|
+
desc: "Wait until the page is ready, or fail fast with a typed state (challenge, login, not-found, error)",
|
|
842
|
+
args: [],
|
|
843
|
+
opts: {
|
|
844
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
845
|
+
text: "Page text that marks a ready page",
|
|
846
|
+
"url-prefix": "Expected URL prefix; anything else is a bounce",
|
|
847
|
+
"empty-text": "Text of an explicit no-results render (reports state 'empty')",
|
|
848
|
+
accept: "Negative states to return instead of fail (comma list)",
|
|
849
|
+
timeout: "Max wait time in ms (default: 20000, max: 120000)",
|
|
850
|
+
interval: "Poll interval in ms (default: 400)",
|
|
851
|
+
},
|
|
852
|
+
examples: [
|
|
853
|
+
{ cmd: 'wait.ready --selector ".results"', desc: "Wait for content; fail fast on a login bounce" },
|
|
854
|
+
{ cmd: 'wait.ready --url-prefix "https://app.example.com/" --empty-text "No results"', desc: "Distinguish empty from blocked" },
|
|
855
|
+
{ cmd: "wait.ready --accept login --json", desc: "Return the login state to the caller" },
|
|
856
|
+
]
|
|
857
|
+
},
|
|
787
858
|
}
|
|
788
859
|
},
|
|
789
860
|
input: {
|
|
@@ -838,17 +909,52 @@ const TOOLS = {
|
|
|
838
909
|
"drag": { desc: "Drag between points", args: [], opts: { from: "Start x,y", to: "End x,y" } },
|
|
839
910
|
}
|
|
840
911
|
},
|
|
912
|
+
extract: {
|
|
913
|
+
desc: "Scripted extraction in an owned tab",
|
|
914
|
+
commands: {
|
|
915
|
+
"extract": {
|
|
916
|
+
desc: "Open a URL in a fresh tab, wait until it is ready, run a page-side script that returns JSON, print rows",
|
|
917
|
+
args: ["url"],
|
|
918
|
+
opts: {
|
|
919
|
+
file: "Script file; must `return` JSON (an array, or an object with a rows/items/results array)",
|
|
920
|
+
code: "Inline script instead of --file",
|
|
921
|
+
options: "JSON object exposed to the script as SURF_OPTIONS",
|
|
922
|
+
"options-file": "Read the options object from a JSON file",
|
|
923
|
+
"ready-selector": "wait.ready --selector before extracting",
|
|
924
|
+
"ready-text": "wait.ready --text before extracting",
|
|
925
|
+
"ready-url-prefix": "wait.ready --url-prefix; a different URL is a bounce",
|
|
926
|
+
"empty-text": "wait.ready --empty-text; lets an explicit no-results page pass the zero-rows check",
|
|
927
|
+
"ready-timeout": "Readiness timeout in ms (default: 20000)",
|
|
928
|
+
"ready-interval": "Readiness polling interval in ms (default: 400)",
|
|
929
|
+
rows: "Key of the row array in the script result (default: auto)",
|
|
930
|
+
retry: "Fresh-tab retries on transient failures (default: 1, max: 5)",
|
|
931
|
+
"retry-delay-ms": "Delay between attempts (default: 500)",
|
|
932
|
+
"allow-empty": "Accept zero rows",
|
|
933
|
+
"keep-tab": "Leave the owned tab open on success and report its id",
|
|
934
|
+
"tab-id": "Extract from an existing tab instead (no fresh tab, no retry; navigates only if a URL is given)",
|
|
935
|
+
session: "Extract from a session's tab instead (same rules as --tab-id)",
|
|
936
|
+
json: "Print {data, rows, rowCount, attempts, readiness} as JSON",
|
|
937
|
+
},
|
|
938
|
+
examples: [
|
|
939
|
+
{ cmd: 'extract "https://example.com/list" --file rows.js --ready-selector ".item"', desc: "Fresh tab, wait for items, print a Markdown table" },
|
|
940
|
+
{ cmd: 'extract "https://example.com/search?q=x" --file rows.js --options \'{"limit": 20}\' --empty-text "No results" --json', desc: "Options prelude, explicit empty state, JSON output" },
|
|
941
|
+
{ cmd: "extract --tab-id 42 --code 'return [...document.querySelectorAll(\"h2\")].map(h => ({ title: h.textContent }))'", desc: "Read an existing tab in place" },
|
|
942
|
+
]
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
},
|
|
841
946
|
js: {
|
|
842
947
|
desc: "JavaScript execution",
|
|
843
948
|
commands: {
|
|
844
949
|
"js": {
|
|
845
950
|
desc: "Execute JavaScript (use 'return' for values)",
|
|
846
951
|
args: ["code"],
|
|
847
|
-
opts: { file: "Run JS from file" },
|
|
952
|
+
opts: { file: "Run JS from file", options: "JSON object exposed to the script as a frozen SURF_OPTIONS constant" },
|
|
848
953
|
examples: [
|
|
849
954
|
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
850
955
|
{ cmd: 'js "document.body.style.background = \'red\'"', desc: "Run code" },
|
|
851
956
|
{ cmd: "js --file script.js", desc: "Run file" },
|
|
957
|
+
{ cmd: 'js --file script.js --options \'{"limit": 20}\'', desc: "Run file with SURF_OPTIONS.limit" },
|
|
852
958
|
]
|
|
853
959
|
},
|
|
854
960
|
}
|
|
@@ -1116,7 +1222,7 @@ const TOOLS = {
|
|
|
1116
1222
|
"frame.js": {
|
|
1117
1223
|
desc: "Execute JS in specific frame",
|
|
1118
1224
|
args: ["code"],
|
|
1119
|
-
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
1225
|
+
opts: { id: "Frame ID from frame.list", file: "Run JS from file", options: "JSON object exposed as SURF_OPTIONS" },
|
|
1120
1226
|
examples: [
|
|
1121
1227
|
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
1122
1228
|
]
|
|
@@ -1561,8 +1667,8 @@ Exclude text content:
|
|
|
1561
1667
|
};
|
|
1562
1668
|
|
|
1563
1669
|
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",
|
|
1670
|
+
"session.new", "session.ensure", "session.list", "session.cleanup", "session.info", "session.close", "session.rebind", "session.reopen",
|
|
1671
|
+
"ai", "screenshot", "record", "video.start", "video.stop", "video.status", "video.restart", "animate-audit", "perf-audit", "navigate",
|
|
1566
1672
|
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1567
1673
|
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
1568
1674
|
"javascript_tool", "health", "smoke",
|
|
@@ -1574,7 +1680,8 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1574
1680
|
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
|
|
1575
1681
|
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1576
1682
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1577
|
-
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1683
|
+
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load", "wait.ready",
|
|
1684
|
+
"page.readiness",
|
|
1578
1685
|
"click", "hover", "drag",
|
|
1579
1686
|
"js", "console", "network",
|
|
1580
1687
|
"network.get", "network.body", "network.curl", "network.origins",
|
|
@@ -1621,10 +1728,16 @@ const SEE_ALSO = {
|
|
|
1621
1728
|
"navigate": ["wait.load", "page.read"],
|
|
1622
1729
|
"screenshot": ["page.read", "scroll.bottom for fullpage"],
|
|
1623
1730
|
"record": ["screenshot", "animate-audit", "perf-audit"],
|
|
1731
|
+
"video.start": ["video.stop", "video.status"],
|
|
1732
|
+
"video.stop": ["video.status", "video.restart"],
|
|
1733
|
+
"video.status": ["video.start", "video.stop"],
|
|
1734
|
+
"video.restart": ["video.stop", "video.status"],
|
|
1624
1735
|
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1625
1736
|
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1626
1737
|
"search": ["locate.text", "page.read"],
|
|
1627
|
-
"wait.element": ["wait.load", "wait.network"],
|
|
1738
|
+
"wait.element": ["wait.load", "wait.network", "wait.ready"],
|
|
1739
|
+
"wait.ready": ["page.readiness", "wait.element", "wait.url"],
|
|
1740
|
+
"page.readiness": ["wait.ready", "page.state"],
|
|
1628
1741
|
"wait.load": ["wait.element", "wait.network"],
|
|
1629
1742
|
"wait.network": ["wait.load", "wait.element"],
|
|
1630
1743
|
"scroll.to": ["click", "page.read"],
|
|
@@ -1641,11 +1754,16 @@ Usage: surf <command> [args] [options]
|
|
|
1641
1754
|
|
|
1642
1755
|
Common Commands:
|
|
1643
1756
|
session.ensure <name> [url] Idempotently create or reuse a tab-bound session
|
|
1757
|
+
session.cleanup --idle-after <duration> Remove idle sessions (opt-in; use --dry-run first)
|
|
1644
1758
|
navigate <url> Go to URL (alias: go)
|
|
1645
1759
|
click <ref> Click element by ref or selector
|
|
1646
1760
|
type <text> Type text at cursor or into element
|
|
1647
1761
|
screenshot Capture screenshot (alias: snap)
|
|
1648
1762
|
record Capture screenshot frames into an animated GIF
|
|
1763
|
+
video start <path> Start a long-running local WebM recording
|
|
1764
|
+
video stop Stop the active WebM recording
|
|
1765
|
+
video status Show WebM recording status
|
|
1766
|
+
video restart <path> Replace the active WebM recording
|
|
1649
1767
|
animate-audit JSON timeline of element animation/style samples
|
|
1650
1768
|
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1651
1769
|
page.read Get page accessibility tree (alias: read)
|
|
@@ -1673,6 +1791,9 @@ More Help:
|
|
|
1673
1791
|
--no-wait Return tab_busy/browser_busy instead of queueing
|
|
1674
1792
|
--remote <host>:<port> Route requests to a remote native host
|
|
1675
1793
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1794
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1795
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1796
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1676
1797
|
surf remote authorize <label> --output <path>
|
|
1677
1798
|
surf remote list | surf remote revoke <label>
|
|
1678
1799
|
surf --help-full All commands
|
|
@@ -1690,6 +1811,7 @@ Purpose: control Chrome from shell. Commands are \`surf <command> [args] [option
|
|
|
1690
1811
|
Core loop: navigate -> wait/read -> act -> screenshot/read.
|
|
1691
1812
|
Navigate: surf navigate "https://example.com" # alias: surf go "..."
|
|
1692
1813
|
Wait after navigation: surf wait 2 # or wait.load for load complete
|
|
1814
|
+
Wait for real content: surf wait.ready --selector ".results" # fails fast with page_login / page_challenge / page_not_found; --accept login returns the state
|
|
1693
1815
|
Read DOM/refs: surf page.read --depth 3 --compact # alias: surf read
|
|
1694
1816
|
Refs: use e1/e2 refs from page.read; prefer refs over CSS when available.
|
|
1695
1817
|
Click ref: surf click e5
|
|
@@ -1698,15 +1820,18 @@ Type: surf type "text" --submit # use --ref e5 to target a fiel
|
|
|
1698
1820
|
Screenshot: surf screenshot /tmp/shot.png # auto-saves to /tmp if no path
|
|
1699
1821
|
Full page screenshot: surf screenshot --full-page /tmp/full.png
|
|
1700
1822
|
Record animation: surf record --duration 2000 --fps 10 --output /tmp/anim.gif
|
|
1823
|
+
Video recording: surf video start ./demo.webm --fps 30; surf video stop
|
|
1701
1824
|
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1702
1825
|
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1703
1826
|
JavaScript: surf js "return document.title"
|
|
1827
|
+
Frames: surf frame.list | surf frame.diagnose # diagnose explains why a selector misses inside iframes (shadow roots, srcdoc, out-of-process)
|
|
1704
1828
|
Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf scroll top
|
|
1705
1829
|
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1706
1830
|
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
1707
1831
|
Cookies: surf cookie list | surf cookie get "name" | surf cookie delete "name"
|
|
1708
1832
|
Session targeting: surf --session research read | SURF_SESSION=research surf read
|
|
1709
1833
|
Session status/queue: surf session.info research | surf session.list --refresh
|
|
1834
|
+
Session cleanup: surf session.cleanup --idle-after 1h [--dry-run]
|
|
1710
1835
|
Recovery: run the exact command printed after Recovery: on tab_gone, session_epoch_stale, tab_busy, or browser_busy
|
|
1711
1836
|
Concurrency: commands for different session tabs can overlap; each tab remains FIFO; provider flows are browser-exclusive
|
|
1712
1837
|
Doctor: surf doctor --browser all # native host/socket diagnostics
|
|
@@ -1742,15 +1867,25 @@ Playbooks:
|
|
|
1742
1867
|
Options:
|
|
1743
1868
|
--remote <host>:<port> Route requests to a remote native host
|
|
1744
1869
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1870
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1871
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1872
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1745
1873
|
--session <name> Target a durable named session (or set SURF_SESSION)
|
|
1746
1874
|
--tab-id <id> Target specific tab
|
|
1747
1875
|
--window-id <id> Target specific window
|
|
1748
1876
|
--no-wait Return immediately when the tab/browser is busy
|
|
1749
1877
|
--json Output raw JSON including target metadata
|
|
1750
1878
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1751
|
-
--soft-fail
|
|
1879
|
+
--soft-fail Host tool errors: warn on stderr, exit 0, no JSON error output
|
|
1752
1880
|
--no-lock Bypass the legacy lock for compound client-side commands
|
|
1753
1881
|
|
|
1882
|
+
Host tool-response errors: stderr includes [code] on the first line when supplied;
|
|
1883
|
+
--json also writes {"error":{"code":"...","message":"..."}} to stdout; exit 1.
|
|
1884
|
+
Host details, when present, are included without redundant code/message fields.
|
|
1885
|
+
Missing codes use "error" in JSON. --soft-fail keeps the original warning text.
|
|
1886
|
+
This is not a universal error format: local validation, transport and parser
|
|
1887
|
+
failures keep their existing output/status; --soft-fail does not mask them.
|
|
1888
|
+
|
|
1754
1889
|
Remote Credentials (run on the browser host):
|
|
1755
1890
|
surf remote authorize <label> --output <credential-file>
|
|
1756
1891
|
surf remote list
|
|
@@ -2014,12 +2149,17 @@ Options:
|
|
|
2014
2149
|
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
2015
2150
|
--listen <tailscale-ip>:<port>
|
|
2016
2151
|
Requires surf remote authorize <label> --output <path> first.
|
|
2152
|
+
--socket-mode <600|660>
|
|
2153
|
+
Persist the local POSIX socket mode (default: 600); 660 requires --socket-group.
|
|
2154
|
+
--socket-group <group-or-gid>
|
|
2155
|
+
Persist the local POSIX socket group for mode 660.
|
|
2017
2156
|
|
|
2018
2157
|
Examples:
|
|
2019
2158
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
2020
2159
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser brave
|
|
2021
2160
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser all
|
|
2022
2161
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --target linux
|
|
2162
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --socket-mode 660 --socket-group surf
|
|
2023
2163
|
`);
|
|
2024
2164
|
process.exit(0);
|
|
2025
2165
|
}
|
|
@@ -2514,6 +2654,150 @@ if (args[0] === "do") {
|
|
|
2514
2654
|
return;
|
|
2515
2655
|
}
|
|
2516
2656
|
|
|
2657
|
+
// Handle `surf extract`: a page-side script in an owned tab with a
|
|
2658
|
+
// readiness gate, bounded fresh-tab retry and the zero-rows invariant.
|
|
2659
|
+
if (args[0] === "extract") {
|
|
2660
|
+
const extractArgs = args.slice(1);
|
|
2661
|
+
const valueFlags = new Set([
|
|
2662
|
+
"file", "code", "options", "options-file", "ready-selector", "ready-text", "ready-url-prefix",
|
|
2663
|
+
"ready-timeout", "ready-interval", "empty-text", "rows", "retry", "retry-delay-ms",
|
|
2664
|
+
"tab-id", "session",
|
|
2665
|
+
]);
|
|
2666
|
+
const boolFlags = new Set(["allow-empty", "keep-tab", "json", "no-wait", "help"]);
|
|
2667
|
+
const opts = {};
|
|
2668
|
+
let url = null;
|
|
2669
|
+
for (let i = 0; i < extractArgs.length; i++) {
|
|
2670
|
+
const arg = extractArgs[i];
|
|
2671
|
+
if (arg === "-f") {
|
|
2672
|
+
opts.file = flagValue(extractArgs, arg);
|
|
2673
|
+
i++;
|
|
2674
|
+
} else if (arg.startsWith("--")) {
|
|
2675
|
+
const key = arg.slice(2);
|
|
2676
|
+
if (boolFlags.has(key)) opts[key] = true;
|
|
2677
|
+
else if (valueFlags.has(key)) {
|
|
2678
|
+
opts[key] = key === "options" && extractArgs[i + 1] === ""
|
|
2679
|
+
? ""
|
|
2680
|
+
: flagValue(extractArgs, arg);
|
|
2681
|
+
i++;
|
|
2682
|
+
} else {
|
|
2683
|
+
console.error(`Error: unknown extract option --${key}`);
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
} else if (url === null) {
|
|
2687
|
+
url = arg;
|
|
2688
|
+
} else {
|
|
2689
|
+
console.error(`Error: unexpected argument ${arg}`);
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (opts.help) {
|
|
2694
|
+
showToolHelp("extract");
|
|
2695
|
+
process.exit(0);
|
|
2696
|
+
}
|
|
2697
|
+
const wantJson = opts.json === true;
|
|
2698
|
+
const fail = (code, message, details) => {
|
|
2699
|
+
if (wantJson) {
|
|
2700
|
+
console.log(JSON.stringify({ error: { code, message, ...(details ? { details } : {}) } }, null, 2));
|
|
2701
|
+
} else {
|
|
2702
|
+
console.error(`Error: ${message}${code ? ` [${code}]` : ""}`);
|
|
2703
|
+
}
|
|
2704
|
+
process.exit(1);
|
|
2705
|
+
};
|
|
2706
|
+
if (opts.options !== undefined && opts["options-file"] !== undefined) {
|
|
2707
|
+
fail("usage", "use either --options or --options-file, not both");
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
let code = null;
|
|
2711
|
+
try {
|
|
2712
|
+
if (opts.file && opts.code) fail("usage", "use either --file or --code, not both");
|
|
2713
|
+
if (opts.file) code = fs.readFileSync(opts.file, "utf8");
|
|
2714
|
+
else if (typeof opts.code === "string") code = opts.code;
|
|
2715
|
+
else fail("usage", "an extraction script is required: --file script.js or --code 'return {...}'");
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
fail("usage", `Failed to read script: ${error.message}`);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
let scriptOptions = {};
|
|
2721
|
+
try {
|
|
2722
|
+
if (opts["options-file"]) scriptOptions = parseScriptOptions(fs.readFileSync(opts["options-file"], "utf8"));
|
|
2723
|
+
else scriptOptions = parseScriptOptions(opts.options);
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
fail("usage", error.message);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
const toInt = (key, fallback) => {
|
|
2729
|
+
if (opts[key] === undefined) return fallback;
|
|
2730
|
+
const parsed = Number(opts[key]);
|
|
2731
|
+
if (!/^\d+$/.test(opts[key]) || !Number.isSafeInteger(parsed)) {
|
|
2732
|
+
fail("usage", `--${key} must be a non-negative integer`);
|
|
2733
|
+
}
|
|
2734
|
+
return parsed;
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
const targetOptions = resolveEarlyTargetOptions(extractArgs, {
|
|
2738
|
+
allowWindow: false,
|
|
2739
|
+
allowEnvironmentSession: false,
|
|
2740
|
+
});
|
|
2741
|
+
const hasTarget = Boolean(targetOptions.tabId || targetOptions.session);
|
|
2742
|
+
if (!hasTarget && !url) fail("usage", "a URL is required unless --tab-id or --session names the page to read");
|
|
2743
|
+
|
|
2744
|
+
const settings = {
|
|
2745
|
+
code,
|
|
2746
|
+
url: url ?? undefined,
|
|
2747
|
+
options: scriptOptions,
|
|
2748
|
+
ready: {
|
|
2749
|
+
selector: opts["ready-selector"],
|
|
2750
|
+
text: opts["ready-text"],
|
|
2751
|
+
urlPrefix: opts["ready-url-prefix"],
|
|
2752
|
+
emptyText: opts["empty-text"],
|
|
2753
|
+
timeout: toInt("ready-timeout", undefined),
|
|
2754
|
+
interval: toInt("ready-interval", undefined),
|
|
2755
|
+
},
|
|
2756
|
+
retry: { count: toInt("retry", undefined), delayMs: toInt("retry-delay-ms", undefined) },
|
|
2757
|
+
keepTab: opts["keep-tab"] === true,
|
|
2758
|
+
allowEmpty: opts["allow-empty"] === true,
|
|
2759
|
+
rowsKey: opts.rows,
|
|
2760
|
+
target: hasTarget,
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
const runExtract = async () => {
|
|
2764
|
+
let transport;
|
|
2765
|
+
try {
|
|
2766
|
+
transport = await openClientTransport(endpoint);
|
|
2767
|
+
const baseContext = { ...targetOptions, endpoint, transport };
|
|
2768
|
+
const executeTool = (toolName, toolArgs, ownedTabId) => {
|
|
2769
|
+
const context = ownedTabId
|
|
2770
|
+
? { tabId: ownedTabId, admission: targetOptions.admission, endpoint, transport }
|
|
2771
|
+
: baseContext;
|
|
2772
|
+
return sendDoRequest(toolName, toolArgs, context);
|
|
2773
|
+
};
|
|
2774
|
+
const result = await runExtraction({
|
|
2775
|
+
...settings,
|
|
2776
|
+
executeTool,
|
|
2777
|
+
onEvent: (event) => {
|
|
2778
|
+
if (wantJson) return;
|
|
2779
|
+
if (event.type === "attempt" && event.of > 1) console.error(`[surf] extract attempt ${event.attempt}/${event.of}`);
|
|
2780
|
+
if (event.type === "attempt-failed" && event.retryable) console.error(`[surf] attempt ${event.attempt} failed (${event.error}); retrying with a fresh tab`);
|
|
2781
|
+
},
|
|
2782
|
+
});
|
|
2783
|
+
if (wantJson) {
|
|
2784
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2785
|
+
} else {
|
|
2786
|
+
console.log(renderExtractionMarkdown(result.data, result.rows, { title: url ? `Extraction from ${url}` : "Extraction" }));
|
|
2787
|
+
if (result.tabId) console.error(`[surf] tab ${result.tabId} left open (--keep-tab)`);
|
|
2788
|
+
}
|
|
2789
|
+
return 0;
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
fail(error.code || "extraction_failed", error.message, error.details);
|
|
2792
|
+
} finally {
|
|
2793
|
+
await transport?.close();
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
runExtract().then((exitCode) => process.exit(exitCode));
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2517
2801
|
// Handle workflow management commands
|
|
2518
2802
|
if (args[0] === "workflow.list") {
|
|
2519
2803
|
const workflows = listWorkflows();
|
|
@@ -2686,6 +2970,7 @@ if (tool === "session" && firstArg) {
|
|
|
2686
2970
|
new: "session.new",
|
|
2687
2971
|
ensure: "session.ensure",
|
|
2688
2972
|
list: "session.list",
|
|
2973
|
+
cleanup: "session.cleanup",
|
|
2689
2974
|
info: "session.info",
|
|
2690
2975
|
close: "session.close",
|
|
2691
2976
|
rebind: "session.rebind",
|
|
@@ -2715,6 +3000,21 @@ if (tool === "cookie" && firstArg) {
|
|
|
2715
3000
|
}
|
|
2716
3001
|
}
|
|
2717
3002
|
|
|
3003
|
+
if (tool === "video" && firstArg) {
|
|
3004
|
+
const videoSubcommands = {
|
|
3005
|
+
start: "video.start",
|
|
3006
|
+
stop: "video.stop",
|
|
3007
|
+
status: "video.status",
|
|
3008
|
+
restart: "video.restart",
|
|
3009
|
+
};
|
|
3010
|
+
const videoTool = videoSubcommands[firstArg];
|
|
3011
|
+
if (videoTool) {
|
|
3012
|
+
tool = videoTool;
|
|
3013
|
+
positional = [tool, ...positional.slice(2)];
|
|
3014
|
+
firstArg = positional[1];
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2718
3018
|
if (!tool) {
|
|
2719
3019
|
console.error("Error: No command specified");
|
|
2720
3020
|
process.exit(1);
|
|
@@ -2879,6 +3179,11 @@ if (tool === "record" && firstArg !== undefined && toolArgs.output === undefined
|
|
|
2879
3179
|
firstArg = undefined;
|
|
2880
3180
|
}
|
|
2881
3181
|
|
|
3182
|
+
if ((tool === "video.start" || tool === "video.restart") && firstArg !== undefined && toolArgs.output === undefined) {
|
|
3183
|
+
toolArgs.output = firstArg;
|
|
3184
|
+
firstArg = undefined;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
2882
3187
|
if (firstArg !== undefined) {
|
|
2883
3188
|
const primaryKey = PRIMARY_ARG_MAP[tool];
|
|
2884
3189
|
if (primaryKey && toolArgs[primaryKey] === undefined) {
|
|
@@ -2904,6 +3209,17 @@ if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
|
2904
3209
|
}
|
|
2905
3210
|
}
|
|
2906
3211
|
|
|
3212
|
+
if ((tool === "js" || tool === "frame.js") && toolArgs.options !== undefined) {
|
|
3213
|
+
try {
|
|
3214
|
+
if (typeof toolArgs.code !== "string") throw new Error("--options needs code (inline or --file)");
|
|
3215
|
+
toolArgs.code = applyOptionsPrelude(toolArgs.code, toolArgs.options);
|
|
3216
|
+
delete toolArgs.options;
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
console.error(`Error: ${e.message}`);
|
|
3219
|
+
process.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
2907
3223
|
if (tool === "batch" && toolArgs.file) {
|
|
2908
3224
|
try {
|
|
2909
3225
|
const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
|
|
@@ -3007,8 +3323,11 @@ if (tool === "gemini") {
|
|
|
3007
3323
|
if (tool === "network.export" && outputPath !== undefined) {
|
|
3008
3324
|
toolArgs.output = outputPath;
|
|
3009
3325
|
}
|
|
3326
|
+
if ((tool === "video.start" || tool === "video.restart") && outputPath !== undefined) {
|
|
3327
|
+
toolArgs.output = outputPath;
|
|
3328
|
+
}
|
|
3010
3329
|
|
|
3011
|
-
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save") && outputPath && typeof outputPath !== "string") {
|
|
3330
|
+
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save" || tool === "video.start" || tool === "video.restart") && outputPath && typeof outputPath !== "string") {
|
|
3012
3331
|
console.error("Error: --output requires a file path");
|
|
3013
3332
|
process.exit(1);
|
|
3014
3333
|
}
|
|
@@ -3018,6 +3337,16 @@ if (tool === "page.save" && !outputPath) {
|
|
|
3018
3337
|
process.exit(1);
|
|
3019
3338
|
}
|
|
3020
3339
|
|
|
3340
|
+
if (tool === "video.start" || tool === "video.restart") {
|
|
3341
|
+
try {
|
|
3342
|
+
parseVideoFps(toolArgs.fps);
|
|
3343
|
+
validateVideoOutputPath(toolArgs.output, { createParent: false });
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
console.error(`Error: ${error.message}`);
|
|
3346
|
+
process.exit(1);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3021
3350
|
if (tool === "screenshot" && outputPath) {
|
|
3022
3351
|
toolArgs.savePath = outputPath;
|
|
3023
3352
|
if (options.full) toolArgs.full = true;
|
|
@@ -3540,7 +3869,25 @@ async function handleResponse(response) {
|
|
|
3540
3869
|
socket.end();
|
|
3541
3870
|
process.exit(0);
|
|
3542
3871
|
}
|
|
3543
|
-
|
|
3872
|
+
// Host tool-response errors carry codes separately from their display text.
|
|
3873
|
+
const errorCode = typeof response.error.code === "string" ? response.error.code : null;
|
|
3874
|
+
const [firstLine, ...restLines] = errContent.split("\n");
|
|
3875
|
+
const display = errorCode && !firstLine.includes(`[${errorCode}]`)
|
|
3876
|
+
? [`${firstLine} [${errorCode}]`, ...restLines].join("\n")
|
|
3877
|
+
: errContent;
|
|
3878
|
+
console.error("Error:", display);
|
|
3879
|
+
if (wantJson) {
|
|
3880
|
+
// details repeats code/message when the error serialises itself; keep the rest.
|
|
3881
|
+
const { code: _code, message: _message, ...details } =
|
|
3882
|
+
response.error.details && typeof response.error.details === "object" ? response.error.details : {};
|
|
3883
|
+
console.log(JSON.stringify({
|
|
3884
|
+
error: {
|
|
3885
|
+
code: errorCode || "error",
|
|
3886
|
+
message: typeof response.error.message === "string" ? response.error.message : firstLine,
|
|
3887
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
3888
|
+
},
|
|
3889
|
+
}, null, 2));
|
|
3890
|
+
}
|
|
3544
3891
|
|
|
3545
3892
|
if (autoCapture) {
|
|
3546
3893
|
await performAutoCapture();
|
|
@@ -3615,6 +3962,21 @@ async function handleResponse(response) {
|
|
|
3615
3962
|
].join("\t"));
|
|
3616
3963
|
}
|
|
3617
3964
|
}
|
|
3965
|
+
} else if (finalTool === "session.cleanup" && data?.success) {
|
|
3966
|
+
const removed = Array.isArray(data.removed) ? data.removed : [];
|
|
3967
|
+
if (removed.length === 0) {
|
|
3968
|
+
console.log("No browser sessions matched the idle cleanup threshold.");
|
|
3969
|
+
} else {
|
|
3970
|
+
const verb = data.dryRun ? "Would remove" : "Removed";
|
|
3971
|
+
for (const entry of removed) {
|
|
3972
|
+
const target = entry.targetAction === "close"
|
|
3973
|
+
? "target closed"
|
|
3974
|
+
: entry.targetAction === "keep"
|
|
3975
|
+
? "target kept"
|
|
3976
|
+
: "target already gone";
|
|
3977
|
+
console.log(`${verb} session ${entry.name} tab=${entry.tabId ?? "-"} (${target})`);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3618
3980
|
} else if (finalTool === "session.info" && data?.session) {
|
|
3619
3981
|
const entry = data.session;
|
|
3620
3982
|
console.log(`Session: ${entry.name}`);
|
|
@@ -3631,6 +3993,18 @@ async function handleResponse(response) {
|
|
|
3631
3993
|
console.log(`Use: export SURF_SESSION=${entry.name}`);
|
|
3632
3994
|
} else if (finalTool === "session.close" && data?.success) {
|
|
3633
3995
|
console.log(`Session ${data.name} closed (${data.targetClosed ? "target closed" : "target kept"})`);
|
|
3996
|
+
} else if (finalTool === "video.start" && data?.status === "active") {
|
|
3997
|
+
console.log(`Video recording started: ${data.path} (${data.fps}fps)`);
|
|
3998
|
+
} else if (finalTool === "video.restart" && data?.status === "active") {
|
|
3999
|
+
console.log(`Video recording restarted: ${data.path} (${data.fps}fps)`);
|
|
4000
|
+
} else if (finalTool === "video.stop" && data?.status === "stopped") {
|
|
4001
|
+
console.log(`Saved video recording to ${data.path} (${data.frames} frames, ${data.capturedFrames} captured frames @ ${data.fps}fps)`);
|
|
4002
|
+
} else if (finalTool === "video.status") {
|
|
4003
|
+
if (data?.status === "active") {
|
|
4004
|
+
console.log(`Video recording active: ${data.path} (${data.fps}fps, ${data.frames} frames, ${data.capturedFrames} captured frames)`);
|
|
4005
|
+
} else {
|
|
4006
|
+
console.log("No active video recording");
|
|
4007
|
+
}
|
|
3634
4008
|
} else if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3635
4009
|
const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
|
|
3636
4010
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
@@ -3705,6 +4079,62 @@ async function handleResponse(response) {
|
|
|
3705
4079
|
}
|
|
3706
4080
|
console.log("\nUsage: surf emulate.device \"<device name>\"");
|
|
3707
4081
|
console.log('Reset: surf emulate.device "reset"');
|
|
4082
|
+
} else if (tool === "wait.ready" || tool === "page.readiness") {
|
|
4083
|
+
const lines = [`state: ${data?.state ?? "unknown"}`];
|
|
4084
|
+
if (data?.href) lines.push(`url: ${data.href}`);
|
|
4085
|
+
if (data?.title) lines.push(`title: ${data.title}`);
|
|
4086
|
+
if (typeof data?.waited === "number") lines.push(`waited: ${data.waited}ms (${data.polls} poll${data.polls === 1 ? "" : "s"})`);
|
|
4087
|
+
if (data?.accepted) lines.push("accepted: negative state returned because of --accept");
|
|
4088
|
+
for (const item of Array.isArray(data?.evidence) ? data.evidence : []) lines.push(`- ${item}`);
|
|
4089
|
+
console.log(lines.join("\n"));
|
|
4090
|
+
} else if (tool === "frame.diagnose" && data?.counts) {
|
|
4091
|
+
// Keep frame URLs readable: embed runners carry kilobyte-long state
|
|
4092
|
+
// parameters that bury the report (use --json for the full URLs).
|
|
4093
|
+
const abbreviateUrl = (url, max = 100) => {
|
|
4094
|
+
if (typeof url !== "string" || url.length <= max) return url;
|
|
4095
|
+
try {
|
|
4096
|
+
const parsed = new URL(url);
|
|
4097
|
+
const base = `${parsed.origin}${parsed.pathname}`;
|
|
4098
|
+
const trailing = url.length - base.length;
|
|
4099
|
+
if (trailing > 0 && base.length <= max - 12) return `${base}?...(+${trailing} chars)`;
|
|
4100
|
+
} catch {}
|
|
4101
|
+
return `${url.slice(0, max - 3)}...`;
|
|
4102
|
+
};
|
|
4103
|
+
const lines = [];
|
|
4104
|
+
lines.push(`Frame diagnosis for ${data.mainPage?.href ?? "?"}${data.mainPage?.title ? ` (${data.mainPage.title})` : ""}`);
|
|
4105
|
+
lines.push(`DOM iframes: ${data.counts.domIframes}, extension frames: ${data.counts.extensionFrames} (incl. main), CDP frames: ${data.counts.cdpFrames}`);
|
|
4106
|
+
if (Array.isArray(data.domIframes) && data.domIframes.length > 0) {
|
|
4107
|
+
lines.push("", "DOM iframes:");
|
|
4108
|
+
for (const f of data.domIframes) {
|
|
4109
|
+
const flags = [
|
|
4110
|
+
f.blank ? "blank" : null,
|
|
4111
|
+
f.crossOrigin ? "cross-origin" : null,
|
|
4112
|
+
f.scriptsBlocked ? "scripts-blocked" : null,
|
|
4113
|
+
f.zeroSize ? "0-size" : null,
|
|
4114
|
+
].filter(Boolean).join(",");
|
|
4115
|
+
const links = [
|
|
4116
|
+
f.extensionFrameIds?.length ? `ext ${f.extensionFrameIds.join("/")}` : "ext -",
|
|
4117
|
+
f.cdpFrameIds?.length ? `cdp ${f.cdpFrameIds.join("/")}` : "cdp -",
|
|
4118
|
+
].join(", ");
|
|
4119
|
+
lines.push(` [${f.domIndex}] ${f.srcdoc ? "srcdoc" : abbreviateUrl(f.src || "about:blank")} ${Math.round(f.rect?.width ?? 0)}x${Math.round(f.rect?.height ?? 0)}${f.name ? ` name=${f.name}` : f.id ? ` id=${f.id}` : ""}${f.sandbox !== null && f.sandbox !== undefined ? ` sandbox="${f.sandbox}"` : ""}${f.shadowHost ? ` in shadow root of ${f.shadowHost}` : ""}${flags ? ` [${flags}]` : ""} -> ${links}`);
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
if (Array.isArray(data.extensionFrames) && data.extensionFrames.length > 0) {
|
|
4123
|
+
lines.push("", "Extension frames (frame.switch indexes, webNavigation ids):");
|
|
4124
|
+
for (const f of data.extensionFrames) {
|
|
4125
|
+
const reach = f.contentScriptReachable ? "content-script ok" : `content-script unreachable${f.contentScriptError ? ` (${f.contentScriptError})` : ""}`;
|
|
4126
|
+
lines.push(` ${f.isMain ? "main" : `[${f.switchIndex}]`} #${f.frameId}${f.isMain ? "" : ` parent ${f.parentFrameId}`} ${abbreviateUrl(f.url)}${f.crossOrigin ? " [cross-origin]" : ""} - ${reach}`);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
if (Array.isArray(data.cdpFrames) && data.cdpFrames.length > 0) {
|
|
4130
|
+
lines.push("", "CDP frames (frame.js ids):");
|
|
4131
|
+
for (const f of data.cdpFrames) {
|
|
4132
|
+
lines.push(` ${f.frameId}${f.isMain ? " main" : ` parent ${f.parentId}`} ${abbreviateUrl(f.url)}${f.name ? ` name=${f.name}` : ""}${f.extensionFrameIds?.length ? ` -> ext ${f.extensionFrameIds.join("/")}` : ""}`);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
lines.push("", Array.isArray(data.warnings) && data.warnings.length > 0 ? "Warnings:" : "No warnings.");
|
|
4136
|
+
for (const w of Array.isArray(data.warnings) ? data.warnings : []) lines.push(` - ${w}`);
|
|
4137
|
+
console.log(lines.join("\n"));
|
|
3708
4138
|
} else if (tool === "js") {
|
|
3709
4139
|
if (data?.result !== undefined) {
|
|
3710
4140
|
const val = data.result.value ?? data.result;
|