surf-cli 2.11.0 → 2.13.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 CHANGED
@@ -230,9 +230,14 @@ surf read --compact # Remove empty structural elements
230
230
  surf read --depth 3 --compact # Both (60% smaller output)
231
231
  surf read --max-bytes 2000 # Cap visible text on a UTF-8 byte boundary
232
232
  surf page.text # Raw text content only
233
+ surf page.html # Rendered document HTML
234
+ surf page.html --strip-scripts > artifact.html # Save a safe static Claude artifact
235
+ surf page.save --selector "#artifact" --strip-scripts --output artifact.html # Save one rendered element
233
236
  surf page.state # Modals, loading state, scroll position
234
237
  ```
235
238
 
239
+ Use `surf page.html --strip-scripts` after the page loads when you need a static export of a Claude artifact or other rendered DOM. Use `--selector <css>` to export one element. Both commands target the active frame when `frame.switch` is active.
240
+
236
241
  Element refs (`e1`, `e2`, `e3`...) are stable identifiers from the accessibility tree - semantic, predictable, and resilient to DOM changes.
237
242
 
238
243
  ### Semantic Locators
@@ -485,6 +490,12 @@ surf aistudio "explain quantum computing"
485
490
  surf aistudio "redteam this" --with-page # Include page context
486
491
  surf aistudio "quick answer" --model gemini-3-flash-preview # Model selection
487
492
 
493
+ # Kimi (queries kimi.com - Moonshot K-series - using your browser login)
494
+ surf kimi "explain quantum computing"
495
+ surf kimi "summarize" --with-page # Include page context
496
+ surf kimi "quick answer" --model thinking # Models: instant (default), thinking, high
497
+ surf kimi --validate # Check kimi.com UI and available models
498
+
488
499
  # AI Studio App Builder (generates full web apps from a prompt)
489
500
  surf aistudio.build "build a portfolio site"
490
501
  surf aistudio.build "todo app" --model gemini-3.1-pro-preview # Model override
@@ -707,6 +718,21 @@ surf use page read --json
707
718
  surf use <site> <write-op> --write --resource-id 123
708
719
  ```
709
720
 
721
+ Playbook read ops can also use a trusted `script` strategy when fixed JSON steps are too rigid. Scripts require `--allow-script` at run time. Only run scripts from playbooks you trust. This is not a security sandbox.
722
+
723
+ The script gets `input`, `tools.run`, `tools.all`, `tools.ref`/`refs`, `emit`, and `console`. Tool calls still use Surf workflow step behavior, including auto-waits unless `autoWait` is `false`.
724
+
725
+ ```json
726
+ {
727
+ "using": "script",
728
+ "script": [
729
+ "const page = await tools.run('page', { tool: 'page.text', args: {} });",
730
+ "const clicked = await tools.all(input.selectors.map((selector) => ({ key: selector.slice(1), tool: 'click', args: { selector } })));",
731
+ "return { page: page.output, clicked: clicked.map((link) => link.output) };"
732
+ ]
733
+ }
734
+ ```
735
+
710
736
  Project playbooks in `./.surf/playbooks/` override user playbooks in `~/.surf/playbooks/`; built-ins are the final fallback. `show` reports the selected source. Provider compatibility commands continue to use their validated command paths until provider playbooks have real login-flow validation.
711
737
 
712
738
  Author a playbook from redacted recent activity or an explicit evidence record:
@@ -932,6 +958,19 @@ cp -r skills/surf ~/.pi/agent/skills/
932
958
 
933
959
  See [`skills/README.md`](skills/README.md) for details.
934
960
 
961
+ ### Pi extension
962
+
963
+ Surf also includes an optional Pi extension. Install or load Surf as a Pi package, or load it from a checkout:
964
+
965
+ ```bash
966
+ pi install npm:surf-cli
967
+ pi -e /path/to/surf-cli/pi-extension/surf.ts
968
+ ```
969
+
970
+ It registers `surf_read`, `surf_screenshot`, `surf_click`, `surf_type`, `surf_tool`, and the `surf_oracle_*` tools. Browser calls use Surf's native-host socket, not shell commands. If `pi-subagents/background-work` is installed, the extension also reports active oracle jobs started by that Pi session. Pi still loads the browser tools when pi-subagents is not installed.
971
+
972
+ Surf agents share one browser session. Use read tools for parallel scouts when possible. `surf_click` and `surf_type` can interfere with another agent's browser actions. Browser leases are not available yet.
973
+
935
974
  ## Development
