@browserbridge/bbx 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
@@ -6,7 +6,11 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
- import { SUPPORTED_BROWSERS } from '../../native-host/src/config.js';
9
+ import {
10
+ applyWindowsTcpTransportDefaults,
11
+ SUPPORTED_BROWSERS,
12
+ } from '../../native-host/src/config.js';
13
+ import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
10
14
  import { uninstallNativeManifest } from '../../native-host/src/install-manifest.js';
11
15
  import {
12
16
  createRuntimeContext,
@@ -23,8 +27,8 @@ import {
23
27
  methodNeedsTab,
24
28
  parseIntArg,
25
29
  parseJsonObject,
30
+ sanitizeOutput,
26
31
  } from './cli-helpers.js';
27
- import { detectMcpClients, detectSkillTargets } from './detect.js';
28
32
  import {
29
33
  findInstalledManagedTargets,
30
34
  installAgentFiles,
@@ -42,51 +46,16 @@ import {
42
46
  MCP_CLIENT_NAMES,
43
47
  removeMcpConfig,
44
48
  } from './mcp-config.js';
45
- import {
46
- getDoctorReport,
47
- requestBridge,
48
- resolveRef,
49
- } from './runtime.js';
49
+ import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
50
50
  import { collectSetupStatus } from './setup-status.js';
51
- import {
52
- annotateBridgeSummary,
53
- summarizeBridgeResponse,
54
- } from './subagent.js';
51
+ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
55
52
 
56
53
  /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
57
54
  /** @typedef {{ image: string, rect: Record<string, unknown> }} ScreenshotResult */
58
55
 
59
56
  const REQUEST_SOURCE = 'cli';
60
-
61
- /**
62
- * Strip ANSI escape sequences from a string to prevent terminal injection
63
- * from malicious page content (e.g. DOM text, console output, eval results).
64
- *
65
- * @param {string} str
66
- * @returns {string}
67
- */
68
- function stripAnsi(str) {
69
- // eslint-disable-next-line no-control-regex
70
- return str.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\x1b[^[]/g, '');
71
- }
72
-
73
- /**
74
- * Recursively sanitize all string values in a value tree by stripping ANSI
75
- * escape sequences. Only strings are touched; structure is preserved.
76
- *
77
- * @param {unknown} value
78
- * @returns {unknown}
79
- */
80
- function sanitizeOutput(value) {
81
- if (typeof value === 'string') return stripAnsi(value);
82
- if (Array.isArray(value)) return value.map(sanitizeOutput);
83
- if (value !== null && typeof value === 'object') {
84
- return Object.fromEntries(
85
- Object.entries(/** @type {Record<string, unknown>} */ (value)).map(([k, v]) => [k, sanitizeOutput(v)])
86
- );
87
- }
88
- return value;
89
- }
57
+ const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
58
+ const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
90
59
 
91
60
  /**
92
61
  * Read all of stdin as UTF-8 text. Resolves once stdin closes.
@@ -98,9 +67,7 @@ function readStdin() {
98
67
  const chunks = /** @type {Buffer[]} */ ([]);
99
68
  process.stdin.setEncoding('utf8');
100
69
  process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
101
- process.stdin.on('end', () =>
102
- resolve(Buffer.concat(chunks).toString('utf8').trim()),
103
- );
70
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
104
71
  process.stdin.on('error', reject);
105
72
  // If stdin is a TTY and nothing is piped, read nothing
106
73
  if (process.stdin.isTTY) {
@@ -117,7 +84,10 @@ if (!command || ['help', '--help', '-h'].includes(command)) {
117
84
  }
118
85
 
119
86
  if (['--version', '-v'].includes(command)) {
120
- const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../package.json');
87
+ const pkgPath = path.resolve(
88
+ path.dirname(fileURLToPath(import.meta.url)),
89
+ '../../../package.json'
90
+ );
121
91
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
122
92
  process.stdout.write(`${pkg.version}\n`);
123
93
  process.exit(0);
@@ -125,7 +95,7 @@ if (['--version', '-v'].includes(command)) {
125
95
 
126
96
  if (command === 'skill') {
127
97
  process.stdout.write(
128
- `${JSON.stringify(createRuntimeContext(), null, process.stdout.isTTY ? 2 : undefined)}\n`,
98
+ `${JSON.stringify(createRuntimeContext(), null, process.stdout.isTTY ? 2 : undefined)}\n`
129
99
  );
130
100
  process.exit(0);
131
101
  }
@@ -135,7 +105,7 @@ if (command === 'install') {
135
105
  const { fileURLToPath } = await import('node:url');
136
106
  const installScript = path.resolve(
137
107
  path.dirname(fileURLToPath(import.meta.url)),
138
- '../../native-host/bin/install-manifest.js',
108
+ '../../native-host/bin/install-manifest.js'
139
109
  );
140
110
  execFileSync(process.execPath, [installScript, ...rest], {
141
111
  stdio: 'inherit',
@@ -167,18 +137,19 @@ if (command === 'install-skill') {
167
137
  global: isGlobal,
168
138
  cwd: process.cwd(),
169
139
  projectPath: isGlobal ? os.homedir() : process.cwd(),
140
+ ...getSetupStatusTestOverrides(),
170
141
  });
171
142
  /** @type {import('./install.js').SupportedTarget[]} */
172
- const detected = detectSkillTargets();
143
+ const detected = /** @type {import('./install.js').SupportedTarget[]} */ (
144
+ setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
145
+ );
173
146
  const installedManagedTargets = new Set(
174
147
  setupStatus.skillTargets
175
148
  .filter((entry) => entry.installed && entry.managed)
176
- .map((entry) => entry.key),
149
+ .map((entry) => entry.key)
177
150
  );
178
151
  const installedManagedTargetList =
179
- /** @type {import('./install.js').SupportedTarget[]} */ ([
180
- ...installedManagedTargets,
181
- ]);
152
+ /** @type {import('./install.js').SupportedTarget[]} */ ([...installedManagedTargets]);
182
153
 
183
154
  // Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
184
155
  const items = SUPPORTED_TARGETS.map((t) => ({
@@ -193,30 +164,23 @@ if (command === 'install-skill') {
193
164
 
194
165
  const selected = await interactiveCheckbox(
195
166
  'Select agents to install skill for (↑↓ move · space toggle · a all · enter confirm)',
196
- items,
167
+ items
197
168
  );
198
169
 
199
170
  /** @type {import('./install.js').SupportedTarget[]} */
200
171
  let targets;
201
172
  if (selected === null) {
202
173
  // Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
203
- targets =
204
- installedManagedTargets.size > 0
205
- ? installedManagedTargetList
206
- : detected;
174
+ targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
207
175
  } else {
208
- targets = /** @type {import('./install.js').SupportedTarget[]} */ (
209
- selected
210
- );
176
+ targets = /** @type {import('./install.js').SupportedTarget[]} */ (selected);
211
177
  }
212
178
 
213
179
  const projectPath = isGlobal ? os.homedir() : process.cwd();
214
180
  if (selected !== null) {
215
181
  const deselectedTargets =
216
182
  /** @type {import('./install.js').SupportedTarget[]} */ (
217
- installedManagedTargetList.filter(
218
- (target) => !targets.includes(target),
219
- )
183
+ installedManagedTargetList.filter((target) => !targets.includes(target))
220
184
  );
221
185
  const removableTargets = await findInstalledManagedTargets({
222
186
  targets: deselectedTargets,
@@ -225,7 +189,7 @@ if (command === 'install-skill') {
225
189
  });
226
190
  if (removableTargets.length > 0) {
227
191
  const confirmed = await interactiveConfirm(
228
- `Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}?`,
192
+ `Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}?`
229
193
  );
230
194
  if (confirmed) {
231
195
  const removedPaths = await removeAgentFiles({
@@ -286,17 +250,16 @@ if (command === 'install-mcp') {
286
250
  global: isGlobal,
287
251
  cwd: process.cwd(),
288
252
  projectPath: process.cwd(),
253
+ ...getSetupStatusTestOverrides(),
289
254
  });
290
- const detected = detectMcpClients();
255
+ const detected = /** @type {import('./mcp-config.js').McpClientName[]} */ (
256
+ setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
257
+ );
291
258
  const configuredClients = new Set(
292
- setupStatus.mcpClients
293
- .filter((entry) => entry.configured)
294
- .map((entry) => entry.key),
259
+ setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
295
260
  );
296
261
  const configuredClientList =
297
- /** @type {import('./mcp-config.js').McpClientName[]} */ ([
298
- ...configuredClients,
299
- ]);
262
+ /** @type {import('./mcp-config.js').McpClientName[]} */ ([...configuredClients]);
300
263
  const items = MCP_CLIENT_NAMES.map((c) => ({
301
264
  value: c,
302
265
  label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
@@ -309,7 +272,7 @@ if (command === 'install-mcp') {
309
272
 
310
273
  const selected = await interactiveCheckbox(
311
274
  'Select clients to configure (↑↓ move · space toggle · a all · enter confirm)',
312
- items,
275
+ items
313
276
  );
314
277
 
315
278
  if (selected === null) {
@@ -321,17 +284,13 @@ if (command === 'install-mcp') {
321
284
  ? detected
322
285
  : [...MCP_CLIENT_NAMES];
323
286
  } else {
324
- clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (
325
- selected
326
- );
287
+ clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (selected);
327
288
  }
328
289
 
329
290
  if (selected !== null) {
330
291
  const deselectedClients =
331
292
  /** @type {import('./mcp-config.js').McpClientName[]} */ (
332
- configuredClientList.filter(
333
- (clientName) => !clients.includes(clientName),
334
- )
293
+ configuredClientList.filter((clientName) => !clients.includes(clientName))
335
294
  );
336
295
  const removableClients = await findConfiguredMcpClients({
337
296
  clients: deselectedClients,
@@ -340,7 +299,7 @@ if (command === 'install-mcp') {
340
299
  });
341
300
  if (removableClients.length > 0) {
342
301
  const confirmed = await interactiveConfirm(
343
- `Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}?`,
302
+ `Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}?`
344
303
  );
345
304
  if (confirmed) {
346
305
  for (const clientName of removableClients) {
@@ -371,7 +330,7 @@ if (command === 'install-mcp') {
371
330
  for (const part of parts) {
372
331
  if (!isMcpClientName(part)) {
373
332
  process.stderr.write(
374
- `Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n`,
333
+ `Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n`
375
334
  );
376
335
  process.exit(1);
377
336
  }
@@ -396,9 +355,7 @@ if (command === 'mcp') {
396
355
  }
397
356
  if (subcommand === 'config') {
398
357
  if (!clientName || !isMcpClientName(clientName)) {
399
- process.stderr.write(
400
- `Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`,
401
- );
358
+ process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
402
359
  process.exit(1);
403
360
  }
404
361
  process.stdout.write(formatMcpConfig(clientName));
@@ -408,7 +365,11 @@ if (command === 'mcp') {
408
365
  process.exit(1);
409
366
  }
410
367
 
411
- const client = new BridgeClient();
368
+ const clientTimeoutMs = getClientTimeoutOverride();
369
+ applyWindowsTcpTransportDefaults();
370
+ const client = new BridgeClient(
371
+ clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : undefined
372
+ );
412
373
 
413
374
  await main();
414
375
 
@@ -419,7 +380,7 @@ async function main() {
419
380
  client,
420
381
  'health.ping',
421
382
  {},
422
- { source: REQUEST_SOURCE },
383
+ { source: REQUEST_SOURCE }
423
384
  );
424
385
  await printSummary(healthResponse);
425
386
  return;
@@ -427,12 +388,7 @@ async function main() {
427
388
 
428
389
  if (command === 'access-request') {
429
390
  await printSummary(
430
- await requestBridge(
431
- client,
432
- 'access.request',
433
- {},
434
- { source: REQUEST_SOURCE },
435
- ),
391
+ await requestBridge(client, 'access.request', {}, { source: REQUEST_SOURCE })
436
392
  );
437
393
  return;
438
394
  }
@@ -444,28 +400,31 @@ async function main() {
444
400
  summary:
445
401
  report.issues.length === 0
446
402
  ? 'Browser Bridge is ready.'
447
- : `Browser Bridge has ${report.issues.length} setup issue(s).`,
403
+ : `Browser Bridge has ${report.issues.length} readiness issue(s).`,
448
404
  evidence: report,
449
405
  });
450
406
  return;
451
407
  }
452
408
 
409
+ if (command === 'restart') {
410
+ const result = await restartBridgeDaemon();
411
+ printJson({
412
+ ok: true,
413
+ summary: result.previouslyRunning
414
+ ? 'Browser Bridge daemon restarted.'
415
+ : 'Browser Bridge daemon started.',
416
+ evidence: result,
417
+ });
418
+ return;
419
+ }
420
+
453
421
  if (command === 'logs') {
454
- await printSummary(
455
- await requestBridge(client, 'log.tail', {}, { source: REQUEST_SOURCE }),
456
- );
422
+ await printSummary(await requestBridge(client, 'log.tail', {}, { source: REQUEST_SOURCE }));
457
423
  return;
458
424
  }
459
425
 
460
426
  if (command === 'tabs') {
461
- await printSummary(
462
- await requestBridge(
463
- client,
464
- 'tabs.list',
465
- {},
466
- { source: REQUEST_SOURCE },
467
- ),
468
- );
427
+ await printSummary(await requestBridge(client, 'tabs.list', {}, { source: REQUEST_SOURCE }));
469
428
  return;
470
429
  }
471
430
 
@@ -477,7 +436,7 @@ async function main() {
477
436
  {
478
437
  url: url || undefined,
479
438
  },
480
- { source: REQUEST_SOURCE },
439
+ { source: REQUEST_SOURCE }
481
440
  );
482
441
  await printSummary(response);
483
442
  return;
@@ -494,7 +453,7 @@ async function main() {
494
453
  {
495
454
  tabId: parseIntArg(tabId, 'tabId'),
496
455
  },
497
- { source: REQUEST_SOURCE },
456
+ { source: REQUEST_SOURCE }
498
457
  );
499
458
  await printSummary(response);
500
459
  return;
@@ -506,7 +465,7 @@ async function main() {
506
465
  tabId,
507
466
  source: REQUEST_SOURCE,
508
467
  });
509
- printJson(response.ok ? response.result : response);
468
+ printCallResponse(response);
510
469
  return;
511
470
  }
512
471
 
@@ -514,9 +473,7 @@ async function main() {
514
473
  await ensureClientConnection();
515
474
  const input = rest[0];
516
475
  if (!input) {
517
- throw new Error(
518
- 'Usage: batch \'[{"method":"...","params":{...}}, ...]\'',
519
- );
476
+ throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
520
477
  }
521
478
  const calls = JSON.parse(input);
522
479
  if (!Array.isArray(calls)) {
@@ -534,7 +491,10 @@ async function main() {
534
491
  durationMs: 0,
535
492
  approxTokens: 0,
536
493
  meta: { protocol_version: '1.0' },
537
- error: { code: 'INVALID_REQUEST', message: 'Each batch call needs a method.' },
494
+ error: {
495
+ code: 'INVALID_REQUEST',
496
+ message: 'Each batch call needs a method.',
497
+ },
538
498
  response: null,
539
499
  };
540
500
  }
@@ -548,14 +508,16 @@ async function main() {
548
508
  durationMs: 0,
549
509
  approxTokens: 0,
550
510
  meta: { protocol_version: '1.0' },
551
- error: { code: 'INVALID_REQUEST', message: `Unknown bridge method "${call.method}".` },
511
+ error: {
512
+ code: 'INVALID_REQUEST',
513
+ message: `Unknown bridge method "${call.method}".`,
514
+ },
552
515
  response: null,
553
516
  };
554
517
  }
555
518
  const method = /** @type {BridgeMethod} */ (call.method);
556
- const tabId = methodNeedsTab(call.method) && typeof call.tabId === 'number'
557
- ? call.tabId
558
- : null;
519
+ const tabId =
520
+ methodNeedsTab(call.method) && typeof call.tabId === 'number' ? call.tabId : null;
559
521
  const startTime = Date.now();
560
522
  try {
561
523
  const response = await client.request({
@@ -578,25 +540,19 @@ async function main() {
578
540
  durationMs: Date.now() - startTime,
579
541
  });
580
542
  }
581
- }),
543
+ })
582
544
  );
583
545
  printJson(results);
584
546
  return;
585
547
  }
586
548
 
587
- if (
588
- command.includes('.')
589
- && METHODS.includes(/** @type {BridgeMethod} */ (command))
590
- ) {
591
- const { tabId, method, params } = await parseCallCommand([
592
- command,
593
- ...rest,
594
- ]);
549
+ if (command.includes('.') && METHODS.includes(/** @type {BridgeMethod} */ (command))) {
550
+ const { tabId, method, params } = await parseCallCommand([command, ...rest]);
595
551
  const response = await requestBridge(client, method, params, {
596
552
  tabId,
597
553
  source: REQUEST_SOURCE,
598
554
  });
599
- printJson(response.ok ? response.result : response);
555
+ printCallResponse(response);
600
556
  return;
601
557
  }
602
558
 
@@ -605,18 +561,13 @@ async function main() {
605
561
  let elementRef;
606
562
  if (shortcutCmd.resolve) {
607
563
  if (!rest[0]) throw new Error(`Usage: ${command} <ref|selector>`);
608
- elementRef = await resolveRef(
609
- client,
610
- rest[0],
611
- null,
612
- REQUEST_SOURCE,
613
- );
564
+ elementRef = await resolveRef(client, rest[0], null, REQUEST_SOURCE);
614
565
  }
615
566
  const response = await requestBridge(
616
567
  client,
617
568
  shortcutCmd.method,
618
569
  shortcutCmd.build(rest, elementRef),
619
- { source: REQUEST_SOURCE },
570
+ { source: REQUEST_SOURCE }
620
571
  );
621
572
  await printSummary(response, shortcutCmd.printMethod);
622
573
  return;
@@ -626,12 +577,7 @@ async function main() {
626
577
  const [key, refOrSelector] = rest;
627
578
  if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
628
579
  const elementRef = refOrSelector
629
- ? await resolveRef(
630
- client,
631
- refOrSelector,
632
- null,
633
- REQUEST_SOURCE,
634
- )
580
+ ? await resolveRef(client, refOrSelector, null, REQUEST_SOURCE)
635
581
  : undefined;
636
582
  const response = await requestBridge(
637
583
  client,
@@ -640,42 +586,51 @@ async function main() {
640
586
  key,
641
587
  target: elementRef ? { elementRef } : undefined,
642
588
  },
643
- { source: REQUEST_SOURCE },
589
+ { source: REQUEST_SOURCE }
644
590
  );
645
591
  await printSummary(response);
646
592
  return;
647
593
  }
648
594
 
595
+ if (command === 'cdp-press-key') {
596
+ const parsed = extractTabFlag(rest);
597
+ const [key, code] = parsed.rest;
598
+ if (!key) throw new Error('Usage: cdp-press-key [--tab <tabId>] <key> [code]');
599
+ const response = await requestBridge(
600
+ client,
601
+ 'cdp.dispatch_key_event',
602
+ {
603
+ key,
604
+ code,
605
+ },
606
+ {
607
+ tabId: parsed.tabId,
608
+ source: REQUEST_SOURCE,
609
+ }
610
+ );
611
+ await printSummary(response, 'cdp.dispatch_key_event');
612
+ return;
613
+ }
614
+
649
615
  if (command === 'screenshot') {
650
616
  const [refOrSelector, outputPath] = rest;
651
617
  if (!refOrSelector) throw new Error('Usage: screenshot <ref|selector> [path]');
652
- const elementRef = await resolveRef(
653
- client,
654
- refOrSelector,
655
- null,
656
- REQUEST_SOURCE,
657
- );
618
+ const elementRef = await resolveRef(client, refOrSelector, null, REQUEST_SOURCE);
658
619
  const response = await requestBridge(
659
620
  client,
660
621
  'screenshot.capture_element',
661
622
  {
662
623
  elementRef,
663
624
  },
664
- { source: REQUEST_SOURCE },
625
+ { source: REQUEST_SOURCE }
665
626
  );
666
627
  if (!response.ok) {
667
628
  await printSummary(response);
668
629
  return;
669
630
  }
670
- const screenshotResult = /** @type {ScreenshotResult} */ (
671
- response.result
672
- );
673
- const filePath =
674
- outputPath || path.join(os.tmpdir(), `bbx-${Date.now()}.png`);
675
- const data = screenshotResult.image.replace(
676
- /^data:image\/png;base64,/,
677
- '',
678
- );
631
+ const screenshotResult = /** @type {ScreenshotResult} */ (response.result);
632
+ const filePath = outputPath || path.join(os.tmpdir(), `bbx-${Date.now()}.png`);
633
+ const data = screenshotResult.image.replace(/^data:image\/png;base64,/, '');
679
634
  await fs.promises.writeFile(filePath, Buffer.from(data, 'base64'));
680
635
  printJson({
681
636
  ok: true,
@@ -689,9 +644,7 @@ async function main() {
689
644
  let expression = rest.join(' ');
690
645
  if (!expression || expression === '-') expression = await readStdin();
691
646
  if (!expression)
692
- throw new Error(
693
- 'Usage: eval <expression> (or pipe via stdin: echo "expr" | bbx eval -)',
694
- );
647
+ throw new Error('Usage: eval <expression> (or pipe via stdin: echo "expr" | bbx eval -)');
695
648
  const response = await requestBridge(
696
649
  client,
697
650
  'page.evaluate',
@@ -699,7 +652,7 @@ async function main() {
699
652
  expression,
700
653
  returnByValue: true,
701
654
  },
702
- { source: REQUEST_SOURCE },
655
+ { source: REQUEST_SOURCE }
703
656
  );
704
657
  await printSummary(response);
705
658
  return;
@@ -710,12 +663,9 @@ async function main() {
710
663
  process.exitCode = 1;
711
664
  } catch (error) {
712
665
  const message = error instanceof Error ? error.message : String(error);
713
- const raw =
714
- error instanceof Error && 'code' in error
715
- ? /** @type {any} */ (error).code
716
- : '';
666
+ const raw = error instanceof Error && 'code' in error ? /** @type {any} */ (error).code : '';
717
667
  let code = 'ERROR';
718
- if (raw === 'ENOENT' || raw === 'ECONNREFUSED') {
668
+ if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
719
669
  code = 'DAEMON_OFFLINE';
720
670
  } else if (raw === 'BRIDGE_TIMEOUT') {
721
671
  code = 'BRIDGE_TIMEOUT';
@@ -735,6 +685,48 @@ async function main() {
735
685
  }
736
686
  }
737
687
 
688
+ /**
689
+ * Allow tests to shrink request timeouts without changing the shared default.
690
+ *
691
+ * @returns {number | undefined}
692
+ */
693
+ function getClientTimeoutOverride() {
694
+ const raw = process.env[TEST_TIMEOUT_ENV];
695
+ if (!raw) {
696
+ return undefined;
697
+ }
698
+
699
+ const value = Number.parseInt(raw, 10);
700
+ return Number.isFinite(value) && value > 0 ? value : undefined;
701
+ }
702
+
703
+ /**
704
+ * Allow CLI tests to provide deterministic MCP client detection without relying
705
+ * on whatever tools happen to be installed on the host machine.
706
+ *
707
+ * @returns {{
708
+ * mcpDetectors?: Record<string, () => boolean>,
709
+ * }}
710
+ */
711
+ function getSetupStatusTestOverrides() {
712
+ if (!(TEST_DETECTED_MCP_CLIENTS_ENV in process.env)) {
713
+ return {};
714
+ }
715
+
716
+ const detectedClients = new Set(
717
+ (process.env[TEST_DETECTED_MCP_CLIENTS_ENV] || '')
718
+ .split(',')
719
+ .map((value) => value.trim().toLowerCase())
720
+ .filter(Boolean)
721
+ );
722
+
723
+ return {
724
+ mcpDetectors: Object.fromEntries(
725
+ MCP_CLIENT_NAMES.map((clientName) => [clientName, () => detectedClients.has(clientName)])
726
+ ),
727
+ };
728
+ }
729
+
738
730
  /**
739
731
  * @param {{ detected: boolean, installed: boolean }} options
740
732
  * @returns {string | undefined}
@@ -766,10 +758,7 @@ async function ensureClientConnection() {
766
758
  * @returns {Promise<void>}
767
759
  */
768
760
  async function printSummary(response, method) {
769
- printJson(annotateBridgeSummary(
770
- summarizeBridgeResponse(response, method),
771
- response,
772
- ));
761
+ printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
773
762
  }
774
763
 
775
764
  /**
@@ -778,10 +767,28 @@ async function printSummary(response, method) {
778
767
  */
779
768
  function printJson(value) {
780
769
  process.stdout.write(
781
- `${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`,
770
+ `${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`
782
771
  );
783
772
  }
784
773
 
774
+ /**
775
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
776
+ * @returns {void}
777
+ */
778
+ function printCallResponse(response) {
779
+ if (response.ok) {
780
+ printJson(response.result);
781
+ return;
782
+ }
783
+
784
+ process.exitCode = 1;
785
+ const errorText = `${response.error.code}: ${response.error.message}`;
786
+ process.stderr.write(
787
+ `${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
788
+ );
789
+ printJson(response);
790
+ }
791
+
785
792
  function printUsage() {
786
793
  const blocks = ['Usage: bbx <command> [args]'];
787
794
  for (const section of CLI_HELP_SECTIONS) {
@@ -846,9 +853,7 @@ async function parseCallCommand(args) {
846
853
  if (first.includes('.')) {
847
854
  const method = /** @type {BridgeMethod} */ (first);
848
855
  if (!METHODS.includes(method)) {
849
- throw new Error(
850
- `Unknown method "${first}". Run bbx skill to see available methods.`,
851
- );
856
+ throw new Error(`Unknown method "${first}". Run bbx skill to see available methods.`);
852
857
  }
853
858
  let rawParams = second;
854
859
  // Support piped stdin: `echo '{"key":"val"}' | bbx call method -`