@browserbridge/bbx 1.8.0 → 1.10.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/PRIVACY.md +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +70 -6
- package/packages/agent-client/src/cli-batch.js +127 -59
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +254 -26
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +14 -4
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +99 -7
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +333 -60
- package/packages/mcp-server/src/handlers-utils.js +141 -151
- package/packages/mcp-server/src/handlers.js +17 -4
- package/packages/mcp-server/src/server.js +269 -51
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +457 -27
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +38 -29
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +4 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +226 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +490 -4
- package/skills/browser-bridge/SKILL.md +26 -8
- package/skills/browser-bridge/references/protocol.md +125 -15
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
package/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Browser Bridge (BBX) Privacy Policy
|
|
2
2
|
|
|
3
|
-
Last updated: 2026-07-
|
|
3
|
+
Last updated: 2026-07-23
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -16,8 +16,9 @@ When you enable Browser Bridge for a Chrome window, Browser Bridge can access pa
|
|
|
16
16
|
- Console messages and uncaught page errors
|
|
17
17
|
- Network request metadata captured from page `fetch` and `XMLHttpRequest` activity, such as URL, method, status, timing, type, timestamp, and size
|
|
18
18
|
- Optional Chrome DevTools Protocol metadata for broader resource activity, including request ID, redacted URL, method, resource type, status, MIME type, protocol, cache indicators, redirects, failure reason, duration, and timestamp
|
|
19
|
+
- Metadata-only HAR 1.2 documents exported from an explicitly armed CDP capture, including sanitized URLs and observed status, redirect, failure, cache, and service-worker metadata
|
|
19
20
|
- Current JavaScript dialog information when observable and explicitly requested, including dialog type, bounded message text, and bounded default prompt text
|
|
20
|
-
- `localStorage` and `sessionStorage`
|
|
21
|
+
- `localStorage` and `sessionStorage` key metadata through ordinary inspection, and one exact value only through a deliberate `sensitive.read` request
|
|
21
22
|
- Screenshots or cropped image captures when explicitly requested
|
|
22
23
|
- User-triggered interaction data and results such as clicks, native pointer movement/drag, DOM or native text dispatch, key presses, selection changes, dialog decisions, and navigation actions
|
|
23
24
|
- Bounded semantic descriptors for optional stale-element recovery, represented in memory with normalized hashes and structural context rather than page text values or attributes written into the application DOM
|
|
@@ -26,10 +27,31 @@ All-resource network inspection excludes request and response bodies, cookies,
|
|
|
26
27
|
authorization values, and complete headers. It removes URL credentials and
|
|
27
28
|
fragments, replaces query parameter values with a redaction marker, and
|
|
28
29
|
summarizes data/blob URLs before returning them. Browser Bridge does not expose
|
|
29
|
-
these excluded values through that feature.
|
|
30
|
+
these excluded values through that feature. HAR exports use the same URL
|
|
31
|
+
sanitization: schemes, hosts, paths, and query parameter names remain visible,
|
|
32
|
+
but credentials, fragments, and query values do not. HAR request/response
|
|
33
|
+
headers and cookies are empty, bodies are absent, and unavailable byte sizes and
|
|
34
|
+
timing phases are reported as `-1`.
|
|
35
|
+
|
|
36
|
+
Recovery telemetry contains only fixed category names, bounded counts, rates,
|
|
37
|
+
timestamps, and active-loop flags. It does not include page content, URLs,
|
|
38
|
+
selectors, method/group identifiers, request payloads, or error text. The fixed
|
|
39
|
+
categories are automatic MCP retry, stale-reference recovery, debugger
|
|
40
|
+
reattachment, content-script reinjection, native-host reconnect, and request
|
|
41
|
+
outcome.
|
|
30
42
|
|
|
31
43
|
Browser Bridge is designed for local debugging and automation of the browser state you intentionally expose to it.
|
|
32
44
|
|
|
45
|
+
Ordinary storage inspection does not return value prefixes. Exact Web Storage
|
|
46
|
+
values require a separate key-specific sensitive read, are returned whole or
|
|
47
|
+
rejected whole, are never batched or retried automatically, and produce a
|
|
48
|
+
textual Sensitive access warning in Recent Activity. The warning stores the
|
|
49
|
+
source/category and key length, not the key or value. Powerful explicit methods
|
|
50
|
+
such as page evaluation and DOM inspection can still expose page data by design.
|
|
51
|
+
Every page evaluation is therefore labeled as Sensitive access in Recent
|
|
52
|
+
Activity, and its returned value and derived size are excluded from persisted
|
|
53
|
+
activity diagnostics.
|
|
54
|
+
|
|
33
55
|
## How Browser Bridge uses data
|
|
34
56
|
|
|
35
57
|
Browser Bridge uses this data only to provide its user-facing purpose: letting you inspect, test, and patch the currently enabled browser window from a connected developer tool or agent.
|
|
@@ -61,9 +83,13 @@ Browser Bridge keeps data only as long as needed for the current local session a
|
|
|
61
83
|
|
|
62
84
|
- Window enablement state is stored in session storage and cleared when disabled or when the browser session ends.
|
|
63
85
|
- Action log entries are stored for the current browser session only.
|
|
86
|
+
- Incidental action/log URLs remove credentials and fragments and redact query values; sensitive structured fields and local path prefixes are reduced before persistence.
|
|
64
87
|
- Console and fetch/XHR network buffers are bounded in memory and cleared when disabled or when the page state is reset.
|
|
65
88
|
- Optional CDP network capture is bounded in memory, explicitly started/stopped, expires after a safety interval, and is cleared on relevant debugger, tab, or access teardown.
|
|
89
|
+
- HAR export does not start, stop, or clear capture. Inline size limits remove whole oldest entries; artifact delivery uses opaque owner-scoped handles with no browser-host path. Artifacts expire after five minutes, are removed on daemon/extension teardown, and use private daemon directory/file modes on POSIX systems.
|
|
90
|
+
- Recovery telemetry is held in fixed five-second buckets over a five-minute process-local window. Extension-side summaries reset with the extension service worker, daemon summaries reset with the daemon, and neither is durable history.
|
|
66
91
|
- Dialog message/default prompt text is returned to the requesting caller when inspected but is excluded from persisted action-log summaries and size diagnostics.
|
|
92
|
+
- Exact sensitive values traverse the selected client in memory but are excluded from action summaries, diagnostic size counters, daemon logs, and automatic artifact storage.
|
|
67
93
|
- Patch rollback records and remembered element references live in the current document. Browser Bridge attempts patch rollback when access is disabled or switched, while page replacement/navigation removes document-local state.
|
|
68
94
|
- Hashed semantic descriptors used for optional stale recovery are bounded in memory with the element registry and do not survive the document/registry lifecycle.
|
|
69
95
|
|
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
|
|
|
93
93
|
## What it's for
|
|
94
94
|
|
|
95
95
|
- Debugging a UI on `localhost`: read DOM, computed styles, layout, console logs, and network state without a screenshot
|
|
96
|
+
- Exporting sanitized, metadata-only HAR 1.2 evidence after explicitly capturing a network reproduction
|
|
96
97
|
- Verifying a code change actually rendered the expected result in Chrome
|
|
97
98
|
- Patching the live page to prove a fix visually, then moving it into source and rolling the patch back
|
|
98
99
|
- Running structured browser checks from any local agent or IDE, not just one AI product
|
|
@@ -108,9 +109,10 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
|
|
|
108
109
|
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
110
|
2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
|
|
110
111
|
3. Run `bbx install` (Chromium on Linux, Chrome elsewhere), or target a specific browser with `bbx install --browser chrome`, `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
|
|
111
|
-
4.
|
|
112
|
-
5.
|
|
113
|
-
6.
|
|
112
|
+
4. Optionally run `bbx config set auto-update compatible` to keep the global CLI on the highest stable npm release supported by the connected extension
|
|
113
|
+
5. In the extension side panel, install MCP or CLI (skill) for your agent of choice
|
|
114
|
+
6. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
|
|
115
|
+
7. 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
116
|
|
|
115
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.
|
|
116
118
|
|
|
@@ -122,12 +124,17 @@ MCP mode is self-contained: the server exposes tools and startup instructions, s
|
|
|
122
124
|
- Requests default to the active tab in that window unless a tab is targeted explicitly
|
|
123
125
|
- Targeted input actions reject hidden, disabled, ambiguous, stale, or obscured targets instead of silently guessing; their responses report the resolution and DOM/CDP execution path (`cdp_press_key` and `scroll_into_view` use separate contracts)
|
|
124
126
|
- Native pointer/text input, dialog handling, all-resource network capture, accessibility trees, screenshots, and raw CDP reads use the existing debugger permission only when requested
|
|
127
|
+
- HAR export is a read of an already armed CDP capture: start capture, reproduce, export, then stop. Export never starts, stops, or clears capture, and omits headers, cookies, bodies, authorization values, and unsupported size/timing detail
|
|
128
|
+
- HAR and screenshot artifacts are opaque, owner-scoped daemon handles with a five-minute lifetime and no browser-host path; CLI capture commands download, verify, delete, and write them on the CLI host
|
|
125
129
|
- Element references are document-local and strict by default; stale input recovery is opt-in, same-document, requires one strong unique semantic match, and fails safely when its bounded scan cannot prove uniqueness
|
|
130
|
+
- Semantic DOM baselines provide explicit create/compare/describe/release verification without modifying the page; compact snapshots stay memory-only, expire after five minutes, and are invalidated by navigation or access teardown
|
|
126
131
|
- Patches keep per-document rollback history and Browser Bridge attempts best-effort rollback when window access is disabled or switched; committing the patch baseline keeps current changes but discards their rollback history
|
|
127
132
|
- URL waits observe full navigation and same-document SPA changes, and report the final URL and observed navigation kind
|
|
128
133
|
- Structured DOM/style/layout reads are the primary transport; screenshots are a fallback
|
|
129
134
|
- Browser input dispatch does not prove the application accepted the intended change, so agents should verify with a wait or structured read
|
|
130
135
|
- Open-ended investigation should start with structured reads on a smaller, lower-cost subagent when the client supports delegation
|
|
136
|
+
- Recovery diagnostics use process-local five-minute counters for six fixed, content-free categories. `health.ping`, `daemon.metrics`, and `bbx doctor` expose no page data; three failures in the same category group within 60 seconds produce an active-loop warning and fixed recovery guidance
|
|
137
|
+
- Performance reads are raw browser-maintained CDP counter point samples with browser-defined names and units, not Web Vitals or Browser Bridge navigation measurements
|
|
131
138
|
- The native host daemon auto-starts on demand
|
|
132
139
|
|
|
133
140
|
## Documentation
|
package/SECURITY.md
CHANGED
|
@@ -10,6 +10,13 @@ Only the latest version published to npm (`@browserbridge/bbx`) and the Chrome W
|
|
|
10
10
|
receives security fixes. Older versions are not patched retroactively; upgrade to the
|
|
11
11
|
latest release before reporting.
|
|
12
12
|
|
|
13
|
+
The optional `compatible` npm auto-update policy follows the connected
|
|
14
|
+
extension's advertised `major.minor` protocol line. It installs only stable
|
|
15
|
+
versions of the fixed `@browserbridge/bbx` package, never downgrades, and runs
|
|
16
|
+
only from a verified global npm installation. If an older extension line is
|
|
17
|
+
still installed, this policy may intentionally select an older release than the
|
|
18
|
+
latest security-supported version.
|
|
19
|
+
|
|
13
20
|
## Reporting a Vulnerability
|
|
14
21
|
|
|
15
22
|
Please do **not** open a public GitHub issue for security vulnerabilities.
|
|
@@ -50,3 +57,8 @@ Out of scope:
|
|
|
50
57
|
- Proxy mode (LAN exposure) is opt-in via `bbx proxy enable` and always provisions a
|
|
51
58
|
token; an invalid `bindHost` in the config rejects the config rather than widening
|
|
52
59
|
the bind address.
|
|
60
|
+
- Incidental persisted activity and daemon logs recursively redact sensitive keys,
|
|
61
|
+
URL credentials/fragments/query values, and local path prefixes.
|
|
62
|
+
- Ordinary Web Storage reads return key/presence metadata only. The separate
|
|
63
|
+
`sensitive.read` method requires one exact key, is excluded from batches and
|
|
64
|
+
automatic retries, and returns a value whole or fails atomically.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -81,12 +81,18 @@
|
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/chrome": "^0.2.2",
|
|
83
83
|
"@types/node": "^26.1.1",
|
|
84
|
+
"ajv": "^8.20.0",
|
|
85
|
+
"ajv-formats": "^3.0.1",
|
|
84
86
|
"c8": "^12.0.0",
|
|
87
|
+
"har-schema": "^2.0.0",
|
|
85
88
|
"oxfmt": "^0.60.0",
|
|
86
89
|
"oxlint": "^1.75.0",
|
|
87
90
|
"tsx": "^4.23.1",
|
|
88
91
|
"typescript": "^7.0.2"
|
|
89
92
|
},
|
|
93
|
+
"overrides": {
|
|
94
|
+
"@hono/node-server": "2.0.11"
|
|
95
|
+
},
|
|
90
96
|
"engines": {
|
|
91
97
|
"node": ">=20"
|
|
92
98
|
}
|
|
@@ -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.
|
|
@@ -84,18 +87,79 @@ export function extractScreenshotFlags(args) {
|
|
|
84
87
|
return { format: rawFormat, quality, rest };
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {string[]} args
|
|
92
|
+
* @returns {{ limit: number | undefined, urlPattern: string | undefined, delivery: 'inline' | 'artifact' | 'auto', rest: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
export function extractHarFlags(args) {
|
|
95
|
+
const rest = [...args];
|
|
96
|
+
const limitIndex = rest.indexOf('--limit');
|
|
97
|
+
let limit;
|
|
98
|
+
if (limitIndex !== -1) {
|
|
99
|
+
limit = Number(rest[limitIndex + 1]);
|
|
100
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
101
|
+
throw new Error('--limit must be an integer from 1 to 200.');
|
|
102
|
+
}
|
|
103
|
+
rest.splice(limitIndex, 2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const urlPatternIndex = rest.indexOf('--url-pattern');
|
|
107
|
+
const urlPattern = urlPatternIndex === -1 ? undefined : rest[urlPatternIndex + 1];
|
|
108
|
+
if (urlPatternIndex !== -1) {
|
|
109
|
+
if (!urlPattern || urlPattern.startsWith('--')) {
|
|
110
|
+
throw new Error('--url-pattern requires a value.');
|
|
111
|
+
}
|
|
112
|
+
rest.splice(urlPatternIndex, 2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const deliveryIndex = rest.indexOf('--delivery');
|
|
116
|
+
const rawDelivery = deliveryIndex === -1 ? 'auto' : rest[deliveryIndex + 1];
|
|
117
|
+
if (rawDelivery !== 'inline' && rawDelivery !== 'artifact' && rawDelivery !== 'auto') {
|
|
118
|
+
throw new Error('--delivery must be inline, artifact, or auto.');
|
|
119
|
+
}
|
|
120
|
+
if (deliveryIndex !== -1) rest.splice(deliveryIndex, 2);
|
|
121
|
+
|
|
122
|
+
return { limit, urlPattern, delivery: rawDelivery, rest };
|
|
123
|
+
}
|
|
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
|
+
|
|
87
150
|
/**
|
|
88
151
|
* @param {string[]} args
|
|
89
152
|
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
90
153
|
*/
|
|
91
154
|
export async function parseCallCommand(args) {
|
|
92
|
-
const
|
|
155
|
+
const presetParsed = extractPresetFlag(args);
|
|
156
|
+
const parsed = extractTabFlag(presetParsed.rest);
|
|
93
157
|
const [first, second, ...extra] = parsed.rest;
|
|
94
158
|
if (!first) {
|
|
95
|
-
throw new Error(
|
|
159
|
+
throw new Error(CALL_USAGE);
|
|
96
160
|
}
|
|
97
161
|
if (extra.length > 0) {
|
|
98
|
-
throw new Error(
|
|
162
|
+
throw new Error(CALL_USAGE);
|
|
99
163
|
}
|
|
100
164
|
|
|
101
165
|
if (first.includes('.')) {
|
|
@@ -111,11 +175,11 @@ export async function parseCallCommand(args) {
|
|
|
111
175
|
return {
|
|
112
176
|
method,
|
|
113
177
|
tabId: methodNeedsTab(method) ? parsed.tabId : null,
|
|
114
|
-
params: parseJsonObject(rawParams),
|
|
178
|
+
params: applyMethodBudgetPreset(method, parseJsonObject(rawParams), presetParsed.preset),
|
|
115
179
|
};
|
|
116
180
|
}
|
|
117
181
|
|
|
118
|
-
throw new Error(
|
|
182
|
+
throw new Error(CALL_USAGE);
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
/**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
applyMethodBudgetPreset,
|
|
4
5
|
getProtocolVersion,
|
|
6
|
+
isBatchSafeBridgeCall,
|
|
7
|
+
MAX_BATCH_CALLS,
|
|
8
|
+
MAX_BATCH_CONCURRENCY,
|
|
5
9
|
METHODS,
|
|
6
10
|
summarizeBatchErrorItem,
|
|
7
11
|
summarizeBatchResponseItem,
|
|
@@ -11,6 +15,7 @@ import { requestBridge } from './runtime.js';
|
|
|
11
15
|
|
|
12
16
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
13
17
|
/** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
|
|
18
|
+
/** @typedef {import('../../protocol/src/defaults.js').BudgetPresetName} BudgetPresetName */
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* @typedef {{
|
|
@@ -27,6 +32,10 @@ import { requestBridge } from './runtime.js';
|
|
|
27
32
|
* }} InvalidBatchItem
|
|
28
33
|
*/
|
|
29
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {{ method: BridgeMethod, params: Record<string, unknown>, tabId: number | null }} PreparedBatchCall
|
|
37
|
+
*/
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* @typedef {ReturnType<typeof summarizeBatchResponseItem>
|
|
32
41
|
* | ReturnType<typeof summarizeBatchErrorItem>
|
|
@@ -59,15 +68,16 @@ function invalidBatchItem(method, message) {
|
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
/**
|
|
62
|
-
* Parse and execute `bbx batch '[...]'`: validate
|
|
63
|
-
*
|
|
71
|
+
* Parse and execute `bbx batch '[...]'`: validate read-only calls, run them
|
|
72
|
+
* with bounded concurrency, and return ordered per-call summary items.
|
|
64
73
|
*
|
|
65
74
|
* @param {import('./client.js').BridgeClient} client
|
|
66
75
|
* @param {string | undefined} input - Raw JSON array argument
|
|
67
76
|
* @param {BridgeRequestSource} source - Request source tag (e.g. 'cli')
|
|
77
|
+
* @param {{ preset?: BudgetPresetName | null }} [options] - Optional budget preset applied to every call
|
|
68
78
|
* @returns {Promise<BatchResultItem[]>}
|
|
69
79
|
*/
|
|
70
|
-
export async function runBatchCalls(client, input, source) {
|
|
80
|
+
export async function runBatchCalls(client, input, source, options = {}) {
|
|
71
81
|
if (!input) {
|
|
72
82
|
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
73
83
|
}
|
|
@@ -80,61 +90,119 @@ export async function runBatchCalls(client, input, source) {
|
|
|
80
90
|
if (!Array.isArray(calls)) {
|
|
81
91
|
throw new Error('Batch input must be a JSON array.');
|
|
82
92
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
93
|
+
if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
|
|
94
|
+
throw new Error(`Batch input must contain between 1 and ${MAX_BATCH_CALLS} calls.`);
|
|
95
|
+
}
|
|
96
|
+
const prepared = calls.map((call) => prepareBatchCall(call, options.preset ?? null));
|
|
97
|
+
if (prepared.some((item) => 'error' in item)) {
|
|
98
|
+
return prepared.map((item) =>
|
|
99
|
+
'error' in item
|
|
100
|
+
? item.error
|
|
101
|
+
: invalidBatchItem(
|
|
102
|
+
item.value.method,
|
|
103
|
+
'Batch was not executed because another call failed validation.'
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const validPrepared = /** @type {Array<{ value: PreparedBatchCall }>} */ (prepared);
|
|
108
|
+
if (!client.connected) await client.connect();
|
|
109
|
+
return mapWithConcurrency(validPrepared, MAX_BATCH_CONCURRENCY, async (item) => {
|
|
110
|
+
const { method, params, tabId } = item.value;
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
try {
|
|
113
|
+
const response = await requestBridge(client, method, params, {
|
|
114
|
+
tabId,
|
|
115
|
+
source,
|
|
116
|
+
});
|
|
117
|
+
return summarizeBatchResponseItem({
|
|
118
|
+
method,
|
|
119
|
+
tabId,
|
|
120
|
+
response,
|
|
121
|
+
durationMs: Date.now() - startTime,
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return summarizeBatchErrorItem({
|
|
125
|
+
method,
|
|
126
|
+
tabId,
|
|
127
|
+
error: err,
|
|
128
|
+
durationMs: Date.now() - startTime,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {unknown} call
|
|
136
|
+
* @param {BudgetPresetName | null} preset
|
|
137
|
+
* @returns {{ value: PreparedBatchCall } | { error: InvalidBatchItem }}
|
|
138
|
+
*/
|
|
139
|
+
function prepareBatchCall(call, preset) {
|
|
140
|
+
if (!call || typeof call !== 'object' || Array.isArray(call)) {
|
|
141
|
+
return { error: invalidBatchItem('', 'Each batch call needs a method.') };
|
|
142
|
+
}
|
|
143
|
+
const batchCall = /** @type {Record<string, unknown>} */ (call);
|
|
144
|
+
if (typeof batchCall.method !== 'string') {
|
|
145
|
+
return { error: invalidBatchItem('', 'Each batch call needs a method.') };
|
|
146
|
+
}
|
|
147
|
+
if (!METHODS.includes(/** @type {BridgeMethod} */ (batchCall.method))) {
|
|
148
|
+
return {
|
|
149
|
+
error: invalidBatchItem(batchCall.method, `Unknown bridge method "${batchCall.method}".`),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const method = /** @type {BridgeMethod} */ (batchCall.method);
|
|
153
|
+
if (method === 'sensitive.read') {
|
|
154
|
+
return {
|
|
155
|
+
error: invalidBatchItem(
|
|
156
|
+
method,
|
|
157
|
+
'sensitive.read is never allowed in batch execution. Call it sequentially.'
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
batchCall.params !== undefined &&
|
|
163
|
+
(!batchCall.params || typeof batchCall.params !== 'object' || Array.isArray(batchCall.params))
|
|
164
|
+
) {
|
|
165
|
+
return { error: invalidBatchItem(method, 'Batch call params must be a JSON object.') };
|
|
166
|
+
}
|
|
167
|
+
const params =
|
|
168
|
+
batchCall.params === undefined ? {} : /** @type {Record<string, unknown>} */ (batchCall.params);
|
|
169
|
+
const mergedParams = applyMethodBudgetPreset(method, params, preset);
|
|
170
|
+
if (!isBatchSafeBridgeCall(method, mergedParams)) {
|
|
171
|
+
return {
|
|
172
|
+
error: invalidBatchItem(
|
|
173
|
+
method,
|
|
174
|
+
`${method} is not safe for batch execution. Call it sequentially.`
|
|
175
|
+
),
|
|
176
|
+
};
|
|
85
177
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
? batchCall.tabId
|
|
117
|
-
: null;
|
|
118
|
-
const startTime = Date.now();
|
|
119
|
-
try {
|
|
120
|
-
const response = await requestBridge(client, method, params, {
|
|
121
|
-
tabId,
|
|
122
|
-
source,
|
|
123
|
-
});
|
|
124
|
-
return summarizeBatchResponseItem({
|
|
125
|
-
method,
|
|
126
|
-
tabId,
|
|
127
|
-
response,
|
|
128
|
-
durationMs: Date.now() - startTime,
|
|
129
|
-
});
|
|
130
|
-
} catch (err) {
|
|
131
|
-
return summarizeBatchErrorItem({
|
|
132
|
-
method,
|
|
133
|
-
tabId,
|
|
134
|
-
error: err,
|
|
135
|
-
durationMs: Date.now() - startTime,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
);
|
|
178
|
+
const tabId =
|
|
179
|
+
methodNeedsTab(method) &&
|
|
180
|
+
typeof batchCall.tabId === 'number' &&
|
|
181
|
+
Number.isInteger(batchCall.tabId) &&
|
|
182
|
+
batchCall.tabId > 0
|
|
183
|
+
? batchCall.tabId
|
|
184
|
+
: null;
|
|
185
|
+
return { value: { method, params: mergedParams, tabId } };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @template T, R
|
|
190
|
+
* @param {T[]} values
|
|
191
|
+
* @param {number} concurrency
|
|
192
|
+
* @param {(value: T) => Promise<R>} callback
|
|
193
|
+
* @returns {Promise<R[]>}
|
|
194
|
+
*/
|
|
195
|
+
async function mapWithConcurrency(values, concurrency, callback) {
|
|
196
|
+
/** @type {R[]} */
|
|
197
|
+
const results = new Array(values.length);
|
|
198
|
+
let nextIndex = 0;
|
|
199
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
200
|
+
while (nextIndex < values.length) {
|
|
201
|
+
const index = nextIndex;
|
|
202
|
+
nextIndex += 1;
|
|
203
|
+
results[index] = await callback(values[index]);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
await Promise.all(workers);
|
|
207
|
+
return results;
|
|
140
208
|
}
|
|
@@ -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).
|
|
@@ -28,10 +28,17 @@ export async function printSummary(response, method) {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
31
|
+
* @param {string} [method]
|
|
31
32
|
* @returns {void}
|
|
32
33
|
*/
|
|
33
|
-
export function printCallResponse(response) {
|
|
34
|
+
export function printCallResponse(response, method) {
|
|
34
35
|
if (response.ok) {
|
|
36
|
+
if (method === 'sensitive.read') {
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
`${JSON.stringify(response.result, null, process.stdout.isTTY ? 2 : undefined)}\n`
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
35
42
|
printJson(response.result);
|
|
36
43
|
return;
|
|
37
44
|
}
|
|
@@ -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
|
|