@browserbridge/bbx 1.0.1 → 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 (63) 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 +116 -41
  5. package/packages/agent-client/src/client.js +29 -4
  6. package/packages/agent-client/src/command-registry.js +3 -0
  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.js +28 -7
  13. package/packages/mcp-server/src/server.js +12 -2
  14. package/packages/native-host/bin/bridge-daemon.js +33 -4
  15. package/packages/native-host/bin/install-manifest.js +24 -2
  16. package/packages/native-host/src/config.js +131 -6
  17. package/packages/native-host/src/daemon-process.js +396 -0
  18. package/packages/native-host/src/daemon.js +217 -68
  19. package/packages/native-host/src/framing.js +131 -11
  20. package/packages/native-host/src/install-manifest.js +121 -7
  21. package/packages/native-host/src/native-host.js +110 -73
  22. package/packages/protocol/src/capabilities.js +3 -0
  23. package/packages/protocol/src/defaults.js +1 -0
  24. package/packages/protocol/src/errors.js +4 -0
  25. package/packages/protocol/src/payload-cost.js +19 -6
  26. package/packages/protocol/src/protocol.js +143 -7
  27. package/packages/protocol/src/registry.js +11 -0
  28. package/packages/protocol/src/summary.js +18 -10
  29. package/packages/protocol/src/types.js +28 -3
  30. package/skills/browser-bridge/SKILL.md +2 -1
  31. package/skills/browser-bridge/references/interaction.md +1 -0
  32. package/skills/browser-bridge/references/protocol.md +2 -1
  33. package/CHANGELOG.md +0 -55
  34. package/assets/banner.jpg +0 -0
  35. package/assets/logo.png +0 -0
  36. package/assets/logo.svg +0 -65
  37. package/docs/api-reference.md +0 -157
  38. package/docs/cli-guide.md +0 -128
  39. package/docs/index.md +0 -25
  40. package/docs/manual-setup.md +0 -140
  41. package/docs/mcp-vs-cli.md +0 -258
  42. package/docs/publishing.md +0 -112
  43. package/docs/quickstart.md +0 -104
  44. package/docs/troubleshooting.md +0 -59
  45. package/docs/unpacked-extension.md +0 -72
  46. package/docs/usage-scenarios.md +0 -136
  47. package/manifest.json +0 -38
  48. package/packages/extension/assets/icon-128.png +0 -0
  49. package/packages/extension/assets/icon-16.png +0 -0
  50. package/packages/extension/assets/icon-32.png +0 -0
  51. package/packages/extension/assets/icon-48.png +0 -0
  52. package/packages/extension/src/background-helpers.js +0 -474
  53. package/packages/extension/src/background-routing.js +0 -89
  54. package/packages/extension/src/background.js +0 -3490
  55. package/packages/extension/src/content-script-helpers.js +0 -282
  56. package/packages/extension/src/content-script.js +0 -2043
  57. package/packages/extension/src/debugger-coordinator.js +0 -188
  58. package/packages/extension/src/sidepanel-helpers.js +0 -104
  59. package/packages/extension/ui/popup.html +0 -35
  60. package/packages/extension/ui/popup.js +0 -298
  61. package/packages/extension/ui/sidepanel.html +0 -102
  62. package/packages/extension/ui/sidepanel.js +0 -1771
  63. 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
+ }
@@ -1,7 +1,9 @@
1
1
  // @ts-check
2
2
 
