@browserbridge/bbx 1.2.0 → 1.4.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 (35) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -2
  3. package/packages/agent-client/src/cli.js +56 -31
  4. package/packages/agent-client/src/client.js +81 -65
  5. package/packages/agent-client/src/command-registry.js +4 -15
  6. package/packages/agent-client/src/detect.js +3 -3
  7. package/packages/agent-client/src/install.js +3 -7
  8. package/packages/agent-client/src/mcp-config.js +20 -5
  9. package/packages/agent-client/src/runtime.js +7 -41
  10. package/packages/agent-client/src/setup-status.js +3 -13
  11. package/packages/agent-client/src/types.ts +139 -0
  12. package/packages/mcp-server/src/guidance.js +241 -0
  13. package/packages/mcp-server/src/handlers-capture.js +91 -16
  14. package/packages/mcp-server/src/handlers-dom.js +59 -4
  15. package/packages/mcp-server/src/handlers-navigation.js +22 -2
  16. package/packages/mcp-server/src/handlers-page.js +6 -11
  17. package/packages/mcp-server/src/handlers-utils.js +69 -1
  18. package/packages/mcp-server/src/server.js +111 -28
  19. package/packages/native-host/bin/postinstall.js +42 -21
  20. package/packages/native-host/src/auth-token.js +92 -0
  21. package/packages/native-host/src/daemon-process.js +1 -2
  22. package/packages/native-host/src/daemon.js +199 -30
  23. package/packages/native-host/src/framing.js +13 -0
  24. package/packages/native-host/src/native-host.js +25 -7
  25. package/packages/protocol/src/defaults.js +3 -0
  26. package/packages/protocol/src/json-lines.js +29 -1
  27. package/packages/protocol/src/protocol.js +43 -0
  28. package/packages/protocol/src/registry.js +3 -9
  29. package/packages/protocol/src/types.ts +574 -0
  30. package/skills/browser-bridge/SKILL.md +21 -5
  31. package/skills/browser-bridge/agents/openai.yaml +1 -1
  32. package/skills/browser-bridge/references/interaction.md +6 -6
  33. package/skills/browser-bridge/references/protocol.md +57 -54
  34. package/skills/browser-bridge/references/ui-workflows.md +1 -1
  35. package/packages/protocol/src/types.js +0 -626
package/README.md CHANGED
@@ -99,17 +99,20 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
99
99
 
100
100
  ## Why Browser Bridge
101
101
 
102
- Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, but is vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
102
+ Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, and the [Codex extension](https://chromewebstore.google.com/detail/codex/hehggadaopoacecdllhhajmbjkdcmajg) is a great option if you use Codex, but both are vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
103
103
 
104
104
  Browser Bridge is optimized for the opposite starting point: **inspect the state that already exists** in a real tab - logged-in sessions, feature flags, seeded storage, SPA state - use structured reads to understand it, test a patch in place, then fix the source. It's open-source, agent-agnostic, and scoped to explicit tab sessions rather than ambient browser control.
105
105
 
106
106
  ## Setup
107
107
 
108
- 1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie)
108
+ 1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie) in Chrome or another Chromium-based browser
109
109
  2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
