surf-cli 2.5.2 → 2.7.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();
@@ -693,6 +697,57 @@ function handleToolRequest(msg, socket) {
693
697
  });
694
698
  writeMessage({ type: "GET_GOOGLE_COOKIES", id: cookieId });
695
699
  }),
700
+ createTab: () => new Promise((resolve) => {
701
+ const tabCreateId = ++requestCounter;
702
+ pendingToolRequests.set(tabCreateId, {
703
+ socket: null,
704
+ originalId: null,
705
+ tool: "create_tab",
706
+ onComplete: (r) => resolve(r)
707
+ });
708
+ writeMessage({ type: "GEMINI_NEW_TAB", id: tabCreateId });
709
+ }),
710
+ closeTab: (tabIdToClose) => new Promise((resolve) => {
711
+ const tabCloseId = ++requestCounter;
712
+ pendingToolRequests.set(tabCloseId, {
713
+ socket: null,
714
+ originalId: null,
715
+ tool: "close_tab",
716
+ onComplete: (r) => resolve(r)
717
+ });
718
+ writeMessage({ type: "GEMINI_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
719
+ }),
720
+ jsEval: (tabId, code) => new Promise((resolve) => {
721
+ const jsId = ++requestCounter;
722
+ pendingToolRequests.set(jsId, {
723
+ socket: null,
724
+ originalId: null,
725
+ tool: "js_eval",
726
+ onComplete: (r) => resolve(r)
727
+ });
728
+ log(`[gemini] Sending EXECUTE_JAVASCRIPT id=${jsId} tabId=${tabId} code=${code.length} chars`);
729
+ writeMessage({ type: "EXECUTE_JAVASCRIPT", tabId, code, id: jsId });
730
+ }),
731
+ uploadFile: (tabId, filePaths) => new Promise((resolve) => {
732
+ const uploadId = ++requestCounter;
733
+ pendingToolRequests.set(uploadId, {
734
+ socket: null,
735
+ originalId: null,
736
+ tool: "upload_file",
737
+ onComplete: (r) => resolve(r)
738
+ });
739
+ writeMessage({ type: "UPLOAD_FILE_TO_TAB", tabId, filePaths, id: uploadId });
740
+ }),
741
+ fetchUrl: (url) => new Promise((resolve) => {
742
+ const fetchId = ++requestCounter;
743
+ pendingToolRequests.set(fetchId, {
744
+ socket: null,
745
+ originalId: null,
746
+ tool: "fetch_url",
747
+ onComplete: (r) => resolve(r)
748
+ });
749
+ writeMessage({ type: "GEMINI_FETCH_URL", url, id: fetchId });
750
+ }),
696
751
  log: (msg) => log(`[gemini] ${msg}`)
697
752
  });
698
753
 
@@ -910,6 +965,198 @@ function handleToolRequest(msg, socket) {
910
965
 
911
966
  return;
912
967
  }
