@browserbridge/bbx 1.0.1 → 1.2.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 (70) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +122 -45
  5. package/packages/agent-client/src/client.js +134 -8
  6. package/packages/agent-client/src/command-registry.js +4 -1
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers-capture.js +279 -0
  13. package/packages/mcp-server/src/handlers-dom.js +196 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +296 -0
  17. package/packages/mcp-server/src/handlers.js +63 -1159
  18. package/packages/mcp-server/src/server.js +13 -3
  19. package/packages/native-host/bin/bridge-daemon.js +34 -4
  20. package/packages/native-host/bin/install-manifest.js +32 -2
  21. package/packages/native-host/bin/postinstall.js +16 -0
  22. package/packages/native-host/src/config.js +131 -6
  23. package/packages/native-host/src/daemon-logger.js +157 -0
  24. package/packages/native-host/src/daemon-process.js +422 -0
  25. package/packages/native-host/src/daemon.js +322 -77
  26. package/packages/native-host/src/framing.js +131 -11
  27. package/packages/native-host/src/install-manifest.js +121 -7
  28. package/packages/native-host/src/native-host.js +110 -73
  29. package/packages/protocol/src/capabilities.js +4 -0
  30. package/packages/protocol/src/defaults.js +1 -0
  31. package/packages/protocol/src/errors.js +4 -0
  32. package/packages/protocol/src/payload-cost.js +19 -6
  33. package/packages/protocol/src/protocol.js +143 -7
  34. package/packages/protocol/src/registry.js +13 -0
  35. package/packages/protocol/src/summary.js +18 -10
  36. package/packages/protocol/src/types.js +28 -3
  37. package/skills/browser-bridge/SKILL.md +2 -1
  38. package/skills/browser-bridge/references/interaction.md +1 -0
  39. package/skills/browser-bridge/references/protocol.md +2 -1
  40. package/CHANGELOG.md +0 -55
  41. package/assets/banner.jpg +0 -0
  42. package/assets/logo.png +0 -0
  43. package/assets/logo.svg +0 -65
  44. package/docs/api-reference.md +0 -157
  45. package/docs/cli-guide.md +0 -128
  46. package/docs/index.md +0 -25
  47. package/docs/manual-setup.md +0 -140
  48. package/docs/mcp-vs-cli.md +0 -258
  49. package/docs/publishing.md +0 -112
  50. package/docs/quickstart.md +0 -104
  51. package/docs/troubleshooting.md +0 -59
  52. package/docs/unpacked-extension.md +0 -72
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -38
  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 -474
  60. package/packages/extension/src/background-routing.js +0 -89
  61. package/packages/extension/src/background.js +0 -3490
  62. package/packages/extension/src/content-script-helpers.js +0 -282
  63. package/packages/extension/src/content-script.js +0 -2043
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -104
  66. package/packages/extension/ui/popup.html +0 -35
  67. package/packages/extension/ui/popup.js +0 -298
  68. package/packages/extension/ui/sidepanel.html +0 -102
  69. package/packages/extension/ui/sidepanel.js +0 -1771
  70. package/packages/extension/ui/ui.css +0 -1160
@@ -1,15 +1,32 @@
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}
@@ -26,12 +43,12 @@ function getVsCodeUserDataDir() {
26
43
  }
27
44
 
28
45
  /**
29
- * @param {string} p
30
- * @returns {boolean}
46
+ * @param {string} targetPath
47
+ * @returns {Promise<boolean>}
31
48
  */
