@browserbridge/bbx 1.7.3 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +7 -7
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-helpers.js +20 -3
  4. package/packages/agent-client/src/cli.js +273 -76
  5. package/packages/agent-client/src/command-registry.js +12 -6
  6. package/packages/agent-client/src/install.js +39 -4
  7. package/packages/agent-client/src/mcp-config.js +28 -14
  8. package/packages/agent-client/src/remotes.js +93 -15
  9. package/packages/agent-client/src/runtime.js +6 -3
  10. package/packages/mcp-server/src/bin.js +10 -1
  11. package/packages/mcp-server/src/guidance.js +2 -2
  12. package/packages/mcp-server/src/handlers-capture.js +62 -32
  13. package/packages/mcp-server/src/handlers-dom.js +2 -2
  14. package/packages/mcp-server/src/handlers-navigation.js +30 -9
  15. package/packages/mcp-server/src/handlers-page.js +170 -143
  16. package/packages/mcp-server/src/handlers-utils.js +277 -8
  17. package/packages/mcp-server/src/handlers.js +31 -10
  18. package/packages/mcp-server/src/lifecycle.js +413 -0
  19. package/packages/mcp-server/src/server.js +59 -30
  20. package/packages/native-host/src/daemon.js +3 -1
  21. package/packages/protocol/src/capabilities.js +1 -1
  22. package/packages/protocol/src/defaults.js +2 -0
  23. package/packages/protocol/src/protocol.js +122 -9
  24. package/packages/protocol/src/registry.js +14 -14
  25. package/packages/protocol/src/summary.js +61 -30
  26. package/packages/protocol/src/types.ts +30 -1
  27. package/skills/browser-bridge/SKILL.md +4 -4
  28. package/skills/browser-bridge/references/interaction.md +1 -1
  29. package/skills/browser-bridge/references/protocol.md +3 -3
