@browserbridge/bbx 1.9.0 → 1.11.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 +1 -1
- package/package.json +8 -10
- package/packages/agent-client/src/cli-args.js +35 -6
- package/packages/agent-client/src/cli-batch.js +10 -5
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-proxy-remote.js +7 -4
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +35 -15
- package/packages/agent-client/src/command-registry.js +7 -3
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/npm-self-update.js +9 -2
- package/packages/agent-client/src/runtime.js +14 -5
- package/packages/agent-client/src/types.ts +2 -0
- package/packages/mcp-server/src/bin.js +1 -1
- package/packages/mcp-server/src/guidance.js +43 -6
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-page.js +162 -9
- package/packages/mcp-server/src/handlers-utils.js +145 -169
- package/packages/mcp-server/src/handlers.js +17 -5
- package/packages/mcp-server/src/server.js +255 -91
- package/packages/mcp-server/src/toolset.js +73 -0
- package/packages/native-host/src/daemon.js +24 -107
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +14 -28
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/protocol.js +10 -0
- package/packages/protocol/src/registry.js +83 -0
- package/packages/protocol/src/types.ts +3 -0
- package/skills/browser-bridge/SKILL.md +4 -0
- package/skills/browser-bridge/references/protocol.md +11 -5
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
|
|
|
116
116
|
|
|
117
117
|
On Ubuntu, Chromium is commonly installed as a strict snap, and Flatpak Chromium is similarly sandboxed. If native messaging stays disconnected there, use a non-sandboxed Chromium-based browser such as Google Chrome, Brave, or Edge.
|
|
118
118
|
|
|
119
|
-
MCP mode is self-contained: the server exposes tools and startup instructions
|
|
119
|
+
MCP mode is self-contained: the dual-era stdio server supports legacy MCP clients and modern stateless MCP revision `2026-07-28`, and exposes tools and matching startup instructions without requiring a separate CLI skill.
|
|
120
120
|
|
|
121
121
|
## How it works
|
|
122
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -73,26 +73,24 @@
|
|
|
73
73
|
"install-manifest": "node packages/native-host/bin/install-manifest.js"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@modelcontextprotocol/
|
|
76
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
77
77
|
"@mozilla/readability": "^0.6.0",
|
|
78
78
|
"linkedom": "^0.18.13",
|
|
79
79
|
"zod": "^4.4.3"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@
|
|
83
|
-
"@types/
|
|
82
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
83
|
+
"@types/chrome": "^0.2.5",
|
|
84
|
+
"@types/node": "^26.2.0",
|
|
84
85
|
"ajv": "^8.20.0",
|
|
85
86
|
"ajv-formats": "^3.0.1",
|
|
86
87
|
"c8": "^12.0.0",
|
|
87
88
|
"har-schema": "^2.0.0",
|
|
88
|
-
"oxfmt": "^0.
|
|
89
|
-
"oxlint": "^1.
|
|
90
|
-
"tsx": "^4.23.
|
|
89
|
+
"oxfmt": "^0.63.0",
|
|
90
|
+
"oxlint": "^1.78.0",
|
|
91
|
+
"tsx": "^4.23.12",
|
|
91
92
|
"typescript": "^7.0.2"
|
|
92
93
|
},
|
|
93
|
-
"overrides": {
|
|
94
|
-
"@hono/node-server": "2.0.11"
|
|
95
|
-
},
|
|
96
94
|
"engines": {
|
|
97
95
|
"node": ">=20"
|
|
98
96
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { METHODS } from '../../protocol/src/index.js';
|
|
3
|
+
import { applyMethodBudgetPreset, isBudgetPresetName, METHODS } from '../../protocol/src/index.js';
|
|
4
4
|
import { methodNeedsTab, parseIntArg, parseJsonObject } from './cli-helpers.js';
|
|
5
5
|
|
|
6
6
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
7
|
+
/** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
|
|
8
|
+
|
|
9
|
+
const CALL_USAGE = 'Usage: call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson]';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
@@ -119,18 +122,44 @@ export function extractHarFlags(args) {
|
|
|
119
122
|
return { limit, urlPattern, delivery: rawDelivery, rest };
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Extract the --preset flag from anywhere in the argument list. Preset names
|
|
127
|
+
* are validated against the shared budget presets; explicit per-call params
|
|
128
|
+
* always override preset defaults.
|
|
129
|
+
*
|
|
130
|
+
* @param {string[]} args
|
|
131
|
+
* @returns {{ preset: BudgetPresetName | null, rest: string[] }}
|
|
132
|
+
*/
|
|
133
|
+
export function extractPresetFlag(args) {
|
|
134
|
+
const rest = [...args];
|
|
135
|
+
const index = rest.indexOf('--preset');
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
return { preset: null, rest };
|
|
138
|
+
}
|
|
139
|
+
const value = rest[index + 1];
|
|
140
|
+
if (!value || value.startsWith('--')) {
|
|
141
|
+
throw new Error('--preset requires a value: quick, normal, or deep.');
|
|
142
|
+
}
|
|
143
|
+
if (!isBudgetPresetName(value)) {
|
|
144
|
+
throw new Error(`Unknown preset "${value}". Valid presets: quick, normal, deep.`);
|
|
145
|
+
}
|
|
146
|
+
rest.splice(index, 2);
|
|
147
|
+
return { preset: value, rest };
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
/**
|
|
123
151
|
* @param {string[]} args
|
|
124
152
|
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
125
153
|
*/
|
|
126
154
|
export async function parseCallCommand(args) {
|
|
127
|
-
const
|
|
155
|
+
const presetParsed = extractPresetFlag(args);
|
|
156
|
+
const parsed = extractTabFlag(presetParsed.rest);
|
|
128
157
|
const [first, second, ...extra] = parsed.rest;
|
|
129
158
|
if (!first) {
|
|
130
|
-
throw new Error(
|
|
159
|
+
throw new Error(CALL_USAGE);
|
|
131
160
|
}
|
|
132
161
|
if (extra.length > 0) {
|
|
133
|
-
throw new Error(
|
|
162
|
+
throw new Error(CALL_USAGE);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
if (first.includes('.')) {
|
|
@@ -146,11 +175,11 @@ export async function parseCallCommand(args) {
|
|
|
146
175
|
return {
|
|
147
176
|
method,
|
|
148
177
|
tabId: methodNeedsTab(method) ? parsed.tabId : null,
|
|
149
|
-
params: parseJsonObject(rawParams),
|
|
178
|
+
params: applyMethodBudgetPreset(method, parseJsonObject(rawParams), presetParsed.preset),
|
|
150
179
|
};
|
|
151
180
|
}
|
|
152
181
|
|
|
153
|
-
throw new Error(
|
|
182
|
+
throw new Error(CALL_USAGE);
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
applyMethodBudgetPreset,
|
|
4
5
|
getProtocolVersion,
|
|
5
6
|
isBatchSafeBridgeCall,
|
|
6
7
|
MAX_BATCH_CALLS,
|
|
@@ -14,6 +15,7 @@ import { requestBridge } from './runtime.js';
|
|
|
14
15
|
|
|
15
16
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
16
17
|
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
18
|
+
/** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @typedef {{
|
|
@@ -72,9 +74,10 @@ function invalidBatchItem(method, message) {
|
|
|
72
74
|
* @param {import('./client.js').BridgeClient} client
|
|
73
75
|
* @param {string | undefined} input - Raw JSON array argument
|
|
74
76
|
* @param {BridgeRequestSource} source - Request source tag (e.g. 'cli')
|
|
77
|
+
* @param {{ preset?: BudgetPresetName | null }} [options] - Optional budget preset applied to every call
|
|
75
78
|
* @returns {Promise<BatchResultItem[]>}
|
|
76
79
|
*/
|
|
77
|
-
export async function runBatchCalls(client, input, source) {
|
|
80
|
+
export async function runBatchCalls(client, input, source, options = {}) {
|
|
78
81
|
if (!input) {
|
|
79
82
|
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
80
83
|
}
|
|
@@ -90,7 +93,7 @@ export async function runBatchCalls(client, input, source) {
|
|
|
90
93
|
if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
|
|
91
94
|
throw new Error(`Batch input must contain between 1 and ${MAX_BATCH_CALLS} calls.`);
|
|
92
95
|
}
|
|
93
|
-
const prepared = calls.map((call) => prepareBatchCall(call));
|
|
96
|
+
const prepared = calls.map((call) => prepareBatchCall(call, options.preset ?? null));
|
|
94
97
|
if (prepared.some((item) => 'error' in item)) {
|
|
95
98
|
return prepared.map((item) =>
|
|
96
99
|
'error' in item
|
|
@@ -130,9 +133,10 @@ export async function runBatchCalls(client, input, source) {
|
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
135
|
* @param {unknown} call
|
|
136
|
+
* @param {BudgetPresetName | null} preset
|
|
133
137
|
* @returns {{ value: PreparedBatchCall } | { error: InvalidBatchItem }}
|
|
134
138
|
*/
|
|
135
|
-
function prepareBatchCall(call) {
|
|
139
|
+
function prepareBatchCall(call, preset) {
|
|
136
140
|
if (!call || typeof call !== 'object' || Array.isArray(call)) {
|
|
137
141
|
return { error: invalidBatchItem('', 'Each batch call needs a method.') };
|
|
138
142
|
}
|
|
@@ -162,7 +166,8 @@ function prepareBatchCall(call) {
|
|
|
162
166
|
}
|
|
163
167
|
const params =
|
|
164
168
|
batchCall.params === undefined ? {} : /** @type {Record<string, unknown>} */ (batchCall.params);
|
|
165
|
-
|
|
169
|
+
const mergedParams = applyMethodBudgetPreset(method, params, preset);
|
|
170
|
+
if (!isBatchSafeBridgeCall(method, mergedParams)) {
|
|
166
171
|
return {
|
|
167
172
|
error: invalidBatchItem(
|
|
168
173
|
method,
|
|
@@ -177,7 +182,7 @@ function prepareBatchCall(call) {
|
|
|
177
182
|
batchCall.tabId > 0
|
|
178
183
|
? batchCall.tabId
|
|
179
184
|
: null;
|
|
180
|
-
return { value: { method, params, tabId } };
|
|
185
|
+
return { value: { method, params: mergedParams, tabId } };
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
/**
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import readline from 'node:readline';
|
|
4
4
|
import { bridgeMethodNeedsTab } from '../../protocol/src/index.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} value
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isElementRef(value) {
|
|
11
|
+
return value.startsWith('el_');
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Strip ANSI escape sequences from a string to prevent terminal injection
|
|
8
16
|
* from malicious page content (e.g. DOM text, console output, eval results).
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
readProxyConfig,
|
|
12
12
|
} from '../../native-host/src/config.js';
|
|
13
13
|
import { pingExistingDaemon } from '../../native-host/src/daemon.js';
|
|
14
|
-
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
14
|
+
import { restartBridgeDaemon, stopBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
15
15
|
import { atomicWriteFile } from './atomic-write.js';
|
|
16
16
|
import { parseIntArg } from './cli-helpers.js';
|
|
17
17
|
import {
|
|
@@ -44,6 +44,8 @@ export async function handleProxyCommand(args) {
|
|
|
44
44
|
randomUUID
|
|
45
45
|
);
|
|
46
46
|
assertProxyBindSafety(existing, options, bindHost);
|
|
47
|
+
// Stop against the current config before changing its port or auth token.
|
|
48
|
+
const stopResult = await stopBridgeDaemon();
|
|
47
49
|
await atomicWriteFile(getBridgeAuthTokenPath(), `${token}\n`, {
|
|
48
50
|
encoding: 'utf8',
|
|
49
51
|
mode: 0o600,
|
|
@@ -75,7 +77,7 @@ export async function handleProxyCommand(args) {
|
|
|
75
77
|
'',
|
|
76
78
|
`Token: ${token}${tokenNote}`,
|
|
77
79
|
`Config: ${configPath}`,
|
|
78
|
-
`Daemon: ${
|
|
80
|
+
`Daemon: ${stopResult.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
79
81
|
];
|
|
80
82
|
if (tokenSource !== 'existing') {
|
|
81
83
|
if (isLoopbackHost(bindHost)) {
|
|
@@ -106,10 +108,11 @@ export async function handleProxyCommand(args) {
|
|
|
106
108
|
|
|
107
109
|
if (subcommand === 'disable') {
|
|
108
110
|
const configPath = getProxyConfigPath();
|
|
111
|
+
const stopResult = await stopBridgeDaemon();
|
|
109
112
|
await fs.promises.rm(configPath, { force: true });
|
|
110
|
-
|
|
113
|
+
await restartBridgeDaemon();
|
|
111
114
|
process.stdout.write(
|
|
112
|
-
`Browser Bridge proxy disabled. Daemon ${
|
|
115
|
+
`Browser Bridge proxy disabled. Daemon ${stopResult.previouslyRunning ? 'restarted' : 'started'}.\n`
|
|
113
116
|
);
|
|
114
117
|
return;
|
|
115
118
|
}
|
|
@@ -150,7 +150,8 @@ export async function runInstallMcpCommand(args) {
|
|
|
150
150
|
/** @type {string[]} */
|
|
151
151
|
const positionals = [];
|
|
152
152
|
|
|
153
|
-
for (
|
|
153
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
154
|
+
const arg = args[index];
|
|
154
155
|
if (arg === '--local') {
|
|
155
156
|
isGlobal = false;
|
|
156
157
|
continue;
|
|
@@ -276,6 +277,7 @@ export async function runInstallMcpCommand(args) {
|
|
|
276
277
|
projectPath: process.cwd(),
|
|
277
278
|
stdout: process.stdout,
|
|
278
279
|
});
|
|
280
|
+
|
|
279
281
|
process.exit(0);
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -10,7 +10,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
|
|
11
11
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
12
12
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createRuntimeContext,
|
|
15
|
+
describeBridgeMethods,
|
|
16
|
+
isBridgeMethod,
|
|
17
|
+
isHarDocument,
|
|
18
|
+
METHODS,
|
|
19
|
+
} from '../../protocol/src/index.js';
|
|
14
20
|
import {
|
|
15
21
|
restartRegisteredMcpProcesses,
|
|
16
22
|
tryStartMcpProcessControl,
|
|
@@ -19,6 +25,7 @@ import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
|
19
25
|
import {
|
|
20
26
|
extractRemoteFlag,
|
|
21
27
|
extractHarFlags,
|
|
28
|
+
extractPresetFlag,
|
|
22
29
|
extractScreenshotFlags,
|
|
23
30
|
extractTabFlag,
|
|
24
31
|
parseCallCommand,
|
|
@@ -26,7 +33,7 @@ import {
|
|
|
26
33
|
readStdin,
|
|
27
34
|
} from './cli-args.js';
|
|
28
35
|
import { runBatchCalls } from './cli-batch.js';
|
|
29
|
-
import { parseIntArg } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, parseIntArg } from './cli-helpers.js';
|
|
30
37
|
import { printCallResponse, printJson, printSummary, printUsage } from './cli-output.js';
|
|
31
38
|
import { handleProxyCommand, handleRemoteCommand } from './cli-proxy-remote.js';
|
|
32
39
|
import {
|
|
@@ -64,6 +71,7 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
64
71
|
'--version',
|
|
65
72
|
'-v',
|
|
66
73
|
'skill',
|
|
74
|
+
'protocol',
|
|
67
75
|
'install',
|
|
68
76
|
'uninstall',
|
|
69
77
|
'install-skill',
|
|
@@ -111,6 +119,22 @@ if (command === 'skill') {
|
|
|
111
119
|
process.exit(0);
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (command === 'protocol') {
|
|
123
|
+
const [subcommand, target, ...extra] = rest;
|
|
124
|
+
if (subcommand !== 'describe' || extra.length > 0) {
|
|
125
|
+
process.stderr.write('Usage: bbx protocol describe [method|group]\n');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
await runLocalCommand(async () => {
|
|
129
|
+
printJson(
|
|
130
|
+
target
|
|
131
|
+
? describeBridgeMethods(isBridgeMethod(target) ? { method: target } : { group: target })
|
|
132
|
+
: describeBridgeMethods()
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
process.exit();
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
if (command === 'install') {
|
|
115
139
|
const { execFileSync } = await import('node:child_process');
|
|
116
140
|
const installScript = path.resolve(
|
|
@@ -142,7 +166,7 @@ if (command === 'install-mcp') {
|
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
if (command === 'mcp') {
|
|
145
|
-
const [subcommand, clientName] = rest;
|
|
169
|
+
const [subcommand, clientName, ...extra] = rest;
|
|
146
170
|
if (subcommand === 'serve') {
|
|
147
171
|
const control = await tryStartMcpProcessControl();
|
|
148
172
|
try {
|
|
@@ -154,7 +178,7 @@ if (command === 'mcp') {
|
|
|
154
178
|
await new Promise(() => {});
|
|
155
179
|
}
|
|
156
180
|
if (subcommand === 'config') {
|
|
157
|
-
if (!clientName || !isMcpClientName(clientName)) {
|
|
181
|
+
if (!clientName || !isMcpClientName(clientName) || extra.length > 0) {
|
|
158
182
|
process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
|
|
159
183
|
process.exit(1);
|
|
160
184
|
}
|
|
@@ -260,8 +284,10 @@ async function main() {
|
|
|
260
284
|
}
|
|
261
285
|
|
|
262
286
|
if (command === 'access-request') {
|
|
287
|
+
if (rest.length > 1) throw new Error('Usage: bbx access-request [intent]');
|
|
288
|
+
const params = SHORTCUT_COMMANDS['access-request'].build(rest);
|
|
263
289
|
await printSummary(
|
|
264
|
-
await requestBridge(client, 'access.request',
|
|
290
|
+
await requestBridge(client, 'access.request', params, { source: REQUEST_SOURCE })
|
|
265
291
|
);
|
|
266
292
|
return;
|
|
267
293
|
}
|
|
@@ -377,7 +403,8 @@ async function main() {
|
|
|
377
403
|
}
|
|
378
404
|
|
|
379
405
|
if (command === 'batch') {
|
|
380
|
-
const
|
|
406
|
+
const { preset, rest: batchArgs } = extractPresetFlag(rest);
|
|
407
|
+
const results = await runBatchCalls(client, batchArgs[0], REQUEST_SOURCE, { preset });
|
|
381
408
|
if (results.some((result) => !result.ok)) {
|
|
382
409
|
process.exitCode = 1;
|
|
383
410
|
}
|
|
@@ -412,7 +439,7 @@ async function main() {
|
|
|
412
439
|
const [key, refOrSelector] = rest;
|
|
413
440
|
if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
|
|
414
441
|
const target = refOrSelector
|
|
415
|
-
? refOrSelector
|
|
442
|
+
? isElementRef(refOrSelector)
|
|
416
443
|
? { elementRef: refOrSelector }
|
|
417
444
|
: { selector: refOrSelector }
|
|
418
445
|
: undefined;
|
|
@@ -755,14 +782,7 @@ function assertValidHarDocument(document) {
|
|
|
755
782
|
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
756
783
|
throw new Error('HAR response does not contain an inline HAR object.');
|
|
757
784
|
}
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
!log ||
|
|
761
|
-
typeof log !== 'object' ||
|
|
762
|
-
Array.isArray(log) ||
|
|
763
|
-
Reflect.get(log, 'version') !== '1.2' ||
|
|
764
|
-
!Array.isArray(Reflect.get(log, 'entries'))
|
|
765
|
-
) {
|
|
785
|
+
if (!isHarDocument(document)) {
|
|
766
786
|
throw new Error('HAR response does not contain a valid HAR 1.2 document.');
|
|
767
787
|
}
|
|
768
788
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
|
|
4
4
|
import {
|
|
5
|
+
isElementRef,
|
|
5
6
|
parseCommaList,
|
|
6
7
|
parseIntArg,
|
|
7
8
|
parseNumberArg,
|
|
@@ -39,7 +40,7 @@ function createShortcutCommand(method, usage, build, options = {}) {
|
|
|
39
40
|
*/
|
|
40
41
|
function createAtomicTarget(value) {
|
|
41
42
|
if (!value) throw new Error('A ref or selector is required.');
|
|
42
|
-
return value
|
|
43
|
+
return isElementRef(value) ? { elementRef: value } : { selector: value };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** @type {Record<string, ShortcutCommand>} */
|
|
@@ -266,6 +267,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
266
267
|
'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
|
|
267
268
|
'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
|
|
268
269
|
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
|
|
270
|
+
' MCP uses a compact surface: legacy clients can load typed tools; modern stateless clients use browser_call.',
|
|
269
271
|
'bbx status Check bridge connection',
|
|
270
272
|
'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
|
|
271
273
|
'bbx restart Restart the local daemon and running MCP servers',
|
|
@@ -285,15 +287,17 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
285
287
|
'bbx tab-close <tabId> Close a tab',
|
|
286
288
|
'bbx tab-activate <tabId> Bring a tab to the foreground',
|
|
287
289
|
'bbx skill Runtime budget presets and method groups',
|
|
290
|
+
'bbx protocol describe [method|group] Describe method params or list registry groups locally',
|
|
288
291
|
'bbx mcp serve Start Browser Bridge as an MCP stdio server',
|
|
289
292
|
],
|
|
290
293
|
},
|
|
291
294
|
{
|
|
292
295
|
title: 'Generic RPC',
|
|
293
296
|
lines: [
|
|
294
|
-
'bbx call [--tab <tabId>] <method> [paramsJson|-]
|
|
297
|
+
'bbx call [--tab <tabId>] [--preset quick|normal|deep] <method> [paramsJson|-] Call any bridge method (- reads JSON from stdin)',
|
|
295
298
|
'bbx <method> [--tab <tabId>] [paramsJson|-] Direct alias for exact bridge methods such as page.get_state',
|
|
296
|
-
"bbx batch '[{method,params,tabId?},...]'
|
|
299
|
+
"bbx batch [--preset quick|normal|deep] '[{method,params,tabId?},...]' Up to 20 parallel read-only calls",
|
|
300
|
+
'--preset fills budget gaps (maxNodes, maxDepth, textBudget, limit) from the shared quick|normal|deep presets; explicit params always win.',
|
|
297
301
|
'Every bridge command accepts --remote <name> (or BBX_REMOTE env) to target a remote destination from `bbx remote list`.',
|
|
298
302
|
'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
|
|
299
303
|
'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
|
|
@@ -91,23 +91,23 @@ export function getMcpConfigShape(clientName) {
|
|
|
91
91
|
* env: Record<string, string>
|
|
92
92
|
* } | {
|
|
93
93
|
* type: 'local',
|
|
94
|
-
* command: string[]
|
|
94
|
+
* command: string[],
|
|
95
|
+
* environment?: Record<string, string>
|
|
95
96
|
* }}
|
|
96
97
|
*/
|
|
97
98
|
function createBaseServerConfig(clientName) {
|
|
98
|
-
const serverConfig = {
|
|
99
|
-
command: MCP_COMMAND,
|
|
100
|
-
args: [...MCP_ARGS],
|
|
101
|
-
env: {},
|
|
102
|
-
};
|
|
103
|
-
|
|
104
99
|
if (clientName === 'opencode') {
|
|
105
100
|
return {
|
|
106
101
|
type: 'local',
|
|
107
102
|
command: [MCP_COMMAND, ...MCP_ARGS],
|
|
108
103
|
};
|
|
109
104
|
}
|
|
110
|
-
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
command: MCP_COMMAND,
|
|
108
|
+
args: [...MCP_ARGS],
|
|
109
|
+
env: {},
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** @type {Record<McpClientName, { key: string, includeType: boolean, keepEmptyBlock?: boolean }>} */
|
|
@@ -14,6 +14,8 @@ const DEFAULT_LOCK_TIMEOUT_MS = 120_000;
|
|
|
14
14
|
const UPDATE_LOCK_PORT_START = 49_152;
|
|
15
15
|
const UPDATE_LOCK_PORT_COUNT = 16_384;
|
|
16
16
|
const UPDATE_LOCK_PORT_ATTEMPTS = 32;
|
|
17
|
+
const UPDATE_LOCK_PORT_STEP = UPDATE_LOCK_PORT_COUNT / UPDATE_LOCK_PORT_ATTEMPTS;
|
|
18
|
+
const UNAVAILABLE_LOCK_PORT_CODES = new Set(['EACCES', 'EADDRINUSE']);
|
|
17
19
|
|
|
18
20
|
/** @typedef {import('./types.js').NpmUpdateResult} NpmUpdateResult */
|
|
19
21
|
|
|
@@ -113,6 +115,7 @@ export async function runNpmCommand(args, options = {}) {
|
|
|
113
115
|
? process.execPath
|
|
114
116
|
: npmExecPath || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
115
117
|
const commandArgs = useNode && npmExecPath ? [npmExecPath, ...args] : args;
|
|
118
|
+
const useWindowsShell = process.platform === 'win32' && /\.(?:cmd|bat)$/iu.test(command);
|
|
116
119
|
|
|
117
120
|
return new Promise((resolve, reject) => {
|
|
118
121
|
execFile(
|
|
@@ -124,6 +127,7 @@ export async function runNpmCommand(args, options = {}) {
|
|
|
124
127
|
timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
125
128
|
maxBuffer: 4 * 1024 * 1024,
|
|
126
129
|
windowsHide: true,
|
|
130
|
+
shell: useWindowsShell,
|
|
127
131
|
},
|
|
128
132
|
(error, stdout, stderr) => {
|
|
129
133
|
if (!error) {
|
|
@@ -154,7 +158,8 @@ export function getNpmUpdateLockPort(lockKey, offset = 0) {
|
|
|
154
158
|
const digest = createHash('sha256').update(lockKey).digest();
|
|
155
159
|
return (
|
|
156
160
|
UPDATE_LOCK_PORT_START +
|
|
157
|
-
((digest.readUInt32BE(0) + Math.max(0, Math.trunc(offset))) %
|
|
161
|
+
((digest.readUInt32BE(0) + Math.max(0, Math.trunc(offset)) * UPDATE_LOCK_PORT_STEP) %
|
|
162
|
+
UPDATE_LOCK_PORT_COUNT)
|
|
158
163
|
);
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -200,7 +205,9 @@ export async function acquireNpmUpdateLock(lockKey, timeoutMs = DEFAULT_LOCK_TIM
|
|
|
200
205
|
const server = net.createServer((socket) => socket.end(`${identity}\n`));
|
|
201
206
|
const acquired = await new Promise((resolve, reject) => {
|
|
202
207
|
server.once('error', (error) => {
|
|
203
|
-
if (
|
|
208
|
+
if (
|
|
209
|
+
UNAVAILABLE_LOCK_PORT_CODES.has(/** @type {NodeJS.ErrnoException} */ (error).code ?? '')
|
|
210
|
+
) {
|
|
204
211
|
resolve(false);
|
|
205
212
|
return;
|
|
206
213
|
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
sanitizeIncidentalPath,
|
|
34
34
|
sanitizeIncidentalText,
|
|
35
35
|
} from '../../protocol/src/index.js';
|
|
36
|
-
import { methodNeedsTab } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, methodNeedsTab } from './cli-helpers.js';
|
|
37
37
|
import { BridgeClient } from './client.js';
|
|
38
38
|
import { applyConfiguredAutoUpdate } from './config.js';
|
|
39
39
|
import { readRemoteConfig } from './remotes.js';
|
|
@@ -106,7 +106,7 @@ export async function ensureClientConnected(client) {
|
|
|
106
106
|
* @param {BridgeClient} client
|
|
107
107
|
* @param {BridgeMethod} method
|
|
108
108
|
* @param {Record<string, unknown>} [params={}]
|
|
109
|
-
* @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
|
|
109
|
+
* @param {{ tabId?: number | null, source?: BridgeRequestSource, mcpEra?: import('./types.js').McpProtocolEra, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
|
|
110
110
|
* @returns {Promise<BridgeResponse>}
|
|
111
111
|
*/
|
|
112
112
|
export async function requestBridge(client, method, params = {}, options = {}) {
|
|
@@ -127,7 +127,12 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
127
127
|
method,
|
|
128
128
|
params,
|
|
129
129
|
tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
|
|
130
|
-
meta: withRequestMeta(
|
|
130
|
+
meta: withRequestMeta(
|
|
131
|
+
options.source,
|
|
132
|
+
options.mcpEra,
|
|
133
|
+
options.tokenBudget,
|
|
134
|
+
options.automaticRetry
|
|
135
|
+
),
|
|
131
136
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
132
137
|
});
|
|
133
138
|
}
|
|
@@ -140,7 +145,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
140
145
|
* @returns {Promise<string>}
|
|
141
146
|
*/
|
|
142
147
|
export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
143
|
-
if (refOrSelector
|
|
148
|
+
if (isElementRef(refOrSelector)) {
|
|
144
149
|
return refOrSelector;
|
|
145
150
|
}
|
|
146
151
|
|
|
@@ -168,16 +173,20 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
|
168
173
|
|
|
169
174
|
/**
|
|
170
175
|
* @param {BridgeRequestSource | undefined} source
|
|
176
|
+
* @param {import('./types.js').McpProtocolEra | undefined} mcpEra
|
|
171
177
|
* @param {number | null | undefined} tokenBudget
|
|
172
178
|
* @param {'mcp_second_attempt' | undefined} automaticRetry
|
|
173
179
|
* @returns {BridgeMeta}
|
|
174
180
|
*/
|
|
175
|
-
function withRequestMeta(source, tokenBudget, automaticRetry) {
|
|
181
|
+
function withRequestMeta(source, mcpEra, tokenBudget, automaticRetry) {
|
|
176
182
|
/** @type {BridgeMeta} */
|
|
177
183
|
const meta = {};
|
|
178
184
|
if (source) {
|
|
179
185
|
meta.source = source;
|
|
180
186
|
}
|
|
187
|
+
if (source === 'mcp' && mcpEra) {
|
|
188
|
+
meta.mcp_era = mcpEra;
|
|
189
|
+
}
|
|
181
190
|
if (typeof tokenBudget === 'number' && Number.isFinite(tokenBudget)) {
|
|
182
191
|
meta.token_budget = tokenBudget;
|
|
183
192
|
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
BridgeMethod,
|
|
5
5
|
BridgeRequestSource,
|
|
6
6
|
BridgeResponse,
|
|
7
|
+
McpProtocolEra,
|
|
7
8
|
RecoveryEventKind,
|
|
8
9
|
RecoveryTelemetrySummary,
|
|
9
10
|
ScreenshotResult,
|
|
@@ -17,6 +18,7 @@ export type {
|
|
|
17
18
|
BridgeRequestSource,
|
|
18
19
|
BridgeResponse,
|
|
19
20
|
BridgeTransport,
|
|
21
|
+
McpProtocolEra,
|
|
20
22
|
ScreenshotResult,
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -9,7 +9,7 @@ import { startBridgeMcpServer } from './server.js';
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {{
|
|
12
|
-
* start?: () => Promise<void>,
|
|
12
|
+
* start?: () => void | Promise<void>,
|
|
13
13
|
* startControl?: typeof tryStartMcpProcessControl,
|
|
14
14
|
* argv?: string[],
|
|
15
15
|
* stdout?: { write: (chunk: string) => unknown },
|