@browserbridge/bbx 1.7.8 → 1.9.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.
Files changed (44) hide show
  1. package/PRIVACY.md +57 -12
  2. package/README.md +19 -8
  3. package/SECURITY.md +12 -0
  4. package/package.json +15 -6
  5. package/packages/agent-client/src/cli-args.js +59 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +272 -70
  9. package/packages/agent-client/src/client.js +296 -8
  10. package/packages/agent-client/src/command-registry.js +64 -55
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/content-extract.js +198 -0
  13. package/packages/agent-client/src/npm-self-update.js +305 -0
  14. package/packages/agent-client/src/remotes.js +4 -2
  15. package/packages/agent-client/src/runtime.js +864 -39
  16. package/packages/agent-client/src/types.ts +169 -6
  17. package/packages/mcp-server/src/guidance.js +1 -1
  18. package/packages/mcp-server/src/handlers-capture.js +69 -6
  19. package/packages/mcp-server/src/handlers-dom.js +61 -4
  20. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  21. package/packages/mcp-server/src/handlers-page.js +211 -54
  22. package/packages/mcp-server/src/handlers-utils.js +101 -10
  23. package/packages/mcp-server/src/handlers.js +14 -7
  24. package/packages/mcp-server/src/server.js +176 -11
  25. package/packages/native-host/src/artifact-store.js +353 -0
  26. package/packages/native-host/src/config.js +9 -1
  27. package/packages/native-host/src/daemon-logger.js +3 -1
  28. package/packages/native-host/src/daemon.js +701 -33
  29. package/packages/native-host/src/native-host.js +28 -3
  30. package/packages/protocol/src/capability-values.js +2 -0
  31. package/packages/protocol/src/defaults.js +31 -0
  32. package/packages/protocol/src/errors.js +102 -6
  33. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  34. package/packages/protocol/src/index.js +2 -0
  35. package/packages/protocol/src/protocol.js +612 -6
  36. package/packages/protocol/src/recovery-telemetry.js +302 -0
  37. package/packages/protocol/src/registry.js +206 -38
  38. package/packages/protocol/src/summary.js +203 -30
  39. package/packages/protocol/src/types.ts +721 -2
  40. package/skills/browser-bridge/SKILL.md +54 -19
  41. package/skills/browser-bridge/references/interaction.md +55 -11
  42. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  43. package/skills/browser-bridge/references/protocol.md +265 -86
  44. package/skills/browser-bridge/references/token-efficiency.md +29 -13
@@ -2,9 +2,13 @@
2
2
 
3
3
  import {
4
4
  DEFAULT_CONSOLE_LIMIT,
5
+ DEFAULT_HAR_LIMIT,
5
6
  DEFAULT_NETWORK_LIMIT,
6
7
  ERROR_CODES,
7
8
  getErrorRecovery,
9
+ isBatchSafeBridgeCall,
10
+ MAX_BATCH_CALLS,
11
+ MAX_BATCH_CONCURRENCY,
8
12
  METHOD_SET,
9
13
  } from '../../protocol/src/index.js';
10
14
  import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
@@ -33,54 +37,6 @@ import { createScreenshotResult } from './handlers-capture.js';
33
37
  /** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
34
38
  /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
35
39
 
36
- export const MAX_BATCH_CALLS = 20;
37
- export const MAX_BATCH_CONCURRENCY = 5;
38
-
39
- /** @type {ReadonlySet<BridgeMethod>} */
40
- const BATCH_SAFE_METHODS = new Set([
41
- 'health.ping',
42
- 'daemon.metrics',
43
- 'tabs.list',
44
- 'skill.get_runtime_context',
45
- 'setup.get_status',
46
- 'log.tail',
47
- 'page.get_state',
48
- 'page.get_console',
49
- 'page.wait_for_load_state',
50
- 'page.get_storage',
51
- 'page.get_text',
52
- 'page.get_network',
53
- 'dom.query',
54
- 'dom.describe',
55
- 'dom.get_text',
56
- 'dom.get_attributes',
57
- 'dom.wait_for',
58
- 'dom.find_by_text',
59
- 'dom.find_by_role',
60
- 'dom.get_html',
61
- 'dom.get_accessibility_tree',
62
- 'layout.get_box_model',
63
- 'layout.hit_test',
64
- 'styles.get_computed',
65
- 'styles.get_matched_rules',
66
- 'network.intercept.list',
67
- 'patch.list',
68
- 'performance.get_metrics',
69
- ]);
70
-
71
- /**
72
- * @param {BridgeMethod} method
73
- * @param {Record<string, unknown>} params
74
- * @returns {boolean}
75
- */
76
- export function isBatchSafeBridgeCall(method, params) {
77
- if (!BATCH_SAFE_METHODS.has(method)) return false;
78
- if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
79
- return false;
80
- }
81
- return true;
82
- }
83
-
84
40
  /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
