@browserbridge/bbx 1.7.8 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/PRIVACY.md +57 -12
  2. package/README.md +19 -8
  3. package/SECURITY.md +12 -0
  4. package/package.json +15 -6
  5. package/packages/agent-client/src/cli-args.js +59 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +272 -70
  9. package/packages/agent-client/src/client.js +296 -8
  10. package/packages/agent-client/src/command-registry.js +64 -55
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/content-extract.js +198 -0
  13. package/packages/agent-client/src/npm-self-update.js +305 -0
  14. package/packages/agent-client/src/remotes.js +4 -2
  15. package/packages/agent-client/src/runtime.js +864 -39
  16. package/packages/agent-client/src/types.ts +169 -6
  17. package/packages/mcp-server/src/guidance.js +1 -1
  18. package/packages/mcp-server/src/handlers-capture.js +69 -6
  19. package/packages/mcp-server/src/handlers-dom.js +61 -4
  20. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  21. package/packages/mcp-server/src/handlers-page.js +211 -54
  22. package/packages/mcp-server/src/handlers-utils.js +101 -10
  23. package/packages/mcp-server/src/handlers.js +14 -7
  24. package/packages/mcp-server/src/server.js +176 -11
  25. package/packages/native-host/src/artifact-store.js +353 -0
  26. package/packages/native-host/src/config.js +9 -1
  27. package/packages/native-host/src/daemon-logger.js +3 -1
  28. package/packages/native-host/src/daemon.js +701 -33
  29. package/packages/native-host/src/native-host.js +28 -3
  30. package/packages/protocol/src/capability-values.js +2 -0
  31. package/packages/protocol/src/defaults.js +31 -0
  32. package/packages/protocol/src/errors.js +102 -6
  33. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  34. package/packages/protocol/src/index.js +2 -0
  35. package/packages/protocol/src/protocol.js +612 -6
  36. package/packages/protocol/src/recovery-telemetry.js +302 -0
  37. package/packages/protocol/src/registry.js +206 -38
  38. package/packages/protocol/src/summary.js +203 -30
  39. package/packages/protocol/src/types.ts +721 -2
  40. package/skills/browser-bridge/SKILL.md +54 -19
  41. package/skills/browser-bridge/references/interaction.md +55 -11
  42. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  43. package/skills/browser-bridge/references/protocol.md +265 -86
  44. package/skills/browser-bridge/references/token-efficiency.md +29 -13
package/PRIVACY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Browser Bridge (BBX) Privacy Policy
2
2
 
3
- Last updated: 2026-03-27
3
+ Last updated: 2026-07-23
4
4
 
5
5
  ## Overview
6
6
 
@@ -12,30 +12,61 @@ When you enable Browser Bridge for a Chrome window, Browser Bridge can access pa
12
12
 
13
13
  - Page metadata such as URL, title, viewport, and scroll position
14
14
  - DOM structure, element attributes, visible text, and layout information
15
- - Computed styles, matched rules, and accessibility-tree data
15
+ - Computed styles, class/inline-style context, and semantic accessibility-tree data
16
16
  - Console messages and uncaught page errors
17
- - Network request metadata captured from page `fetch` and `XMLHttpRequest` activity, such as URL, method, status, timing, and size
18
- - `localStorage` and `sessionStorage` values when explicitly requested
17
+ - Network request metadata captured from page `fetch` and `XMLHttpRequest` activity, such as URL, method, status, timing, type, timestamp, and size
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
20
+ - Current JavaScript dialog information when observable and explicitly requested, including dialog type, bounded message text, and bounded default prompt text
21
+ - `localStorage` and `sessionStorage` key metadata through ordinary inspection, and one exact value only through a deliberate `sensitive.read` request
19
22
  - Screenshots or cropped image captures when explicitly requested
