@browserbridge/bbx 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +6 -6
- package/packages/agent-client/src/cli.js +133 -14
- package/packages/agent-client/src/command-registry.js +24 -2
- package/packages/agent-client/src/runtime.js +15 -0
- package/packages/mcp-server/src/guidance.js +15 -11
- package/packages/mcp-server/src/server.js +1 -1
- package/packages/native-host/src/install-manifest.js +34 -0
- package/packages/protocol/src/capabilities.js +10 -0
- package/packages/protocol/src/protocol.js +6 -0
- package/packages/protocol/src/registry.js +43 -0
- package/packages/protocol/src/types.ts +12 -1
- package/skills/browser-bridge/SKILL.md +19 -5
- package/skills/browser-bridge/references/interaction.md +47 -0
- package/skills/browser-bridge/references/protocol.md +6 -0
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
|
|
|
112
112
|
5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
|
|
113
113
|
6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the installed Browser Bridge skill in CLI mode (`/browser-bridge`, `browser-bridge`, or the client-specific skill trigger)
|
|
114
114
|
|
|
115
|
+
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
|
+
|
|
115
117
|
MCP mode is self-contained: the server exposes tools, startup instructions, and prompt templates, so a separate CLI skill is not required for MCP guidance.
|
|
116
118
|
|
|
117
119
|
## How it works
|
|
@@ -129,6 +131,7 @@ MCP mode is self-contained: the server exposes tools, startup instructions, and
|
|
|
129
131
|
- [Quickstart](https://github.com/koltyakov/browser-bridge/blob/main/docs/quickstart.md)
|
|
130
132
|
- [Usage scenarios](https://github.com/koltyakov/browser-bridge/blob/main/docs/usage-scenarios.md)
|
|
131
133
|
- [Manual setup](https://github.com/koltyakov/browser-bridge/blob/main/docs/manual-setup.md)
|
|
134
|
+
- [Agent permissions](https://github.com/koltyakov/browser-bridge/blob/main/docs/agent-permissions.md)
|
|
132
135
|
- [CLI guide](https://github.com/koltyakov/browser-bridge/blob/main/docs/cli-guide.md)
|
|
133
136
|
- [MCP vs CLI](https://github.com/koltyakov/browser-bridge/blob/main/docs/mcp-vs-cli.md)
|
|
134
137
|
- [Troubleshooting](https://github.com/koltyakov/browser-bridge/blob/main/docs/troubleshooting.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -69,15 +69,15 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
72
|
-
"zod": "^4.3
|
|
72
|
+
"zod": "^4.4.3"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@types/chrome": "^0.1.
|
|
76
|
-
"@types/node": "^25.
|
|
75
|
+
"@types/chrome": "^0.1.43",
|
|
76
|
+
"@types/node": "^25.9.2",
|
|
77
77
|
"c8": "^11.0.0",
|
|
78
78
|
"linkedom": "^0.18.12",
|
|
79
|
-
"oxfmt": "^0.
|
|
80
|
-
"oxlint": "^1.
|
|
79
|
+
"oxfmt": "^0.54.0",
|
|
80
|
+
"oxlint": "^1.69.0",
|
|
81
81
|
"typescript": "^6.0.3"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|
|
@@ -476,6 +476,23 @@ async function main() {
|
|
|
476
476
|
return;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
if (command === 'tab-activate') {
|
|
480
|
+
const [tabId] = rest;
|
|
481
|
+
if (!tabId) {
|
|
482
|
+
throw new Error('Usage: tab-activate <tabId>');
|
|
483
|
+
}
|
|
484
|
+
const response = await requestBridge(
|
|
485
|
+
client,
|
|
486
|
+
'tabs.activate',
|
|
487
|
+
{
|
|
488
|
+
tabId: parseIntArg(tabId, 'tabId'),
|
|
489
|
+
},
|
|
490
|
+
{ source: REQUEST_SOURCE }
|
|
491
|
+
);
|
|
492
|
+
await printSummary(response);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
479
496
|
if (command === 'call') {
|
|
480
497
|
const { tabId, method, params } = await parseCallCommand(rest);
|
|
481
498
|
const response = await requestBridge(client, method, params, {
|
|
@@ -580,18 +597,47 @@ async function main() {
|
|
|
580
597
|
|
|
581
598
|
const shortcutCmd = SHORTCUT_COMMANDS[command];
|
|
582
599
|
if (shortcutCmd) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
600
|
+
// Allow `bbx <shortcut> --tab <id> ...` to target a specific tab.
|
|
601
|
+
// Without this, --tab gets eaten as a positional argument and the
|
|
602
|
+
// request hits whatever tab the bridge route happens to be pointing
|
|
603
|
+
// at (typically the active tab). For element-resolving shortcuts
|
|
604
|
+
// the ref must be resolved against the SAME tab the action targets,
|
|
605
|
+
// so we pass tabId to both resolveRef and the subsequent request.
|
|
606
|
+
const { tabId, rest: shortcutArgs } = extractTabFlag(rest);
|
|
607
|
+
const selectorInput = shortcutCmd.resolve ? shortcutArgs[0] : null;
|
|
608
|
+
if (shortcutCmd.resolve && !selectorInput) {
|
|
609
|
+
throw new Error(`Usage: ${command} <ref|selector>`);
|
|
587
610
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
)
|
|
594
|
-
|
|
611
|
+
|
|
612
|
+
// Retry-on-stale: if the action fails with ELEMENT_STALE and the
|
|
613
|
+
// original input was a selector (not an el_xxx ref), re-resolve the
|
|
614
|
+
// selector and retry once. This handles the common case where the
|
|
615
|
+
// agent resolved an element, then the page re-rendered (React
|
|
616
|
+
// reconciliation, SPA navigation) before the action was dispatched.
|
|
617
|
+
const canRetry = typeof selectorInput === 'string' && !selectorInput.startsWith('el_');
|
|
618
|
+
const maxAttempts = canRetry ? 2 : 1;
|
|
619
|
+
/** @type {import('./runtime.js').BridgeResponse | undefined} */
|
|
620
|
+
let response;
|
|
621
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
622
|
+
let elementRef;
|
|
623
|
+
if (shortcutCmd.resolve && selectorInput) {
|
|
624
|
+
elementRef = await resolveRef(client, selectorInput, tabId, REQUEST_SOURCE);
|
|
625
|
+
}
|
|
626
|
+
response = await requestBridge(
|
|
627
|
+
client,
|
|
628
|
+
shortcutCmd.method,
|
|
629
|
+
shortcutCmd.build(shortcutArgs, elementRef),
|
|
630
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
631
|
+
);
|
|
632
|
+
const isStale = !response.ok && response.error?.code === 'ELEMENT_STALE';
|
|
633
|
+
if (isStale && attempt < maxAttempts) {
|
|
634
|
+
process.stderr.write(
|
|
635
|
+
`bbx: ELEMENT_STALE on "${selectorInput}", re-resolving and retrying...\n`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (!isStale || attempt >= maxAttempts) break;
|
|
639
|
+
}
|
|
640
|
+
if (response) await printSummary(response, shortcutCmd.printMethod);
|
|
595
641
|
return;
|
|
596
642
|
}
|
|
597
643
|
|
|
@@ -664,19 +710,92 @@ async function main() {
|
|
|
664
710
|
return;
|
|
665
711
|
}
|
|
666
712
|
|
|
713
|
+
if (command === 'intercept') {
|
|
714
|
+
const { tabId, rest: iArgs } = extractTabFlag(rest);
|
|
715
|
+
const sub = iArgs[0];
|
|
716
|
+
if (sub === 'add') {
|
|
717
|
+
const pattern = iArgs[1];
|
|
718
|
+
if (!pattern)
|
|
719
|
+
throw new Error(
|
|
720
|
+
'Usage: intercept add <urlPattern> [--respond <body>] [--status <code>] [--block]'
|
|
721
|
+
);
|
|
722
|
+
const isBlock = iArgs.includes('--block');
|
|
723
|
+
const statusIdx = iArgs.indexOf('--status');
|
|
724
|
+
const statusCode = statusIdx !== -1 ? parseIntArg(iArgs[statusIdx + 1], 'status') : 200;
|
|
725
|
+
const respondIdx = iArgs.indexOf('--respond');
|
|
726
|
+
const body =
|
|
727
|
+
respondIdx !== -1
|
|
728
|
+
? iArgs
|
|
729
|
+
.slice(respondIdx + 1)
|
|
730
|
+
.filter((a) => !a.startsWith('--'))
|
|
731
|
+
.join(' ')
|
|
732
|
+
: undefined;
|
|
733
|
+
const response = await requestBridge(
|
|
734
|
+
client,
|
|
735
|
+
'network.intercept.add',
|
|
736
|
+
{
|
|
737
|
+
urlPattern: pattern,
|
|
738
|
+
action: isBlock ? 'block' : body != null ? 'fulfill' : 'continue',
|
|
739
|
+
statusCode,
|
|
740
|
+
body,
|
|
741
|
+
},
|
|
742
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
743
|
+
);
|
|
744
|
+
await printSummary(response);
|
|
745
|
+
} else if (sub === 'remove') {
|
|
746
|
+
const ruleId = iArgs[1];
|
|
747
|
+
if (!ruleId) throw new Error('Usage: intercept remove <ruleId>');
|
|
748
|
+
const response = await requestBridge(
|
|
749
|
+
client,
|
|
750
|
+
'network.intercept.remove',
|
|
751
|
+
{ ruleId },
|
|
752
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
753
|
+
);
|
|
754
|
+
await printSummary(response);
|
|
755
|
+
} else if (sub === 'list') {
|
|
756
|
+
const response = await requestBridge(
|
|
757
|
+
client,
|
|
758
|
+
'network.intercept.list',
|
|
759
|
+
{},
|
|
760
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
761
|
+
);
|
|
762
|
+
await printSummary(response);
|
|
763
|
+
} else if (sub === 'clear') {
|
|
764
|
+
const response = await requestBridge(
|
|
765
|
+
client,
|
|
766
|
+
'network.intercept.clear',
|
|
767
|
+
{},
|
|
768
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
769
|
+
);
|
|
770
|
+
await printSummary(response);
|
|
771
|
+
} else {
|
|
772
|
+
throw new Error('Usage: intercept <add|remove|list|clear> [args]');
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
667
777
|
if (command === 'eval') {
|
|
668
|
-
|
|
778
|
+
const { tabId, rest: evalArgs } = extractTabFlag(rest);
|
|
779
|
+
// --await: wait for the result if the expression returns a Promise
|
|
780
|
+
const awaitIndex = evalArgs.indexOf('--await');
|
|
781
|
+
const awaitPromise = awaitIndex !== -1;
|
|
782
|
+
if (awaitIndex !== -1) evalArgs.splice(awaitIndex, 1);
|
|
783
|
+
|
|
784
|
+
let expression = evalArgs.join(' ');
|
|
669
785
|
if (!expression || expression === '-') expression = await readStdin();
|
|
670
786
|
if (!expression)
|
|
671
|
-
throw new Error(
|
|
787
|
+
throw new Error(
|
|
788
|
+
'Usage: eval [--tab <id>] [--await] <expression> (or pipe via stdin: echo "expr" | bbx eval -)'
|
|
789
|
+
);
|
|
672
790
|
const response = await requestBridge(
|
|
673
791
|
client,
|
|
674
792
|
'page.evaluate',
|
|
675
793
|
{
|
|
676
794
|
expression,
|
|
677
795
|
returnByValue: true,
|
|
796
|
+
...(awaitPromise ? { awaitPromise: true } : {}),
|
|
678
797
|
},
|
|
679
|
-
{ source: REQUEST_SOURCE }
|
|
798
|
+
{ source: REQUEST_SOURCE, tabId }
|
|
680
799
|
);
|
|
681
800
|
await printSummary(response);
|
|
682
801
|
return;
|
|
@@ -76,6 +76,19 @@ export const SHORTCUT_COMMANDS = {
|
|
|
76
76
|
(r, ref) => ({ target: { elementRef: ref }, text: r.slice(1).join(' ') }),
|
|
77
77
|
{ resolve: true }
|
|
78
78
|
),
|
|
79
|
+
fill: createShortcutCommand(
|
|
80
|
+
'input.fill',
|
|
81
|
+
'bbx fill <ref|selector> <value>',
|
|
82
|
+
(r, ref) => ({
|
|
83
|
+
target: { elementRef: ref },
|
|
84
|
+
value: r.slice(1).join(' '),
|
|
85
|
+
mode: 'auto',
|
|
86
|
+
}),
|
|
87
|
+
{
|
|
88
|
+
resolve: true,
|
|
89
|
+
description: 'Set input/textarea value (React/Vue/Angular-safe, auto fallback to keystrokes)',
|
|
90
|
+
}
|
|
91
|
+
),
|
|
79
92
|
hover: createShortcutCommand(
|
|
80
93
|
'input.hover',
|
|
81
94
|
'bbx hover <ref|selector>',
|
|
@@ -249,6 +262,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
249
262
|
'bbx tabs List available tabs',
|
|
250
263
|
'bbx tab-create [url] Create a new tab',
|
|
251
264
|
'bbx tab-close <tabId> Close a tab',
|
|
265
|
+
'bbx tab-activate <tabId> Bring a tab to the foreground',
|
|
252
266
|
'bbx skill Runtime budget presets and method groups',
|
|
253
267
|
'bbx mcp serve Start Browser Bridge as an MCP stdio server',
|
|
254
268
|
],
|
|
@@ -261,6 +275,10 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
261
275
|
"bbx batch '[{method,params,tabId?},...]' Parallel method calls",
|
|
262
276
|
'Advanced bridge params stay available through `bbx call`, even when shortcuts expose only the common case.',
|
|
263
277
|
'For open-ended investigation, start with `bbx batch` on `page.get_state`, `dom.query`, and `page.get_text` before any screenshot or CDP call.',
|
|
278
|
+
'',
|
|
279
|
+
'Interaction methods need a target wrapper (not a bare ref):',
|
|
280
|
+
' bbx call input.click \'{"target":{"elementRef":"el_xxx"}}\'',
|
|
281
|
+
' bbx call input.type \'{"target":{"elementRef":"el_xxx"},"text":"hello"}\'',
|
|
264
282
|
],
|
|
265
283
|
},
|
|
266
284
|
{
|
|
@@ -294,7 +312,11 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
294
312
|
{
|
|
295
313
|
title: 'Page',
|
|
296
314
|
lines: [
|
|
297
|
-
'bbx
|
|
315
|
+
'bbx intercept add <pattern> [--respond <body>] [--block] Add a request interception rule',
|
|
316
|
+
'bbx intercept list List active interception rules',
|
|
317
|
+
'bbx intercept remove <ruleId> Remove a rule',
|
|
318
|
+
'bbx intercept clear Remove all rules',
|
|
319
|
+
'bbx eval [--await] <expression> Evaluate JS in page context (--await for async, - for stdin)',
|
|
298
320
|
...[
|
|
299
321
|
'console',
|
|
300
322
|
'network',
|
|
@@ -316,7 +338,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
316
338
|
{
|
|
317
339
|
title: 'Interact',
|
|
318
340
|
lines: [
|
|
319
|
-
...['click', 'focus', 'type', 'hover'].map(
|
|
341
|
+
...['click', 'focus', 'type', 'fill', 'hover'].map(
|
|
320
342
|
(command) =>
|
|
321
343
|
`${SHORTCUT_COMMANDS[command].usage.padEnd(64)} ${SHORTCUT_COMMANDS[command].description}`
|
|
322
344
|
),
|
|
@@ -21,6 +21,9 @@ import { BridgeClient } from './client.js';
|
|
|
21
21
|
/** @typedef {import('./types.js').DoctorReport} DoctorReport */
|
|
22
22
|
/** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
|
|
23
23
|
|
|
24
|
+
const CHROMIUM_SANDBOXED_MANIFEST_RE =
|
|
25
|
+
/(?:^|[/\\])(?:snap[/\\]chromium|\.var[/\\]app[/\\]org\.chromium\.Chromium)[/\\]/;
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* @param {BridgeClient} client
|
|
26
29
|
* @returns {Promise<void>}
|
|
@@ -164,6 +167,12 @@ export async function getDoctorReport(options = {}) {
|
|
|
164
167
|
|
|
165
168
|
const browserManifests = await (options.checkBrowserManifests || checkBrowserManifests)();
|
|
166
169
|
const manifestInstalled = Boolean(manifest) || browserManifests.some((b) => b.installed);
|
|
170
|
+
const chromiumSandboxedManifestInstalled = browserManifests.some(
|
|
171
|
+
(entry) =>
|
|
172
|
+
entry.installed &&
|
|
173
|
+
entry.browser === 'chromium' &&
|
|
174
|
+
CHROMIUM_SANDBOXED_MANIFEST_RE.test(entry.manifestPath)
|
|
175
|
+
);
|
|
167
176
|
|
|
168
177
|
/** @type {DoctorReport} */
|
|
169
178
|
const report = {
|
|
@@ -228,6 +237,12 @@ export async function getDoctorReport(options = {}) {
|
|
|
228
237
|
}
|
|
229
238
|
if (report.daemonReachable && !report.extensionConnected) {
|
|
230
239
|
report.issues.push('extension_disconnected');
|
|
240
|
+
if (chromiumSandboxedManifestInstalled) {
|
|
241
|
+
report.issues.push('chromium_sandboxed_native_host_limited');
|
|
242
|
+
report.nextSteps.push(
|
|
243
|
+
"Detected a sandboxed Chromium native host manifest for snap or Flatpak. Sandboxed Chromium may not be able to launch Browser Bridge's Node-based native host; use Google Chrome, Brave, or Edge from a non-sandboxed package and run `bbx install --browser <browser>`."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
231
246
|
report.nextSteps.push(
|
|
232
247
|
'Open Chrome and make sure the Browser Bridge extension is installed and active.'
|
|
233
248
|
);
|
|
@@ -7,12 +7,14 @@ import * as z from 'zod/v4';
|
|
|
7
7
|
|
|
8
8
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
9
9
|
"Browser Bridge MCP inspects and interacts with the user's real Chrome tab through typed MCP tools.",
|
|
10
|
+
'In permission-ask hosts, use browser_call as the default Browser Bridge MCP tool so the user can approve one BBX tool instead of separate browser_status, browser_page, browser_dom, browser_input, and other tools.',
|
|
10
11
|
'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
|
|
11
|
-
'
|
|
12
|
-
'Use structured reads first:
|
|
12
|
+
'Start with browser_call method health.ping. If window access is disabled, call browser_call method access.request once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
|
|
13
|
+
'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, layout.get_box_model, or batch. Keep budgets quick or normal before widening.',
|
|
13
14
|
'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
|
|
14
|
-
'Escalate to
|
|
15
|
-
'Use
|
|
15
|
+
'Escalate to screenshot.capture_element, screenshot.capture_region, accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
|
|
16
|
+
'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
|
|
17
|
+
'Only use the specialized Browser Bridge MCP tools directly when the host has already allowed them or the user explicitly wants typed tool calls.',
|
|
16
18
|
].join('\n');
|
|
17
19
|
|
|
18
20
|
export const MCP_GUIDANCE_PROMPT_NAMES = Object.freeze([
|
|
@@ -97,13 +99,15 @@ function createGuidePrompt() {
|
|
|
97
99
|
'Use Browser Bridge MCP for this browser task.',
|
|
98
100
|
'',
|
|
99
101
|
'Rules:',
|
|
100
|
-
'1. Prefer MCP
|
|
101
|
-
'2.
|
|
102
|
-
'3. Start with
|
|
103
|
-
'4.
|
|
104
|
-
'5.
|
|
105
|
-
'6.
|
|
106
|
-
'7.
|
|
102
|
+
'1. Prefer MCP over `bbx`; do not shell out unless setup, doctor, logs, or raw CLI debugging is explicitly needed.',
|
|
103
|
+
'2. In permission-ask hosts, use `browser_call` as the default tool so the user can approve one BBX MCP tool instead of separate tools for status, page, DOM, input, and patches.',
|
|
104
|
+
'3. Start with `browser_call` method `health.ping`. If access is disabled, call `browser_call` method `access.request` once, ask the user to click Enable, then retry once.',
|
|
105
|
+
'4. Start with structured reads via `browser_call`: `page.get_state`, `dom.query`, `dom.find_by_text`, `dom.find_by_role`, `styles.get_computed`, and `batch`.',
|
|
106
|
+
'5. Keep budgets tight with `budgetPreset: "quick"` or `"normal"`; widen only when results are truncated.',
|
|
107
|
+
'6. Reuse `elementRef` values returned by DOM tools instead of rescanning.',
|
|
108
|
+
'7. Escalate to screenshots, accessibility tree, `page.evaluate`, viewport resize, or CDP only when structured reads cannot answer.',
|
|
109
|
+
'8. Use `patch.apply_styles` or `patch.apply_dom` for temporary experiments and rollback before finishing unless the user asks to keep patches.',
|
|
110
|
+
'9. Only use specialized Browser Bridge MCP tools directly when the host has already allowed them or the user explicitly wants typed tool calls.',
|
|
107
111
|
'',
|
|
108
112
|
'Return concise findings with evidence. Edit source code only after the live page behavior is understood.',
|
|
109
113
|
].join('\n')
|
|
@@ -656,7 +656,7 @@ export function createBridgeMcpServer() {
|
|
|
656
656
|
{
|
|
657
657
|
title: 'Raw Browser Bridge Call',
|
|
658
658
|
description:
|
|
659
|
-
'
|
|
659
|
+
'Primary Browser Bridge tool for permission-ask hosts: call any bridge method directly by name so the user can approve one BBX MCP tool instead of each specialized tool separately.',
|
|
660
660
|
inputSchema: {
|
|
661
661
|
method: z.string().describe('Bridge method name (e.g., "dom.query", "input.click")'),
|
|
662
662
|
params: z
|
|
@@ -19,6 +19,10 @@ export const DEFAULT_EXTENSION_ID_ENV = 'BROWSER_BRIDGE_EXTENSION_ID';
|
|
|
19
19
|
export const BUILT_IN_EXTENSION_ID_SOURCE = 'built_in';
|
|
20
20
|
export const INSTALL_NATIVE_MANIFEST_ERROR = 'INSTALL_NATIVE_MANIFEST_FAILED';
|
|
21
21
|
|
|
22
|
+
const CHROMIUM_SNAP_NATIVE_MESSAGING_RE = /(?:^|[/\\])snap[/\\]chromium[/\\]/;
|
|
23
|
+
const CHROMIUM_FLATPAK_NATIVE_MESSAGING_RE =
|
|
24
|
+
/(?:^|[/\\])\.var[/\\]app[/\\]org\.chromium\.Chromium[/\\]/;
|
|
25
|
+
|
|
22
26
|
/** @typedef {import('./config.js').SupportedBrowser} SupportedBrowser */
|
|
23
27
|
/** @typedef {'env' | 'built_in' | 'none' | 'invalid_env'} ExtensionIdSource */
|
|
24
28
|
/** @typedef {NodeJS.ErrnoException & { cause?: unknown }} MaybeErrnoError */
|
|
@@ -360,6 +364,18 @@ exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
|
|
|
360
364
|
);
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
if (isChromiumSnapManifestInstall(browser, installDir)) {
|
|
368
|
+
stderr.write(
|
|
369
|
+
'Compatibility warning: detected a Linux Chromium snap native messaging path. Strict snap Chromium commonly blocks launching Browser Bridge\'s Node-based native host with AppArmor, which leaves `bbx status` at "Extension: disconnected". Use a non-snap Chromium-based browser such as Google Chrome, Brave, or Edge and run `bbx install --browser <browser>`.\n'
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isChromiumFlatpakManifestInstall(browser, installDir)) {
|
|
374
|
+
stderr.write(
|
|
375
|
+
'Compatibility warning: detected a Linux Chromium Flatpak native messaging path. Sandboxed Flatpak Chromium commonly blocks launching Browser Bridge\'s Node-based native host, which leaves `bbx status` at "Extension: disconnected". Use a non-sandboxed Chromium-based browser such as Google Chrome, Brave, or Edge and run `bbx install --browser <browser>`.\n'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
363
379
|
const hasPlaceholder = allowedOrigins.some((origin) =>
|
|
364
380
|
origin.includes('__REPLACE_WITH_EXTENSION_ID__')
|
|
365
381
|
);
|
|
@@ -378,6 +394,24 @@ exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
|
|
|
378
394
|
};
|
|
379
395
|
}
|
|
380
396
|
|
|
397
|
+
/**
|
|
398
|
+
* @param {SupportedBrowser | undefined} browser
|
|
399
|
+
* @param {string} installDir
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
402
|
+
function isChromiumSnapManifestInstall(browser, installDir) {
|
|
403
|
+
return browser === 'chromium' && CHROMIUM_SNAP_NATIVE_MESSAGING_RE.test(installDir);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {SupportedBrowser | undefined} browser
|
|
408
|
+
* @param {string} installDir
|
|
409
|
+
* @returns {boolean}
|
|
410
|
+
*/
|
|
411
|
+
function isChromiumFlatpakManifestInstall(browser, installDir) {
|
|
412
|
+
return browser === 'chromium' && CHROMIUM_FLATPAK_NATIVE_MESSAGING_RE.test(installDir);
|
|
413
|
+
}
|
|
414
|
+
|
|
381
415
|
/**
|
|
382
416
|
* @param {UninstallManifestOptions} [options={}]
|
|
383
417
|
* @returns {Promise<{ manifestPath: string, bridgeDir: string, removedManifest: boolean, removedBridgeDir: boolean }>}
|
|
@@ -22,6 +22,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
22
22
|
TABS_MANAGE: 'tabs.manage',
|
|
23
23
|
PERFORMANCE_READ: 'performance.read',
|
|
24
24
|
NETWORK_READ: 'network.read',
|
|
25
|
+
NETWORK_INTERCEPT: 'network.intercept',
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export const DEFAULT_CAPABILITIES = Object.freeze([
|
|
@@ -43,6 +44,7 @@ export const DEFAULT_CAPABILITIES = Object.freeze([
|
|
|
43
44
|
CAPABILITIES.TABS_MANAGE,
|
|
44
45
|
CAPABILITIES.PERFORMANCE_READ,
|
|
45
46
|
CAPABILITIES.NETWORK_READ,
|
|
47
|
+
CAPABILITIES.NETWORK_INTERCEPT,
|
|
46
48
|
]);
|
|
47
49
|
|
|
48
50
|
/** @type {Readonly<Record<CapabilityMethod, Capability | null>>} */
|
|
@@ -51,6 +53,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
51
53
|
'tabs.list': null,
|
|
52
54
|
'tabs.create': CAPABILITIES.TABS_MANAGE,
|
|
53
55
|
'tabs.close': CAPABILITIES.TABS_MANAGE,
|
|
56
|
+
'tabs.activate': CAPABILITIES.TABS_MANAGE,
|
|
54
57
|
'skill.get_runtime_context': null,
|
|
55
58
|
'setup.get_status': null,
|
|
56
59
|
'setup.install': null,
|
|
@@ -61,6 +64,12 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
61
64
|
'page.get_storage': CAPABILITIES.PAGE_READ,
|
|
62
65
|
'page.get_text': CAPABILITIES.PAGE_READ,
|
|
63
66
|
'page.get_network': CAPABILITIES.NETWORK_READ,
|
|
67
|
+
// Interception mutates traffic (block/forge responses) — distinct from
|
|
68
|
+
// passive network reads so hosts can gate it independently.
|
|
69
|
+
'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
|
|
70
|
+
'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
|
|
71
|
+
'network.intercept.list': CAPABILITIES.NETWORK_INTERCEPT,
|
|
72
|
+
'network.intercept.clear': CAPABILITIES.NETWORK_INTERCEPT,
|
|
64
73
|
'navigation.navigate': CAPABILITIES.NAVIGATION_CONTROL,
|
|
65
74
|
'navigation.reload': CAPABILITIES.NAVIGATION_CONTROL,
|
|
66
75
|
'navigation.go_back': CAPABILITIES.NAVIGATION_CONTROL,
|
|
@@ -83,6 +92,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
|
|
|
83
92
|
'input.click': CAPABILITIES.AUTOMATION_INPUT,
|
|
84
93
|
'input.focus': CAPABILITIES.AUTOMATION_INPUT,
|
|
85
94
|
'input.type': CAPABILITIES.AUTOMATION_INPUT,
|
|
95
|
+
'input.fill': CAPABILITIES.AUTOMATION_INPUT,
|
|
86
96
|
'input.press_key': CAPABILITIES.AUTOMATION_INPUT,
|
|
87
97
|
'input.set_checked': CAPABILITIES.AUTOMATION_INPUT,
|
|
88
98
|
'input.select_option': CAPABILITIES.AUTOMATION_INPUT,
|
|
@@ -380,6 +380,10 @@ function normalizeTarget(target) {
|
|
|
380
380
|
*/
|
|
381
381
|
export function normalizeInputAction(params = {}) {
|
|
382
382
|
const button = params.button === 'middle' || params.button === 'right' ? params.button : 'left';
|
|
383
|
+
const mode =
|
|
384
|
+
params.mode === 'setter' || params.mode === 'keystrokes' || params.mode === 'auto'
|
|
385
|
+
? params.mode
|
|
386
|
+
: 'auto';
|
|
383
387
|
|
|
384
388
|
return {
|
|
385
389
|
target: normalizeTarget(
|
|
@@ -390,6 +394,8 @@ export function normalizeInputAction(params = {}) {
|
|
|
390
394
|
button,
|
|
391
395
|
clickCount: clampInt(params.clickCount, 1, 2, 1),
|
|
392
396
|
text: typeof params.text === 'string' ? params.text : '',
|
|
397
|
+
value: typeof params.value === 'string' ? params.value : '',
|
|
398
|
+
mode,
|
|
393
399
|
clear: Boolean(params.clear),
|
|
394
400
|
submit: Boolean(params.submit),
|
|
395
401
|
key: typeof params.key === 'string' ? params.key : '',
|
|
@@ -28,6 +28,7 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
28
28
|
'tabs.list': 'List tabs in the enabled window.',
|
|
29
29
|
'tabs.create': 'Create a new tab in the enabled window.',
|
|
30
30
|
'tabs.close': 'Close a tab in the enabled window.',
|
|
31
|
+
'tabs.activate': 'Bring a tab to the foreground in the enabled window.',
|
|
31
32
|
'skill.get_runtime_context': 'Return runtime method groups, budgets, and limits.',
|
|
32
33
|
'setup.get_status': 'Return MCP and skill setup status.',
|
|
33
34
|
'setup.install': 'Install or uninstall MCP or skill integration targets.',
|
|
@@ -38,6 +39,10 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
38
39
|
'page.get_storage': 'Read local or session storage values.',
|
|
39
40
|
'page.get_text': 'Read bounded visible text from the page.',
|
|
40
41
|
'page.get_network': 'Read buffered fetch and XHR network activity.',
|
|
42
|
+
'network.intercept.add': 'Add a request interception rule (CDP Fetch domain).',
|
|
43
|
+
'network.intercept.remove': 'Remove a request interception rule by ID.',
|
|
44
|
+
'network.intercept.list': 'List active interception rules.',
|
|
45
|
+
'network.intercept.clear': 'Remove all interception rules and disable interception.',
|
|
41
46
|
'navigation.navigate': 'Navigate the current tab to a URL.',
|
|
42
47
|
'navigation.reload': 'Reload the current tab.',
|
|
43
48
|
'navigation.go_back': 'Navigate backward in tab history.',
|
|
@@ -60,6 +65,7 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
60
65
|
'input.click': 'Click an element.',
|
|
61
66
|
'input.focus': 'Focus an element.',
|
|
62
67
|
'input.type': 'Type text into an element.',
|
|
68
|
+
'input.fill': 'Set value of an input/textarea element (React/Vue/Angular-safe).',
|
|
63
69
|
'input.press_key': 'Send a key press to the page or an element.',
|
|
64
70
|
'input.set_checked': 'Set checkbox or radio checked state.',
|
|
65
71
|
'input.select_option': 'Select options in a select element.',
|
|
@@ -130,6 +136,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
130
136
|
'tabs.list': createRegistryEntry('tabs.list', 'tabs', false, [], 'trivial'),
|
|
131
137
|
'tabs.create': createRegistryEntry('tabs.create', 'tabs', false, ['url', 'active'], 'trivial'),
|
|
132
138
|
'tabs.close': createRegistryEntry('tabs.close', 'tabs', false, ['tabId'], 'trivial'),
|
|
139
|
+
'tabs.activate': createRegistryEntry('tabs.activate', 'tabs', false, ['tabId'], 'trivial'),
|
|
133
140
|
// page — low (basic reads), moderate (evaluate, debugger-backed)
|
|
134
141
|
'page.get_state': createRegistryEntry('page.get_state', 'page', true, [], 'low'),
|
|
135
142
|
'page.evaluate': {
|
|
@@ -170,6 +177,35 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
170
177
|
['clear', 'limit', 'urlPattern'],
|
|
171
178
|
'low'
|
|
172
179
|
),
|
|
180
|
+
// network intercept — moderate (holds debugger session)
|
|
181
|
+
'network.intercept.add': createRegistryEntry(
|
|
182
|
+
'network.intercept.add',
|
|
183
|
+
'page',
|
|
184
|
+
true,
|
|
185
|
+
['urlPattern', 'action', 'statusCode', 'body', 'headers'],
|
|
186
|
+
'moderate'
|
|
187
|
+
),
|
|
188
|
+
'network.intercept.remove': createRegistryEntry(
|
|
189
|
+
'network.intercept.remove',
|
|
190
|
+
'page',
|
|
191
|
+
true,
|
|
192
|
+
['ruleId'],
|
|
193
|
+
'trivial'
|
|
194
|
+
),
|
|
195
|
+
'network.intercept.list': createRegistryEntry(
|
|
196
|
+
'network.intercept.list',
|
|
197
|
+
'page',
|
|
198
|
+
true,
|
|
199
|
+
[],
|
|
200
|
+
'trivial'
|
|
201
|
+
),
|
|
202
|
+
'network.intercept.clear': createRegistryEntry(
|
|
203
|
+
'network.intercept.clear',
|
|
204
|
+
'page',
|
|
205
|
+
true,
|
|
206
|
+
[],
|
|
207
|
+
'low'
|
|
208
|
+
),
|
|
173
209
|
// navigation — low
|
|
174
210
|
'navigation.navigate': createRegistryEntry(
|
|
175
211
|
'navigation.navigate',
|
|
@@ -322,6 +358,13 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
322
358
|
['target', 'text', 'clear', 'submit', 'modifiers'],
|
|
323
359
|
'low'
|
|
324
360
|
),
|
|
361
|
+
'input.fill': createRegistryEntry(
|
|
362
|
+
'input.fill',
|
|
363
|
+
'interact',
|
|
364
|
+
true,
|
|
365
|
+
['target', 'value', 'mode'],
|
|
366
|
+
'low'
|
|
367
|
+
),
|
|
325
368
|
'input.press_key': createRegistryEntry(
|
|
326
369
|
'input.press_key',
|
|
327
370
|
'interact',
|
|
@@ -18,7 +18,8 @@ export type Capability =
|
|
|
18
18
|
| 'automation.input'
|
|
19
19
|
| 'tabs.manage'
|
|
20
20
|
| 'performance.read'
|
|
21
|
-
| 'network.read'
|
|
21
|
+
| 'network.read'
|
|
22
|
+
| 'network.intercept';
|
|
22
23
|
|
|
23
24
|
export type ErrorCode =
|
|
24
25
|
| 'ACCESS_DENIED'
|
|
@@ -37,6 +38,7 @@ export type BridgeMethod =
|
|
|
37
38
|
| 'tabs.list'
|
|
38
39
|
| 'tabs.create'
|
|
39
40
|
| 'tabs.close'
|
|
41
|
+
| 'tabs.activate'
|
|
40
42
|
| 'skill.get_runtime_context'
|
|
41
43
|
| 'setup.get_status'
|
|
42
44
|
| 'setup.install'
|
|
@@ -47,6 +49,10 @@ export type BridgeMethod =
|
|
|
47
49
|
| 'page.get_storage'
|
|
48
50
|
| 'page.get_text'
|
|
49
51
|
| 'page.get_network'
|
|
52
|
+
| 'network.intercept.add'
|
|
53
|
+
| 'network.intercept.remove'
|
|
54
|
+
| 'network.intercept.list'
|
|
55
|
+
| 'network.intercept.clear'
|
|
50
56
|
| 'navigation.navigate'
|
|
51
57
|
| 'navigation.reload'
|
|
52
58
|
| 'navigation.go_back'
|
|
@@ -69,6 +75,7 @@ export type BridgeMethod =
|
|
|
69
75
|
| 'input.click'
|
|
70
76
|
| 'input.focus'
|
|
71
77
|
| 'input.type'
|
|
78
|
+
| 'input.fill'
|
|
72
79
|
| 'input.press_key'
|
|
73
80
|
| 'input.set_checked'
|
|
74
81
|
| 'input.select_option'
|
|
@@ -215,6 +222,8 @@ export interface InputActionParams {
|
|
|
215
222
|
button?: 'left' | 'middle' | 'right';
|
|
216
223
|
clickCount?: number;
|
|
217
224
|
text?: string;
|
|
225
|
+
value?: string;
|
|
226
|
+
mode?: 'auto' | 'setter' | 'keystrokes';
|
|
218
227
|
clear?: boolean;
|
|
219
228
|
submit?: boolean;
|
|
220
229
|
key?: string;
|
|
@@ -226,6 +235,8 @@ export interface NormalizedInputAction extends BridgeParams {
|
|
|
226
235
|
button: 'left' | 'middle' | 'right';
|
|
227
236
|
clickCount: number;
|
|
228
237
|
text: string;
|
|
238
|
+
value: string;
|
|
239
|
+
mode: 'auto' | 'setter' | 'keystrokes';
|
|
229
240
|
clear: boolean;
|
|
230
241
|
submit: boolean;
|
|
231
242
|
key: string;
|
|
@@ -9,6 +9,8 @@ Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through
|
|
|
9
9
|
|
|
10
10
|
This CLI skill is for agents that can run shell commands and where direct `bbx` control fits better than MCP tools: manual debugging, terminal reproduction, install/doctor flows, raw protocol access, or environments without MCP.
|
|
11
11
|
|
|
12
|
+
Permission prompts are controlled by the host agent, not by Browser Bridge. In permission-ask MCP hosts, use the generic `browser_call` MCP tool by default so the user can approve one BBX tool instead of separate status, page, DOM, input, and patch tools. In Claude Code CLI mode, allow `bbx` with `Bash(bbx *)` when direct shell access is desired. Exact permission syntax varies by client.
|
|
13
|
+
|
|
12
14
|
Skill name: `browser-bridge` (also known as `bbx`). In GitHub Copilot, invoke as `/browser-bridge`. `bbx` is the CLI command used throughout this skill.
|
|
13
15
|
When the runtime supports subagents, delegate bridge inspection to a smaller, lower-cost worker and return only concise findings to the parent.
|
|
14
16
|
For open-ended investigation, start with structured reads (`page.get_state`, `dom.query`, `page.get_text`, `styles.get_computed`, `bbx batch`) and escalate to screenshots or debugger-backed methods only when structured evidence is insufficient.
|
|
@@ -28,6 +30,7 @@ bbx tabs # list available tabs (prefer this)
|
|
|
28
30
|
bbx logs # recent bridge request log
|
|
29
31
|
bbx tab-create [url] # open a new tab (avoid unless necessary)
|
|
30
32
|
bbx tab-close <tabId> # close a tab
|
|
33
|
+
bbx tab-activate <tabId> # bring a tab to the foreground
|
|
31
34
|
bbx skill # live runtime presets + limits
|
|
32
35
|
```
|
|
33
36
|
|
|
@@ -51,9 +54,11 @@ bbx a11y-tree [maxNodes] [maxDepth] # accessibility tree
|
|
|
51
54
|
### Page & Evaluate
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
|
-
bbx eval <expression>
|
|
57
|
+
bbx eval [--await] <expression> # JS eval (--await for promises, - for stdin)
|
|
55
58
|
bbx console [level] # console output
|
|
56
59
|
bbx network [limit] # network requests
|
|
60
|
+
bbx intercept add <pattern> [--respond <body>] [--status <code>] [--block] # intercept matching requests
|
|
61
|
+
bbx intercept list|remove <id>|clear # manage interception rules
|
|
57
62
|
bbx page-text [budget] # full page text
|
|
58
63
|
bbx storage [local|session] [keys] # browser storage
|
|
59
64
|
bbx perf # performance metrics
|
|
@@ -71,6 +76,7 @@ bbx resize <width> <height> # resize viewport
|
|
|
71
76
|
bbx click <ref> [button] # click element
|
|
72
77
|
bbx focus <ref> # focus element
|
|
73
78
|
bbx type <ref> <text...> # type into element
|
|
79
|
+
bbx fill <ref|selector> <value...> # set input value (React/Vue/Angular-safe)
|
|
74
80
|
bbx press-key <key> [ref] # send key event
|
|
75
81
|
bbx cdp-press-key --tab <id> Escape # CDP key event without foreground focus
|
|
76
82
|
bbx hover <ref> # hover over element
|
|
@@ -211,9 +217,9 @@ bbx page-text 2000 # extract page content
|
|
|
211
217
|
| Inspect | `dom.query`, `dom.describe`, `dom.get_html`, `styles.get_computed`, `layout.get_box_model` |
|
|
212
218
|
| Find | `dom.find_by_text`, `dom.find_by_role`, `dom.wait_for`, `dom.get_accessibility_tree` |
|
|
213
219
|
| Page State | `page.get_console`, `page.get_storage`, `page.get_text`, `page.wait_for_load_state`, `page.evaluate` (debugger-backed) |
|
|
214
|
-
| Network | `page.get_network`
|
|
215
|
-
| Interact | `input.click`, `input.type`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view`
|
|
216
|
-
| Tabs | `tabs.list` (preferred), `tabs.create` (avoid unless necessary), `tabs.close`
|
|
220
|
+
| Network | `page.get_network`, `network.intercept.add/remove/list/clear` (debugger-backed; rules are in-memory and drop if the debugger detaches — verify with `list`) |
|
|
221
|
+
| Interact | `input.click`, `input.type`, `input.fill`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view` |
|
|
222
|
+
| Tabs | `tabs.list` (preferred), `tabs.create` (avoid unless necessary), `tabs.close`, `tabs.activate` |
|
|
217
223
|
| Patch | `patch.apply_styles`, `patch.apply_dom`, `patch.rollback` |
|
|
218
224
|
| Navigate | `navigation.navigate`, `viewport.scroll`, `viewport.resize` |
|
|
219
225
|
| Performance | `performance.get_metrics` (debugger-backed) |
|
|
@@ -281,7 +287,15 @@ Every CLI shortcut command produces consistent `{ok, summary, evidence}` JSON. U
|
|
|
281
287
|
## CLI Raw Params Gotchas
|
|
282
288
|
|
|
283
289
|
- Use `selector`, not `scope`, to narrow `dom.find_by_text` and `dom.find_by_role`.
|
|
284
|
-
- Wrap interaction targets as `target: { elementRef }` or `target: { selector }`; `viewport.scroll` also uses the `target` wrapper for element scrolling.
|
|
290
|
+
- Wrap interaction targets as `target: { elementRef }` or `target: { selector }`; `viewport.scroll` also uses the `target` wrapper for element scrolling. **Do not pass `ref` or `elementRef` at the top level** -- interaction methods require the nested `target` wrapper:
|
|
291
|
+
```bash
|
|
292
|
+
# CORRECT
|
|
293
|
+
bbx call input.click '{"target":{"elementRef":"el_xxx"}}'
|
|
294
|
+
bbx call input.type '{"target":{"elementRef":"el_xxx"},"text":"hello"}'
|
|
295
|
+
# WRONG -- will fail with "Target not found"
|
|
296
|
+
bbx call input.click '{"ref":"el_xxx"}'
|
|
297
|
+
bbx call input.click '{"elementRef":"el_xxx"}'
|
|
298
|
+
```
|
|
285
299
|
- `input.drag` uses `source`, `destination`, and optional destination offsets `offsetX` / `offsetY`.
|
|
286
300
|
- Raw `screenshot.capture_region` and `screenshot.capture_full_page` return base64 JSON; prefer `bbx screenshot <ref> [outPath]` when one element is enough.
|
|
287
301
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| `input.click` | `click <ref> [button]` | DOM-level click |
|
|
8
8
|
| `input.focus` | `focus <ref>` | Focus an element |
|
|
9
9
|
| `input.type` | `type <ref> <text>` | Type into input/textarea/contenteditable |
|
|
10
|
+
| `input.fill` | `fill <ref> <value>` | Set field value via native setter (React/Vue/Angular-safe) |
|
|
10
11
|
| `input.press_key` | `press-key <key> [ref]` | Send keyboard key (Enter, Backspace, etc.) |
|
|
11
12
|
| `cdp.dispatch_key_event` | `cdp-press-key --tab <id> <key>` | CDP keyDown/keyUp without focusing the target tab |
|
|
12
13
|
| `input.set_checked` | `call input.set_checked '{...}'` | Toggle checkbox/radio |
|
|
@@ -64,6 +65,7 @@ bbx tabs # list available tabs (start here)
|
|
|
64
65
|
bbx tab-create https://example.com # open new tab (avoid unless necessary)
|
|
65
66
|
bbx tab-create # open blank tab (avoid unless necessary)
|
|
66
67
|
bbx tab-close 12345 # close tab by ID
|
|
68
|
+
bbx tab-activate 12345 # bring a tab to the foreground
|
|
67
69
|
bbx call tabs.create '{"url":"https://example.com","active":false}'
|
|
68
70
|
```
|
|
69
71
|
|
|
@@ -156,6 +158,24 @@ Typical workflow - debug API calls:
|
|
|
156
158
|
3. Cross-reference with `page.get_console` for errors
|
|
157
159
|
4. Use `page.evaluate` to replay or inspect response data
|
|
158
160
|
|
|
161
|
+
## Network Interception
|
|
162
|
+
|
|
163
|
+
Block, stub, or modify matching requests via CDP (debugger-backed). Patterns are globs: `*` matches any characters, `?` matches one character.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
bbx intercept add 'https://api.example.com/users*' --respond '{"users":[]}' --status 200
|
|
167
|
+
bbx intercept add '*analytics*' --block # fail matching requests
|
|
168
|
+
bbx intercept list # active rules
|
|
169
|
+
bbx intercept remove intercept_1
|
|
170
|
+
bbx intercept clear # remove all rules, detach debugger
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Caveats:
|
|
174
|
+
|
|
175
|
+
- Rules are **in-memory and per-tab**. They drop silently if the debugger detaches (user dismisses the infobar, tab closes, extension service worker restarts). Verify with `bbx intercept list` before relying on them.
|
|
176
|
+
- Sessions auto-expire after 10 minutes as a safety net.
|
|
177
|
+
- Always `bbx intercept clear` when finished so the page returns to normal traffic.
|
|
178
|
+
|
|
159
179
|
## Form Controls
|
|
160
180
|
|
|
161
181
|
**Checkbox/radio:**
|
|
@@ -172,6 +192,15 @@ bbx call input.select_option '{"target":{"elementRef":"el_456"},"values":["us"]}
|
|
|
172
192
|
|
|
173
193
|
Select by value, label, or index. Multiple values for multi-select.
|
|
174
194
|
|
|
195
|
+
**Text fields — `fill` vs `type`:**
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bbx fill el_123 hello@example.com # set value instantly (preferred for forms)
|
|
199
|
+
bbx type el_123 hello # simulate per-character keystrokes
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Prefer `fill` for setting form values: it uses the native prototype setter plus `input`/`change`/`blur` events, which React, Vue, and Angular pick up reliably. `mode` defaults to `auto` (setter first, keystroke fallback if the value did not stick); pass `"mode":"keystrokes"` via `bbx call input.fill` for components that only react to per-key events. Use `type` when page logic depends on individual key events (autocomplete, masked inputs).
|
|
203
|
+
|
|
175
204
|
## Hover
|
|
176
205
|
|
|
177
206
|
Dispatch mouse events to trigger CSS `:hover` rules, tooltip display, dropdown menus, etc.
|
|
@@ -263,6 +292,24 @@ bbx call page.wait_for_load_state '{"timeoutMs":10000}'
|
|
|
263
292
|
|
|
264
293
|
Use after clicking navigation links.
|
|
265
294
|
|
|
295
|
+
## Raw `bbx call` for Interaction Methods
|
|
296
|
+
|
|
297
|
+
All interaction methods (`input.click`, `input.type`, `input.focus`, `input.hover`, etc.) require the target wrapped in a `target` object -- do not pass `ref` or `elementRef` at the top level:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# CORRECT
|
|
301
|
+
bbx call input.click '{"target":{"elementRef":"el_xxx"}}'
|
|
302
|
+
bbx call input.click '{"target":{"elementRef":"el_xxx"},"button":"right"}'
|
|
303
|
+
bbx call input.type '{"target":{"elementRef":"el_xxx"},"text":"hello"}'
|
|
304
|
+
bbx call input.focus '{"target":{"selector":"#search-input"}}'
|
|
305
|
+
|
|
306
|
+
# WRONG -- "Target not found"
|
|
307
|
+
bbx call input.click '{"ref":"el_xxx"}'
|
|
308
|
+
bbx call input.click '{"elementRef":"el_xxx"}'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The CLI shortcuts (`bbx click el_xxx`) handle this wrapping automatically, but `bbx call` passes params as-is.
|
|
312
|
+
|
|
266
313
|
## Interaction Flow
|
|
267
314
|
|
|
268
315
|
1. **Find target**: `dom.find_by_text`, `dom.find_by_role`, or `dom.query` → get `elementRef`
|
|
@@ -90,6 +90,12 @@ The table below includes the legacy capability bucket for each method so agents
|
|
|
90
90
|
| 57 | `cdp.get_box_model` | Yes | CDP | cdp | `cdp.box_model` | DevTools-backed element geometry |
|
|
91
91
|
| 58 | `cdp.get_computed_styles_for_node` | Yes | CDP | cdp | `cdp.styles` | DevTools-backed computed styles |
|
|
92
92
|
| 59 | `cdp.dispatch_key_event` | Yes | CDP | cdp | `cdp.input` | DevTools keyDown/keyUp without foreground focus |
|
|
93
|
+
| 60 | `tabs.activate` | No | - | tabs | `tabs.manage` | Bring a tab to the foreground in the enabled window |
|
|
94
|
+
| 61 | `input.fill` | Yes | - | interact | `automation.input` | Set field value via native setter; `mode`: auto/setter/keystrokes |
|
|
95
|
+
| 62 | `network.intercept.add` | Yes | CDP | page | `network.intercept` | Add interception rule; action fulfill/continue/block |
|
|
96
|
+
| 63 | `network.intercept.remove` | Yes | CDP | page | `network.intercept` | Remove rule by `ruleId` |
|
|
97
|
+
| 64 | `network.intercept.list` | Yes | CDP | page | `network.intercept` | List active rules (rules drop on debugger detach) |
|
|
98
|
+
| 65 | `network.intercept.clear` | Yes | CDP | page | `network.intercept` | Remove all rules, detach debugger |
|
|
93
99
|
|
|
94
100
|
## CLI
|
|
95
101
|
|