85
41
  export const PAGE_ACTIONS = {
86
42
  state: { method: 'page.get_state', params: () => ({}) },
@@ -97,9 +53,22 @@ export const PAGE_ACTIONS = {
97
53
  method: 'page.get_console',
98
54
  params: (a) => ({ level: a.level, clear: a.clear, limit: a.limit }),
99
55
  },
56
+ handle_dialog: {
57
+ method: 'page.handle_dialog',
58
+ params: (a) => ({
59
+ action: a.dialogAction,
60
+ promptText: a.promptText,
61
+ expectedDialogId: a.expectedDialogId,
62
+ }),
63
+ },
100
64
  wait_for_load: {
101
65
  method: 'page.wait_for_load_state',
102
- params: (a) => ({ timeoutMs: a.timeoutMs }),
66
+ params: (a) => ({
67
+ timeoutMs: a.timeoutMs,
68
+ waitForLoad: a.waitForLoad,
69
+ url: a.url,
70
+ urlMatch: a.urlMatch,
71
+ }),
103
72
  },
104
73
  storage: {
105
74
  method: 'page.get_storage',
@@ -109,24 +78,45 @@ export const PAGE_ACTIONS = {
109
78
  method: 'page.get_text',
110
79
  params: (a) => ({ textBudget: a.textBudget }),
111
80
  },
81
+ extract_content: {
82
+ method: 'page.extract_content',
83
+ params: (a) => ({
84
+ format: a.format,
85
+ selector: a.selector,
86
+ includeMetadata: a.includeMetadata,
87
+ consistency: a.consistency,
88
+ textBudget: a.textBudget,
89
+ settleTimeoutMs: a.settleTimeoutMs,
90
+ }),
91
+ },
112
92
  network: {
113
93
  method: 'page.get_network',
114
94
  params: (a) => ({
115
95
  clear: a.clear,
116
96
  limit: a.limit,
117
97
  urlPattern: a.urlPattern,
98
+ source: a.source,
99
+ capture: a.capture,
100
+ }),
101
+ },
102
+ har: {
103
+ method: 'network.export_har',
104
+ params: (a) => ({
105
+ limit: a.limit,
106
+ urlPattern: a.urlPattern,
107
+ delivery: a.delivery,
118
108
  }),
119
109
  },
120
110
  performance: { method: 'performance.get_metrics', params: () => ({}) },
121
111
  };
122
112
 
123
113
  /**
124
- * @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, urlPattern?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
114
+ * @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, format?: 'text' | 'markdown', selector?: string, includeMetadata?: boolean, consistency?: 'best_effort' | 'settled', settleTimeoutMs?: number, urlPattern?: string, source?: 'fetch-xhr' | 'cdp', capture?: 'read' | 'start' | 'clear' | 'stop', delivery?: 'auto' | 'inline' | 'artifact', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
125
115
  * @returns {Promise<ToolResult>}
126
116
  */
127
117
  export async function handlePageTool(args) {
128
118
  let normalizedArgs = args;
129
- if (args.action === 'text') {
119
+ if (args.action === 'text' || args.action === 'extract_content') {
130
120
  normalizedArgs = applyPageTextBudgetPreset(args);
131
121
  } else if (args.action === 'console') {
132
122
  normalizedArgs = applyLimitBudgetPreset(args, {
@@ -140,6 +130,12 @@ export async function handlePageTool(args) {
140
130
  normal: DEFAULT_NETWORK_LIMIT,
141
131
  deep: 100,
142
132
  });
133
+ } else if (args.action === 'har') {
134
+ normalizedArgs = applyLimitBudgetPreset(args, {
135
+ quick: 20,
136
+ normal: DEFAULT_HAR_LIMIT,
137
+ deep: 100,
138
+ });
143
139
  }
144
140
  const entry = PAGE_ACTIONS[normalizedArgs.action];
145
141
  if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
@@ -149,13 +145,53 @@ export async function handlePageTool(args) {
149
145
  ) {
150
146
  return summarizeToolError('expression is required for page evaluate.');
151
147
  }
152
- return callBridgeTool(entry.method, entry.params(normalizedArgs), {
148
+ const params = entry.params(normalizedArgs);
149
+ if (entry.method === 'network.export_har') {
150
+ return withToolClient(
151
+ async (client) => {
152
+ const response = await requestBridgeWithRetry(client, entry.method, params, {
153
+ tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
154
+ source: REQUEST_SOURCE,
155
+ tokenBudget: getToolTokenBudget(normalizedArgs),
156
+ });
157
+ return createHarExportResult(response);
158
+ },
159
+ { destinationId: normalizedArgs.destinationId ?? null }
160
+ );
161
+ }
162
+ return callBridgeTool(entry.method, params, {
153
163
  tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
154
164
  tokenBudget: getToolTokenBudget(normalizedArgs),
155
165
  destinationId: normalizedArgs.destinationId ?? null,
156
166
  });
157
167
  }
158
168
 
169
+ /**
170
+ * Deliberately read one exact storage value without retries, budgeting, or
171
+ * value-derived delivery metrics.
172
+ *
173
+ * @param {{ source: 'local_storage' | 'session_storage', key: string, tabId?: number, destinationId?: string }} args
174
+ * @returns {Promise<ToolResult>}
175
+ */
176
+ export async function handleSensitiveReadTool(args) {
177
+ return withToolClient(
178
+ async (client) => {
179
+ const response = await requestBridgeWithRetry(
180
+ client,
181
+ 'sensitive.read',
182
+ { source: args.source, key: args.key },
183
+ {
184
+ tabId: typeof args.tabId === 'number' ? args.tabId : null,
185
+ source: REQUEST_SOURCE,
186
+ tokenBudget: null,
187
+ }
188
+ );
189
+ return createSensitiveReadResult(response);
190
+ },
191
+ { destinationId: args.destinationId ?? null }
192
+ );
193
+ }
194
+
159
195
  /**
160
196
  * @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
161
197
  * @returns {Promise<ToolResult>}
@@ -201,7 +237,9 @@ export async function handleBatchTool(args) {
201
237
  /** @type {unknown} */
202
238
  let callError = null;
203
239
  try {
204
- callClient = await createBridgeClientForDestination(destinationId);
240
+ callClient = await createBridgeClientForDestination(destinationId, {
241
+ checkProtocolOnConnect: false,
242
+ });
205
243
  await callClient.connect();
206
244
  const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
207
245
  response = await requestBridgeWithRetry(callClient, method, params, {
@@ -324,17 +362,136 @@ export async function handleRawCallTool(args) {
324
362
  const response = await requestBridgeWithRetry(client, method, params, {
325
363
  tabId: typeof args.tabId === 'number' ? args.tabId : null,
326
364
  source: REQUEST_SOURCE,
327
- tokenBudget: method.startsWith('screenshot.') ? null : getToolTokenBudget(args),
365
+ tokenBudget:
366
+ method.startsWith('screenshot.') || method === 'sensitive.read'
367
+ ? null
368
+ : getToolTokenBudget(args),
328
369
  });
329
370
  if (response.ok && method.startsWith('screenshot.')) {
330
371
  return createScreenshotResult(response, method);
331
372
  }
373
+ if (method === 'network.export_har') {
374
+ return createHarExportResult(response);
375
+ }
376
+ if (method === 'sensitive.read') {
377
+ return createSensitiveReadResult(response);
378
+ }
332
379
  return summarizeToolResponse(response, method, params);
333
380
  },
334
381
  { destinationId: args.destinationId ?? null }
335
382
  );
336
383
  }
337
384
 
385
+ /**
386
+ * Preserve the atomic HAR payload exactly. Generic evidence bounding must not
387
+ * turn a valid HAR document into a structurally invalid partial document.
388
+ *
389
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
390
+ * @returns {ToolResult}
391
+ */
392
+ export function createHarExportResult(response) {
393
+ if (!response.ok) return summarizeToolResponse(response, 'network.export_har', {});
394
+ const result =
395
+ response.result && typeof response.result === 'object' && !Array.isArray(response.result)
396
+ ? /** @type {Record<string, unknown>} */ (response.result)
397
+ : {};
398
+ const numericFields = [
399
+ 'byteLength',
400
+ 'entryCount',
401
+ 'totalEntries',
402
+ 'dropped',
403
+ 'abandoned',
404
+ 'inflight',
405
+ ];
406
+ const captureState = result.captureState;
407
+ const validMetadata =
408
+ result.format === 'har' &&
409
+ result.harVersion === '1.2' &&
410
+ result.mimeType === 'application/json' &&
411
+ numericFields.every((field) => typeof result[field] === 'number') &&
412
+ (typeof result.startedAt === 'number' || result.startedAt === null) &&
413
+ (captureState === 'armed' ||
414
+ captureState === 'stop_failed' ||
415
+ captureState === 'stopped' ||
416
+ captureState === 'instrumented') &&
417
+ typeof result.truncated === 'boolean' &&
418
+ result.truncation !== null &&
419
+ typeof result.truncation === 'object';
420
+ if (!validMetadata) {
421
+ return summarizeToolError('network.export_har returned invalid HAR metadata.');
422
+ }
423
+
424
+ if (result.delivery === 'inline') {
425
+ const har =
426
+ result.har && typeof result.har === 'object' && !Array.isArray(result.har)
427
+ ? /** @type {Record<string, unknown>} */ (result.har)
428
+ : {};
429
+ const log =
430
+ har.log && typeof har.log === 'object' && !Array.isArray(har.log)
431
+ ? /** @type {Record<string, unknown>} */ (har.log)
432
+ : {};
433
+ if (log.version !== '1.2' || !Array.isArray(log.entries)) {
434
+ return summarizeToolError('network.export_har returned an invalid inline HAR document.');
435
+ }
436
+ } else if (result.delivery === 'artifact') {
437
+ const artifact =
438
+ result.artifact && typeof result.artifact === 'object' && !Array.isArray(result.artifact)
439
+ ? /** @type {Record<string, unknown>} */ (result.artifact)
440
+ : {};
441
+ if (
442
+ artifact.kind !== 'har' ||
443
+ artifact.mimeType !== 'application/json' ||
444
+ typeof artifact.artifactId !== 'string' ||
445
+ typeof artifact.byteLength !== 'number' ||
446
+ typeof artifact.sha256 !== 'string' ||
447
+ !/^[a-f0-9]{64}$/u.test(artifact.sha256) ||
448
+ typeof artifact.chunkSize !== 'number' ||
449
+ typeof artifact.chunkCount !== 'number' ||
450
+ typeof artifact.createdAt !== 'string' ||
451
+ typeof artifact.expiresAt !== 'string'
452
+ ) {
453
+ return summarizeToolError('network.export_har returned invalid artifact metadata.');
454
+ }
455
+ } else {
456
+ return summarizeToolError('network.export_har returned an invalid delivery mode.');
457
+ }
458
+
459
+ const summary = summarizeBridgeResponse(response, 'network.export_har');
460
+ return createToolResult(summary.summary, result);
461
+ }
462
+
463
+ /**
464
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
465
+ * @returns {ToolResult}
466
+ */
467
+ function createSensitiveReadResult(response) {
468
+ if (!response.ok) return summarizeToolResponse(response, 'sensitive.read', {});
469
+ const result =
470
+ response.result && typeof response.result === 'object'
471
+ ? /** @type {Record<string, unknown>} */ (response.result)
472
+ : {};
473
+ if (
474
+ (result.source !== 'local_storage' && result.source !== 'session_storage') ||
475
+ typeof result.value !== 'string' ||
476
+ result.exact !== true
477
+ ) {
478
+ return summarizeToolError('sensitive.read returned an invalid exact-value response.');
479
+ }
480
+ return {
481
+ content: [
482
+ {
483
+ type: 'text',
484
+ text: 'Sensitive storage read succeeded. The exact value is available in structuredContent.value.',
485
+ },
486
+ ],
487
+ structuredContent: {
488
+ source: result.source,
489
+ value: result.value,
490
+ exact: true,
491
+ },
492
+ };
493
+ }
494
+
338
495
  /**
339
496
  * @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
340
497
  */
@@ -1,9 +1,12 @@
1
1
  // @ts-check
2
2
 
3
+ import { randomUUID } from 'node:crypto';
4
+
3
5
  import {
4
6
  BridgeError,
5
7
  bridgeMethodNeedsTab,
6
8
  DEFAULT_CONSOLE_LIMIT,
9
+ DEFAULT_HAR_LIMIT,
7
10
  DEFAULT_LOG_TAIL_LIMIT,
8
11
  DEFAULT_MAX_HTML_LENGTH,
9
12
  DEFAULT_MAX_NODES,
@@ -33,6 +36,7 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
33
36
  /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
34
37
 
35
38
  export const REQUEST_SOURCE = 'mcp';
39
+ const MCP_CLIENT_ID = `mcp_${randomUUID()}`;
36
40
 
37
41
  /** @type {ReadonlySet<BridgeMethod>} */
38
42
  const RETRY_SAFE_METHODS = new Set([
@@ -45,7 +49,10 @@ const RETRY_SAFE_METHODS = new Set([
45
49
  'page.get_state',
46
50
  'page.get_storage',
47
51
  'page.get_text',
52
+ 'page.extract_content',
48
53
  'dom.query',
54
+ 'dom.baseline.compare',
55
+ 'dom.baseline.describe',
49
56
  'dom.describe',
50
57
  'dom.get_text',
51
58
  'dom.get_attributes',
@@ -75,7 +82,13 @@ const RETRY_SAFE_METHODS = new Set([
75
82
  */
76
83
  export function isRetrySafeBridgeMethod(method, params) {
77
84
  if (method === 'page.get_console' || method === 'page.get_network') {
78
- return params.clear !== true;
85
+ return (
86
+ params.clear !== true &&
87
+ (method !== 'page.get_network' ||
88
+ params.source !== 'cdp' ||
89
+ params.capture === undefined ||
90
+ params.capture === 'read')
91
+ );
79
92
  }
80
93
  return RETRY_SAFE_METHODS.has(method);
81
94
  }
@@ -136,6 +149,7 @@ export function summarizeToolResponse(response, method, params = {}) {
136
149
  if (response.ok) {
137
150
  const evidence = getRequestAwareEvidence(response.result, method, params, summary.evidence);
138
151
  summary.evidence = evidence.value;
152
+ if (evidence.metadata) summaryRecord.evidenceMeta = evidence.metadata;
139
153
  if (evidence.truncated) {
140
154
  summaryRecord.outputTruncated = true;
141
155
  summaryRecord.outputLimit = evidence.limit;
@@ -190,9 +204,15 @@ export function summarizeToolError(error) {
190
204
  export async function withToolClient(callback, options = {}) {
191
205
  try {
192
206
  if (!options.destinationId) {
193
- return await withBridgeClient(callback);
207
+ return await withBridgeClient(callback, {
208
+ checkProtocolOnConnect: false,
209
+ clientId: MCP_CLIENT_ID,
210
+ });
194
211
  }
195
- const client = await createBridgeClientForDestination(options.destinationId);
212
+ const client = await createBridgeClientForDestination(options.destinationId, {
213
+ checkProtocolOnConnect: false,
214
+ clientId: MCP_CLIENT_ID,
215
+ });
196
216
  await client.connect();
197
217
  try {
198
218
  return await callback(client);
@@ -357,14 +377,26 @@ export function applyHtmlBudgetPreset(args) {
357
377
  */
358
378
  export function applyMethodBudgetPreset(method, params, budgetPreset) {
359
379
  const args = { ...params, budgetPreset };
380
+ /** @type {Record<string, unknown>} */
360
381
  let normalized = args;
361
- if (method === 'dom.query' || method === 'dom.get_accessibility_tree') {
382
+ if (
383
+ method === 'dom.query' ||
384
+ method === 'dom.get_accessibility_tree' ||
385
+ method === 'dom.baseline.create'
386
+ ) {
362
387
  normalized = applyTreeBudgetPreset(args);
388
+ } else if (method === 'dom.baseline.compare') {
389
+ const preset = getBudgetPresetName(budgetPreset);
390
+ const defaults = { quick: 10, normal: 50, deep: 100 };
391
+ normalized = {
392
+ ...args,
393
+ maxChanges: params.maxChanges ?? (preset ? defaults[preset] : undefined),
394
+ };
363
395
  } else if (method === 'dom.get_text') {
364
396
  normalized = applyTextBudgetPreset(args);
365
397
  } else if (method === 'dom.get_html') {
366
398
  normalized = applyHtmlBudgetPreset(args);
367
- } else if (method === 'page.get_text') {
399
+ } else if (method === 'page.get_text' || method === 'page.extract_content') {
368
400
  normalized = applyPageTextBudgetPreset(args);
369
401
  } else if (method === 'page.get_console') {
370
402
  normalized = applyLimitBudgetPreset(args, {
@@ -378,6 +410,12 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
378
410
  normal: DEFAULT_NETWORK_LIMIT,
379
411
  deep: 100,
380
412
  });
413
+ } else if (method === 'network.export_har') {
414
+ normalized = applyLimitBudgetPreset(args, {
415
+ quick: 20,
416
+ normal: DEFAULT_HAR_LIMIT,
417
+ deep: 100,
418
+ });
381
419
  } else if (method === 'log.tail') {
382
420
  normalized = applyLimitBudgetPreset(args, {
383
421
  quick: 10,
@@ -392,7 +430,7 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
392
430
  /**
393
431
  * @param {unknown} input
394
432
  * @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
395
- * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
433
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
396
434
  */
397
435
  export function boundToolValue(input, options = {}) {
398
436
  const maxEntries = options.maxEntries ?? 500;
@@ -451,13 +489,27 @@ export function boundToolValue(input, options = {}) {
451
489
  * @param {string | undefined} method
452
490
  * @param {Record<string, unknown>} params
453
491
  * @param {unknown} fallback
454
- * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
492
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
455
493
  */
456
494
  function getRequestAwareEvidence(rawResult, method, params, fallback) {
457
495
  const result =
458
496
  rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
459
497
  ? /** @type {Record<string, unknown>} */ (rawResult)
460
498
  : {};
499
+ if (method === 'page.extract_content' && typeof result.content === 'string') {
500
+ const requested = positiveInteger(params.textBudget) ?? DEFAULT_PAGE_TEXT_BUDGET;
501
+ const maxCharacters = Math.min(requested, 16_000);
502
+ const content = result.content.slice(0, maxCharacters);
503
+ return {
504
+ value: {
505
+ ...result,
506
+ content,
507
+ returnedChars: content.length,
508
+ },
509
+ truncated: result.content.length > maxCharacters,
510
+ limit: { maxCharacters },
511
+ };
512
+ }
461
513
  if (method === 'page.get_text' || method === 'dom.get_text') {
462
514
  const text = typeof result.text === 'string' ? result.text : result.value;
463
515
  if (typeof text === 'string') {
@@ -506,14 +558,30 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
506
558
  const node =
507
559
  value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
508
560
  return {
561
+ nodeId: node.nodeId,
509
562
  role: node.role,
510
563
  name: node.name,
511
564
  ...(node.interactive === true ? { interactive: true } : {}),
565
+ ...(node.semanticInteractive === true ? { semanticInteractive: true } : {}),
566
+ ...(node.focusableAndEnabled === true ? { focusableAndEnabled: true } : {}),
512
567
  ...(node.value !== undefined ? { value: node.value } : {}),
568
+ childIds: Array.isArray(node.childIds) ? node.childIds : [],
513
569
  };
514
570
  });
515
571
  return {
516
572
  value: nodes,
573
+ metadata: {
574
+ rootIds: Array.isArray(result.rootIds) ? result.rootIds : [],
575
+ count: result.count,
576
+ total: result.total,
577
+ rawTotal: result.rawTotal,
578
+ source: result.source,
579
+ compact: result.compact,
580
+ interactiveOnly: result.interactiveOnly,
581
+ truncated: result.truncated,
582
+ truncation: result.truncation,
583
+ continuationHint: result.continuationHint,
584
+ },
517
585
  truncated: result.truncated === true || result.nodes.length > maxEntries,
518
586
  limit: { maxEntries },
519
587
  };
@@ -535,7 +603,27 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
535
603
  : result.entries.slice(0, maxEntries);
536
604
  const bounded = boundToolValue(selected, { maxEntries: 500, maxCharacters: 20_000 });
537
605
  return {
538
- value: bounded.value,
606
+ value:
607
+ method === 'page.get_network'
608
+ ? {
609
+ entries: bounded.value,
610
+ count: result.count,
611
+ total: result.total,
612
+ filteredTotal: result.filteredTotal,
613
+ dropped: result.dropped,
614
+ abandoned: result.abandoned,
615
+ source: result.source,
616
+ capture: result.capture,
617
+ armed: result.armed,
618
+ armedDuringCapture: result.armedDuringCapture,
619
+ captureState: result.captureState,
620
+ startedAt: result.startedAt,
621
+ inflight: result.inflight,
622
+ ownershipHeld: result.ownershipHeld,
623
+ truncated: result.truncated,
624
+ truncation: result.truncation,
625
+ }
626
+ : bounded.value,
539
627
  truncated: result.entries.length > maxEntries || bounded.truncated,
540
628
  limit: { maxEntries, maxCharacters: 20_000 },
541
629
  };
@@ -577,7 +665,7 @@ function compactDomNode(value) {
577
665
  * @param {import('../../agent-client/src/client.js').BridgeClient} client
578
666
  * @param {BridgeMethod} method
579
667
  * @param {Record<string, unknown>} params
580
- * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null }} options
668
+ * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} options
581
669
  * @returns {Promise<BridgeResponse>}
582
670
  */
583
671
  export async function requestBridgeWithRetry(client, method, params, options) {
@@ -592,7 +680,10 @@ export async function requestBridgeWithRetry(client, method, params, options) {
592
680
  `[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
593
681
  );
594
682
  await new Promise((r) => setTimeout(r, delay));
595
- return requestBridge(client, method, params, options);
683
+ return requestBridge(client, method, params, {
684
+ ...options,
685
+ automaticRetry: 'mcp_second_attempt',
686
+ });
596
687
  }
597
688
  return response;
598
689
  }
@@ -54,22 +54,24 @@ export {
54
54
  } from './handlers-capture.js';
55
55
 
56
56
  export {
57
- MAX_BATCH_CALLS,
58
- MAX_BATCH_CONCURRENCY,
59
57
  PAGE_ACTIONS,
60
58
  handlePageTool,
59
+ handleSensitiveReadTool,
61
60
  handleBatchTool,
62
61
  handleRawCallTool,
62
+ createHarExportResult,
63
63
  handleInvestigateTool,
64
64
  } from './handlers-page.js';
65
65
 
66
+ export { MAX_BATCH_CALLS, MAX_BATCH_CONCURRENCY } from '../../protocol/src/index.js';
67
+
66
68
  /** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
67
69
  /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
68
70
 
69
71
  const HOME_DIR = os.homedir();
70
72
 
71
73
  /**
72
- * @param {{ destinationId?: string }} [args]
74
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
73
75
  * @returns {Promise<ToolResult>}
74
76
  */
75
77
  export async function handleStatusTool(args = {}) {
@@ -97,8 +99,13 @@ export async function handleStatusTool(args = {}) {
97
99
  }
98
100
 
99
101
  const report = await getDoctorReport();
102
+ const configuredDestinations = await getBridgeDestinations();
103
+ const destinationsToInspect =
104
+ requestedDestinationId === 'local'
105
+ ? configuredDestinations.filter((destination) => destination.local)
106
+ : configuredDestinations;
100
107
  const destinations = await Promise.all(
101
- (await getBridgeDestinations()).map(async (destination) => {
108
+ destinationsToInspect.map(async (destination) => {
102
109
  if (destination.local) {
103
110
  return {
104
111
  ...destination,
@@ -155,7 +162,7 @@ async function callRemoteHealth(destinationId) {
155
162
  }
156
163
 
157
164
  /**
158
- * @param {{ destinationId?: string }} [args]
165
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
159
166
  * @returns {Promise<ToolResult>}
160
167
  */
161
168
  export async function handleSkillTool(args = {}) {
@@ -231,13 +238,13 @@ export async function handleHealthTool(args = {}) {
231
238
  }
232
239
 
233
240
  /**
234
- * @param {{ destinationId?: string }} [args]
241
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
235
242
  * @returns {Promise<ToolResult>}
236
243
  */
237
244
  export async function handleAccessTool(args = {}) {
238
245
  return callBridgeTool(
239
246
  'access.request',
240
- {},
247
+ { intent: args.intent },
241
248
  {
242
249
  destinationId: typeof args.destinationId === 'string' ? args.destinationId : null,
243
250
  }