32
- function fsExists(p) {
49
+ async function fsExists(targetPath) {
33
50
  try {
34
- fs.accessSync(p);
51
+ await fs.promises.access(targetPath, fs.constants.F_OK);
35
52
  return true;
36
53
  } catch {
37
54
  return false;
@@ -39,24 +56,102 @@ function fsExists(p) {
39
56
  }
40
57
 
41
58
  /**
42
- * @param {string} cmd
43
- * @returns {boolean}
59
+ * @param {string} command
60
+ * @returns {string}
44
61
  */
45
- function commandExists(cmd) {
46
- try {
47
- execFileSync(platform === 'win32' ? 'where' : 'which', [cmd], {
48
- stdio: 'ignore',
49
- });
50
- return true;
51
- } catch {
52
- 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;
53
78
  }
79
+ return entryName.slice(0, -extension.length).toLowerCase();
54
80
  }
55
81
 
56
- /** @returns {boolean} */
57
- function detectCopilot() {
58
- if (fsExists(path.join(getVsCodeUserDataDir(), 'User'))) return true;
59
- if (fsExists(path.join(home, '.vscode'))) return true;
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;
60
155
  if (platform === 'darwin') return fsExists('/Applications/Visual Studio Code.app');
61
156
  if (platform === 'win32') {
62
157
  const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
@@ -65,47 +160,47 @@ function detectCopilot() {
65
160
  return commandExists('code');
66
161
  }
67
162
 
68
- /** @returns {boolean} */
69
- function detectCursor() {
70
- 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;
71
166
  if (platform === 'darwin') return fsExists('/Applications/Cursor.app');
72
167
  return commandExists('cursor');
73
168
  }
74
169
 
75
- /** @returns {boolean} */
76
- function detectWindsurf() {
77
- 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;
78
173
  if (platform === 'darwin') return fsExists('/Applications/Windsurf.app');
79
174
  return commandExists('windsurf');
80
175
  }
81
176
 
82
- /** @returns {boolean} */
83
- function detectClaude() {
84
- if (fsExists(path.join(home, '.claude'))) return true;
85
- 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;
86
181
  return commandExists('claude');
87
182
  }
88
183
 
89
- /** @returns {boolean} */
90
- function detectCodex() {
91
- 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;
92
187
  return commandExists('codex');
93
188
  }
94
189
 
95
- /** @returns {boolean} */
96
- function detectOpencode() {
97
- if (fsExists(path.join(home, '.config', 'opencode'))) return true;
98
- 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;
99
194
  return commandExists('opencode');
100
195
  }
101
196
 
102
- /** @returns {boolean} */
103
- function detectAntigravity() {
104
- 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;
105
200
  return commandExists('agy');
106
201
  }
107
202
 
108
- /** @type {Record<string, () => boolean>} */
203
+ /** @type {Record<string, Detector>} */
109
204
  const DETECTORS = {
110
205
  codex: detectCodex,
111
206
  claude: detectClaude,
@@ -138,26 +233,42 @@ const SKILL_TARGET_KEYS = [
138
233
  'windsurf',
139
234
  ];
140
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
+
141
252
  /**
142
253
  * Detect which MCP clients are installed on this machine.
143
254
  *
144
- * @param {Record<string, () => boolean>} [detectors=DETECTORS]
145
- * @returns {McpClientName[]}
255
+ * @param {Record<string, Detector>} [detectors=DETECTORS]
256
+ * @returns {Promise<McpClientName[]>}
146
257
  */
147
- export function detectMcpClients(detectors = DETECTORS) {
148
- return MCP_CLIENT_KEYS.filter((name) => detectors[name]());
258
+ export async function detectMcpClients(detectors = DETECTORS) {
259
+ return detectTargets(MCP_CLIENT_KEYS, detectors);
149
260
  }
150
261
 
151
262
  /**
152
263
  * Detect which skill targets are installed on this machine.
153
264
  * Always includes 'agents' as a generic fallback.
154
265
  *
155
- * @param {Record<string, () => boolean>} [detectors=DETECTORS]
156
- * @returns {SupportedTarget[]}
266
+ * @param {Record<string, Detector>} [detectors=DETECTORS]
267
+ * @returns {Promise<SupportedTarget[]>}
157
268
  */
158
- export function detectSkillTargets(detectors = DETECTORS) {
269
+ export async function detectSkillTargets(detectors = DETECTORS) {
159
270
  /** @type {SupportedTarget[]} */
160
- const detected = SKILL_TARGET_KEYS.filter((name) => detectors[name]());
271
+ const detected = await detectTargets(SKILL_TARGET_KEYS, detectors);
161
272
  detected.push('agents');
162
273
  return detected;
163
274
  }
@@ -157,6 +157,8 @@ export function parseInstallAgentArgs(args, cwd = process.cwd()) {
157
157
  export async function installAgentFiles(options) {
158
158
  /** @type {string[]} */
159
159
  const created = [];
160
+ /** @type {Array<{ path: string, existedBefore: boolean }>} */
161
+ const attempted = [];
160
162
  /** @type {Set<string>} */
161
163
  const seenTargets = new Set();
162
164
 
@@ -168,7 +170,14 @@ export async function installAgentFiles(options) {
168
170
  continue;
169
171
  }
170
172
  seenTargets.add(skillTargetDir);
171
- 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
+ }
172
181
  created.push(skillTargetDir);
173
182
  }
174
183
  }
@@ -176,6 +185,20 @@ export async function installAgentFiles(options) {
176
185
  return created;
177
186
  }
178
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
+
179
202
  /**
180
203
  * Write MCP config for the given clients.
181
204
  *
@@ -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}
@@ -93,17 +96,29 @@ export function getMcpConfigShape(clientName) {
93
96
  * }}
94
97
  */
95
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
+
96
112
  if (clientName === 'opencode') {
97
113
  return {
98
114
  type: 'local',
99
- command: ['bbx', 'mcp', 'serve'],
115
+ command:
116
+ process.platform === 'win32'
117
+ ? [process.execPath, mcpServerBinPath]
118
+ : ['bbx', 'mcp', 'serve'],
100
119
  };
101
120
  }
102
- return {
103
- command: 'bbx',
104
- args: ['mcp', 'serve'],
105
- env: {},
106
- };
121
+ return windowsCommand;
107
122
  }
108
123
 
109
124
  /** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
@@ -129,11 +144,13 @@ const MCP_CONFIG_SHAPES = {
129
144
  */
130
145
  export function buildMcpConfig(clientName) {
131
146
  if (clientName === 'codex') {
147
+ const command = process.platform === 'win32' ? process.execPath : 'bbx';
148
+ const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
132
149
  return {
133
150
  mcp_servers: {
134
151
  [BROWSER_BRIDGE_SERVER_NAME]: {
135
- command: 'bbx',
136
- args: ['mcp', 'serve'],
152
+ command,
153
+ args,
137
154
  },
138
155
  },
139
156
  };
@@ -262,10 +279,12 @@ export async function getMcpConfigPaths(clientName, options) {
262
279
  * @returns {string}
263
280
  */
264
281
  function formatCodexServerBlock() {
282
+ const command = process.platform === 'win32' ? process.execPath : 'bbx';
283
+ const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
265
284
  return [
266
285
  `[mcp_servers."${BROWSER_BRIDGE_SERVER_NAME}"]`,
267
- 'command = "bbx"',
268
- 'args = ["mcp", "serve"]',
286
+ `command = ${JSON.stringify(command)}`,
287
+ `args = ${JSON.stringify(args)}`,
269
288
  '',
270
289
  ].join('\n');
271
290
  }
@@ -57,8 +57,8 @@ const SKILL_TARGET_LABELS = {
57
57
  * global?: boolean,
58
58
  * cwd?: string,
59
59
  * projectPath?: string,
60
- * mcpDetectors?: Record<string, () => boolean>,
61
- * skillDetectors?: Record<string, () => boolean>,
60
+ * mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
61
+ * skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
62
62
  * access?: (targetPath: string) => Promise<void>,
63
63
  * readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
64
64
  * }} SetupStatusOptions
@@ -76,8 +76,16 @@ export async function collectSetupStatus(options = {}) {
76
76
  const projectPath = options.projectPath || cwd;
77
77
  const access = options.access || fs.promises.access.bind(fs.promises);
78
78
  const readFile = options.readFile || fs.promises.readFile.bind(fs.promises);
79
- const detectedMcpClients = new Set(detectMcpClients(options.mcpDetectors));
80
- const detectedSkillTargets = new Set(detectSkillTargets(options.skillDetectors));
79
+ const [detectedMcpClientResult, detectedSkillTargetResult] = await Promise.allSettled([
80
+ detectMcpClients(options.mcpDetectors),
81
+ detectSkillTargets(options.skillDetectors),
82
+ ]);
83
+ const detectedMcpClientNames =
84
+ detectedMcpClientResult.status === 'fulfilled' ? detectedMcpClientResult.value : [];
85
+ const detectedSkillTargetNames =
86
+ detectedSkillTargetResult.status === 'fulfilled' ? detectedSkillTargetResult.value : [];
87
+ const detectedMcpClients = new Set(detectedMcpClientNames);
88
+ const detectedSkillTargets = new Set(detectedSkillTargetNames);
81
89
  for (const clientName of detectedMcpClients) {
82
90
  if (SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))) {
83
91
  detectedSkillTargets.add(/** @type {SupportedTarget} */ (clientName));
@@ -1,10 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
4
7
  import { startBridgeMcpServer } from './server.js';
5
8
 
6
- startBridgeMcpServer().catch((error) => {
7
- const message = error instanceof Error ? error.stack || error.message : String(error);
8
- process.stderr.write(`${message}\n`);
9
- process.exit(1);
10
- });
9
+ /**
10
+ * @typedef {{
11
+ * start?: () => Promise<void>,
12
+ * argv?: string[],
13
+ * stdout?: { write: (chunk: string) => unknown },
14
+ * stderr?: { write: (chunk: string) => unknown },
15
+ * exit?: (code: number) => unknown,
16
+ * }} BridgeMcpCliOptions
17
+ */
18
+
19
+ const HELP_FLAGS = new Set(['help', '--help', '-h']);
20
+
21
+ const MCP_USAGE = [
22
+ 'Usage: bbx-mcp [--help]',
23
+ '',
24
+ 'Start the Browser Bridge MCP stdio server.',
25
+ ].join('\n');
26
+
27
+ /**
28
+ * Start the MCP server CLI and report startup failures to stderr.
29
+ *
30
+ * @param {BridgeMcpCliOptions} [options]
31
+ * @returns {Promise<number>}
32
+ */
33
+ export async function runBridgeMcpCli(options = {}) {
34
+ const {
35
+ start = startBridgeMcpServer,
36
+ argv = process.argv.slice(2),
37
+ stdout = process.stdout,
38
+ stderr = process.stderr,
39
+ exit = process.exit,
40
+ } = options;
41
+
42
+ if (argv.some((arg) => HELP_FLAGS.has(arg))) {
43
+ stdout.write(`${MCP_USAGE}\n`);
44
+ return 0;
45
+ }
46
+
47
+ try {
48
+ await start();
49
+ return 0;
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.stack || error.message : String(error);
52
+ stderr.write(`${message}\n`);
53
+ exit(1);
54
+ return 1;
55
+ }
56
+ }
57
+
58
+ const entryPointUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
59
+
60
+ if (entryPointUrl === import.meta.url) {
61
+ void runBridgeMcpCli();
62
+ }