@browserbridge/bbx 1.0.0 → 1.1.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.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
@@ -1,23 +1,39 @@
1
1
  // @ts-check
2
2
 
3
- import { execFileSync } from 'node:child_process';
4
3
  import fs from 'node:fs';
5
4
  import os from 'node:os';
6
5
  import path from 'node:path';
7
6
 
8
7
  /** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
9
8
  /** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
9
+ /** @typedef {() => boolean | Promise<boolean>} Detector */
10
10
 
11
11
  const home = os.homedir();
12
12
  const platform = process.platform;
13
+ const WINDOWS_EXECUTABLE_EXTENSIONS = new Set(
14
+ (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
15
+ .split(';')
16
+ .map((extension) => extension.trim().toLowerCase())
17
+ .filter(Boolean)
18
+ );
19
+ const DEFAULT_COMMAND_NAMES =
20
+ platform === 'darwin'
21
+ ? ['codex', 'claude', 'opencode', 'agy']
22
+ : platform === 'linux'
23
+ ? ['codex', 'claude', 'cursor', 'code', 'opencode', 'agy', 'windsurf']
24
+ : ['codex', 'claude', 'cursor', 'opencode', 'agy', 'windsurf'];
25
+
26
+ const PATH_DELIMITER = platform === 'win32' ? ';' : ':';
27
+
28
+ /** @type {Promise<Set<string>> | null} */
29
+ let availableCommandsPromise = null;
13
30
 
14
31
  /**
15
32
  * @returns {string}
16
33
  */
17
34
  function getVsCodeUserDataDir() {
18
35
  if (platform === 'win32') {
19
- const appData =
20
- process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
36
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
21
37
  return path.join(appData, 'Code');
22
38
  }
23
39
  if (platform === 'linux') {
@@ -27,12 +43,12 @@ function getVsCodeUserDataDir() {
27
43
  }
28
44
 
29
45
  /**
30
- * @param {string} p
31
- * @returns {boolean}
46
+ * @param {string} targetPath
47
+ * @returns {Promise<boolean>}
32
48
  */
33
- function fsExists(p) {
49
+ async function fsExists(targetPath) {
34
50
  try {
35
- fs.accessSync(p);
51
+ await fs.promises.access(targetPath, fs.constants.F_OK);
36
52
  return true;
37
53
  } catch {
38
54
  return false;
@@ -40,75 +56,151 @@ function fsExists(p) {
40
56
  }
41
57
 
42
58
  /**
43
- * @param {string} cmd
44
- * @returns {boolean}
59
+ * @param {string} command
60
+ * @returns {string}
45
61
  */
46
- function commandExists(cmd) {
47
- try {
48
- execFileSync(platform === 'win32' ? 'where' : 'which', [cmd], {
49
- stdio: 'ignore',
50
- });
51
- return true;
52
- } catch {
53
- return false;
62
+ function normalizeCommandName(command) {
63
+ return platform === 'win32' ? command.toLowerCase() : command;
64
+ }
65
+
66
+ /**
67
+ * @param {string} entryName
68
+ * @returns {string | null}
69
+ */
70
+ function getCommandNameFromPathEntry(entryName) {
71
+ if (platform !== 'win32') {
72
+ return entryName;
73
+ }
74
+
75
+ const extension = path.extname(entryName).toLowerCase();
76
+ if (!WINDOWS_EXECUTABLE_EXTENSIONS.has(extension)) {
77
+ return null;
54
78
  }
79
+ return entryName.slice(0, -extension.length).toLowerCase();
55
80
  }
56
81
 
57
- /** @returns {boolean} */
58
- function detectCopilot() {
59
- if (fsExists(path.join(getVsCodeUserDataDir(), 'User'))) return true;
60
- if (fsExists(path.join(home, '.vscode'))) return true;
61
- if (platform === 'darwin')
62
- return fsExists('/Applications/Visual Studio Code.app');
82
+ /**
83
+ * @param {readonly string[]} commands
84
+ * @returns {Promise<Set<string>>}
85
+ */
86
+ async function resolveAvailableCommands(commands) {
87
+ const resolved = new Set();
88
+ const unresolved = new Set(commands.map((command) => normalizeCommandName(command)));
89
+ const pathEntries = (process.env.PATH || '').split(PATH_DELIMITER).filter(Boolean);
90
+ const executeAccessMode = platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK;
91
+
92
+ for (const directory of pathEntries) {
93
+ if (unresolved.size === 0) {
94
+ break;
95
+ }
96
+
97
+ let entries;
98
+ try {
99
+ entries = await fs.promises.readdir(directory);
100
+ } catch {
101
+ continue;
102
+ }
103
+
104
+ const matches = await Promise.all(
105
+ entries.map(async (entryName) => {
106
+ const commandName = getCommandNameFromPathEntry(entryName);
107
+ if (!commandName || !unresolved.has(commandName)) {
108
+ return null;
109
+ }
110
+
111
+ try {
112
+ await fs.promises.access(path.join(directory, entryName), executeAccessMode);
113
+ return commandName;
114
+ } catch {
115
+ return null;
116
+ }
117
+ })
118
+ );
119
+
120
+ for (const commandName of matches) {
121
+ if (!commandName) {
122
+ continue;
123
+ }
124
+ unresolved.delete(commandName);
125
+ resolved.add(commandName);
126
+ }
127
+ }
128
+
129
+ return resolved;
130
+ }
131
+
132
+ /**
133
+ * @returns {Promise<Set<string>>}
134
+ */
135
+ function getAvailableCommands() {
136
+ if (!availableCommandsPromise) {
137
+ availableCommandsPromise = resolveAvailableCommands(DEFAULT_COMMAND_NAMES);
138
+ }
139
+ return availableCommandsPromise;
140
+ }
141
+
142
+ /**
143
+ * @param {string} command
144
+ * @returns {Promise<boolean>}
145
+ */
146
+ async function commandExists(command) {
147
+ const availableCommands = await getAvailableCommands();
148
+ return availableCommands.has(normalizeCommandName(command));
149
+ }
150
+
151
+ /** @returns {Promise<boolean>} */
152
+ async function detectCopilot() {
153
+ if (await fsExists(path.join(getVsCodeUserDataDir(), 'User'))) return true;
154
+ if (await fsExists(path.join(home, '.vscode'))) return true;
155
+ if (platform === 'darwin') return fsExists('/Applications/Visual Studio Code.app');
63
156
  if (platform === 'win32') {
64
- const localAppData =
65
- process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
157
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
66
158
  return fsExists(path.join(localAppData, 'Programs', 'Microsoft VS Code'));
67
159
  }
68
160
  return commandExists('code');
69
161
  }
70
162
 
71
- /** @returns {boolean} */
72
- function detectCursor() {
73
- if (fsExists(path.join(home, '.cursor'))) return true;
163
+ /** @returns {Promise<boolean>} */
164
+ async function detectCursor() {
165
+ if (await fsExists(path.join(home, '.cursor'))) return true;
74
166
  if (platform === 'darwin') return fsExists('/Applications/Cursor.app');
75
167
  return commandExists('cursor');
76
168
  }
77
169
 
78
- /** @returns {boolean} */
79
- function detectWindsurf() {
80
- if (fsExists(path.join(home, '.codeium', 'windsurf'))) return true;
170
+ /** @returns {Promise<boolean>} */
171
+ async function detectWindsurf() {
172
+ if (await fsExists(path.join(home, '.codeium', 'windsurf'))) return true;
81
173
  if (platform === 'darwin') return fsExists('/Applications/Windsurf.app');
82
174
  return commandExists('windsurf');
83
175
  }
84
176
 
85
- /** @returns {boolean} */
86
- function detectClaude() {
87
- if (fsExists(path.join(home, '.claude'))) return true;
88
- if (fsExists(path.join(home, '.claude.json'))) return true;
177
+ /** @returns {Promise<boolean>} */
178
+ async function detectClaude() {
179
+ if (await fsExists(path.join(home, '.claude'))) return true;
180
+ if (await fsExists(path.join(home, '.claude.json'))) return true;
89
181
  return commandExists('claude');
90
182
  }
91
183
 
92
- /** @returns {boolean} */
93
- function detectCodex() {
94
- if (fsExists(path.join(home, '.codex'))) return true;
184
+ /** @returns {Promise<boolean>} */
185
+ async function detectCodex() {
186
+ if (await fsExists(path.join(home, '.codex'))) return true;
95
187
  return commandExists('codex');
96
188
  }
97
189
 
98
- /** @returns {boolean} */
99
- function detectOpencode() {
100
- if (fsExists(path.join(home, '.config', 'opencode'))) return true;
101
- if (fsExists(path.join(home, '.opencode'))) return true;
190
+ /** @returns {Promise<boolean>} */
191
+ async function detectOpencode() {
192
+ if (await fsExists(path.join(home, '.config', 'opencode'))) return true;
193
+ if (await fsExists(path.join(home, '.opencode'))) return true;
102
194
  return commandExists('opencode');
103
195
  }
104
196
 
105
- /** @returns {boolean} */
106
- function detectAntigravity() {
107
- if (fsExists(path.join(home, '.gemini', 'antigravity'))) return true;
197
+ /** @returns {Promise<boolean>} */
198
+ async function detectAntigravity() {
199
+ if (await fsExists(path.join(home, '.gemini', 'antigravity'))) return true;
108
200
  return commandExists('agy');
109
201
  }
110
202
 
111
- /** @type {Record<string, () => boolean>} */
203
+ /** @type {Record<string, Detector>} */
112
204
  const DETECTORS = {
113
205
  codex: detectCodex,
114
206
  claude: detectClaude,
@@ -141,26 +233,42 @@ const SKILL_TARGET_KEYS = [
141
233
  'windsurf',
142
234
  ];
143
235
 
236
+ /**
237
+ * @template {string} T
238
+ * @param {readonly T[]} keys
239
+ * @param {Record<string, Detector>} detectors
240
+ * @returns {Promise<T[]>}
241
+ */
242
+ async function detectTargets(keys, detectors) {
243
+ const detectionResults = await Promise.all(
244
+ keys.map(async (name) => ({
245
+ name,
246
+ detected: await (detectors[name]?.() ?? false),
247
+ }))
248
+ );
249
+ return detectionResults.filter((entry) => entry.detected).map((entry) => entry.name);
250
+ }
251
+
144
252
  /**
145
253
  * Detect which MCP clients are installed on this machine.
146
254
  *
147
- * @param {Record<string, () => boolean>} [detectors=DETECTORS]
148
- * @returns {McpClientName[]}
255
+ * @param {Record<string, Detector>} [detectors=DETECTORS]
256
+ * @returns {Promise<McpClientName[]>}
149
257
  */
150
- export function detectMcpClients(detectors = DETECTORS) {
151
- return MCP_CLIENT_KEYS.filter((name) => detectors[name]());
258
+ export async function detectMcpClients(detectors = DETECTORS) {
259
+ return detectTargets(MCP_CLIENT_KEYS, detectors);
152
260
  }
153
261
 
154
262
  /**
155
263
  * Detect which skill targets are installed on this machine.
156
264
  * Always includes 'agents' as a generic fallback.
157
265
  *
158
- * @param {Record<string, () => boolean>} [detectors=DETECTORS]
159
- * @returns {SupportedTarget[]}
266
+ * @param {Record<string, Detector>} [detectors=DETECTORS]
267
+ * @returns {Promise<SupportedTarget[]>}
160
268
  */
161
- export function detectSkillTargets(detectors = DETECTORS) {
269
+ export async function detectSkillTargets(detectors = DETECTORS) {
162
270
  /** @type {SupportedTarget[]} */
163
- const detected = SKILL_TARGET_KEYS.filter((name) => detectors[name]());
271
+ const detected = await detectTargets(SKILL_TARGET_KEYS, detectors);
164
272
  detected.push('agents');
165
273
  return detected;
166
274
  }
@@ -29,9 +29,7 @@ const targetAliases = /** @type {const} */ ({
29
29
 
30
30
  const packageManifest = loadPackageManifest();
31
31
  const managedPackageName =
32
- typeof packageManifest.name === 'string'
33
- ? packageManifest.name
34
- : '@browserbridge/bbx';
32
+ typeof packageManifest.name === 'string' ? packageManifest.name : '@browserbridge/bbx';
35
33
  const managedPackageVersion =
36
34
  typeof packageManifest.version === 'string' ? packageManifest.version : null;
37
35
  const copilotBrowserBridgeNote = [
@@ -91,9 +89,7 @@ export function parseInstallAgentArgs(args, cwd = process.cwd()) {
91
89
  if (arg === '--agents' || arg === '--agent') {
92
90
  const value = args[index + 1];
93
91
  if (!value) {
94
- throw new Error(
95
- 'Usage: install-skill [targets|all] [--project <path>]',
96
- );
92
+ throw new Error('Usage: install-skill [targets|all] [--project <path>]');
97
93
  }
98
94
  targets = parseTargetList(value);
99
95
  index += 1;
@@ -113,9 +109,7 @@ export function parseInstallAgentArgs(args, cwd = process.cwd()) {
113
109
  if (arg === '--project') {
114
110
  const value = args[index + 1];
115
111
  if (!value) {
116
- throw new Error(
117
- 'Usage: install-skill [targets|all] [--project <path>] [--global]',
118
- );
112
+ throw new Error('Usage: install-skill [targets|all] [--project <path>] [--global]');
119
113
  }
120
114
  projectPath = path.resolve(cwd, value);
121
115
  isGlobal = false;
@@ -163,6 +157,8 @@ export function parseInstallAgentArgs(args, cwd = process.cwd()) {
163
157
  export async function installAgentFiles(options) {
164
158
  /** @type {string[]} */
165
159
  const created = [];
160
+ /** @type {Array<{ path: string, existedBefore: boolean }>} */
161
+ const attempted = [];
166
162
  /** @type {Set<string>} */
167
163
  const seenTargets = new Set();
168
164
 
@@ -174,7 +170,14 @@ export async function installAgentFiles(options) {
174
170
  continue;
175
171
  }
176
172
  seenTargets.add(skillTargetDir);
177
- await installManagedSkill(skillName, target, skillTargetDir);
173
+ const existedBefore = await pathExists(skillTargetDir);
174
+ attempted.push({ path: skillTargetDir, existedBefore });
175
+ try {
176
+ await installManagedSkill(skillName, target, skillTargetDir);
177
+ } catch (error) {
178
+ await rollbackInstalledSkillDirs(attempted);
179
+ throw error;
180
+ }
178
181
  created.push(skillTargetDir);
179
182
  }
180
183
  }
@@ -182,6 +185,20 @@ export async function installAgentFiles(options) {
182
185
  return created;
183
186
  }
184
187
 
188
+ /**
189
+ * Remove any newly created managed skill directories after a failed install so a
190
+ * later retry starts cleanly. Pre-existing directories are left untouched.
191
+ *
192
+ * @param {Array<{ path: string, existedBefore: boolean }>} attempted
193
+ * @returns {Promise<void>}
194
+ */
195
+ async function rollbackInstalledSkillDirs(attempted) {
196
+ const rollbackPaths = attempted
197
+ .filter((entry) => !entry.existedBefore)
198
+ .map((entry) => fs.promises.rm(entry.path, { recursive: true, force: true }));
199
+ await Promise.allSettled(rollbackPaths);
200
+ }
201
+
185
202
  /**
186
203
  * Write MCP config for the given clients.
187
204
  *
@@ -204,7 +221,7 @@ export async function installMcpClientSetup(clients, options) {
204
221
  global: options.global,
205
222
  cwd: options.projectPath,
206
223
  stdout: options.stdout,
207
- }),
224
+ })
208
225
  );
209
226
  }
210
227
 
@@ -289,7 +306,7 @@ function parseTargetList(raw) {
289
306
  );
290
307
  if (!canonical) {
291
308
  throw new Error(
292
- `Unknown install-skill target "${value}". Supported targets: ${supportedTargets.join(', ')}, all. Aliases: openai -> codex, google -> antigravity.`,
309
+ `Unknown install-skill target "${value}". Supported targets: ${supportedTargets.join(', ')}, all. Aliases: openai -> codex, google -> antigravity.`
293
310
  );
294
311
  }
295
312
  parsed.add(canonical);
@@ -380,7 +397,7 @@ export function formatManagedSkillSentinel(skillName) {
380
397
  version: managedPackageVersion,
381
398
  },
382
399
  null,
383
- 2,
400
+ 2
384
401
  )}\n`;
385
402
  }
386
403
 
@@ -414,10 +431,7 @@ export function parseManagedSkillSentinel(raw) {
414
431
  * @param {string | null} [currentVersion=managedPackageVersion]
415
432
  * @returns {boolean}
416
433
  */
417
- export function isManagedVersionOutdated(
418
- installedVersion,
419
- currentVersion = managedPackageVersion,
420
- ) {
434
+ export function isManagedVersionOutdated(installedVersion, currentVersion = managedPackageVersion) {
421
435
  if (!currentVersion) {
422
436
  return false;
423
437
  }
@@ -439,19 +453,13 @@ async function installManagedSkill(skillName, target, targetDir) {
439
453
  const targetExists = await pathExists(targetDir);
440
454
 
441
455
  if (targetExists && !(await pathExists(sentinelPath))) {
442
- throw new Error(
443
- `Refusing to overwrite unmanaged skill directory: ${targetDir}`,
444
- );
456
+ throw new Error(`Refusing to overwrite unmanaged skill directory: ${targetDir}`);
445
457
  }
446
458
 
447
459
  await fs.promises.rm(targetDir, { recursive: true, force: true });
448
460
  await copyDir(sourceDir, targetDir);
449
461
  await applyManagedSkillPatches(skillName, target, targetDir);
450
- await fs.promises.writeFile(
451
- sentinelPath,
452
- formatManagedSkillSentinel(skillName),
453
- 'utf8',
454
- );
462
+ await fs.promises.writeFile(sentinelPath, formatManagedSkillSentinel(skillName), 'utf8');
455
463
  }
456
464
 
457
465
  /**
@@ -532,9 +540,7 @@ async function applyManagedSkillPatches(skillName, target, targetDir) {
532
540
  */
533
541
  function loadPackageManifest() {
534
542
  try {
535
- return JSON.parse(
536
- fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
537
- );
543
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
538
544
  } catch {
539
545
  return {};
540
546
  }
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  /**
8
9
  * @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} McpClientName
@@ -41,6 +42,8 @@ export function isMcpClientName(value) {
41
42
  }
42
43
 
43
44
  const BROWSER_BRIDGE_SERVER_NAME = 'browser-bridge';
45
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
46
+ const mcpServerBinPath = path.join(packageRoot, 'packages', 'mcp-server', 'src', 'bin.js');
44
47
 
45
48
  /**
46
49
  * @returns {string}
@@ -48,8 +51,7 @@ const BROWSER_BRIDGE_SERVER_NAME = 'browser-bridge';
48
51
  function getVsCodeUserDataDir() {
49
52
  const home = os.homedir();
50
53
  if (process.platform === 'win32') {
51
- const appData =
52
- process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
54
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
53
55
  return path.join(appData, 'Code');
54
56
  }
55
57
  if (process.platform === 'linux') {
@@ -79,9 +81,7 @@ function getLegacyCopilotVsCodeConfigPath() {
79
81
  * @returns {{ key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }}
80
82
  */
81
83
  export function getMcpConfigShape(clientName) {
82
- return (
83
- MCP_CONFIG_SHAPES[clientName] ?? { key: 'mcpServers', includeType: false }
84
- );
84
+ return MCP_CONFIG_SHAPES[clientName] ?? { key: 'mcpServers', includeType: false };
85
85
  }
86
86
 
87
87
  /**
@@ -96,17 +96,29 @@ export function getMcpConfigShape(clientName) {
96
96
  * }}
97
97
  */
98
98
  function createBaseServerConfig(clientName) {
99
+ const windowsCommand =
100
+ process.platform === 'win32'
101
+ ? {
102
+ command: process.execPath,
103
+ args: [mcpServerBinPath],
104
+ env: {},
105
+ }
106
+ : {
107
+ command: 'bbx',
108
+ args: ['mcp', 'serve'],
109
+ env: {},
110
+ };
111
+
99
112
  if (clientName === 'opencode') {
100
113
  return {
101
114
  type: 'local',
102
- command: ['bbx', 'mcp', 'serve'],
115
+ command:
116
+ process.platform === 'win32'
117
+ ? [process.execPath, mcpServerBinPath]
118
+ : ['bbx', 'mcp', 'serve'],
103
119
  };
104
120
  }
105
- return {
106
- command: 'bbx',
107
- args: ['mcp', 'serve'],
108
- env: {},
109
- };
121
+ return windowsCommand;
110
122
  }
111
123
 
112
124
  /** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
@@ -132,20 +144,20 @@ const MCP_CONFIG_SHAPES = {
132
144
  */
133
145
  export function buildMcpConfig(clientName) {
134
146
  if (clientName === 'codex') {
147
+ const command = process.platform === 'win32' ? process.execPath : 'bbx';
148
+ const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
135
149
  return {
136
150
  mcp_servers: {
137
151
  [BROWSER_BRIDGE_SERVER_NAME]: {
138
- command: 'bbx',
139
- args: ['mcp', 'serve'],
152
+ command,
153
+ args,
140
154
  },
141
155
  },
142
156
  };
143
157
  }
144
158
  const serverConfig = createBaseServerConfig(clientName);
145
159
  const shape = getMcpConfigShape(clientName);
146
- const entry = shape.includeType
147
- ? { type: 'stdio', ...serverConfig }
148
- : serverConfig;
160
+ const entry = shape.includeType ? { type: 'stdio', ...serverConfig } : serverConfig;
149
161
  return { [shape.key]: { [BROWSER_BRIDGE_SERVER_NAME]: entry } };
150
162
  }
151
163
 
@@ -169,10 +181,7 @@ export function formatMcpConfig(clientName) {
169
181
  * @param {{ global: boolean, cwd?: string }} options
170
182
  * @returns {string}
171
183
  */
172
- export function getMcpConfigPath(
173
- clientName,
174
- { global: isGlobal, cwd = process.cwd() },
175
- ) {
184
+ export function getMcpConfigPath(clientName, { global: isGlobal, cwd = process.cwd() }) {
176
185
  const home = os.homedir();
177
186
 
178
187
  if (!isGlobal) {
@@ -252,7 +261,9 @@ export async function getMcpConfigPaths(clientName, options) {
252
261
  const entries = await readdir(profilesDir, { withFileTypes: true });
253
262
  const profilePaths = entries
254
263
  .filter((/** @type {import('node:fs').Dirent} */ entry) => entry.isDirectory())
255
- .map((/** @type {import('node:fs').Dirent} */ entry) => path.join(profilesDir, entry.name, 'mcp.json'));
264
+ .map((/** @type {import('node:fs').Dirent} */ entry) =>
265
+ path.join(profilesDir, entry.name, 'mcp.json')
266
+ );
256
267
  for (const profilePath of profilePaths) {
257
268
  if (!paths.includes(profilePath)) {
258
269
  paths.push(profilePath);
@@ -268,10 +279,12 @@ export async function getMcpConfigPaths(clientName, options) {
268
279
  * @returns {string}
269
280
  */
270
281
  function formatCodexServerBlock() {
282
+ const command = process.platform === 'win32' ? process.execPath : 'bbx';
283
+ const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
271
284
  return [
272
285
  `[mcp_servers."${BROWSER_BRIDGE_SERVER_NAME}"]`,
273
- 'command = "bbx"',
274
- 'args = ["mcp", "serve"]',
286
+ `command = ${JSON.stringify(command)}`,
287
+ `args = ${JSON.stringify(args)}`,
275
288
  '',
276
289
  ].join('\n');
277
290
  }
@@ -533,11 +546,7 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
533
546
  }
534
547
 
535
548
  await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
536
- await fs.promises.writeFile(
537
- configPath,
538
- `${JSON.stringify(merged, null, 2)}\n`,
539
- 'utf8',
540
- );
549
+ await fs.promises.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
541
550
  stdout.write(`Wrote ${configPath}\n`);
542
551
  }
543
552
 
@@ -600,11 +609,7 @@ async function removeJsonMcpConfig(clientName, configPath, stdout) {
600
609
  }
601
610
 
602
611
  await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
603
- await fs.promises.writeFile(
604
- configPath,
605
- `${JSON.stringify(merged, null, 2)}\n`,
606
- 'utf8',
607
- );
612
+ await fs.promises.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
608
613
  stdout.write(`Removed ${configPath}\n`);
609
614
  return true;
610
615
  }
@@ -647,16 +652,11 @@ export function parseInstalledMcpConfig(clientName, raw) {
647
652
  const parsed = JSON.parse(raw);
648
653
  const block =
649
654
  parsed && typeof parsed === 'object' && !Array.isArray(parsed)
650
- ? getMergedJsonMcpBlock(
651
- /** @type {Record<string, unknown>} */ (parsed),
652
- clientName,
653
- )
655
+ ? getMergedJsonMcpBlock(/** @type {Record<string, unknown>} */ (parsed), clientName)
654
656
  : {};
655
657
  return {
656
658
  configured: Boolean(
657
- block &&
658
- typeof block === 'object' &&
659
- Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME),
659
+ block && typeof block === 'object' && Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME)
660
660
  ),
661
661
  };
662
662
  } catch {