@browserbridge/bbx 1.7.4 → 1.7.6

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/package.json +6 -6
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-args.js +152 -0
  4. package/packages/agent-client/src/cli-batch.js +140 -0
  5. package/packages/agent-client/src/cli-helpers.js +20 -3
  6. package/packages/agent-client/src/cli-output.js +57 -0
  7. package/packages/agent-client/src/cli-proxy-remote.js +350 -0
  8. package/packages/agent-client/src/cli-setup-commands.js +404 -0
  9. package/packages/agent-client/src/cli.js +63 -906
  10. package/packages/agent-client/src/command-registry.js +12 -6
  11. package/packages/agent-client/src/install.js +39 -4
  12. package/packages/agent-client/src/mcp-config.js +28 -14
  13. package/packages/agent-client/src/remotes.js +93 -15
  14. package/packages/agent-client/src/runtime.js +6 -4
  15. package/packages/mcp-server/src/bin.js +10 -1
  16. package/packages/mcp-server/src/guidance.js +1 -1
  17. package/packages/mcp-server/src/handlers-capture.js +8 -5
  18. package/packages/mcp-server/src/handlers-dom.js +2 -2
  19. package/packages/mcp-server/src/handlers-navigation.js +20 -9
  20. package/packages/mcp-server/src/handlers-page.js +111 -111
  21. package/packages/mcp-server/src/handlers-utils.js +268 -8
  22. package/packages/mcp-server/src/handlers.js +31 -10
  23. package/packages/mcp-server/src/lifecycle.js +413 -0
  24. package/packages/mcp-server/src/server.js +27 -19
  25. package/packages/native-host/src/auth-token.js +1 -1
  26. package/packages/native-host/src/config.js +9 -4
  27. package/packages/native-host/src/daemon-process.js +8 -1
  28. package/packages/protocol/src/capabilities.js +33 -110
  29. package/packages/protocol/src/capability-values.js +45 -0
  30. package/packages/protocol/src/defaults.js +19 -2
  31. package/packages/protocol/src/protocol.js +106 -5
  32. package/packages/protocol/src/registry.js +299 -86
  33. package/packages/protocol/src/types.ts +19 -1
  34. package/skills/browser-bridge/SKILL.md +1 -1
@@ -3,13 +3,17 @@
3
3
  import {
4
4
  DEFAULT_CONSOLE_LIMIT,
5
5
  DEFAULT_NETWORK_LIMIT,
6
+ ERROR_CODES,
7
+ getErrorRecovery,
6
8
  METHOD_SET,
7
9
  } from '../../protocol/src/index.js';
8
10
  import { createBridgeClientForDestination } from '../../agent-client/src/remotes.js';
9
11
  import {
10
12
  annotateBridgeSummary,
11
13
  applyLimitBudgetPreset,
14
+ applyMethodBudgetPreset,
12
15
  applyPageTextBudgetPreset,
16
+ boundToolValue,
13
17
  bridgeMethodNeedsTab,
14
18
  callBridgeTool,
15
19
  createToolResult,
@@ -19,9 +23,11 @@ import {
19
23
  summarizeBatchResponseItem,
20
24
  summarizeBridgeResponse,
21
25
  summarizeToolError,
26
+ summarizeToolResponse,
22
27
  withToolClient,
23
28
  REQUEST_SOURCE,
24
29
  } from './handlers-utils.js';
30
+ import { createScreenshotResult } from './handlers-capture.js';
25
31
 
26
32
  /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
27
33
  /** @typedef {import('../../agent-client/src/client.js').BridgeClient} BridgeClient */
@@ -178,92 +184,105 @@ export async function handleBatchTool(args) {
178
184
  }
179
185
  }
180
186
 
