surf-cli 2.5.2 → 2.6.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/host.cjs CHANGED
@@ -10,9 +10,14 @@ const chatgptClient = require("./chatgpt-client.cjs");
10
10
  const geminiClient = require("./gemini-client.cjs");
11
11
  const perplexityClient = require("./perplexity-client.cjs");
12
12
  const grokClient = require("./grok-client.cjs");
13
+ const aistudioClient = require("./aistudio-client.cjs");
14
+ const aistudioBuild = require("./aistudio-build.cjs");
13
15
  const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
14
16
 
15
- const SOCKET_PATH = "/tmp/surf.sock";
17
+ const IS_WIN = process.platform === "win32";
18
+ const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
19
+ const SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
20
+ if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
16
21
 
17
22
  // Cross-platform image resize (macOS: sips, Linux: ImageMagick)
18
23
  function resizeImage(filePath, maxSize) {
@@ -27,12 +32,13 @@ function resizeImage(filePath, maxSize) {
27
32
  const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
28
33
  return { success: true, width, height };
29
34
  } else {
30
- // Linux/other: use ImageMagick (try IM6 first, then IM7)
35
+ // Linux/Windows: use ImageMagick (try IM6 first, then IM7)
36
+ const resizeArg = IS_WIN ? `"${maxSize}x${maxSize}>"` : `${maxSize}x${maxSize}\\>`;
31
37
  try {
32
- execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
38
+ execSync(`convert "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
33
39
  } catch {
34
40
  // IM7 uses 'magick' as main command
35
- execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
41
+ execSync(`magick "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
36
42
  }
37
43
  // Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
38
44
  let sizeInfo;
@@ -73,7 +79,7 @@ async function processAiQueue() {
73
79
  setTimeout(processAiQueue, 2000);
74
80
  }
75
81
  }
76
- const LOG_FILE = "/tmp/surf-host.log";
82
+ const LOG_FILE = path.join(SURF_TMP, "surf-host.log");
77
83
  const AUTH_FILE = path.join(os.homedir(), ".pi", "agent", "auth.json");
78
84
 
79
85
  const DEFAULT_RETRY_OPTIONS = {
@@ -288,9 +294,7 @@ const log = (msg) => {
288
294
 
289
295
  log("Host starting...");
290
296
 
291
- try {
292
- fs.unlinkSync(SOCKET_PATH);
293
- } catch {}
297
+ if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
294
298
 
295
299
  const pendingRequests = new Map();
296
300
  const pendingToolRequests = new Map();
@@ -910,6 +914,198 @@ function handleToolRequest(msg, socket) {
910
914
 
911
915
  return;
912
916
  }
917
+
918
+ if (extensionMsg.type === "AISTUDIO_QUERY") {
919
+ const { query, model, withPage, timeout } = extensionMsg;
920
+
921
+ queueAiRequest(async () => {
922
+ const EXT_CALL_TIMEOUT_MS = 30000;
923
+
924
+ const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => new Promise((resolve, reject) => {
925
+ const id = ++requestCounter;
926
+
927
+ if (msg && msg.type === "AISTUDIO_NEW_TAB") {
928
+ log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/prompts/new_chat")}`);
929
+ }
930
+
931
+ const timeoutId = setTimeout(() => {
932
+ pendingToolRequests.delete(id);
933
+ reject(new Error(`Timeout waiting for extension: ${toolName}`));
934
+ }, timeoutMs);
935
+
936
+ pendingToolRequests.set(id, {
937
+ socket: null,
938
+ originalId: null,
939
+ tool: toolName,
940
+ onComplete: (r) => {
941
+ clearTimeout(timeoutId);
942
+ resolve(r);
943
+ }
944
+ });
945
+
946
+ writeMessage({ ...msg, id });
947
+ });
948
+
949
+ // 1. Get page context if requested
950
+ let pageContext = null;
951
+ if (withPage) {
952
+ const pageResult = await callExtension(
953
+ "get_page_text",
954
+ { type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
955
+ 45000
956
+ );
957
+
958
+ if (pageResult && !pageResult.error) {
959
+ pageContext = {
960
+ url: pageResult.url,
961
+ text: pageResult.text || pageResult.pageContent || ""
962
+ };
963
+ }
964
+ }
965
+
966
+ // 2. Build full prompt
967
+ let fullPrompt = query || "";
968
+ if (pageContext) {
969
+ const MAX_PAGE_CONTEXT_CHARS = 20000;
970
+ const pageText = String(pageContext.text || "");
971
+ const truncated = pageText.length > MAX_PAGE_CONTEXT_CHARS
972
+ ? pageText.slice(0, MAX_PAGE_CONTEXT_CHARS) + "\n\n[...truncated...]"
973
+ : pageText;
974
+
975
+ fullPrompt = `Page: ${pageContext.url}\n\n${truncated}\n\n---\n\n${fullPrompt}`;
976
+ }
977
+
978
+ // 3. Call AI Studio client
979
+ const result = await aistudioClient.query({
980
+ prompt: fullPrompt,
981
+ model: model || undefined,
982
+ timeout: timeout || 300000,
983
+ getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
984
+ createTab: (url) => callExtension(
985
+ "create_tab",
986
+ { type: "AISTUDIO_NEW_TAB", url },
987
+ 45000
988
+ ),
989
+ closeTab: (tabIdToClose) => callExtension(
990
+ "close_tab",
991
+ { type: "AISTUDIO_CLOSE_TAB", tabId: tabIdToClose },
992
+ 45000
993
+ ),
994
+ cdpEvaluate: (tabId, expression) => callExtension(
995
+ "cdp_evaluate",
996
+ { type: "AISTUDIO_EVALUATE", tabId, expression }
997
+ ),
998
+ cdpCommand: (tabId, method, params) => callExtension(
999
+ "cdp_command",
1000
+ { type: "AISTUDIO_CDP_COMMAND", tabId, method, params }
1001
+ ),
1002
+ readNetworkEntries: (tabIdToRead) => callExtension(
1003
+ "read_network_entries",
1004
+ {
1005
+ type: "READ_NETWORK_REQUESTS",
1006
+ tabId: tabIdToRead,
1007
+ full: true,
1008
+ limit: 100,
1009
+ urlPattern: "MakerSuiteService/GenerateContent"
1010
+ },
1011
+ 45000
1012
+ ),
1013
+ log: (msg) => log(`[aistudio] ${msg}`)
1014
+ });
1015
+
1016
+ return result;
1017
+ }).then((result) => {
1018
+ const payload = {
1019
+ response: result.response,
1020
+ model: result.model,
1021
+ thinkingTime: result.thinkingTime,
1022
+ tookMs: result.tookMs
1023
+ };
1024
+
1025
+ sendToolResponse(socket, originalId, { output: JSON.stringify(payload) }, null);
1026
+ }).catch((err) => {
1027
+ sendToolResponse(socket, originalId, null, err.message);
1028
+ });
1029
+
1030
+ return;
1031
+ }
1032
+
1033
+ if (extensionMsg.type === "AISTUDIO_BUILD") {
1034
+ const { query, model, output, keepOpen, timeout } = extensionMsg;
1035
+
1036
+ queueAiRequest(async () => {
1037
+ const EXT_CALL_TIMEOUT_MS = 30000;
1038
+
1039
+ const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => new Promise((resolve, reject) => {
1040
+ const id = ++requestCounter;
1041
+
1042
+ if (msg && msg.type === "AISTUDIO_NEW_TAB") {
1043
+ log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/apps")}`);
1044
+ }
1045
+
1046
+ const timeoutId = setTimeout(() => {
1047
+ pendingToolRequests.delete(id);
1048
+ reject(new Error(`Timeout waiting for extension: ${toolName}`));
1049
+ }, timeoutMs);
1050
+
1051
+ pendingToolRequests.set(id, {
1052
+ socket: null,
1053
+ originalId: null,
1054
+ tool: toolName,
1055
+ onComplete: (r) => {
1056
+ clearTimeout(timeoutId);
1057
+ resolve(r);
1058
+ }
1059
+ });
1060
+
1061
+ writeMessage({ ...msg, id });
1062
+ });
1063
+
1064
+ const result = await aistudioBuild.build({
1065
+ prompt: query,
1066
+ model: model || undefined,
1067
+ output,
1068
+ keepOpen,
1069
+ timeout: timeout || 600000,
1070
+ getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
1071
+ createTab: (url) => callExtension(
1072
+ "create_tab",
1073
+ { type: "AISTUDIO_NEW_TAB", url },
1074
+ 45000
1075
+ ),
1076
+ closeTab: (tabIdToClose) => callExtension(
1077
+ "close_tab",
1078
+ { type: "AISTUDIO_CLOSE_TAB", tabId: tabIdToClose },
1079
+ 45000
1080
+ ),
1081
+ cdpEvaluate: (tabId, expression) => callExtension(
1082
+ "cdp_evaluate",
1083
+ { type: "AISTUDIO_EVALUATE", tabId, expression }
1084
+ ),
1085
+ cdpCommand: (tabId, method, params) => callExtension(
1086
+ "cdp_command",
1087
+ { type: "AISTUDIO_CDP_COMMAND", tabId, method, params }
1088
+ ),
1089
+ searchDownloads: async (params) => {
1090
+ const result = await callExtension(
1091
+ "downloads_search",
1092
+ { type: "DOWNLOADS_SEARCH", searchParams: params },
1093
+ 10000
1094
+ );
1095
+ return result?.downloads || [];
1096
+ },
1097
+ log: (msg) => log(`[aistudio:build] ${msg}`)
1098
+ });
1099
+
1100
+ return result;
1101
+ }).then((result) => {
1102
+ sendToolResponse(socket, originalId, { output: JSON.stringify(result) }, null);
1103
+ }).catch((err) => {
1104
+ sendToolResponse(socket, originalId, null, err.message);
1105
+ });
1106
+
1107
+ return;
1108
+ }
913
1109
 
914
1110
  if (extensionMsg.type === "EXECUTE_KEY_REPEAT") {
915
1111
  const { key, repeat, tabId: tid } = extensionMsg;
@@ -1204,15 +1400,15 @@ function processInput() {
1204
1400
  } else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
1205
1401
 
1206
1402
  const screenshotId = ++requestCounter;
1207
- const screenshotPath = `/tmp/pi-auto-${Date.now()}.png`;
1403
+ const screenshotPath = path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
1208
1404
 
1209
- const autoFiles = fs.readdirSync("/tmp")
1405
+ const autoFiles = fs.readdirSync(SURF_TMP)
1210
1406
  .filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
1211
1407
  .map(f => ({ name: f, time: parseInt(f.match(/pi-auto-(\d+)\.png/)?.[1] || "0", 10) }))
1212
1408
  .sort((a, b) => b.time - a.time);
1213
1409
  if (autoFiles.length >= 10) {
1214
1410
  autoFiles.slice(9).forEach(f => {
1215
- try { fs.unlinkSync(path.join("/tmp", f.name)); } catch (e) {}
1411
+ try { fs.unlinkSync(path.join(SURF_TMP, f.name)); } catch (e) {}
1216
1412
  });
1217
1413
  }
1218
1414
  pendingToolRequests.set(screenshotId, {
@@ -1440,7 +1636,7 @@ const server = net.createServer((socket) => {
1440
1636
 
1441
1637
  server.listen(SOCKET_PATH, () => {
1442
1638
  log("Socket server listening on " + SOCKET_PATH);
1443
- fs.chmodSync(SOCKET_PATH, 0o600);
1639
+ if (!IS_WIN) { try { fs.chmodSync(SOCKET_PATH, 0o600); } catch {} }
1444
1640
  writeMessage({ type: "HOST_READY" });
1445
1641
  log("Sent HOST_READY to extension");
1446
1642
  });
@@ -1452,14 +1648,14 @@ server.on("error", (err) => {
1452
1648
  process.on("SIGTERM", () => {
1453
1649
  log("SIGTERM received");
1454
1650
  server.close();
1455
- try { fs.unlinkSync(SOCKET_PATH); } catch {}
1651
+ if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
1456
1652
  process.exit(0);
1457
1653
  });
1458
1654
 
1459
1655
  process.on("SIGINT", () => {
1460
1656
  log("SIGINT received");
1461
1657
  server.close();
1462
- try { fs.unlinkSync(SOCKET_PATH); } catch {}
1658
+ if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
1463
1659
  process.exit(0);
1464
1660
  });
1465
1661
 
@@ -4,7 +4,7 @@ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
4
4
  const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
5
  const { z } = require("zod");
6
6
 
7
- const SOCKET_PATH = "/tmp/surf.sock";
7
+ const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
8
8
  const REQUEST_TIMEOUT = 30000;
9
9
 
10
10
  const TOOL_SCHEMAS = {
@@ -13,7 +13,9 @@ const crypto = require("crypto");
13
13
  const readline = require("readline");
14
14
 
15
15
  // Configuration
16
- const DEFAULT_BASE = "/tmp/surf";
16
+ const DEFAULT_BASE = process.platform === "win32"
17
+ ? require("path").join(require("os").tmpdir(), "surf")
18
+ : "/tmp/surf";
17
19
  const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours
18
20
  const DEFAULT_MAX_SIZE = 200 * 1024 * 1024; // 200MB
19
21
  const AUTO_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -30,6 +30,7 @@
30
30
  "native/",
31
31
  "scripts/",
32
32
  "dist/",
33
+ "skills/",
33
34
  "README.md",
34
35
  "LICENSE"
35
36
  ],
@@ -55,7 +56,7 @@
55
56
  "crypto-browserify": "^3.12.1",
56
57
  "events": "^3.3.0",
57
58
  "stream-browserify": "^3.0.0",
58
- "vite-plugin-node-polyfills": "^0.24.0",
59
+ "vite-plugin-node-polyfills": "^0.25.0",
59
60
  "zod": "^4.3.5"
60
61
  },
61
62
  "devDependencies": {
@@ -39,6 +39,12 @@ const BROWSERS = {
39
39
  linux: null,
40
40
  win32: null,
41
41
  },
42
+ helium: {
43
+ name: "Helium",
44
+ darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
45
+ linux: null,
46
+ win32: null,
47
+ },
42
48
  };
43
49
 
44
50
  const NODE_PATHS = {
@@ -58,6 +64,9 @@ const NODE_PATHS = {
58
64
  };
59
65
 
60
66
  function findNode() {
67
+ if (process.env.SURF_NODE_PATH && fs.existsSync(process.env.SURF_NODE_PATH)) {
68
+ return process.env.SURF_NODE_PATH;
69
+ }
61
70
  const platform = process.platform;
62
71
  const paths = NODE_PATHS[platform] || [];
63
72
  for (const p of paths) {
@@ -95,6 +104,9 @@ function getWrapperDir() {
95
104
  }
96
105
 
97
106
  function getHostPath() {
107
+ if (process.env.SURF_HOST_PATH && fs.existsSync(process.env.SURF_HOST_PATH)) {
108
+ return process.env.SURF_HOST_PATH;
109
+ }
98
110
  const npmRoot = findNpmGlobalRoot();
99
111
  if (npmRoot) {
100
112
  const globalPath = path.join(npmRoot, "surf-cli/native/host.cjs");
@@ -218,7 +230,7 @@ Arguments:
218
230
 
219
231
  Options:
220
232
  -b, --browser Browser(s) to install for (default: chrome)
221
- Values: chrome, chromium, brave, edge, arc, all
233
+ Values: chrome, chromium, brave, edge, arc, helium, all
222
234
  Multiple: --browser chrome,brave
223
235
 
224
236
  Examples:
@@ -233,7 +245,7 @@ function main() {
233
245
 
234
246
  if (!extensionId) {
235
247
  console.error("Error: Extension ID required");
236
- console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|brave|edge|all]");
248
+ console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all]");
237
249
  console.error("\nFind your extension ID at chrome://extensions (enable Developer Mode)");
238
250
  process.exit(1);
239
251
  }
@@ -37,6 +37,12 @@ const BROWSERS = {
37
37
  linux: null,
38
38
  win32: null,
39
39
  },
40
+ helium: {
41
+ name: "Helium",
42
+ darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
43
+ linux: null,
44
+ win32: null,
45
+ },
40
46
  };
41
47
 
42
48
  function getWrapperDir() {
@@ -137,7 +143,7 @@ Usage: uninstall-native-host.cjs [options]
137
143
 
138
144
  Options:
139
145
  -b, --browser Browser(s) to uninstall from (default: chrome)
140
- Values: chrome, chromium, brave, edge, arc, all
146
+ Values: chrome, chromium, brave, edge, arc, helium, all
141
147
  -a, --all Uninstall from all browsers and remove wrapper
142
148
 
143
149
  Examples:
@@ -0,0 +1,21 @@
1
+ # Surf Skills
2
+
3
+ This directory contains skill files for AI coding agents.
4
+
5
+ ## Pi Agent
6
+
7
+ To use the surf skill with [Pi coding agent](https://github.com/badlogic/pi-mono):
8
+
9
+ ```bash
10
+ # Option 1: Symlink (auto-updates)
11
+ ln -s "$(pwd)/skills/surf" ~/.pi/agent/skills/surf
12
+
13
+ # Option 2: Copy
14
+ cp -r skills/surf ~/.pi/agent/skills/
15
+ ```
16
+
17
+ The skill will be available when pi detects browser automation tasks.
18
+
19
+ ## Other Agents
20
+
21
+ The `SKILL.md` file is a comprehensive reference that can be adapted for other AI coding agents or used as documentation for LLM prompts.