@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
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Browser Bridge
1
+ # Browser Bridge (BBX)
2
2
 
3
- ![Browser Bridge: Connect AI Agent and Browsers](https://raw.githubusercontent.com/koltyakov/browser-bridge/main/assets/banner.jpg)
3
+ ![Browser Bridge (BBX): Connect AI Agent and Browsers](https://raw.githubusercontent.com/koltyakov/browser-bridge/main/assets/banner.jpg)
4
4
 
5
- > **Chrome Web Store status:** The extension is currently under review. Until the listing is live, use the unpacked install flow in [docs/unpacked-extension.md](https://github.com/koltyakov/browser-bridge/blob/main/docs/unpacked-extension.md).
5
+ > **Chrome Web Store:** Install `Browser Bridge (BBX)` from the [Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie). For local or custom builds, use the unpacked install flow in [docs/unpacked-extension.md](https://github.com/koltyakov/browser-bridge/blob/main/docs/unpacked-extension.md).
6
6
 
7
7
  A local bridge between your coding agent and a real Chrome tab. Browser Bridge gives the agent structured access to DOM, styles, layout, console, network, and reversible patches - starting from the actual tab you already have open, with all its real state intact.
8
8
 
@@ -105,7 +105,7 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
105
105
 
106
106
  ## Setup
107
107
 
108
- 1. Install [Browser Bridge from the Chrome Web Store](https://chrome.google.com/webstore/detail/jjjkmmcdkpcgamlopogicbnnhdgebhie) <!-- TODO: replace with final store link after publishing -->
108
+ 1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie)
109
109
  2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
110
110
  3. In the extension side panel, install MCP or CLI (skill) for your agent of choice
111
111
  4. Enable Browser Bridge for the Chrome window you want to inspect/control with the AI agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -32,21 +32,17 @@
32
32
  "bbx-mcp": "packages/mcp-server/src/bin.js"
33
33
  },
34
34
  "files": [
35
- "assets",
36
- "CHANGELOG.md",
37
35
  "LICENSE",
38
36
  "README.md",
39
- "docs",
40
- "manifest.json",
41
37
  "packages/agent-client/src",
42
- "packages/extension/assets",
43
- "packages/extension/src",
44
- "packages/extension/ui",
45
38
  "packages/mcp-server/src",
46
39
  "packages/native-host/bin",
47
40
  "packages/native-host/src",
48
41
  "packages/protocol/src",
49
- "skills/browser-bridge"
42
+ "skills/browser-bridge",
43
+ "!**/test/**",
44
+ "!**/*.test.*",
45
+ "!**/*.spec.*"
50
46
  ],
51
47
  "type": "module",
52
48
  "publishConfig": {
@@ -56,16 +52,17 @@
56
52
  "lint": "oxlint . && oxfmt --check .",
57
53
  "format": "oxfmt .",
58
54
  "format:check": "oxfmt --check .",
59
- "test:runtime": "node --test packages/protocol/test/*.test.js packages/native-host/test/*.test.js packages/agent-client/test/*.test.js packages/mcp-server/test/*.test.js packages/extension/test/*.test.js",
60
- "test": "c8 node --test packages/protocol/test/*.test.js packages/native-host/test/*.test.js packages/agent-client/test/*.test.js packages/mcp-server/test/*.test.js packages/extension/test/*.test.js",
55
+ "test:runtime": "node --test",
56
+ "test": "c8 node --test",
61
57
  "coverage": "c8 report --reporter=text --reporter=text-summary",
62
- "coverage:check": "c8 check-coverage --lines 70",
58
+ "coverage:review": "c8 report --reporter=json-summary && node scripts/review-coverage.mjs",
59
+ "coverage:check": "c8 check-coverage --lines 80 --branches 75",
63
60
  "typecheck": "tsc --noEmit",
64
61
  "postinstall": "node packages/native-host/bin/postinstall.js",
65
62
  "package:extension": "node scripts/package-extension.mjs",
66
63
  "check:extension-zip": "node scripts/check-extension-zip.mjs",
67
64
  "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm pack --dry-run",
68
- "prepublishOnly": "npm run lint && npm run typecheck && npm test",
65
+ "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run coverage:check",
69
66
  "status": "node packages/agent-client/src/cli.js status",
70
67
  "daemon": "node packages/native-host/bin/bridge-daemon.js",
71
68
  "install-manifest": "node packages/native-host/bin/install-manifest.js"
@@ -78,6 +75,7 @@
78
75
  "@types/chrome": "^0.1.40",
79
76
  "@types/node": "^25.5.2",
80
77
  "c8": "^11.0.0",
78
+ "linkedom": "^0.18.12",
81
79
  "oxfmt": "^0.47.0",
82
80
  "oxlint": "^1.62.0",
83
81
  "typescript": "^6.0.3"
@@ -3,6 +3,39 @@
3
3
  import readline from 'node:readline';
4
4
  import { bridgeMethodNeedsTab } from '../../protocol/src/index.js';
5
5
 
6
+ /**
7
+ * Strip ANSI escape sequences from a string to prevent terminal injection
8
+ * from malicious page content (e.g. DOM text, console output, eval results).
9
+ *
10
+ * @param {string} str
11
+ * @returns {string}
12
+ */
13
+ export function stripAnsi(str) {
14
+ // oxlint-disable-next-line no-control-regex
15
+ return str.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\x1b[^[]/g, '');
16
+ }
17
+
18
+ /**
19
+ * Recursively sanitize all string values in a value tree by stripping ANSI
20
+ * escape sequences. Only strings are touched; structure is preserved.
21
+ *
22
+ * @param {unknown} value
23
+ * @returns {unknown}
24
+ */
25
+ export function sanitizeOutput(value) {
26
+ if (typeof value === 'string') return stripAnsi(value);
27
+ if (Array.isArray(value)) return value.map(sanitizeOutput);
28
+ if (value !== null && typeof value === 'object') {
29
+ return Object.fromEntries(
30
+ Object.entries(/** @type {Record<string, unknown>} */ (value)).map(([k, v]) => [
31
+ k,
32
+ sanitizeOutput(v),
33
+ ])
34
+ );
35
+ }
36
+ return value;
37
+ }
38
+
6
39
  /**
7
40
  * @param {string[]} values
8
41
  * @returns {Record<string, string>}
@@ -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,
@@ -50,39 +54,8 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
50
54
  /** @typedef {{ image: string, rect: Record<string, unknown> }} ScreenshotResult */
51
55
 
52
56
  const REQUEST_SOURCE = 'cli';
53
-
54
- /**
55
- * Strip ANSI escape sequences from a string to prevent terminal injection
56
- * from malicious page content (e.g. DOM text, console output, eval results).
57
- *
58
- * @param {string} str
59
- * @returns {string}
60
- */
61
- function stripAnsi(str) {
62
- // oxlint-disable-next-line no-control-regex
63
- return str.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\x1b[^[]/g, '');
64
- }
65
-
66
- /**
67
- * Recursively sanitize all string values in a value tree by stripping ANSI
68
- * escape sequences. Only strings are touched; structure is preserved.
69
- *
70
- * @param {unknown} value
71
- * @returns {unknown}
72
- */
73
- function sanitizeOutput(value) {
74
- if (typeof value === 'string') return stripAnsi(value);
75
- if (Array.isArray(value)) return value.map(sanitizeOutput);
76
- if (value !== null && typeof value === 'object') {
77
- return Object.fromEntries(
78
- Object.entries(/** @type {Record<string, unknown>} */ (value)).map(([k, v]) => [
79
- k,
80
- sanitizeOutput(v),
81
- ])
82
- );
83
- }
84
- return value;
85
- }
57
+ const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
58
+ const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
86
59
 
87
60
  /**
88
61
  * Read all of stdin as UTF-8 text. Resolves once stdin closes.
@@ -164,9 +137,12 @@ if (command === 'install-skill') {
164
137
  global: isGlobal,
165
138
  cwd: process.cwd(),
166
139
  projectPath: isGlobal ? os.homedir() : process.cwd(),
140
+ ...getSetupStatusTestOverrides(),
167
141
  });
168
142
  /** @type {import('./install.js').SupportedTarget[]} */
169
- const detected = detectSkillTargets();
143
+ const detected = /** @type {import('./install.js').SupportedTarget[]} */ (
144
+ setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
145
+ );
170
146
  const installedManagedTargets = new Set(
171
147
  setupStatus.skillTargets
172
148
  .filter((entry) => entry.installed && entry.managed)
@@ -274,8 +250,11 @@ if (command === 'install-mcp') {
274
250
  global: isGlobal,
275
251
  cwd: process.cwd(),
276
252
  projectPath: process.cwd(),
253
+ ...getSetupStatusTestOverrides(),
277
254
  });
278
- const detected = detectMcpClients();
255
+ const detected = /** @type {import('./mcp-config.js').McpClientName[]} */ (
256
+ setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
257
+ );
279
258
  const configuredClients = new Set(
280
259
  setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
281
260
  );
@@ -386,7 +365,11 @@ if (command === 'mcp') {
386
365
  process.exit(1);
387
366
  }
388
367
 
389
- const client = new BridgeClient();
368
+ const clientTimeoutMs = getClientTimeoutOverride();
369
+ applyWindowsTcpTransportDefaults();
370
+ const client = new BridgeClient(
371
+ clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : undefined
372
+ );
390
373
 
391
374
  await main();
392
375
 
@@ -423,6 +406,18 @@ async function main() {
423
406
  return;
424
407
  }
425
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
+
426
421
  if (command === 'logs') {
427
422
  await printSummary(await requestBridge(client, 'log.tail', {}, { source: REQUEST_SOURCE }));
428
423
  return;
@@ -470,7 +465,7 @@ async function main() {
470
465
  tabId,
471
466
  source: REQUEST_SOURCE,
472
467
  });
473
- printJson(response.ok ? response.result : response);
468
+ printCallResponse(response);
474
469
  return;
475
470
  }
476
471
 
@@ -557,7 +552,7 @@ async function main() {
557
552
  tabId,
558
553
  source: REQUEST_SOURCE,
559
554
  });
560
- printJson(response.ok ? response.result : response);
555
+ printCallResponse(response);
561
556
  return;
562
557
  }
563
558
 
@@ -597,6 +592,26 @@ async function main() {
597
592
  return;
598
593
  }
599
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
+
600
615
  if (command === 'screenshot') {
601
616
  const [refOrSelector, outputPath] = rest;
602
617
  if (!refOrSelector) throw new Error('Usage: screenshot <ref|selector> [path]');
@@ -650,7 +665,7 @@ async function main() {
650
665
  const message = error instanceof Error ? error.message : String(error);
651
666
  const raw = error instanceof Error && 'code' in error ? /** @type {any} */ (error).code : '';
652
667
  let code = 'ERROR';
653
- if (raw === 'ENOENT' || raw === 'ECONNREFUSED') {
668
+ if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
654
669
  code = 'DAEMON_OFFLINE';
655
670
  } else if (raw === 'BRIDGE_TIMEOUT') {
656
671
  code = 'BRIDGE_TIMEOUT';
@@ -670,6 +685,48 @@ async function main() {
670
685
  }
671
686
  }
672
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
+
673
730
  /**
674
731
  * @param {{ detected: boolean, installed: boolean }} options
675
732
  * @returns {string | undefined}
@@ -714,6 +771,24 @@ function printJson(value) {
714
771
  );
715
772
  }
716
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
+
717
792
  function printUsage() {
718
793
  const blocks = ['Usage: bbx <command> [args]'];
719
794
  for (const section of CLI_HELP_SECTIONS) {
@@ -10,7 +10,13 @@ import {
10
10
  PROTOCOL_VERSION,
11
11
  parseJsonLines,
12
12
  } from '../../protocol/src/index.js';
13
- import { getSocketPath } from '../../native-host/src/config.js';
13
+ import {
14
+ createSocketBridgeTransport,
15
+ getBridgeTransport,
16
+ getSocketPath,
17
+ } from '../../native-host/src/config.js';
18
+
19
+ /** @typedef {import('../../native-host/src/config.js').BridgeTransport} BridgeTransport */
14
20
 
15
21
  /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
16
22
  /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
@@ -42,6 +48,16 @@ import { getSocketPath } from '../../native-host/src/config.js';
42
48
  * }} PendingRequest
43
49
  */
44
50
 
51
+ /**
52
+ * @typedef {{
53
+ * transport?: BridgeTransport,
54
+ * socketPath?: string,
55
+ * clientId?: string,
56
+ * defaultTimeoutMs?: number,
57
+ * autoReconnect?: boolean,
58
+ * }} BridgeClientOptions
59
+ */
60
+
45
61
  /**
46
62
  * @param {string} method
47
63
  * @param {number} timeoutMs
@@ -56,14 +72,20 @@ function createTimeoutError(method, timeoutMs) {
56
72
  }
57
73
 
58
74
  export class BridgeClient extends EventEmitter {
75
+ /**
76
+ * @param {BridgeClientOptions} [options={}]
77
+ */
59
78
  constructor({
60
- socketPath = getSocketPath(),
79
+ transport = getBridgeTransport(),
80
+ socketPath = undefined,
61
81
  clientId = `agent_${randomUUID()}`,
62
82
  defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
63
83
  autoReconnect = false,
64
84
  } = {}) {
65
85
  super();
66
- this.socketPath = socketPath;
86
+ this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
87
+ this.socketPath =
88
+ this.transport.type === 'socket' ? this.transport.socketPath : getSocketPath();
67
89
  this.clientId = clientId;
68
90
  this.defaultTimeoutMs = defaultTimeoutMs;
69
91
  this.autoReconnect = autoReconnect;
@@ -83,7 +105,10 @@ export class BridgeClient extends EventEmitter {
83
105
  if (this.socket) {
84
106
  throw new Error('BridgeClient is already connected.');
85
107
  }
86
- const socket = net.createConnection(this.socketPath);
108
+ const socket =
109
+ this.transport.type === 'tcp'
110
+ ? net.createConnection({ host: this.transport.host, port: this.transport.port })
111
+ : net.createConnection(this.transport.socketPath);
87
112
  this.socket = socket;
88
113
  try {
89
114
  await new Promise((resolve, reject) => {
@@ -237,6 +237,7 @@ export const CLI_METHOD_BINDINGS = Object.freeze({
237
237
  ),
238
238
  ...Object.fromEntries(BRIDGE_METHODS.map((method) => [method, method])),
239
239
  'press-key': 'input.press_key',
240
+ 'cdp-press-key': 'cdp.dispatch_key_event',
240
241
  screenshot: 'screenshot.capture_element',
241
242
  eval: 'page.evaluate',
242
243
  });
@@ -253,6 +254,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
253
254
  'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf',
254
255
  'bbx status Check bridge connection',
255
256
  'bbx doctor Diagnose install, daemon, extension, and access readiness',
257
+ 'bbx restart Restart the local Browser Bridge daemon',
256
258
  'bbx access-request Request Browser Bridge access for the focused window',
257
259
  'bbx logs Recent bridge logs',
258
260
  'bbx tabs List available tabs',
@@ -330,6 +332,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
330
332
  `${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
331
333
  ),
332
334
  'bbx press-key <key> [ref|selector] Send key event',
335
+ 'bbx cdp-press-key [--tab <tabId>] <key> [code] Dispatch CDP keyDown/keyUp without focusing the tab',
333
336
  ],
334
337
  },
335
338
  {