@browserbridge/bbx 1.7.7 → 1.8.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 (34) hide show
  1. package/PRIVACY.md +111 -0
  2. package/README.md +13 -5
  3. package/RESPONSIBLE_USE.md +16 -0
  4. package/SECURITY.md +52 -0
  5. package/package.json +12 -6
  6. package/packages/agent-client/src/cli-args.js +24 -0
  7. package/packages/agent-client/src/cli.js +46 -52
  8. package/packages/agent-client/src/client.js +140 -5
  9. package/packages/agent-client/src/command-registry.js +56 -53
  10. package/packages/agent-client/src/content-extract.js +198 -0
  11. package/packages/agent-client/src/remotes.js +1 -1
  12. package/packages/agent-client/src/runtime.js +768 -35
  13. package/packages/agent-client/src/types.ts +131 -6
  14. package/packages/mcp-server/src/guidance.js +1 -1
  15. package/packages/mcp-server/src/handlers-capture.js +41 -5
  16. package/packages/mcp-server/src/handlers-dom.js +8 -2
  17. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  18. package/packages/mcp-server/src/handlers-page.js +41 -4
  19. package/packages/mcp-server/src/handlers-utils.js +67 -7
  20. package/packages/mcp-server/src/handlers.js +4 -4
  21. package/packages/mcp-server/src/server.js +102 -6
  22. package/packages/native-host/src/daemon.js +158 -6
  23. package/packages/native-host/src/native-host.js +3 -1
  24. package/packages/protocol/src/defaults.js +3 -0
  25. package/packages/protocol/src/errors.js +54 -1
  26. package/packages/protocol/src/protocol.js +297 -1
  27. package/packages/protocol/src/registry.js +66 -35
  28. package/packages/protocol/src/summary.js +87 -5
  29. package/packages/protocol/src/types.ts +234 -0
  30. package/skills/browser-bridge/SKILL.md +34 -13
  31. package/skills/browser-bridge/references/interaction.md +55 -11
  32. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  33. package/skills/browser-bridge/references/protocol.md +157 -82
  34. package/skills/browser-bridge/references/token-efficiency.md +19 -13
@@ -4,10 +4,19 @@ import type {
4
4
  BridgeMethod,
5
5
  BridgeRequestSource,
6
6
  BridgeResponse,
7
+ ScreenshotResult,
8
+ SetupStatus,
7
9
  } from '../../protocol/src/types.js';
