@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.
- package/README.md +4 -4
- package/package.json +11 -13
- package/packages/agent-client/src/cli-helpers.js +33 -0
- package/packages/agent-client/src/cli.js +122 -45
- package/packages/agent-client/src/client.js +134 -8
- package/packages/agent-client/src/command-registry.js +4 -1
- package/packages/agent-client/src/detect.js +159 -48
- package/packages/agent-client/src/install.js +24 -1
- package/packages/agent-client/src/mcp-config.js +29 -10
- package/packages/agent-client/src/setup-status.js +12 -4
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers-capture.js +279 -0
- package/packages/mcp-server/src/handlers-dom.js +196 -0
- package/packages/mcp-server/src/handlers-navigation.js +79 -0
- package/packages/mcp-server/src/handlers-page.js +365 -0
- package/packages/mcp-server/src/handlers-utils.js +296 -0
- package/packages/mcp-server/src/handlers.js +63 -1159
- package/packages/mcp-server/src/server.js +13 -3
- package/packages/native-host/bin/bridge-daemon.js +34 -4
- package/packages/native-host/bin/install-manifest.js +32 -2
- package/packages/native-host/bin/postinstall.js +16 -0
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-logger.js +157 -0
- package/packages/native-host/src/daemon-process.js +422 -0
- package/packages/native-host/src/daemon.js +322 -77
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +121 -7
- package/packages/native-host/src/native-host.js +110 -73
- package/packages/protocol/src/capabilities.js +4 -0
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +4 -0
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +143 -7
- package/packages/protocol/src/registry.js +13 -0
- package/packages/protocol/src/summary.js +18 -10
- package/packages/protocol/src/types.js +28 -3
- package/skills/browser-bridge/SKILL.md +2 -1
- package/skills/browser-bridge/references/interaction.md +1 -0
- package/skills/browser-bridge/references/protocol.md +2 -1
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -112
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/unpacked-extension.md +0 -72
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -38
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -474
- package/packages/extension/src/background-routing.js +0 -89
- package/packages/extension/src/background.js +0 -3490
- package/packages/extension/src/content-script-helpers.js +0 -282
- package/packages/extension/src/content-script.js +0 -2043
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -104
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -298
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1771
- 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
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
> **Chrome Web Store
|
|
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://
|
|
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
|
|
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
|
|
60
|
-
"test": "c8 node --test
|
|
55
|
+
"test:runtime": "node --test",
|
|
56
|
+
"test": "c8 node --test",
|
|
61
57
|
"coverage": "c8 report --reporter=text --reporter=text-summary",
|
|
62
|
-
"coverage:
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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]
|
|
350
|
+
'bbx screenshot [--tab <tabId>] <ref|selector> [path] Capture partial element screenshot',
|
|
348
351
|
],
|
|
349
352
|
},
|
|
350
353
|
]);
|