@browserbridge/bbx 1.5.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.5.2",
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.6"
72
+ "zod": "^4.4.3"
73
73
  },
74
74
  "devDependencies": {
75
- "@types/chrome": "^0.1.40",
76
- "@types/node": "^25.5.2",
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.47.0",
80
- "oxlint": "^1.62.0",
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
- let elementRef;
584
- if (shortcutCmd.resolve) {
585
- if (!rest[0]) throw new Error(`Usage: ${command} <ref|selector>`);
586
- elementRef = await resolveRef(client, rest[0], null, REQUEST_SOURCE);
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
- const response = await requestBridge(
589
- client,
590
- shortcutCmd.method,
591
- shortcutCmd.build(rest, elementRef),
592
- { source: REQUEST_SOURCE }
593
- );
594
- await printSummary(response, shortcutCmd.printMethod);
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
- let expression = rest.join(' ');
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('Usage: eval <expression> (or pipe via stdin: echo "expr" | bbx eval -)');
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 eval <expression> Evaluate JS in page context (use - for stdin)',
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
  ),
@@ -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> # JS eval (- for stdin)
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