@browserbridge/bbx 1.0.1 → 1.2.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 (70) 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 +122 -45
  5. package/packages/agent-client/src/client.js +134 -8
  6. package/packages/agent-client/src/command-registry.js +4 -1
  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-capture.js +279 -0
  13. package/packages/mcp-server/src/handlers-dom.js +196 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +296 -0
  17. package/packages/mcp-server/src/handlers.js +63 -1159
  18. package/packages/mcp-server/src/server.js +13 -3
  19. package/packages/native-host/bin/bridge-daemon.js +34 -4
  20. package/packages/native-host/bin/install-manifest.js +32 -2
  21. package/packages/native-host/bin/postinstall.js +16 -0
  22. package/packages/native-host/src/config.js +131 -6
  23. package/packages/native-host/src/daemon-logger.js +157 -0
  24. package/packages/native-host/src/daemon-process.js +422 -0
  25. package/packages/native-host/src/daemon.js +322 -77
  26. package/packages/native-host/src/framing.js +131 -11
  27. package/packages/native-host/src/install-manifest.js +121 -7
  28. package/packages/native-host/src/native-host.js +110 -73
  29. package/packages/protocol/src/capabilities.js +4 -0
  30. package/packages/protocol/src/defaults.js +1 -0
  31. package/packages/protocol/src/errors.js +4 -0
  32. package/packages/protocol/src/payload-cost.js +19 -6
  33. package/packages/protocol/src/protocol.js +143 -7
  34. package/packages/protocol/src/registry.js +13 -0
  35. package/packages/protocol/src/summary.js +18 -10
  36. package/packages/protocol/src/types.js +28 -3
  37. package/skills/browser-bridge/SKILL.md +2 -1
  38. package/skills/browser-bridge/references/interaction.md +1 -0
  39. package/skills/browser-bridge/references/protocol.md +2 -1
  40. package/CHANGELOG.md +0 -55
  41. package/assets/banner.jpg +0 -0
  42. package/assets/logo.png +0 -0
  43. package/assets/logo.svg +0 -65
  44. package/docs/api-reference.md +0 -157
  45. package/docs/cli-guide.md +0 -128
  46. package/docs/index.md +0 -25
  47. package/docs/manual-setup.md +0 -140
  48. package/docs/mcp-vs-cli.md +0 -258
  49. package/docs/publishing.md +0 -112
  50. package/docs/quickstart.md +0 -104
  51. package/docs/troubleshooting.md +0 -59
  52. package/docs/unpacked-extension.md +0 -72
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -38
  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 -474
  60. package/packages/extension/src/background-routing.js +0 -89
  61. package/packages/extension/src/background.js +0 -3490
  62. package/packages/extension/src/content-script-helpers.js +0 -282
  63. package/packages/extension/src/content-script.js +0 -2043
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -104
  66. package/packages/extension/ui/popup.html +0 -35
  67. package/packages/extension/ui/popup.js +0 -298
  68. package/packages/extension/ui/sidepanel.html +0 -102
  69. package/packages/extension/ui/sidepanel.js +0 -1771
  70. 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.2.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,17 +592,39 @@ 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
- const [refOrSelector, outputPath] = rest;
602
- if (!refOrSelector) throw new Error('Usage: screenshot <ref|selector> [path]');
603
- const elementRef = await resolveRef(client, refOrSelector, null, REQUEST_SOURCE);
616
+ const parsed = extractTabFlag(rest);
617
+ const [refOrSelector, outputPath] = parsed.rest;
618
+ if (!refOrSelector)
619
+ throw new Error('Usage: screenshot [--tab <tabId>] <ref|selector> [path]');
620
+ const elementRef = await resolveRef(client, refOrSelector, parsed.tabId, REQUEST_SOURCE);
604
621
  const response = await requestBridge(
605
622
  client,
606
623
  'screenshot.capture_element',
607
624
  {
608
625
  elementRef,
609
626
  },
610
- { source: REQUEST_SOURCE }
627
+ { tabId: parsed.tabId, source: REQUEST_SOURCE }
611
628
  );
