@browserbridge/bbx 1.5.2 → 1.6.1
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/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/mcp-server/src/server.js +1 -3
- 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 +17 -5
- package/skills/browser-bridge/references/interaction.md +47 -0
- package/skills/browser-bridge/references/protocol.md +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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
|
),
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
getMethodsByMaxComplexity,
|
|
41
41
|
} from '../../protocol/src/index.js';
|
|
42
42
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
43
|
-
import { MCP_SERVER_INSTRUCTIONS
|
|
43
|
+
import { MCP_SERVER_INSTRUCTIONS } from './guidance.js';
|
|
44
44
|
|
|
45
45
|
export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
|
|
46
46
|
export const TAB_ID_DESCRIPTION =
|
|
@@ -735,8 +735,6 @@ export function createBridgeMcpServer() {
|
|
|
735
735
|
handleInvestigateTool
|
|
736
736
|
);
|
|
737
737
|
|
|
738
|
-
registerBridgeMcpGuidance(server);
|
|
739
|
-
|
|
740
738
|
return server;
|
|
741
739
|
}
|
|
742
740
|
|
|
@@ -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;
|
|
@@ -30,6 +30,7 @@ bbx tabs # list available tabs (prefer this)
|
|
|
30
30
|
bbx logs # recent bridge request log
|
|
31
31
|
bbx tab-create [url] # open a new tab (avoid unless necessary)
|
|
32
32
|
bbx tab-close <tabId> # close a tab
|
|
33
|
+
bbx tab-activate <tabId> # bring a tab to the foreground
|
|
33
34
|
bbx skill # live runtime presets + limits
|
|
34
35
|
```
|
|
35
36
|
|
|
@@ -53,9 +54,11 @@ bbx a11y-tree [maxNodes] [maxDepth] # accessibility tree
|
|
|
53
54
|
### Page & Evaluate
|
|
54
55
|
|
|
55
56
|
```bash
|
|
56
|
-
bbx eval <expression>
|
|
57
|
+
bbx eval [--await] <expression> # JS eval (--await for promises, - for stdin)
|
|
57
58
|
bbx console [level] # console output
|
|
58
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
|
|
59
62
|
bbx page-text [budget] # full page text
|
|
60
63
|
bbx storage [local|session] [keys] # browser storage
|
|
61
64
|
bbx perf # performance metrics
|
|
@@ -73,6 +76,7 @@ bbx resize <width> <height> # resize viewport
|
|
|
73
76
|
bbx click <ref> [button] # click element
|
|
74
77
|
bbx focus <ref> # focus element
|
|
75
78
|
bbx type <ref> <text...> # type into element
|
|
79
|
+
bbx fill <ref|selector> <value...> # set input value (React/Vue/Angular-safe)
|
|
76
80
|
bbx press-key <key> [ref] # send key event
|
|
77
81
|
bbx cdp-press-key --tab <id> Escape # CDP key event without foreground focus
|
|
78
82
|
bbx hover <ref> # hover over element
|
|
@@ -213,9 +217,9 @@ bbx page-text 2000 # extract page content
|
|
|
213
217
|
| Inspect | `dom.query`, `dom.describe`, `dom.get_html`, `styles.get_computed`, `layout.get_box_model` |
|
|
214
218
|
| Find | `dom.find_by_text`, `dom.find_by_role`, `dom.wait_for`, `dom.get_accessibility_tree` |
|
|
215
219
|
| Page State | `page.get_console`, `page.get_storage`, `page.get_text`, `page.wait_for_load_state`, `page.evaluate` (debugger-backed) |
|
|
216
|
-
| Network | `page.get_network`
|
|
217
|
-
| Interact | `input.click`, `input.type`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view`
|
|
218
|
-
| 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` |
|
|
219
223
|
| Patch | `patch.apply_styles`, `patch.apply_dom`, `patch.rollback` |
|
|
220
224
|
| Navigate | `navigation.navigate`, `viewport.scroll`, `viewport.resize` |
|
|
221
225
|
| Performance | `performance.get_metrics` (debugger-backed) |
|
|
@@ -283,7 +287,15 @@ Every CLI shortcut command produces consistent `{ok, summary, evidence}` JSON. U
|
|
|
283
287
|
## CLI Raw Params Gotchas
|
|
284
288
|
|
|
285
289
|
- Use `selector`, not `scope`, to narrow `dom.find_by_text` and `dom.find_by_role`.
|
|
286
|
-
- 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
|
+
```
|
|
287
299
|
- `input.drag` uses `source`, `destination`, and optional destination offsets `offsetX` / `offsetY`.
|
|
288
300
|
- Raw `screenshot.capture_region` and `screenshot.capture_full_page` return base64 JSON; prefer `bbx screenshot <ref> [outPath]` when one element is enough.
|
|
289
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
|
|