20
- - User-triggered interaction data such as clicks, typing, key presses, selection changes, and navigation actions
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
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
25
+
26
+ All-resource network inspection excludes request and response bodies, cookies,
27
+ authorization values, and complete headers. It removes URL credentials and
28
+ fragments, replaces query parameter values with a redaction marker, and
29
+ summarizes data/blob URLs before returning them. Browser Bridge does not expose
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.
21
42
 
22
43
  Browser Bridge is designed for local debugging and automation of the browser state you intentionally expose to it.
23
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
+
24
55
  ## How Browser Bridge uses data
25
56
 
26
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.
27
58
 
28
- The extension sends request and response data locally through:
59
+ The normal local path sends request and response data through:
29
60
 
30
61
  1. The Browser Bridge Chrome extension
31
62
  2. The Browser Bridge native messaging host running on your machine
32
- 3. The local agent client or IDE that you intentionally connect
63
+ 3. The agent client or IDE that you intentionally connect
33
64
 
34
- Browser Bridge does not operate a Browser Bridge cloud service and does not send extension data to developer-operated remote servers.
65
+ The extension/native-host segment remains on the browser machine. If you explicitly configure an authenticated remote destination, the daemon can send bridge results over your own SSH tunnel or network route to that selected client. Browser Bridge does not operate a Browser Bridge cloud service and does not send extension data to developer-operated remote servers. Raw remote TCP is not presented as encrypted.
35
66
 
36
67
  ## Sharing and downstream processing
37
68
 
38
- Browser Bridge shares page data only with the local companion components and local client you choose to connect.
69
+ Browser Bridge shares page data only with the companion components and client you choose to connect. The normal path is local. If you explicitly configure Browser Bridge's authenticated remote proxy and your own tunnel or network route, the selected remote client can receive bridge results through that user-configured path; Browser Bridge does not provide or operate that network service.
39
70
 
40
71
  Important:
41
72
 
@@ -52,8 +83,15 @@ Browser Bridge keeps data only as long as needed for the current local session a
52
83
 
53
84
  - Window enablement state is stored in session storage and cleared when disabled or when the browser session ends.
54
85
  - Action log entries are stored for the current browser session only.
55
- - Console and network buffers are bounded in memory and cleared when disabled or when the page state is reset.
56
- - Patch state and remembered element references are session-scoped and cleared on disable, page replacement, or navigation as needed.
86
+ - Incidental action/log URLs remove credentials and fragments and redact query values; sensitive structured fields and local path prefixes are reduced before persistence.
87
+ - Console and fetch/XHR network buffers are bounded in memory and cleared when disabled or when the page state is reset.
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.
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.
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.
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.
57
95
 
58
96
  Browser Bridge does not provide Browser Bridge-hosted accounts, sync, or long-term server-side storage.
59
97
 
@@ -72,12 +110,19 @@ Browser Bridge does not load remotely hosted extension code.
72
110
 
73
111
  Browser Bridge can, when you explicitly invoke features such as page evaluation, execute user-requested expressions in the inspected page context through Chrome's documented Debugger API. That execution affects only the currently enabled browser window and is part of Browser Bridge's developer-tool functionality.
74
112
 
113
+ The same existing Debugger API permission is used only when requested for
114
+ native pointer/text dispatch, JavaScript dialog observation and explicit
115
+ handling, accessibility reads, screenshots, performance/CDP reads, interception,
116
+ and optional all-resource network capture. Browser Bridge does not silently
117
+ accept/dismiss dialogs and does not claim that accepted browser input proves an
118
+ application-level outcome.
119
+
75
120
  ## Security
76
121
 
77
122
  - Extension code is packaged with the extension; Browser Bridge does not fetch and execute remote extension scripts.
78
123
  - Communication with the companion host uses Chrome Native Messaging.
79
124
  - Access is limited to the explicitly enabled browser window.
80
- - The extension includes bounded buffers and session-scoped rollback for temporary page patches.
125
+ - The extension includes bounded buffers and document-local rollback records for temporary page patches, with rollback attempted when enabled-window access ends or switches.
81
126
 
82
127
  ## Chrome Web Store Limited Use statement