968
+
969
+ if (extensionMsg.type === "AISTUDIO_QUERY") {
970
+ const { query, model, withPage, timeout } = extensionMsg;
971
+
972
+ queueAiRequest(async () => {
973
+ const EXT_CALL_TIMEOUT_MS = 30000;
974
+
975
+ const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => new Promise((resolve, reject) => {
976
+ const id = ++requestCounter;
977
+
978
+ if (msg && msg.type === "AISTUDIO_NEW_TAB") {
979
+ log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/prompts/new_chat")}`);
980
+ }
981
+
982
+ const timeoutId = setTimeout(() => {
983
+ pendingToolRequests.delete(id);
984
+ reject(new Error(`Timeout waiting for extension: ${toolName}`));
985
+ }, timeoutMs);
986
+
987
+ pendingToolRequests.set(id, {
988
+ socket: null,
989
+ originalId: null,
990
+ tool: toolName,
991
+ onComplete: (r) => {
992
+ clearTimeout(timeoutId);
993
+ resolve(r);
994
+ }
995
+ });
996
+
997
+ writeMessage({ ...msg, id });
998
+ });
999
+
1000
+ // 1. Get page context if requested
1001
+ let pageContext = null;
1002
+ if (withPage) {
1003
+ const pageResult = await callExtension(
1004
+ "get_page_text",
1005
+ { type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
1006
+ 45000
1007
+ );
1008
+
1009
+ if (pageResult && !pageResult.error) {
1010
+ pageContext = {
1011
+ url: pageResult.url,
1012
+ text: pageResult.text || pageResult.pageContent || ""
1013
+ };
1014
+ }
1015
+ }
1016
+
1017
+ // 2. Build full prompt
1018
+ let fullPrompt = query || "";
1019
+ if (pageContext) {
1020
+ const MAX_PAGE_CONTEXT_CHARS = 20000;
1021
+ const pageText = String(pageContext.text || "");
1022
+ const truncated = pageText.length > MAX_PAGE_CONTEXT_CHARS
1023
+ ? pageText.slice(0, MAX_PAGE_CONTEXT_CHARS) + "\n\n[...truncated...]"
1024
+ : pageText;
1025
+
1026
+ fullPrompt = `Page: ${pageContext.url}\n\n${truncated}\n\n---\n\n${fullPrompt}`;
1027
+ }
1028
+
1029
+ // 3. Call AI Studio client
1030
+ const result = await aistudioClient.query({
1031
+ prompt: fullPrompt,
1032
+ model: model || undefined,
1033
+ timeout: timeout || 300000,
1034
+ getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
1035
+ createTab: (url) => callExtension(
1036
+ "create_tab",
1037
+ { type: "AISTUDIO_NEW_TAB", url },
1038
+ 45000
1039
+ ),
1040
+ closeTab: (tabIdToClose) => callExtension(
1041
+ "close_tab",
1042
+ { type: "AISTUDIO_CLOSE_TAB", tabId: tabIdToClose },
1043
+ 45000
1044
+ ),
1045
+ cdpEvaluate: (tabId, expression) => callExtension(
1046
+ "cdp_evaluate",
1047
+ { type: "AISTUDIO_EVALUATE", tabId, expression }
1048
+ ),
1049
+ cdpCommand: (tabId, method, params) => callExtension(
1050
+ "cdp_command",
1051
+ { type: "AISTUDIO_CDP_COMMAND", tabId, method, params }
1052
+ ),
1053
+ readNetworkEntries: (tabIdToRead) => callExtension(
1054
+ "read_network_entries",
1055
+ {
1056
+ type: "READ_NETWORK_REQUESTS",
1057
+ tabId: tabIdToRead,
1058
+ full: true,
1059
+ limit: 100,
1060
+ urlPattern: "MakerSuiteService/GenerateContent"
1061
+ },
1062
+ 45000
1063
+ ),
1064
+ log: (msg) => log(`[aistudio] ${msg}`)
1065
+ });
1066
+
1067
+ return result;
1068
+ }).then((result) => {
1069
+ const payload = {
1070
+ response: result.response,
1071
+ model: result.model,
1072
+ thinkingTime: result.thinkingTime,
1073
+ tookMs: result.tookMs
1074
+ };
1075
+
1076
+ sendToolResponse(socket, originalId, { output: JSON.stringify(payload) }, null);
1077
+ }).catch((err) => {
1078
+ sendToolResponse(socket, originalId, null, err.message);
1079
+ });
1080
+
1081
+ return;
1082
+ }
1083
+
1084
+ if (extensionMsg.type === "AISTUDIO_BUILD") {
1085
+ const { query, model, output, keepOpen, timeout } = extensionMsg;
1086
+
1087
+ queueAiRequest(async () => {
1088
+ const EXT_CALL_TIMEOUT_MS = 30000;
1089
+
1090
+ const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => new Promise((resolve, reject) => {
1091
+ const id = ++requestCounter;
1092
+
1093
+ if (msg && msg.type === "AISTUDIO_NEW_TAB") {
1094
+ log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/apps")}`);
1095
+ }
1096
+
1097
+ const timeoutId = setTimeout(() => {
1098
+ pendingToolRequests.delete(id);
1099
+ reject(new Error(`Timeout waiting for extension: ${toolName}`));
1100
+ }, timeoutMs);
1101
+
1102
+ pendingToolRequests.set(id, {
1103
+ socket: null,
1104
+ originalId: null,
1105
+ tool: toolName,
1106
+ onComplete: (r) => {
1107
+ clearTimeout(timeoutId);
1108
+ resolve(r);
1109
+ }
1110
+ });
1111
+
1112
+ writeMessage({ ...msg, id });
1113
+ });
1114
+
1115
+ const result = await aistudioBuild.build({
1116
+ prompt: query,
1117
+ model: model || undefined,
1118
+ output,
1119
+ keepOpen,
1120
+ timeout: timeout || 600000,
1121
+ getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
1122
+ createTab: (url) => callExtension(
1123
+ "create_tab",
1124
+ { type: "AISTUDIO_NEW_TAB", url },
1125
+ 45000
1126
+ ),
1127
+ closeTab: (tabIdToClose) => callExtension(
1128
+ "close_tab",
1129
+ { type: "AISTUDIO_CLOSE_TAB", tabId: tabIdToClose },
1130
+ 45000
1131
+ ),
1132
+ cdpEvaluate: (tabId, expression) => callExtension(
1133
+ "cdp_evaluate",
1134
+ { type: "AISTUDIO_EVALUATE", tabId, expression }
1135
+ ),
1136
+ cdpCommand: (tabId, method, params) => callExtension(
1137
+ "cdp_command",
1138
+ { type: "AISTUDIO_CDP_COMMAND", tabId, method, params }
1139
+ ),
1140
+ searchDownloads: async (params) => {
1141
+ const result = await callExtension(
1142
+ "downloads_search",
1143
+ { type: "DOWNLOADS_SEARCH", searchParams: params },
1144
+ 10000
1145
+ );
1146
+ return result?.downloads || [];
1147
+ },
1148
+ log: (msg) => log(`[aistudio:build] ${msg}`)
1149
+ });
1150
+
1151
+ return result;
1152
+ }).then((result) => {
1153
+ sendToolResponse(socket, originalId, { output: JSON.stringify(result) }, null);
1154
+ }).catch((err) => {
1155
+ sendToolResponse(socket, originalId, null, err.message);
1156
+ });
1157
+
1158
+ return;
1159
+ }
913
1160
 
914
1161
  if (extensionMsg.type === "EXECUTE_KEY_REPEAT") {
915
1162
  const { key, repeat, tabId: tid } = extensionMsg;
@@ -1204,15 +1451,15 @@ function processInput() {
1204
1451
  } else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
1205
1452
 
1206
1453
  const screenshotId = ++requestCounter;
1207
- const screenshotPath = `/tmp/pi-auto-${Date.now()}.png`;
1454
+ const screenshotPath = path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
1208
1455
 
1209
- const autoFiles = fs.readdirSync("/tmp")
1456
+ const autoFiles = fs.readdirSync(SURF_TMP)
1210
1457
  .filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
1211
1458
  .map(f => ({ name: f, time: parseInt(f.match(/pi-auto-(\d+)\.png/)?.[1] || "0", 10) }))
1212
1459
  .sort((a, b) => b.time - a.time);
1213
1460
  if (autoFiles.length >= 10) {
1214
1461
  autoFiles.slice(9).forEach(f => {
1215
- try { fs.unlinkSync(path.join("/tmp", f.name)); } catch (e) {}
1462
+ try { fs.unlinkSync(path.join(SURF_TMP, f.name)); } catch (e) {}
1216
1463
  });
1217
1464
  }
1218
1465
  pendingToolRequests.set(screenshotId, {
@@ -1440,7 +1687,7 @@ const server = net.createServer((socket) => {
1440
1687
 
1441
1688
  server.listen(SOCKET_PATH, () => {
1442
1689
  log("Socket server listening on " + SOCKET_PATH);
1443
- fs.chmodSync(SOCKET_PATH, 0o600);
1690
+ if (!IS_WIN) { try { fs.chmodSync(SOCKET_PATH, 0o600); } catch {} }
1444
1691
  writeMessage({ type: "HOST_READY" });
1445
1692
  log("Sent HOST_READY to extension");
1446
1693
  });
@@ -1452,14 +1699,14 @@ server.on("error", (err) => {
1452
1699
  process.on("SIGTERM", () => {
1453
1700
  log("SIGTERM received");
1454
1701
  server.close();
1455
- try { fs.unlinkSync(SOCKET_PATH); } catch {}
1702
+ if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
1456
1703
  process.exit(0);
1457
1704
  });
1458
1705
 
1459
1706
  process.on("SIGINT", () => {
1460
1707
  log("SIGINT received");
1461
1708
  server.close();
1462
- try { fs.unlinkSync(SOCKET_PATH); } catch {}
1709
+ if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
1463
1710
  process.exit(0);
1464
1711
  });
1465
1712
 
@@ -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.7.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
  ],
@@ -50,21 +51,21 @@
50
51
  },
51
52
  "dependencies": {
52
53
  "@google/generative-ai": "^0.24.1",
53
- "@modelcontextprotocol/sdk": "^1.7.0",
54
+ "@modelcontextprotocol/sdk": "^1.26.0",
54
55
  "buffer": "^6.0.3",
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
- "zod": "^4.3.5"
59
+ "vite-plugin-node-polyfills": "^0.25.0",
60
+ "zod": "^4.3.6"
60
61
  },
61
62
  "devDependencies": {
62
- "@biomejs/biome": "^2.3.11",
63
- "@types/chrome": "^0.0.287",
64
- "@vitest/coverage-v8": "^4.0.16",
65
- "@vitest/ui": "^4.0.16",
63
+ "@biomejs/biome": "^2.4.4",
64
+ "@types/chrome": "^0.1.37",
65
+ "@vitest/coverage-v8": "^4.0.18",
66
+ "@vitest/ui": "^4.0.18",
66
67
  "typescript": "^5.7.2",
67
68
  "vite": "^7.3.1",
68
- "vitest": "^4.0.16"
69
+ "vitest": "^4.0.18"
69
70
  }
70
71
  }
@@ -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" ~/.agents/skills/surf
12
+
13
+ # Option 2: Copy
14
+ cp -r skills/surf ~/.agents/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.