surf-cli 2.10.0 → 2.12.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/native/cli.cjs CHANGED
@@ -19,6 +19,11 @@ const {
19
19
  const { executeDoSteps } = require("./do-executor.cjs");
20
20
  const { openClientTransport } = require("./client-transport.cjs");
21
21
  const { version: VERSION } = require("../package.json");
22
+ const {
23
+ formatOracleError,
24
+ formatOracleOutput,
25
+ handleOracleCli,
26
+ } = require("./oracle-cli.cjs");
22
27
  const { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser } = require("./playbook-cli.cjs");
23
28
 
24
29
  const IS_WIN = process.platform === "win32";
@@ -73,6 +78,28 @@ function installBrowserLock({ noLock, timeoutMs }, endpoint) {
73
78
  });
74
79
  }
75
80
 
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
+
76
103
  // Cross-platform image resize (macOS: sips, Linux: ImageMagick)
77
104
  function resizeImage(filePath, maxSize) {
78
105
  const platform = process.platform;
@@ -153,6 +180,28 @@ try {
153
180
  process.exit(1);
154
181
  }
155
182
 
183
+ if (args[0] === "oracle") {
184
+ handleOracleCli(args, {
185
+ endpoint,
186
+ cwd: process.cwd(),
187
+ withBrowserLock: (operation) => runWithBrowserLock(
188
+ parseBrowserLockOptions(args.includes("--no-lock")),
189
+ endpoint,
190
+ operation,
191
+ ),
192
+ })
193
+ .then((result) => {
194
+ if (!result.handled) throw new Error("Oracle command was not handled");
195
+ if (result.value !== undefined) console.log(formatOracleOutput(result.value, result.json));
196
+ process.exit(0);
197
+ })
198
+ .catch((error) => {
199
+ console.error(formatOracleError(error, args.includes("--json")));
200
+ process.exit(1);
201
+ });
202
+ return;
203
+ }
204
+
156
205
  if (["playbook", "pb", "use"].includes(args[0])) {
157
206
  if (playbookCommandNeedsBrowser(args)) {
158
207
  installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
@@ -484,7 +533,7 @@ const TOOLS = {
484
533
  "scroll": {
485
534
  desc: "Scroll in direction",
486
535
  args: ["direction", "pixels"],
487
- opts: { direction: "up|down|left|right", amount: "Scroll amount (1-10)" },
536
+ opts: { direction: "up|down|left|right", amount: "Scroll amount in 100 px steps (1-10)" },
488
537
  examples: [
489
538
  { cmd: "scroll down 800", desc: "Scroll down 800px" },
490
539
  { cmd: "scroll --direction down --amount 3", desc: "Scroll down" },
@@ -527,6 +576,18 @@ const TOOLS = {
527
576
  },
528
577
  "read": { desc: "Alias for page.read", args: [], alias: "page.read" },
529
578
  "page.text": { desc: "Extract all text from page", args: [] },
579
+ "page.html": {
580
+ desc: "Print rendered document HTML",
581
+ args: [],
582
+ opts: { selector: "Export matching CSS selector", "strip-scripts": "Remove script elements" },
583
+ examples: [{ cmd: "page.html", desc: "Print current document HTML" }],
584
+ },
585
+ "page.save": {
586
+ desc: "Save rendered document HTML",
587
+ args: [],
588
+ opts: { output: "File path", selector: "Export matching CSS selector", "strip-scripts": "Remove script elements" },
589
+ examples: [{ cmd: "page.save --output page.html", desc: "Save current document HTML" }],
590
+ },
530
591
  "page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
531
592
  }
532
593
  },
@@ -1415,7 +1476,7 @@ const ALL_SOCKET_TOOLS = [
1415
1476
  "click_type", "click_type_submit", "type", "key", "type_submit",
1416
1477
  "scroll", "scroll_to", "hover", "left_click_drag", "drag", "wait",
1417
1478
  "computer",
1418
- "page.read", "page.text", "page.state",
1479
+ "page.read", "page.text", "page.html", "page.save", "page.state",
1419
1480
  "locate.role", "locate.text", "locate.label",
1420
1481
  "tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
1421
1482
  "tab.group", "tab.ungroup", "tab.groups", "tab.reload",
@@ -1496,6 +1557,7 @@ Common Commands:
1496
1557
  search <term> Search for text in page (alias: find)
1497
1558
  window.new <url> Create isolated browser window
1498
1559
  doctor Diagnose native host/socket setup
1560
+ oracle ask <prompt> Start a durable ChatGPT consult
1499
1561
  wait <seconds> Wait N seconds
1500
1562
 
1501
1563
  Quick Examples:
@@ -1555,6 +1617,9 @@ const showFullHelp = () => {
1555
1617
 
1556
1618
  Usage: surf <command> [args] [options]
1557
1619
 
1620
+ Oracle:
1621
+ surf oracle <ask|status|result|follow|list>
1622
+
1558
1623
  Playbooks:
1559
1624
  surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>
1560
1625
  surf use <playbook> <op> [--arg value]
@@ -2765,11 +2830,16 @@ if (tool === "network.export" && outputPath !== undefined) {
2765
2830
  toolArgs.output = outputPath;
2766
2831
  }
2767
2832
 
2768
- if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
2833
+ if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save") && outputPath && typeof outputPath !== "string") {
2769
2834
  console.error("Error: --output requires a file path");
2770
2835
  process.exit(1);
2771
2836
  }
2772
2837
 
2838
+ if (tool === "page.save" && !outputPath) {
2839
+ console.error("Error: page.save requires --output <path>");
2840
+ process.exit(1);
2841
+ }
2842
+
2773
2843
  if (tool === "screenshot" && outputPath) {
2774
2844
  toolArgs.savePath = outputPath;
2775
2845
  if (options.full) toolArgs.full = true;
@@ -3241,6 +3311,17 @@ async function handleResponse(response) {
3241
3311
  data = { response: data };
3242
3312
  }
3243
3313
 
3314
+ if (tool === "page.save" && typeof data?.html === "string") {
3315
+ const saveTo = path.resolve(outputPath);
3316
+ fs.mkdirSync(path.dirname(saveTo), { recursive: true });
3317
+ fs.writeFileSync(saveTo, data.html);
3318
+ if (!wantJson) {
3319
+ console.log(`Saved rendered page HTML to ${saveTo}`);
3320
+ socket.end();
3321
+ process.exit(0);
3322
+ }
3323
+ }
3324
+
3244
3325
  if (tool === "perf-audit" && outputPath) {
3245
3326
  const saveTo = path.resolve(outputPath);
3246
3327
  fs.mkdirSync(path.dirname(saveTo), { recursive: true });
@@ -3322,6 +3403,8 @@ async function handleResponse(response) {
3322
3403
  console.log(data.pageContent);
3323
3404
  } else if (tool === "page.text" && data?.text) {
3324
3405
  console.log(data.text);
3406
+ } else if (tool === "page.html" && typeof data?.html === "string") {
3407
+ console.log(data.html);
3325
3408
  } else if (tool === "emulate.device" && data?.devices) {
3326
3409
  console.log("Available devices:\n");
3327
3410
  const devices = data.devices;
@@ -13,6 +13,21 @@ function normalizeModelString(model) {
13
13
  return String(model || "").trim().toLowerCase();
14
14
  }
15
15
 
16
+ function formatToolError(error) {
17
+ const message = error instanceof Error
18
+ ? error.message
19
+ : typeof error === "string"
20
+ ? error
21
+ : error?.message || String(error);
22
+ const result = { content: [{ type: "text", text: message }] };
23
+ if (error && typeof error === "object") {
24
+ result.message = message;
25
+ if (typeof error.code === "string") result.code = error.code;
26
+ if (typeof error.jobId === "string") result.jobId = error.jobId;
27
+ }
28
+ return result;
29
+ }
30
+
16
31
  /**
17
32
  * Format tool result content for MCP response
18
33
  * @param {*} result - The result object from the extension
@@ -921,6 +936,18 @@ function mapToolToMessage(tool, args, tabId) {
921
936
  }
922
937
  case "page.text":
923
938
  return { type: "GET_PAGE_TEXT", ...baseMsg };
939
+ case "page.html":
940
+ case "page.save": {
941
+ if (a.selector !== undefined && (typeof a.selector !== "string" || a.selector.length === 0)) {
942
+ throw new Error("selector must be a non-empty string");
943
+ }
944
+ return {
945
+ type: "GET_PAGE_HTML",
946
+ selector: a.selector,
947
+ stripScripts: a["strip-scripts"] === true,
948
+ ...baseMsg,
949
+ };
950
+ }
924
951
  case "page.state":
925
952
  return { type: "PAGE_STATE", ...baseMsg };
926
953
  case "locate.role":
@@ -1082,6 +1109,16 @@ function mapToolToMessage(tool, args, tabId) {
1082
1109
  case "history.search":
1083
1110
  if (!a.query) throw new Error("query required");
1084
1111
  return { type: "HISTORY_SEARCH", query: a.query, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 };
1112
+ case "oracle.ask":
1113
+ if (!a.prompt) throw new Error("prompt required");
1114
+ return { ...a, type: "ORACLE_ASK" };
1115
+ case "oracle.status":
1116
+ return { ...a, type: "ORACLE_STATUS" };
1117
+ case "oracle.result":
1118
+ if (!a.id) throw new Error("id required");
1119
+ return { ...a, type: "ORACLE_RESULT" };
1120
+ case "oracle.list":
1121
+ return { type: "ORACLE_LIST" };
1085
1122
  case "chatgpt":
1086
1123
  if (!a.query) throw new Error("query required");
1087
1124
  return {
@@ -1196,4 +1233,4 @@ function mapToolToMessage(tool, args, tabId) {
1196
1233
  }
1197
1234
  }
1198
1235
 
1199
- module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
1236
+ module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage };
@@ -23,6 +23,7 @@ const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
23
23
  gemini: 300,
24
24
  grok: 300,
25
25
  perplexity: 120,
26
+ "oracle.result": 300,
26
27
  "playbook.run": 600,
27
28
  };
28
29
 
package/native/host.cjs CHANGED
@@ -14,7 +14,8 @@ const perplexityClient = require("./perplexity-client.cjs");
14
14
  const grokClient = require("./grok-client.cjs");
15
15
  const aistudioClient = require("./aistudio-client.cjs");
16
16
  const aistudioBuild = require("./aistudio-build.cjs");
17
- const { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage } = require("./host-helpers.cjs");
17
+ const { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage } = require("./host-helpers.cjs");
18
+ const { createOracleHost } = require("./oracle-host.cjs");
18
19
 
19
20
  const IS_WIN = process.platform === "win32";
20
21
  const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
@@ -402,6 +403,15 @@ aiQueue = new BoundedAiQueue({
402
403
  : handler(),
403
404
  });
404
405
 
406
+ const oracleHost = createOracleHost({
407
+ queueAiRequest,
408
+ requestCallExtension,
409
+ buildProviderUploadMessage,
410
+ log,
411
+ });
412
+ const adoptedOracleJobs = oracleHost.adoptOrphans();
413
+ log(`Oracle adoption: ${adoptedOracleJobs.length} job(s); ids=${adoptedOracleJobs.map((job) => job.id).join(",") || "none"}`);
414
+
405
415
  function sendSocket(socket, value, options = {}) {
406
416
  const writer = socketWriters.get(socket);
407
417
  return writer ? writer.send(value, options) : writeFrame(socket, value);
@@ -513,6 +523,7 @@ async function runHostPlaybook(msg, request) {
513
523
  onEvent: report,
514
524
  beforeDispatch: async () => updateReceipt(receipt, "dispatched"),
515
525
  afterDispatch: async ({ status, error }) => updateReceipt(receipt, status, { error }),
526
+ allowScript: params.allowScript === true,
516
527
  });
517
528
  }
518
529
 
@@ -672,8 +683,14 @@ function sendToolResponse(socket, id, result, error) {
672
683
  } catch (transferFailure) {
673
684
  finalError = transferFailure.message;
674
685
  }
675
- if (finalError && request) {
676
- finalError = rewriteTransferPaths(finalError, request.pathRewrites || []);
686
+ const formattedError = finalError ? formatToolError(finalError) : null;
687
+ if (formattedError && request) {
688
+ const rewrittenMessage = rewriteTransferPaths(
689
+ formattedError.content[0].text,
690
+ request.pathRewrites || [],
691
+ );
692
+ formattedError.content[0].text = rewrittenMessage;
693
+ if (formattedError.message) formattedError.message = rewrittenMessage;
677
694
  }
678
695
  if (request?.tool && !request.tool.startsWith("playbook.")) {
679
696
  const metadata = commandMetadata(request.tool);
@@ -697,7 +714,7 @@ function sendToolResponse(socket, id, result, error) {
697
714
  : finalError ? "error" : "completed";
698
715
  await completeOwnedRequest(context, id, outcome);
699
716
  const response = { type: "tool_response", id };
700
- if (finalError) response.error = { content: [{ type: "text", text: finalError }] };
717
+ if (formattedError) response.error = formattedError;
701
718
  else response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
702
719
  if (!context?.closed) await sendSocket(socket, response);
703
720
  })().catch((sendError) => log(`Error sending tool_response: ${sendError.message}`));
@@ -800,6 +817,13 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
800
817
  .catch((error) => sendToolResponse(socket, originalId, null, error.message));
801
818
  return;
802
819
  }
820
+
821
+ if (extensionMsg.type.startsWith("ORACLE_")) {
822
+ Promise.resolve(oracleHost.handle(requestContext, extensionMsg))
823
+ .then((result) => sendToolResponse(socket, originalId, result, null))
824
+ .catch((error) => sendToolResponse(socket, originalId, null, error));
825
+ return;
826
+ }
803
827
 
804
828
  if (extensionMsg.type === "BATCH_EXECUTE") {
805
829
  executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId, requestContext);
@@ -1948,6 +1972,7 @@ const handleClient = (socket) => {
1948
1972
  }
1949
1973
  log(`Handling tool_request: ${msg.method} ${tool}${principal ? ` for ${principal.label}` : ""}`);
1950
1974
  try {
1975
+ if (tool.startsWith("oracle.")) oracleHost.assertLocal(request);
1951
1976
  if (isRemote) {
1952
1977
  await applyRequestTransfers(msg, request, transferState, ensureTransferState);
1953
1978
  }
@@ -1955,7 +1980,7 @@ const handleClient = (socket) => {
1955
1980
  requestStorage.run(request, () => handleToolRequest(msg, socket, request));
1956
1981
  } catch (e) {
1957
1982
  await discardRequestTransfers(msg, transferState);
1958
- sendToolResponse(socket, msg.id || null, null, e.message || "Request failed");
1983
+ sendToolResponse(socket, msg.id || null, null, e?.code ? e : e.message || "Request failed");
1959
1984
  }
1960
1985
  return;
1961
1986
  }