83
128
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  > **Chrome Web Store:** Install `Browser Bridge (BBX)` from the [Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie). For local or custom builds, use the unpacked install flow in [docs/unpacked-extension.md](https://github.com/koltyakov/browser-bridge/blob/main/docs/unpacked-extension.md).
6
6
 
7
- A local bridge between your coding agent and a real Chrome tab. Browser Bridge gives the agent structured access to DOM, styles, layout, console, network, and reversible patches - starting from the actual tab you already have open, with all its real state intact.
7
+ A local bridge between your coding agent and a real Chrome tab. Browser Bridge gives the agent structured access to DOM, styles, layout, console, network, accessibility data, reliable browser input, explicit JavaScript dialog handling, and reversible patches - starting from the actual tab you already have open, with all its real state intact.
8
8
 
9
9
  See [Quickstart](https://github.com/koltyakov/browser-bridge/blob/main/docs/quickstart.md) to get started in another repo, or browse the rest of the guides in [docs/index.md](https://github.com/koltyakov/browser-bridge/blob/main/docs/index.md).
10
10
 
@@ -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
@@ -101,16 +102,17 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
101
102
 
102
103
  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
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
+ 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 one explicitly enabled browser window rather than ambient browser control.
105
106
 
106
107
  ## Setup
107
108
 
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. 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)
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
 
@@ -120,10 +122,19 @@ MCP mode is self-contained: the server exposes tools and startup instructions, s
120
122
 
121
123
  - The extension is scoped to one explicitly enabled Chrome window at a time - no ambient browser access
122
124
  - Requests default to the active tab in that window unless a tab is targeted explicitly
123
- - Sessions are tab and origin scoped, auto-refreshed when possible
124
- - All patch operations are reversible and session-scoped
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)
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
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
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
132
+ - URL waits observe full navigation and same-document SPA changes, and report the final URL and observed navigation kind
125
133
  - Structured DOM/style/layout reads are the primary transport; screenshots are a fallback
134
+ - Browser input dispatch does not prove the application accepted the intended change, so agents should verify with a wait or structured read
126
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
127
138
  - The native host daemon auto-starts on demand
128
139
 
129
140
  ## Documentation
@@ -139,7 +150,7 @@ MCP mode is self-contained: the server exposes tools and startup instructions, s
139
150
 
140
151
  ## Privacy
141
152
 
142
- Browser Bridge itself routes extension data locally through the Chrome extension, native host, and the local client you choose to connect. Browser Bridge does not operate a Browser Bridge cloud service.
153
+ The extension and native host communicate on the browser machine, and local clients connect to that daemon by default. If you explicitly configure an authenticated remote destination, bridge results can travel over your user-provided SSH tunnel or network route to the selected client. Browser Bridge does not operate a Browser Bridge cloud service, and raw remote TCP is not presented as encrypted.
143
154
 
144
155
  Your connected agent or IDE may still forward tool calls or tool results to remote services under that product's own settings and privacy policy. See [PRIVACY.md](https://github.com/koltyakov/browser-bridge/blob/main/PRIVACY.md) for the Browser Bridge policy.
145
156
 
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.7.8",
3
+ "version": "1.9.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -55,8 +55,9 @@
55
55
  "lint": "oxlint . && oxfmt --check .",
56
56
  "format": "oxfmt .",
57
57
  "format:check": "oxfmt --check .",
58
- "test:runtime": "node --test",
59
- "test": "c8 node --test",
58
+ "fixture:browser": "node tests/fixtures/browser-reliability/server.mjs",
59
+ "test:runtime": "node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
60
+ "test": "c8 node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
60
61
  "coverage": "c8 report --reporter=text --reporter=text-summary",
61
62
  "coverage:review": "c8 report --reporter=json-summary && node scripts/review-coverage.mjs",
62
63
  "coverage:check": "c8 check-coverage --lines 92 --branches 83",
@@ -73,17 +74,25 @@
73
74
  },
