surf-cli 2.7.1 → 2.8.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.
@@ -2,6 +2,14 @@ const fs = require("fs");
2
2
  const networkFormatters = require("./formatters/network.cjs");
3
3
  const networkStore = require("./network-store.cjs");
4
4
 
5
+ function buildProviderUploadMessage(provider, tabId, filePaths, id) {
6
+ const normalizedProvider = String(provider || "").toLowerCase();
7
+ if (!["chatgpt", "gemini"].includes(normalizedProvider)) {
8
+ throw new Error(`Unsupported upload provider: ${provider}`);
9
+ }
10
+ return { type: "AI_UPLOAD_FILE_TO_TAB", provider: normalizedProvider, tabId, filePaths, id };
11
+ }
12
+
5
13
  function normalizeModelString(model) {
6
14
  return String(model || "").trim().toLowerCase();
7
15
  }
@@ -342,6 +350,19 @@ function formatToolContent(result, log = () => {}) {
342
350
  }
343
351
  return text(output);
344
352
  }
353
+
354
+ if (
355
+ result.scrollTop !== undefined &&
356
+ result.scrollHeight !== undefined &&
357
+ result.scrollPercentage === undefined
358
+ ) {
359
+ return text(`Scrolled to Y:${result.scrollTop} (page height: ${result.scrollHeight})`);
360
+ }
361
+
362
+ if (result.scrollY !== undefined) {
363
+ const pageHeight = result.pageHeight !== undefined ? ` (page height: ${result.pageHeight})` : "";
364
+ return text(`Scrolled to Y:${result.scrollY}${pageHeight}`);
365
+ }
345
366
 
346
367
  if (result.success && result.name && result.tabId !== undefined) {
347
368
  return text(`Registered tab ${result.tabId} as "${result.name}"`);
@@ -478,14 +499,15 @@ function mapComputerAction(args, tabId) {
478
499
  return { type: "FIND_AND_TYPE", text, submit: a.submit ?? false, submitKey: a.submitKey || "Enter", ...baseMsg };
479
500
 
480
501
  case "scroll": {
481
- const amount = (scroll_amount || 3) * 100;
502
+ const direction = a.direction || scroll_direction;
503
+ const amount = a.scroll_pixels ?? ((a.amount ?? scroll_amount ?? 3) * 100);
482
504
  const deltas = {
483
505
  up: { deltaX: 0, deltaY: -amount },
484
506
  down: { deltaX: 0, deltaY: amount },
485
507
  left: { deltaX: -amount, deltaY: 0 },
486
508
  right: { deltaX: amount, deltaY: 0 },
487
509
  };
488
- const { deltaX, deltaY } = deltas[scroll_direction] || { deltaX: 0, deltaY: 0 };
510
+ const { deltaX, deltaY } = deltas[direction] || { deltaX: 0, deltaY: 0 };
489
511
  return { type: "EXECUTE_SCROLL", deltaX, deltaY, x: coordinate?.[0], y: coordinate?.[1], ...baseMsg };
490
512
  }
491
513
 
@@ -575,7 +597,7 @@ function mapToolToMessage(tool, args, tabId) {
575
597
  type: "EXECUTE_SCREENSHOT",
576
598
  savePath: a.savePath || a.output, // Accept both savePath (CLI) and output (MCP)
577
599
  annotate: a.annotate || false,
578
- fullpage: a.fullpage || false,
600
+ fullpage: a.fullpage || a["full-page"] || false,
579
601
  maxHeight: a["max-height"] || 4000,
580
602
  fullRes: a.full || false,
581
603
  maxSize: a["max-size"] || 1200,
@@ -583,6 +605,31 @@ function mapToolToMessage(tool, args, tabId) {
583
605
  };
584
606
  case "javascript_tool":
585
607
  return { type: "EXECUTE_JAVASCRIPT", code: a.code, ...baseMsg };
608
+ case "animate-audit": {
609
+ if (!a.selector || typeof a.selector !== "string") throw new Error("selector required");
610
+ if (typeof a.duration === "boolean") throw new Error("duration must be a number");
611
+ if (typeof a.fps === "boolean") throw new Error("fps must be a number");
612
+ const durationMs = a.duration !== undefined ? Number(a.duration) : 2000;
613
+ const fps = a.fps !== undefined ? Number(a.fps) : 10;
614
+ if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
615
+ throw new Error("duration must be between 100 and 10000 ms");
616
+ }
617
+ if (!Number.isFinite(fps) || fps < 1 || fps > 30) {
618
+ throw new Error("fps must be between 1 and 30");
619
+ }
620
+ return { type: "ANIMATE_AUDIT", selector: a.selector, durationMs, fps, ...baseMsg };
621
+ }
622
+ case "perf-audit": {
623
+ if (typeof a.duration === "boolean") throw new Error("duration must be a number");
624
+ if (a.trigger !== undefined && typeof a.trigger !== "string") {
625
+ throw new Error("trigger must be action:target");
626
+ }
627
+ const durationMs = a.duration !== undefined ? Number(a.duration) : 3000;
628
+ if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
629
+ throw new Error("duration must be between 100 and 10000 ms");
630
+ }
631
+ return { type: "PERF_AUDIT", durationMs, trigger: a.trigger, ...baseMsg };
632
+ }
586
633
  case "wait_for_element":
587
634
  return {
588
635
  type: "WAIT_FOR_ELEMENT",
@@ -1132,4 +1179,4 @@ function mapToolToMessage(tool, args, tabId) {
1132
1179
  }
1133
1180
  }
1134
1181
 
1135
- module.exports = { mapToolToMessage, mapComputerAction, formatToolContent };
1182
+ module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
package/native/host.cjs CHANGED
@@ -12,11 +12,10 @@ const perplexityClient = require("./perplexity-client.cjs");
12
12
  const grokClient = require("./grok-client.cjs");
13
13
  const aistudioClient = require("./aistudio-client.cjs");
14
14
  const aistudioBuild = require("./aistudio-build.cjs");
15
- const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
15
+ const { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage } = require("./host-helpers.cjs");
16
16
 
17
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";
18
+ const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
20
19
  if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
21
20
 
22
21
  // Cross-platform image resize (macOS: sips, Linux: ImageMagick)
@@ -534,6 +533,16 @@ function handleToolRequest(msg, socket) {
534
533
  });