@@ -6,12 +6,14 @@ import {
6
6
  DEFAULT_A11Y_MAX_DEPTH,
7
7
  DEFAULT_A11Y_MAX_NODES,
8
8
  DEFAULT_CONSOLE_LIMIT,
9
+ DEFAULT_DEVICE_SCALE_FACTOR,
9
10
  DEFAULT_EVAL_TIMEOUT_MS,
10
11
  DEFAULT_LOG_TAIL_LIMIT,
11
12
  DEFAULT_MAX_DEPTH,
12
13
  DEFAULT_MAX_HTML_LENGTH,
13
14
  DEFAULT_MAX_NODES,
14
15
  DEFAULT_NAV_TIMEOUT_MS,
16
+ DEFAULT_NETWORK_INTERCEPT_ACTION,
15
17
  DEFAULT_NETWORK_LIMIT,
16
18
  DEFAULT_PAGE_TEXT_BUDGET,
17
19
  DEFAULT_TEXT_BUDGET,
@@ -42,6 +44,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
42
44
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
43
45
  /** @typedef {import('./types.js').LogTailParams} LogTailParams */
44
46
  /** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
47
+ /** @typedef {import('./types.js').NetworkInterceptAddParams} NetworkInterceptAddParams */
45
48
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
46
49
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
47
50
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
@@ -58,6 +61,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
58
61
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
59
62
  /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
60
63
  /** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
64
+ /** @typedef {import('./types.js').NormalizedNetworkInterceptAddParams} NormalizedNetworkInterceptAddParams */
61
65
  /** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
62
66
  /** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
63
67
  /** @typedef {import('./types.js').NormalizedPatchOperation} NormalizedPatchOperation */
@@ -151,6 +155,30 @@ function clampInt(value, min, max, fallback) {
151
155
  return Math.min(Math.max(integer, min), max);
152
156
  }
153
157
 
158
+ /**
159
+ * Clamp a finite number without discarding its fractional component.
160
+ *
161
+ * @param {unknown} value
162
+ * @param {number} min
163
+ * @param {number} max
164
+ * @param {number} fallback
165
+ * @returns {number}
166
+ */
167
+ function clampNumber(value, min, max, fallback) {
168
+ const numeric = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
169
+ return Math.min(Math.max(numeric, min), max);
170
+ }
171
+
172
+ /**
173
+ * @param {unknown} value
174
+ * @returns {value is Record<string, unknown>}
175
+ */
176
+ function isPlainRecord(value) {
177
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
178
+ const prototype = Object.getPrototypeOf(value);
179
+ return prototype === Object.prototype || prototype === null;
180
+ }
181
+
154
182
  /** @type {ReadonlyArray<BridgeMethod>} */
155
183
  export const METHODS = BRIDGE_METHODS;
156
184
 
@@ -336,6 +364,8 @@ function normalizeRequestParams(method, params) {
336
364
  return normalizePageTextParams(params);
337
365
  case 'page.get_network':
338
366
  return normalizeNetworkParams(params);
367
+ case 'network.intercept.add':
368
+ return normalizeNetworkInterceptAddParams(params);
339
369
  case 'navigation.navigate':
340
370
  case 'navigation.reload':
341
371
  case 'navigation.go_back':
@@ -361,6 +391,7 @@ function normalizeRequestParams(method, params) {
361
391
  case 'input.click':
362
392
  case 'input.focus':
363
393
  case 'input.type':
394
+ case 'input.fill':
364
395
  case 'input.press_key':
365
396
  return normalizeInputAction(params);
366
397
  case 'cdp.dispatch_key_event':
@@ -412,7 +443,7 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
412
443
  case 'page.evaluate':
413
444
  return normalizeEvaluateParams(params).timeoutMs;
414
445
  case 'dom.wait_for':
415
- return normalizeWaitForParams(params).timeoutMs;
446
+ return clampInt(params.timeoutMs, 100, 30_000, 5_000);
416
447
  default:
417
448
  return null;
418
449
  }
@@ -423,11 +454,13 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
423
454
  * @returns {NormalizedDomQuery}
424
455
  */
425
456
  export function normalizeDomQuery(params = {}) {
457
+ const nestedBudget =
458
+ params.budget && typeof params.budget === 'object' ? params.budget : undefined;
426
459
  return {
427
460
  selector:
428
461
  typeof params.selector === 'string' && params.selector.trim() ? params.selector : 'body',
429
462
  withinRef: typeof params.withinRef === 'string' ? params.withinRef : null,
430
- budget: applyBudget(params),
463
+ budget: applyBudget({ ...nestedBudget, ...params }),
431
464
  };
432
465
  }
433
466
 
@@ -640,7 +673,7 @@ export function normalizeNavigationAction(params = {}) {
640
673
  return {
641
674
  url,
642
675
  waitForLoad: params.waitForLoad !== false,
643
- timeoutMs: clampInt(params.timeoutMs, 500, 120_000, 15_000),
676
+ timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
644
677
  };
645
678
  }
646
679
 
@@ -679,11 +712,17 @@ export function normalizeEvaluateParams(params = {}) {
679
712
  `Expression too large (${expression.length} chars). Maximum is 100,000.`
680
713
  );
681
714
  }
715
+ if (params.returnByValue === false) {
716
+ throw new BridgeError(
717
+ ERROR_CODES.INVALID_REQUEST,
718
+ 'returnByValue=false is not supported because remote CDP objects cannot be consumed.'
719
+ );
720
+ }
682
721
  return {
683
722
  expression,
684
723
  awaitPromise: Boolean(params.awaitPromise),
685
- timeoutMs: clampInt(params.timeoutMs, 100, 30_000, 5_000),
686
- returnByValue: params.returnByValue !== false,
724
+ timeoutMs: clampInt(params.timeoutMs, 100, 30_000, DEFAULT_EVAL_TIMEOUT_MS),
725
+ returnByValue: true,
687
726
  };
688
727
  }
689
728
 
