@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.
- package/package.json +1 -1
- package/packages/agent-client/src/cli-args.js +35 -6
- package/packages/agent-client/src/cli-batch.js +10 -5
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +35 -15
- package/packages/agent-client/src/command-registry.js +7 -3
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/runtime.js +2 -2
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-page.js +162 -9
- package/packages/mcp-server/src/handlers-utils.js +126 -167
- package/packages/mcp-server/src/handlers.js +7 -1
- package/packages/mcp-server/src/server.js +195 -46
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/daemon.js +20 -106
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +14 -28
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/registry.js +83 -0
- package/packages/protocol/src/types.ts +1 -0
- package/skills/browser-bridge/SKILL.md +4 -0
- package/skills/browser-bridge/references/protocol.md +11 -5
|
@@ -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
|
-
|
|
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, {
|
|
200
|
+
return createToolResult(summary, {
|
|
201
|
+
ok: true,
|
|
202
|
+
status,
|
|
203
|
+
});
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
/**
|