@browserbridge/bbx 1.0.0 → 1.0.1

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 (52) hide show
  1. package/README.md +3 -1
  2. package/docs/api-reference.md +33 -33
  3. package/docs/mcp-vs-cli.md +104 -104
  4. package/docs/publishing.md +1 -3
  5. package/docs/quickstart.md +6 -6
  6. package/docs/unpacked-extension.md +72 -0
  7. package/manifest.json +3 -17
  8. package/package.json +44 -42
  9. package/packages/agent-client/src/cli-helpers.js +10 -5
  10. package/packages/agent-client/src/cli.js +65 -135
  11. package/packages/agent-client/src/client.js +37 -17
  12. package/packages/agent-client/src/command-registry.js +101 -69
  13. package/packages/agent-client/src/detect.js +3 -6
  14. package/packages/agent-client/src/install.js +10 -27
  15. package/packages/agent-client/src/mcp-config.js +11 -30
  16. package/packages/agent-client/src/runtime.js +41 -20
  17. package/packages/agent-client/src/setup-status.js +13 -28
  18. package/packages/extension/src/background-helpers.js +51 -36
  19. package/packages/extension/src/background-routing.js +11 -13
  20. package/packages/extension/src/background.js +562 -299
  21. package/packages/extension/src/content-script-helpers.js +17 -16
  22. package/packages/extension/src/content-script.js +175 -109
  23. package/packages/extension/src/sidepanel-helpers.js +3 -1
  24. package/packages/extension/ui/popup.js +39 -20
  25. package/packages/extension/ui/sidepanel.js +108 -191
  26. package/packages/extension/ui/ui.css +2 -1
  27. package/packages/mcp-server/src/handlers.js +546 -250
  28. package/packages/mcp-server/src/server.js +558 -257
  29. package/packages/native-host/bin/bridge-daemon.js +6 -2
  30. package/packages/native-host/bin/install-manifest.js +2 -2
  31. package/packages/native-host/bin/postinstall.js +4 -2
  32. package/packages/native-host/src/config.js +11 -7
  33. package/packages/native-host/src/daemon.js +143 -92
  34. package/packages/native-host/src/install-manifest.js +73 -22
  35. package/packages/native-host/src/native-host.js +55 -40
  36. package/packages/protocol/src/budget.js +3 -7
  37. package/packages/protocol/src/capabilities.js +3 -3
  38. package/packages/protocol/src/errors.js +11 -11
  39. package/packages/protocol/src/protocol.js +104 -71
  40. package/packages/protocol/src/registry.js +300 -45
  41. package/packages/protocol/src/summary.js +249 -106
  42. package/packages/protocol/src/types.js +1 -1
  43. package/skills/browser-bridge/SKILL.md +1 -1
  44. package/skills/browser-bridge/agents/openai.yaml +3 -3
  45. package/skills/browser-bridge/references/interaction.md +33 -11
  46. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  47. package/skills/browser-bridge/references/protocol.md +125 -70
  48. package/skills/browser-bridge/references/tailwind.md +12 -11
  49. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  50. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  51. package/packages/extension/ui/offscreen.html +0 -6
  52. package/packages/extension/ui/offscreen.js +0 -61
@@ -4,7 +4,12 @@ import { EventEmitter, once } from 'node:events';
4
4
  import net from 'node:net';
5
5
  import { randomUUID } from 'node:crypto';
6
6
 
7
- import { createRequest, DEFAULT_CLIENT_REQUEST_TIMEOUT_MS, PROTOCOL_VERSION, parseJsonLines } from '../../protocol/src/index.js';
7
+ import {
8
+ createRequest,
9
+ DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
10
+ PROTOCOL_VERSION,
11
+ parseJsonLines,
12
+ } from '../../protocol/src/index.js';
8
13
  import { getSocketPath } from '../../native-host/src/config.js';
9
14
 
10
15
  /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