@@ -706,9 +745,18 @@ export function normalizeConsoleParams(params = {}) {
706
745
  */
707
746
  export function normalizeWaitForParams(params = {}) {
708
747
  const validStates = ['attached', 'detached', 'visible', 'hidden'];
748
+ const selector =
749
+ typeof params.selector === 'string' && params.selector.trim() ? params.selector : '';
750
+ const text = typeof params.text === 'string' && params.text.trim() ? params.text : null;
751
+ if (!selector && text === null) {
752
+ throw new BridgeError(
753
+ ERROR_CODES.INVALID_REQUEST,
754
+ 'selector or text is required for dom.wait_for.'
755
+ );
756
+ }
709
757
  return {
710
- selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '',
711
- text: params.text != null ? String(params.text) : null,
758
+ selector: selector || '*',
759
+ text,
712
760
  state: validStates.includes(String(params.state ?? ''))
713
761
  ? /** @type {'attached' | 'detached' | 'visible' | 'hidden'} */ (String(params.state))
714
762
  : 'attached',
@@ -817,7 +865,7 @@ export function normalizeStorageParams(params = {}) {
817
865
  export function normalizeWaitForLoadStateParams(params = {}) {
818
866
  return {
819
867
  waitForLoad: params.waitForLoad !== false,
820
- timeoutMs: clampInt(params.timeoutMs, 500, 120_000, 15_000),
868
+ timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
821
869
  };
822
870
  }
823
871
 
@@ -886,6 +934,71 @@ export function normalizeNetworkParams(params = {}) {
886
934
  };
887
935
  }
888
936
 
937
+ /**
938
+ * @param {NetworkInterceptAddParams} [params={}]
939
+ * @returns {NormalizedNetworkInterceptAddParams}
940
+ */
941
+ export function normalizeNetworkInterceptAddParams(params = {}) {
942
+ if (typeof params.urlPattern !== 'string' || !params.urlPattern) {
943
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern is required.');
944
+ }
945
+
946
+ const action = params.action ?? DEFAULT_NETWORK_INTERCEPT_ACTION;
947
+ if (action !== 'fulfill' && action !== 'continue' && action !== 'block') {
948
+ throw new BridgeError(
949
+ ERROR_CODES.INVALID_REQUEST,
950
+ 'action must be one of fulfill, continue, or block.'
951
+ );
952
+ }
953
+ if (
954
+ params.statusCode !== undefined &&
955
+ (!Number.isInteger(params.statusCode) || params.statusCode < 100 || params.statusCode > 599)
956
+ ) {
957
+ throw new BridgeError(
958
+ ERROR_CODES.INVALID_REQUEST,
959
+ 'statusCode must be an integer between 100 and 599.'
960
+ );
961
+ }
962
+ if (params.body !== undefined && typeof params.body !== 'string') {
963
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'body must be a string.');
964
+ }
965
+
966
+ /** @type {Record<string, string> | undefined} */
967
+ let headers;
968
+ if (params.headers !== undefined) {
969
+ if (!isPlainRecord(params.headers)) {
970
+ throw new BridgeError(
971
+ ERROR_CODES.INVALID_REQUEST,
972
+ 'headers must be an object containing string values.'
973
+ );
974
+ }
975
+ headers = {};
976
+ for (const [name, value] of Object.entries(params.headers)) {
977
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
978
+ throw new BridgeError(
979
+ ERROR_CODES.INVALID_REQUEST,
980
+ `Invalid header name: ${name || '(empty)'}.`
981
+ );
982
+ }
983
+ if (typeof value !== 'string' || /[\r\n]/.test(value)) {
984
+ throw new BridgeError(
985
+ ERROR_CODES.INVALID_REQUEST,
986
+ `Header ${name} must have a string value without newlines.`
987
+ );
988
+ }
989
+ headers[name] = value;
990
+ }
991
+ }
992
+
993
+ return {
994
+ urlPattern: params.urlPattern,
995
+ action,
996
+ ...(params.statusCode === undefined ? {} : { statusCode: params.statusCode }),
997
+ ...(params.body === undefined ? {} : { body: params.body }),
998
+ ...(headers === undefined ? {} : { headers }),
999
+ };
1000
+ }
1001
+
889
1002
  /**
890
1003
  * @param {PageTextParams} [params={}]
891
1004
  * @returns {NormalizedPageTextParams}
@@ -914,7 +1027,7 @@ export function normalizeViewportResizeParams(params = {}) {
914
1027
  return {
915
1028
  width: clampInt(params.width, 320, 7680, DEFAULT_VIEWPORT_WIDTH),
916
1029
  height: clampInt(params.height, 200, 4320, DEFAULT_VIEWPORT_HEIGHT),
917
- deviceScaleFactor: clampInt(params.deviceScaleFactor, 0, 4, 0),
1030
+ deviceScaleFactor: clampNumber(params.deviceScaleFactor, 0, 4, DEFAULT_DEVICE_SCALE_FACTOR),
918
1031
  reset: Boolean(params.reset),
919
1032
  };
920
1033
  }
@@ -112,7 +112,7 @@ function createRegistryEntry(method, group, tab, params, complexity = 'low') {
112
112
 
113
113
  /** @type {Readonly<Record<import('./types.js').BridgeMethod, BridgeMethodRegistryEntry>>} */
114
114
  export const BRIDGE_METHOD_REGISTRY = Object.freeze({
115
- // system — trivial
115
+ // system - trivial
116
116
  'access.request': createRegistryEntry('access.request', 'system', false, [], 'trivial'),
117
117
  'skill.get_runtime_context': createRegistryEntry(
118
118
  'skill.get_runtime_context',
@@ -132,12 +132,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
132
132
  'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
133
133
  'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
134
134
  'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
135
- // tabs — trivial
135
+ // tabs - trivial
136
136
  'tabs.list': createRegistryEntry('tabs.list', 'tabs', false, [], 'trivial'),
137
137
  'tabs.create': createRegistryEntry('tabs.create', 'tabs', false, ['url', 'active'], 'trivial'),
138
138
  'tabs.close': createRegistryEntry('tabs.close', 'tabs', false, ['tabId'], 'trivial'),
139
139
  'tabs.activate': createRegistryEntry('tabs.activate', 'tabs', false, ['tabId'], 'trivial'),
140
- // page — low (basic reads), moderate (evaluate, debugger-backed)
140
+ // page - low (basic reads), moderate (evaluate, debugger-backed)
141
141
  'page.get_state': createRegistryEntry('page.get_state', 'page', true, [], 'low'),
142
142
  'page.evaluate': {
143
143
  ...createRegistryEntry(
@@ -177,7 +177,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
177
177
  ['clear', 'limit', 'urlPattern'],
178
178
  'low'
179
179
  ),
180
- // network intercept — moderate (holds debugger session)
180
+ // network intercept - moderate (holds debugger session)
181
181
  'network.intercept.add': createRegistryEntry(
182
182
  'network.intercept.add',
183
183
  'page',
@@ -206,7 +206,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
206
206
  [],
207
207
  'low'
208
208
  ),
209
- // navigation — low
209
+ // navigation - low
210
210
  'navigation.navigate': createRegistryEntry(
211
211
  'navigation.navigate',
212
212
  'navigate',
@@ -235,7 +235,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
235
235
  ['waitForLoad', 'timeoutMs'],
236
236
  'low'
237
237
  ),
238
- // dom — low (reads), moderate (accessibility tree)
238
+ // dom - low (reads), moderate (accessibility tree)
239
239
  'dom.query': {
240
240
  ...createRegistryEntry(
241
241
  'dom.query',
@@ -303,7 +303,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
303
303
  ['maxNodes', 'maxDepth'],
304
304
  'moderate'
305
305
  ),
306
- // layout — low
306
+ // layout - low
307
307
  'layout.get_box_model': createRegistryEntry(
308
308
  'layout.get_box_model',
309
309
  'inspect',
@@ -312,7 +312,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
312
312
  'low'
313
313
  ),
314
314
  'layout.hit_test': createRegistryEntry('layout.hit_test', 'inspect', true, ['x', 'y'], 'low'),
315
- // styles — low (computed), moderate (matched rules)
315
+ // styles - low (computed), moderate (matched rules)
316
316
  'styles.get_computed': createRegistryEntry(
317
317
  'styles.get_computed',
318
318
  'inspect',
@@ -327,7 +327,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
327
327
  ['elementRef'],
328
328
  'moderate'
329
329
  ),
330
- // viewport — low (scroll), moderate (resize, debugger-backed)
330
+ // viewport - low (scroll), moderate (resize, debugger-backed)
331
331
  'viewport.scroll': createRegistryEntry(
332
332
  'viewport.scroll',
333
333
  'navigate',
@@ -342,7 +342,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
342
342
  ['width', 'height', 'deviceScaleFactor', 'reset'],
343
343
  'moderate'
344
344
  ),
345
- // input — low (simple), moderate (drag)
345
+ // input - low (simple), moderate (drag)
346
346
  'input.click': createRegistryEntry(
347
347
  'input.click',
348
348
  'interact',
@@ -407,7 +407,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
407
407
  ['target'],
408
408
  'low'
409
409
  ),
410
- // capture — high (screenshots, CDP)
410
+ // capture - high (screenshots, CDP)
411
411
  'screenshot.capture_region': createRegistryEntry(
412
412
  'screenshot.capture_region',
413
413
  'capture',
@@ -429,7 +429,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
429
429
  [],
430
430
  'high'
431
431
  ),
432
- // patch — moderate (side effects)
432
+ // patch - moderate (side effects)
433
433
  'patch.apply_styles': createRegistryEntry(
434
434
  'patch.apply_styles',
435
435
  'patch',
@@ -453,7 +453,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
453
453
  [],
454
454
  'low'
455
455
  ),
456
- // cdp — high (raw protocol, large payloads)
456
+ // cdp - high (raw protocol, large payloads)
457
457
  'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
458
458
  'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
459
459
  'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
@@ -471,7 +471,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
471
471
  ['key', 'code', 'modifiers'],
472
472
  'moderate'
473
473
  ),
474
- // performance — moderate (debugger-backed)
474
+ // performance - moderate (debugger-backed)
475
475
  'performance.get_metrics': createRegistryEntry(
476
476
  'performance.get_metrics',
477
477
  'performance',
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {
4
4
  estimateSerializedPayloadCost,
5
+ getErrorRecovery,
5
6
  getCostClass,
6
7
  getProtocolVersion,
7
8
  serializeJsonPayload,
@@ -26,7 +27,8 @@ import {
26
27
  * @typedef {{
27
28
  * ok: boolean,
28
29
  * summary: string,
29
- * evidence: unknown
30
+ * evidence: unknown,
31
+ * recovery?: import('./types.js').BridgeRecovery | null
30
32
  * }} BridgeSummary
31
33
  */
32
34
 
@@ -49,7 +51,7 @@ import {
49
51
  * approxTokens: number,
50
52
  * meta?: Record<string, unknown>,
51
53
  * error: unknown,
52
- * response: unknown
54
+ * response?: unknown
53
55
  * }} BatchItemSummary
54
56
  */
55
57
 
@@ -164,14 +166,15 @@ function getMetaCostClass(meta, field) {
164
166
  export function summarizeBridgeResponse(response, method) {
165
167
  const protocolWarning = getProtocolWarning(response.meta);
166
168
  if (!response.ok) {
167
- const hint = summarizeErrorHint(response.error.code);
169
+ const recovery = response.error.recovery ?? getErrorRecovery(response.error.code);
168
170
  return {
169
171
  ok: false,
170
172
  summary: appendProtocolWarning(
171
- `${response.error.code}: ${response.error.message}${hint ? ` ${hint}` : ''}`,
173
+ `${response.error.code}: ${response.error.message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
172
174
  protocolWarning
173
175
  ),
174
176
  evidence: response.error.details ?? null,
177
+ recovery,
175
178
  };
176
179
  }
177
180
 
@@ -348,16 +351,38 @@ export function summarizeBridgeResponse(response, method) {
348
351
  }
349
352
  return entry;
350
353
  });
351
- let label;
352
- if (method === 'dom.find_by_text' || method === 'dom.find_by_role') {
353
- label = 'Found';
354
- } else {
355
- label = 'DOM query returned';
354
+ const isFind = method === 'dom.find_by_text' || method === 'dom.find_by_role';
355
+ if (isFind) {
356
+ const count = typeof result.count === 'number' ? result.count : nodes.length;
357
+ const scanned = typeof result.scanned === 'number' ? result.scanned : null;
358
+ const truncated = result.truncated === true;
359
+ let summary;
360
+ if (count === 0) {
361
+ summary = truncated
362
+ ? `No matching elements returned; search stopped${scanned === null ? '' : ` after scanning ${scanned}`}.`
363
+ : 'No matching elements found.';
364
+ } else if (truncated) {
365
+ summary = `Found ${count} matching element(s); additional matches were omitted.`;
366
+ } else {
367
+ summary = `Found ${count} matching element(s)${scanned === null ? '' : ` after scanning ${scanned}`}.`;
368
+ }
369
+ return {
370
+ ok: true,
371
+ summary: appendProtocolWarning(summary, protocolWarning),
372
+ evidence: {
373
+ found: count > 0,
374
+ count,
375
+ scanned,
376
+ truncated,
377
+ truncationReason: result.truncationReason ?? null,
378
+ nodes: compact,
379
+ },
380
+ };
356
381
  }
357
382
  return {
358
383
  ok: true,
359
384
  summary: appendProtocolWarning(
360
- `${label} ${nodes.length} element(s)${nodes.length > 15 ? '; showing first 15' : ''}.`,
385
+ `DOM query returned ${nodes.length} element(s)${nodes.length > 15 ? '; showing first 15' : ''}.`,
361
386
  protocolWarning
362
387
  ),
363
388
  evidence: compact,
@@ -666,9 +691,10 @@ export function summarizeBridgeResponse(response, method) {
666
691
 
667
692
  /**
668
693
  * @param {{ method: SummaryMethod, tabId: number | null, response: BridgeResponse, durationMs: number }} input
694
+ * @param {{ compact?: boolean }} [options]
669
695
  * @returns {BatchItemSummary}
670
696
  */
671
- export function summarizeBatchResponseItem({ method, tabId, response, durationMs }) {
697
+ export function summarizeBatchResponseItem({ method, tabId, response, durationMs }, options = {}) {
672
698
  const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
673
699
  return {
674
700
  method,
@@ -676,17 +702,18 @@ export function summarizeBatchResponseItem({ method, tabId, response, durationMs
676
702
  ...summary,
677
703
  durationMs,
678
704
  approxTokens: summary.transportTokens,
679
- meta: response.meta,
705
+ meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
680
706
  error: response.ok ? null : response.error,
681
- response: response.ok ? response.result : null,
707
+ ...(options.compact ? {} : { response: response.ok ? response.result : null }),
682
708
  };
683
709
  }
684
710
 
685
711
  /**
686
712
  * @param {{ method: SummaryMethod, tabId: number | null, error: unknown, durationMs: number }} input
713
+ * @param {{ compact?: boolean }} [options]
687
714
  * @returns {BatchItemSummary}
688
715
  */
689
- export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
716
+ export function summarizeBatchErrorItem({ method, tabId, error, durationMs }, options = {}) {
690
717
  const message = error instanceof Error ? error.message : String(error);
691
718
  const response = /** @type {BridgeResponse} */ ({
692
719
  id: 'batch_error',
@@ -706,12 +733,30 @@ export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
706
733
  ...summary,
707
734
  durationMs,
708
735
  approxTokens: 0,
709
- meta: response.meta,
736
+ meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
710
737
  error: response.error,
711
- response: null,
738
+ ...(options.compact ? {} : { response: null }),
712
739
  };
713
740
  }
714
741
 
742
+ /**
743
+ * @param {Record<string, unknown>} meta
744
+ * @returns {Record<string, unknown>}
745
+ */
746
+ function compactBatchMeta(meta) {
747
+ /** @type {Record<string, unknown>} */
748
+ const compact = {};
749
+ for (const key of [
750
+ 'protocol_version',
751
+ 'protocol_warning',
752
+ 'budget_truncated',
753
+ 'continuation_hint',
754
+ ]) {
755
+ if (meta[key] !== undefined && meta[key] !== null) compact[key] = meta[key];
756
+ }
757
+ return compact;
758
+ }
759
+
715
760
  /**
716
761
  * @param {unknown} value
717
762
  * @returns {Record<string, unknown>}
@@ -758,20 +803,6 @@ function truncateUrl(url) {
758
803
  }
759
804
  }
760
805
 
761
- /**
762
- * @param {string} code
763
- * @returns {string}
764
- */
765
- function summarizeErrorHint(code) {
766
- if (code === 'ELEMENT_STALE') {
767
- return 'Re-query the current page after navigation or DOM updates.';
768
- }
769
- if (code === 'RESULT_TRUNCATED') {
770
- return 'Narrow the query or raise the relevant budget if more detail is required.';
771
- }
772
- return '';
773
- }
774
-
775
806
  /**
776
807
  * Check if an array looks like a list of patch entries.
777
808
  * Returns true for empty arrays when method context suggests patch.list.
@@ -140,6 +140,7 @@ export interface BridgeRecovery {
140
140
  hint: string;
141
141
  retry?: boolean;
142
142
  retryAfterMs?: number;
143
+ alternativeMethod?: string;
143
144
  }
144
145
 
145
146
  export interface BridgeFailure {
@@ -192,6 +193,7 @@ export interface TruncateResult {
192
193
  export interface DomQueryParams extends BudgetOptions {
193
194
  selector?: string;
194
195
  withinRef?: string | null;
196
+ budget?: BudgetOptions;
195
197
  }
196
198
 
197
199
  export interface NormalizedDomQuery extends BridgeParams {
@@ -200,6 +202,15 @@ export interface NormalizedDomQuery extends BridgeParams {
200
202
  budget: Budget;
201
203
  }
202
204
 
205
+ export interface DomFindResult {
206
+ found: boolean;
207
+ nodes: Record<string, unknown>[];
208
+ count: number;
209
+ scanned: number;
210
+ truncated: boolean;
211
+ truncationReason: 'maxResults' | 'scanLimit' | null;
212
+ }
213
+
203
214
  export interface StyleQueryParams {
204
215
  elementRef?: string;
205
216
  target?: InputTarget;
@@ -397,7 +408,7 @@ export interface NormalizedEvaluateParams extends BridgeParams {
397
408
  expression: string;
398
409
  awaitPromise: boolean;
399
410
  timeoutMs: number;
400
- returnByValue: boolean;
411
+ returnByValue: true;
401
412
  }
402
413
 
403
414
  export interface ConsoleParams {
@@ -554,6 +565,24 @@ export interface NormalizedNetworkParams extends BridgeParams {
554
565
  urlPattern: string | null;
555
566
  }
556
567
 
568
+ export type NetworkInterceptAction = 'fulfill' | 'continue' | 'block';
569
+
570
+ export interface NetworkInterceptAddParams {
571
+ urlPattern?: string;
572
+ action?: NetworkInterceptAction;
573
+ statusCode?: number;
574
+ body?: string;
575
+ headers?: Record<string, string>;
576
+ }
577
+
578
+ export interface NormalizedNetworkInterceptAddParams extends BridgeParams {
579
+ urlPattern: string;
580
+ action: NetworkInterceptAction;
581
+ statusCode?: number;
582
+ body?: string;
583
+ headers?: Record<string, string>;
584
+ }
585
+
557
586
  export interface PageTextParams {
558
587
  textBudget?: number;
559
588
  }
@@ -5,7 +5,7 @@ description: 'Token-efficient Chrome tab inspection, interaction, and patching v
5
5
 
6
6
  # Browser Bridge
7
7
 
8
- Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches — all from the real tab the user already has open.
8
+ Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches - all from the real tab the user already has open.
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
 
@@ -21,7 +21,7 @@ For open-ended investigation, start with structured reads (`page.get_state`, `do
21
21
  bbx status # daemon + extension health
22
22
  bbx doctor # install/access readiness
23
23
  bbx access-request # ask user to enable access for the focused window
24
- bbx restart # start/restart the local daemon non-interactively
24
+ bbx restart # start/restart the daemon and running MCP servers
25
25
  bbx call <method> '{...}' # any RPC method (raw output)
26
26
  bbx <method> '{...}' # direct alias for an exact bridge method such as page.get_state
27
27
  bbx call --tab 123 <method> '{...}' # explicit tab override
@@ -235,7 +235,7 @@ bbx page-text 2000 # extract page content
235
235
  | Inspect | `dom.query`, `dom.describe`, `dom.get_html`, `styles.get_computed`, `layout.get_box_model` |
236
236
  | Find | `dom.find_by_text`, `dom.find_by_role`, `dom.wait_for`, `dom.get_accessibility_tree` |
237
237
  | Page State | `page.get_console`, `page.get_storage`, `page.get_text`, `page.wait_for_load_state`, `page.evaluate` (debugger-backed) |
238
- | 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`) |
238
+ | 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`) |
239
239
  | Interact | `input.click`, `input.type`, `input.fill`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view` |
240
240
  | Tabs | `tabs.list` (preferred), `tabs.create` (avoid unless necessary), `tabs.close`, `tabs.activate` |
241
241
  | Patch | `patch.apply_styles`, `patch.apply_dom`, `patch.rollback` |
@@ -291,7 +291,7 @@ dom.find_by_role('button', 'Login') → input.click
291
291
 
292
292
  `bbx a11y-tree` and `dom.get_accessibility_tree` are sensitive to `maxDepth` and `maxNodes`. Shallow runs can undercount interactive nodes on real pages, so widen those limits before treating a low interactive count as a bug.
293
293
 
294
- > **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly — do not shell out to `bbx`. The MCP tools map 1:1 to CLI capabilities. In prompts, `BB MCP` and `Browser Bridge MCP` both work. Do not treat `bbx-mcp` as a skill alias.
294
+ > **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly - do not shell out to `bbx`. The MCP tools map 1:1 to CLI capabilities. In prompts, `BB MCP` and `Browser Bridge MCP` both work. Do not treat `bbx-mcp` as a skill alias.
295
295
  >
296
296
  > For open-ended MCP inspection tasks, prefer `browser_investigate` first. It is read-only, designed for cheaper delegated investigation, and falls back to a deterministic sequence when the client cannot delegate. Escalate to `browser_capture` only when the structured investigation is not enough.
297
297
 
@@ -195,7 +195,7 @@ bbx call input.select_option '{"target":{"elementRef":"el_456"},"values":["us"]}
195
195
 
196
196
  Select by value, label, or index. Multiple values for multi-select.
197
197
 
198
- **Text fields — `fill` vs `type`:**
198
+ **Text fields - `fill` vs `type`:**
199
199
 
200
200
  ```bash
201
201
  bbx fill el_123 hello@example.com # set value instantly (preferred for forms)
@@ -121,7 +121,7 @@ bbx access-request
121
121
  bbx call access.request
122
122
  ```
123
123
 
124
- If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically — so explicit `access.request` is optional but useful for proactive setup.
124
+ If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically - so explicit `access.request` is optional but useful for proactive setup.
125
125
 
126
126
  If access is already pending for a window, do not call `access.request` again. Ask the user to click `Enable` for the requested window and wait for confirmation before continuing.
127
127
 
@@ -139,7 +139,7 @@ bbx call page.evaluate '{"expression":"await fetch(\"/api/health\").then(r=>r.js
139
139
 
140
140
  ### page.get_console
141
141
 
142
- Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections.
142
+ Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections. `level` is a minimum severity, so `warn` includes errors, exceptions, and rejections; `exception` and `rejection` select only that exact stream.
143
143
 
144
144
  To capture a reproduction reliably, call it once with `clear: true` before triggering the event. This installs the interceptor and removes older entries. Reproduce the issue, then read again without `clear`.
145
145
 
@@ -182,7 +182,7 @@ If `_registryPruned` is true, refresh previously cached refs before reusing them
182
182
 
183
183
  ### dom.wait_for
184
184
 
185
- Wait for a DOM condition using MutationObserver + 250 ms polling fallback. Returns `{found, elementRef, duration}`.
185
+ Wait for a DOM condition using MutationObserver + 250 ms polling fallback. A text-only wait searches all elements. `hidden` succeeds when no matching visible element remains, including when it is absent. Returns `{found, elementRef, duration}`.
186
186
 
187
187
  - `state`: `attached` (default), `detached`, `visible`, `hidden`
188
188
  - `text`: optional text content filter