@browserbridge/bbx 1.7.8 → 1.8.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 +30 -11
- package/README.md +9 -5
- package/package.json +9 -6
- package/packages/agent-client/src/cli-args.js +24 -0
- package/packages/agent-client/src/cli.js +46 -52
- package/packages/agent-client/src/client.js +140 -5
- package/packages/agent-client/src/command-registry.js +56 -53
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/remotes.js +1 -1
- package/packages/agent-client/src/runtime.js +768 -35
- package/packages/agent-client/src/types.ts +131 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +41 -5
- package/packages/mcp-server/src/handlers-dom.js +8 -2
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +41 -4
- package/packages/mcp-server/src/handlers-utils.js +67 -7
- package/packages/mcp-server/src/handlers.js +4 -4
- package/packages/mcp-server/src/server.js +102 -6
- package/packages/native-host/src/daemon.js +158 -6
- package/packages/native-host/src/native-host.js +3 -1
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/errors.js +54 -1
- package/packages/protocol/src/protocol.js +297 -1
- package/packages/protocol/src/registry.js +66 -35
- package/packages/protocol/src/summary.js +87 -5
- package/packages/protocol/src/types.ts +234 -0
- package/skills/browser-bridge/SKILL.md +34 -13
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +157 -82
- package/skills/browser-bridge/references/token-efficiency.md +19 -13
package/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Browser Bridge (BBX) Privacy Policy
|
|
2
2
|
|
|
3
|
-
Last updated: 2026-
|
|
3
|
+
Last updated: 2026-07-22
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -12,12 +12,21 @@ 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,
|
|
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
|
|
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
|
+
- Current JavaScript dialog information when observable and explicitly requested, including dialog type, bounded message text, and bounded default prompt text
|
|
18
20
|
- `localStorage` and `sessionStorage` values when explicitly requested
|
|
19
21
|
- Screenshots or cropped image captures when explicitly requested
|
|
20
|
-
- User-triggered interaction data such as clicks,
|
|
22
|
+
- 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
|
+
- 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
|
|
24
|
+
|
|
25
|
+
All-resource network inspection excludes request and response bodies, cookies,
|
|
26
|
+
authorization values, and complete headers. It removes URL credentials and
|
|
27
|
+
fragments, replaces query parameter values with a redaction marker, and
|
|
28
|
+
summarizes data/blob URLs before returning them. Browser Bridge does not expose
|
|
29
|
+
these excluded values through that feature.
|
|
21
30
|
|
|
22
31
|
Browser Bridge is designed for local debugging and automation of the browser state you intentionally expose to it.
|
|
23
32
|
|
|
@@ -25,17 +34,17 @@ Browser Bridge is designed for local debugging and automation of the browser sta
|
|
|
25
34
|
|
|
26
35
|
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
36
|
|
|
28
|
-
The
|
|
37
|
+
The normal local path sends request and response data through:
|
|
29
38
|
|
|
30
39
|
1. The Browser Bridge Chrome extension
|
|
31
40
|
2. The Browser Bridge native messaging host running on your machine
|
|
32
|
-
3. The
|
|
41
|
+
3. The agent client or IDE that you intentionally connect
|
|
33
42
|
|
|
34
|
-
Browser Bridge does not operate a Browser Bridge cloud service and does not send extension data to developer-operated remote servers.
|
|
43
|
+
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
44
|
|
|
36
45
|
## Sharing and downstream processing
|
|
37
46
|
|
|
38
|
-
Browser Bridge shares page data only with the
|
|
47
|
+
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
48
|
|
|
40
49
|
Important:
|
|
41
50
|
|
|
@@ -52,8 +61,11 @@ Browser Bridge keeps data only as long as needed for the current local session a
|
|
|
52
61
|
|
|
53
62
|
- Window enablement state is stored in session storage and cleared when disabled or when the browser session ends.
|
|
54
63
|
- 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
|
-
-
|
|
64
|
+
- Console and fetch/XHR network buffers are bounded in memory and cleared when disabled or when the page state is reset.
|
|
65
|
+
- 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.
|
|
66
|
+
- Dialog message/default prompt text is returned to the requesting caller when inspected but is excluded from persisted action-log summaries and size diagnostics.
|
|
67
|
+
- 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
|
+
- 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
69
|
|
|
58
70
|
Browser Bridge does not provide Browser Bridge-hosted accounts, sync, or long-term server-side storage.
|
|
59
71
|
|
|
@@ -72,12 +84,19 @@ Browser Bridge does not load remotely hosted extension code.
|
|
|
72
84
|
|
|
73
85
|
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
86
|
|
|
87
|
+
The same existing Debugger API permission is used only when requested for
|
|
88
|
+
native pointer/text dispatch, JavaScript dialog observation and explicit
|
|
89
|
+
handling, accessibility reads, screenshots, performance/CDP reads, interception,
|
|
90
|
+
and optional all-resource network capture. Browser Bridge does not silently
|
|
91
|
+
accept/dismiss dialogs and does not claim that accepted browser input proves an
|
|
92
|
+
application-level outcome.
|
|
93
|
+
|
|
75
94
|
## Security
|
|
76
95
|
|
|
77
96
|
- Extension code is packaged with the extension; Browser Bridge does not fetch and execute remote extension scripts.
|
|
78
97
|
- Communication with the companion host uses Chrome Native Messaging.
|
|
79
98
|
- Access is limited to the explicitly enabled browser window.
|
|
80
|
-
- The extension includes bounded buffers and
|
|
99
|
+
- 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
100
|
|
|
82
101
|
## Chrome Web Store Limited Use statement
|
|
83
102
|
|
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
|
|
|
@@ -101,7 +101,7 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
|
|
|
101
101
|
|
|
102
102
|
Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, and the [Codex extension](https://chromewebstore.google.com/detail/codex/hehggadaopoacecdllhhajmbjkdcmajg) is a great option if you use Codex, but both are vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
|
|
103
103
|
|
|
104
|
-
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
|
|
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 one explicitly enabled browser window rather than ambient browser control.
|
|
105
105
|
|
|
106
106
|
## Setup
|
|
107
107
|
|
|
@@ -120,9 +120,13 @@ MCP mode is self-contained: the server exposes tools and startup instructions, s
|
|
|
120
120
|
|
|
121
121
|
- The extension is scoped to one explicitly enabled Chrome window at a time - no ambient browser access
|
|
122
122
|
- Requests default to the active tab in that window unless a tab is targeted explicitly
|
|
123
|
-
-
|
|
124
|
-
-
|
|
123
|
+
- 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
|
+
- 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
|
|
125
|
+
- 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
|
|
126
|
+
- 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
|
+
- URL waits observe full navigation and same-document SPA changes, and report the final URL and observed navigation kind
|
|
125
128
|
- Structured DOM/style/layout reads are the primary transport; screenshots are a fallback
|
|
129
|
+
- Browser input dispatch does not prove the application accepted the intended change, so agents should verify with a wait or structured read
|
|
126
130
|
- Open-ended investigation should start with structured reads on a smaller, lower-cost subagent when the client supports delegation
|
|
127
131
|
- The native host daemon auto-starts on demand
|
|
128
132
|
|
|
@@ -139,7 +143,7 @@ MCP mode is self-contained: the server exposes tools and startup instructions, s
|
|
|
139
143
|
|
|
140
144
|
## Privacy
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
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
147
|
|
|
144
148
|
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
149
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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
|
-
"
|
|
59
|
-
"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,15 +74,17 @@
|
|
|
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",
|
|
81
84
|
"c8": "^12.0.0",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
+
"oxfmt": "^0.60.0",
|
|
86
|
+
"oxlint": "^1.75.0",
|
|
87
|
+
"tsx": "^4.23.1",
|
|
85
88
|
"typescript": "^7.0.2"
|
|
86
89
|
},
|
|
87
90
|
"engines": {
|
|
@@ -60,6 +60,30 @@ 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
|
+
|
|
63
87
|
/**
|
|
64
88
|
* @param {string[]} args
|
|
65
89
|
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
17
17
|
import {
|
|
18
18
|
extractRemoteFlag,
|
|
19
|
+
extractScreenshotFlags,
|
|
19
20
|
extractTabFlag,
|
|
20
21
|
parseCallCommand,
|
|
21
22
|
parseInterceptAddArgs,
|
|
@@ -197,10 +198,10 @@ async function runLocalCommand(run) {
|
|
|
197
198
|
*/
|
|
198
199
|
async function createCliClient() {
|
|
199
200
|
try {
|
|
200
|
-
return await createBridgeClientForDestination(
|
|
201
|
-
|
|
202
|
-
clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : {}
|
|
203
|
-
);
|
|
201
|
+
return await createBridgeClientForDestination(remoteDestinationId, {
|
|
202
|
+
checkProtocolOnConnect: false,
|
|
203
|
+
...(clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : {}),
|
|
204
|
+
});
|
|
204
205
|
} catch (error) {
|
|
205
206
|
printJson({
|
|
206
207
|
ok: false,
|
|
@@ -362,62 +363,31 @@ async function main() {
|
|
|
362
363
|
|
|
363
364
|
const shortcutCmd = SHORTCUT_COMMANDS[command];
|
|
364
365
|
if (shortcutCmd) {
|
|
365
|
-
// Allow `bbx <shortcut> --tab <id> ...` to target a specific tab.
|
|
366
|
-
// Without this, --tab gets eaten as a positional argument and the
|
|
367
|
-
// request hits whatever tab the bridge route happens to be pointing
|
|
368
|
-
// at (typically the active tab). For element-resolving shortcuts
|
|
369
|
-
// the ref must be resolved against the SAME tab the action targets,
|
|
370
|
-
// so we pass tabId to both resolveRef and the subsequent request.
|
|
371
366
|
const { tabId, rest: shortcutArgs } = extractTabFlag(rest);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// selector and retry once. This handles the common case where the
|
|
380
|
-
// agent resolved an element, then the page re-rendered (React
|
|
381
|
-
// reconciliation, SPA navigation) before the action was dispatched.
|
|
382
|
-
const canRetry = typeof selectorInput === 'string' && !selectorInput.startsWith('el_');
|
|
383
|
-
const maxAttempts = canRetry ? 2 : 1;
|
|
384
|
-
/** @type {import('./runtime.js').BridgeResponse | undefined} */
|
|
385
|
-
let response;
|
|
386
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
387
|
-
let elementRef;
|
|
388
|
-
if (shortcutCmd.resolve && selectorInput) {
|
|
389
|
-
elementRef = await resolveRef(client, selectorInput, tabId, REQUEST_SOURCE);
|
|
390
|
-
}
|
|
391
|
-
response = await requestBridge(
|
|
392
|
-
client,
|
|
393
|
-
shortcutCmd.method,
|
|
394
|
-
shortcutCmd.build(shortcutArgs, elementRef),
|
|
395
|
-
{ source: REQUEST_SOURCE, tabId }
|
|
396
|
-
);
|
|
397
|
-
const isStale = !response.ok && response.error?.code === 'ELEMENT_STALE';
|
|
398
|
-
if (isStale && attempt < maxAttempts) {
|
|
399
|
-
process.stderr.write(
|
|
400
|
-
`bbx: ELEMENT_STALE on "${selectorInput}", re-resolving and retrying...\n`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
if (!isStale || attempt >= maxAttempts) break;
|
|
404
|
-
}
|
|
405
|
-
if (response) await printSummary(response, shortcutCmd.printMethod);
|
|
367
|
+
const response = await requestBridge(
|
|
368
|
+
client,
|
|
369
|
+
shortcutCmd.method,
|
|
370
|
+
shortcutCmd.build(shortcutArgs),
|
|
371
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
372
|
+
);
|
|
373
|
+
await printSummary(response, shortcutCmd.printMethod);
|
|
406
374
|
return;
|
|
407
375
|
}
|
|
408
376
|
|
|
409
377
|
if (command === 'press-key') {
|
|
410
378
|
const [key, refOrSelector] = rest;
|
|
411
379
|
if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
|
|
412
|
-
const
|
|
413
|
-
?
|
|
380
|
+
const target = refOrSelector
|
|
381
|
+
? refOrSelector.startsWith('el_')
|
|
382
|
+
? { elementRef: refOrSelector }
|
|
383
|
+
: { selector: refOrSelector }
|
|
414
384
|
: undefined;
|
|
415
385
|
const response = await requestBridge(
|
|
416
386
|
client,
|
|
417
387
|
'input.press_key',
|
|
418
388
|
{
|
|
419
389
|
key,
|
|
420
|
-
target
|
|
390
|
+
target,
|
|
421
391
|
},
|
|
422
392
|
{ source: REQUEST_SOURCE }
|
|
423
393
|
);
|
|
@@ -447,15 +417,20 @@ async function main() {
|
|
|
447
417
|
|
|
448
418
|
if (command === 'screenshot') {
|
|
449
419
|
const parsed = extractTabFlag(rest);
|
|
450
|
-
const
|
|
420
|
+
const screenshotOptions = extractScreenshotFlags(parsed.rest);
|
|
421
|
+
const [refOrSelector, outputPath] = screenshotOptions.rest;
|
|
451
422
|
if (!refOrSelector)
|
|
452
|
-
throw new Error(
|
|
423
|
+
throw new Error(
|
|
424
|
+
'Usage: screenshot [--tab <tabId>] [--format png|jpeg|webp] [--quality 0-100] <ref|selector> [path]'
|
|
425
|
+
);
|
|
453
426
|
const elementRef = await resolveRef(client, refOrSelector, parsed.tabId, REQUEST_SOURCE);
|
|
454
427
|
const response = await requestBridge(
|
|
455
428
|
client,
|
|
456
429
|
'screenshot.capture_element',
|
|
457
430
|
{
|
|
458
431
|
elementRef,
|
|
432
|
+
format: screenshotOptions.format,
|
|
433
|
+
quality: screenshotOptions.quality,
|
|
459
434
|
},
|
|
460
435
|
{ tabId: parsed.tabId, source: REQUEST_SOURCE }
|
|
461
436
|
);
|
|
@@ -464,13 +439,32 @@ async function main() {
|
|
|
464
439
|
return;
|
|
465
440
|
}
|
|
466
441
|
const screenshotResult = /** @type {ScreenshotResult} */ (response.result);
|
|
467
|
-
const
|
|
468
|
-
const
|
|
442
|
+
const extension = screenshotOptions.format === 'jpeg' ? 'jpg' : screenshotOptions.format;
|
|
443
|
+
const acceptedExtensions =
|
|
444
|
+
screenshotOptions.format === 'jpeg'
|
|
445
|
+
? new Set(['.jpg', '.jpeg'])
|
|
446
|
+
: new Set([`.${extension}`]);
|
|
447
|
+
let filePath = outputPath || path.join(os.tmpdir(), `bbx-${Date.now()}.${extension}`);
|
|
448
|
+
if (outputPath && !path.extname(outputPath)) filePath = `${outputPath}.${extension}`;
|
|
449
|
+
if (
|
|
450
|
+
outputPath &&
|
|
451
|
+
path.extname(outputPath) &&
|
|
452
|
+
!acceptedExtensions.has(path.extname(outputPath))
|
|
453
|
+
) {
|
|
454
|
+
throw new Error(`Screenshot path extension must match ${screenshotOptions.format} format.`);
|
|
455
|
+
}
|
|
456
|
+
const data = screenshotResult.image.replace(/^data:image\/(?:png|jpeg|webp);base64,/, '');
|
|
469
457
|
await fs.promises.writeFile(filePath, Buffer.from(data, 'base64'));
|
|
470
458
|
printJson({
|
|
471
459
|
ok: true,
|
|
472
460
|
summary: `Screenshot saved to ${filePath}.`,
|
|
473
|
-
evidence: {
|
|
461
|
+
evidence: {
|
|
462
|
+
savedTo: filePath,
|
|
463
|
+
rect: screenshotResult.rect,
|
|
464
|
+
format: screenshotResult.format,
|
|
465
|
+
complete: screenshotResult.complete,
|
|
466
|
+
clipped: screenshotResult.clipped,
|
|
467
|
+
},
|
|
474
468
|
});
|
|
475
469
|
return;
|
|
476
470
|
}
|
|
@@ -6,12 +6,16 @@ import net from 'node:net';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
createFailure,
|
|
9
10
|
createRequest,
|
|
11
|
+
createSuccess,
|
|
10
12
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
13
|
+
ERROR_CODES,
|
|
11
14
|
getProtocolVersion,
|
|
12
15
|
parseJsonLines,
|
|
13
16
|
setProtocolPackageVersion,
|
|
14
17
|
} from '../../protocol/src/index.js';
|
|
18
|
+
import { extractContentFromHtml } from './content-extract.js';
|
|
15
19
|
import {
|
|
16
20
|
createSocketBridgeTransport,
|
|
17
21
|
getBridgeTransport,
|
|
@@ -112,6 +116,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
112
116
|
clientId = `agent_${randomUUID()}`,
|
|
113
117
|
defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
114
118
|
autoReconnect = false,
|
|
119
|
+
checkProtocolOnConnect = true,
|
|
115
120
|
restartDaemonOnVersionMismatch = shouldAutoRestartDaemonOnVersionMismatch(),
|
|
116
121
|
restartDaemonFn = restartBridgeDaemon,
|
|
117
122
|
authToken = undefined,
|
|
@@ -123,6 +128,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
123
128
|
this.clientId = clientId;
|
|
124
129
|
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
125
130
|
this.autoReconnect = autoReconnect;
|
|
131
|
+
this.checkProtocolOnConnect = checkProtocolOnConnect;
|
|
126
132
|
this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
|
|
127
133
|
this.restartDaemonFn = restartDaemonFn;
|
|
128
134
|
this.authToken = authToken;
|
|
@@ -246,6 +252,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
246
252
|
|
|
247
253
|
this.protocolCompatibility = null;
|
|
248
254
|
this.protocolWarning = null;
|
|
255
|
+
if (!this.checkProtocolOnConnect) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
249
258
|
|
|
250
259
|
/** @type {ProtocolHealthResult | null} */
|
|
251
260
|
let healthResult = null;
|
|
@@ -312,6 +321,113 @@ export class BridgeClient extends EventEmitter {
|
|
|
312
321
|
meta,
|
|
313
322
|
});
|
|
314
323
|
|
|
324
|
+
if (method === 'page.extract_content') {
|
|
325
|
+
const params =
|
|
326
|
+
/** @type {import('../../protocol/src/types.js').NormalizedExtractContentParams} */ (
|
|
327
|
+
request.params
|
|
328
|
+
);
|
|
329
|
+
const titleResponse = params.includeMetadata
|
|
330
|
+
? await this.request({ method: 'page.get_state', tabId, meta, timeoutMs })
|
|
331
|
+
: null;
|
|
332
|
+
if (titleResponse && !titleResponse.ok) {
|
|
333
|
+
return {
|
|
334
|
+
...titleResponse,
|
|
335
|
+
id: request.id,
|
|
336
|
+
meta: { ...titleResponse.meta, method },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const titleResult =
|
|
340
|
+
titleResponse?.ok && titleResponse.result && typeof titleResponse.result === 'object'
|
|
341
|
+
? /** @type {Record<string, unknown>} */ (titleResponse.result)
|
|
342
|
+
: {};
|
|
343
|
+
const readSnapshot = () =>
|
|
344
|
+
this.request({
|
|
345
|
+
method: 'dom.get_html',
|
|
346
|
+
tabId,
|
|
347
|
+
params: {
|
|
348
|
+
target: { selector: params.selector ?? 'body' },
|
|
349
|
+
outer: true,
|
|
350
|
+
maxLength: 50_000,
|
|
351
|
+
},
|
|
352
|
+
meta: { ...meta, token_budget: null },
|
|
353
|
+
timeoutMs,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
let snapshotResponse = await readSnapshot();
|
|
357
|
+
if (!snapshotResponse.ok) {
|
|
358
|
+
return {
|
|
359
|
+
...snapshotResponse,
|
|
360
|
+
id: request.id,
|
|
361
|
+
meta: { ...snapshotResponse.meta, method },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
let snapshot = normalizeHtmlSnapshot(snapshotResponse.result);
|
|
365
|
+
if (snapshot.truncated) {
|
|
366
|
+
return createFailure(
|
|
367
|
+
request.id,
|
|
368
|
+
ERROR_CODES.RESULT_TRUNCATED,
|
|
369
|
+
'The bounded HTML snapshot was too large for semantic extraction.',
|
|
370
|
+
{
|
|
371
|
+
selector: params.selector,
|
|
372
|
+
omitted: snapshot.omitted,
|
|
373
|
+
maxLength: 50_000,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
method,
|
|
377
|
+
continuation_hint: 'Retry with a narrower selector.',
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let settlement;
|
|
383
|
+
if (params.consistency === 'settled') {
|
|
384
|
+
const deadline = Date.now() + params.settleTimeoutMs;
|
|
385
|
+
let timedOut = false;
|
|
386
|
+
while (true) {
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
388
|
+
const nextResponse = await readSnapshot();
|
|
389
|
+
if (!nextResponse.ok) {
|
|
390
|
+
return {
|
|
391
|
+
...nextResponse,
|
|
392
|
+
id: request.id,
|
|
393
|
+
meta: { ...nextResponse.meta, method },
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const nextSnapshot = normalizeHtmlSnapshot(nextResponse.result);
|
|
397
|
+
if (nextSnapshot.truncated) {
|
|
398
|
+
return createFailure(
|
|
399
|
+
request.id,
|
|
400
|
+
ERROR_CODES.RESULT_TRUNCATED,
|
|
401
|
+
'The bounded HTML snapshot was too large for semantic extraction.',
|
|
402
|
+
{ selector: params.selector, omitted: nextSnapshot.omitted, maxLength: 50_000 },
|
|
403
|
+
{ method, continuation_hint: 'Retry with a narrower selector.' }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (nextSnapshot.html === snapshot.html) {
|
|
407
|
+
snapshot = nextSnapshot;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
snapshot = nextSnapshot;
|
|
411
|
+
if (Date.now() >= deadline) {
|
|
412
|
+
timedOut = true;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
settlement = { requested: true, quietMs: 100, timedOut };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const result = extractContentFromHtml(snapshot.html, params, {
|
|
420
|
+
title: typeof titleResult.title === 'string' ? titleResult.title : undefined,
|
|
421
|
+
settlement,
|
|
422
|
+
});
|
|
423
|
+
return this.attachProtocolWarning(
|
|
424
|
+
createSuccess(request.id, result, {
|
|
425
|
+
method,
|
|
426
|
+
node_processed: true,
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
315
431
|
const responsePromise = new Promise((resolve, reject) => {
|
|
316
432
|
const timeoutId = setTimeout(() => {
|
|
317
433
|
this.waiting.delete(request.id);
|
|
@@ -419,10 +535,13 @@ export class BridgeClient extends EventEmitter {
|
|
|
419
535
|
? healthResult.supported_versions
|
|
420
536
|
: [];
|
|
421
537
|
const latestRemote = remoteVersions[0];
|
|
538
|
+
const extensionVersions = Array.isArray(healthResult?.extension_supported_versions)
|
|
539
|
+
? healthResult.extension_supported_versions
|
|
540
|
+
: healthResult?.supported_versions;
|
|
422
541
|
if (
|
|
423
542
|
healthResult?.extensionConnected === true &&
|
|
424
|
-
Array.isArray(
|
|
425
|
-
!
|
|
543
|
+
Array.isArray(extensionVersions) &&
|
|
544
|
+
!extensionVersions.includes(getProtocolVersion())
|
|
426
545
|
) {
|
|
427
546
|
return false;
|
|
428
547
|
}
|
|
@@ -466,9 +585,11 @@ export class BridgeClient extends EventEmitter {
|
|
|
466
585
|
* @returns {{ compatible: boolean, localVersion: string, remoteVersions: string[], warning?: string }}
|
|
467
586
|
*/
|
|
468
587
|
static checkProtocolVersion(healthResult) {
|
|
469
|
-
const remoteVersions = Array.isArray(healthResult?.
|
|
470
|
-
? healthResult.
|
|
471
|
-
:
|
|
588
|
+
const remoteVersions = Array.isArray(healthResult?.extension_supported_versions)
|
|
589
|
+
? healthResult.extension_supported_versions
|
|
590
|
+
: Array.isArray(healthResult?.supported_versions)
|
|
591
|
+
? healthResult.supported_versions
|
|
592
|
+
: [];
|
|
472
593
|
if (remoteVersions.length === 0) {
|
|
473
594
|
return {
|
|
474
595
|
compatible: true,
|
|
@@ -520,3 +641,17 @@ export class BridgeClient extends EventEmitter {
|
|
|
520
641
|
};
|
|
521
642
|
}
|
|
522
643
|
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @param {unknown} result
|
|
647
|
+
* @returns {{ html: string, truncated: boolean, omitted: number }}
|
|
648
|
+
*/
|
|
649
|
+
function normalizeHtmlSnapshot(result) {
|
|
650
|
+
const value =
|
|
651
|
+
result && typeof result === 'object' ? /** @type {Record<string, unknown>} */ (result) : {};
|
|
652
|
+
return {
|
|
653
|
+
html: typeof value.html === 'string' ? value.html : '',
|
|
654
|
+
truncated: value.truncated === true,
|
|
655
|
+
omitted: typeof value.omitted === 'number' ? value.omitted : 0,
|
|
656
|
+
};
|
|
657
|
+
}
|