612
629
  if (!response.ok) {
613
630
  await printSummary(response);
@@ -650,7 +667,7 @@ async function main() {
650
667
  const message = error instanceof Error ? error.message : String(error);
651
668
  const raw = error instanceof Error && 'code' in error ? /** @type {any} */ (error).code : '';
652
669
  let code = 'ERROR';
653
- if (raw === 'ENOENT' || raw === 'ECONNREFUSED') {
670
+ if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
654
671
  code = 'DAEMON_OFFLINE';
655
672
  } else if (raw === 'BRIDGE_TIMEOUT') {
656
673
  code = 'BRIDGE_TIMEOUT';
@@ -670,6 +687,48 @@ async function main() {
670
687
  }
671
688
  }
672
689
 
690
+ /**
691
+ * Allow tests to shrink request timeouts without changing the shared default.
692
+ *
693
+ * @returns {number | undefined}
694
+ */
695
+ function getClientTimeoutOverride() {
696
+ const raw = process.env[TEST_TIMEOUT_ENV];
697
+ if (!raw) {
698
+ return undefined;
699
+ }
700
+
701
+ const value = Number.parseInt(raw, 10);
702
+ return Number.isFinite(value) && value > 0 ? value : undefined;
703
+ }
704
+
705
+ /**
706
+ * Allow CLI tests to provide deterministic MCP client detection without relying
707
+ * on whatever tools happen to be installed on the host machine.
708
+ *
709
+ * @returns {{
710
+ * mcpDetectors?: Record<string, () => boolean>,
711
+ * }}
712
+ */
713
+ function getSetupStatusTestOverrides() {
714
+ if (!(TEST_DETECTED_MCP_CLIENTS_ENV in process.env)) {
715
+ return {};
716
+ }
717
+
718
+ const detectedClients = new Set(
719
+ (process.env[TEST_DETECTED_MCP_CLIENTS_ENV] || '')
720
+ .split(',')
721
+ .map((value) => value.trim().toLowerCase())
722
+ .filter(Boolean)
723
+ );
724
+
725
+ return {
726
+ mcpDetectors: Object.fromEntries(
727
+ MCP_CLIENT_NAMES.map((clientName) => [clientName, () => detectedClients.has(clientName)])
728
+ ),
729
+ };
730
+ }
731
+
673
732
  /**
674
733
  * @param {{ detected: boolean, installed: boolean }} options
675
734
  * @returns {string | undefined}
@@ -714,6 +773,24 @@ function printJson(value) {
714
773
  );
715
774
  }
716
775
 
776
+ /**
777
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
778
+ * @returns {void}
779
+ */
780
+ function printCallResponse(response) {
781
+ if (response.ok) {
782
+ printJson(response.result);
783
+ return;
784
+ }
785
+
786
+ process.exitCode = 1;
787
+ const errorText = `${response.error.code}: ${response.error.message}`;
788
+ process.stderr.write(
789
+ `${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
790
+ );
791
+ printJson(response);
792
+ }
793
+
717
794
  function printUsage() {
718
795
  const blocks = ['Usage: bbx <command> [args]'];
719
796
  for (const section of CLI_HELP_SECTIONS) {
@@ -10,14 +10,23 @@ 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
+ import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
19
+
20
+ /** @typedef {import('../../native-host/src/config.js').BridgeTransport} BridgeTransport */
14
21
 
15
22
  /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
16
23
  /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
17
24
  /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
18
25
  /**
19
26
  * @typedef {{
27
+ * extensionConnected?: boolean,
20
28
  * supported_versions?: string[],
29
+ * daemon_supported_versions?: string[],
21
30
  * deprecated_since?: string,
22
31
  * migration_hint?: string
23
32
  * }} ProtocolHealthResult
@@ -42,6 +51,36 @@ import { getSocketPath } from '../../native-host/src/config.js';
42
51
  * }} PendingRequest
43
52
  */
44
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
+ */
65
+
66
+ /**
67
+ * @param {string} left
68
+ * @param {string} right
69
+ * @returns {number}
70
+ */
71
+ function compareProtocolVersions(left, right) {
72
+ const leftParts = left.split('.').map((part) => Number(part) || 0);
73
+ const rightParts = right.split('.').map((part) => Number(part) || 0);
74
+ const length = Math.max(leftParts.length, rightParts.length);
75
+ for (let index = 0; index < length; index += 1) {
76
+ const delta = (leftParts[index] || 0) - (rightParts[index] || 0);
77
+ if (delta !== 0) {
78
+ return delta > 0 ? 1 : -1;
79
+ }
80
+ }
81
+ return 0;
82
+ }
83
+
45
84
  /**
46
85
  * @param {string} method
47
86
  * @param {number} timeoutMs
@@ -56,17 +95,27 @@ function createTimeoutError(method, timeoutMs) {
56
95
  }
57
96
 
58
97
  export class BridgeClient extends EventEmitter {
98
+ /**
99
+ * @param {BridgeClientOptions} [options={}]
100
+ */
59
101
  constructor({
60
- socketPath = getSocketPath(),
102
+ transport = getBridgeTransport(),
103
+ socketPath = undefined,
61
104
  clientId = `agent_${randomUUID()}`,
62
105
  defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
63
106
  autoReconnect = false,
107
+ restartDaemonOnVersionMismatch = true,
108
+ restartDaemonFn = restartBridgeDaemon,
64
109
  } = {}) {
65
110
  super();
66
- this.socketPath = socketPath;
111
+ this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
112
+ this.socketPath =
113
+ this.transport.type === 'socket' ? this.transport.socketPath : getSocketPath();
67
114
  this.clientId = clientId;
68
115
  this.defaultTimeoutMs = defaultTimeoutMs;
69
116
  this.autoReconnect = autoReconnect;
117
+ this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
118
+ this.restartDaemonFn = restartDaemonFn;
70
119
  this.socket = null;
71
120
  this.connected = false;
72
121
  this.protocolCompatibility = null;
@@ -74,6 +123,7 @@ export class BridgeClient extends EventEmitter {
74
123
  /** @type {Map<string, PendingRequest>} */
75
124
  this.waiting = new Map();
76
125
  this._reconnecting = false;
126
+ this._attemptedVersionMismatchRestart = false;
77
127
  }
78
128
 
79
129
  /**
@@ -83,7 +133,10 @@ export class BridgeClient extends EventEmitter {
83
133
  if (this.socket) {
84
134
  throw new Error('BridgeClient is already connected.');
85
135
  }
86
- const socket = net.createConnection(this.socketPath);
136
+ const socket =
137
+ this.transport.type === 'tcp'
138
+ ? net.createConnection({ host: this.transport.host, port: this.transport.port })
139
+ : net.createConnection(this.transport.socketPath);
87
140
  this.socket = socket;
88
141
  try {
89
142
  await new Promise((resolve, reject) => {
@@ -148,19 +201,38 @@ export class BridgeClient extends EventEmitter {
148
201
  });
149
202
  });
150
203
 
204
+ this.protocolCompatibility = null;
205
+ this.protocolWarning = null;
206
+
207
+ /** @type {ProtocolHealthResult | null} */
208
+ let healthResult = null;
151
209
  try {
152
210
  const healthResponse = await this.request({
153
211
  method: 'health.ping',
154
212
  });
155
213
  if (healthResponse.ok) {
156
- this.protocolCompatibility = BridgeClient.checkProtocolVersion(
157
- /** @type {ProtocolHealthResult} */ (healthResponse.result)
158
- );
159
- this.protocolWarning = this.protocolCompatibility.warning ?? null;
214
+ healthResult = /** @type {ProtocolHealthResult} */ (healthResponse.result);
160
215
  }
161
216
  } catch {
162
217
  this.protocolCompatibility = null;
163
218
  this.protocolWarning = null;
219
+ return;
220
+ }
221
+
222
+ if (!healthResult) {
223
+ return;
224
+ }
225
+
226
+ this.protocolCompatibility = BridgeClient.checkProtocolVersion(healthResult);
227
+ this.protocolWarning = this.protocolCompatibility.warning ?? null;
228
+ if (this.protocolCompatibility.compatible) {
229
+ this._attemptedVersionMismatchRestart = false;
230
+ }
231
+ if (this.shouldRestartDaemonForProtocolMismatch(healthResult)) {
232
+ this._attemptedVersionMismatchRestart = true;
233
+ await this.disconnectForDaemonRestart();
234
+ await this.restartDaemonFn({ transport: this.transport });
235
+ await this.connect();
164
236
  }
165
237
  }
166
238
 
@@ -280,6 +352,60 @@ export class BridgeClient extends EventEmitter {
280
352
  this._reconnecting = false;
281
353
  }
282
354
 
355
+ /**
356
+ * @param {ProtocolHealthResult} healthResult
357
+ * @returns {boolean}
358
+ */
359
+ shouldRestartDaemonForProtocolMismatch(healthResult) {
360
+ if (
361
+ !this.restartDaemonOnVersionMismatch ||
362
+ this._attemptedVersionMismatchRestart ||
363
+ !this.protocolCompatibility ||
364
+ this.protocolCompatibility.compatible
365
+ ) {
366
+ return false;
367
+ }
368
+
369
+ const remoteVersions = Array.isArray(healthResult?.daemon_supported_versions)
370
+ ? healthResult.daemon_supported_versions
371
+ : healthResult?.extensionConnected === true
372
+ ? []
373
+ : Array.isArray(healthResult?.supported_versions)
374
+ ? healthResult.supported_versions
375
+ : [];
376
+ const latestRemote = remoteVersions[0];
377
+ return (
378
+ typeof latestRemote === 'string' &&
379
+ compareProtocolVersions(latestRemote, PROTOCOL_VERSION) < 0
380
+ );
381
+ }
382
+
383
+ /**
384
+ * Drop the current socket before forcing a daemon restart so the next
385
+ * connect() call observes a fresh local process rather than the existing one.
386
+ *
387
+ * @returns {Promise<void>}
388
+ */
389
+ async disconnectForDaemonRestart() {
390
+ const socket = this.socket;
391
+ if (!socket) {
392
+ return;
393
+ }
394
+
395
+ const previousAutoReconnect = this.autoReconnect;
396
+ this.autoReconnect = false;
397
+ this.connected = false;
398
+ this.socket = null;
399
+
400
+ if (!socket.destroyed) {
401
+ const closed = once(socket, 'close').catch(() => {});
402
+ socket.destroy();
403
+ await closed;
404
+ }
405
+
406
+ this.autoReconnect = previousAutoReconnect;
407
+ }
408
+
283
409
  /**
284
410
  * Check whether the remote side supports our protocol version.
285
411
  * Call after a successful health.ping to get early warnings about version drift.
@@ -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
  {
@@ -344,7 +347,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
344
347
  {
345
348
  title: 'Capture',
346
349
  lines: [
347
- 'bbx screenshot <ref|selector> [path] Capture partial element screenshot',
350
+ 'bbx screenshot [--tab <tabId>] <ref|selector> [path] Capture partial element screenshot',
348
351
  ],
349
352
  },
350
353
  ]);