@browserbridge/bbx 1.7.3 → 1.7.4

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.7.3",
3
+ "version": "1.7.4",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -73,13 +73,13 @@
73
73
  "zod": "^4.4.3"
74
74
  },
75
75
  "devDependencies": {
76
- "@types/chrome": "^0.1.43",
77
- "@types/node": "^25.9.2",
76
+ "@types/chrome": "^0.2.2",
77
+ "@types/node": "^26.1.1",
78
78
  "c8": "^11.0.0",
79
- "linkedom": "^0.18.12",
80
- "oxfmt": "^0.54.0",
81
- "oxlint": "^1.69.0",
82
- "typescript": "^6.0.3"
79
+ "linkedom": "^0.18.13",
80
+ "oxfmt": "^0.58.0",
81
+ "oxlint": "^1.73.0",
82
+ "typescript": "^7.0.2"
83
83
  },
84
84
  "engines": {
85
85
  "node": ">=18"
@@ -966,9 +966,9 @@ async function handleProxyCommand(args) {
966
966
  const exampleHost = getProxyExampleHost(bindHost);
967
967
  const tokenNote =
968
968
  tokenSource === 'existing'
969
- ? ' (unchanged — already-configured clients keep working; pass --rotate-token to generate a new secret)'
969
+ ? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
970
970
  : tokenSource === 'generated' && existing
971
- ? ' (rotated — update every configured client with `bbx remote add`)'
971
+ ? ' (rotated - update every configured client with `bbx remote add`)'
972
972
  : '';
973
973
  const lines = [
974
974
  `Browser Bridge proxy enabled on ${bindHost}:${port}.`,
@@ -350,7 +350,7 @@ export async function checkNativeHostManifestHealth(browserManifests) {
350
350
  */
351
351
  export async function checkUnwritableBridgePaths() {
352
352
  // The root-owned-files scenario comes from sudo installs, which do not exist
353
- // on Windows — and fs.access(W_OK) there only reflects the read-only file
353
+ // on Windows - and fs.access(W_OK) there only reflects the read-only file
354
354
  // attribute, so it would produce false positives.
355
355
  if (os.platform() === 'win32') {
356
356
  return [];
@@ -10,7 +10,7 @@ export const MCP_SERVER_INSTRUCTIONS = [
10
10
  'In permission-ask hosts, use browser_call as the default Browser Bridge MCP tool so the user can approve one BBX tool instead of separate browser_status, browser_page, browser_dom, browser_input, and other tools.',
11
11
  'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
12
12
  'Start with browser_call method health.ping. If window access is disabled, call browser_call method access.request once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
13
- 'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, or layout.get_box_model. Use browser_batch for parallel reads and keep limits tight before widening.',
13
+ 'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, or layout.get_box_model. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
14
14
  'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
15
15
  'Escalate to screenshot.capture_element, screenshot.capture_region, dom.get_accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
16
16
  'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
@@ -6,7 +6,6 @@ import {
6
6
  REQUEST_SOURCE,
7
7
  requestBridgeWithRetry,
8
8
  resolveToolRef,
9
- resolveRef,
10
9
  summarizeToolError,
11
10
  summarizeToolResponse,
12
11
  withToolClient,
@@ -269,6 +268,7 @@ export const INPUT_ACTION_METHODS = {
269
268
  click: 'input.click',
270
269
  focus: 'input.focus',
271
270
  type: 'input.type',
271
+ fill: 'input.fill',
272
272
  press_key: 'input.press_key',
273
273
  cdp_press_key: 'cdp.dispatch_key_event',
274
274
  set_checked: 'input.set_checked',
@@ -279,13 +279,30 @@ export const INPUT_ACTION_METHODS = {
279
279
  };
280
280
 
281
281
  /**
282
- * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
282
+ * Keep selector-based actions atomic instead of resolving a ref in a separate
283
+ * bridge request that can race with a page rerender.
284
+ *
285
+ * @param {unknown} elementRef
286
+ * @param {unknown} selector
287
+ * @returns {import('../../protocol/src/types.js').InputTarget | null}
288
+ */
289
+ function createInputTarget(elementRef, selector) {
290
+ if (typeof elementRef === 'string' && elementRef) return { elementRef };
291
+ if (typeof selector === 'string' && selector) return { selector };
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
283
297
  * @returns {Promise<ToolResult>}
284
298
  */
285
299
  export async function handleInputTool(args) {
286
300
  if (args.action === 'type' && !hasText(args.text)) {
287
301
  return summarizeToolError('text is required for input.type.');
288
302
  }
303
+ if (args.action === 'fill' && typeof args.value !== 'string') {
304
+ return summarizeToolError('value is required for input.fill.');
305
+ }
289
306
  if ((args.action === 'press_key' || args.action === 'cdp_press_key') && !hasText(args.key)) {
290
307
  return summarizeToolError('key is required for key input actions.');
291
308
  }
@@ -301,9 +318,11 @@ export async function handleInputTool(args) {
301
318
  return withToolClient(
302
319
  async (client) => {
303
320
  const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
304
- const elementTarget = async () => ({
305
- elementRef: await resolveToolRef(client, args, requestedTabId),
306
- });
321
+ const elementTarget = () => {
322
+ const target = createInputTarget(args.elementRef, args.selector);
323
+ if (!target) throw new Error('Provide either elementRef or selector.');
324
+ return target;
325
+ };
307
326
 
308
327
  switch (args.action) {
309
328
  case 'click': {
@@ -311,7 +330,7 @@ export async function handleInputTool(args) {
311
330
  client,
312
331
  'input.click',
313
332
  {
314
- target: await elementTarget(),
333
+ target: elementTarget(),
315
334
  button: args.button,
316
335
  clickCount: args.clickCount,
317
336
  modifiers: args.modifiers,
@@ -329,7 +348,7 @@ export async function handleInputTool(args) {
329
348
  client,
330
349
  'input.focus',
331
350
  {
332
- target: await elementTarget(),
351
+ target: elementTarget(),
333
352
  },
334
353
  {
335
354
  tabId: requestedTabId,
@@ -344,7 +363,7 @@ export async function handleInputTool(args) {
344
363
  client,
345
364
  'input.type',
346
365
  {
347
- target: await elementTarget(),
366
+ target: elementTarget(),
348
367
  text: args.text,
349
368
  clear: args.clear,
350
369
  submit: args.submit,
@@ -358,8 +377,25 @@ export async function handleInputTool(args) {
358
377
  );
359
378
  return summarizeToolResponse(response, 'input.type');
360
379
  }
380
+ case 'fill': {
381
+ const response = await requestBridgeWithRetry(
382
+ client,
383
+ 'input.fill',
384
+ {
385
+ target: elementTarget(),
386
+ value: args.value,
387
+ mode: args.mode,
388
+ },
389
+ {
390
+ tabId: requestedTabId,
391
+ source: REQUEST_SOURCE,
392
+ tokenBudget: getToolTokenBudget(args),
393
+ }
394
+ );
395
+ return summarizeToolResponse(response, 'input.fill');
396
+ }
361
397
  case 'press_key': {
362
- const target = args.elementRef || args.selector ? await elementTarget() : undefined;
398
+ const target = args.elementRef || args.selector ? elementTarget() : undefined;
363
399
  const response = await requestBridgeWithRetry(
364
400
  client,
365
401
  'input.press_key',
@@ -398,7 +434,7 @@ export async function handleInputTool(args) {
398
434
  client,
399
435
  'input.set_checked',
400
436
  {
401
- target: await elementTarget(),
437
+ target: elementTarget(),
402
438
  checked: args.checked,
403
439
  },
404
440
  {
@@ -414,7 +450,7 @@ export async function handleInputTool(args) {
414
450
  client,
415
451
  'input.select_option',
416
452
  {
417
- target: await elementTarget(),
453
+ target: elementTarget(),
418
454
  values: args.values,
419
455
  labels: args.labels,
420
456
  indexes: args.indexes,
@@ -432,7 +468,7 @@ export async function handleInputTool(args) {
432
468
  client,
433
469
  'input.hover',
434
470
  {
435
- target: await elementTarget(),
471
+ target: elementTarget(),
436
472
  duration: args.duration,
437
473
  modifiers: args.modifiers,
438
474
  },
@@ -445,21 +481,12 @@ export async function handleInputTool(args) {
445
481
  return summarizeToolResponse(response, 'input.hover');
446
482
  }
447
483
  case 'drag': {
448
- const source = {
449
- elementRef:
450
- args.sourceElementRef ||
451
- (args.sourceSelector
452
- ? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
453
- : ''),
454
- };
455
- const destination = {
456
- elementRef:
457
- args.destinationElementRef ||
458
- (args.destinationSelector
459
- ? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
460
- : ''),
461
- };
462
- if (!source.elementRef || !destination.elementRef) {
484
+ const source = createInputTarget(args.sourceElementRef, args.sourceSelector);
485
+ const destination = createInputTarget(
486
+ args.destinationElementRef,
487
+ args.destinationSelector
488
+ );
489
+ if (!source || !destination) {
463
490
  return summarizeToolError(
464
491
  'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
465
492
  );
@@ -486,7 +513,7 @@ export async function handleInputTool(args) {
486
513
  client,
487
514
  'input.scroll_into_view',
488
515
  {
489
- target: await elementTarget(),
516
+ target: elementTarget(),
490
517
  },
491
518
  {
492
519
  tabId: requestedTabId,
@@ -96,6 +96,16 @@ export async function handleTabsTool(args) {
96
96
  { destinationId: args.destinationId ?? null }
97
97
  );
98
98
  }
99
+ if (args.action === 'activate') {
100
+ if (typeof args.tabId !== 'number') {
101
+ return summarizeToolError('tabId is required for tabs.activate.');
102
+ }
103
+ return callBridgeTool(
104
+ 'tabs.activate',
105
+ { tabId: args.tabId },
106
+ { destinationId: args.destinationId ?? null }
107
+ );
108
+ }
99
109
  return summarizeToolError(`Unsupported tabs action "${args.action}".`);
100
110
  }
101
111
 
@@ -30,6 +30,51 @@ import {
30
30
  export const MAX_BATCH_CALLS = 20;
31
31
  export const MAX_BATCH_CONCURRENCY = 5;
32
32
 
33
+ /** @type {ReadonlySet<BridgeMethod>} */
34
+ const BATCH_SAFE_METHODS = new Set([
35
+ 'health.ping',
36
+ 'daemon.metrics',
37
+ 'tabs.list',
38
+ 'skill.get_runtime_context',
39
+ 'setup.get_status',
40
+ 'log.tail',
41
+ 'page.get_state',
42
+ 'page.get_console',
43
+ 'page.wait_for_load_state',
44
+ 'page.get_storage',
45
+ 'page.get_text',
46
+ 'page.get_network',
47
+ 'dom.query',
48
+ 'dom.describe',
49
+ 'dom.get_text',
50
+ 'dom.get_attributes',
51
+ 'dom.wait_for',
52
+ 'dom.find_by_text',
53
+ 'dom.find_by_role',
54
+ 'dom.get_html',
55
+ 'dom.get_accessibility_tree',
56
+ 'layout.get_box_model',
57
+ 'layout.hit_test',
58
+ 'styles.get_computed',
59
+ 'styles.get_matched_rules',
60
+ 'network.intercept.list',
61
+ 'patch.list',
62
+ 'performance.get_metrics',
63
+ ]);
64
+
65
+ /**
66
+ * @param {BridgeMethod} method
67
+ * @param {Record<string, unknown>} params
68
+ * @returns {boolean}
69
+ */
70
+ export function isBatchSafeBridgeCall(method, params) {
71
+ if (!BATCH_SAFE_METHODS.has(method)) return false;
72
+ if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+
33
78
  /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
34
79
  export const PAGE_ACTIONS = {
35
80
  state: { method: 'page.get_state', params: () => ({}) },
@@ -118,54 +163,24 @@ export async function handleBatchTool(args) {
118
163
  }
119
164
 
120
165
  const calls = args.calls;
166
+ for (const [index, call] of calls.entries()) {
167
+ if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
168
+ return summarizeToolError(`calls[${index}] needs a method.`);
169
+ }
170
+ if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
171
+ return summarizeToolError(`Unknown bridge method "${call.method}" at calls[${index}].`);
172
+ }
173
+ const method = /** @type {BridgeMethod} */ (call.method);
174
+ if (!isBatchSafeBridgeCall(method, call.params || {})) {
175
+ return summarizeToolError(
176
+ `${method} is not safe for parallel batch execution. Use its specialized tool or browser_call sequentially.`
177
+ );
178
+ }
179
+ }
180
+
121
181
  /** @param {BridgeClient | null} client */
122
182
  const executeBatch = async (client) => {
123
183
  const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
124
- if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
125
- return {
126
- method: '',
127
- tabId: null,
128
- ok: false,
129
- summary: 'INVALID_REQUEST: Each batch call needs a method.',
130
- evidence: null,
131
- error: {
132
- code: 'INVALID_REQUEST',
133
- message: 'Each batch call needs a method.',
134
- },
135
- response: null,
136
- };
137
- }
138
-
139
- if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
140
- return {
141
- method: call.method,
142
- tabId: null,
143
- ok: false,
144
- summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
145
- evidence: null,
146
- error: {
147
- code: 'INVALID_REQUEST',
148
- message: `Unknown bridge method "${call.method}".`,
149
- },
150
- response: null,
151
- };
152
- }
153
-
154
- if (call.method === 'setup.install') {
155
- return {
156
- method: call.method,
157
- tabId: null,
158
- ok: false,
159
- summary: 'INVALID_REQUEST: Use browser_setup for setup changes.',
160
- evidence: null,
161
- error: {
162
- code: 'INVALID_REQUEST',
163
- message: 'Use browser_setup for setup changes.',
164
- },
165
- response: null,
166
- };
167
- }
168
-
169
184
  const method = /** @type {BridgeMethod} */ (call.method);
170
185
  const tabId = bridgeMethodNeedsTab(method)
171
186
  ? typeof call.tabId === 'number'
@@ -180,12 +195,15 @@ export async function handleBatchTool(args) {
180
195
  ? await createBridgeClientForDestination(destinationId)
181
196
  : client;
182
197
  if (!callClient) {
183
- return summarizeBatchErrorItem({
184
- method,
185
- tabId,
186
- error: new Error('A local bridge client is required for calls without destinationId.'),
187
- durationMs: Date.now() - startTime,
188
- });
198
+ return summarizeBatchErrorItem(
199
+ {
200
+ method,
201
+ tabId,
202
+ error: new Error('A local bridge client is required for calls without destinationId.'),
203
+ durationMs: Date.now() - startTime,
204
+ },
205
+ { compact: true }
206
+ );
189
207
  }
190
208
  try {
191
209
  if (destinationId) {
@@ -198,20 +216,26 @@ export async function handleBatchTool(args) {
198
216
  });
199
217
  return {
200
218
  destinationId,
201
- ...summarizeBatchResponseItem({
219
+ ...summarizeBatchResponseItem(
220
+ {
221
+ method,
222
+ tabId,
223
+ response,
224
+ durationMs: Date.now() - startTime,
225
+ },
226
+ { compact: true }
227
+ ),
228
+ };
229
+ } catch (error) {
230
+ return summarizeBatchErrorItem(
231
+ {
202
232
  method,
203
233
  tabId,
204
- response,
234
+ error,
205
235
  durationMs: Date.now() - startTime,
206
- }),
207
- };
208
- } catch (error) {
209
- return summarizeBatchErrorItem({
210
- method,
211
- tabId,
212
- error,
213
- durationMs: Date.now() - startTime,
214
- });
236
+ },
237
+ { compact: true }
238
+ );
215
239
  } finally {
216
240
  if (destinationId) {
217
241
  await callClient.close();
@@ -228,6 +252,9 @@ export async function handleBatchTool(args) {
228
252
  summary,
229
253
  {
230
254
  ok: failureCount === 0,
255
+ successCount: results.length - failureCount,
256
+ failureCount,
257
+ compactOutput: true,
231
258
  results,
232
259
  },
233
260
  failureCount > 0
@@ -134,11 +134,17 @@ export function summarizeToolResponse(response, method) {
134
134
  */
135
135
  export function summarizeToolError(error) {
136
136
  const message = error instanceof Error ? error.message : String(error);
137
+ const record =
138
+ error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
139
+ const code = typeof record.code === 'string' ? record.code : 'INTERNAL_ERROR';
140
+ const recovery = getErrorRecovery(code);
137
141
  return createToolResult(
138
- `ERROR: ${message}`,
142
+ `${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
139
143
  {
140
144
  ok: false,
141
145
  evidence: null,
146
+ error: { code, message },
147
+ recovery,
142
148
  },
143
149
  true
144
150
  );
@@ -317,7 +323,10 @@ export function applyHtmlBudgetPreset(args) {
317
323
  */
318
324
  export async function requestBridgeWithRetry(client, method, params, options) {
319
325
  const response = await requestBridge(client, method, params, options);
320
- const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
326
+ const recovery =
327
+ !response.ok && response.error
328
+ ? (response.error.recovery ?? getErrorRecovery(response.error.code))
329
+ : null;
321
330
  if (!response.ok && recovery?.retry && isRetrySafeBridgeMethod(method, params)) {
322
331
  const delay = recovery.retryAfterMs ?? 1000;
323
332
  process.stderr.write(
@@ -175,14 +175,19 @@ export function createBridgeMcpServer() {
175
175
  {
176
176
  title: 'Browser Tabs',
177
177
  description:
178
- 'List, create, or close browser tabs. Prefer "list" to work in existing tabs; only use "create" when the user explicitly requests a new page.',
178
+ 'List, create, close, or activate browser tabs. Prefer "list" to work in existing tabs; only use "create" when the user explicitly requests a new page.',
179
179
  inputSchema: {
180
180
  action: z
181
- .enum(['list', 'create', 'close'])
182
- .describe('"list" (preferred), "create" (only when needed), or "close"'),
181
+ .enum(['list', 'create', 'close', 'activate'])
182
+ .describe('"list" (preferred), "create" (only when needed), "close", or "activate"'),
183
183
  url: z.string().optional().describe('URL for create action'),
184
184
  active: z.boolean().optional().describe('Focus the new tab (default: true)'),
185
- tabId: z.number().int().positive().optional().describe('Tab ID (required for close)'),
185
+ tabId: z
186
+ .number()
187
+ .int()
188
+ .positive()
189
+ .optional()
190
+ .describe('Tab ID (required for close/activate)'),
186
191
  destinationId: z.string().optional().describe(DESTINATION_ID_DESCRIPTION),
187
192
  },
188
193
  },
@@ -194,7 +199,7 @@ export function createBridgeMcpServer() {
194
199
  {
195
200
  title: 'Browser DOM',
196
201
  description:
197
- 'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed — use query/find first.',
202
+ 'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For full-page text, use browser_page action "text". accessibility_tree is debugger-backed - use query/find first.',
198
203
  inputSchema: {
199
204
  action: z
200
205
  .enum([
@@ -270,7 +275,9 @@ export function createBridgeMcpServer() {
270
275
  state: z
271
276
  .enum(['attached', 'detached', 'visible', 'hidden'])
272
277
  .optional()
273
- .describe('Expected element state (for wait action)'),
278
+ .describe(
279
+ 'Expected element state (for wait action); hidden also matches complete absence'
280
+ ),
274
281
  timeoutMs: z
275
282
  .number()
276
283
  .int()
@@ -334,7 +341,7 @@ export function createBridgeMcpServer() {
334
341
  {
335
342
  title: 'Browser Page State',
336
343
  description:
337
- 'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed — prefer lighter reads first.',
344
+ 'Read page-level data: state (URL/title), evaluate (JS), console, storage, text, network, or performance. For element-level reads, use browser_dom. evaluate and performance are debugger-backed - prefer lighter reads first.',
338
345
  inputSchema: {
339
346
  action: z
340
347
  .enum([
@@ -370,9 +377,11 @@ export function createBridgeMcpServer() {
370
377
  .optional()
371
378
  .describe('Return actual value vs JSON (default: true)'),
372
379
  level: z
373
- .string()
380
+ .enum(['all', 'debug', 'log', 'info', 'warn', 'error', 'exception', 'rejection'])
374
381
  .optional()
375
- .describe('Minimum console level: log, warn, error (default: all)'),
382
+ .describe(
383
+ 'Minimum console severity (default: all); exception/rejection select that exact stream'
384
+ ),
376
385
  clear: z.boolean().optional().describe('Clear buffer after reading (default: false)'),
377
386
  limit: z
378
387
  .number()
@@ -405,7 +414,7 @@ export function createBridgeMcpServer() {
405
414
  {
406
415
  title: 'Browser Navigation',
407
416
  description:
408
- 'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed — use only for exact viewport overrides.',
417
+ 'Navigate to a URL, reload, go back/forward, scroll, or resize the viewport. resize is debugger-backed - use only for exact viewport overrides.',
409
418
  inputSchema: {
410
419
  action: z
411
420
  .enum(['navigate', 'reload', 'go_back', 'go_forward', 'scroll', 'resize'])
@@ -456,6 +465,7 @@ export function createBridgeMcpServer() {
456
465
  'click',
457
466
  'focus',
458
467
  'type',
468
+ 'fill',
459
469
  'press_key',
460
470
  'cdp_press_key',
461
471
  'set_checked',
@@ -488,6 +498,11 @@ export function createBridgeMcpServer() {
488
498
  .optional()
489
499
  .describe('Click count (1=single, 2=double)'),
490
500
  text: z.string().max(100000).optional().describe('Text to type (for type action)'),
501
+ value: z.string().max(100000).optional().describe('Value to set (for fill action)'),
502
+ mode: z
503
+ .enum(['auto', 'setter', 'keystrokes'])
504
+ .optional()
505
+ .describe('Fill strategy (default: auto)'),
491
506
  clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
492
507
  submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
493
508
  key: z
@@ -594,7 +609,7 @@ export function createBridgeMcpServer() {
594
609
  {
595
610
  title: 'Browser Capture',
596
611
  description:
597
- 'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive — use only when structured reads (browser_dom, browser_styles_layout) are insufficient. Prefer element, then tight region; full_page only for document-level context.',
612
+ 'Capture screenshots or CDP snapshots. Debugger-backed and token-expensive - use only when structured reads (browser_dom, browser_styles_layout) are insufficient. Prefer element, then tight region; full_page only for document-level context.',
598
613
  inputSchema: {
599
614
  action: z
600
615
  .enum([
@@ -645,7 +660,13 @@ export function createBridgeMcpServer() {
645
660
  {
646
661
  title: 'Browser Bridge Batch',
647
662
  description:
648
- 'Execute multiple bridge calls in parallel to reduce round-trips. Combine independent reads (e.g., styles + text + console) in one call. Preserves call order in the response.',
663
+ 'Execute independent read-only bridge calls in parallel to reduce round-trips. Mutations and destructive reads are rejected; run them sequentially with specialized tools or browser_call. Preserves call order in the response.',
664
+ annotations: {
665
+ readOnlyHint: true,
666
+ destructiveHint: false,
667
+ idempotentHint: true,
668
+ openWorldHint: true,
669
+ },
649
670
  inputSchema: {
650
671
  calls: z
651
672
  .array(
@@ -602,7 +602,9 @@ export class BridgeDaemon {
602
602
 
603
603
  if (message?.type === 'agent.request') {
604
604
  const isExtensionSetupRequest =
605
- socket.__role === 'extension' && message.request?.method === 'setup.install';
605
+ socket.__role === 'extension' &&
606
+ (message.request?.method === 'setup.get_status' ||
607
+ message.request?.method === 'setup.install');
606
608
  if (socket.__role !== 'agent' && !isExtensionSetupRequest) {
607
609
  return this.rejectMessageForRole(socket, message);
608
610
  }
@@ -64,7 +64,7 @@ export const METHOD_CAPABILITIES = Object.freeze({
64
64
  'page.get_storage': CAPABILITIES.PAGE_READ,
65
65
  'page.get_text': CAPABILITIES.PAGE_READ,
66
66
  'page.get_network': CAPABILITIES.NETWORK_READ,
67
- // Interception mutates traffic (block/forge responses) — distinct from
67
+ // Interception mutates traffic (block/forge responses) - distinct from
68
68
  // passive network reads so hosts can gate it independently.
69
69
  'network.intercept.add': CAPABILITIES.NETWORK_INTERCEPT,
70
70
  'network.intercept.remove': CAPABILITIES.NETWORK_INTERCEPT,
@@ -361,6 +361,7 @@ function normalizeRequestParams(method, params) {
361
361
  case 'input.click':
362
362
  case 'input.focus':
363
363
  case 'input.type':
364
+ case 'input.fill':
364
365
  case 'input.press_key':
365
366
  return normalizeInputAction(params);
366
367
  case 'cdp.dispatch_key_event':
@@ -412,7 +413,7 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
412
413
  case 'page.evaluate':
413
414
  return normalizeEvaluateParams(params).timeoutMs;
414
415
  case 'dom.wait_for':
415
- return normalizeWaitForParams(params).timeoutMs;
416
+ return clampInt(params.timeoutMs, 100, 30_000, 5_000);
416
417
  default:
417
418
  return null;
418
419
  }
@@ -423,11 +424,13 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
423
424
  * @returns {NormalizedDomQuery}
424
425
  */
425
426
  export function normalizeDomQuery(params = {}) {
427
+ const nestedBudget =
428
+ params.budget && typeof params.budget === 'object' ? params.budget : undefined;
426
429
  return {
427
430
  selector:
428
431
  typeof params.selector === 'string' && params.selector.trim() ? params.selector : 'body',
429
432
  withinRef: typeof params.withinRef === 'string' ? params.withinRef : null,
430
- budget: applyBudget(params),
433
+ budget: applyBudget({ ...nestedBudget, ...params }),
431
434
  };
432
435
  }
433
436
 
@@ -706,9 +709,18 @@ export function normalizeConsoleParams(params = {}) {
706
709
  */
707
710
  export function normalizeWaitForParams(params = {}) {
708
711
  const validStates = ['attached', 'detached', 'visible', 'hidden'];
712
+ const selector =
713
+ typeof params.selector === 'string' && params.selector.trim() ? params.selector : '';
714
+ const text = typeof params.text === 'string' && params.text.trim() ? params.text : null;
715
+ if (!selector && text === null) {
716
+ throw new BridgeError(
717
+ ERROR_CODES.INVALID_REQUEST,
718
+ 'selector or text is required for dom.wait_for.'
719
+ );
720
+ }
709
721
  return {
710
- selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '',
711
- text: params.text != null ? String(params.text) : null,
722
+ selector: selector || '*',
723
+ text,
712
724
  state: validStates.includes(String(params.state ?? ''))
713
725
  ? /** @type {'attached' | 'detached' | 'visible' | 'hidden'} */ (String(params.state))
714
726
  : 'attached',
@@ -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;
@@ -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
 
@@ -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