mcpbrowser 0.3.28 → 0.3.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbrowser",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "mcpName": "io.github.cherchyk/mcpbrowser",
5
5
  "type": "module",
6
6
  "description": "MCP browser server - fetch web pages using real Chrome/Edge/Brave browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { getBrowser, getValidatedPage } from '../core/browser.js';
28
- import { extractAndProcessHtml, waitForPageStability } from '../core/page.js';
28
+ import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
29
29
  import { MCPResponse, InformationalResponse } from '../core/responses.js';
30
30
  import logger from '../core/logger.js';
31
31
 
@@ -262,60 +262,36 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
262
262
  logger.debug(`Clicking: ${selector || `text="${text}"`}`);
263
263
  await elementHandle.click();
264
264
 
265
- if (returnHtml) {
266
- // Wait for page to stabilize (handles both navigation and SPA content updates)
267
- // This ensures content is fully loaded before returning, just like fetch_webpage does
268
- logger.debug('Waiting for page stability...');
269
- await waitForPageStability(page);
270
-
271
- // Wait for SPAs to render dynamic content after click
272
- if (postClickWait > 0) {
273
- await new Promise(resolve => setTimeout(resolve, postClickWait));
274
- }
275
-
276
- const currentUrl = page.url();
277
- const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
278
-
279
- logger.info(`click_element completed: ${selector || `text="${text}"`}`);
280
-
281
- return new ClickElementSuccessResponse(
282
- currentUrl,
283
- selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
284
- html,
285
- [
265
+ // Wait for page to stabilize (handles both navigation and SPA content updates)
266
+ logger.debug(`Waiting for page to be ready${returnHtml ? '' : ' (fast mode)'}...`);
267
+ await waitForPageReady(page, { afterInteraction: true });
268
+
269
+ // Wait for SPAs to render dynamic content after click
270
+ if (postClickWait > 0) {
271
+ await new Promise(resolve => setTimeout(resolve, postClickWait));
272
+ }
273
+
274
+ const currentUrl = page.url();
275
+ const clickMessage = selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`;
276
+ const html = returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
277
+ const nextSteps = returnHtml
278
+ ? [
286
279
  "Use MCPBrowser's click_element again to navigate further",
287
280
  "Use MCPBrowser's type_text to fill forms if needed",
288
281
  "Use MCPBrowser's get_current_html to refresh page state",
289
282
  "Use MCPBrowser's take_screenshot if page has popups or visual content that's hard to parse from HTML",
290
283
  "Use MCPBrowser's close_tab when finished"
291
284
  ]
292
- );
293
- } else {
294
- // Wait for page to stabilize even for fast clicks (ensures JS has finished)
295
- logger.debug('Waiting for page stability (fast mode)...');
296
- await waitForPageStability(page);
297
-
298
- // Wait for SPAs to render dynamic content after click
299
- if (postClickWait > 0) {
300
- await new Promise(resolve => setTimeout(resolve, postClickWait));
301
- }
302
-
303
- const currentUrl = page.url();
304
-
305
- logger.info(`click_element completed: ${selector || `text="${text}"`}`);
306
-
307
- return new ClickElementSuccessResponse(
308
- currentUrl,
309
- selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
310
- null,
311
- [
285
+ : [
312
286
  "Use MCPBrowser's get_current_html to see updated page state",
313
287
  "Use MCPBrowser's take_screenshot if the page has popups, modals, or visual content",
314
288
  "Use MCPBrowser's click_element or MCPBrowser's type_text for more interactions",
315
289
  "Use MCPBrowser's close_tab when finished"
316
- ]
317
- );
318
- }
290
+ ];
291
+
292
+ logger.info(`click_element completed: ${selector || `text="${text}"`}`);
293
+
294
+ return new ClickElementSuccessResponse(currentUrl, clickMessage, html, nextSteps);
319
295
  } catch (err) {
320
296
  logger.error(`click_element failed: ${err.message}`);
321
297
  return new InformationalResponse(
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { getBrowser, domainPages } from '../core/browser.js';
7
- import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extractAndProcessHtml, waitForPageStability } from '../core/page.js';
8
- import { detectRedirectType, waitForAutoAuth, waitForManualAuth } from '../core/auth.js';
7
+ import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extractAndProcessHtml } from '../core/page.js';
8
+ import { isLikelyAuthUrl, waitForAuth } from '../core/auth.js';
9
9
  import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
10
10
  import logger from '../core/logger.js';
11
11
 
@@ -141,7 +141,7 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
141
141
 
142
142
  // Queue this request - processed sequentially, one at a time
143
143
  return queueRequest(async () => {
144
- return await doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, postLoadWait });
144
+ return await doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait });
145
145
  });
146
146
  }
147
147
 
@@ -149,12 +149,8 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
149
149
  * Internal function that does the actual page fetching.
150
150
  * Called by the queue processor - only one runs at a time.
151
151
  */
152
- async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, postLoadWait }) {
153
- // Hardcoded smart defaults
154
- const waitUntil = "domcontentloaded";
155
- const navigationTimeout = 30000;
156
- const authCompletionTimeout = 600000;
157
- const reuseLastKeptPage = true;
152
+ async function doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait }) {
153
+ const originalHostname = new URL(url).hostname;
158
154
 
159
155
  // Ensure browser connection
160
156
  let browserInstance;
@@ -174,94 +170,36 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
174
170
  }
175
171
 
176
172
  try {
177
- // Get or create page for this domain (simple - no locks needed)
178
- let page = await getOrCreatePage(browserInstance, hostname, reuseLastKeptPage);
179
-
180
- // Navigate to URL (pure navigation)
181
- const { statusCode, statusText } = await navigateToUrl(page, url, waitUntil, navigationTimeout);
182
-
183
- // Wait for page content to be ready (handles SPAs automatically)
173
+ let page = await getOrCreatePage(browserInstance, originalHostname);
174
+ let { statusCode, statusText } = await navigateToUrl(page, url, 'domcontentloaded', 30000);
184
175
  await waitForPageReady(page);
185
-
186
- const currentUrl = page.url();
187
- const currentHostname = new URL(currentUrl).hostname;
188
-
189
- // Detect redirect type and handle accordingly
190
- const redirectInfo = detectRedirectType(url, hostname, currentUrl, currentHostname);
191
-
192
- if (redirectInfo.type === 'requested_auth') {
193
- logger.debug('User requested auth page directly, returning content');
194
- // Update domain mapping if needed
195
- if (redirectInfo.currentHostname !== hostname) {
196
- domainPages.delete(hostname);
197
- domainPages.set(redirectInfo.currentHostname, page);
198
- hostname = redirectInfo.currentHostname;
199
- }
200
- } else if (redirectInfo.type === 'permanent') {
201
- logger.debug(`Redirect: ${hostname} → ${redirectInfo.currentHostname}`);
202
-
203
- // Check if we already have a tab for the redirected hostname
204
- // (can happen after reconnect - we mapped mail.google.com but not gmail.com)
205
- const existingPage = domainPages.get(redirectInfo.currentHostname);
206
- if (existingPage && existingPage !== page && !existingPage.isClosed()) {
207
- logger.debug(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
208
- // Close the new tab we just opened, use the existing one
209
- await page.close().catch(() => {});
210
- domainPages.delete(hostname);
211
- page = existingPage;
212
- // Map original hostname to existing page
213
- domainPages.set(hostname, existingPage);
214
- } else {
215
- // Map both original and final hostname to the same page
216
- domainPages.set(hostname, page);
217
- domainPages.set(redirectInfo.currentHostname, page);
218
- }
219
- hostname = redirectInfo.currentHostname;
220
- } else if (redirectInfo.type === 'auth') {
221
- logger.info(`Authentication required: ${redirectInfo.flowType}`);
222
- logger.info(`Auth URL: ${redirectInfo.currentUrl}`);
223
-
224
- // Try auto-auth first (check if existing session works)
225
- const autoAuthResult = await waitForAutoAuth(page);
226
-
227
- if (autoAuthResult.success) {
228
- logger.info(`Auto-auth successful, now at: ${page.url()}`);
229
- // Update hostname to where we landed
230
- if (autoAuthResult.hostname !== hostname) {
231
- domainPages.delete(hostname);
232
- domainPages.set(autoAuthResult.hostname, page);
233
- hostname = autoAuthResult.hostname;
234
- }
235
- } else {
236
- // Wait for manual auth
237
- const manualAuthResult = await waitForManualAuth(page, authCompletionTimeout);
238
-
239
- if (!manualAuthResult.success) {
240
- logger.error(`Authentication failed: ${manualAuthResult.error}`);
241
- return new ErrorResponse(
242
- manualAuthResult.error,
243
- [
244
- "Complete authentication in the browser window",
245
- "Call MCPBrowser's fetch_webpage again with the same URL to retry",
246
- "Use MCPBrowser's close_tab to reset the session if authentication fails"
247
- ]
248
- );
249
- }
250
-
251
- // Update hostname to where we landed
252
- if (manualAuthResult.hostname !== hostname) {
253
- domainPages.delete(hostname);
254
- domainPages.set(manualAuthResult.hostname, page);
255
- hostname = manualAuthResult.hostname;
256
- }
257
- logger.info(`Authentication successful, now at: ${page.url()}`);
176
+
177
+ // Auth: handle multi-step auth flows (e.g., server OIDC → client MSAL)
178
+ // Loop because some sites bounce through auth multiple times before landing.
179
+ let authAttempts = 0;
180
+ while (isLikelyAuthUrl(page.url()) && !isLikelyAuthUrl(url) && authAttempts < 3) {
181
+ authAttempts++;
182
+ logger.info(`Authentication required (attempt ${authAttempts}): ${page.url()}`);
183
+ const authResult = await waitForAuth(page);
184
+ if (!authResult.success) {
185
+ return new ErrorResponse(
186
+ authResult.error,
187
+ [
188
+ "Complete authentication in the browser window",
189
+ "Call MCPBrowser's fetch_webpage again with the same URL to retry",
190
+ "Use MCPBrowser's close_tab to reset the session if authentication fails"
191
+ ]
192
+ );
258
193
  }
259
-
260
- // Wait for page stability after auth
261
- await waitForPageStability(page);
194
+ await waitForPageReady(page, { afterInteraction: true });
195
+ statusCode = null;
196
+ statusText = '';
262
197
  }
263
-
264
- // Additional wait if requested (for pages that need extra time)
198
+
199
+ // Reconcile domain mappings after any redirect (permanent, auth, or none)
200
+ page = reconcileDomainMapping(page, originalHostname);
201
+
202
+ // Additional wait if requested
265
203
  if (postLoadWait > 0) {
266
204
  logger.debug(`Waiting ${postLoadWait}ms (postLoadWait)...`);
267
205
  await new Promise(resolve => setTimeout(resolve, postLoadWait));
@@ -272,15 +210,10 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
272
210
 
273
211
  logger.info(`fetch_webpage completed: ${page.url()}`);
274
212
 
275
- // Check for non-2xx HTTP status codes - return informational response (not red error)
213
+ // Check for non-2xx HTTP status codes
276
214
  if (statusCode && (statusCode >= 400 && statusCode < 600)) {
277
215
  logger.debug(`HTTP ${statusCode} ${statusText} - returning as informational response`);
278
- return new HttpStatusResponse(
279
- page.url(),
280
- statusCode,
281
- statusText,
282
- processedHtml
283
- );
216
+ return new HttpStatusResponse(page.url(), statusCode, statusText, processedHtml);
284
217
  }
285
218
 
286
219
  return new FetchPageSuccessResponse(
@@ -306,3 +239,32 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
306
239
  );
307
240
  }
308
241
  }
242
+
243
+ /**
244
+ * Reconcile domain mappings after navigation/redirect.
245
+ * If the page ended up on a different hostname, map both to the same tab.
246
+ * If another tab already exists for the new hostname, reuse it.
247
+ * @param {Page} page - The current Puppeteer page
248
+ * @param {string} originalHostname - The hostname from the original URL
249
+ * @returns {Page} The page to use (may differ if an existing tab was found)
250
+ */
251
+ function reconcileDomainMapping(page, originalHostname) {
252
+ const currentHostname = new URL(page.url()).hostname;
253
+ if (currentHostname === originalHostname) return page;
254
+
255
+ logger.debug(`Redirect: ${originalHostname} → ${currentHostname}`);
256
+
257
+ const existing = domainPages.get(currentHostname);
258
+ if (existing && existing !== page && !existing.isClosed()) {
259
+ // Another tab already owns this hostname — close ours, reuse existing
260
+ logger.debug(`Found existing tab for ${currentHostname}, reusing it`);
261
+ page.close().catch(() => {});
262
+ domainPages.set(originalHostname, existing);
263
+ return existing;
264
+ }
265
+
266
+ // Map both hostnames to same page
267
+ domainPages.set(originalHostname, page);
268
+ domainPages.set(currentHostname, page);
269
+ return page;
270
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { getBrowser, getValidatedPage } from '../core/browser.js';
6
- import { extractAndProcessHtml, waitForPageStability } from '../core/page.js';
6
+ import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
7
7
  import { MCPResponse, ErrorResponse, InformationalResponse } from '../core/responses.js';
8
8
  import logger from '../core/logger.js';
9
9
 
@@ -226,59 +226,35 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
226
226
  ? filledSelectors[0]
227
227
  : `${filledSelectors.length} fields (${filledSelectors.join(', ')})`;
228
228
 
229
- if (returnHtml) {
230
- // Wait for page to stabilize (handles form validation, autocomplete, etc.)
231
- logger.debug('Waiting for page stability after typing...');
232
- await waitForPageStability(page);
233
-
234
- // Wait for SPAs to render dynamic content after typing
235
- if (postTypeWait > 0) {
236
- await new Promise(resolve => setTimeout(resolve, postTypeWait));
237
- }
238
-
239
- const currentUrl = page.url();
240
- const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
241
-
242
- logger.info(`type_text completed: typed into ${fieldsSummary}`);
243
-
244
- return new TypeTextSuccessResponse(
245
- currentUrl,
246
- `Typed text into: ${fieldsSummary}`,
247
- html,
248
- [
229
+ // Wait for page to stabilize (handles form validation, autocomplete, etc.)
230
+ logger.debug(`Waiting for page to be ready after typing${returnHtml ? '' : ' (fast mode)'}...`);
231
+ await waitForPageReady(page, { afterInteraction: true });
232
+
233
+ // Wait for SPAs to render dynamic content after typing
234
+ if (postTypeWait > 0) {
235
+ await new Promise(resolve => setTimeout(resolve, postTypeWait));
236
+ }
237
+
238
+ const currentUrl = page.url();
239
+ const html = returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
240
+ const nextSteps = returnHtml
241
+ ? [
249
242
  "Use MCPBrowser's type_text to fill additional fields",
250
243
  "Use MCPBrowser's click_element to submit the form or navigate",
251
244
  "Use MCPBrowser's get_current_html to check for validation messages",
252
245
  "Use MCPBrowser's take_screenshot if form has visual feedback or validation that's hard to parse from HTML",
253
246
  "Use MCPBrowser's close_tab when finished"
254
247
  ]
255
- );
256
- } else {
257
- // Wait for page to stabilize even without returning HTML
258
- logger.debug('Waiting for page stability after typing (fast mode)...');
259
- await waitForPageStability(page);
260
-
261
- // Wait for SPAs to render dynamic content after typing
262
- if (postTypeWait > 0) {
263
- await new Promise(resolve => setTimeout(resolve, postTypeWait));
264
- }
265
-
266
- const currentUrl = page.url();
267
-
268
- logger.info(`type_text completed: typed into ${fieldsSummary} (no HTML)`);
269
-
270
- return new TypeTextSuccessResponse(
271
- currentUrl,
272
- `Typed text into: ${fieldsSummary}`,
273
- null,
274
- [
248
+ : [
275
249
  "Use MCPBrowser's get_current_html to see updated page state",
276
250
  "Use MCPBrowser's take_screenshot if the page has visual feedback that's hard to parse",
277
251
  "Use MCPBrowser's type_text for additional fields or MCPBrowser's click_element to submit",
278
252
  "Use MCPBrowser's close_tab when finished"
279
- ]
280
- );
281
- }
253
+ ];
254
+
255
+ logger.info(`type_text completed: typed into ${fieldsSummary}${returnHtml ? '' : ' (no HTML)'}`);
256
+
257
+ return new TypeTextSuccessResponse(currentUrl, `Typed text into: ${fieldsSummary}`, html, nextSteps);
282
258
  } catch (err) {
283
259
  // Build informative error message for agent
284
260
  const totalFields = fields.length;
@@ -9,8 +9,9 @@ import puppeteer from 'puppeteer-core';
9
9
  import { existsSync } from "fs";
10
10
  import os from "os";
11
11
  import path from "path";
12
- import { spawn } from "child_process";
12
+ import { spawn, execSync } from "child_process";
13
13
  import logger from '../core/logger.js';
14
+ import { isWSL, wslToWindowsPath, windowsPathToWSL } from '../utils.js';
14
15
 
15
16
  /**
16
17
  * Base class for all Chromium-based browsers
@@ -49,6 +50,18 @@ export class ChromiumBrowser extends BaseBrowser {
49
50
  const platform = os.platform();
50
51
  const home = os.homedir();
51
52
 
53
+ // In WSL the browser is a Windows process, so the user-data-dir
54
+ // must reside on a Windows-accessible filesystem (e.g. /mnt/c/…).
55
+ if (isWSL()) {
56
+ const windowsLocalAppData = this._getWindowsLocalAppData();
57
+ if (windowsLocalAppData) {
58
+ // windowsLocalAppData is already a WSL path like /mnt/c/Users/.../AppData/Local
59
+ return path.join(windowsLocalAppData, `MCPBrowser/${this.config.userDataDirName}`);
60
+ }
61
+ // Fallback: predictable location on C: drive
62
+ return `/mnt/c/MCPBrowserData/${this.config.userDataDirName}`;
63
+ }
64
+
52
65
  if (platform === "win32") {
53
66
  return path.join(home, `AppData/Local/MCPBrowser/${this.config.userDataDirName}`);
54
67
  } else if (platform === "darwin") {
@@ -74,6 +87,37 @@ export class ChromiumBrowser extends BaseBrowser {
74
87
  }
75
88
  }
76
89
 
90
+ /**
91
+ * Resolve the Windows %LOCALAPPDATA% directory as a WSL path.
92
+ * Cached per-process. Returns null on failure.
93
+ * @returns {string|null} WSL-style path (e.g. /mnt/c/Users/.../AppData/Local)
94
+ * @private
95
+ */
96
+ _getWindowsLocalAppData() {
97
+ // Use a class-level cache so we only shell out once
98
+ if (ChromiumBrowser._wslLocalAppData !== undefined) {
99
+ return ChromiumBrowser._wslLocalAppData;
100
+ }
101
+
102
+ try {
103
+ const raw = execSync('cmd.exe /c echo %LOCALAPPDATA%', {
104
+ encoding: 'utf8',
105
+ timeout: 5000,
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ }).trim();
108
+
109
+ if (raw && !raw.includes('%LOCALAPPDATA%')) {
110
+ ChromiumBrowser._wslLocalAppData = windowsPathToWSL(raw);
111
+ return ChromiumBrowser._wslLocalAppData;
112
+ }
113
+ } catch {
114
+ // cmd.exe not available or timed out
115
+ }
116
+
117
+ ChromiumBrowser._wslLocalAppData = null;
118
+ return null;
119
+ }
120
+
77
121
  /**
78
122
  * Find the browser executable path
79
123
  * @returns {string|undefined}
@@ -109,14 +153,24 @@ export class ChromiumBrowser extends BaseBrowser {
109
153
  }
110
154
 
111
155
  const userDataDir = this.getDefaultUserDataDir();
156
+
157
+ // When running inside WSL the browser executable is a Windows process.
158
+ // It does not understand WSL paths (/mnt/c/…), so convert to a native
159
+ // Windows path for the --user-data-dir argument.
160
+ const userDataDirArg = isWSL() ? wslToWindowsPath(userDataDir) : userDataDir;
161
+
112
162
  const args = [
113
163
  `--remote-debugging-port=${this.config.port}`,
114
- `--user-data-dir=${userDataDir}`,
164
+ `--user-data-dir=${userDataDirArg}`,
115
165
  '--no-first-run',
116
166
  '--no-default-browser-check'
117
167
  ];
118
168
 
119
169
  logger.info(`Launching ${this.config.name} with remote debugging on port ${this.config.port}...`);
170
+ if (isWSL()) {
171
+ logger.info(`WSL detected – launching Windows browser at ${execPath}`);
172
+ logger.info(` user-data-dir (Windows): ${userDataDirArg}`);
173
+ }
120
174
 
121
175
  const child = spawn(execPath, args, {
122
176
  detached: true,
@@ -181,3 +235,6 @@ export class ChromiumBrowser extends BaseBrowser {
181
235
  this.browser = null;
182
236
  }
183
237
  }
238
+
239
+ // Static cache for WSL %LOCALAPPDATA% lookup (undefined = not yet resolved)
240
+ ChromiumBrowser._wslLocalAppData = undefined;
@@ -5,9 +5,11 @@
5
5
 
6
6
  import { ChromiumBrowser } from './ChromiumBrowser.js';
7
7
  import os from "os";
8
+ import { isWSL } from '../utils.js';
8
9
 
9
10
  /**
10
11
  * Get platform-specific default paths where Brave browser is typically installed.
12
+ * When running under WSL, Windows-side paths (via /mnt/c/) are also included.
11
13
  * @returns {string[]} Array of possible Brave executable paths for the current platform
12
14
  */
13
15
  function getDefaultBravePaths() {
@@ -24,13 +26,21 @@ function getDefaultBravePaths() {
24
26
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
25
27
  ];
26
28
  } else {
27
- return [
29
+ const paths = [
28
30
  "/usr/bin/brave",
29
31
  "/usr/bin/brave-browser",
30
32
  "/usr/bin/brave-browser-stable",
31
33
  "/opt/brave.com/brave/brave-browser",
32
34
  "/snap/bin/brave",
33
35
  ];
36
+ // In WSL, also look for Windows-side Brave via /mnt/c/
37
+ if (isWSL()) {
38
+ paths.push(
39
+ "/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe",
40
+ "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
41
+ );
42
+ }
43
+ return paths;
34
44
  }
35
45
  }
36
46
 
@@ -5,9 +5,11 @@
5
5
 
6
6
  import { ChromiumBrowser } from './ChromiumBrowser.js';
7
7
  import os from "os";
8
+ import { isWSL } from '../utils.js';
8
9
 
9
10
  /**
10
11
  * Get platform-specific default paths where Chrome is typically installed.
12
+ * When running under WSL, Windows-side paths (via /mnt/c/) are also included.
11
13
  * @returns {string[]} Array of possible Chrome executable paths for the current platform
12
14
  */
13
15
  function getDefaultChromePaths() {
@@ -24,11 +26,19 @@ function getDefaultChromePaths() {
24
26
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
25
27
  ];
26
28
  } else {
27
- return [
29
+ const paths = [
28
30
  "/usr/bin/google-chrome",
29
31
  "/usr/bin/chromium-browser",
30
32
  "/usr/bin/chromium",
31
33
  ];
34
+ // In WSL, also look for Windows-side Chrome via /mnt/c/
35
+ if (isWSL()) {
36
+ paths.push(
37
+ "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
38
+ "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
39
+ );
40
+ }
41
+ return paths;
32
42
  }
33
43
  }
34
44
 
@@ -5,9 +5,11 @@
5
5
 
6
6
  import { ChromiumBrowser } from './ChromiumBrowser.js';
7
7
  import os from "os";
8
+ import { isWSL } from '../utils.js';
8
9
 
9
10
  /**
10
11
  * Get platform-specific default paths where Edge browser is typically installed.
12
+ * When running under WSL, Windows-side paths (via /mnt/c/) are also included.
11
13
  * @returns {string[]} Array of possible Edge executable paths for the current platform
12
14
  */
13
15
  function getDefaultEdgePaths() {
@@ -23,13 +25,21 @@ function getDefaultEdgePaths() {
23
25
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
24
26
  ];
25
27
  } else {
26
- return [
28
+ const paths = [
27
29
  "/usr/bin/microsoft-edge",
28
30
  "/usr/bin/microsoft-edge-stable",
29
31
  "/usr/bin/microsoft-edge-beta",
30
32
  "/usr/bin/microsoft-edge-dev",
31
33
  "/opt/microsoft/msedge/msedge",
32
34
  ];
35
+ // In WSL, also look for Windows-side Edge via /mnt/c/
36
+ if (isWSL()) {
37
+ paths.push(
38
+ "/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe",
39
+ "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
40
+ );
41
+ }
42
+ return paths;
33
43
  }
34
44
  }
35
45