@browserbridge/bbx 1.9.0 → 1.10.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.
@@ -6,7 +6,6 @@ import {
6
6
  BridgeError,
7
7
  bridgeMethodNeedsTab,
8
8
  DEFAULT_CONSOLE_LIMIT,
9
- DEFAULT_HAR_LIMIT,
10
9
  DEFAULT_LOG_TAIL_LIMIT,
11
10
  DEFAULT_MAX_HTML_LENGTH,
12
11
  DEFAULT_MAX_NODES,
@@ -15,11 +14,21 @@ import {
15
14
  DEFAULT_TEXT_BUDGET,
16
15
  estimateJsonPayloadCost,
17
16
  getBudgetPreset,
17
+ getBudgetPresetName,
18
18
  getErrorRecovery,
19
- isBudgetPresetName,
20
19
  summarizeBatchErrorItem,
21
20
  summarizeBatchResponseItem,
22
21
  } from '../../protocol/src/index.js';
22
+
23
+ export {
24
+ applyHtmlBudgetPreset,
25
+ applyLimitBudgetPreset,
26
+ applyMethodBudgetPreset,
27
+ applyPageTextBudgetPreset,
28
+ applyTextBudgetPreset,
29
+ applyTreeBudgetPreset,
30
+ getBudgetPresetName,
31
+ } from '../../protocol/src/index.js';
23
32
  import {
24
33
  getDoctorReport,
25
34
  requestBridge,
@@ -68,6 +77,8 @@ const RETRY_SAFE_METHODS = new Set([
68
77
  'screenshot.capture_region',
69
78
  'screenshot.capture_element',
70
79
  'screenshot.capture_full_page',
80
+ 'artifact.read',
81
+ 'network.intercept.list',
71
82
  'performance.get_metrics',
72
83
  'cdp.get_document',
73
84
  'cdp.get_dom_snapshot',
@@ -240,14 +251,6 @@ export async function resolveToolRef(client, input, tabId = null) {
240
251
  throw new BridgeError('INVALID_REQUEST', 'Provide either elementRef or selector.');
241
252
  }
242
253
 
243
- /**
244
- * @param {unknown} value
245
- * @returns {'quick' | 'normal' | 'deep' | null}
246
- */
247
- export function getBudgetPresetName(value) {
248
- return isBudgetPresetName(value) ? value : null;
249
- }
250
-
251
254
  /**
252
255
  * @param {{ budgetPreset?: unknown, selector?: unknown, elementRef?: unknown }} args
253
256
  * @returns {'quick' | 'normal' | 'deep' | null}
@@ -271,162 +274,6 @@ export function getToolTokenBudget(args) {
271
274
  return presetName ? getBudgetPreset(presetName).tokenBudget : null;
272
275
  }
273
276
 
274
- /**
275
- * @template {{ budgetPreset?: unknown, maxNodes?: unknown, maxDepth?: unknown, textBudget?: unknown }} T
276
- * @param {T} args
277
- * @returns {T}
278
- */
279
- export function applyTreeBudgetPreset(args) {
280
- const presetName = getBudgetPresetName(args.budgetPreset);
281
- if (!presetName) {
282
- return args;
283
- }
284
- const preset = getBudgetPreset(presetName);
285
- return /** @type {T} */ ({
286
- ...args,
287
- maxNodes: args.maxNodes ?? preset.maxNodes,
288
- maxDepth: args.maxDepth ?? preset.maxDepth,
289
- textBudget: args.textBudget ?? preset.textBudget,
290
- });
291
- }
292
-
293
- /**
294
- * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
295
- * @param {T} args
296
- * @returns {T}
297
- */
298
- export function applyTextBudgetPreset(args) {
299
- const presetName = getBudgetPresetName(args.budgetPreset);
300
- if (!presetName) {
301
- return args;
302
- }
303
- const preset = getBudgetPreset(presetName);
304
- return /** @type {T} */ ({
305
- ...args,
306
- textBudget: args.textBudget ?? preset.textBudget,
307
- });
308
- }
309
-
310
- /**
311
- * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
312
- * @param {T} args
313
- * @returns {T}
314
- */
315
- export function applyPageTextBudgetPreset(args) {
316
- const presetName = getBudgetPresetName(args.budgetPreset);
317
- if (!presetName) {
318
- return args;
319
- }
320
- const textBudgetByPreset = {
321
- quick: 2000,
322
- normal: DEFAULT_PAGE_TEXT_BUDGET,
323
- deep: DEFAULT_PAGE_TEXT_BUDGET * 2,
324
- };
325
- return /** @type {T} */ ({
326
- ...args,
327
- textBudget: args.textBudget ?? textBudgetByPreset[presetName],
328
- });
329
- }
330
-
331
- /**
332
- * @template {{ budgetPreset?: unknown, limit?: unknown }} T
333
- * @param {T} args
334
- * @param {{ quick: number, normal: number, deep: number }} defaults
335
- * @returns {T}
336
- */
337
- export function applyLimitBudgetPreset(args, defaults) {
338
- const presetName = getBudgetPresetName(args.budgetPreset);
339
- if (!presetName) {
340
- return args;
341
- }
342
- return /** @type {T} */ ({
343
- ...args,
344
- limit: args.limit ?? defaults[presetName],
345
- });
346
- }
347
-
348
- /**
349
- * @template {{ budgetPreset?: unknown, maxLength?: unknown }} T
350
- * @param {T} args
351
- * @returns {T}
352
- */
353
- export function applyHtmlBudgetPreset(args) {
354
- const presetName = getBudgetPresetName(args.budgetPreset);
355
- if (!presetName) {
356
- return args;
357
- }
358
- const maxLengthByPreset = {
359
- quick: 600,
360
- normal: DEFAULT_MAX_HTML_LENGTH,
361
- deep: 6000,
362
- };
363
- return /** @type {T} */ ({
364
- ...args,
365
- maxLength: args.maxLength ?? maxLengthByPreset[presetName],
366
- });
367
- }
368
-
369
- /**
370
- * Apply a preset to method parameters before dispatch. Explicit method params
371
- * always win over preset defaults.
372
- *
373
- * @param {BridgeMethod} method
374
- * @param {Record<string, unknown>} params
375
- * @param {unknown} budgetPreset
376
- * @returns {Record<string, unknown>}
377
- */
378
- export function applyMethodBudgetPreset(method, params, budgetPreset) {
379
- const args = { ...params, budgetPreset };
380
- /** @type {Record<string, unknown>} */
381
- let normalized = args;
382
- if (
383
- method === 'dom.query' ||
384
- method === 'dom.get_accessibility_tree' ||
385
- method === 'dom.baseline.create'
386
- ) {
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
- };
395
- } else if (method === 'dom.get_text') {
396
- normalized = applyTextBudgetPreset(args);
397
- } else if (method === 'dom.get_html') {
398
- normalized = applyHtmlBudgetPreset(args);
399
- } else if (method === 'page.get_text' || method === 'page.extract_content') {
400
- normalized = applyPageTextBudgetPreset(args);
401
- } else if (method === 'page.get_console') {
402
- normalized = applyLimitBudgetPreset(args, {
403
- quick: 10,
404
- normal: DEFAULT_CONSOLE_LIMIT,
405
- deep: 100,
406
- });
407
- } else if (method === 'page.get_network') {
408
- normalized = applyLimitBudgetPreset(args, {
409
- quick: 10,
410
- normal: DEFAULT_NETWORK_LIMIT,
411
- deep: 100,
412
- });
413
- } else if (method === 'network.export_har') {
414
- normalized = applyLimitBudgetPreset(args, {
415
- quick: 20,
416
- normal: DEFAULT_HAR_LIMIT,
417
- deep: 100,
418
- });
419
- } else if (method === 'log.tail') {
420
- normalized = applyLimitBudgetPreset(args, {
421
- quick: 10,
422
- normal: DEFAULT_LOG_TAIL_LIMIT,
423
- deep: 100,
424
- });
425
- }
426
- const { budgetPreset: _budgetPreset, ...methodParams } = normalized;
427
- return methodParams;
428
- }
429
-
430
277
  /**
431
278
  * @param {unknown} input
432
279
  * @param {{ maxEntries?: number, maxCharacters?: number, maxStringLength?: number }} [options]
@@ -586,6 +433,14 @@ function getRequestAwareEvidence(rawResult, method, params, fallback) {
586
433
  limit: { maxEntries },
587
434
  };
588
435
  }
436
+ if (method?.startsWith('network.intercept.')) {
437
+ const bounded = boundToolValue(result, { maxEntries: 100, maxCharacters: 10_000 });
438
+ return {
439
+ value: bounded.value,
440
+ truncated: bounded.truncated,
441
+ limit: { maxEntries: 100, maxCharacters: 10_000 },
442
+ };
443
+ }
589
444
  if (
590
445
  (method === 'page.get_console' || method === 'page.get_network' || method === 'log.tail') &&
591
446
  Array.isArray(result.entries)
@@ -661,6 +516,92 @@ function compactDomNode(value) {
661
516
  return compact;
662
517
  }
663
518
 
519
+ /**
520
+ * Socket-level Node error codes that mean the daemon connection itself was
521
+ * lost, as opposed to an application-level bridge failure.
522
+ *
523
+ * @type {ReadonlySet<string>}
524
+ */
525
+ const CONNECTION_LOSS_CODES = new Set([
526
+ 'ENOTCONN',
527
+ 'ECONNREFUSED',
528
+ 'ECONNRESET',
529
+ 'EPIPE',
530
+ 'ERR_SOCKET_CLOSED',
531
+ ]);
532
+
533
+ const RECONNECT_WAIT_TIMEOUT_MS = 5_000;
534
+ const RECONNECT_POLL_INTERVAL_MS = 250;
535
+
536
+ /**
537
+ * Detect thrown transport errors caused by a lost daemon connection. These
538
+ * never arrive as bridge failure responses because the socket died before (or
539
+ * while) the daemon could reply.
540
+ *
541
+ * @param {unknown} error
542
+ * @returns {boolean}
543
+ */
544
+ export function isConnectionLossError(error) {
545
+ if (!error || typeof error !== 'object') {
546
+ return false;
547
+ }
548
+ const code = /** @type {{ code?: unknown }} */ (error).code;
549
+ if (typeof code === 'string' && CONNECTION_LOSS_CODES.has(code)) {
550
+ return true;
551
+ }
552
+ const message = error instanceof Error ? error.message : '';
553
+ return /Bridge socket closed|BridgeClient is not connected/u.test(message);
554
+ }
555
+
556
+ /**
557
+ * Wait, bounded, for the client to regain its daemon connection.
558
+ *
559
+ * When the client's own auto-reconnect loop is active it owns reconnecting and
560
+ * emits `reconnected`; otherwise one manual connect is attempted per poll
561
+ * interval. Application state is never touched here - this only restores the
562
+ * transport.
563
+ *
564
+ * @param {import('../../agent-client/src/client.js').BridgeClient} client
565
+ * @param {number} [timeoutMs=RECONNECT_WAIT_TIMEOUT_MS]
566
+ * @returns {Promise<boolean>}
567
+ */
568
+ export async function waitForClientReconnect(client, timeoutMs = RECONNECT_WAIT_TIMEOUT_MS) {
569
+ const deadline = Date.now() + timeoutMs;
570
+ while (!client.connected && Date.now() < deadline) {
571
+ if (client.autoReconnect) {
572
+ await new Promise((resolve) => {
573
+ const remaining = Math.max(0, deadline - Date.now());
574
+ /** @type {ReturnType<typeof setTimeout>} */
575
+ let timer;
576
+ const cleanup = () => {
577
+ clearTimeout(timer);
578
+ client.off('reconnected', onReconnected);
579
+ resolve(undefined);
580
+ };
581
+ const onReconnected = () => cleanup();
582
+ timer = setTimeout(cleanup, Math.min(RECONNECT_POLL_INTERVAL_MS, remaining));
583
+ client.on('reconnected', onReconnected);
584
+ });
585
+ continue;
586
+ }
587
+ try {
588
+ await client.connect();
589
+ } catch (error) {
590
+ if (error instanceof Error && /already connected/u.test(error.message)) {
591
+ break;
592
+ }
593
+ // Daemon still unreachable; keep waiting inside the bounded window.
594
+ }
595
+ if (!client.connected) {
596
+ const remaining = Math.max(0, deadline - Date.now());
597
+ await new Promise((resolve) =>
598
+ setTimeout(resolve, Math.min(RECONNECT_POLL_INTERVAL_MS, remaining))
599
+ );
600
+ }
601
+ }
602
+ return client.connected;
603
+ }
604
+
664
605
  /**
665
606
  * @param {import('../../agent-client/src/client.js').BridgeClient} client
666
607
  * @param {BridgeMethod} method
@@ -669,7 +610,25 @@ function compactDomNode(value) {
669
610
  * @returns {Promise<BridgeResponse>}
670
611
  */
671
612
  export async function requestBridgeWithRetry(client, method, params, options) {
672
- const response = await requestBridge(client, method, params, options);
613
+ /** @type {BridgeResponse} */
614
+ let response;
615
+ try {
616
+ response = await requestBridge(client, method, params, options);
617
+ } catch (error) {
618
+ if (!isConnectionLossError(error) || !isRetrySafeBridgeMethod(method, params)) {
619
+ throw error;
620
+ }
621
+ process.stderr.write(
622
+ `[bbx-mcp] ${method} lost its daemon connection; waiting up to ${RECONNECT_WAIT_TIMEOUT_MS}ms for reconnect before one retry.\n`
623
+ );
624
+ if (!(await waitForClientReconnect(client))) {
625
+ throw error;
626
+ }
627
+ return requestBridge(client, method, params, {
628
+ ...options,
629
+ automaticRetry: 'mcp_second_attempt',
630
+ });
631
+ }
673
632
  const recovery =
674
633
  !response.ok && response.error
675
634
  ? (response.error.recovery ?? getErrorRecovery(response.error.code))
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import os from 'node:os';
4
+
4
5
  import { DEFAULT_LOG_TAIL_LIMIT, createRuntimeContext } from '../../protocol/src/index.js';
5
6
  import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
6
7
  import {
@@ -56,6 +57,8 @@ export {
56
57
  export {
57
58
  PAGE_ACTIONS,
58
59
  handlePageTool,
60
+ handleArtifactTool,
61
+ handleInterceptTool,
59
62
  handleSensitiveReadTool,
60
63
  handleBatchTool,
61
64
  handleRawCallTool,
@@ -194,7 +197,10 @@ export async function handleSetupTool(args) {
194
197
  const configuredMcp = status.mcpClients.filter((e) => e.configured).length;
195
198
  const installedSkills = status.skillTargets.filter((e) => e.installed).length;
196
199
  const summary = `Optional agent integration status: ${configuredMcp}/${status.mcpClients.length} MCP clients configured, ${installedSkills}/${status.skillTargets.length} skills installed.`;
197
- return createToolResult(summary, { ok: true, status });
200
+ return createToolResult(summary, {
201
+ ok: true,
202
+ status,
203
+ });
198
204
  }
199
205
 
200
206
  /**