181
- /** @param {BridgeClient | null} client */
182
- const executeBatch = async (client) => {
183
- const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
184
- const method = /** @type {BridgeMethod} */ (call.method);
185
- const tabId = bridgeMethodNeedsTab(method)
186
- ? typeof call.tabId === 'number'
187
- ? call.tabId
188
- : null
189
- : null;
190
- const tokenBudget = getToolTokenBudget(call);
191
- const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
192
-
193
- const startTime = Date.now();
194
- const callClient = destinationId
195
- ? await createBridgeClientForDestination(destinationId)
196
- : client;
197
- if (!callClient) {
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
- );
207
- }
208
- try {
209
- if (destinationId) {
210
- await callClient.connect();
211
- }
212
- const response = await requestBridgeWithRetry(callClient, method, call.params || {}, {
213
- tabId,
214
- source: REQUEST_SOURCE,
215
- tokenBudget,
216
- });
217
- return {
218
- destinationId,
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
- {
232
- method,
233
- tabId,
234
- error,
235
- durationMs: Date.now() - startTime,
236
- },
237
- { compact: true }
238
- );
239
- } finally {
240
- if (destinationId) {
187
+ const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
188
+ const method = /** @type {BridgeMethod} */ (call.method);
189
+ const tabId = bridgeMethodNeedsTab(method)
190
+ ? typeof call.tabId === 'number'
191
+ ? call.tabId
192
+ : null
193
+ : null;
194
+ const tokenBudget = getToolTokenBudget(call);
195
+ const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
196
+ const startTime = Date.now();
197
+ /** @type {BridgeClient | null} */
198
+ let callClient = null;
199
+ /** @type {import('../../protocol/src/types.js').BridgeResponse | null} */
200
+ let response = null;
201
+ /** @type {unknown} */
202
+ let callError = null;
203
+ try {
204
+ callClient = await createBridgeClientForDestination(destinationId);
205
+ await callClient.connect();
206
+ const params = applyMethodBudgetPreset(method, call.params || {}, call.budgetPreset);
207
+ response = await requestBridgeWithRetry(callClient, method, params, {
208
+ tabId,
209
+ source: REQUEST_SOURCE,
210
+ tokenBudget,
211
+ });
212
+ } catch (error) {
213
+ callError = error;
214
+ } finally {
215
+ if (callClient) {
216
+ try {
241
217
  await callClient.close();
218
+ } catch (error) {
219
+ callError ??= error;
242
220
  }
243
221
  }
244
- });
222
+ }
223
+ if (callError || !response) {
224
+ return {
225
+ destinationId,
226
+ ...summarizeThrownBatchError(method, tabId, callError, Date.now() - startTime),
227
+ };
228
+ }
229
+ return {
230
+ destinationId,
231
+ ...summarizeBatchResponseItem(
232
+ { method, tabId, response, durationMs: Date.now() - startTime },
233
+ { compact: true }
234
+ ),
235
+ };
236
+ });
245
237
 
246
- const failureCount = results.filter((result) => !result.ok).length;
247
- const summary =
248
- failureCount === 0
249
- ? `Batch executed ${results.length} call(s).`
250
- : `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
251
- return createToolResult(
252
- summary,
253
- {
254
- ok: failureCount === 0,
255
- successCount: results.length - failureCount,
256
- failureCount,
257
- compactOutput: true,
258
- results,
259
- },
260
- failureCount > 0
261
- );
262
- };
238
+ const failureCount = results.filter((result) => !result.ok).length;
239
+ const summary =
240
+ failureCount === 0
241
+ ? `Batch executed ${results.length} call(s).`
242
+ : `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
243
+ return createToolResult(
244
+ summary,
245
+ {
246
+ ok: failureCount === 0,
247
+ successCount: results.length - failureCount,
248
+ failureCount,
249
+ compactOutput: true,
250
+ results,
251
+ },
252
+ failureCount > 0
253
+ );
254
+ }
255
+
256
+ /** @type {ReadonlySet<string>} */
257
+ const RECOGNIZED_ERROR_CODES = new Set(Object.values(ERROR_CODES));
263
258
 