3
+ import os from 'node:os';
3
4
  import {
4
5
  bridgeMethodNeedsTab,
6
+ createRuntimeContext,
5
7
  DEFAULT_CONSOLE_LIMIT,
6
8
  DEFAULT_MAX_HTML_LENGTH,
7
9
  DEFAULT_NETWORK_LIMIT,
@@ -9,7 +11,7 @@ import {
9
11
  getBudgetPreset,
10
12
  getErrorRecovery,
11
13
  isBudgetPresetName,
12
- METHODS,
14
+ METHOD_SET,
13
15
  summarizeBatchErrorItem,
14
16
  summarizeBatchResponseItem,
15
17
  } from '../../protocol/src/index.js';
@@ -20,11 +22,13 @@ import {
20
22
  withBridgeClient,
21
23
  } from '../../agent-client/src/runtime.js';
22
24
  import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-client/src/subagent.js';
25
+ import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
23
26
 
24
27
  /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
25
28
  /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
26
29
 
27
30
  const REQUEST_SOURCE = 'mcp';
31
+ const HOME_DIR = os.homedir();
28
32
 
29
33
  /**
30
34
  * @typedef {{
@@ -594,6 +598,7 @@ export const INPUT_ACTION_METHODS = {
594
598
  focus: 'input.focus',
595
599
  type: 'input.type',
596
600
  press_key: 'input.press_key',
601
+ cdp_press_key: 'cdp.dispatch_key_event',
597
602
  set_checked: 'input.set_checked',
598
603
  select_option: 'input.select_option',
599
604
  hover: 'input.hover',
@@ -602,7 +607,7 @@ export const INPUT_ACTION_METHODS = {
602
607
  };
603
608
 
604
609
  /**
605
- * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
610
+ * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
606
611
  * @returns {Promise<ToolResult>}
607
612
  */
608
613
  export async function handleInputTool(args) {
@@ -681,6 +686,23 @@ export async function handleInputTool(args) {
681
686
  );
682
687
  return summarizeToolResponse(response, 'input.press_key');
683
688
  }
689
+ case 'cdp_press_key': {
690
+ const response = await requestBridge(
691
+ client,
692
+ 'cdp.dispatch_key_event',
693
+ {
694
+ key: args.key,
695
+ code: args.code,
696
+ modifiers: args.modifiers,
697
+ },
698
+ {
699
+ tabId: requestedTabId,
700
+ source: REQUEST_SOURCE,
701
+ tokenBudget: getToolTokenBudget(args),
702
+ }
703
+ );
704
+ return summarizeToolResponse(response, 'cdp.dispatch_key_event');
705
+ }
684
706
  case 'set_checked': {
685
707
  const response = await requestBridge(
686
708
  client,
@@ -896,7 +918,6 @@ export async function handleCaptureTool(args) {
896
918
  */
897
919
  export async function handleSkillTool() {
898
920
  try {
899
- const { createRuntimeContext } = await import('../../protocol/src/index.js');
900
921
  const ctx = createRuntimeContext();
901
922
  return createToolResult('Runtime context retrieved.', {
902
923
  ok: true,
@@ -914,8 +935,7 @@ export async function handleSkillTool() {
914
935
  * @returns {Promise<ToolResult>}
915
936
  */
916
937
  export async function handleSetupTool(args) {
917
- const { collectSetupStatus } = await import('../../agent-client/src/setup-status.js');
918
- const projectPath = args.global !== false ? (await import('node:os')).homedir() : process.cwd();
938
+ const projectPath = args.global !== false ? HOME_DIR : process.cwd();
919
939
  const status = await collectSetupStatus({
920
940
  global: args.global !== false,
921
941
  cwd: process.cwd(),
@@ -993,7 +1013,7 @@ export async function handleBatchTool(args) {
993
1013
  };
994
1014
  }
995
1015
 
996
- if (!METHODS.includes(/** @type {BridgeMethod} */ (call.method))) {
1016
+ if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
997
1017
  return {
998
1018
  method: call.method,
999
1019
  tabId: null,
@@ -1065,7 +1085,7 @@ export async function handleBatchTool(args) {
1065
1085
  * @returns {Promise<ToolResult>}
1066
1086
  */
1067
1087
  export async function handleRawCallTool(args) {
1068
- if (!METHODS.includes(/** @type {BridgeMethod} */ (args.method))) {
1088
+ if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
1069
1089
  return summarizeToolError(`Unknown bridge method "${args.method}".`);
1070
1090
  }
1071
1091
 
@@ -1247,6 +1267,7 @@ export async function handleInvestigateTool(args) {
1247
1267
  scope: scopeName,
1248
1268
  heuristicFallback: true,
1249
1269
  steps: stepResults,
1270
+ failedSteps,
1250
1271
  },
1251
1272
  !allOk
1252
1273
  );
@@ -37,6 +37,7 @@ import {
37
37
  DEFAULT_WAIT_TIMEOUT_MS,
38
38
  getMethodsByMaxComplexity,
39
39
  } from '../../protocol/src/index.js';
40
+ import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
40
41
 
41
42
  export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
42
43
  export const TAB_ID_DESCRIPTION =
@@ -369,7 +370,7 @@ export function createBridgeMcpServer() {
369
370
  {
370
371
  title: 'Browser Input',
371
372
  description:
372
- 'Simulate user input: click, focus, type, press keys, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
373
+ 'Simulate user input: click, focus, type, press keys, CDP key events, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
373
374
  inputSchema: {
374
375
  action: z
375
376
  .enum([
@@ -377,6 +378,7 @@ export function createBridgeMcpServer() {
377
378
  'focus',
378
379
  'type',
379
380
  'press_key',
381
+ 'cdp_press_key',
380
382
  'set_checked',
381
383
  'select_option',
382
384
  'hover',
@@ -402,7 +404,14 @@ export function createBridgeMcpServer() {
402
404
  text: z.string().optional().describe('Text to type (for type action)'),
403
405
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
404
406
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
405
- key: z.string().optional().describe('Key to press (e.g., "Enter", "Tab", "ArrowDown")'),
407
+ key: z
408
+ .string()
409
+ .optional()
410
+ .describe('Key to press (e.g., "Escape", "Enter", "Tab", "ArrowDown")'),
411
+ code: z
412
+ .string()
413
+ .optional()
414
+ .describe('Optional physical key code for cdp_press_key (e.g., "Escape", "KeyA")'),
406
415
  modifiers: z
407
416
  .array(z.enum(['Alt', 'Control', 'Meta', 'Shift']))
408
417
  .optional()
@@ -652,6 +661,7 @@ export function createBridgeMcpServer() {
652
661
  * @returns {Promise<void>}
653
662
  */
654
663
  export async function startBridgeMcpServer() {
664
+ applyWindowsTcpTransportDefaults();
655
665
  const server = createBridgeMcpServer();
656
666
  const transport = new StdioServerTransport();
657
667
  await server.connect(transport);