8
10
  import type { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
9
11
 
10
- export type { BridgeMeta, BridgeMethod, BridgeRequestSource, BridgeResponse, BridgeTransport };
12
+ export type {
13
+ BridgeMeta,
14
+ BridgeMethod,
15
+ BridgeRequestSource,
16
+ BridgeResponse,
17
+ BridgeTransport,
18
+ ScreenshotResult,
19
+ };
11
20
 
12
21
  export type McpClientName =
13
22
  | 'codex'
@@ -43,6 +52,7 @@ export interface SetupStatusOptions {
43
52
  export interface ProtocolHealthResult {
44
53
  extensionConnected?: boolean;
45
54
  supported_versions?: string[];
55
+ extension_supported_versions?: string[];
46
56
  daemon_supported_versions?: string[];
47
57
  deprecated_since?: string;
48
58
  migration_hint?: string;
@@ -78,6 +88,7 @@ export interface BridgeClientOptions {
78
88
  clientId?: string;
79
89
  defaultTimeoutMs?: number;
80
90
  autoReconnect?: boolean;
91
+ checkProtocolOnConnect?: boolean;
81
92
  restartDaemonOnVersionMismatch?: boolean;
82
93
  restartDaemonFn?: typeof restartBridgeDaemon;
83
94
  authToken?: string | null;
@@ -110,6 +121,106 @@ export interface NativeHostManifestIssue {
110
121
  message: string;
111
122
  }
112
123
 
124
+ export interface DoctorTransportDiagnostics {
125
+ kind: 'socket' | 'tcp' | 'unknown';
126
+ local: true;
127
+ status: 'reachable' | 'offline' | 'authentication_failed';
128
+ proxyConfigured: boolean;
129
+ proxyExposed: boolean | null;
130
+ credentials: 'not_required' | 'accepted' | 'rejected' | 'unknown';
131
+ }
132
+
133
+ export interface DoctorConnectionDiagnostics {
134
+ extensionCount: number;
135
+ profileCount: number;
136
+ }
137
+
138
+ export interface DoctorProtocolDiagnostics {
139
+ clientVersion: string;
140
+ daemonVersion: string | null;
141
+ daemonSupportedVersions: string[];
142
+ extensionSupportedVersions: string[];
143
+ daemonCompatible: boolean | null;
144
+ extensionCompatible: boolean | null;
145
+ compatible: boolean | null;
146
+ migration: 'none' | 'update_client' | 'restart_daemon' | 'update_extension' | 'unknown';
147
+ }
148
+
149
+ export interface DoctorDebuggerDiagnostics {
150
+ state: 'idle' | 'active' | 'conflict' | 'detached' | 'unknown';
151
+ attachedTabCount: number | null;
152
+ heldTabCount: number | null;
153
+ pendingTabCount: number | null;
154
+ recentReason:
155
+ | 'debugger_conflict'
156
+ | 'debugger_detached'
157
+ | 'debugger_replaced'
158
+ | 'debugger_canceled'
159
+ | 'target_closed'
160
+ | null;
161
+ captureState:
162
+ | 'idle'
163
+ | 'stopped'
164
+ | 'armed'
165
+ | 'active'
166
+ | 'capturing'
167
+ | 'stop_failed'
168
+ | 'unavailable'
169
+ | 'unknown';
170
+ captureActiveTabCount: number | null;
171
+ captureOwnershipCount: number | null;
172
+ captureInflightCount: number | null;
173
+ interceptionActiveTabCount: number | null;
174
+ interceptionRuleCount: number | null;
175
+ }
176
+
177
+ export interface DoctorDaemonMetrics {
178
+ uptimeMs: number;
179
+ activeAgents: number;
180
+ activeExtensions: number;
181
+ pendingRequests: number;
182
+ requestsProcessed: number;
183
+ requestsFailed: number;
184
+ avgResponseTimeMs: number;
185
+ }
186
+
187
+ export interface DoctorRecentEvent {
188
+ at?: string;
189
+ method?: BridgeMethod;
190
+ ok?: boolean;
191
+ source?: BridgeRequestSource;
192
+ cause?:
193
+ | 'debugger_conflict'
194
+ | 'debugger_detached'
195
+ | 'debugger_replaced'
196
+ | 'debugger_canceled'
197
+ | 'target_closed'
198
+ | 'dialog_conflict'
199
+ | 'extension_disconnected'
200
+ | 'wrong_window';
201
+ }
202
+
203
+ export interface DoctorSetupSummary {
204
+ source: 'daemon' | 'direct';
205
+ scope: 'global' | 'local';
206
+ mcp: {
207
+ detected: number;
208
+ configured: number;
209
+ };
210
+ skills: {
211
+ detected: number;
212
+ installed: number;
213
+ managed: number;
214
+ updatesAvailable: number;
215
+ };
216
+ }
217
+
218
+ export interface DoctorRemoteDiagnostics {
219
+ configuredCount: number;
220
+ status: 'not_configured' | 'not_probed_local_only' | 'config_unavailable';
221
+ credentials: 'not_configured' | 'unverified';
222
+ }
223
+
113
224
  export interface DoctorReport {
114
225
  manifestInstalled: boolean;
115
226
  manifestPath: string;
@@ -117,6 +228,7 @@ export interface DoctorReport {
117
228
  defaultExtensionId: string | null;
118
229
  defaultExtensionIdSource: string;
119
230
  daemonReachable: boolean;
231
+ healthAvailable: boolean;
120
232
  extensionConnected: boolean;
121
233
  accessEnabled: boolean;
122
234
  enabledWindowId: number | null;
@@ -130,6 +242,16 @@ export interface DoctorReport {
130
242
  issues: string[];
131
243
  nextSteps: string[];
132
244
  browserManifests: BrowserManifestStatus[];
245
+ transport: DoctorTransportDiagnostics;
246
+ connections: DoctorConnectionDiagnostics;
247
+ protocol: DoctorProtocolDiagnostics;
248
+ debugger: DoctorDebuggerDiagnostics;
249
+ metrics: DoctorDaemonMetrics | null;
250
+ recentEvents: DoctorRecentEvent[];
251
+ recentCauses: DoctorRecentEvent['cause'][];
252
+ setup: DoctorSetupSummary | null;
253
+ remoteDestinations: DoctorRemoteDiagnostics;
254
+ diagnosticFailures: string[];
133
255
  }
134
256
 
135
257
  export interface DoctorReportOptions {
@@ -145,6 +267,14 @@ export interface DoctorReportOptions {
145
267
  checkNativeHostManifestHealth?: (
146
268
  browserManifests: BrowserManifestStatus[]
147
269
  ) => Promise<NativeHostManifestIssue[]>;
270
+ readInstalledExtensionIds?: (browserManifests: BrowserManifestStatus[]) => Promise<string[]>;
271
+ collectSetupStatus?: () => Promise<SetupStatus>;
272
+ readRemoteConfig?: () => Promise<{
273
+ remotes: Array<{ id: string; host: string; port: number; token: string }>;
274
+ }>;
275
+ getLocalTransport?: () => BridgeTransport;
276
+ readProxyConfig?: () => { enabled: boolean } | null;
277
+ includeSetupStatus?: boolean;
148
278
  }
149
279
 
150
280
  export type BridgeClientRequest = (options: {
@@ -154,8 +284,3 @@ export type BridgeClientRequest = (options: {
154
284
  meta?: BridgeMeta;
155
285
  timeoutMs?: number;
156
286
  }) => Promise<BridgeResponse>;
157
-
158
- export interface ScreenshotResult {
159
- image: string;
160
- rect: Record<string, unknown>;
161
- }
@@ -5,7 +5,7 @@ export const MCP_SERVER_INSTRUCTIONS = [
5
5
  '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.',
6
6
  'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
7
7
  '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.',
8
- '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.',
8
+ 'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, page.extract_content, styles.get_computed, or layout.get_box_model. Prefer semantic extraction for articles and documentation. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
9
9
  'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
10
10
  '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.',
11
11
  'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
@@ -22,17 +22,28 @@ export const CAPTURE_ACTIONS = {
22
22
  element: {
23
23
  ref: true,
24
24
  method: 'screenshot.capture_element',
25
- params: (_, r) => ({ elementRef: r }),
25
+ params: (a, r) => ({
26
+ elementRef: r,
27
+ ...(a.format ? { format: a.format } : {}),
28
+ ...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
29
+ }),
26
30
  },
27
31
  region: {
28
32
  ref: false,
29
33
  method: 'screenshot.capture_region',
30
- params: (a) => /** @type {Record<string, unknown>} */ (a.rect || {}),
34
+ params: (a) => ({
35
+ .../** @type {Record<string, unknown>} */ (a.rect || {}),
36
+ ...(a.format ? { format: a.format } : {}),
37
+ ...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
38
+ }),
31
39
  },
32
40
  full_page: {
33
41
  ref: false,
34
42
  method: 'screenshot.capture_full_page',
35
- params: () => ({}),
43
+ params: (a) => ({
44
+ ...(a.format ? { format: a.format } : {}),
45
+ ...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
46
+ }),
36
47
  },
37
48
  cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
38
49
  cdp_dom_snapshot: {
@@ -82,7 +93,7 @@ function isValidCaptureRegion(rect) {
82
93
  }
83
94
 
84
95
  /**
85
- * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
96
+ * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, format?: 'png' | 'jpeg' | 'webp', quality?: number, nodeId?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
86
97
  * @returns {Promise<ToolResult>}
87
98
  */
88
99
  export async function handleCaptureTool(args) {
@@ -148,6 +159,9 @@ export function createScreenshotResult(response, method) {
148
159
  mimeType: image.mimeType,
149
160
  byteLength: image.byteLength,
150
161
  ...(rect ? { rect } : {}),
162
+ format: result.format ?? image.mimeType.replace('image/', ''),
163
+ complete: result.complete === true,
164
+ clipped: result.clipped === true,
151
165
  },
152
166
  false,
153
167
  [{ type: 'image', data: image.data, mimeType: image.mimeType }]
@@ -294,7 +308,20 @@ function createInputTarget(elementRef, selector) {
294
308
  }
295
309
 
296
310
  /**
297
- * @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
311
+ * Avoid changing legacy handler payloads when new options are omitted.
312
+ *
313
+ * @param {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }} args
314
+ * @returns {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }}
315
+ */
316
+ function createInputOptions(args) {
317
+ return {
318
+ ...(args.executionMode ? { executionMode: args.executionMode } : {}),
319
+ ...(typeof args.recoverStale === 'boolean' ? { recoverStale: args.recoverStale } : {}),
320
+ };
321
+ }
322
+
323
+ /**
324
+ * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', executionMode?: 'dom' | 'cdp', recoverStale?: boolean, 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
298
325
  * @returns {Promise<ToolResult>}
299
326
  */
300
327
  export async function handleInputTool(args) {
@@ -337,6 +364,7 @@ export async function handleInputTool(args) {
337
364
  button: args.button,
338
365
  clickCount: args.clickCount,
339
366
  modifiers: args.modifiers,
367
+ ...createInputOptions(args),
340
368
  },
341
369
  {
342
370
  tabId: requestedTabId,
@@ -352,6 +380,7 @@ export async function handleInputTool(args) {
352
380
  'input.focus',
353
381
  {
354
382
  target: elementTarget(),
383
+ ...createInputOptions(args),
355
384
  },
356
385
  {
357
386
  tabId: requestedTabId,
@@ -371,6 +400,7 @@ export async function handleInputTool(args) {
371
400
  clear: args.clear,
372
401
  submit: args.submit,
373
402
  modifiers: args.modifiers,
403
+ ...createInputOptions(args),
374
404
  },
375
405
  {
376
406
  tabId: requestedTabId,
@@ -388,6 +418,7 @@ export async function handleInputTool(args) {
388
418
  target: elementTarget(),
389
419
  value: args.value,
390
420
  mode: args.mode,
421
+ ...createInputOptions(args),
391
422
  },
392
423
  {
393
424
  tabId: requestedTabId,
@@ -406,6 +437,7 @@ export async function handleInputTool(args) {
406
437
  target,
407
438
  key: args.key,
408
439
  modifiers: args.modifiers,
440
+ ...createInputOptions(args),
409
441
  },
410
442
  {
411
443
  tabId: requestedTabId,
@@ -439,6 +471,7 @@ export async function handleInputTool(args) {
439
471
  {
440
472
  target: elementTarget(),
441
473
  checked: args.checked,
474
+ ...createInputOptions(args),
442
475
  },
443
476
  {
444
477
  tabId: requestedTabId,
@@ -457,6 +490,7 @@ export async function handleInputTool(args) {
457
490
  values: args.values,
458
491
  labels: args.labels,
459
492
  indexes: args.indexes,
493
+ ...createInputOptions(args),
460
494
  },
461
495
  {
462
496
  tabId: requestedTabId,
@@ -474,6 +508,7 @@ export async function handleInputTool(args) {
474
508
  target: elementTarget(),
475
509
  duration: args.duration,
476
510
  modifiers: args.modifiers,
511
+ ...createInputOptions(args),
477
512
  },
478
513
  {
479
514
  tabId: requestedTabId,
@@ -502,6 +537,7 @@ export async function handleInputTool(args) {
502
537
  destination,
503
538
  offsetX: args.offsetX,
504
539
  offsetY: args.offsetY,
540
+ ...createInputOptions(args),
505
541
  },
506
542
  {
507
543
  tabId: requestedTabId,
@@ -85,12 +85,18 @@ export const DOM_ACTIONS = {
85
85
  accessibility_tree: {
86
86
  ref: false,
87
87
  method: 'dom.get_accessibility_tree',
88
- params: (a) => ({ maxNodes: a.maxNodes, maxDepth: a.maxDepth }),
88
+ params: (a) => ({
89
+ selector: a.selector,
90
+ maxNodes: a.maxNodes,
91
+ maxDepth: a.maxDepth,
92
+ compact: a.compact,
93
+ interactiveOnly: a.interactiveOnly,
94
+ }),
89
95
  },
90
96
  };
91
97
 
92
98
  /**
93
- * @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
99
+ * @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
94
100
  * @returns {Promise<ToolResult>}
95
101
  */
96
102
  export async function handleDomTool(args) {
@@ -32,7 +32,8 @@ export async function handleTabsTool(args) {
32
32
  let client = null;
33
33
  try {
34
34
  client = await createBridgeClientForDestination(
35
- destination.local ? null : destination.id
35
+ destination.local ? null : destination.id,
36
+ { checkProtocolOnConnect: false }
36
37
  );
37
38
  await client.connect();
38
39
  const response = await requestBridgeWithRetry(
@@ -49,6 +49,7 @@ const BATCH_SAFE_METHODS = new Set([
49
49
  'page.wait_for_load_state',
50
50
  'page.get_storage',
51
51
  'page.get_text',
52
+ 'page.extract_content',
52
53
  'page.get_network',
53
54
  'dom.query',
54
55
  'dom.describe',
@@ -78,6 +79,14 @@ export function isBatchSafeBridgeCall(method, params) {
78
79
  if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
79
80
  return false;
80
81
  }
82
+ if (
83
+ method === 'page.get_network' &&
84
+ params.source === 'cdp' &&
85
+ params.capture !== undefined &&
86
+ params.capture !== 'read'
87
+ ) {
88
+ return false;
89
+ }
81
90
  return true;
82
91
  }
83
92
 
@@ -97,9 +106,22 @@ export const PAGE_ACTIONS = {
97
106
  method: 'page.get_console',
98
107
  params: (a) => ({ level: a.level, clear: a.clear, limit: a.limit }),
99
108
  },
109
+ handle_dialog: {
110
+ method: 'page.handle_dialog',
111
+ params: (a) => ({
112
+ action: a.dialogAction,
113
+ promptText: a.promptText,
114
+ expectedDialogId: a.expectedDialogId,
115
+ }),
116
+ },
100
117
  wait_for_load: {
101
118
  method: 'page.wait_for_load_state',
102
- params: (a) => ({ timeoutMs: a.timeoutMs }),
119
+ params: (a) => ({
120
+ timeoutMs: a.timeoutMs,
121
+ waitForLoad: a.waitForLoad,
122
+ url: a.url,
123
+ urlMatch: a.urlMatch,
124
+ }),
103
125
  },
104
126
  storage: {
105
127
  method: 'page.get_storage',
@@ -109,24 +131,37 @@ export const PAGE_ACTIONS = {
109
131
  method: 'page.get_text',
110
132
  params: (a) => ({ textBudget: a.textBudget }),
111
133
  },
134
+ extract_content: {
135
+ method: 'page.extract_content',
136
+ params: (a) => ({
137
+ format: a.format,
138
+ selector: a.selector,
139
+ includeMetadata: a.includeMetadata,
140
+ consistency: a.consistency,
141
+ textBudget: a.textBudget,
142
+ settleTimeoutMs: a.settleTimeoutMs,
143
+ }),
144
+ },
112
145
  network: {
113
146
  method: 'page.get_network',
114
147
  params: (a) => ({
115
148
  clear: a.clear,
116
149
  limit: a.limit,
117
150
  urlPattern: a.urlPattern,
151
+ source: a.source,
152
+ capture: a.capture,
118
153
  }),
119
154
  },
120
155
  performance: { method: 'performance.get_metrics', params: () => ({}) },
121
156
  };
122
157
 
123
158
  /**
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
159
+ * @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', dialogAction?: string, promptText?: string, expectedDialogId?: string, waitForLoad?: boolean, url?: string, urlMatch?: string, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
125
160
  * @returns {Promise<ToolResult>}
126
161
  */
127
162
  export async function handlePageTool(args) {
128
163
  let normalizedArgs = args;
129
- if (args.action === 'text') {
164
+ if (args.action === 'text' || args.action === 'extract_content') {
130
165
  normalizedArgs = applyPageTextBudgetPreset(args);
131
166
  } else if (args.action === 'console') {
132
167
  normalizedArgs = applyLimitBudgetPreset(args, {
@@ -201,7 +236,9 @@ export async function handleBatchTool(args) {
201
236
  /** @type {unknown} */
202
237
  let callError = null;
203
238
  try {
204
- callClient = await createBridgeClientForDestination(destinationId);
239
+ callClient = await createBridgeClientForDestination(destinationId, {
240
+ checkProtocolOnConnect: false,
241
+ });
205
242
  await callClient.connect();
206
243
  const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
207
244
  response = await requestBridgeWithRetry(callClient, method, params, {
@@ -45,6 +45,7 @@ const RETRY_SAFE_METHODS = new Set([
45
45
  'page.get_state',
46
46
  'page.get_storage',
47
47
  'page.get_text',
48
+ 'page.extract_content',
48
49
  'dom.query',
49
50
  'dom.describe',
50
51
  'dom.get_text',
@@ -75,7 +76,13 @@ const RETRY_SAFE_METHODS = new Set([
75
76
  */
76
77
  export function isRetrySafeBridgeMethod(method, params) {
77
78
  if (method === 'page.get_console' || method === 'page.get_network') {
78
- return params.clear !== true;
79
+ return (
80
+ params.clear !== true &&
81
+ (method !== 'page.get_network' ||
82
+ params.source !== 'cdp' ||
83
+ params.capture === undefined ||
84
+ params.capture === 'read')
85
+ );
79
86
  }
80
87
  return RETRY_SAFE_METHODS.has(method);
81
88
  }
@@ -136,6 +143,7 @@ export function summarizeToolResponse(response, method, params = {}) {
136
143
  if (response.ok) {
137
144
  const evidence = getRequestAwareEvidence(response.result, method, params, summary.evidence);
138
145
  summary.evidence = evidence.value;
146
+ if (evidence.metadata) summaryRecord.evidenceMeta = evidence.metadata;
139
147
  if (evidence.truncated) {
140
148
  summaryRecord.outputTruncated = true;
141
149
  summaryRecord.outputLimit = evidence.limit;
@@ -190,9 +198,11 @@ export function summarizeToolError(error) {
190
198
  export async function withToolClient(callback, options = {}) {
191
199
  try {
192
200
  if (!options.destinationId) {
193
- return await withBridgeClient(callback);
201
+ return await withBridgeClient(callback, { checkProtocolOnConnect: false });
194
202
  }
195
- const client = await createBridgeClientForDestination(options.destinationId);
203
+ const client = await createBridgeClientForDestination(options.destinationId, {
204
+ checkProtocolOnConnect: false,
205
+ });
196
206
  await client.connect();
197
207
  try {
198
208
  return await callback(client);
@@ -364,7 +374,7 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
364
374
  normalized = applyTextBudgetPreset(args);
365
375
  } else if (method === 'dom.get_html') {
366
376
  normalized = applyHtmlBudgetPreset(args);
367
- } else if (method === 'page.get_text') {
377
+ } else if (method === 'page.get_text' || method === 'page.extract_content') {
368
378
  normalized = applyPageTextBudgetPreset(args);
369
379
  } else if (method === 'page.get_console') {
370
380
  normalized = applyLimitBudgetPreset(args, {
@@ -392,7 +402,7 @@ export function applyMethodBudgetPreset(method, params, budgetPreset) {
392
402
  /**
393
403
  * @param {unknown} input
394
404
  * @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
395
- * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
405
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
396
406
  */
397
407
  export function boundToolValue(input, options = {}) {
398
408
  const maxEntries = options.maxEntries ?? 500;
@@ -451,13 +461,27 @@ export function boundToolValue(input, options = {}) {
451
461
  * @param {string | undefined} method
452
462
  * @param {Record<string, unknown>} params
453
463
  * @param {unknown} fallback
454
- * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
464
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number>, metadata?: Record<string, unknown> }}
455
465
  */
456
466
  function getRequestAwareEvidence(rawResult, method, params, fallback) {
457
467
  const result =
458
468
  rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
459
469
  ? /** @type {Record<string, unknown>} */ (rawResult)
460
470
  : {};
471
+ if (method === 'page.extract_content' && typeof result.content === 'string') {
472
+ const requested = positiveInteger(params.textBudget) ?? DEFAULT_PAGE_TEXT_BUDGET;
473
+ const maxCharacters = Math.min(requested, 16_000);
474
+ const content = result.content.slice(0, maxCharacters);
475
+ return {
476
+ value: {
477
+ ...result,
478
+ content,
479
+ returnedChars: content.length,
480
+ },
481
+ truncated: result.content.length > maxCharacters,
482
+ limit: { maxCharacters },
483
+ };
484
+ }
461
485
  if (method === 'page.get_text' || method === 'dom.get_text') {
462
486
  const text = typeof result.text === 'string' ? result.text : result.value;
463
487
  if (typeof text === 'string') {
@@ -506,14 +530,30 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
506
530
  const node =
507
531
  value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
508
532
  return {
533
+ nodeId: node.nodeId,
509
534
  role: node.role,
510
535
  name: node.name,
511
536
  ...(node.interactive === true ? { interactive: true } : {}),
537
+ ...(node.semanticInteractive === true ? { semanticInteractive: true } : {}),
538
+ ...(node.focusableAndEnabled === true ? { focusableAndEnabled: true } : {}),
512
539
  ...(node.value !== undefined ? { value: node.value } : {}),
540
+ childIds: Array.isArray(node.childIds) ? node.childIds : [],
513
541
  };
514
542
  });
515
543
  return {
516
544
  value: nodes,
545
+ metadata: {
546
+ rootIds: Array.isArray(result.rootIds) ? result.rootIds : [],
547
+ count: result.count,
548
+ total: result.total,
549
+ rawTotal: result.rawTotal,
550
+ source: result.source,
551
+ compact: result.compact,
552
+ interactiveOnly: result.interactiveOnly,
553
+ truncated: result.truncated,
554
+ truncation: result.truncation,
555
+ continuationHint: result.continuationHint,
556
+ },
517
557
  truncated: result.truncated === true || result.nodes.length > maxEntries,
518
558
  limit: { maxEntries },
519
559
  };
@@ -535,7 +575,27 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
535
575
  : result.entries.slice(0, maxEntries);
536
576
  const bounded = boundToolValue(selected, { maxEntries: 500, maxCharacters: 20_000 });
537
577
  return {
538
- value: bounded.value,
578
+ value:
579
+ method === 'page.get_network'
580
+ ? {
581
+ entries: bounded.value,
582
+ count: result.count,
583
+ total: result.total,
584
+ filteredTotal: result.filteredTotal,
585
+ dropped: result.dropped,
586
+ abandoned: result.abandoned,
587
+ source: result.source,
588
+ capture: result.capture,
589
+ armed: result.armed,
590
+ armedDuringCapture: result.armedDuringCapture,
591
+ captureState: result.captureState,
592
+ startedAt: result.startedAt,
593
+ inflight: result.inflight,
594
+ ownershipHeld: result.ownershipHeld,
595
+ truncated: result.truncated,
596
+ truncation: result.truncation,
597
+ }
598
+ : bounded.value,
539
599
  truncated: result.entries.length > maxEntries || bounded.truncated,
540
600
  limit: { maxEntries, maxCharacters: 20_000 },
541
601
  };
@@ -69,7 +69,7 @@ export {
69
69
  const HOME_DIR = os.homedir();
70
70
 
71
71
  /**
72
- * @param {{ destinationId?: string }} [args]
72
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
73
73
  * @returns {Promise<ToolResult>}
74
74
  */
75
75
  export async function handleStatusTool(args = {}) {
@@ -155,7 +155,7 @@ async function callRemoteHealth(destinationId) {
155
155
  }
156
156
 
157
157
  /**
158
- * @param {{ destinationId?: string }} [args]
158
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
159
159
  * @returns {Promise<ToolResult>}
160
160
  */
161
161
  export async function handleSkillTool(args = {}) {
@@ -231,13 +231,13 @@ export async function handleHealthTool(args = {}) {
231
231
  }
232
232
 
233
233
  /**
234
- * @param {{ destinationId?: string }} [args]
234
+ * @param {{ destinationId?: string, intent?: import('../../protocol/src/types.js').AccessIntent }} [args]
235
235
  * @returns {Promise<ToolResult>}
236
236
  */
237
237
  export async function handleAccessTool(args = {}) {
238
238
  return callBridgeTool(
239
239
  'access.request',
240
- {},
240
+ { intent: args.intent },
241
241
  {
242
242
  destinationId: typeof args.destinationId === 'string' ? args.destinationId : null,
243
243
  }