936
975
 
937
976
  ```bash
package/native/cli.cjs CHANGED
@@ -355,6 +355,22 @@ const TOOLS = {
355
355
  { cmd: 'aistudio "quick answer" --model gemini-3-flash-preview', desc: "Model selection" },
356
356
  ]
357
357
  },
358
+ "kimi": {
359
+ desc: "Query Kimi AI (kimi.com, Moonshot K-series) using your browser session",
360
+ args: ["query"],
361
+ opts: {
362
+ "with-page": "Include current page context",
363
+ model: "Model: instant (default), thinking, high - or any model label shown in kimi.com's model picker",
364
+ timeout: "Timeout in seconds (default: 300)",
365
+ validate: "Check kimi.com UI and available models (no query sent)"
366
+ },
367
+ examples: [
368
+ { cmd: 'kimi "explain quantum computing"', desc: "Basic query" },
369
+ { cmd: 'kimi "summarize" --with-page', desc: "With page context" },
370
+ { cmd: 'kimi "deep dive" --model thinking', desc: "Try the Thinking model" },
371
+ { cmd: 'kimi --validate', desc: "Check UI and list available models" },
372
+ ]
373
+ },
358
374
  "aistudio.build": {
359
375
  desc: "Build an app via Google AI Studio App Builder (uses browser session)",
360
376
  args: ["query"],
@@ -533,7 +549,7 @@ const TOOLS = {
533
549
  "scroll": {
534
550
  desc: "Scroll in direction",
535
551
  args: ["direction", "pixels"],
536
- opts: { direction: "up|down|left|right", amount: "Scroll amount (1-10)" },
552
+ opts: { direction: "up|down|left|right", amount: "Scroll amount in 100 px steps (1-10)" },
537
553
  examples: [
538
554
  { cmd: "scroll down 800", desc: "Scroll down 800px" },
539
555
  { cmd: "scroll --direction down --amount 3", desc: "Scroll down" },
@@ -576,6 +592,18 @@ const TOOLS = {
576
592
  },
577
593
  "read": { desc: "Alias for page.read", args: [], alias: "page.read" },
578
594
  "page.text": { desc: "Extract all text from page", args: [] },
595
+ "page.html": {
596
+ desc: "Print rendered document HTML",
597
+ args: [],
598
+ opts: { selector: "Export matching CSS selector", "strip-scripts": "Remove script elements" },
599
+ examples: [{ cmd: "page.html", desc: "Print current document HTML" }],
600
+ },
601
+ "page.save": {
602
+ desc: "Save rendered document HTML",
603
+ args: [],
604
+ opts: { output: "File path", selector: "Export matching CSS selector", "strip-scripts": "Remove script elements" },
605
+ examples: [{ cmd: "page.save --output page.html", desc: "Save current document HTML" }],
606
+ },
579
607
  "page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
580
608
  }
581
609
  },
@@ -1464,7 +1492,7 @@ const ALL_SOCKET_TOOLS = [
1464
1492
  "click_type", "click_type_submit", "type", "key", "type_submit",
1465
1493
  "scroll", "scroll_to", "hover", "left_click_drag", "drag", "wait",
1466
1494
  "computer",
1467
- "page.read", "page.text", "page.state",
1495
+ "page.read", "page.text", "page.html", "page.save", "page.state",
1468
1496
  "locate.role", "locate.text", "locate.label",
1469
1497
  "tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
1470
1498
  "tab.group", "tab.ungroup", "tab.groups", "tab.reload",
@@ -2613,6 +2641,7 @@ const PRIMARY_ARG_MAP = {
2613
2641
  grok: "query",
2614
2642
  aistudio: "query",
2615
2643
  "aistudio.build": "query",
2644
+ kimi: "query",
2616
2645
  navigate: "url",
2617
2646
  go: "url",
2618
2647
  js: "code",
@@ -2818,11 +2847,16 @@ if (tool === "network.export" && outputPath !== undefined) {
2818
2847
  toolArgs.output = outputPath;
2819
2848
  }
2820
2849
 
2821
- if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
2850
+ if ((tool === "screenshot" || tool === "record" || tool === "perf-audit" || tool === "page.save") && outputPath && typeof outputPath !== "string") {
2822
2851
  console.error("Error: --output requires a file path");
2823
2852
  process.exit(1);
2824
2853
  }
2825
2854
 
2855
+ if (tool === "page.save" && !outputPath) {
2856
+ console.error("Error: page.save requires --output <path>");
2857
+ process.exit(1);
2858
+ }
2859
+
2826
2860
  if (tool === "screenshot" && outputPath) {
2827
2861
  toolArgs.savePath = outputPath;
2828
2862
  if (options.full) toolArgs.full = true;
@@ -3294,6 +3328,21 @@ async function handleResponse(response) {
3294
3328
  data = { response: data };
3295
3329
  }
3296
3330
 
3331
+ if (tool === 'kimi' && typeof data === 'string') {
3332
+ data = { response: data };
3333
+ }
3334
+
3335
+ if (tool === "page.save" && typeof data?.html === "string") {
3336
+ const saveTo = path.resolve(outputPath);
3337
+ fs.mkdirSync(path.dirname(saveTo), { recursive: true });
3338
+ fs.writeFileSync(saveTo, data.html);
3339
+ if (!wantJson) {
3340
+ console.log(`Saved rendered page HTML to ${saveTo}`);
3341
+ socket.end();
3342
+ process.exit(0);
3343
+ }
3344
+ }
3345
+
3297
3346
  if (tool === "perf-audit" && outputPath) {
3298
3347
  const saveTo = path.resolve(outputPath);
3299
3348
  fs.mkdirSync(path.dirname(saveTo), { recursive: true });
@@ -3375,6 +3424,8 @@ async function handleResponse(response) {
3375
3424
  console.log(data.pageContent);
3376
3425
  } else if (tool === "page.text" && data?.text) {
3377
3426
  console.log(data.text);
3427
+ } else if (tool === "page.html" && typeof data?.html === "string") {
3428
+ console.log(data.html);
3378
3429
  } else if (tool === "emulate.device" && data?.devices) {
3379
3430
  console.log("Available devices:\n");
3380
3431
  const devices = data.devices;
@@ -3500,7 +3551,18 @@ async function handleResponse(response) {
3500
3551
  if (meta.length > 0) {
3501
3552
  console.error(`[${meta.join(" | ")}]`);
3502
3553
  }
3503
- } else if (tool === "perplexity" && data?.response) {
3554
+ } else if (tool === "kimi" && data?.response) {
3555
+ console.log(data.response);
3556
+ const meta = [];
3557
+ if (data.model) meta.push(data.model);
3558
+ if (data.partial) meta.push("partial");
3559
+ if (Number.isFinite(data.tookMs)) meta.push(`${(data.tookMs / 1000).toFixed(1)}s`);
3560
+ if (meta.length > 0) console.error(`\n[${meta.join(' | ')}]`);
3561
+ if (data.url) console.error(`URL: ${data.url}`);
3562
+ if (data.warnings?.length) {
3563
+ for (const w of data.warnings) console.warn(`Warning: ${w}`);
3564
+ }
3565
+ } else if (tool === "perplexity" && data?.response) {
3504
3566
  console.log(data.response);
3505
3567
  const meta = [];
3506
3568
  if (data.sources) meta.push(`${data.sources} sources`);
@@ -77,8 +77,23 @@ function formatToolContent(result, log = () => {}, options = {}) {
77
77
  return text(output);
78
78
  }
79
79
 
80
- // Handle Grok validation results
81
- if (result.authenticated !== undefined && result.models !== undefined && result.expectedModels !== undefined) {
80
+ // Handle Kimi validation results (must precede the Grok branch - same field names)
81
+ if (result.kimiValidate) {
82
+ let output = "## Kimi Validation Results\n\n";
83
+ output += `**Authenticated:** ${result.authenticated ? 'Yes' : 'No'}\n`;
84
+ output += `**Input Field:** ${result.inputFound ? 'Found' : 'Not Found'}\n`;
85
+ output += `**Send Button:** ${result.sendButtonFound ? 'Found' : 'Not Found'}\n\n`;
86
+ output += `**Available Models:** ${result.models.length > 0 ? result.models.join(', ') : 'None found'}\n`;
87
+ output += `**Expected Models:** ${result.expectedModels.join(', ')}\n\n`;
88
+ if (result.errors && result.errors.length > 0) {
89
+ output += `**Errors:**\n${result.errors.map(e => `- ${e}`).join('\n')}\n\n`;
90
+ }
91
+ output += `*Completed in ${result.tookMs}ms*`;
92
+ return text(output);
93
+ }
94
+
95
+ // Handle Grok validation results
96
+ if (result.authenticated !== undefined && result.models !== undefined && result.expectedModels !== undefined) {
82
97
  let output = "## Grok Validation Results\n\n";
83
98
  output += `**Authenticated:** ${result.authenticated ? 'Yes' : 'No'}\n`;
84
99
  output += `**Premium:** ${result.premium ? 'Yes' : 'No'}\n`;
@@ -936,6 +951,18 @@ function mapToolToMessage(tool, args, tabId) {
936
951
  }
937
952
  case "page.text":
938
953
  return { type: "GET_PAGE_TEXT", ...baseMsg };
954
+ case "page.html":
955
+ case "page.save": {
956
+ if (a.selector !== undefined && (typeof a.selector !== "string" || a.selector.length === 0)) {
957
+ throw new Error("selector must be a non-empty string");
958
+ }
959
+ return {
960
+ type: "GET_PAGE_HTML",
961
+ selector: a.selector,
962
+ stripScripts: a["strip-scripts"] === true,
963
+ ...baseMsg,
964
+ };
965
+ }
939
966
  case "page.state":
940
967
  return { type: "PAGE_STATE", ...baseMsg };
941
968
  case "locate.role":
@@ -1188,6 +1215,22 @@ function mapToolToMessage(tool, args, tabId) {
1188
1215
  ...baseMsg,
1189
1216
  };
1190
1217
  }
1218
+ case "kimi":
1219
+ if (a.validate) {
1220
+ return {
1221
+ type: "KIMI_VALIDATE",
1222
+ ...baseMsg
1223
+ };
1224
+ }
1225
+ if (!a.query) throw new Error("query required");
1226
+ return {
1227
+ type: "KIMI_QUERY",
1228
+ query: a.query,
1229
+ model: a.model,
1230
+ withPage: a["with-page"],
1231
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
1232
+ ...baseMsg
1233
+ };
1191
1234
  case "window.new":
1192
1235
  return {
1193
1236
  type: "WINDOW_NEW",
@@ -22,6 +22,7 @@ const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
22
22
  chatgpt: 2700,
23
23
  gemini: 300,
24
24
  grok: 300,
25
+ kimi: 300,
25
26
  perplexity: 120,
26
27
  "oracle.result": 300,
27
28
  "playbook.run": 600,
package/native/host.cjs CHANGED
@@ -12,6 +12,7 @@ const chatgptClient = require("./chatgpt-client.cjs");
12
12
  const geminiClient = require("./gemini-client.cjs");
13
13
  const perplexityClient = require("./perplexity-client.cjs");
14
14
  const grokClient = require("./grok-client.cjs");
15
+ const kimiClient = require("./kimi-client.cjs");
15
16
  const aistudioClient = require("./aistudio-client.cjs");
16
17
  const aistudioBuild = require("./aistudio-build.cjs");
17
18
  const { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage } = require("./host-helpers.cjs");
@@ -523,6 +524,7 @@ async function runHostPlaybook(msg, request) {
523
524
  onEvent: report,
524
525
  beforeDispatch: async () => updateReceipt(receipt, "dispatched"),
525
526
  afterDispatch: async ({ status, error }) => updateReceipt(receipt, status, { error }),
527
+ allowScript: params.allowScript === true,
526
528
  });
527
529
  }
528
530
 
@@ -1236,13 +1238,115 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
1236
1238
  }
1237
1239
  sendToolResponse(socket, originalId, result, null);
1238
1240
  }).catch((err) => {
1239
- sendToolResponse(socket, originalId, null, err.message);
1240
- });
1241
-
1242
- return;
1243
- }
1241
+ sendToolResponse(socket, originalId, null, err.message);
1242
+ });
1243
+
1244
+ return;
1245
+ }
1246
+
1247
+ if (extensionMsg.type === "KIMI_QUERY") {
1248
+ const { query, model, withPage, timeout } = extensionMsg;
1249
+
1250
+ queueAiRequest(async () => {
1251
+ // 1. Get page context if requested
1252
+ let pageContext = null;
1253
+ if (withPage) {
1254
+ const pageResult = await requestCallExtension(
1255
+ requestContext,
1256
+ "get_page_text",
1257
+ { type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
1258
+ 45000,
1259
+ );
1260
+ if (pageResult && !pageResult.error) {
1261
+ pageContext = {
1262
+ url: pageResult.url,
1263
+ text: pageResult.text || pageResult.pageContent || ""
1264
+ };
1265
+ }
1266
+ }
1267
+
1268
+ // 2. Build full prompt
1269
+ let fullPrompt = query || "";
1270
+ if (pageContext) {
1271
+ fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${fullPrompt}`;
1272
+ }
1273
+
1274
+ // 3. Call Kimi client (uses generic browser primitives, no provider CDP types)
1275
+ const result = await kimiClient.query({
1276
+ prompt: fullPrompt,
1277
+ extractionPrompt: query,
1278
+ signal: requestContext.signal,
1279
+ model: model,
1280
+ timeout: timeout || 300000,
1281
+ createTab: () => requestCallExtension(
1282
+ requestContext,
1283
+ "create_tab",
1284
+ { type: "NEW_TAB", url: "https://www.kimi.com/" },
1285
+ ),
1286
+ closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
1287
+ jsEval: (tabId, code) => requestCallExtension(
1288
+ requestContext,
1289
+ "js_eval",
1290
+ { type: "EXECUTE_JAVASCRIPT", tabId, code },
1291
+ ),
1292
+ log: (msg) => log(`[kimi] ${msg}`)
1293
+ });
1294
+
1295
+ return result;
1296
+ }).then((result) => {
1297
+ const response = {
1298
+ response: result.response,
1299
+ model: result.model,
1300
+ tookMs: result.tookMs
1301
+ };
1302
+ if (result.partial) {
1303
+ response.partial = true;
1304
+ }
1305
+ if (result.warnings && result.warnings.length > 0) {
1306
+ response.warnings = result.warnings;
1307
+ }
1308
+ if (result.modelSelectionFailed) {
1309
+ response.modelSelectionFailed = true;
1310
+ }
1311
+ if (result.url) {
1312
+ response.url = result.url;
1313
+ }
1314
+ sendToolResponse(socket, originalId, response, null);
1315
+ }).catch((err) => {
1316
+ sendToolResponse(socket, originalId, null, err.message);
1317
+ });
1318
+
1319
+ return;
1320
+ }
1321
+
1322
+ if (extensionMsg.type === "KIMI_VALIDATE") {
1323
+ queueAiRequest(async () => {
1324
+ const result = await kimiClient.validate({
1325
+ signal: requestContext.signal,
1326
+ createTab: () => requestCallExtension(
1327
+ requestContext,
1328
+ "create_tab",
1329
+ { type: "NEW_TAB", url: "https://www.kimi.com/" },
1330
+ ),
1331
+ closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
1332
+ jsEval: (tabId, code) => requestCallExtension(
1333
+ requestContext,
1334
+ "js_eval",
1335
+ { type: "EXECUTE_JAVASCRIPT", tabId, code },
1336
+ ),
1337
+ log: (msg) => log(`[kimi] ${msg}`)
1338
+ });
1339
+ return result;
1340
+ }).then((result) => {
1341
+ sendToolResponse(socket, originalId, result, null);
1342
+ }).catch((err) => {
1343
+ sendToolResponse(socket, originalId, null, err.message);
1344
+ });
1345
+
1346
+ return;
1347
+ }
1244
1348
 
1245
- if (extensionMsg.type === "AISTUDIO_QUERY") {
1349
+ if (extensionMsg.type === "AISTUDIO_QUERY") {
1246
1350
  const { query, model, withPage, timeout } = extensionMsg;
1247
1351
 
1248
1352
  queueAiRequest(async () => {