535
534
  writeMessage({ type: "CHATGPT_CDP_COMMAND", tabId, method, params, id: cmdId });
536
535
  }),
536
+ uploadFile: (tabId, filePaths) => new Promise((resolve) => {
537
+ const uploadId = ++requestCounter;
538
+ pendingToolRequests.set(uploadId, {
539
+ socket: null,
540
+ originalId: null,
541
+ tool: "upload_file",
542
+ onComplete: (r) => resolve(r)
543
+ });
544
+ writeMessage(buildProviderUploadMessage("chatgpt", tabId, filePaths, uploadId));
545
+ }),
537
546
  log: (msg) => log(`[chatgpt] ${msg}`)
538
547
  });
539
548
 
@@ -736,7 +745,7 @@ function handleToolRequest(msg, socket) {
736
745
  tool: "upload_file",
737
746
  onComplete: (r) => resolve(r)
738
747
  });
739
- writeMessage({ type: "UPLOAD_FILE_TO_TAB", tabId, filePaths, id: uploadId });
748
+ writeMessage(buildProviderUploadMessage("gemini", tabId, filePaths, uploadId));
740
749
  }),
741
750
  fetchUrl: (url) => new Promise((resolve) => {
742
751
  const fetchId = ++requestCounter;
@@ -941,19 +950,25 @@ function handleToolRequest(msg, socket) {
941
950
  }).then((result) => {
942
951
  // If --save-models flag was passed and we found models, save them
943
952
  if (saveModels && result.models && result.models.length > 0) {
944
- // Convert scraped model names to our format
953
+ // Convert scraped model names to selectable IDs.
945
954
  const modelMap = {};
955
+ const defaultModels = Object.values(grokClient.DEFAULT_GROK_MODELS || {});
946
956
  result.models.forEach(name => {
947
957
  const nameLower = name.toLowerCase();
958
+ const normalizedName = grokClient.normalizeGrokModelLabel(name);
959
+ const knownModel = defaultModels.find(model => {
960
+ const normalizedDefaultName = grokClient.normalizeGrokModelLabel(model.name);
961
+ return normalizedName.includes(normalizedDefaultName) || normalizedDefaultName.includes(normalizedName);
962
+ });
948
963
  // Match known model keywords to generate consistent short IDs
949
964
  let shortId;
950
- if (nameLower.includes('thinking')) shortId = 'thinking';
965
+ if (knownModel) shortId = knownModel.id;
951
966
  else if (nameLower.includes('expert')) shortId = 'expert';
952
967
  else if (nameLower.includes('fast')) shortId = 'fast';
953
968
  else if (nameLower.includes('auto')) shortId = 'auto';
954
969
  else shortId = nameLower.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
955
970
 
956
- modelMap[shortId] = { id: shortId, name: name, desc: "" };
971
+ modelMap[shortId] = { id: shortId, name: name, desc: knownModel?.desc || "" };
957
972
  });
958
973
  const saveResult = grokClient.saveModels(modelMap);
959
974
  result.savedModels = saveResult;
@@ -3,8 +3,8 @@ const net = require("net");
3
3
  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
+ const { SOCKET_PATH, formatSocketError } = require("./socket-path.cjs");
6
7
 
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 = {
@@ -279,7 +279,9 @@ function sendSocketRequest(tool, args = {}) {
279
279
  });
280
280
 
281
281
  let buf = "";
282
+ let settled = false;
282
283
  const timeout = setTimeout(() => {
284
+ settled = true;
283
285
  sock.destroy();
284
286
  reject(new Error("Request timeout"));
285
287
  }, REQUEST_TIMEOUT);
@@ -291,11 +293,13 @@ function sendSocketRequest(tool, args = {}) {
291
293
  for (const line of lines) {
292
294
  if (!line.trim()) continue;
293
295
  try {
296
+ settled = true;
294
297
  clearTimeout(timeout);
295
298
  const resp = JSON.parse(line);
296
299
  sock.end();
297
300
  resolve(resp);
298
301
  } catch {
302
+ settled = true;
299
303
  clearTimeout(timeout);
300
304
  sock.end();
301
305
  reject(new Error("Invalid JSON response"));
@@ -304,17 +308,16 @@ function sendSocketRequest(tool, args = {}) {
304
308
  });
305
309
 
306
310
  sock.on("error", (e) => {
311
+ settled = true;
307
312
  clearTimeout(timeout);
308
- if (e.code === "ENOENT") {
309
- reject(new Error("Socket not found. Is Chrome running with the surf extension?"));
310
- } else {
311
- reject(e);
312
- }
313
+ reject(new Error(formatSocketError(e)));
313
314
  });
314
315
 
315
316
  sock.on("close", () => {
316
317
  clearTimeout(timeout);
317
- reject(new Error("Socket closed unexpectedly"));
318
+ if (!settled) {
319
+ reject(new Error(`Socket closed unexpectedly\nAttempted socket: ${SOCKET_PATH}`));
320
+ }
318
321
  });
319
322
  });
320
323
  }
@@ -0,0 +1,46 @@
1
+ const path = require("path");
2
+ const os = require("os");
3
+
4
+ const IS_WIN = process.platform === "win32";
5
+ const DEFAULT_SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
6
+ const SOCKET_PATH = process.env.SURF_SOCKET || DEFAULT_SOCKET_PATH;
7
+ const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
8
+
9
+ function getSocketTroubleshootingHint() {
10
+ const lines = [
11
+ `Attempted socket: ${SOCKET_PATH}`,
12
+ "Make sure the browser is running with the Surf extension enabled, then restart the browser after native host install changes.",
13
+ "Run `surf doctor --browser all` for detailed native host diagnostics.",
14
+ ];
15
+
16
+ if (process.env.SURF_SOCKET) {
17
+ lines.push("SURF_SOCKET is set; make sure the native host and CLI use the same value.");
18
+ }
19
+
20
+ if (process.platform === "linux") {
21
+ lines.push("On WSL2 with Windows Chrome, run `surf install <extension-id>` from WSL and restart Windows Chrome.");
22
+ }
23
+
24
+ return lines.join("\n");
25
+ }
26
+
27
+ function formatSocketError(error, context = "connect") {
28
+ let message;
29
+ if (error && error.code === "ENOENT") {
30
+ message = "Socket not found.";
31
+ } else if (error && error.code === "ECONNREFUSED") {
32
+ message = "Connection refused. Native host is not accepting connections.";
33
+ } else {
34
+ message = error && error.message ? error.message : String(error);
35
+ }
36
+
37
+ return `Socket ${context} failed: ${message}\n${getSocketTroubleshootingHint()}`;
38
+ }
39
+
40
+ module.exports = {
41
+ DEFAULT_SOCKET_PATH,
42
+ SOCKET_PATH,
43
+ SURF_TMP,
44
+ formatSocketError,
45
+ getSocketTroubleshootingHint,
46
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -62,10 +62,10 @@
62
62
  "devDependencies": {
63
63
  "@biomejs/biome": "^2.4.4",
64
64
  "@types/chrome": "^0.1.37",
65
- "@vitest/coverage-v8": "^4.0.18",
66
- "@vitest/ui": "^4.0.18",
67
- "typescript": "^5.7.2",
65
+ "@vitest/coverage-v8": "^4.1.9",
66
+ "@vitest/ui": "^4.1.9",
67
+ "typescript": "^6.0.3",
68
68
  "vite": "^7.3.1",
69
- "vitest": "^4.0.18"
69
+ "vitest": "^4.1.9"
70
70
  }
71
71
  }
@@ -2,7 +2,7 @@
2
2
  const fs = require("fs");
3
3
  const path = require("path");
4
4
  const os = require("os");
5
- const { execSync, spawnSync } = require("child_process");
5
+ const { execFileSync, execSync } = require("child_process");
6
6
 
7
7
  const HOST_NAME = "surf.browser.host";
8
8
 
@@ -12,12 +12,14 @@ const BROWSERS = {
12
12
  darwin: "Library/Application Support/Google/Chrome/NativeMessagingHosts",
13
13
  linux: ".config/google-chrome/NativeMessagingHosts",
14
14
  win32: "Google\\Chrome",
15
+ wsl: "Google/Chrome/User Data/NativeMessagingHosts",
15
16
  },
16
17
  chromium: {
17
18
  name: "Chromium",
18
19
  darwin: "Library/Application Support/Chromium/NativeMessagingHosts",
19
20
  linux: ".config/chromium/NativeMessagingHosts",
20
21
  win32: "Chromium",
22
+ wsl: "Chromium/User Data/NativeMessagingHosts",
21
23
  },
22
24
  brave: {
23
25
  name: "Brave",
@@ -25,12 +27,14 @@ const BROWSERS = {
25
27
  "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts",
26
28
  linux: ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
27
29
  win32: "BraveSoftware\\Brave-Browser",
30
+ wsl: "BraveSoftware/Brave-Browser/User Data/NativeMessagingHosts",
28
31
  },
29
32
  edge: {
30
33
  name: "Microsoft Edge",
31
34
  darwin: "Library/Application Support/Microsoft Edge/NativeMessagingHosts",
32
35
  linux: ".config/microsoft-edge/NativeMessagingHosts",
33
36
  win32: "Microsoft\\Edge",
37
+ wsl: "Microsoft/Edge/User Data/NativeMessagingHosts",
34
38
  },
35
39
  arc: {
36
40
  name: "Arc",
@@ -38,12 +42,14 @@ const BROWSERS = {
38
42
  "Library/Application Support/Arc/User Data/NativeMessagingHosts",
39
43
  linux: null,
40
44
  win32: null,
45
+ wsl: null,
41
46
  },
42
47
  helium: {
43
48
  name: "Helium",
44
49
  darwin: "Library/Application Support/net.imput.helium/NativeMessagingHosts",
45
50
  linux: null,
46
51
  win32: null,
52
+ wsl: null,
47
53
  },
48
54
  };
49
55
 
@@ -63,6 +69,16 @@ const NODE_PATHS = {
63
69
  ],
64
70
  };
65
71
 
72
+ function isWsl() {
73
+ if (process.platform !== "linux") return false;
74
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
75
+ try {
76
+ return /microsoft|wsl/i.test(fs.readFileSync("/proc/version", "utf8"));
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
66
82
  function findNode() {
67
83
  if (process.env.SURF_NODE_PATH && fs.existsSync(process.env.SURF_NODE_PATH)) {
68
84
  return process.env.SURF_NODE_PATH;
@@ -88,10 +104,14 @@ function findNpmGlobalRoot() {
88
104
  }
89
105
  }
90
106
 
91
- function getWrapperDir() {
92
- const platform = process.platform;
107
+ function getWrapperDir(target = process.platform) {
93
108
  const home = os.homedir();
94
- switch (platform) {
109
+ if (target === "wsl-windows") {
110
+ const localAppData = getWindowsEnv("LOCALAPPDATA");
111
+ if (!localAppData) return null;
112
+ return path.join(windowsPathToWslPath(localAppData), "surf-cli");
113
+ }
114
+ switch (process.platform) {
95
115
  case "darwin":
96
116
  return path.join(home, "Library/Application Support/surf-cli");
97
117
  case "linux":
@@ -117,72 +137,123 @@ function getHostPath() {
117
137
  return null;
118
138
  }
119
139
 
120
- function createWrapper(wrapperDir, nodePath, hostPath) {
121
- const platform = process.platform;
140
+ function getWindowsEnv(name) {
141
+ try {
142
+ return execFileSync("cmd.exe", ["/c", "echo", `%${name}%`], { encoding: "utf8" })
143
+ .trim()
144
+ .replace(/\r/g, "");
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function windowsPathToWslPath(winPath) {
151
+ const normalized = winPath.replace(/\\/g, "/");
152
+ const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
153
+ if (!match) return normalized;
154
+ return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
155
+ }
156
+
157
+ function wslPathToWindowsPath(wslPath) {
158
+ try {
159
+ return execFileSync("wslpath", ["-w", wslPath], { encoding: "utf8" }).trim().replace(/\r/g, "");
160
+ } catch {
161
+ const match = wslPath.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
162
+ if (match) return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
163
+ return wslPath;
164
+ }
165
+ }
166
+
167
+ function createWrapper(wrapperDir, nodePath, hostPath, target = process.platform) {
122
168
  fs.mkdirSync(wrapperDir, { recursive: true });
123
169
 
124
- if (platform === "win32") {
170
+ if (target === "wsl-windows") {
171
+ const cmdPath = path.join(wrapperDir, "host-wrapper-wsl.cmd");
172
+ const distroArg = process.env.WSL_DISTRO_NAME ? ` -d "${process.env.WSL_DISTRO_NAME}"` : "";
173
+ const content = `@echo off\r\nwsl.exe${distroArg} --cd "${path.dirname(hostPath)}" --exec "${nodePath}" "${hostPath}" %*\r\n`;
174
+ fs.writeFileSync(cmdPath, content);
175
+ return wslPathToWindowsPath(cmdPath);
176
+ }
177
+
178
+ if (process.platform === "win32") {
125
179
  const batPath = path.join(wrapperDir, "host-wrapper.bat");
126
- const content = `@echo off\r\n"${nodePath}" "${hostPath}"\r\n`;
180
+ const content = `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`;
127
181
  fs.writeFileSync(batPath, content);
128
182
  return batPath;
129
- } else {
130
- const shPath = path.join(wrapperDir, "host-wrapper.sh");
131
- const hostDir = path.dirname(hostPath);
132
- const content = `#!/bin/bash
183
+ }
184
+
185
+ const shPath = path.join(wrapperDir, "host-wrapper.sh");
186
+ const hostDir = path.dirname(hostPath);
187
+ const content = `#!/usr/bin/env bash
133
188
  cd "${hostDir}"
134
- exec "${nodePath}" "${hostPath}"
189
+ exec "${nodePath}" "${hostPath}" "$@"
135
190
  `;
136
- fs.writeFileSync(shPath, content);
137
- fs.chmodSync(shPath, "755");
138
- return shPath;
139
- }
191
+ fs.writeFileSync(shPath, content);
192
+ fs.chmodSync(shPath, "755");
193
+ return shPath;
140
194
  }
141
195
 
142
- function installManifest(browser, extensionId, wrapperPath) {
143
- const platform = process.platform;
144
- const browserConfig = BROWSERS[browser];
145
-
146
- if (!browserConfig || !browserConfig[platform]) {
147
- return null;
148
- }
149
-
150
- if (platform === "win32") {
151
- return installWindowsRegistry(browser, extensionId, wrapperPath);
152
- }
196
+ function readExistingManifest(manifestPath) {
197
+ if (!fs.existsSync(manifestPath)) return {};
198
+ return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
199
+ }
153
200
 
154
- const manifestDir = path.join(os.homedir(), browserConfig[platform]);
155
- fs.mkdirSync(manifestDir, { recursive: true });
201
+ function writeManifest(manifestPath, extensionId, wrapperPath) {
202
+ const origin = `chrome-extension://${extensionId}/`;
203
+ const existing = readExistingManifest(manifestPath);
204
+ const allowedOrigins = Array.isArray(existing.allowed_origins) ? existing.allowed_origins : [];
156
205
 
157
206
  const manifest = {
207
+ ...existing,
158
208
  name: HOST_NAME,
159
- description: "Surf CLI Native Host",
209
+ description: existing.description || "Surf CLI Native Host",
160
210
  path: wrapperPath,
161
211
  type: "stdio",
162
- allowed_origins: [`chrome-extension://${extensionId}/`],
212
+ allowed_origins: Array.from(new Set([...allowedOrigins, origin])),
163
213
  };
164
214
 
165
- const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
215
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
166
216
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
167
217
  return manifestPath;
168
218
  }
169
219
 
220
+ function getWslWindowsManifestDir(browserConfig) {
221
+ const localAppData = getWindowsEnv("LOCALAPPDATA");
222
+ if (!localAppData || !browserConfig.wsl) return null;
223
+ return path.join(windowsPathToWslPath(localAppData), browserConfig.wsl);
224
+ }
225
+
226
+ function installManifest(browser, extensionId, wrapperPath, target) {
227
+ const browserConfig = BROWSERS[browser];
228
+
229
+ if (!browserConfig) return null;
230
+
231
+ if (target === "wsl-windows") {
232
+ const manifestDir = getWslWindowsManifestDir(browserConfig);
233
+ if (!manifestDir) return null;
234
+ const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
235
+ return writeManifest(manifestPath, extensionId, wrapperPath);
236
+ }
237
+
238
+ const platform = process.platform;
239
+ if (!browserConfig[platform]) return null;
240
+
241
+ if (platform === "win32") {
242
+ return installWindowsRegistry(browser, extensionId, wrapperPath);
243
+ }
244
+
245
+ const manifestDir = path.join(os.homedir(), browserConfig[platform]);
246
+ const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
247
+ return writeManifest(manifestPath, extensionId, wrapperPath);
248
+ }
249
+
170
250
  function installWindowsRegistry(browser, extensionId, wrapperPath) {
171
251
  const browserConfig = BROWSERS[browser];
172
252
  const regPath = `HKCU\\Software\\${browserConfig.win32}\\NativeMessagingHosts\\${HOST_NAME}`;
173
253
 
174
254
  const manifestDir = getWrapperDir();
175
255
  const manifestPath = path.join(manifestDir, `${HOST_NAME}.json`);
176
-
177
- const manifest = {
178
- name: HOST_NAME,
179
- description: "Surf CLI Native Host",
180
- path: wrapperPath,
181
- type: "stdio",
182
- allowed_origins: [`chrome-extension://${extensionId}/`],
183
- };
184
-
185
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
256
+ writeManifest(manifestPath, extensionId, wrapperPath);
186
257
 
187
258
  try {
188
259
  execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, {
@@ -197,7 +268,7 @@ function installWindowsRegistry(browser, extensionId, wrapperPath) {
197
268
 
198
269
  function parseArgs() {
199
270
  const args = process.argv.slice(2);
200
- const result = { extensionId: null, browsers: ["chrome"] };
271
+ const result = { extensionId: null, browsers: ["chrome"], target: "auto" };
201
272
 
202
273
  for (let i = 0; i < args.length; i++) {
203
274
  const arg = args[i];
@@ -208,6 +279,8 @@ function parseArgs() {
208
279
  } else {
209
280
  result.browsers = browserArg.split(",").map((b) => b.trim().toLowerCase());
210
281
  }
282
+ } else if (arg === "--target") {
283
+ result.target = args[++i];
211
284
  } else if (arg === "--help" || arg === "-h") {
212
285
  printHelp();
213
286
  process.exit(0);
@@ -232,20 +305,23 @@ Options:
232
305
  -b, --browser Browser(s) to install for (default: chrome)
233
306
  Values: chrome, chromium, brave, edge, arc, helium, all
234
307
  Multiple: --browser chrome,brave
308
+ --target Install target: auto, linux, windows
309
+ On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
235
310
 
236
311
  Examples:
237
312
  node install-native-host.cjs abcdefghijklmnopabcdefghijklmnop
238
313
  node install-native-host.cjs abcdefghijklmnop --browser brave
239
314
  node install-native-host.cjs abcdefghijklmnop --browser all
315
+ node install-native-host.cjs abcdefghijklmnop --target linux
240
316
  `);
241
317
  }
242
318
 
243
319
  function main() {
244
- const { extensionId, browsers } = parseArgs();
320
+ const { extensionId, browsers, target } = parseArgs();
245
321
 
246
322
  if (!extensionId) {
247
323
  console.error("Error: Extension ID required");
248
- console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all]");
324
+ console.error("Usage: install-native-host.cjs <extension-id> [--browser chrome|chromium|brave|edge|arc|helium|all] [--target auto|linux|windows]");
249
325
  console.error("\nFind your extension ID at chrome://extensions (enable Developer Mode)");
250
326
  process.exit(1);
251
327
  }
@@ -256,6 +332,24 @@ function main() {
256
332
  process.exit(1);
257
333
  }
258
334
 
335
+ if (!["auto", "linux", "windows"].includes(target)) {
336
+ console.error("Error: Invalid --target value. Expected auto, linux, or windows");
337
+ process.exit(1);
338
+ }
339
+
340
+ const runningInWsl = isWsl();
341
+ if (target === "windows" && !runningInWsl && process.platform !== "win32") {
342
+ console.error("Error: --target windows is only supported on Windows or WSL2");
343
+ process.exit(1);
344
+ }
345
+
346
+ if (target === "linux" && process.platform !== "linux") {
347
+ console.error("Error: --target linux is only supported on Linux or WSL2");
348
+ process.exit(1);
349
+ }
350
+
351
+ const effectiveTarget = runningInWsl && target !== "linux" ? "wsl-windows" : process.platform;
352
+
259
353
  const nodePath = findNode();
260
354
  if (!nodePath) {
261
355
  console.error("Error: Could not find Node.js");
@@ -270,19 +364,20 @@ function main() {
270
364
  process.exit(1);
271
365
  }
272
366
 
273
- const wrapperDir = getWrapperDir();
367
+ const wrapperDir = getWrapperDir(effectiveTarget);
274
368
  if (!wrapperDir) {
275
- console.error("Error: Unsupported platform");
369
+ console.error("Error: Unsupported platform or Windows interop unavailable");
276
370
  process.exit(1);
277
371
  }
278
372
 
279
- console.log(`Platform: ${process.platform}`);
373
+ console.log(`Platform: ${process.platform}${runningInWsl ? " (WSL2 detected)" : ""}`);
374
+ console.log(`Target: ${effectiveTarget === "wsl-windows" ? "Windows browser from WSL2" : effectiveTarget}`);
280
375
  console.log(`Node: ${nodePath}`);
281
376
  console.log(`Host: ${hostPath}`);
282
377
  console.log(`Wrapper dir: ${wrapperDir}`);
283
378
  console.log("");
284
379
 
285
- const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath);
380
+ const wrapperPath = createWrapper(wrapperDir, nodePath, hostPath, effectiveTarget);
286
381
  console.log(`Created wrapper: ${wrapperPath}`);
287
382
  console.log("");
288
383
 
@@ -295,7 +390,7 @@ function main() {
295
390
  continue;
296
391
  }
297
392
 
298
- const result = installManifest(browser, extensionId, wrapperPath);
393
+ const result = installManifest(browser, extensionId, wrapperPath, effectiveTarget);
299
394
  if (result) {
300
395
  installed.push({ browser: BROWSERS[browser].name, path: result });
301
396
  } else {
@@ -311,10 +406,17 @@ function main() {
311
406
  }
312
407
 
313
408
  if (skipped.length > 0) {
314
- console.log(`\nSkipped (not supported on ${process.platform}): ${skipped.join(", ")}`);
409
+ console.log(`\nSkipped (not supported for ${effectiveTarget}): ${skipped.join(", ")}`);
315
410
  }
316
411
 
317
412
  console.log("\nDone! Restart your browser for changes to take effect.");
318
413
  }
319
414
 
320
- main();
415
+ if (require.main === module) {
416
+ main();
417
+ }
418
+
419
+ module.exports = {
420
+ createWrapper,
421
+ writeManifest,
422
+ };