74
75
  "dependencies": {
75
76
  "@modelcontextprotocol/sdk": "^1.29.0",
77
+ "@mozilla/readability": "^0.6.0",
78
+ "linkedom": "^0.18.13",
76
79
  "zod": "^4.4.3"
77
80
  },
78
81
  "devDependencies": {
79
82
  "@types/chrome": "^0.2.2",
80
83
  "@types/node": "^26.1.1",
84
+ "ajv": "^8.20.0",
85
+ "ajv-formats": "^3.0.1",
81
86
  "c8": "^12.0.0",
82
- "linkedom": "^0.18.13",
83
- "oxfmt": "^0.59.0",
84
- "oxlint": "^1.74.0",
87
+ "har-schema": "^2.0.0",
88
+ "oxfmt": "^0.60.0",
89
+ "oxlint": "^1.75.0",
90
+ "tsx": "^4.23.1",
85
91
  "typescript": "^7.0.2"
86
92
  },
93
+ "overrides": {
94
+ "@hono/node-server": "2.0.11"
95
+ },
87
96
  "engines": {
88
97
  "node": ">=20"
89
98
  }
@@ -60,6 +60,65 @@ export function extractTabFlag(args) {
60
60
  return { tabId, rest };
61
61
  }
62
62
 
63
+ /**
64
+ * @param {string[]} args
65
+ * @returns {{ format: 'png' | 'jpeg' | 'webp', quality: number | undefined, rest: string[] }}
66
+ */
67
+ export function extractScreenshotFlags(args) {
68
+ const rest = [...args];
69
+ const formatIndex = rest.indexOf('--format');
70
+ const rawFormat = formatIndex === -1 ? 'png' : rest[formatIndex + 1];
71
+ if (rawFormat !== 'png' && rawFormat !== 'jpeg' && rawFormat !== 'webp') {
72
+ throw new Error('--format must be png, jpeg, or webp.');
73
+ }
74
+ if (formatIndex !== -1) rest.splice(formatIndex, 2);
75
+ const qualityIndex = rest.indexOf('--quality');
76
+ let quality;
77
+ if (qualityIndex !== -1) {
78
+ quality = Number(rest[qualityIndex + 1]);
79
+ if (!Number.isInteger(quality) || quality < 0 || quality > 100) {
80
+ throw new Error('--quality must be an integer from 0 to 100.');
81
+ }
82
+ rest.splice(qualityIndex, 2);
83
+ }
84
+ return { format: rawFormat, quality, rest };
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} args
89
+ * @returns {{ limit: number | undefined, urlPattern: string | undefined, delivery: 'inline' | 'artifact' | 'auto', rest: string[] }}
90
+ */
91
+ export function extractHarFlags(args) {
92
+ const rest = [...args];
93
+ const limitIndex = rest.indexOf('--limit');
94
+ let limit;
95
+ if (limitIndex !== -1) {
96
+ limit = Number(rest[limitIndex + 1]);
97
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
98
+ throw new Error('--limit must be an integer from 1 to 200.');
99
+ }
100
+ rest.splice(limitIndex, 2);
101
+ }
102
+
103
+ const urlPatternIndex = rest.indexOf('--url-pattern');
104
+ const urlPattern = urlPatternIndex === -1 ? undefined : rest[urlPatternIndex + 1];
105
+ if (urlPatternIndex !== -1) {
106
+ if (!urlPattern || urlPattern.startsWith('--')) {
107
+ throw new Error('--url-pattern requires a value.');
108
+ }
109
+ rest.splice(urlPatternIndex, 2);
110
+ }
111
+
112
+ const deliveryIndex = rest.indexOf('--delivery');
113
+ const rawDelivery = deliveryIndex === -1 ? 'auto' : rest[deliveryIndex + 1];
114
+ if (rawDelivery !== 'inline' && rawDelivery !== 'artifact' && rawDelivery !== 'auto') {
115
+ throw new Error('--delivery must be inline, artifact, or auto.');
116
+ }
117
+ if (deliveryIndex !== -1) rest.splice(deliveryIndex, 2);
118
+
119
+ return { limit, urlPattern, delivery: rawDelivery, rest };
120
+ }
121
+
63
122
  /**
64
123
  * @param {string[]} args
65
124
  * @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
@@ -2,6 +2,9 @@
2
2
 
3
3
  import {
4
4
  getProtocolVersion,
5
+ isBatchSafeBridgeCall,
6
+ MAX_BATCH_CALLS,
7
+ MAX_BATCH_CONCURRENCY,
5
8
  METHODS,
6
9
  summarizeBatchErrorItem,
7
10
  summarizeBatchResponseItem,
@@ -27,6 +30,10 @@ import { requestBridge } from './runtime.js';
27
30
  * }} InvalidBatchItem
28
31
  */
