@browserbridge/bbx 1.2.0 → 1.4.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 +8 -5
- package/package.json +2 -2
- package/packages/agent-client/src/cli.js +56 -31
- package/packages/agent-client/src/client.js +81 -65
- package/packages/agent-client/src/command-registry.js +4 -15
- package/packages/agent-client/src/detect.js +3 -3
- package/packages/agent-client/src/install.js +3 -7
- package/packages/agent-client/src/mcp-config.js +20 -5
- package/packages/agent-client/src/runtime.js +7 -41
- package/packages/agent-client/src/setup-status.js +3 -13
- package/packages/agent-client/src/types.ts +139 -0
- package/packages/mcp-server/src/guidance.js +241 -0
- package/packages/mcp-server/src/handlers-capture.js +91 -16
- package/packages/mcp-server/src/handlers-dom.js +59 -4
- package/packages/mcp-server/src/handlers-navigation.js +22 -2
- package/packages/mcp-server/src/handlers-page.js +6 -11
- package/packages/mcp-server/src/handlers-utils.js +69 -1
- package/packages/mcp-server/src/server.js +111 -28
- package/packages/native-host/bin/postinstall.js +42 -21
- package/packages/native-host/src/auth-token.js +92 -0
- package/packages/native-host/src/daemon-process.js +1 -2
- package/packages/native-host/src/daemon.js +199 -30
- package/packages/native-host/src/framing.js +13 -0
- package/packages/native-host/src/native-host.js +25 -7
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/json-lines.js +29 -1
- package/packages/protocol/src/protocol.js +43 -0
- package/packages/protocol/src/registry.js +3 -9
- package/packages/protocol/src/types.ts +574 -0
- package/skills/browser-bridge/SKILL.md +21 -5
- package/skills/browser-bridge/agents/openai.yaml +1 -1
- package/skills/browser-bridge/references/interaction.md +6 -6
- package/skills/browser-bridge/references/protocol.md +57 -54
- package/skills/browser-bridge/references/ui-workflows.md +1 -1
- package/packages/protocol/src/types.js +0 -626
package/README.md
CHANGED
|
@@ -99,17 +99,20 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
|
|
|
99
99
|
|
|
100
100
|
## Why Browser Bridge
|
|
101
101
|
|
|
102
|
-
Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows,
|
|
102
|
+
Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, and the [Codex extension](https://chromewebstore.google.com/detail/codex/hehggadaopoacecdllhhajmbjkdcmajg) is a great option if you use Codex, but both are vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
|
|
103
103
|
|
|
104
104
|
Browser Bridge is optimized for the opposite starting point: **inspect the state that already exists** in a real tab - logged-in sessions, feature flags, seeded storage, SPA state - use structured reads to understand it, test a patch in place, then fix the source. It's open-source, agent-agnostic, and scoped to explicit tab sessions rather than ambient browser control.
|
|
105
105
|
|
|
106
106
|
## Setup
|
|
107
107
|
|
|
108
|
-
1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie)
|
|
108
|
+
1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie) in Chrome or another Chromium-based browser
|
|
109
109
|
2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
|
|
110
|
-
3.
|
|
111
|
-
4.
|
|
112
|
-
5.
|
|
110
|
+
3. Run `bbx install`, or target a specific browser with `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
|
|
111
|
+
4. In the extension side panel, install MCP or CLI (skill) for your agent of choice
|
|
112
|
+
5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
|
|
113
|
+
6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the installed Browser Bridge skill in CLI mode (`/browser-bridge`, `browser-bridge`, or the client-specific skill trigger)
|
|
114
|
+
|
|
115
|
+
MCP mode is self-contained: the server exposes tools, startup instructions, and prompt templates, so a separate CLI skill is not required for MCP guidance.
|
|
113
116
|
|
|
114
117
|
## How it works
|
|
115
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"postinstall": "node packages/native-host/bin/postinstall.js",
|
|
62
62
|
"package:extension": "node scripts/package-extension.mjs",
|
|
63
63
|
"check:extension-zip": "node scripts/check-extension-zip.mjs",
|
|
64
|
-
"release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm pack --dry-run",
|
|
64
|
+
"release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm run check:extension-zip && npm pack --dry-run",
|
|
65
65
|
"prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run coverage:check",
|
|
66
66
|
"status": "node packages/agent-client/src/cli.js status",
|
|
67
67
|
"daemon": "node packages/native-host/bin/bridge-daemon.js",
|
|
@@ -50,8 +50,8 @@ import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
|
50
50
|
import { collectSetupStatus } from './setup-status.js';
|
|
51
51
|
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
52
52
|
|
|
53
|
-
/** @typedef {import('
|
|
54
|
-
/** @typedef {
|
|
53
|
+
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
54
|
+
/** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
|
|
55
55
|
|
|
56
56
|
const REQUEST_SOURCE = 'cli';
|
|
57
57
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
@@ -128,19 +128,25 @@ if (command === 'install-skill') {
|
|
|
128
128
|
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
129
129
|
|
|
130
130
|
if (positional.length === 0) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
let scopeOptions;
|
|
132
|
+
try {
|
|
133
|
+
scopeOptions = parseInstallAgentArgs(rest);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isGlobal = scopeOptions.global !== false;
|
|
140
|
+
const projectPath = isGlobal ? os.homedir() : scopeOptions.projectPath;
|
|
135
141
|
|
|
136
142
|
const setupStatus = await collectSetupStatus({
|
|
137
143
|
global: isGlobal,
|
|
138
144
|
cwd: process.cwd(),
|
|
139
|
-
projectPath
|
|
145
|
+
projectPath,
|
|
140
146
|
...getSetupStatusTestOverrides(),
|
|
141
147
|
});
|
|
142
|
-
/** @type {import('./
|
|
143
|
-
const detected = /** @type {import('./
|
|
148
|
+
/** @type {import('./types.js').SupportedTarget[]} */
|
|
149
|
+
const detected = /** @type {import('./types.js').SupportedTarget[]} */ (
|
|
144
150
|
setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
145
151
|
);
|
|
146
152
|
const installedManagedTargets = new Set(
|
|
@@ -149,7 +155,7 @@ if (command === 'install-skill') {
|
|
|
149
155
|
.map((entry) => entry.key)
|
|
150
156
|
);
|
|
151
157
|
const installedManagedTargetList =
|
|
152
|
-
/** @type {import('./
|
|
158
|
+
/** @type {import('./types.js').SupportedTarget[]} */ ([...installedManagedTargets]);
|
|
153
159
|
|
|
154
160
|
// Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
|
|
155
161
|
const items = SUPPORTED_TARGETS.map((t) => ({
|
|
@@ -167,19 +173,18 @@ if (command === 'install-skill') {
|
|
|
167
173
|
items
|
|
168
174
|
);
|
|
169
175
|
|
|
170
|
-
/** @type {import('./
|
|
176
|
+
/** @type {import('./types.js').SupportedTarget[]} */
|
|
171
177
|
let targets;
|
|
172
178
|
if (selected === null) {
|
|
173
179
|
// Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
|
|
174
180
|
targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
|
|
175
181
|
} else {
|
|
176
|
-
targets = /** @type {import('./
|
|
182
|
+
targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
const projectPath = isGlobal ? os.homedir() : process.cwd();
|
|
180
185
|
if (selected !== null) {
|
|
181
186
|
const deselectedTargets =
|
|
182
|
-
/** @type {import('./
|
|
187
|
+
/** @type {import('./types.js').SupportedTarget[]} */ (
|
|
183
188
|
installedManagedTargetList.filter((target) => !targets.includes(target))
|
|
184
189
|
);
|
|
185
190
|
const removableTargets = await findInstalledManagedTargets({
|
|
@@ -226,22 +231,34 @@ if (command === 'install-skill') {
|
|
|
226
231
|
}
|
|
227
232
|
|
|
228
233
|
if (command === 'install-mcp') {
|
|
229
|
-
const argsLeft = [...rest];
|
|
230
234
|
let isGlobal = true;
|
|
235
|
+
/** @type {string[]} */
|
|
236
|
+
const positionals = [];
|
|
231
237
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
for (const arg of rest) {
|
|
239
|
+
if (arg === '--local') {
|
|
240
|
+
isGlobal = false;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (arg === '--global') {
|
|
244
|
+
isGlobal = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg.startsWith('--')) {
|
|
248
|
+
process.stderr.write(`Unknown install-mcp option "${arg}".\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
positionals.push(arg);
|
|
236
252
|
}
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
253
|
+
|
|
254
|
+
if (positionals.length > 1) {
|
|
255
|
+
process.stderr.write(`Unexpected extra argument "${positionals[1]}".\n`);
|
|
256
|
+
process.exit(1);
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
const clientArg =
|
|
259
|
+
const clientArg = positionals[0];
|
|
243
260
|
|
|
244
|
-
/** @type {import('./
|
|
261
|
+
/** @type {import('./types.js').McpClientName[]} */
|
|
245
262
|
let clients;
|
|
246
263
|
|
|
247
264
|
if (!clientArg) {
|
|
@@ -252,14 +269,14 @@ if (command === 'install-mcp') {
|
|
|
252
269
|
projectPath: process.cwd(),
|
|
253
270
|
...getSetupStatusTestOverrides(),
|
|
254
271
|
});
|
|
255
|
-
const detected = /** @type {import('./
|
|
272
|
+
const detected = /** @type {import('./types.js').McpClientName[]} */ (
|
|
256
273
|
setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
257
274
|
);
|
|
258
275
|
const configuredClients = new Set(
|
|
259
276
|
setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
|
|
260
277
|
);
|
|
261
278
|
const configuredClientList =
|
|
262
|
-
/** @type {import('./
|
|
279
|
+
/** @type {import('./types.js').McpClientName[]} */ ([...configuredClients]);
|
|
263
280
|
const items = MCP_CLIENT_NAMES.map((c) => ({
|
|
264
281
|
value: c,
|
|
265
282
|
label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
|
|
@@ -284,12 +301,12 @@ if (command === 'install-mcp') {
|
|
|
284
301
|
? detected
|
|
285
302
|
: [...MCP_CLIENT_NAMES];
|
|
286
303
|
} else {
|
|
287
|
-
clients = /** @type {import('./
|
|
304
|
+
clients = /** @type {import('./types.js').McpClientName[]} */ (selected);
|
|
288
305
|
}
|
|
289
306
|
|
|
290
307
|
if (selected !== null) {
|
|
291
308
|
const deselectedClients =
|
|
292
|
-
/** @type {import('./
|
|
309
|
+
/** @type {import('./types.js').McpClientName[]} */ (
|
|
293
310
|
configuredClientList.filter((clientName) => !clients.includes(clientName))
|
|
294
311
|
);
|
|
295
312
|
const removableClients = await findConfiguredMcpClients({
|
|
@@ -470,15 +487,20 @@ async function main() {
|
|
|
470
487
|
}
|
|
471
488
|
|
|
472
489
|
if (command === 'batch') {
|
|
473
|
-
await ensureClientConnection();
|
|
474
490
|
const input = rest[0];
|
|
475
491
|
if (!input) {
|
|
476
492
|
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
477
493
|
}
|
|
478
|
-
|
|
494
|
+
let calls;
|
|
495
|
+
try {
|
|
496
|
+
calls = JSON.parse(input);
|
|
497
|
+
} catch {
|
|
498
|
+
throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
|
|
499
|
+
}
|
|
479
500
|
if (!Array.isArray(calls)) {
|
|
480
501
|
throw new Error('Batch input must be a JSON array.');
|
|
481
502
|
}
|
|
503
|
+
await ensureClientConnection();
|
|
482
504
|
const results = await Promise.all(
|
|
483
505
|
calls.map(async (call) => {
|
|
484
506
|
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
@@ -847,10 +869,13 @@ async function uninstallBrowserBridge() {
|
|
|
847
869
|
*/
|
|
848
870
|
async function parseCallCommand(args) {
|
|
849
871
|
const parsed = extractTabFlag(args);
|
|
850
|
-
const [first, second] = parsed.rest;
|
|
872
|
+
const [first, second, ...extra] = parsed.rest;
|
|
851
873
|
if (!first) {
|
|
852
874
|
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
853
875
|
}
|
|
876
|
+
if (extra.length > 0) {
|
|
877
|
+
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
878
|
+
}
|
|
854
879
|
|
|
855
880
|
if (first.includes('.')) {
|
|
856
881
|
const method = /** @type {BridgeMethod} */ (first);
|
|
@@ -15,53 +15,16 @@ import {
|
|
|
15
15
|
getBridgeTransport,
|
|
16
16
|
getSocketPath,
|
|
17
17
|
} from '../../native-host/src/config.js';
|
|
18
|
+
import { normalizeBridgeAuthToken, readBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
18
19
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
19
20
|
|
|
20
|
-
/** @typedef {import('
|
|
21
|
-
|
|
22
|
-
/** @typedef {import('
|
|
23
|
-
/** @typedef {import('
|
|
24
|
-
/** @typedef {import('
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
* extensionConnected?: boolean,
|
|
28
|
-
* supported_versions?: string[],
|
|
29
|
-
* daemon_supported_versions?: string[],
|
|
30
|
-
* deprecated_since?: string,
|
|
31
|
-
* migration_hint?: string
|
|
32
|
-
* }} ProtocolHealthResult
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @typedef {{
|
|
37
|
-
* type: 'registered',
|
|
38
|
-
* role: 'agent' | 'extension',
|
|
39
|
-
* clientId?: string
|
|
40
|
-
* } | {
|
|
41
|
-
* type: 'agent.response',
|
|
42
|
-
* response: BridgeResponse
|
|
43
|
-
* }} ClientMessage
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @typedef {{
|
|
48
|
-
* resolve: (value: any) => void,
|
|
49
|
-
* reject: (error: Error) => void,
|
|
50
|
-
* timeoutId: NodeJS.Timeout
|
|
51
|
-
* }} PendingRequest
|
|
52
|
-
*/
|
|
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
|
-
*/
|
|
21
|
+
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
22
|
+
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
23
|
+
/** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
|
|
24
|
+
/** @typedef {import('./types.js').BridgeClientOptions} BridgeClientOptions */
|
|
25
|
+
/** @typedef {import('./types.js').ClientMessage} ClientMessage */
|
|
26
|
+
/** @typedef {import('./types.js').PendingRequest} PendingRequest */
|
|
27
|
+
/** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
|
|
65
28
|
|
|
66
29
|
/**
|
|
67
30
|
* @param {string} left
|
|
@@ -94,6 +57,22 @@ function createTimeoutError(method, timeoutMs) {
|
|
|
94
57
|
return error;
|
|
95
58
|
}
|
|
96
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @param {net.Socket} socket
|
|
62
|
+
* @param {string} line
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async function writeSocketLine(socket, line) {
|
|
66
|
+
if (!socket.write(line)) {
|
|
67
|
+
await Promise.race([
|
|
68
|
+
once(socket, 'drain'),
|
|
69
|
+
once(socket, 'close').then(() => {
|
|
70
|
+
throw new Error('Bridge socket closed while writing.');
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
97
76
|
export class BridgeClient extends EventEmitter {
|
|
98
77
|
/**
|
|
99
78
|
* @param {BridgeClientOptions} [options={}]
|
|
@@ -106,6 +85,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
106
85
|
autoReconnect = false,
|
|
107
86
|
restartDaemonOnVersionMismatch = true,
|
|
108
87
|
restartDaemonFn = restartBridgeDaemon,
|
|
88
|
+
authToken = undefined,
|
|
109
89
|
} = {}) {
|
|
110
90
|
super();
|
|
111
91
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -116,6 +96,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
116
96
|
this.autoReconnect = autoReconnect;
|
|
117
97
|
this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
|
|
118
98
|
this.restartDaemonFn = restartDaemonFn;
|
|
99
|
+
this.authToken = authToken;
|
|
119
100
|
this.socket = null;
|
|
120
101
|
this.connected = false;
|
|
121
102
|
this.protocolCompatibility = null;
|
|
@@ -149,6 +130,18 @@ export class BridgeClient extends EventEmitter {
|
|
|
149
130
|
throw error;
|
|
150
131
|
}
|
|
151
132
|
|
|
133
|
+
const registrationPromise = new Promise((resolve, reject) => {
|
|
134
|
+
const timeoutId = setTimeout(() => {
|
|
135
|
+
this.waiting.delete('registered');
|
|
136
|
+
reject(createTimeoutError('register', this.defaultTimeoutMs));
|
|
137
|
+
}, this.defaultTimeoutMs);
|
|
138
|
+
this.waiting.set('registered', {
|
|
139
|
+
resolve,
|
|
140
|
+
reject,
|
|
141
|
+
timeoutId,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
152
145
|
parseJsonLines(socket, (raw) => {
|
|
153
146
|
const message = /** @type {ClientMessage} */ (raw);
|
|
154
147
|
if (message.type === 'registered') {
|
|
@@ -162,6 +155,16 @@ export class BridgeClient extends EventEmitter {
|
|
|
162
155
|
return;
|
|
163
156
|
}
|
|
164
157
|
|
|
158
|
+
if (message.type === 'registration_failed') {
|
|
159
|
+
const pending = this.waiting.get('registered');
|
|
160
|
+
if (pending) {
|
|
161
|
+
this.waiting.delete('registered');
|
|
162
|
+
clearTimeout(pending.timeoutId);
|
|
163
|
+
pending.reject(new Error(message.error?.message || 'Bridge daemon registration failed.'));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
165
168
|
if (message.type === 'agent.response') {
|
|
166
169
|
const pending = this.waiting.get(message.response.id);
|
|
167
170
|
if (pending) {
|
|
@@ -186,20 +189,31 @@ export class BridgeClient extends EventEmitter {
|
|
|
186
189
|
// 'close' fires after 'error'; reconnect is triggered there.
|
|
187
190
|
});
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const authToken =
|
|
193
|
+
this.authToken === undefined
|
|
194
|
+
? this.transport.type === 'tcp'
|
|
195
|
+
? await readBridgeAuthToken()
|
|
196
|
+
: null
|
|
197
|
+
: normalizeBridgeAuthToken(this.authToken);
|
|
198
|
+
try {
|
|
199
|
+
await writeSocketLine(
|
|
200
|
+
socket,
|
|
201
|
+
`${JSON.stringify({
|
|
202
|
+
type: 'register',
|
|
203
|
+
role: 'agent',
|
|
204
|
+
clientId: this.clientId,
|
|
205
|
+
...(authToken ? { authToken } : {}),
|
|
206
|
+
})}\n`
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const pending = this.waiting.get('registered');
|
|
210
|
+
if (pending) {
|
|
211
|
+
clearTimeout(pending.timeoutId);
|
|
194
212
|
this.waiting.delete('registered');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
reject,
|
|
200
|
-
timeoutId,
|
|
201
|
-
});
|
|
202
|
-
});
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
await registrationPromise;
|
|
203
217
|
|
|
204
218
|
this.protocolCompatibility = null;
|
|
205
219
|
this.protocolWarning = null;
|
|
@@ -282,13 +296,15 @@ export class BridgeClient extends EventEmitter {
|
|
|
282
296
|
});
|
|
283
297
|
});
|
|
284
298
|
|
|
285
|
-
|
|
286
|
-
await
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
299
|
+
try {
|
|
300
|
+
await writeSocketLine(this.socket, `${JSON.stringify({ type: 'agent.request', request })}\n`);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const pending = this.waiting.get(request.id);
|
|
303
|
+
if (pending) {
|
|
304
|
+
clearTimeout(pending.timeoutId);
|
|
305
|
+
this.waiting.delete(request.id);
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
292
308
|
}
|
|
293
309
|
const response = /** @type {BridgeResponse} */ (await responsePromise);
|
|
294
310
|
return this.attachProtocolWarning(response);
|
|
@@ -3,18 +3,8 @@
|
|
|
3
3
|
import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
|
|
4
4
|
import { parseCommaList, parseIntArg, parsePropertyAssignments } from './cli-helpers.js';
|
|
5
5
|
|
|
6
|
-
/** @typedef {import('
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {{
|
|
10
|
-
* method: BridgeMethod,
|
|
11
|
-
* resolve?: boolean,
|
|
12
|
-
* printMethod?: string,
|
|
13
|
-
* usage: string,
|
|
14
|
-
* description: string,
|
|
15
|
-
* build: (r: string[], ref?: string) => Record<string, unknown>
|
|
16
|
-
* }} ShortcutCommand
|
|
17
|
-
*/
|
|
6
|
+
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
7
|
+
/** @typedef {import('./types.js').ShortcutCommand} ShortcutCommand */
|
|
18
8
|
|
|
19
9
|
/**
|
|
20
10
|
* @param {BridgeMethod} method
|
|
@@ -247,11 +237,10 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
247
237
|
{
|
|
248
238
|
title: 'Setup',
|
|
249
239
|
lines: [
|
|
250
|
-
'bbx install [--browser chrome|edge|brave|chromium] [
|
|
240
|
+
'bbx install [extension-id] [--browser chrome|edge|brave|chromium|arc] [--all] Install native messaging manifest',
|
|
251
241
|
'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
|
|
252
|
-
'bbx install [--all] [--browser <name>] [extension-id] Install native host manifest (--all for all supported browsers)',
|
|
253
242
|
'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
|
|
254
|
-
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf',
|
|
243
|
+
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
|
|
255
244
|
'bbx status Check bridge connection',
|
|
256
245
|
'bbx doctor Diagnose install, daemon, extension, and access readiness',
|
|
257
246
|
'bbx restart Restart the local Browser Bridge daemon',
|
|
@@ -4,9 +4,9 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
-
/** @typedef {import('./
|
|
8
|
-
/** @typedef {import('./
|
|
9
|
-
/** @typedef {()
|
|
7
|
+
/** @typedef {import('./types.js').McpClientName} McpClientName */
|
|
8
|
+
/** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
|
|
9
|
+
/** @typedef {import('./types.js').Detector} Detector */
|
|
10
10
|
|
|
11
11
|
const home = os.homedir();
|
|
12
12
|
const platform = process.platform;
|
|
@@ -41,9 +41,7 @@ const copilotBrowserBridgeNote = [
|
|
|
41
41
|
'',
|
|
42
42
|
].join('\n');
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} SupportedTarget
|
|
46
|
-
*/
|
|
44
|
+
/** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
|
|
47
45
|
|
|
48
46
|
/** @type {SupportedTarget[]} */
|
|
49
47
|
export const SUPPORTED_TARGETS = [...supportedTargets];
|
|
@@ -68,9 +66,7 @@ export function isSupportedTarget(value) {
|
|
|
68
66
|
return supportedTargets.includes(/** @type {SupportedTarget} */ (value));
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
/**
|
|
72
|
-
* @typedef {{targets: SupportedTarget[], projectPath: string, global: boolean}} InstallAgentOptions
|
|
73
|
-
*/
|
|
69
|
+
/** @typedef {import('./types.js').InstallAgentOptions} InstallAgentOptions */
|
|
74
70
|
|
|
75
71
|
/**
|
|
76
72
|
* @param {string[]} args
|
|
@@ -202,7 +198,7 @@ async function rollbackInstalledSkillDirs(attempted) {
|
|
|
202
198
|
/**
|
|
203
199
|
* Write MCP config for the given clients.
|
|
204
200
|
*
|
|
205
|
-
* @param {import('./
|
|
201
|
+
* @param {import('./types.js').McpClientName[]} clients
|
|
206
202
|
* @param {{
|
|
207
203
|
* global: boolean,
|
|
208
204
|
* projectPath: string,
|
|
@@ -5,9 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} McpClientName
|
|
10
|
-
*/
|
|
8
|
+
/** @typedef {import('./types.js').McpClientName} McpClientName */
|
|
11
9
|
|
|
12
10
|
/** @type {McpClientName[]} */
|
|
13
11
|
export const MCP_CLIENT_NAMES = [
|
|
@@ -525,8 +523,13 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
|
|
|
525
523
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
526
524
|
existing = parsed;
|
|
527
525
|
}
|
|
528
|
-
} catch {
|
|
529
|
-
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (!isMissingFileError(error)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Cannot update ${configPath}: existing MCP config is not valid JSON. Fix or remove it first.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
// File missing - start fresh.
|
|
530
533
|
}
|
|
531
534
|
|
|
532
535
|
const shape = getMcpConfigShape(clientName);
|
|
@@ -663,3 +666,15 @@ export function parseInstalledMcpConfig(clientName, raw) {
|
|
|
663
666
|
return { configured: false };
|
|
664
667
|
}
|
|
665
668
|
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* @param {unknown} error
|
|
672
|
+
* @returns {boolean}
|
|
673
|
+
*/
|
|
674
|
+
function isMissingFileError(error) {
|
|
675
|
+
return Boolean(
|
|
676
|
+
error &&
|
|
677
|
+
typeof error === 'object' &&
|
|
678
|
+
/** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
@@ -12,39 +12,14 @@ import { resolveDefaultExtensionId } from '../../native-host/src/install-manifes
|
|
|
12
12
|
import { methodNeedsTab } from './cli-helpers.js';
|
|
13
13
|
import { BridgeClient } from './client.js';
|
|
14
14
|
|
|
15
|
-
/** @typedef {import('
|
|
16
|
-
/** @typedef {import('
|
|
17
|
-
/** @typedef {import('
|
|
18
|
-
/** @typedef {import('
|
|
15
|
+
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
16
|
+
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
17
|
+
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
18
|
+
/** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
|
|
19
19
|
/** @typedef {import('../../native-host/src/config.js').SupportedBrowser} SupportedBrowser */
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
* browser: string,
|
|
24
|
-
* manifestPath: string,
|
|
25
|
-
* installed: boolean
|
|
26
|
-
* }} BrowserManifestStatus
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @typedef {{
|
|
31
|
-
* manifestInstalled: boolean,
|
|
32
|
-
* manifestPath: string,
|
|
33
|
-
* allowedOrigins: string[],
|
|
34
|
-
* defaultExtensionId: string | null,
|
|
35
|
-
* defaultExtensionIdSource: string,
|
|
36
|
-
* daemonReachable: boolean,
|
|
37
|
-
* extensionConnected: boolean,
|
|
38
|
-
* accessEnabled: boolean,
|
|
39
|
-
* enabledWindowId: number | null,
|
|
40
|
-
* routeTabId: number | null,
|
|
41
|
-
* routeReady: boolean,
|
|
42
|
-
* routeReason: string,
|
|
43
|
-
* issues: string[],
|
|
44
|
-
* nextSteps: string[],
|
|
45
|
-
* browserManifests: BrowserManifestStatus[]
|
|
46
|
-
* }} DoctorReport
|
|
47
|
-
*/
|
|
20
|
+
/** @typedef {import('./types.js').BrowserManifestStatus} BrowserManifestStatus */
|
|
21
|
+
/** @typedef {import('./types.js').DoctorReport} DoctorReport */
|
|
22
|
+
/** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
|
|
48
23
|
|
|
49
24
|
/**
|
|
50
25
|
* @param {BridgeClient} client
|
|
@@ -178,15 +153,6 @@ export async function checkBrowserManifests() {
|
|
|
178
153
|
);
|
|
179
154
|
}
|
|
180
155
|
|
|
181
|
-
/**
|
|
182
|
-
* @typedef {{
|
|
183
|
-
* loadManifest?: () => Promise<{allowed_origins?: string[]} | null>,
|
|
184
|
-
* manifestPath?: string,
|
|
185
|
-
* defaultExtensionIdInfo?: { extensionId: string | null, source: string },
|
|
186
|
-
* bridgeClientRunner?: <T>(callback: (client: BridgeClient) => Promise<T>) => Promise<T>
|
|
187
|
-
* }} DoctorReportOptions
|
|
188
|
-
*/
|
|
189
|
-
|
|
190
156
|
/**
|
|
191
157
|
* @param {DoctorReportOptions} [options={}]
|
|
192
158
|
* @returns {Promise<DoctorReport>}
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
SUPPORTED_TARGETS,
|
|
22
22
|
} from './install.js';
|
|
23
23
|
|
|
24
|
-
/** @typedef {import('./
|
|
25
|
-
/** @typedef {import('./
|
|
24
|
+
/** @typedef {import('./types.js').McpClientName} McpClientName */
|
|
25
|
+
/** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
|
|
26
26
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
27
27
|
/** @typedef {import('../../protocol/src/types.js').McpClientStatus} McpClientStatus */
|
|
28
28
|
/** @typedef {import('../../protocol/src/types.js').SkillTargetStatus} SkillTargetStatus */
|
|
@@ -52,17 +52,7 @@ const SKILL_TARGET_LABELS = {
|
|
|
52
52
|
agents: 'Generic agents',
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* @typedef {{
|
|
57
|
-
* global?: boolean,
|
|
58
|
-
* cwd?: string,
|
|
59
|
-
* projectPath?: string,
|
|
60
|
-
* mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
|
|
61
|
-
* skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
|
|
62
|
-
* access?: (targetPath: string) => Promise<void>,
|
|
63
|
-
* readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
|
|
64
|
-
* }} SetupStatusOptions
|
|
65
|
-
*/
|
|
55
|
+
/** @typedef {import('./types.js').SetupStatusOptions} SetupStatusOptions */
|
|
66
56
|
|
|
67
57
|
/**
|
|
68
58
|
* Return Browser Bridge MCP and skill installation status for supported clients.
|