surf-cli 2.18.0 → 2.20.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 +276 -8
- package/dist/content/index.js +4 -4
- package/dist/content/index.js.map +1 -1
- package/dist/options/options.html +1 -18
- package/dist/service-worker/index.js +44 -15
- package/dist/service-worker/index.js.map +1 -1
- package/native/cli.cjs +430 -14
- package/native/do-executor.cjs +35 -8
- package/native/doctor.cjs +211 -42
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/host-helpers.cjs +133 -11
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +53 -23
- package/native/mcp-server.cjs +25 -0
- package/native/native-host-launch-probe.cjs +69 -0
- package/native/private-state.cjs +25 -1
- package/native/script-options.cjs +33 -0
- package/native/semantic-cli.cjs +764 -0
- package/native/semantic-core.cjs +369 -0
- package/native/semantic-credentials.cjs +207 -0
- package/native/semantic-provider.cjs +61 -0
- package/native/semantic-workflow-executor.cjs +65 -0
- package/native/semantic-workflow-state.cjs +271 -0
- package/native/semantic-workflow.cjs +398 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +3 -2
- package/native/workflow-definition.cjs +125 -1
- package/native/workflow-runtime.cjs +71 -5
- package/package.json +11 -7
- package/scripts/install-native-host.cjs +103 -60
- package/scripts/uninstall-native-host.cjs +40 -38
- package/scripts/windows-interop.cjs +89 -0
- package/skills/surf/SKILL.md +96 -1
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, semanticRequestContext, 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 {
|
|
@@ -73,7 +75,7 @@ function positiveIdFlag(argv, flag) {
|
|
|
73
75
|
return parsed;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
78
|
+
function resolveEarlyTargetOptions(argv, { allowWindow = true, allowEnvironmentSession = true } = {}) {
|
|
77
79
|
const explicitSession = flagValue(argv, "--session");
|
|
78
80
|
const tabId = positiveIdFlag(argv, "--tab-id");
|
|
79
81
|
const windowId = allowWindow ? positiveIdFlag(argv, "--window-id") : undefined;
|
|
@@ -82,7 +84,7 @@ function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
|
82
84
|
process.exit(1);
|
|
83
85
|
}
|
|
84
86
|
const environmentSession = process.env.SURF_SESSION;
|
|
85
|
-
const session = explicitSession || (!tabId && !windowId ? environmentSession : undefined);
|
|
87
|
+
const session = explicitSession || (allowEnvironmentSession && !tabId && !windowId ? environmentSession : undefined);
|
|
86
88
|
return {
|
|
87
89
|
...(session ? { session, sessionSource: explicitSession ? "explicit" : "environment" } : {}),
|
|
88
90
|
...(tabId ? { tabId } : {}),
|
|
@@ -200,6 +202,22 @@ try {
|
|
|
200
202
|
process.exit(1);
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
if (args[0] === "semantic" || args[0]?.startsWith("semantic.")) {
|
|
206
|
+
const { formatSemanticOutput, handleSemanticCli } = require("./semantic-cli.cjs");
|
|
207
|
+
handleSemanticCli(args, { endpoint })
|
|
208
|
+
.then((result) => {
|
|
209
|
+
if (!result.handled) throw new Error("Semantic command was not handled");
|
|
210
|
+
if (result.value !== undefined) console.log(formatSemanticOutput(result));
|
|
211
|
+
process.exit(0);
|
|
212
|
+
})
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
const code = error?.code ? ` [${error.code}]` : "";
|
|
215
|
+
console.error(`Error: ${error?.message || String(error)}${code}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
203
221
|
if (args[0] === "oracle") {
|
|
204
222
|
if (args[1] === "ask" || args[1] === "follow") {
|
|
205
223
|
console.error("[surf] Oracle requires exclusive browser access while dispatching; other sessions will queue.");
|
|
@@ -721,6 +739,17 @@ const TOOLS = {
|
|
|
721
739
|
examples: [{ cmd: "page.save --output page.html", desc: "Save current document HTML" }],
|
|
722
740
|
},
|
|
723
741
|
"page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
|
|
742
|
+
"page.readiness": {
|
|
743
|
+
desc: "Classify the page once: ready, empty, loading, login, challenge, not-found, error",
|
|
744
|
+
args: [],
|
|
745
|
+
opts: {
|
|
746
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
747
|
+
text: "Page text that marks a ready page",
|
|
748
|
+
"url-prefix": "Expected URL prefix",
|
|
749
|
+
"empty-text": "Text of an explicit no-results render",
|
|
750
|
+
},
|
|
751
|
+
examples: [{ cmd: "page.readiness --json", desc: "State plus evidence as JSON" }]
|
|
752
|
+
},
|
|
724
753
|
}
|
|
725
754
|
},
|
|
726
755
|
locate: {
|
|
@@ -824,6 +853,24 @@ const TOOLS = {
|
|
|
824
853
|
},
|
|
825
854
|
"wait.dom": { desc: "Wait for DOM to stabilize", args: [], opts: { stable: "Stability window in ms (default: 100)", timeout: "Max wait time in ms" } },
|
|
826
855
|
"wait.load": { desc: "Wait for page to fully load", args: [], opts: { timeout: "Max wait time in ms (default: 30000)" } },
|
|
856
|
+
"wait.ready": {
|
|
857
|
+
desc: "Wait until the page is ready, or fail fast with a typed state (challenge, login, not-found, error)",
|
|
858
|
+
args: [],
|
|
859
|
+
opts: {
|
|
860
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
861
|
+
text: "Page text that marks a ready page",
|
|
862
|
+
"url-prefix": "Expected URL prefix; anything else is a bounce",
|
|
863
|
+
"empty-text": "Text of an explicit no-results render (reports state 'empty')",
|
|
864
|
+
accept: "Negative states to return instead of fail (comma list)",
|
|
865
|
+
timeout: "Max wait time in ms (default: 20000, max: 120000)",
|
|
866
|
+
interval: "Poll interval in ms (default: 400)",
|
|
867
|
+
},
|
|
868
|
+
examples: [
|
|
869
|
+
{ cmd: 'wait.ready --selector ".results"', desc: "Wait for content; fail fast on a login bounce" },
|
|
870
|
+
{ cmd: 'wait.ready --url-prefix "https://app.example.com/" --empty-text "No results"', desc: "Distinguish empty from blocked" },
|
|
871
|
+
{ cmd: "wait.ready --accept login --json", desc: "Return the login state to the caller" },
|
|
872
|
+
]
|
|
873
|
+
},
|
|
827
874
|
}
|
|
828
875
|
},
|
|
829
876
|
input: {
|
|
@@ -878,17 +925,52 @@ const TOOLS = {
|
|
|
878
925
|
"drag": { desc: "Drag between points", args: [], opts: { from: "Start x,y", to: "End x,y" } },
|
|
879
926
|
}
|
|
880
927
|
},
|
|
928
|
+
extract: {
|
|
929
|
+
desc: "Scripted extraction in an owned tab",
|
|
930
|
+
commands: {
|
|
931
|
+
"extract": {
|
|
932
|
+
desc: "Open a URL in a fresh tab, wait until it is ready, run a page-side script that returns JSON, print rows",
|
|
933
|
+
args: ["url"],
|
|
934
|
+
opts: {
|
|
935
|
+
file: "Script file; must `return` JSON (an array, or an object with a rows/items/results array)",
|
|
936
|
+
code: "Inline script instead of --file",
|
|
937
|
+
options: "JSON object exposed to the script as SURF_OPTIONS",
|
|
938
|
+
"options-file": "Read the options object from a JSON file",
|
|
939
|
+
"ready-selector": "wait.ready --selector before extracting",
|
|
940
|
+
"ready-text": "wait.ready --text before extracting",
|
|
941
|
+
"ready-url-prefix": "wait.ready --url-prefix; a different URL is a bounce",
|
|
942
|
+
"empty-text": "wait.ready --empty-text; lets an explicit no-results page pass the zero-rows check",
|
|
943
|
+
"ready-timeout": "Readiness timeout in ms (default: 20000)",
|
|
944
|
+
"ready-interval": "Readiness polling interval in ms (default: 400)",
|
|
945
|
+
rows: "Key of the row array in the script result (default: auto)",
|
|
946
|
+
retry: "Fresh-tab retries on transient failures (default: 1, max: 5)",
|
|
947
|
+
"retry-delay-ms": "Delay between attempts (default: 500)",
|
|
948
|
+
"allow-empty": "Accept zero rows",
|
|
949
|
+
"keep-tab": "Leave the owned tab open on success and report its id",
|
|
950
|
+
"tab-id": "Extract from an existing tab instead (no fresh tab, no retry; navigates only if a URL is given)",
|
|
951
|
+
session: "Extract from a session's tab instead (same rules as --tab-id)",
|
|
952
|
+
json: "Print {data, rows, rowCount, attempts, readiness} as JSON",
|
|
953
|
+
},
|
|
954
|
+
examples: [
|
|
955
|
+
{ cmd: 'extract "https://example.com/list" --file rows.js --ready-selector ".item"', desc: "Fresh tab, wait for items, print a Markdown table" },
|
|
956
|
+
{ 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" },
|
|
957
|
+
{ cmd: "extract --tab-id 42 --code 'return [...document.querySelectorAll(\"h2\")].map(h => ({ title: h.textContent }))'", desc: "Read an existing tab in place" },
|
|
958
|
+
]
|
|
959
|
+
},
|
|
960
|
+
}
|
|
961
|
+
},
|
|
881
962
|
js: {
|
|
882
963
|
desc: "JavaScript execution",
|
|
883
964
|
commands: {
|
|
884
965
|
"js": {
|
|
885
966
|
desc: "Execute JavaScript (use 'return' for values)",
|
|
886
967
|
args: ["code"],
|
|
887
|
-
opts: { file: "Run JS from file" },
|
|
968
|
+
opts: { file: "Run JS from file", options: "JSON object exposed to the script as a frozen SURF_OPTIONS constant" },
|
|
888
969
|
examples: [
|
|
889
970
|
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
890
971
|
{ cmd: 'js "document.body.style.background = \'red\'"', desc: "Run code" },
|
|
891
972
|
{ cmd: "js --file script.js", desc: "Run file" },
|
|
973
|
+
{ cmd: 'js --file script.js --options \'{"limit": 20}\'', desc: "Run file with SURF_OPTIONS.limit" },
|
|
892
974
|
]
|
|
893
975
|
},
|
|
894
976
|
}
|
|
@@ -1134,6 +1216,15 @@ const TOOLS = {
|
|
|
1134
1216
|
args: [],
|
|
1135
1217
|
examples: [{ cmd: "frame.list", desc: "Show frame tree" }]
|
|
1136
1218
|
},
|
|
1219
|
+
"frame.diagnose": {
|
|
1220
|
+
desc: "Compare DOM iframes, extension reachability, and the Chrome DevTools frame tree",
|
|
1221
|
+
args: [],
|
|
1222
|
+
opts: {
|
|
1223
|
+
"tab-id": "Target tab ID",
|
|
1224
|
+
json: "Print full frame inventories as JSON"
|
|
1225
|
+
},
|
|
1226
|
+
examples: [{ cmd: "frame.diagnose", desc: "Explain iframe access and count mismatches" }]
|
|
1227
|
+
},
|
|
1137
1228
|
"frame.switch": {
|
|
1138
1229
|
desc: "Switch to iframe context",
|
|
1139
1230
|
args: [],
|
|
@@ -1156,7 +1247,7 @@ const TOOLS = {
|
|
|
1156
1247
|
"frame.js": {
|
|
1157
1248
|
desc: "Execute JS in specific frame",
|
|
1158
1249
|
args: ["code"],
|
|
1159
|
-
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
1250
|
+
opts: { id: "Frame ID from frame.list", file: "Run JS from file", options: "JSON object exposed as SURF_OPTIONS" },
|
|
1160
1251
|
examples: [
|
|
1161
1252
|
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
1162
1253
|
]
|
|
@@ -1242,7 +1333,10 @@ const TOOLS = {
|
|
|
1242
1333
|
"on-error": "stop (default) | continue",
|
|
1243
1334
|
"no-auto-wait": "Disable automatic waits between steps",
|
|
1244
1335
|
"step-delay": "Delay between steps in ms (default: 100)",
|
|
1245
|
-
"dry-run": "Parse and validate without executing"
|
|
1336
|
+
"dry-run": "Parse and validate without executing",
|
|
1337
|
+
"allow-semantic": "Allow bounded TypeSafe semantic decisions",
|
|
1338
|
+
"allow-write": "Allow declared semantic fill/check/click steps",
|
|
1339
|
+
"inputs-stdin": "Read bounded private input slots as one JSON object from stdin",
|
|
1246
1340
|
},
|
|
1247
1341
|
examples: [
|
|
1248
1342
|
{ cmd: 'do \'go "https://example.com" | click e5 | screenshot\'', desc: "Inline workflow" },
|
|
@@ -1518,8 +1612,19 @@ Tips:
|
|
|
1518
1612
|
- Use window.new --incognito for isolated cookies`
|
|
1519
1613
|
},
|
|
1520
1614
|
semantic: {
|
|
1521
|
-
title: "Semantic
|
|
1522
|
-
content: `
|
|
1615
|
+
title: "Semantic browser decisions and locators",
|
|
1616
|
+
content: `Optional Jev commands (page-derived text is sent to TypeSafe only for these commands):
|
|
1617
|
+
semantic.find "the notification control"
|
|
1618
|
+
semantic.verify "Notification preferences were saved"
|
|
1619
|
+
semantic.filter "notification preferences"
|
|
1620
|
+
semantic.act "Open notification settings" --max-steps 5
|
|
1621
|
+
semantic.act "Fill email" --input email="$EMAIL" --allow-write
|
|
1622
|
+
semantic auth set|status|clear
|
|
1623
|
+
|
|
1624
|
+
Every click/fill requires --allow-write. This broadly authorizes even high-impact controls;
|
|
1625
|
+
repeat --allow-ref <ref> to narrow authorization to exact observed refs.
|
|
1626
|
+
|
|
1627
|
+
Local semantic locators find elements by role, text, or label instead of refs or selectors.
|
|
1523
1628
|
|
|
1524
1629
|
By ARIA role:
|
|
1525
1630
|
locate.role button --name "Submit" --action click
|
|
@@ -1614,7 +1719,8 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1614
1719
|
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
|
|
1615
1720
|
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1616
1721
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1617
|
-
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1722
|
+
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load", "wait.ready",
|
|
1723
|
+
"page.readiness",
|
|
1618
1724
|
"click", "hover", "drag",
|
|
1619
1725
|
"js", "console", "network",
|
|
1620
1726
|
"network.get", "network.body", "network.curl", "network.origins",
|
|
@@ -1624,7 +1730,7 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1624
1730
|
"form.fill",
|
|
1625
1731
|
"perf.start", "perf.stop", "perf.metrics",
|
|
1626
1732
|
"upload",
|
|
1627
|
-
"frame.list", "frame.switch", "frame.main", "frame.js",
|
|
1733
|
+
"frame.list", "frame.diagnose", "frame.switch", "frame.main", "frame.js",
|
|
1628
1734
|
"cookie.list", "cookie.get", "cookie.set", "cookie.clear",
|
|
1629
1735
|
"search", "batch",
|
|
1630
1736
|
"zoom", "resize",
|
|
@@ -1647,6 +1753,7 @@ const SEE_ALSO = {
|
|
|
1647
1753
|
"window.new": ["window.list"],
|
|
1648
1754
|
"window.list": ["tab.list"],
|
|
1649
1755
|
"frame.list": ["frame.switch", "frame.main"],
|
|
1756
|
+
"frame.diagnose": ["frame.list", "frame.switch", "frame.js"],
|
|
1650
1757
|
"frame.switch": ["frame.list", "frame.main", "frame.js"],
|
|
1651
1758
|
"frame.main": ["frame.list", "frame.switch"],
|
|
1652
1759
|
"frame.js": ["frame.switch", "js"],
|
|
@@ -1668,7 +1775,9 @@ const SEE_ALSO = {
|
|
|
1668
1775
|
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1669
1776
|
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1670
1777
|
"search": ["locate.text", "page.read"],
|
|
1671
|
-
"wait.element": ["wait.load", "wait.network"],
|
|
1778
|
+
"wait.element": ["wait.load", "wait.network", "wait.ready"],
|
|
1779
|
+
"wait.ready": ["page.readiness", "wait.element", "wait.url"],
|
|
1780
|
+
"page.readiness": ["wait.ready", "page.state"],
|
|
1672
1781
|
"wait.load": ["wait.element", "wait.network"],
|
|
1673
1782
|
"wait.network": ["wait.load", "wait.element"],
|
|
1674
1783
|
"scroll.to": ["click", "page.read"],
|
|
@@ -1698,6 +1807,8 @@ Common Commands:
|
|
|
1698
1807
|
animate-audit JSON timeline of element animation/style samples
|
|
1699
1808
|
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1700
1809
|
page.read Get page accessibility tree (alias: read)
|
|
1810
|
+
semantic.find Optional Jev-powered candidate selection
|
|
1811
|
+
semantic.act Bounded semantic browser action controller
|
|
1701
1812
|
locate.role <role> Find element by ARIA role
|
|
1702
1813
|
search <term> Search for text in page (alias: find)
|
|
1703
1814
|
window.new <url> Create isolated browser window
|
|
@@ -1722,6 +1833,9 @@ More Help:
|
|
|
1722
1833
|
--no-wait Return tab_busy/browser_busy instead of queueing
|
|
1723
1834
|
--remote <host>:<port> Route requests to a remote native host
|
|
1724
1835
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1836
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1837
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1838
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1725
1839
|
surf remote authorize <label> --output <path>
|
|
1726
1840
|
surf remote list | surf remote revoke <label>
|
|
1727
1841
|
surf --help-full All commands
|
|
@@ -1739,6 +1853,7 @@ Purpose: control Chrome from shell. Commands are \`surf <command> [args] [option
|
|
|
1739
1853
|
Core loop: navigate -> wait/read -> act -> screenshot/read.
|
|
1740
1854
|
Navigate: surf navigate "https://example.com" # alias: surf go "..."
|
|
1741
1855
|
Wait after navigation: surf wait 2 # or wait.load for load complete
|
|
1856
|
+
Wait for real content: surf wait.ready --selector ".results" # fails fast with page_login / page_challenge / page_not_found; --accept login returns the state
|
|
1742
1857
|
Read DOM/refs: surf page.read --depth 3 --compact # alias: surf read
|
|
1743
1858
|
Refs: use e1/e2 refs from page.read; prefer refs over CSS when available.
|
|
1744
1859
|
Click ref: surf click e5
|
|
@@ -1751,6 +1866,7 @@ Video recording: surf video start ./demo.webm --fps 30; surf video stop
|
|
|
1751
1866
|
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1752
1867
|
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1753
1868
|
JavaScript: surf js "return document.title"
|
|
1869
|
+
Frames: surf frame.list | surf frame.diagnose # diagnose explains why a selector misses inside iframes (shadow roots, srcdoc, out-of-process)
|
|
1754
1870
|
Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf scroll top
|
|
1755
1871
|
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1756
1872
|
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
@@ -1770,6 +1886,11 @@ const showFullHelp = () => {
|
|
|
1770
1886
|
|
|
1771
1887
|
Usage: surf <command> [args] [options]
|
|
1772
1888
|
|
|
1889
|
+
Semantic (optional TypeSafe/Jev):
|
|
1890
|
+
surf semantic.find|verify|filter <goal> [--json]
|
|
1891
|
+
surf semantic.act <goal> [--allow-write] [--allow-ref <ref>] [--input <name=value>]
|
|
1892
|
+
surf semantic auth <set|status|clear>
|
|
1893
|
+
|
|
1773
1894
|
Oracle:
|
|
1774
1895
|
surf oracle <ask|status|result|follow|list>
|
|
1775
1896
|
|
|
@@ -1793,15 +1914,25 @@ Playbooks:
|
|
|
1793
1914
|
Options:
|
|
1794
1915
|
--remote <host>:<port> Route requests to a remote native host
|
|
1795
1916
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1917
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1918
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1919
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1796
1920
|
--session <name> Target a durable named session (or set SURF_SESSION)
|
|
1797
1921
|
--tab-id <id> Target specific tab
|
|
1798
1922
|
--window-id <id> Target specific window
|
|
1799
1923
|
--no-wait Return immediately when the tab/browser is busy
|
|
1800
1924
|
--json Output raw JSON including target metadata
|
|
1801
1925
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1802
|
-
--soft-fail
|
|
1926
|
+
--soft-fail Host tool errors: warn on stderr, exit 0, no JSON error output
|
|
1803
1927
|
--no-lock Bypass the legacy lock for compound client-side commands
|
|
1804
1928
|
|
|
1929
|
+
Host tool-response errors: stderr includes [code] on the first line when supplied;
|
|
1930
|
+
--json also writes {"error":{"code":"...","message":"..."}} to stdout; exit 1.
|
|
1931
|
+
Host details, when present, are included without redundant code/message fields.
|
|
1932
|
+
Missing codes use "error" in JSON. --soft-fail keeps the original warning text.
|
|
1933
|
+
This is not a universal error format: local validation, transport and parser
|
|
1934
|
+
failures keep their existing output/status; --soft-fail does not mask them.
|
|
1935
|
+
|
|
1805
1936
|
Remote Credentials (run on the browser host):
|
|
1806
1937
|
surf remote authorize <label> --output <credential-file>
|
|
1807
1938
|
surf remote list
|
|
@@ -2323,9 +2454,12 @@ if (args[0] === "do") {
|
|
|
2323
2454
|
let windowId = undefined;
|
|
2324
2455
|
let explicitSession = undefined;
|
|
2325
2456
|
let noWait = false;
|
|
2457
|
+
let allowSemantic = false;
|
|
2458
|
+
let allowWrite = false;
|
|
2459
|
+
let inputsStdin = false;
|
|
2326
2460
|
|
|
2327
2461
|
// Reserved flags that aren't workflow args
|
|
2328
|
-
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'session', 'no-lock', 'no-wait'];
|
|
2462
|
+
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'session', 'no-lock', 'no-wait', 'allow-semantic', 'allow-write', 'inputs-stdin'];
|
|
2329
2463
|
|
|
2330
2464
|
// Workflow-specific args (collected for variable substitution)
|
|
2331
2465
|
const workflowArgs = {};
|
|
@@ -2365,6 +2499,12 @@ if (args[0] === "do") {
|
|
|
2365
2499
|
i++;
|
|
2366
2500
|
} else if (arg === "--no-wait") {
|
|
2367
2501
|
noWait = true;
|
|
2502
|
+
} else if (arg === "--allow-semantic") {
|
|
2503
|
+
allowSemantic = true;
|
|
2504
|
+
} else if (arg === "--allow-write") {
|
|
2505
|
+
allowWrite = true;
|
|
2506
|
+
} else if (arg === "--inputs-stdin") {
|
|
2507
|
+
inputsStdin = true;
|
|
2368
2508
|
} else if (arg.startsWith("--")) {
|
|
2369
2509
|
// Workflow-specific arg (e.g., --email, --password)
|
|
2370
2510
|
const key = arg.slice(2);
|
|
@@ -2489,6 +2629,27 @@ if (args[0] === "do") {
|
|
|
2489
2629
|
|
|
2490
2630
|
// Apply arg defaults
|
|
2491
2631
|
const vars = workflow ? applyArgDefaults(workflow, workflowArgs) : workflowArgs;
|
|
2632
|
+
let privateInputs = {};
|
|
2633
|
+
if (inputsStdin) {
|
|
2634
|
+
try {
|
|
2635
|
+
const { SEMANTIC_POLICY } = require("./semantic-core.cjs");
|
|
2636
|
+
const input = fs.readFileSync(0, "utf8");
|
|
2637
|
+
if (Buffer.byteLength(input, "utf8") > 262144) throw new Error("input JSON exceeds 256 KiB");
|
|
2638
|
+
privateInputs = JSON.parse(input);
|
|
2639
|
+
if (!privateInputs || typeof privateInputs !== "object" || Array.isArray(privateInputs)) throw new Error("input JSON must be an object");
|
|
2640
|
+
const entries = Object.entries(privateInputs);
|
|
2641
|
+
if (entries.length > SEMANTIC_POLICY.limits.inputSlots) throw new Error(`input JSON supports at most ${SEMANTIC_POLICY.limits.inputSlots} slots`);
|
|
2642
|
+
for (const [name, value] of entries) {
|
|
2643
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name)) throw new Error("input JSON contains an invalid slot name");
|
|
2644
|
+
if (!["string", "number", "boolean"].includes(typeof value)) throw new Error(`input slot '${name}' must be a string, number, or boolean`);
|
|
2645
|
+
if (Buffer.byteLength(String(value), "utf8") > SEMANTIC_POLICY.limits.inputValueBytes) throw new Error(`input slot '${name}' exceeds ${SEMANTIC_POLICY.limits.inputValueBytes / 1024} KiB`);
|
|
2646
|
+
if (Object.hasOwn(vars, name)) throw new Error(`input slot '${name}' was supplied more than once`);
|
|
2647
|
+
}
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
console.error(`Error: Invalid --inputs-stdin JSON: ${error.message}`);
|
|
2650
|
+
process.exit(1);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2492
2653
|
|
|
2493
2654
|
// Validate with --dry-run
|
|
2494
2655
|
if (dryRun) {
|
|
@@ -2509,6 +2670,21 @@ if (args[0] === "do") {
|
|
|
2509
2670
|
process.exit(0);
|
|
2510
2671
|
}
|
|
2511
2672
|
|
|
2673
|
+
const semanticEnabled = Boolean(workflow?.semantic);
|
|
2674
|
+
if (semanticEnabled && !allowSemantic) {
|
|
2675
|
+
console.error("Error: semantic workflows require --allow-semantic");
|
|
2676
|
+
process.exit(1);
|
|
2677
|
+
}
|
|
2678
|
+
if (semanticEnabled && onError === "continue") {
|
|
2679
|
+
console.error("Error: semantic workflows do not support --on-error continue");
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2682
|
+
const writeOps = new Set(["ensureChecked", "fill", "click"]);
|
|
2683
|
+
if (semanticEnabled && steps.some((step) => writeOps.has(step.args?.op)) && !allowWrite) {
|
|
2684
|
+
console.error("Error: mutation-capable semantic steps require --allow-write");
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2512
2688
|
if (!wantJson) {
|
|
2513
2689
|
if (workflowName) {
|
|
2514
2690
|
console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
|
|
@@ -2536,6 +2712,17 @@ if (args[0] === "do") {
|
|
|
2536
2712
|
endpoint,
|
|
2537
2713
|
transport,
|
|
2538
2714
|
},
|
|
2715
|
+
...(semanticEnabled ? {
|
|
2716
|
+
createSemanticExecutor: async ({ context }) => {
|
|
2717
|
+
const { createConcreteSemanticExecutor } = require("./semantic-workflow-executor.cjs");
|
|
2718
|
+
return createConcreteSemanticExecutor({
|
|
2719
|
+
workflow,
|
|
2720
|
+
inputs: { ...vars, ...privateInputs },
|
|
2721
|
+
request: (tool, toolArgs, timeoutMs, identity) =>
|
|
2722
|
+
sendDoRequest(tool, toolArgs, semanticRequestContext(context, timeoutMs, identity)),
|
|
2723
|
+
});
|
|
2724
|
+
},
|
|
2725
|
+
} : {}),
|
|
2539
2726
|
});
|
|
2540
2727
|
|
|
2541
2728
|
// Print summary
|
|
@@ -2570,6 +2757,150 @@ if (args[0] === "do") {
|
|
|
2570
2757
|
return;
|
|
2571
2758
|
}
|
|
2572
2759
|
|
|
2760
|
+
// Handle `surf extract`: a page-side script in an owned tab with a
|
|
2761
|
+
// readiness gate, bounded fresh-tab retry and the zero-rows invariant.
|
|
2762
|
+
if (args[0] === "extract") {
|
|
2763
|
+
const extractArgs = args.slice(1);
|
|
2764
|
+
const valueFlags = new Set([
|
|
2765
|
+
"file", "code", "options", "options-file", "ready-selector", "ready-text", "ready-url-prefix",
|
|
2766
|
+
"ready-timeout", "ready-interval", "empty-text", "rows", "retry", "retry-delay-ms",
|
|
2767
|
+
"tab-id", "session",
|
|
2768
|
+
]);
|
|
2769
|
+
const boolFlags = new Set(["allow-empty", "keep-tab", "json", "no-wait", "help"]);
|
|
2770
|
+
const opts = {};
|
|
2771
|
+
let url = null;
|
|
2772
|
+
for (let i = 0; i < extractArgs.length; i++) {
|
|
2773
|
+
const arg = extractArgs[i];
|
|
2774
|
+
if (arg === "-f") {
|
|
2775
|
+
opts.file = flagValue(extractArgs, arg);
|
|
2776
|
+
i++;
|
|
2777
|
+
} else if (arg.startsWith("--")) {
|
|
2778
|
+
const key = arg.slice(2);
|
|
2779
|
+
if (boolFlags.has(key)) opts[key] = true;
|
|
2780
|
+
else if (valueFlags.has(key)) {
|
|
2781
|
+
opts[key] = key === "options" && extractArgs[i + 1] === ""
|
|
2782
|
+
? ""
|
|
2783
|
+
: flagValue(extractArgs, arg);
|
|
2784
|
+
i++;
|
|
2785
|
+
} else {
|
|
2786
|
+
console.error(`Error: unknown extract option --${key}`);
|
|
2787
|
+
process.exit(1);
|
|
2788
|
+
}
|
|
2789
|
+
} else if (url === null) {
|
|
2790
|
+
url = arg;
|
|
2791
|
+
} else {
|
|
2792
|
+
console.error(`Error: unexpected argument ${arg}`);
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (opts.help) {
|
|
2797
|
+
showToolHelp("extract");
|
|
2798
|
+
process.exit(0);
|
|
2799
|
+
}
|
|
2800
|
+
const wantJson = opts.json === true;
|
|
2801
|
+
const fail = (code, message, details) => {
|
|
2802
|
+
if (wantJson) {
|
|
2803
|
+
console.log(JSON.stringify({ error: { code, message, ...(details ? { details } : {}) } }, null, 2));
|
|
2804
|
+
} else {
|
|
2805
|
+
console.error(`Error: ${message}${code ? ` [${code}]` : ""}`);
|
|
2806
|
+
}
|
|
2807
|
+
process.exit(1);
|
|
2808
|
+
};
|
|
2809
|
+
if (opts.options !== undefined && opts["options-file"] !== undefined) {
|
|
2810
|
+
fail("usage", "use either --options or --options-file, not both");
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
let code = null;
|
|
2814
|
+
try {
|
|
2815
|
+
if (opts.file && opts.code) fail("usage", "use either --file or --code, not both");
|
|
2816
|
+
if (opts.file) code = fs.readFileSync(opts.file, "utf8");
|
|
2817
|
+
else if (typeof opts.code === "string") code = opts.code;
|
|
2818
|
+
else fail("usage", "an extraction script is required: --file script.js or --code 'return {...}'");
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
fail("usage", `Failed to read script: ${error.message}`);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
let scriptOptions = {};
|
|
2824
|
+
try {
|
|
2825
|
+
if (opts["options-file"]) scriptOptions = parseScriptOptions(fs.readFileSync(opts["options-file"], "utf8"));
|
|
2826
|
+
else scriptOptions = parseScriptOptions(opts.options);
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
fail("usage", error.message);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
const toInt = (key, fallback) => {
|
|
2832
|
+
if (opts[key] === undefined) return fallback;
|
|
2833
|
+
const parsed = Number(opts[key]);
|
|
2834
|
+
if (!/^\d+$/.test(opts[key]) || !Number.isSafeInteger(parsed)) {
|
|
2835
|
+
fail("usage", `--${key} must be a non-negative integer`);
|
|
2836
|
+
}
|
|
2837
|
+
return parsed;
|
|
2838
|
+
};
|
|
2839
|
+
|
|
2840
|
+
const targetOptions = resolveEarlyTargetOptions(extractArgs, {
|
|
2841
|
+
allowWindow: false,
|
|
2842
|
+
allowEnvironmentSession: false,
|
|
2843
|
+
});
|
|
2844
|
+
const hasTarget = Boolean(targetOptions.tabId || targetOptions.session);
|
|
2845
|
+
if (!hasTarget && !url) fail("usage", "a URL is required unless --tab-id or --session names the page to read");
|
|
2846
|
+
|
|
2847
|
+
const settings = {
|
|
2848
|
+
code,
|
|
2849
|
+
url: url ?? undefined,
|
|
2850
|
+
options: scriptOptions,
|
|
2851
|
+
ready: {
|
|
2852
|
+
selector: opts["ready-selector"],
|
|
2853
|
+
text: opts["ready-text"],
|
|
2854
|
+
urlPrefix: opts["ready-url-prefix"],
|
|
2855
|
+
emptyText: opts["empty-text"],
|
|
2856
|
+
timeout: toInt("ready-timeout", undefined),
|
|
2857
|
+
interval: toInt("ready-interval", undefined),
|
|
2858
|
+
},
|
|
2859
|
+
retry: { count: toInt("retry", undefined), delayMs: toInt("retry-delay-ms", undefined) },
|
|
2860
|
+
keepTab: opts["keep-tab"] === true,
|
|
2861
|
+
allowEmpty: opts["allow-empty"] === true,
|
|
2862
|
+
rowsKey: opts.rows,
|
|
2863
|
+
target: hasTarget,
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
const runExtract = async () => {
|
|
2867
|
+
let transport;
|
|
2868
|
+
try {
|
|
2869
|
+
transport = await openClientTransport(endpoint);
|
|
2870
|
+
const baseContext = { ...targetOptions, endpoint, transport };
|
|
2871
|
+
const executeTool = (toolName, toolArgs, ownedTabId) => {
|
|
2872
|
+
const context = ownedTabId
|
|
2873
|
+
? { tabId: ownedTabId, admission: targetOptions.admission, endpoint, transport }
|
|
2874
|
+
: baseContext;
|
|
2875
|
+
return sendDoRequest(toolName, toolArgs, context);
|
|
2876
|
+
};
|
|
2877
|
+
const result = await runExtraction({
|
|
2878
|
+
...settings,
|
|
2879
|
+
executeTool,
|
|
2880
|
+
onEvent: (event) => {
|
|
2881
|
+
if (wantJson) return;
|
|
2882
|
+
if (event.type === "attempt" && event.of > 1) console.error(`[surf] extract attempt ${event.attempt}/${event.of}`);
|
|
2883
|
+
if (event.type === "attempt-failed" && event.retryable) console.error(`[surf] attempt ${event.attempt} failed (${event.error}); retrying with a fresh tab`);
|
|
2884
|
+
},
|
|
2885
|
+
});
|
|
2886
|
+
if (wantJson) {
|
|
2887
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2888
|
+
} else {
|
|
2889
|
+
console.log(renderExtractionMarkdown(result.data, result.rows, { title: url ? `Extraction from ${url}` : "Extraction" }));
|
|
2890
|
+
if (result.tabId) console.error(`[surf] tab ${result.tabId} left open (--keep-tab)`);
|
|
2891
|
+
}
|
|
2892
|
+
return 0;
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
fail(error.code || "extraction_failed", error.message, error.details);
|
|
2895
|
+
} finally {
|
|
2896
|
+
await transport?.close();
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
|
|
2900
|
+
runExtract().then((exitCode) => process.exit(exitCode));
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2573
2904
|
// Handle workflow management commands
|
|
2574
2905
|
if (args[0] === "workflow.list") {
|
|
2575
2906
|
const workflows = listWorkflows();
|
|
@@ -2981,6 +3312,17 @@ if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
|
2981
3312
|
}
|
|
2982
3313
|
}
|
|
2983
3314
|
|
|
3315
|
+
if ((tool === "js" || tool === "frame.js") && toolArgs.options !== undefined) {
|
|
3316
|
+
try {
|
|
3317
|
+
if (typeof toolArgs.code !== "string") throw new Error("--options needs code (inline or --file)");
|
|
3318
|
+
toolArgs.code = applyOptionsPrelude(toolArgs.code, toolArgs.options);
|
|
3319
|
+
delete toolArgs.options;
|
|
3320
|
+
} catch (e) {
|
|
3321
|
+
console.error(`Error: ${e.message}`);
|
|
3322
|
+
process.exit(1);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
2984
3326
|
if (tool === "batch" && toolArgs.file) {
|
|
2985
3327
|
try {
|
|
2986
3328
|
const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
|
|
@@ -3630,7 +3972,25 @@ async function handleResponse(response) {
|
|
|
3630
3972
|
socket.end();
|
|
3631
3973
|
process.exit(0);
|
|
3632
3974
|
}
|
|
3633
|
-
|
|
3975
|
+
// Host tool-response errors carry codes separately from their display text.
|
|
3976
|
+
const errorCode = typeof response.error.code === "string" ? response.error.code : null;
|
|
3977
|
+
const [firstLine, ...restLines] = errContent.split("\n");
|
|
3978
|
+
const display = errorCode && !firstLine.includes(`[${errorCode}]`)
|
|
3979
|
+
? [`${firstLine} [${errorCode}]`, ...restLines].join("\n")
|
|
3980
|
+
: errContent;
|
|
3981
|
+
console.error("Error:", display);
|
|
3982
|
+
if (wantJson) {
|
|
3983
|
+
// details repeats code/message when the error serialises itself; keep the rest.
|
|
3984
|
+
const { code: _code, message: _message, ...details } =
|
|
3985
|
+
response.error.details && typeof response.error.details === "object" ? response.error.details : {};
|
|
3986
|
+
console.log(JSON.stringify({
|
|
3987
|
+
error: {
|
|
3988
|
+
code: errorCode || "error",
|
|
3989
|
+
message: typeof response.error.message === "string" ? response.error.message : firstLine,
|
|
3990
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
3991
|
+
},
|
|
3992
|
+
}, null, 2));
|
|
3993
|
+
}
|
|
3634
3994
|
|
|
3635
3995
|
if (autoCapture) {
|
|
3636
3996
|
await performAutoCapture();
|
|
@@ -3822,6 +4182,62 @@ async function handleResponse(response) {
|
|
|
3822
4182
|
}
|
|
3823
4183
|
console.log("\nUsage: surf emulate.device \"<device name>\"");
|
|
3824
4184
|
console.log('Reset: surf emulate.device "reset"');
|
|
4185
|
+
} else if (tool === "wait.ready" || tool === "page.readiness") {
|
|
4186
|
+
const lines = [`state: ${data?.state ?? "unknown"}`];
|
|
4187
|
+
if (data?.href) lines.push(`url: ${data.href}`);
|
|
4188
|
+
if (data?.title) lines.push(`title: ${data.title}`);
|
|
4189
|
+
if (typeof data?.waited === "number") lines.push(`waited: ${data.waited}ms (${data.polls} poll${data.polls === 1 ? "" : "s"})`);
|
|
4190
|
+
if (data?.accepted) lines.push("accepted: negative state returned because of --accept");
|
|
4191
|
+
for (const item of Array.isArray(data?.evidence) ? data.evidence : []) lines.push(`- ${item}`);
|
|
4192
|
+
console.log(lines.join("\n"));
|
|
4193
|
+
} else if (tool === "frame.diagnose" && data?.counts) {
|
|
4194
|
+
// Keep frame URLs readable: embed runners carry kilobyte-long state
|
|
4195
|
+
// parameters that bury the report (use --json for the full URLs).
|
|
4196
|
+
const abbreviateUrl = (url, max = 100) => {
|
|
4197
|
+
if (typeof url !== "string" || url.length <= max) return url;
|
|
4198
|
+
try {
|
|
4199
|
+
const parsed = new URL(url);
|
|
4200
|
+
const base = `${parsed.origin}${parsed.pathname}`;
|
|
4201
|
+
const trailing = url.length - base.length;
|
|
4202
|
+
if (trailing > 0 && base.length <= max - 12) return `${base}?...(+${trailing} chars)`;
|
|
4203
|
+
} catch {}
|
|
4204
|
+
return `${url.slice(0, max - 3)}...`;
|
|
4205
|
+
};
|
|
4206
|
+
const lines = [];
|
|
4207
|
+
lines.push(`Frame diagnosis for ${data.mainPage?.href ?? "?"}${data.mainPage?.title ? ` (${data.mainPage.title})` : ""}`);
|
|
4208
|
+
lines.push(`DOM iframes: ${data.counts.domIframes}, extension frames: ${data.counts.extensionFrames} (incl. main), CDP frames: ${data.counts.cdpFrames}`);
|
|
4209
|
+
if (Array.isArray(data.domIframes) && data.domIframes.length > 0) {
|
|
4210
|
+
lines.push("", "DOM iframes:");
|
|
4211
|
+
for (const f of data.domIframes) {
|
|
4212
|
+
const flags = [
|
|
4213
|
+
f.blank ? "blank" : null,
|
|
4214
|
+
f.crossOrigin ? "cross-origin" : null,
|
|
4215
|
+
f.scriptsBlocked ? "scripts-blocked" : null,
|
|
4216
|
+
f.zeroSize ? "0-size" : null,
|
|
4217
|
+
].filter(Boolean).join(",");
|
|
4218
|
+
const links = [
|
|
4219
|
+
f.extensionFrameIds?.length ? `ext ${f.extensionFrameIds.join("/")}` : "ext -",
|
|
4220
|
+
f.cdpFrameIds?.length ? `cdp ${f.cdpFrameIds.join("/")}` : "cdp -",
|
|
4221
|
+
].join(", ");
|
|
4222
|
+
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}`);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (Array.isArray(data.extensionFrames) && data.extensionFrames.length > 0) {
|
|
4226
|
+
lines.push("", "Extension frames (frame.switch indexes, webNavigation ids):");
|
|
4227
|
+
for (const f of data.extensionFrames) {
|
|
4228
|
+
const reach = f.contentScriptReachable ? "content-script ok" : `content-script unreachable${f.contentScriptError ? ` (${f.contentScriptError})` : ""}`;
|
|
4229
|
+
lines.push(` ${f.isMain ? "main" : `[${f.switchIndex}]`} #${f.frameId}${f.isMain ? "" : ` parent ${f.parentFrameId}`} ${abbreviateUrl(f.url)}${f.crossOrigin ? " [cross-origin]" : ""} - ${reach}`);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
if (Array.isArray(data.cdpFrames) && data.cdpFrames.length > 0) {
|
|
4233
|
+
lines.push("", "CDP frames (frame.js ids):");
|
|
4234
|
+
for (const f of data.cdpFrames) {
|
|
4235
|
+
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("/")}` : ""}`);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
lines.push("", Array.isArray(data.warnings) && data.warnings.length > 0 ? "Warnings:" : "No warnings.");
|
|
4239
|
+
for (const w of Array.isArray(data.warnings) ? data.warnings : []) lines.push(` - ${w}`);
|
|
4240
|
+
console.log(lines.join("\n"));
|
|
3825
4241
|
} else if (tool === "js") {
|
|
3826
4242
|
if (data?.result !== undefined) {
|
|
3827
4243
|
const val = data.result.value ?? data.result;
|