@@ -55,7 +60,7 @@ export class BridgeClient extends EventEmitter {
55
60
  socketPath = getSocketPath(),
56
61
  clientId = `agent_${randomUUID()}`,
57
62
  defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
58
- autoReconnect = false
63
+ autoReconnect = false,
59
64
  } = {}) {
60
65
  super();
61
66
  this.socketPath = socketPath;
@@ -128,7 +133,9 @@ export class BridgeClient extends EventEmitter {
128
133
  // 'close' fires after 'error'; reconnect is triggered there.
129
134
  });
130
135
 
131
- this.socket.write(`${JSON.stringify({ type: 'register', role: 'agent', clientId: this.clientId })}\n`);
136
+ this.socket.write(
137
+ `${JSON.stringify({ type: 'register', role: 'agent', clientId: this.clientId })}\n`
138
+ );
132
139
  await new Promise((resolve, reject) => {
133
140
  const timeoutId = setTimeout(() => {
134
141
  this.waiting.delete('registered');
@@ -137,13 +144,13 @@ export class BridgeClient extends EventEmitter {
137
144
  this.waiting.set('registered', {
138
145
  resolve,
139
146
  reject,
140
- timeoutId
147
+ timeoutId,
141
148
  });
142
149
  });
143
150
 
144
151
  try {
145
152
  const healthResponse = await this.request({
146
- method: 'health.ping'
153
+ method: 'health.ping',
147
154
  });
148
155
  if (healthResponse.ok) {
149
156
  this.protocolCompatibility = BridgeClient.checkProtocolVersion(
@@ -167,9 +174,17 @@ export class BridgeClient extends EventEmitter {
167
174
  * }} options
168
175
  * @returns {Promise<BridgeResponse>}
169
176
  */
170
- async request({ method, params = {}, tabId = null, meta = {}, timeoutMs = this.defaultTimeoutMs }) {
177
+ async request({
178
+ method,
179
+ params = {},
180
+ tabId = null,
181
+ meta = {},
182
+ timeoutMs = this.defaultTimeoutMs,
183
+ }) {
171
184
  if (!this.socket || this.socket.destroyed || !this.socket.writable) {
172
- const err = /** @type {Error & { code: string }} */ (new Error('BridgeClient is not connected.'));
185
+ const err = /** @type {Error & { code: string }} */ (
186
+ new Error('BridgeClient is not connected.')
187
+ );
173
188
  err.code = 'ENOTCONN';
174
189
  throw err;
175
190
  }
@@ -179,7 +194,7 @@ export class BridgeClient extends EventEmitter {
179
194
  method,
180
195
  params,
181
196
  tabId,
182
- meta
197
+ meta,
183
198
  });
184
199
 
185
200
  const responsePromise = new Promise((resolve, reject) => {
@@ -191,14 +206,16 @@ export class BridgeClient extends EventEmitter {
191
206
  this.waiting.set(request.id, {
192
207
  resolve,
193
208
  reject,
194
- timeoutId
209
+ timeoutId,
195
210
  });
196
211
  });
197
212
 
198
213
  if (!this.socket.write(`${JSON.stringify({ type: 'agent.request', request })}\n`)) {
199
214
  await Promise.race([
200
215
  once(this.socket, 'drain'),
201
- once(this.socket, 'close').then(() => { throw new Error('Bridge socket closed while writing.'); })
216
+ once(this.socket, 'close').then(() => {
217
+ throw new Error('Bridge socket closed while writing.');
218
+ }),
202
219
  ]);
203
220
  }
204
221
  const response = /** @type {BridgeResponse} */ (await responsePromise);
@@ -275,7 +292,11 @@ export class BridgeClient extends EventEmitter {
275
292
  ? healthResult.supported_versions
276
293
  : [];
277
294
  if (remoteVersions.length === 0) {
278
- return { compatible: true, localVersion: PROTOCOL_VERSION, remoteVersions };
295
+ return {
296
+ compatible: true,
297
+ localVersion: PROTOCOL_VERSION,
298
+ remoteVersions,
299
+ };
279
300
  }
280
301
  const compatible = remoteVersions.includes(PROTOCOL_VERSION);
281
302
  return {
@@ -284,11 +305,10 @@ export class BridgeClient extends EventEmitter {
284
305
  remoteVersions,
285
306
  ...(!compatible && {
286
307
  warning:
287
- typeof healthResult?.migration_hint === 'string' &&
288
- healthResult.migration_hint
308
+ typeof healthResult?.migration_hint === 'string' && healthResult.migration_hint
289
309
  ? healthResult.migration_hint
290
- : `Protocol mismatch: client speaks ${PROTOCOL_VERSION} but remote supports [${remoteVersions.join(', ')}]. Update the ${remoteVersions[0] > PROTOCOL_VERSION ? 'client (npm)' : 'extension'} to match.`
291
- })
310
+ : `Protocol mismatch: client speaks ${PROTOCOL_VERSION} but remote supports [${remoteVersions.join(', ')}]. Update the ${remoteVersions[0] > PROTOCOL_VERSION ? 'client (npm)' : 'extension'} to match.`,
311
+ }),
292
312
  };
293
313
  }
294
314
 
@@ -316,8 +336,8 @@ export class BridgeClient extends EventEmitter {
316
336
  ...response,
317
337
  meta: {
318
338
  ...response.meta,
319
- protocol_warning: this.protocolWarning
320
- }
339
+ protocol_warning: this.protocolWarning,
340
+ },
321
341
  };
322
342
  }
323
343
  }
@@ -27,21 +27,20 @@ function createShortcutCommand(method, usage, build, options = {}) {
27
27
  return {
28
28
  method,
29
29
  usage,
30
- description: options.description ?? BRIDGE_METHOD_REGISTRY[method].description.replace(/\.$/, ''),
30
+ description:
31
+ options.description ?? BRIDGE_METHOD_REGISTRY[method].description.replace(/\.$/, ''),
31
32
  build,
32
33
  ...(options.resolve ? { resolve: true } : {}),
33
- ...(options.printMethod ? { printMethod: options.printMethod } : {})
34
+ ...(options.printMethod ? { printMethod: options.printMethod } : {}),
34
35
  };
35
36
  }
36
37
 
37
38
  /** @type {Record<string, ShortcutCommand>} */
38
39
  export const SHORTCUT_COMMANDS = {
39
40
  'access-request': createShortcutCommand('access.request', 'bbx access-request', () => ({})),
40
- 'dom-query': createShortcutCommand(
41
- 'dom.query',
42
- 'bbx dom-query [selector]',
43
- (r) => ({ selector: r[0] || 'body' })
44
- ),
41
+ 'dom-query': createShortcutCommand('dom.query', 'bbx dom-query [selector]', (r) => ({
42
+ selector: r[0] || 'body',
43
+ })),
45
44
  describe: createShortcutCommand(
46
45
  'dom.describe',
47
46
  'bbx describe <ref|selector>',
@@ -51,7 +50,10 @@ export const SHORTCUT_COMMANDS = {
51
50
  text: createShortcutCommand(
52
51
  'dom.get_text',
53
52
  'bbx text <ref|selector> [budget]',
54
- (r, ref) => ({ elementRef: ref, textBudget: r[1] ? parseIntArg(r[1], 'budget') : undefined }),
53
+ (r, ref) => ({
54
+ elementRef: ref,
55
+ textBudget: r[1] ? parseIntArg(r[1], 'budget') : undefined,
56
+ }),
55
57
  { resolve: true, printMethod: 'dom.get_text' }
56
58
  ),
57
59
  styles: createShortcutCommand(
@@ -93,27 +95,34 @@ export const SHORTCUT_COMMANDS = {
93
95
  html: createShortcutCommand(
94
96
  'dom.get_html',
95
97
  'bbx html <ref|selector> [maxLen]',
96
- (r, ref) => ({ elementRef: ref, maxLength: r[1] ? parseIntArg(r[1], 'maxLen') : undefined }),
98
+ (r, ref) => ({
99
+ elementRef: ref,
100
+ maxLength: r[1] ? parseIntArg(r[1], 'maxLen') : undefined,
101
+ }),
97
102
  { resolve: true }
98
103
  ),
99
104
  'patch-style': createShortcutCommand(
100
105
  'patch.apply_styles',
101
106
  'bbx patch-style <ref|sel> prop=val',
102
- (r, ref) => ({ target: { elementRef: ref }, declarations: parsePropertyAssignments(r.slice(1)) }),
107
+ (r, ref) => ({
108
+ target: { elementRef: ref },
109
+ declarations: parsePropertyAssignments(r.slice(1)),
110
+ }),
103
111
  { resolve: true }
104
112
  ),
105
113
  'patch-text': createShortcutCommand(
106
114
  'patch.apply_dom',
107
115
  'bbx patch-text <ref|sel> <text...>',
108
- (r, ref) => ({ target: { elementRef: ref }, operation: 'set_text', value: r.slice(1).join(' ') }),
116
+ (r, ref) => ({
117
+ target: { elementRef: ref },
118
+ operation: 'set_text',
119
+ value: r.slice(1).join(' '),
120
+ }),
109
121
  { resolve: true, description: 'Apply a reversible DOM text patch' }
110
122
  ),
111
- patches: createShortcutCommand(
112
- 'patch.list',
113
- 'bbx patches',
114
- () => ({}),
115
- { printMethod: 'patch.list' }
116
- ),
123
+ patches: createShortcutCommand('patch.list', 'bbx patches', () => ({}), {
124
+ printMethod: 'patch.list',
125
+ }),
117
126
  rollback: createShortcutCommand('patch.rollback', 'bbx rollback <patchId>', (r) => {
118
127
  if (!r[0]) throw new Error('Usage: rollback <patchId>');
119
128
  return { patchId: r[0] };
@@ -122,11 +131,17 @@ export const SHORTCUT_COMMANDS = {
122
131
  'page.get_console',
123
132
  'bbx console [level]',
124
133
  (r) => ({ level: r[0] || 'all', clear: false }),
125
- { printMethod: 'page.get_console', description: 'Read buffered console output (log|warn|error|all)' }
134
+ {
135
+ printMethod: 'page.get_console',
136
+ description: 'Read buffered console output (log|warn|error|all)',
137
+ }
126
138
  ),
127
139
  wait: createShortcutCommand('dom.wait_for', 'bbx wait <selector> [timeoutMs]', (r) => {
128
140
  if (!r[0]) throw new Error('Usage: wait <selector> [timeoutMs]');
129
- return { selector: r[0], timeoutMs: r[1] ? parseIntArg(r[1], 'timeoutMs') : 5000 };
141
+ return {
142
+ selector: r[0],
143
+ timeoutMs: r[1] ? parseIntArg(r[1], 'timeoutMs') : 5000,
144
+ };
130
145
  }),
131
146
  find: createShortcutCommand(
132
147
  'dom.find_by_text',
@@ -138,19 +153,23 @@ export const SHORTCUT_COMMANDS = {
138
153
  },
139
154
  { printMethod: 'dom.find_by_text' }
140
155
  ),
141
- 'find-role': createShortcutCommand('dom.find_by_role', 'bbx find-role <role> [name]', (r) => {
142
- if (!r[0]) throw new Error('Usage: find-role <role> [name]');
143
- return { role: r[0], name: r.slice(1).join(' ') || undefined };
144
- }, { printMethod: 'dom.find_by_role' }),
156
+ 'find-role': createShortcutCommand(
157
+ 'dom.find_by_role',
158
+ 'bbx find-role <role> [name]',
159
+ (r) => {
160
+ if (!r[0]) throw new Error('Usage: find-role <role> [name]');
161
+ return { role: r[0], name: r.slice(1).join(' ') || undefined };
162
+ },
163
+ { printMethod: 'dom.find_by_role' }
164
+ ),
145
165
  navigate: createShortcutCommand('navigation.navigate', 'bbx navigate <url>', (r) => {
146
166
  if (!r[0]) throw new Error('Usage: navigate <url>');
147
167
  return { url: r[0] };
148
168
  }),
149
- storage: createShortcutCommand(
150
- 'page.get_storage',
151
- 'bbx storage [local|session] [keys]',
152
- (r) => ({ type: r[0] === 'session' ? 'session' : 'local', keys: r.slice(1).length ? r.slice(1) : undefined })
153
- ),
169
+ storage: createShortcutCommand('page.get_storage', 'bbx storage [local|session] [keys]', (r) => ({
170
+ type: r[0] === 'session' ? 'session' : 'local',
171
+ keys: r.slice(1).length ? r.slice(1) : undefined,
172
+ })),
154
173
  'page-text': createShortcutCommand(
155
174
  'page.get_text',
156
175
  'bbx page-text [textBudget]',
@@ -161,21 +180,33 @@ export const SHORTCUT_COMMANDS = {
161
180
  'page.get_network',
162
181
  'bbx network [limit]',
163
182
  (r) => ({ limit: r[0] ? parseIntArg(r[0], 'limit') : undefined }),
164
- { printMethod: 'page.get_network', description: 'Read buffered network requests (fetch/XHR)' }
183
+ {
184
+ printMethod: 'page.get_network',
185
+ description: 'Read buffered network requests (fetch/XHR)',
186
+ }
165
187
  ),
166
188
  'a11y-tree': createShortcutCommand(
167
189
  'dom.get_accessibility_tree',
168
190
  'bbx a11y-tree [maxNodes] [maxDepth]',
169
- (r) => ({ maxNodes: r[0] ? parseIntArg(r[0], 'maxNodes') : undefined, maxDepth: r[1] ? parseIntArg(r[1], 'maxDepth') : undefined })
191
+ (r) => ({
192
+ maxNodes: r[0] ? parseIntArg(r[0], 'maxNodes') : undefined,
193
+ maxDepth: r[1] ? parseIntArg(r[1], 'maxDepth') : undefined,
194
+ })
170
195
  ),
171
196
  perf: createShortcutCommand('performance.get_metrics', 'bbx perf', () => ({})),
172
197
  scroll: createShortcutCommand('viewport.scroll', 'bbx scroll <top> [left]', (r) => {
173
198
  if (!r[0] && !r[1]) throw new Error('Usage: scroll <top> [left]');
174
- return { top: r[0] ? parseIntArg(r[0], 'top') : undefined, left: r[1] ? parseIntArg(r[1], 'left') : undefined };
199
+ return {
200
+ top: r[0] ? parseIntArg(r[0], 'top') : undefined,
201
+ left: r[1] ? parseIntArg(r[1], 'left') : undefined,
202
+ };
175
203
  }),
176
204
  resize: createShortcutCommand('viewport.resize', 'bbx resize <width> <height>', (r) => {
177
205
  if (!r[0] || !r[1]) throw new Error('Usage: resize <width> <height>');
178
- return { width: parseIntArg(r[0], 'width'), height: parseIntArg(r[1], 'height') };
206
+ return {
207
+ width: parseIntArg(r[0], 'width'),
208
+ height: parseIntArg(r[1], 'height'),
209
+ };
179
210
  }),
180
211
  reload: createShortcutCommand('navigation.reload', 'bbx reload', () => ({})),
181
212
  back: createShortcutCommand('navigation.go_back', 'bbx back', () => ({})),
@@ -191,7 +222,7 @@ export const SHORTCUT_COMMANDS = {
191
222
  'bbx matched-rules <ref|selector>',
192
223
  (_r, ref) => ({ elementRef: ref }),
193
224
  { resolve: true, printMethod: 'styles.get_matched_rules' }
194
- )
225
+ ),
195
226
  };
196
227
 
197
228
  /** @type {Readonly<Record<string, BridgeMethod>>} */
@@ -207,7 +238,7 @@ export const CLI_METHOD_BINDINGS = Object.freeze({
207
238
  ...Object.fromEntries(BRIDGE_METHODS.map((method) => [method, method])),
208
239
  'press-key': 'input.press_key',
209
240
  screenshot: 'screenshot.capture_element',
210
- eval: 'page.evaluate'
241
+ eval: 'page.evaluate',
211
242
  });
212
243
 
213
244
  /** @type {ReadonlyArray<{ title: string, lines: readonly string[] }>} */
@@ -228,18 +259,18 @@ export const CLI_HELP_SECTIONS = Object.freeze([
228
259
  'bbx tab-create [url] Create a new tab',
229
260
  'bbx tab-close <tabId> Close a tab',
230
261
  'bbx skill Runtime budget presets and method groups',
231
- 'bbx mcp serve Start Browser Bridge as an MCP stdio server'
232
- ]
262
+ 'bbx mcp serve Start Browser Bridge as an MCP stdio server',
263
+ ],
233
264
  },
234
265
  {
235
266
  title: 'Generic RPC',
236
267
  lines: [
237
268
  'bbx call [--tab <tabId>] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
238
269
  'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
239
- 'bbx batch \'[{method,params,tabId?},...]\' Parallel method calls',
270
+ "bbx batch '[{method,params,tabId?},...]' Parallel method calls",
240
271
  'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
241
- 'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.'
242
- ]
272
+ 'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
273
+ ],
243
274
  },
244
275
  {
245
276
  title: 'Inspect',
@@ -253,19 +284,21 @@ export const CLI_HELP_SECTIONS = Object.freeze([
253
284
  'attrs',
254
285
  'matched-rules',
255
286
  'box',
256
- 'a11y-tree'
257
- ].map((command) => `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`)
258
- ]
287
+ 'a11y-tree',
288
+ ].map(
289
+ (command) =>
290
+ `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
291
+ ),
292
+ ],
259
293
  },
260
294
  {
261
295
  title: 'Find',
262
296
  lines: [
263
- ...[
264
- 'find',
265
- 'find-role',
266
- 'wait'
267
- ].map((command) => `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`)
268
- ]
297
+ ...['find', 'find-role', 'wait'].map(
298
+ (command) =>
299
+ `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
300
+ ),
301
+ ],
269
302
  },
270
303
  {
271
304
  title: 'Page',
@@ -282,37 +315,36 @@ export const CLI_HELP_SECTIONS = Object.freeze([
282
315
  'forward',
283
316
  'perf',
284
317
  'scroll',
285
- 'resize'
286
- ].map((command) => `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`)
287
- ]
318
+ 'resize',
319
+ ].map(
320
+ (command) =>
321
+ `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
322
+ ),
323
+ ],
288
324
  },
289
325
  {
290
326
  title: 'Interact',
291
327
  lines: [
292
- ...[
293
- 'click',
294
- 'focus',
295
- 'type',
296
- 'hover'
297
- ].map((command) => `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`),
298
- 'bbx press-key <key> [ref|selector] Send key event'
299
- ]
328
+ ...['click', 'focus', 'type', 'hover'].map(
329
+ (command) =>
330
+ `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
331
+ ),
332
+ 'bbx press-key <key> [ref|selector] Send key event',
333
+ ],
300
334
  },
301
335
  {
302
336
  title: 'Patch',
303
337
  lines: [
304
- ...[
305
- 'patch-style',
306
- 'patch-text',
307
- 'patches',
308
- 'rollback'
309
- ].map((command) => `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`)
310
- ]
338
+ ...['patch-style', 'patch-text', 'patches', 'rollback'].map(
339
+ (command) =>
340
+ `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
341
+ ),
342
+ ],
311
343
  },
312
344
  {
313
345
  title: 'Capture',
314
346
  lines: [
315
- 'bbx screenshot <ref|selector> [path] Capture partial element screenshot'
316
- ]
317
- }
347
+ 'bbx screenshot <ref|selector> [path] Capture partial element screenshot',
348
+ ],
349
+ },
318
350
  ]);
@@ -16,8 +16,7 @@ const platform = process.platform;
16
16
  */
17
17
  function getVsCodeUserDataDir() {
18
18
  if (platform === 'win32') {
19
- const appData =
20
- process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
19
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
21
20
  return path.join(appData, 'Code');
22
21
  }
23
22
  if (platform === 'linux') {
@@ -58,11 +57,9 @@ function commandExists(cmd) {
58
57
  function detectCopilot() {
59
58
  if (fsExists(path.join(getVsCodeUserDataDir(), 'User'))) return true;
60
59
  if (fsExists(path.join(home, '.vscode'))) return true;
61
- if (platform === 'darwin')
62
- return fsExists('/Applications/Visual Studio Code.app');
60
+ if (platform === 'darwin') return fsExists('/Applications/Visual Studio Code.app');
63
61
  if (platform === 'win32') {
64
- const localAppData =
65
- process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
62
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
66
63
  return fsExists(path.join(localAppData, 'Programs', 'Microsoft VS Code'));
67
64
  }
68
65
  return commandExists('code');
@@ -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;
@@ -204,7 +198,7 @@ export async function installMcpClientSetup(clients, options) {
204
198
  global: options.global,
205
199
  cwd: options.projectPath,
206
200
  stdout: options.stdout,
207
- }),
201
+ })
208
202
  );
209
203
  }
210
204
 
@@ -289,7 +283,7 @@ function parseTargetList(raw) {
289
283
  );
290
284
  if (!canonical) {
291
285
  throw new Error(
292
- `Unknown install-skill target "${value}". Supported targets: ${supportedTargets.join(', ')}, all. Aliases: openai -> codex, google -> antigravity.`,
286
+ `Unknown install-skill target "${value}". Supported targets: ${supportedTargets.join(', ')}, all. Aliases: openai -> codex, google -> antigravity.`
293
287
  );
294
288
  }
295
289
  parsed.add(canonical);
@@ -380,7 +374,7 @@ export function formatManagedSkillSentinel(skillName) {
380
374
  version: managedPackageVersion,
381
375
  },
382
376
  null,
383
- 2,
377
+ 2
384
378
  )}\n`;
385
379
  }
386
380
 
@@ -414,10 +408,7 @@ export function parseManagedSkillSentinel(raw) {
414
408
  * @param {string | null} [currentVersion=managedPackageVersion]
415
409
  * @returns {boolean}
416
410
  */
417
- export function isManagedVersionOutdated(
418
- installedVersion,
419
- currentVersion = managedPackageVersion,
420
- ) {
411
+ export function isManagedVersionOutdated(installedVersion, currentVersion = managedPackageVersion) {
421
412
  if (!currentVersion) {
422
413
  return false;
423
414
  }
@@ -439,19 +430,13 @@ async function installManagedSkill(skillName, target, targetDir) {
439
430
  const targetExists = await pathExists(targetDir);
440
431
 
441
432
  if (targetExists && !(await pathExists(sentinelPath))) {
442
- throw new Error(
443
- `Refusing to overwrite unmanaged skill directory: ${targetDir}`,
444
- );
433
+ throw new Error(`Refusing to overwrite unmanaged skill directory: ${targetDir}`);
445
434
  }
446
435
 
447
436
  await fs.promises.rm(targetDir, { recursive: true, force: true });
448
437
  await copyDir(sourceDir, targetDir);
449
438
  await applyManagedSkillPatches(skillName, target, targetDir);
450
- await fs.promises.writeFile(
451
- sentinelPath,
452
- formatManagedSkillSentinel(skillName),
453
- 'utf8',
454
- );
439
+ await fs.promises.writeFile(sentinelPath, formatManagedSkillSentinel(skillName), 'utf8');
455
440
  }
456
441
 
457
442
  /**
@@ -532,9 +517,7 @@ async function applyManagedSkillPatches(skillName, target, targetDir) {
532
517
  */
533
518
  function loadPackageManifest() {
534
519
  try {
535
- return JSON.parse(
536
- fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
537
- );
520
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
538
521
  } catch {
539
522
  return {};
540
523
  }
@@ -48,8 +48,7 @@ const BROWSER_BRIDGE_SERVER_NAME = 'browser-bridge';
48
48
  function getVsCodeUserDataDir() {
49
49
  const home = os.homedir();
50
50
  if (process.platform === 'win32') {
51
- const appData =
52
- process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
51
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
53
52
  return path.join(appData, 'Code');
54
53
  }
55
54
  if (process.platform === 'linux') {
@@ -79,9 +78,7 @@ function getLegacyCopilotVsCodeConfigPath() {
79
78
  * @returns {{ key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }}
80
79
  */
81
80
  export function getMcpConfigShape(clientName) {
82
- return (
83
- MCP_CONFIG_SHAPES[clientName] ?? { key: 'mcpServers', includeType: false }
84
- );
81
+ return MCP_CONFIG_SHAPES[clientName] ?? { key: 'mcpServers', includeType: false };
85
82
  }
86
83
 
87
84
  /**
@@ -143,9 +140,7 @@ export function buildMcpConfig(clientName) {
143
140
  }
144
141
  const serverConfig = createBaseServerConfig(clientName);
145
142
  const shape = getMcpConfigShape(clientName);
146
- const entry = shape.includeType
147
- ? { type: 'stdio', ...serverConfig }
148
- : serverConfig;
143
+ const entry = shape.includeType ? { type: 'stdio', ...serverConfig } : serverConfig;
149
144
  return { [shape.key]: { [BROWSER_BRIDGE_SERVER_NAME]: entry } };
150
145
  }
151
146
 
@@ -169,10 +164,7 @@ export function formatMcpConfig(clientName) {
169
164
  * @param {{ global: boolean, cwd?: string }} options
170
165
  * @returns {string}
171
166
  */
172
- export function getMcpConfigPath(
173
- clientName,
174
- { global: isGlobal, cwd = process.cwd() },
175
- ) {
167
+ export function getMcpConfigPath(clientName, { global: isGlobal, cwd = process.cwd() }) {
176
168
  const home = os.homedir();
177
169
 
178
170
  if (!isGlobal) {
@@ -252,7 +244,9 @@ export async function getMcpConfigPaths(clientName, options) {
252
244
  const entries = await readdir(profilesDir, { withFileTypes: true });
253
245
  const profilePaths = entries
254
246
  .filter((/** @type {import('node:fs').Dirent} */ entry) => entry.isDirectory())
255
- .map((/** @type {import('node:fs').Dirent} */ entry) => path.join(profilesDir, entry.name, 'mcp.json'));
247
+ .map((/** @type {import('node:fs').Dirent} */ entry) =>
248
+ path.join(profilesDir, entry.name, 'mcp.json')
249
+ );
256
250
  for (const profilePath of profilePaths) {
257
251
  if (!paths.includes(profilePath)) {
258
252
  paths.push(profilePath);
@@ -533,11 +527,7 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
533
527
  }
534
528
 
535
529
  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
- );
530
+ await fs.promises.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
541
531
  stdout.write(`Wrote ${configPath}\n`);
542
532
  }
543
533
 
@@ -600,11 +590,7 @@ async function removeJsonMcpConfig(clientName, configPath, stdout) {
600
590
  }
601
591
 
602
592
  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
- );
593
+ await fs.promises.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
608
594
  stdout.write(`Removed ${configPath}\n`);
609
595
  return true;
610
596
  }
@@ -647,16 +633,11 @@ export function parseInstalledMcpConfig(clientName, raw) {
647
633
  const parsed = JSON.parse(raw);
648
634
  const block =
649
635
  parsed && typeof parsed === 'object' && !Array.isArray(parsed)
650
- ? getMergedJsonMcpBlock(
651
- /** @type {Record<string, unknown>} */ (parsed),
652
- clientName,
653
- )
636
+ ? getMergedJsonMcpBlock(/** @type {Record<string, unknown>} */ (parsed), clientName)
654
637
  : {};
655
638
  return {
656
639
  configured: Boolean(
657
- block &&
658
- typeof block === 'object' &&
659
- Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME),
640
+ block && typeof block === 'object' && Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME)
660
641
  ),
661
642
  };
662
643
  } catch {