264
- return calls.every((call) => typeof call?.destinationId === 'string')
265
- ? executeBatch(null)
266
- : withToolClient(executeBatch);
259
+ /**
260
+ * @param {BridgeMethod} method
261
+ * @param {number | null} tabId
262
+ * @param {unknown} error
263
+ * @param {number} durationMs
264
+ */
265
+ function summarizeThrownBatchError(method, tabId, error, durationMs) {
266
+ const item = summarizeBatchErrorItem({ method, tabId, error, durationMs }, { compact: true });
267
+ const record =
268
+ error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
269
+ const code = /** @type {import('../../protocol/src/types.js').ErrorCode} */ (
270
+ typeof record.code === 'string' && RECOGNIZED_ERROR_CODES.has(record.code)
271
+ ? record.code
272
+ : 'INTERNAL_ERROR'
273
+ );
274
+ const message = error instanceof Error ? error.message : String(error ?? 'Bridge call failed.');
275
+ const details = Object.hasOwn(record, 'details') ? record.details : null;
276
+ const boundedDetails = boundToolValue(details);
277
+ const recovery = getErrorRecovery(code);
278
+ return {
279
+ ...item,
280
+ summary: `${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
281
+ evidence: boundedDetails.value,
282
+ recovery,
283
+ error: { code, message, details: boundedDetails.value },
284
+ ...(boundedDetails.truncated ? { outputTruncated: true } : {}),
285
+ };
267
286
  }
268
287
 
269
288
  /**
@@ -291,45 +310,26 @@ async function mapWithConcurrency(values, concurrency, callback) {
291
310
  }
292
311
 
293
312
  /**
294
- * @param {{ method: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string }} args
313
+ * @param {{ method: string, params?: Record<string, unknown>, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
295
314
  * @returns {Promise<ToolResult>}
296
315
  */
297
316
  export async function handleRawCallTool(args) {
298
317
  if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
299
318
  return summarizeToolError(`Unknown bridge method "${args.method}".`);
300
319
  }
301
- if (args.method === 'setup.install') {
302
- return summarizeToolError('Use browser_setup for setup changes.');
303
- }
304
-
305
320
  return withToolClient(
306
321
  async (client) => {
307
- const response = await requestBridgeWithRetry(
308
- client,
309
- /** @type {BridgeMethod} */ (args.method),
310
- args.params || {},
311
- {
312
- tabId: typeof args.tabId === 'number' ? args.tabId : null,
313
- source: REQUEST_SOURCE,
314
- }
315
- );
316
-
317
- if (!response.ok) {
318
- return createToolResult(
319
- response.error.message,
320
- {
321
- ok: false,
322
- error: response.error,
323
- response,
324
- },
325
- true
326
- );
327
- }
328
-
329
- return createToolResult(`Called ${args.method}.`, {
330
- ok: true,
331
- response: response.result,
322
+ const method = /** @type {BridgeMethod} */ (args.method);
323
+ const params = applyMethodBudgetPreset(method, args.params || {}, args.budgetPreset);
324
+ const response = await requestBridgeWithRetry(client, method, params, {
325
+ tabId: typeof args.tabId === 'number' ? args.tabId : null,
326
+ source: REQUEST_SOURCE,
327
+ tokenBudget: method.startsWith('screenshot.') ? null : getToolTokenBudget(args),
332
328
  });
329
+ if (response.ok && method.startsWith('screenshot.')) {
330
+ return createScreenshotResult(response, method);
331
+ }
332
+ return summarizeToolResponse(response, method, params);
333
333
  },
334
334
  { destinationId: args.destinationId ?? null }
335
335
  );
@@ -1,9 +1,15 @@
1
1
  // @ts-check
2
2
 
3
3
  import {
4
+ BridgeError,
4
5
  bridgeMethodNeedsTab,
6
+ DEFAULT_CONSOLE_LIMIT,
7
+ DEFAULT_LOG_TAIL_LIMIT,
5
8
  DEFAULT_MAX_HTML_LENGTH,
9
+ DEFAULT_MAX_NODES,
10
+ DEFAULT_NETWORK_LIMIT,
6
11
  DEFAULT_PAGE_TEXT_BUDGET,
12
+ DEFAULT_TEXT_BUDGET,
7
13
  estimateJsonPayloadCost,
8
14
  getBudgetPreset,
9
15
  getErrorRecovery,
@@ -121,10 +127,28 @@ export function createToolResult(
121
127
  /**
122
128
  * @param {BridgeResponse} response
123
129
  * @param {string} [method]
130
+ * @param {Record<string, unknown>} [params]
124
131
  * @returns {ToolResult}
125
132
  */
126
- export function summarizeToolResponse(response, method) {
133
+ export function summarizeToolResponse(response, method, params = {}) {
127
134
  const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
135
+ const summaryRecord = /** @type {Record<string, unknown>} */ (summary);
136
+ if (response.ok) {
137
+ const evidence = getRequestAwareEvidence(response.result, method, params, summary.evidence);
138
+ summary.evidence = evidence.value;
139
+ if (evidence.truncated) {
140
+ summaryRecord.outputTruncated = true;
141
+ summaryRecord.outputLimit = evidence.limit;
142
+ }
143
+ } else {
144
+ const bounded = boundToolValue(summary.evidence);
145
+ summary.evidence = bounded.value;
146
+ summaryRecord.error = {
147
+ ...response.error,
148
+ details: boundToolValue(response.error.details).value,
149
+ };
150
+ if (bounded.truncated) summaryRecord.outputTruncated = true;
151
+ }
128
152
  return createToolResult(summary.summary, summary, !summary.ok);
129
153
  }
130
154
 
@@ -136,15 +160,23 @@ export function summarizeToolError(error) {
136
160
  const message = error instanceof Error ? error.message : String(error);
137
161
  const record =
138
162
  error && typeof error === 'object' ? /** @type {Record<string, unknown>} */ (error) : {};
139
- const code = typeof record.code === 'string' ? record.code : 'INTERNAL_ERROR';
163
+ const code =
164
+ typeof record.code === 'string'
165
+ ? record.code
166
+ : typeof error === 'string'
167
+ ? 'INVALID_REQUEST'
168
+ : 'INTERNAL_ERROR';
169
+ const details = Object.hasOwn(record, 'details') ? record.details : null;
170
+ const boundedDetails = boundToolValue(details);
140
171
  const recovery = getErrorRecovery(code);
141
172
  return createToolResult(
142
173
  `${code}: ${message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
143
174
  {
144
175
  ok: false,
145
- evidence: null,
146
- error: { code, message },
176
+ evidence: boundedDetails.value,
177
+ error: { code, message, details: boundedDetails.value },
147
178
  recovery,
179
+ ...(boundedDetails.truncated ? { outputTruncated: true } : {}),
148
180
  },
149
181
  true
150
182
  );
@@ -185,7 +217,7 @@ export async function resolveToolRef(client, input, tabId = null) {
185
217
  if (typeof input.selector === 'string' && input.selector) {
186
218
  return resolveRef(client, input.selector, tabId, REQUEST_SOURCE);
187
219
  }
188
- throw new Error('Provide either elementRef or selector.');
220
+ throw new BridgeError('INVALID_REQUEST', 'Provide either elementRef or selector.');
189
221
  }
190
222
 
191
223
  /**
@@ -314,6 +346,233 @@ export function applyHtmlBudgetPreset(args) {
314
346
  });
315
347
  }
316
348
 
349
+ /**
350
+ * Apply a preset to method parameters before dispatch. Explicit method params
351
+ * always win over preset defaults.
352
+ *
353
+ * @param {BridgeMethod} method
354
+ * @param {Record<string, unknown>} params
355
+ * @param {unknown} budgetPreset
356
+ * @returns {Record<string, unknown>}
357
+ */
358
+ export function applyMethodBudgetPreset(method, params, budgetPreset) {
359
+ const args = { ...params, budgetPreset };
360
+ let normalized = args;
361
+ if (method === 'dom.query' || method === 'dom.get_accessibility_tree') {
362
+ normalized = applyTreeBudgetPreset(args);
363
+ } else if (method === 'dom.get_text') {
364
+ normalized = applyTextBudgetPreset(args);
365
+ } else if (method === 'dom.get_html') {
366
+ normalized = applyHtmlBudgetPreset(args);
367
+ } else if (method === 'page.get_text') {
368
+ normalized = applyPageTextBudgetPreset(args);
369
+ } else if (method === 'page.get_console') {
370
+ normalized = applyLimitBudgetPreset(args, {
371
+ quick: 10,
372
+ normal: DEFAULT_CONSOLE_LIMIT,
373
+ deep: 100,
374
+ });
375
+ } else if (method === 'page.get_network') {
376
+ normalized = applyLimitBudgetPreset(args, {
377
+ quick: 10,
378
+ normal: DEFAULT_NETWORK_LIMIT,
379
+ deep: 100,
380
+ });
381
+ } else if (method === 'log.tail') {
382
+ normalized = applyLimitBudgetPreset(args, {
383
+ quick: 10,
384
+ normal: DEFAULT_LOG_TAIL_LIMIT,
385
+ deep: 100,
386
+ });
387
+ }
388
+ const { budgetPreset: _budgetPreset, ...methodParams } = normalized;
389
+ return methodParams;
390
+ }
391
+
392
+ /**
393
+ * @param {unknown} input
394
+ * @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
395
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
396
+ */
397
+ export function boundToolValue(input, options = {}) {
398
+ const maxEntries = options.maxEntries ?? 500;
399
+ const maxCharacters = options.maxCharacters ?? 20_000;
400
+ const maxStringLength = options.maxStringLength ?? 4_000;
401
+ const state = { entries: 0, characters: 0, truncated: false };
402
+ /** @type {(value: unknown, depth: number) => unknown} */
403
+ const visit = (value, depth) => {
404
+ if (state.entries >= maxEntries || state.characters >= maxCharacters || depth > 8) {
405
+ state.truncated = true;
406
+ return '[truncated]';
407
+ }
408
+ state.entries += 1;
409
+ if (typeof value === 'string') {
410
+ const limit = Math.min(value.length, maxStringLength, maxCharacters - state.characters);
411
+ state.characters += limit;
412
+ if (limit === value.length) return value;
413
+ state.truncated = true;
414
+ return value.slice(0, Math.max(0, limit));
415
+ }
416
+ if (Array.isArray(value)) {
417
+ const limit = Math.min(value.length, 100, maxEntries - state.entries);
418
+ if (limit < value.length) state.truncated = true;
419
+ return value.slice(0, limit).map((entry) => visit(entry, depth + 1));
420
+ }
421
+ if (value && typeof value === 'object') {
422
+ /** @type {Record<string, unknown>} */
423
+ const output = {};
424
+ const entries = Object.entries(/** @type {Record<string, unknown>} */ (value));
425
+ const limit = Math.min(entries.length, 100);
426
+ if (limit < entries.length) state.truncated = true;
427
+ for (const [key, entry] of entries.slice(0, limit)) {
428
+ if (state.entries >= maxEntries || state.characters >= maxCharacters) {
429
+ state.truncated = true;
430
+ break;
431
+ }
432
+ state.characters += key.length;
433
+ output[key] = visit(entry, depth + 1);
434
+ }
435
+ return output;
436
+ }
437
+ return value;
438
+ };
439
+ return {
440
+ value: visit(input, 0),
441
+ truncated: state.truncated,
442
+ limit: { maxEntries, maxCharacters, maxStringLength },
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Preserve useful evidence up to the caller's requested limits instead of
448
+ * fetching normal/deep results and silently returning only a fixed preview.
449
+ *
450
+ * @param {unknown} rawResult
451
+ * @param {string | undefined} method
452
+ * @param {Record<string, unknown>} params
453
+ * @param {unknown} fallback
454
+ * @returns {{ value: unknown, truncated: boolean, limit: Record<string, number> }}
455
+ */
456
+ function getRequestAwareEvidence(rawResult, method, params, fallback) {
457
+ const result =
458
+ rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)
459
+ ? /** @type {Record<string, unknown>} */ (rawResult)
460
+ : {};
461
+ if (method === 'page.get_text' || method === 'dom.get_text') {
462
+ const text = typeof result.text === 'string' ? result.text : result.value;
463
+ if (typeof text === 'string') {
464
+ const defaultLimit =
465
+ method === 'page.get_text' ? DEFAULT_PAGE_TEXT_BUDGET : DEFAULT_TEXT_BUDGET;
466
+ const requested = positiveInteger(params.textBudget) ?? defaultLimit;
467
+ const maxCharacters = Math.min(requested, 16_000);
468
+ const outputTruncated = text.length > maxCharacters;
469
+ return {
470
+ value: {
471
+ text: text.slice(0, maxCharacters),
472
+ length: typeof result.length === 'number' ? result.length : text.length,
473
+ truncated: result.truncated === true,
474
+ returnedChars: Math.min(text.length, maxCharacters),
475
+ },
476
+ truncated: outputTruncated,
477
+ limit: { maxCharacters },
478
+ };
479
+ }
480
+ }
481
+ if (method === 'dom.get_html' && typeof result.html === 'string') {
482
+ const requested = positiveInteger(params.maxLength) ?? DEFAULT_MAX_HTML_LENGTH;
483
+ const maxCharacters = Math.min(requested, 10_000);
484
+ return {
485
+ value: {
486
+ html: result.html.slice(0, maxCharacters),
487
+ truncated: result.truncated === true,
488
+ returnedChars: Math.min(result.html.length, maxCharacters),
489
+ },
490
+ truncated: result.html.length > maxCharacters,
491
+ limit: { maxCharacters },
492
+ };
493
+ }
494
+ if (method === 'dom.query' && Array.isArray(result.nodes)) {
495
+ const maxEntries = Math.min(positiveInteger(params.maxNodes) ?? DEFAULT_MAX_NODES, 100);
496
+ const nodes = result.nodes.slice(0, maxEntries).map(compactDomNode);
497
+ return {
498
+ value: nodes,
499
+ truncated: result.truncated === true || result.nodes.length > maxEntries,
500
+ limit: { maxEntries },
501
+ };
502
+ }
503
+ if (method === 'dom.get_accessibility_tree' && Array.isArray(result.nodes)) {
504
+ const maxEntries = Math.min(positiveInteger(params.maxNodes) ?? DEFAULT_MAX_NODES, 100);
505
+ const nodes = result.nodes.slice(0, maxEntries).map((value) => {
506
+ const node =
507
+ value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
508
+ return {
509
+ role: node.role,
510
+ name: node.name,
511
+ ...(node.interactive === true ? { interactive: true } : {}),
512
+ ...(node.value !== undefined ? { value: node.value } : {}),
513
+ };
514
+ });
515
+ return {
516
+ value: nodes,
517
+ truncated: result.truncated === true || result.nodes.length > maxEntries,
518
+ limit: { maxEntries },
519
+ };
520
+ }
521
+ if (
522
+ (method === 'page.get_console' || method === 'page.get_network' || method === 'log.tail') &&
523
+ Array.isArray(result.entries)
524
+ ) {
525
+ const defaultLimit =
526
+ method === 'page.get_console'
527
+ ? DEFAULT_CONSOLE_LIMIT
528
+ : method === 'page.get_network'
529
+ ? DEFAULT_NETWORK_LIMIT
530
+ : DEFAULT_LOG_TAIL_LIMIT;
531
+ const maxEntries = Math.min(positiveInteger(params.limit) ?? defaultLimit, 100);
532
+ const selected =
533
+ method === 'log.tail'
534
+ ? result.entries.slice(-maxEntries)
535
+ : result.entries.slice(0, maxEntries);
536
+ const bounded = boundToolValue(selected, { maxEntries: 500, maxCharacters: 20_000 });
537
+ return {
538
+ value: bounded.value,
539
+ truncated: result.entries.length > maxEntries || bounded.truncated,
540
+ limit: { maxEntries, maxCharacters: 20_000 },
541
+ };
542
+ }
543
+ return boundToolValue(fallback);
544
+ }
545
+
546
+ /** @param {unknown} value */
547
+ function positiveInteger(value) {
548
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
549
+ }
550
+
551
+ /**
552
+ * @param {unknown} value
553
+ * @returns {Record<string, unknown>}
554
+ */
555
+ function compactDomNode(value) {
556
+ const node =
557
+ value && typeof value === 'object' ? /** @type {Record<string, unknown>} */ (value) : {};
558
+ const attrs =
559
+ node.attrs && typeof node.attrs === 'object'
560
+ ? /** @type {Record<string, unknown>} */ (node.attrs)
561
+ : {};
562
+ /** @type {Record<string, unknown>} */
563
+ const compact = { ref: node.elementRef, tag: node.tag };
564
+ if (node.id ?? attrs.id) compact.id = node.id ?? attrs.id;
565
+ if (typeof attrs.class === 'string') compact.cls = attrs.class.split(' ').slice(0, 3).join(' ');
566
+ if (node.role ?? attrs.role) compact.role = node.role ?? attrs.role;
567
+ if (node.name ?? attrs['aria-label']) compact.label = node.name ?? attrs['aria-label'];
568
+ if (attrs['data-testid']) compact.testId = attrs['data-testid'];
569
+ const text = typeof node.textExcerpt === 'string' ? node.textExcerpt : node.text;
570
+ if (typeof text === 'string' && text) compact.text = text.slice(0, 120);
571
+ if (Array.isArray(node.children) && node.children.length)
572
+ compact.childCount = node.children.length;
573
+ return compact;
574
+ }
575
+
317
576
  /**
318
577
  * @param {import('../../agent-client/src/client.js').BridgeClient} client
319
578
  * @param {BridgeMethod} method
@@ -352,7 +611,7 @@ export async function callBridgeTool(method, params = {}, options = {}) {
352
611
  source: REQUEST_SOURCE,
353
612
  tokenBudget: options.tokenBudget ?? null,
354
613
  });
355
- return summarizeToolResponse(response, options.summaryMethod || method);
614
+ return summarizeToolResponse(response, options.summaryMethod || method, params);
356
615
  },
357
616
  { destinationId: options.destinationId ?? null }
358
617
  );
@@ -388,12 +647,13 @@ export async function dispatchToolAction(actions, args, toolName) {
388
647
  requestedTabId
389
648
  )
390
649
  : undefined;
391
- const response = await requestBridgeWithRetry(client, entry.method, entry.params(args, ref), {
650
+ const params = entry.params(args, ref);
651
+ const response = await requestBridgeWithRetry(client, entry.method, params, {
392
652
  tabId: requestedTabId,
393
653
  source: REQUEST_SOURCE,
394
654
  tokenBudget: getToolTokenBudget(/** @type {{ budgetPreset?: unknown }} */ (args)),
395
655
  });
396
- return summarizeToolResponse(response, entry.method);
656
+ return summarizeToolResponse(response, entry.method, params);
397
657
  },
398
658
  { destinationId: typeof args.destinationId === 'string' ? args.destinationId : null }
399
659
  );