110
- 3. In the extension side panel, install MCP or CLI (skill) for your agent of choice
111
- 4. Enable Browser Bridge for the Chrome window you want to inspect/control with the AI agent
112
- 5. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the `browser-bridge` / `$bbx` skill in CLI mode
110
+ 3. Run `bbx install`, or target a specific browser with `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
111
+ 4. In the extension side panel, install MCP or CLI (skill) for your agent of choice
112
+ 5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
113
+ 6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the installed Browser Bridge skill in CLI mode (`/browser-bridge`, `browser-bridge`, or the client-specific skill trigger)
114
+
115
+ MCP mode is self-contained: the server exposes tools, startup instructions, and prompt templates, so a separate CLI skill is not required for MCP guidance.
113
116
 
114
117
  ## How it works
115
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -61,7 +61,7 @@
61
61
  "postinstall": "node packages/native-host/bin/postinstall.js",
62
62
  "package:extension": "node scripts/package-extension.mjs",
63
63
  "check:extension-zip": "node scripts/check-extension-zip.mjs",
64
- "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm pack --dry-run",
64
+ "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm run check:extension-zip && npm pack --dry-run",
65
65
  "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run coverage:check",
66
66
  "status": "node packages/agent-client/src/cli.js status",
67
67
  "daemon": "node packages/native-host/bin/bridge-daemon.js",
@@ -50,8 +50,8 @@ import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
50
50
  import { collectSetupStatus } from './setup-status.js';
51
51
  import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
52
52
 
53
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
54
- /** @typedef {{ image: string, rect: Record<string, unknown> }} ScreenshotResult */
53
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
54
+ /** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
55
55
 
56
56
  const REQUEST_SOURCE = 'cli';
57
57
  const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
@@ -128,19 +128,25 @@ if (command === 'install-skill') {
128
128
  const positional = rest.filter((a) => !a.startsWith('--'));
129
129
 
130
130
  if (positional.length === 0) {
131
- // Parse scope flags without going through parseInstallAgentArgs.
132
- let isGlobal = true;
133
- if (rest.includes('--local')) isGlobal = false;
134
- if (rest.includes('--global')) isGlobal = true;
131
+ let scopeOptions;
132
+ try {
133
+ scopeOptions = parseInstallAgentArgs(rest);
134
+ } catch (error) {
135
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
136
+ process.exit(1);
137
+ }
138
+
139
+ const isGlobal = scopeOptions.global !== false;
140
+ const projectPath = isGlobal ? os.homedir() : scopeOptions.projectPath;
135
141
 
136
142
  const setupStatus = await collectSetupStatus({
137
143
  global: isGlobal,
138
144
  cwd: process.cwd(),
139
- projectPath: isGlobal ? os.homedir() : process.cwd(),
145
+ projectPath,
140
146
  ...getSetupStatusTestOverrides(),
141
147
  });
142
- /** @type {import('./install.js').SupportedTarget[]} */
143
- const detected = /** @type {import('./install.js').SupportedTarget[]} */ (
148
+ /** @type {import('./types.js').SupportedTarget[]} */
149
+ const detected = /** @type {import('./types.js').SupportedTarget[]} */ (
144
150
  setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
145
151
  );
146
152
  const installedManagedTargets = new Set(
@@ -149,7 +155,7 @@ if (command === 'install-skill') {
149
155
  .map((entry) => entry.key)
150
156
  );
151
157
  const installedManagedTargetList =
152
- /** @type {import('./install.js').SupportedTarget[]} */ ([...installedManagedTargets]);
158
+ /** @type {import('./types.js').SupportedTarget[]} */ ([...installedManagedTargets]);
153
159
 
154
160
  // Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
155
161
  const items = SUPPORTED_TARGETS.map((t) => ({
@@ -167,19 +173,18 @@ if (command === 'install-skill') {
167
173
  items
168
174
  );
169
175
 
170
- /** @type {import('./install.js').SupportedTarget[]} */
176
+ /** @type {import('./types.js').SupportedTarget[]} */
171
177
  let targets;
172
178
  if (selected === null) {
173
179
  // Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
174
180
  targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
175
181
  } else {
176
- targets = /** @type {import('./install.js').SupportedTarget[]} */ (selected);
182
+ targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
177
183
  }
178
184
 
179
- const projectPath = isGlobal ? os.homedir() : process.cwd();
180
185
  if (selected !== null) {
181
186
  const deselectedTargets =
182
- /** @type {import('./install.js').SupportedTarget[]} */ (
187
+ /** @type {import('./types.js').SupportedTarget[]} */ (
183
188
  installedManagedTargetList.filter((target) => !targets.includes(target))
184
189
  );
185
190
  const removableTargets = await findInstalledManagedTargets({
@@ -226,22 +231,34 @@ if (command === 'install-skill') {
226
231
  }
227
232
 
228
233
  if (command === 'install-mcp') {
229
- const argsLeft = [...rest];
230
234
  let isGlobal = true;
235
+ /** @type {string[]} */
236
+ const positionals = [];
231
237
 
232
- const localIdx = argsLeft.indexOf('--local');
233
- if (localIdx !== -1) {
234
- isGlobal = false;
235
- argsLeft.splice(localIdx, 1);
238
+ for (const arg of rest) {
239
+ if (arg === '--local') {
240
+ isGlobal = false;
241
+ continue;
242
+ }
243
+ if (arg === '--global') {
244
+ isGlobal = true;
245
+ continue;
246
+ }
247
+ if (arg.startsWith('--')) {
248
+ process.stderr.write(`Unknown install-mcp option "${arg}".\n`);
249
+ process.exit(1);
250
+ }
251
+ positionals.push(arg);
236
252
  }
237
- const globalIdx = argsLeft.indexOf('--global');
238
- if (globalIdx !== -1) {
239
- argsLeft.splice(globalIdx, 1);
253
+
254
+ if (positionals.length > 1) {
255
+ process.stderr.write(`Unexpected extra argument "${positionals[1]}".\n`);
256
+ process.exit(1);
240
257
  }
241
258
 
242
- const clientArg = argsLeft[0];
259
+ const clientArg = positionals[0];
243
260
 
244
- /** @type {import('./mcp-config.js').McpClientName[]} */
261
+ /** @type {import('./types.js').McpClientName[]} */
245
262
  let clients;
246
263
 
247
264
  if (!clientArg) {
@@ -252,14 +269,14 @@ if (command === 'install-mcp') {
252
269
  projectPath: process.cwd(),
253
270
  ...getSetupStatusTestOverrides(),
254
271
  });
255
- const detected = /** @type {import('./mcp-config.js').McpClientName[]} */ (
272
+ const detected = /** @type {import('./types.js').McpClientName[]} */ (
256
273
  setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
257
274
  );
258
275
  const configuredClients = new Set(
259
276
  setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
260
277
  );
261
278
  const configuredClientList =
262
- /** @type {import('./mcp-config.js').McpClientName[]} */ ([...configuredClients]);
279
+ /** @type {import('./types.js').McpClientName[]} */ ([...configuredClients]);
263
280
  const items = MCP_CLIENT_NAMES.map((c) => ({
264
281
  value: c,
265
282
  label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
@@ -284,12 +301,12 @@ if (command === 'install-mcp') {
284
301
  ? detected
285
302
  : [...MCP_CLIENT_NAMES];
286
303
  } else {
287
- clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (selected);
304
+ clients = /** @type {import('./types.js').McpClientName[]} */ (selected);
288
305
  }
289
306
 
290
307
  if (selected !== null) {
291
308
  const deselectedClients =
292
- /** @type {import('./mcp-config.js').McpClientName[]} */ (
309
+ /** @type {import('./types.js').McpClientName[]} */ (
293
310
  configuredClientList.filter((clientName) => !clients.includes(clientName))
294
311
  );
295
312
  const removableClients = await findConfiguredMcpClients({
@@ -470,15 +487,20 @@ async function main() {
470
487
  }
471
488
 
472
489
  if (command === 'batch') {
473
- await ensureClientConnection();
474
490
  const input = rest[0];
475
491
  if (!input) {
476
492
  throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
477
493
  }
478
- const calls = JSON.parse(input);
494
+ let calls;
495
+ try {
496
+ calls = JSON.parse(input);
497
+ } catch {
498
+ throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
499
+ }
479
500
  if (!Array.isArray(calls)) {
480
501
  throw new Error('Batch input must be a JSON array.');
481
502
  }
503
+ await ensureClientConnection();
482
504
  const results = await Promise.all(
483
505
  calls.map(async (call) => {
484
506
  if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
@@ -847,10 +869,13 @@ async function uninstallBrowserBridge() {
847
869
  */
848
870
  async function parseCallCommand(args) {
849
871
  const parsed = extractTabFlag(args);
850
- const [first, second] = parsed.rest;
872
+ const [first, second, ...extra] = parsed.rest;
851
873
  if (!first) {
852
874
  throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
853
875
  }
876
+ if (extra.length > 0) {
877
+ throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
878
+ }
854
879
 
855
880
  if (first.includes('.')) {
856
881
  const method = /** @type {BridgeMethod} */ (first);
@@ -15,53 +15,16 @@ import {
15
15
  getBridgeTransport,
16
16
  getSocketPath,
17
17
  } from '../../native-host/src/config.js';
18
+ import { normalizeBridgeAuthToken, readBridgeAuthToken } from '../../native-host/src/auth-token.js';
18
19
  import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
19
20
 
20
- /** @typedef {import('../../native-host/src/config.js').BridgeTransport} BridgeTransport */
21
-
22
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
23
- /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
24
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
25
- /**
26
- * @typedef {{
27
- * extensionConnected?: boolean,
28
- * supported_versions?: string[],
29
- * daemon_supported_versions?: string[],
30
- * deprecated_since?: string,
31
- * migration_hint?: string
32
- * }} ProtocolHealthResult
33
- */
34
-
35
- /**
36
- * @typedef {{
37
- * type: 'registered',
38
- * role: 'agent' | 'extension',
39
- * clientId?: string
40
- * } | {
41
- * type: 'agent.response',
42
- * response: BridgeResponse
43
- * }} ClientMessage
44
- */
45
-
46
- /**
47
- * @typedef {{
48
- * resolve: (value: any) => void,
49
- * reject: (error: Error) => void,
50
- * timeoutId: NodeJS.Timeout
51
- * }} PendingRequest
52
- */
53
-
54
- /**
55
- * @typedef {{
56
- * transport?: BridgeTransport,
57
- * socketPath?: string,
58
- * clientId?: string,
59
- * defaultTimeoutMs?: number,
60
- * autoReconnect?: boolean,
61
- * restartDaemonOnVersionMismatch?: boolean,
62
- * restartDaemonFn?: typeof restartBridgeDaemon,
63
- * }} BridgeClientOptions
64
- */
21
+ /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
22
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
23
+ /** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
24
+ /** @typedef {import('./types.js').BridgeClientOptions} BridgeClientOptions */
25
+ /** @typedef {import('./types.js').ClientMessage} ClientMessage */
26
+ /** @typedef {import('./types.js').PendingRequest} PendingRequest */
27
+ /** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
65
28
 
66
29
  /**
67
30
  * @param {string} left
@@ -94,6 +57,22 @@ function createTimeoutError(method, timeoutMs) {
94
57
  return error;
95
58
  }
96
59
 
60
+ /**
61
+ * @param {net.Socket} socket
62
+ * @param {string} line
63
+ * @returns {Promise<void>}
64
+ */
65
+ async function writeSocketLine(socket, line) {
66
+ if (!socket.write(line)) {
67
+ await Promise.race([
68
+ once(socket, 'drain'),
69
+ once(socket, 'close').then(() => {
70
+ throw new Error('Bridge socket closed while writing.');
71
+ }),
72
+ ]);
73
+ }
74
+ }
75
+
97
76
  export class BridgeClient extends EventEmitter {
98
77
  /**
99
78
  * @param {BridgeClientOptions} [options={}]
@@ -106,6 +85,7 @@ export class BridgeClient extends EventEmitter {
106
85
  autoReconnect = false,
107
86
  restartDaemonOnVersionMismatch = true,
108
87
  restartDaemonFn = restartBridgeDaemon,
88
+ authToken = undefined,
109
89
  } = {}) {
110
90
  super();
111
91
  this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
@@ -116,6 +96,7 @@ export class BridgeClient extends EventEmitter {
116
96
  this.autoReconnect = autoReconnect;
117
97
  this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
118
98
  this.restartDaemonFn = restartDaemonFn;
99
+ this.authToken = authToken;
119
100
  this.socket = null;
120
101
  this.connected = false;
121
102
  this.protocolCompatibility = null;
@@ -149,6 +130,18 @@ export class BridgeClient extends EventEmitter {
149
130
  throw error;
150
131
  }
151
132
 
133
+ const registrationPromise = new Promise((resolve, reject) => {
134
+ const timeoutId = setTimeout(() => {
135
+ this.waiting.delete('registered');
136
+ reject(createTimeoutError('register', this.defaultTimeoutMs));
137
+ }, this.defaultTimeoutMs);
138
+ this.waiting.set('registered', {
139
+ resolve,
140
+ reject,
141
+ timeoutId,
142
+ });
143
+ });
144
+
152
145
  parseJsonLines(socket, (raw) => {
153
146
  const message = /** @type {ClientMessage} */ (raw);
154
147
  if (message.type === 'registered') {
@@ -162,6 +155,16 @@ export class BridgeClient extends EventEmitter {
162
155
  return;
163
156
  }
164
157
 
158
+ if (message.type === 'registration_failed') {
159
+ const pending = this.waiting.get('registered');
160
+ if (pending) {
161
+ this.waiting.delete('registered');
162
+ clearTimeout(pending.timeoutId);
163
+ pending.reject(new Error(message.error?.message || 'Bridge daemon registration failed.'));
164
+ }
165
+ return;
166
+ }
167
+
165
168
  if (message.type === 'agent.response') {
166
169
  const pending = this.waiting.get(message.response.id);
167
170
  if (pending) {
@@ -186,20 +189,31 @@ export class BridgeClient extends EventEmitter {
186
189
  // 'close' fires after 'error'; reconnect is triggered there.
187
190
  });
188
191
 
189
- this.socket.write(
190
- `${JSON.stringify({ type: 'register', role: 'agent', clientId: this.clientId })}\n`
191
- );
192
- await new Promise((resolve, reject) => {
193
- const timeoutId = setTimeout(() => {
192
+ const authToken =
193
+ this.authToken === undefined
194
+ ? this.transport.type === 'tcp'
195
+ ? await readBridgeAuthToken()
196
+ : null
197
+ : normalizeBridgeAuthToken(this.authToken);
198
+ try {
199
+ await writeSocketLine(
200
+ socket,
201
+ `${JSON.stringify({
202
+ type: 'register',
203
+ role: 'agent',
204
+ clientId: this.clientId,
205
+ ...(authToken ? { authToken } : {}),
206
+ })}\n`
207
+ );
208
+ } catch (error) {
209
+ const pending = this.waiting.get('registered');
210
+ if (pending) {
211
+ clearTimeout(pending.timeoutId);
194
212
  this.waiting.delete('registered');
195
- reject(createTimeoutError('register', this.defaultTimeoutMs));
196
- }, this.defaultTimeoutMs);
197
- this.waiting.set('registered', {
198
- resolve,
199
- reject,
200
- timeoutId,
201
- });
202
- });
213
+ }
214
+ throw error;
215
+ }
216
+ await registrationPromise;
203
217
 
204
218
  this.protocolCompatibility = null;
205
219
  this.protocolWarning = null;
@@ -282,13 +296,15 @@ export class BridgeClient extends EventEmitter {
282
296
  });
283
297
  });
284
298
 
285
- if (!this.socket.write(`${JSON.stringify({ type: 'agent.request', request })}\n`)) {
286
- await Promise.race([
287
- once(this.socket, 'drain'),
288
- once(this.socket, 'close').then(() => {
289
- throw new Error('Bridge socket closed while writing.');
290
- }),
291
- ]);
299
+ try {
300
+ await writeSocketLine(this.socket, `${JSON.stringify({ type: 'agent.request', request })}\n`);
301
+ } catch (error) {
302
+ const pending = this.waiting.get(request.id);
303
+ if (pending) {
304
+ clearTimeout(pending.timeoutId);
305
+ this.waiting.delete(request.id);
306
+ }
307
+ throw error;
292
308
  }
293
309
  const response = /** @type {BridgeResponse} */ (await responsePromise);
294
310
  return this.attachProtocolWarning(response);
@@ -3,18 +3,8 @@
3
3
  import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
4
4
  import { parseCommaList, parseIntArg, parsePropertyAssignments } from './cli-helpers.js';
5
5
 
6
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
7
-
8
- /**
9
- * @typedef {{
10
- * method: BridgeMethod,
11
- * resolve?: boolean,
12
- * printMethod?: string,
13
- * usage: string,
14
- * description: string,
15
- * build: (r: string[], ref?: string) => Record<string, unknown>
16
- * }} ShortcutCommand
17
- */
6
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
7
+ /** @typedef {import('./types.js').ShortcutCommand} ShortcutCommand */
18
8
 
19
9
  /**
20
10
  * @param {BridgeMethod} method
@@ -247,11 +237,10 @@ export const CLI_HELP_SECTIONS = Object.freeze([
247
237
  {
248
238
  title: 'Setup',
249
239
  lines: [
250
- 'bbx install [--browser chrome|edge|brave|chromium] [extension-id] Install native messaging manifest',
240
+ 'bbx install [extension-id] [--browser chrome|edge|brave|chromium|arc] [--all] Install native messaging manifest',
251
241
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
252
- 'bbx install [--all] [--browser <name>] [extension-id] Install native host manifest (--all for all supported browsers)',
253
242
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
254
- 'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf',
243
+ 'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
255
244
  'bbx status Check bridge connection',
256
245
  'bbx doctor Diagnose install, daemon, extension, and access readiness',
257
246
  'bbx restart Restart the local Browser Bridge daemon',
@@ -4,9 +4,9 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
 
7
- /** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
8
- /** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
9
- /** @typedef {() => boolean | Promise<boolean>} Detector */
7
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
8
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
9
+ /** @typedef {import('./types.js').Detector} Detector */
10
10
 
11
11
  const home = os.homedir();
12
12
  const platform = process.platform;
@@ -41,9 +41,7 @@ const copilotBrowserBridgeNote = [
41
41
  '',
42
42
  ].join('\n');
43
43
 
44
- /**
45
- * @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} SupportedTarget
46
- */
44
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
47
45
 
48
46
  /** @type {SupportedTarget[]} */
49
47
  export const SUPPORTED_TARGETS = [...supportedTargets];
@@ -68,9 +66,7 @@ export function isSupportedTarget(value) {
68
66
  return supportedTargets.includes(/** @type {SupportedTarget} */ (value));
69
67
  }
70
68
 
71
- /**
72
- * @typedef {{targets: SupportedTarget[], projectPath: string, global: boolean}} InstallAgentOptions
73
- */
69
+ /** @typedef {import('./types.js').InstallAgentOptions} InstallAgentOptions */
74
70
 
75
71
  /**
76
72
  * @param {string[]} args
@@ -202,7 +198,7 @@ async function rollbackInstalledSkillDirs(attempted) {
202
198
  /**
203
199
  * Write MCP config for the given clients.
204
200
  *
205
- * @param {import('./mcp-config.js').McpClientName[]} clients
201
+ * @param {import('./types.js').McpClientName[]} clients
206
202
  * @param {{
207
203
  * global: boolean,
208
204
  * projectPath: string,
@@ -5,9 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
- /**
9
- * @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} McpClientName
10
- */
8
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
11
9
 
12
10
  /** @type {McpClientName[]} */
13
11
  export const MCP_CLIENT_NAMES = [
@@ -525,8 +523,13 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
525
523
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
526
524
  existing = parsed;
527
525
  }
528
- } catch {
529
- // File missing or unparseable - start fresh.
526
+ } catch (error) {
527
+ if (!isMissingFileError(error)) {
528
+ throw new Error(
529
+ `Cannot update ${configPath}: existing MCP config is not valid JSON. Fix or remove it first.`
530
+ );
531
+ }
532
+ // File missing - start fresh.
530
533
  }
531
534
 
532
535
  const shape = getMcpConfigShape(clientName);
@@ -663,3 +666,15 @@ export function parseInstalledMcpConfig(clientName, raw) {
663
666
  return { configured: false };
664
667
  }
665
668
  }
669
+
670
+ /**
671
+ * @param {unknown} error
672
+ * @returns {boolean}
673
+ */
674
+ function isMissingFileError(error) {
675
+ return Boolean(
676
+ error &&
677
+ typeof error === 'object' &&
678
+ /** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
679
+ );
680
+ }
@@ -12,39 +12,14 @@ import { resolveDefaultExtensionId } from '../../native-host/src/install-manifes
12
12
  import { methodNeedsTab } from './cli-helpers.js';
13
13
  import { BridgeClient } from './client.js';
14
14
 
15
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
16
- /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
17
- /** @typedef {import('../../protocol/src/types.js').BridgeRequestSource} BridgeRequestSource */
18
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
15
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
16
+ /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
17
+ /** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
18
+ /** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
19
19
  /** @typedef {import('../../native-host/src/config.js').SupportedBrowser} SupportedBrowser */
20
-
21
- /**
22
- * @typedef {{
23
- * browser: string,
24
- * manifestPath: string,
25
- * installed: boolean
26
- * }} BrowserManifestStatus
27
- */
28
-
29
- /**
30
- * @typedef {{
31
- * manifestInstalled: boolean,
32
- * manifestPath: string,
33
- * allowedOrigins: string[],
34
- * defaultExtensionId: string | null,
35
- * defaultExtensionIdSource: string,
36
- * daemonReachable: boolean,
37
- * extensionConnected: boolean,
38
- * accessEnabled: boolean,
39
- * enabledWindowId: number | null,
40
- * routeTabId: number | null,
41
- * routeReady: boolean,
42
- * routeReason: string,
43
- * issues: string[],
44
- * nextSteps: string[],
45
- * browserManifests: BrowserManifestStatus[]
46
- * }} DoctorReport
47
- */
20
+ /** @typedef {import('./types.js').BrowserManifestStatus} BrowserManifestStatus */
21
+ /** @typedef {import('./types.js').DoctorReport} DoctorReport */
22
+ /** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
48
23
 
49
24
  /**
50
25
  * @param {BridgeClient} client
@@ -178,15 +153,6 @@ export async function checkBrowserManifests() {
178
153
  );
179
154
  }
180
155
 
181
- /**
182
- * @typedef {{
183
- * loadManifest?: () => Promise<{allowed_origins?: string[]} | null>,
184
- * manifestPath?: string,
185
- * defaultExtensionIdInfo?: { extensionId: string | null, source: string },
186
- * bridgeClientRunner?: <T>(callback: (client: BridgeClient) => Promise<T>) => Promise<T>
187
- * }} DoctorReportOptions
188
- */
189
-
190
156
  /**
191
157
  * @param {DoctorReportOptions} [options={}]
192
158
  * @returns {Promise<DoctorReport>}
@@ -21,8 +21,8 @@ import {
21
21
  SUPPORTED_TARGETS,
22
22
  } from './install.js';
23
23
 
24
- /** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
25
- /** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
24
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
25
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
26
26
  /** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
27
27
  /** @typedef {import('../../protocol/src/types.js').McpClientStatus} McpClientStatus */
28
28
  /** @typedef {import('../../protocol/src/types.js').SkillTargetStatus} SkillTargetStatus */
@@ -52,17 +52,7 @@ const SKILL_TARGET_LABELS = {
52
52
  agents: 'Generic agents',
53
53
  };
54
54
 
55
- /**
56
- * @typedef {{
57
- * global?: boolean,
58
- * cwd?: string,
59
- * projectPath?: string,
60
- * mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
61
- * skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
62
- * access?: (targetPath: string) => Promise<void>,
63
- * readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
64
- * }} SetupStatusOptions
65
- */
55
+ /** @typedef {import('./types.js').SetupStatusOptions} SetupStatusOptions */
66
56
 
67
57
  /**
68
58
  * Return Browser Bridge MCP and skill installation status for supported clients.