29
32
 
33
+ /**
34
+ * @typedef {{ method: BridgeMethod, params: Record<string, unknown>, tabId: number | null }} PreparedBatchCall
35
+ */
36
+
30
37
  /**
31
38
  * @typedef {ReturnType<typeof summarizeBatchResponseItem>
32
39
  * | ReturnType<typeof summarizeBatchErrorItem>
@@ -59,8 +66,8 @@ function invalidBatchItem(method, message) {
59
66
  }
60
67
 
61
68
  /**
62
- * Parse and execute `bbx batch '[...]'`: validate each call, run them in
63
- * parallel against the bridge, and return per-call summary items.
69
+ * Parse and execute `bbx batch '[...]'`: validate read-only calls, run them
70
+ * with bounded concurrency, and return ordered per-call summary items.
64
71
  *
65
72
  * @param {import('./client.js').BridgeClient} client
66
73
  * @param {string | undefined} input - Raw JSON array argument
@@ -80,61 +87,117 @@ export async function runBatchCalls(client, input, source) {
80
87
  if (!Array.isArray(calls)) {
81
88
  throw new Error('Batch input must be a JSON array.');
82
89
  }
83
- if (!client.connected) {
84
- await client.connect();
90
+ if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
91
+ throw new Error(`Batch input must contain between 1 and ${MAX_BATCH_CALLS} calls.`);
92
+ }
93
+ const prepared = calls.map((call) => prepareBatchCall(call));
94
+ if (prepared.some((item) => 'error' in item)) {
95
+ return prepared.map((item) =>
96
+ 'error' in item
97
+ ? item.error
98
+ : invalidBatchItem(
99
+ item.value.method,
100
+ 'Batch was not executed because another call failed validation.'
101
+ )
102
+ );
103
+ }
104
+ const validPrepared = /** @type {Array<{ value: PreparedBatchCall }>} */ (prepared);
105
+ if (!client.connected) await client.connect();
106
+ return mapWithConcurrency(validPrepared, MAX_BATCH_CONCURRENCY, async (item) => {
107
+ const { method, params, tabId } = item.value;
108
+ const startTime = Date.now();
109
+ try {
110
+ const response = await requestBridge(client, method, params, {
111
+ tabId,
112
+ source,
113
+ });
114
+ return summarizeBatchResponseItem({
115
+ method,
116
+ tabId,
117
+ response,
118
+ durationMs: Date.now() - startTime,
119
+ });
120
+ } catch (err) {
121
+ return summarizeBatchErrorItem({
122
+ method,
123
+ tabId,
124
+ error: err,
125
+ durationMs: Date.now() - startTime,
126
+ });
127
+ }
128
+ });
129
+ }
130
+
131
+ /**
132
+ * @param {unknown} call
133
+ * @returns {{ value: PreparedBatchCall } | { error: InvalidBatchItem }}
134
+ */
135
+ function prepareBatchCall(call) {
136
+ if (!call || typeof call !== 'object' || Array.isArray(call)) {
137
+ return { error: invalidBatchItem('', 'Each batch call needs a method.') };
138
+ }
139
+ const batchCall = /** @type {Record<string, unknown>} */ (call);
140
+ if (typeof batchCall.method !== 'string') {
141
+ return { error: invalidBatchItem('', 'Each batch call needs a method.') };
142
+ }
143
+ if (!METHODS.includes(/** @type {BridgeMethod} */ (batchCall.method))) {
144
+ return {
145
+ error: invalidBatchItem(batchCall.method, `Unknown bridge method "${batchCall.method}".`),
146
+ };
147
+ }
148
+ const method = /** @type {BridgeMethod} */ (batchCall.method);
149
+ if (method === 'sensitive.read') {
150
+ return {
151
+ error: invalidBatchItem(
152
+ method,
153
+ 'sensitive.read is never allowed in batch execution. Call it sequentially.'
154
+ ),
155
+ };
156
+ }
157
+ if (
158
+ batchCall.params !== undefined &&
159
+ (!batchCall.params || typeof batchCall.params !== 'object' || Array.isArray(batchCall.params))
160
+ ) {
161
+ return { error: invalidBatchItem(method, 'Batch call params must be a JSON object.') };
162
+ }
163
+ const params =
164
+ batchCall.params === undefined ? {} : /** @type {Record<string, unknown>} */ (batchCall.params);
165
+ if (!isBatchSafeBridgeCall(method, params)) {
166
+ return {
167
+ error: invalidBatchItem(
168
+ method,
169
+ `${method} is not safe for batch execution. Call it sequentially.`
170
+ ),
171
+ };
85
172
  }
86
- return Promise.all(
87
- calls.map(async (call) => {
88
- if (!call || typeof call !== 'object' || Array.isArray(call)) {
89
- return invalidBatchItem('', 'Each batch call needs a method.');
90
- }
91
- const batchCall = /** @type {Record<string, unknown>} */ (call);
92
- if (typeof batchCall.method !== 'string') {
93
- return invalidBatchItem('', 'Each batch call needs a method.');
94
- }
95
- if (!METHODS.includes(/** @type {BridgeMethod} */ (batchCall.method))) {
96
- return invalidBatchItem(batchCall.method, `Unknown bridge method "${batchCall.method}".`);
97
- }
98
- const method = /** @type {BridgeMethod} */ (batchCall.method);
99
- if (
100
- batchCall.params !== undefined &&
101
- (!batchCall.params ||
102
- typeof batchCall.params !== 'object' ||
103
- Array.isArray(batchCall.params))
104
- ) {
105
- return invalidBatchItem(method, 'Batch call params must be a JSON object.');
106
- }
107
- const params =
108
- batchCall.params === undefined
109
- ? {}
110
- : /** @type {Record<string, unknown>} */ (batchCall.params);
111
- const tabId =
112
- methodNeedsTab(method) &&
113
- typeof batchCall.tabId === 'number' &&
114
- Number.isInteger(batchCall.tabId) &&
115
- batchCall.tabId > 0
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
- );
173
+ const tabId =
174
+ methodNeedsTab(method) &&
175
+ typeof batchCall.tabId === 'number' &&
176
+ Number.isInteger(batchCall.tabId) &&
177
+ batchCall.tabId > 0
178
+ ? batchCall.tabId
179
+ : null;
180
+ return { value: { method, params, tabId } };
181
+ }
182
+
183
+ /**
184
+ * @template T, R
185
+ * @param {T[]} values
186
+ * @param {number} concurrency
187
+ * @param {(value: T) => Promise<R>} callback
188
+ * @returns {Promise<R[]>}
189
+ */
190
+ async function mapWithConcurrency(values, concurrency, callback) {
191
+ /** @type {R[]} */
192
+ const results = new Array(values.length);
193
+ let nextIndex = 0;
194
+ const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
195
+ while (nextIndex < values.length) {
196
+ const index = nextIndex;
197
+ nextIndex += 1;
198
+ results[index] = await callback(values[index]);
199
+ }
200
+ });
201
+ await Promise.all(workers);
202
+ return results;
140
203
  }
@@ -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
  }