@browserbridge/bbx 1.7.3 → 1.7.5
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 +7 -7
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli.js +273 -76
- package/packages/agent-client/src/command-registry.js +12 -6
- package/packages/agent-client/src/install.js +39 -4
- package/packages/agent-client/src/mcp-config.js +28 -14
- package/packages/agent-client/src/remotes.js +93 -15
- package/packages/agent-client/src/runtime.js +6 -3
- package/packages/mcp-server/src/bin.js +10 -1
- package/packages/mcp-server/src/guidance.js +2 -2
- package/packages/mcp-server/src/handlers-capture.js +62 -32
- package/packages/mcp-server/src/handlers-dom.js +2 -2
- package/packages/mcp-server/src/handlers-navigation.js +30 -9
- package/packages/mcp-server/src/handlers-page.js +170 -143
- package/packages/mcp-server/src/handlers-utils.js +277 -8
- package/packages/mcp-server/src/handlers.js +31 -10
- package/packages/mcp-server/src/lifecycle.js +413 -0
- package/packages/mcp-server/src/server.js +59 -30
- package/packages/native-host/src/daemon.js +3 -1
- package/packages/protocol/src/capabilities.js +1 -1
- package/packages/protocol/src/defaults.js +2 -0
- package/packages/protocol/src/protocol.js +122 -9
- package/packages/protocol/src/registry.js +14 -14
- package/packages/protocol/src/summary.js +61 -30
- package/packages/protocol/src/types.ts +30 -1
- package/skills/browser-bridge/SKILL.md +4 -4
- package/skills/browser-bridge/references/interaction.md +1 -1
- package/skills/browser-bridge/references/protocol.md +3 -3
|
@@ -6,12 +6,14 @@ import {
|
|
|
6
6
|
DEFAULT_A11Y_MAX_DEPTH,
|
|
7
7
|
DEFAULT_A11Y_MAX_NODES,
|
|
8
8
|
DEFAULT_CONSOLE_LIMIT,
|
|
9
|
+
DEFAULT_DEVICE_SCALE_FACTOR,
|
|
9
10
|
DEFAULT_EVAL_TIMEOUT_MS,
|
|
10
11
|
DEFAULT_LOG_TAIL_LIMIT,
|
|
11
12
|
DEFAULT_MAX_DEPTH,
|
|
12
13
|
DEFAULT_MAX_HTML_LENGTH,
|
|
13
14
|
DEFAULT_MAX_NODES,
|
|
14
15
|
DEFAULT_NAV_TIMEOUT_MS,
|
|
16
|
+
DEFAULT_NETWORK_INTERCEPT_ACTION,
|
|
15
17
|
DEFAULT_NETWORK_LIMIT,
|
|
16
18
|
DEFAULT_PAGE_TEXT_BUDGET,
|
|
17
19
|
DEFAULT_TEXT_BUDGET,
|
|
@@ -42,6 +44,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
42
44
|
/** @typedef {import('./types.js').InputActionParams} InputActionParams */
|
|
43
45
|
/** @typedef {import('./types.js').LogTailParams} LogTailParams */
|
|
44
46
|
/** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
|
|
47
|
+
/** @typedef {import('./types.js').NetworkInterceptAddParams} NetworkInterceptAddParams */
|
|
45
48
|
/** @typedef {import('./types.js').NetworkParams} NetworkParams */
|
|
46
49
|
/** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
|
|
47
50
|
/** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
|
|
@@ -58,6 +61,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
58
61
|
/** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
|
|
59
62
|
/** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
|
|
60
63
|
/** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
|
|
64
|
+
/** @typedef {import('./types.js').NormalizedNetworkInterceptAddParams} NormalizedNetworkInterceptAddParams */
|
|
61
65
|
/** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
|
|
62
66
|
/** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
|
|
63
67
|
/** @typedef {import('./types.js').NormalizedPatchOperation} NormalizedPatchOperation */
|
|
@@ -151,6 +155,30 @@ function clampInt(value, min, max, fallback) {
|
|
|
151
155
|
return Math.min(Math.max(integer, min), max);
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Clamp a finite number without discarding its fractional component.
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} value
|
|
162
|
+
* @param {number} min
|
|
163
|
+
* @param {number} max
|
|
164
|
+
* @param {number} fallback
|
|
165
|
+
* @returns {number}
|
|
166
|
+
*/
|
|
167
|
+
function clampNumber(value, min, max, fallback) {
|
|
168
|
+
const numeric = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
169
|
+
return Math.min(Math.max(numeric, min), max);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {unknown} value
|
|
174
|
+
* @returns {value is Record<string, unknown>}
|
|
175
|
+
*/
|
|
176
|
+
function isPlainRecord(value) {
|
|
177
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
178
|
+
const prototype = Object.getPrototypeOf(value);
|
|
179
|
+
return prototype === Object.prototype || prototype === null;
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
/** @type {ReadonlyArray<BridgeMethod>} */
|
|
155
183
|
export const METHODS = BRIDGE_METHODS;
|
|
156
184
|
|
|
@@ -336,6 +364,8 @@ function normalizeRequestParams(method, params) {
|
|
|
336
364
|
return normalizePageTextParams(params);
|
|
337
365
|
case 'page.get_network':
|
|
338
366
|
return normalizeNetworkParams(params);
|
|
367
|
+
case 'network.intercept.add':
|
|
368
|
+
return normalizeNetworkInterceptAddParams(params);
|
|
339
369
|
case 'navigation.navigate':
|
|
340
370
|
case 'navigation.reload':
|
|
341
371
|
case 'navigation.go_back':
|
|
@@ -361,6 +391,7 @@ function normalizeRequestParams(method, params) {
|
|
|
361
391
|
case 'input.click':
|
|
362
392
|
case 'input.focus':
|
|
363
393
|
case 'input.type':
|
|
394
|
+
case 'input.fill':
|
|
364
395
|
case 'input.press_key':
|
|
365
396
|
return normalizeInputAction(params);
|
|
366
397
|
case 'cdp.dispatch_key_event':
|
|
@@ -412,7 +443,7 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
412
443
|
case 'page.evaluate':
|
|
413
444
|
return normalizeEvaluateParams(params).timeoutMs;
|
|
414
445
|
case 'dom.wait_for':
|
|
415
|
-
return
|
|
446
|
+
return clampInt(params.timeoutMs, 100, 30_000, 5_000);
|
|
416
447
|
default:
|
|
417
448
|
return null;
|
|
418
449
|
}
|
|
@@ -423,11 +454,13 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
423
454
|
* @returns {NormalizedDomQuery}
|
|
424
455
|
*/
|
|
425
456
|
export function normalizeDomQuery(params = {}) {
|
|
457
|
+
const nestedBudget =
|
|
458
|
+
params.budget && typeof params.budget === 'object' ? params.budget : undefined;
|
|
426
459
|
return {
|
|
427
460
|
selector:
|
|
428
461
|
typeof params.selector === 'string' && params.selector.trim() ? params.selector : 'body',
|
|
429
462
|
withinRef: typeof params.withinRef === 'string' ? params.withinRef : null,
|
|
430
|
-
budget: applyBudget(params),
|
|
463
|
+
budget: applyBudget({ ...nestedBudget, ...params }),
|
|
431
464
|
};
|
|
432
465
|
}
|
|
433
466
|
|
|
@@ -640,7 +673,7 @@ export function normalizeNavigationAction(params = {}) {
|
|
|
640
673
|
return {
|
|
641
674
|
url,
|
|
642
675
|
waitForLoad: params.waitForLoad !== false,
|
|
643
|
-
timeoutMs: clampInt(params.timeoutMs, 500, 120_000,
|
|
676
|
+
timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
|
|
644
677
|
};
|
|
645
678
|
}
|
|
646
679
|
|
|
@@ -679,11 +712,17 @@ export function normalizeEvaluateParams(params = {}) {
|
|
|
679
712
|
`Expression too large (${expression.length} chars). Maximum is 100,000.`
|
|
680
713
|
);
|
|
681
714
|
}
|
|
715
|
+
if (params.returnByValue === false) {
|
|
716
|
+
throw new BridgeError(
|
|
717
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
718
|
+
'returnByValue=false is not supported because remote CDP objects cannot be consumed.'
|
|
719
|
+
);
|
|
720
|
+
}
|
|
682
721
|
return {
|
|
683
722
|
expression,
|
|
684
723
|
awaitPromise: Boolean(params.awaitPromise),
|
|
685
|
-
timeoutMs: clampInt(params.timeoutMs, 100, 30_000,
|
|
686
|
-
returnByValue:
|
|
724
|
+
timeoutMs: clampInt(params.timeoutMs, 100, 30_000, DEFAULT_EVAL_TIMEOUT_MS),
|
|
725
|
+
returnByValue: true,
|
|
687
726
|
};
|
|
688
727
|
}
|
|
689
728
|
|
|
@@ -706,9 +745,18 @@ export function normalizeConsoleParams(params = {}) {
|
|
|
706
745
|
*/
|
|
707
746
|
export function normalizeWaitForParams(params = {}) {
|
|
708
747
|
const validStates = ['attached', 'detached', 'visible', 'hidden'];
|
|
748
|
+
const selector =
|
|
749
|
+
typeof params.selector === 'string' && params.selector.trim() ? params.selector : '';
|
|
750
|
+
const text = typeof params.text === 'string' && params.text.trim() ? params.text : null;
|
|
751
|
+
if (!selector && text === null) {
|
|
752
|
+
throw new BridgeError(
|
|
753
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
754
|
+
'selector or text is required for dom.wait_for.'
|
|
755
|
+
);
|
|
756
|
+
}
|
|
709
757
|
return {
|
|
710
|
-
selector:
|
|
711
|
-
text
|
|
758
|
+
selector: selector || '*',
|
|
759
|
+
text,
|
|
712
760
|
state: validStates.includes(String(params.state ?? ''))
|
|
713
761
|
? /** @type {'attached' | 'detached' | 'visible' | 'hidden'} */ (String(params.state))
|
|
714
762
|
: 'attached',
|
|
@@ -817,7 +865,7 @@ export function normalizeStorageParams(params = {}) {
|
|
|
817
865
|
export function normalizeWaitForLoadStateParams(params = {}) {
|
|
818
866
|
return {
|
|
819
867
|
waitForLoad: params.waitForLoad !== false,
|
|
820
|
-
timeoutMs: clampInt(params.timeoutMs, 500, 120_000,
|
|
868
|
+
timeoutMs: clampInt(params.timeoutMs, 500, 120_000, DEFAULT_NAV_TIMEOUT_MS),
|
|
821
869
|
};
|
|
822
870
|
}
|
|
823
871
|
|
|
@@ -886,6 +934,71 @@ export function normalizeNetworkParams(params = {}) {
|
|
|
886
934
|
};
|
|
887
935
|
}
|
|
888
936
|
|
|
937
|
+
/**
|
|
938
|
+
* @param {NetworkInterceptAddParams} [params={}]
|
|
939
|
+
* @returns {NormalizedNetworkInterceptAddParams}
|
|
940
|
+
*/
|
|
941
|
+
export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
942
|
+
if (typeof params.urlPattern !== 'string' || !params.urlPattern) {
|
|
943
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern is required.');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const action = params.action ?? DEFAULT_NETWORK_INTERCEPT_ACTION;
|
|
947
|
+
if (action !== 'fulfill' && action !== 'continue' && action !== 'block') {
|
|
948
|
+
throw new BridgeError(
|
|
949
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
950
|
+
'action must be one of fulfill, continue, or block.'
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (
|
|
954
|
+
params.statusCode !== undefined &&
|
|
955
|
+
(!Number.isInteger(params.statusCode) || params.statusCode < 100 || params.statusCode > 599)
|
|
956
|
+
) {
|
|
957
|
+
throw new BridgeError(
|
|
958
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
959
|
+
'statusCode must be an integer between 100 and 599.'
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
if (params.body !== undefined && typeof params.body !== 'string') {
|
|
963
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'body must be a string.');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** @type {Record<string, string> | undefined} */
|
|
967
|
+
let headers;
|
|
968
|
+
if (params.headers !== undefined) {
|
|
969
|
+
if (!isPlainRecord(params.headers)) {
|
|
970
|
+
throw new BridgeError(
|
|
971
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
972
|
+
'headers must be an object containing string values.'
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
headers = {};
|
|
976
|
+
for (const [name, value] of Object.entries(params.headers)) {
|
|
977
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
978
|
+
throw new BridgeError(
|
|
979
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
980
|
+
`Invalid header name: ${name || '(empty)'}.`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (typeof value !== 'string' || /[\r\n]/.test(value)) {
|
|
984
|
+
throw new BridgeError(
|
|
985
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
986
|
+
`Header ${name} must have a string value without newlines.`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
headers[name] = value;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
urlPattern: params.urlPattern,
|
|
995
|
+
action,
|
|
996
|
+
...(params.statusCode === undefined ? {} : { statusCode: params.statusCode }),
|
|
997
|
+
...(params.body === undefined ? {} : { body: params.body }),
|
|
998
|
+
...(headers === undefined ? {} : { headers }),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
889
1002
|
/**
|
|
890
1003
|
* @param {PageTextParams} [params={}]
|
|
891
1004
|
* @returns {NormalizedPageTextParams}
|
|
@@ -914,7 +1027,7 @@ export function normalizeViewportResizeParams(params = {}) {
|
|
|
914
1027
|
return {
|
|
915
1028
|
width: clampInt(params.width, 320, 7680, DEFAULT_VIEWPORT_WIDTH),
|
|
916
1029
|
height: clampInt(params.height, 200, 4320, DEFAULT_VIEWPORT_HEIGHT),
|
|
917
|
-
deviceScaleFactor:
|
|
1030
|
+
deviceScaleFactor: clampNumber(params.deviceScaleFactor, 0, 4, DEFAULT_DEVICE_SCALE_FACTOR),
|
|
918
1031
|
reset: Boolean(params.reset),
|
|
919
1032
|
};
|
|
920
1033
|
}
|
|
@@ -112,7 +112,7 @@ function createRegistryEntry(method, group, tab, params, complexity = 'low') {
|
|
|
112
112
|
|
|
113
113
|
/** @type {Readonly<Record<import('./types.js').BridgeMethod, BridgeMethodRegistryEntry>>} */
|
|
114
114
|
export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
115
|
-
// system
|
|
115
|
+
// system - trivial
|
|
116
116
|
'access.request': createRegistryEntry('access.request', 'system', false, [], 'trivial'),
|
|
117
117
|
'skill.get_runtime_context': createRegistryEntry(
|
|
118
118
|
'skill.get_runtime_context',
|
|
@@ -132,12 +132,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
132
132
|
'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
|
|
133
133
|
'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
|
|
134
134
|
'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
|
|
135
|
-
// tabs
|
|
135
|
+
// tabs - trivial
|
|
136
136
|
'tabs.list': createRegistryEntry('tabs.list', 'tabs', false, [], 'trivial'),
|
|
137
137
|
'tabs.create': createRegistryEntry('tabs.create', 'tabs', false, ['url', 'active'], 'trivial'),
|
|
138
138
|
'tabs.close': createRegistryEntry('tabs.close', 'tabs', false, ['tabId'], 'trivial'),
|
|
139
139
|
'tabs.activate': createRegistryEntry('tabs.activate', 'tabs', false, ['tabId'], 'trivial'),
|
|
140
|
-
// page
|
|
140
|
+
// page - low (basic reads), moderate (evaluate, debugger-backed)
|
|
141
141
|
'page.get_state': createRegistryEntry('page.get_state', 'page', true, [], 'low'),
|
|
142
142
|
'page.evaluate': {
|
|
143
143
|
...createRegistryEntry(
|
|
@@ -177,7 +177,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
177
177
|
['clear', 'limit', 'urlPattern'],
|
|
178
178
|
'low'
|
|
179
179
|
),
|
|
180
|
-
// network intercept
|
|
180
|
+
// network intercept - moderate (holds debugger session)
|
|
181
181
|
'network.intercept.add': createRegistryEntry(
|
|
182
182
|
'network.intercept.add',
|
|
183
183
|
'page',
|
|
@@ -206,7 +206,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
206
206
|
[],
|
|
207
207
|
'low'
|
|
208
208
|
),
|
|
209
|
-
// navigation
|
|
209
|
+
// navigation - low
|
|
210
210
|
'navigation.navigate': createRegistryEntry(
|
|
211
211
|
'navigation.navigate',
|
|
212
212
|
'navigate',
|
|
@@ -235,7 +235,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
235
235
|
['waitForLoad', 'timeoutMs'],
|
|
236
236
|
'low'
|
|
237
237
|
),
|
|
238
|
-
// dom
|
|
238
|
+
// dom - low (reads), moderate (accessibility tree)
|
|
239
239
|
'dom.query': {
|
|
240
240
|
...createRegistryEntry(
|
|
241
241
|
'dom.query',
|
|
@@ -303,7 +303,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
303
303
|
['maxNodes', 'maxDepth'],
|
|
304
304
|
'moderate'
|
|
305
305
|
),
|
|
306
|
-
// layout
|
|
306
|
+
// layout - low
|
|
307
307
|
'layout.get_box_model': createRegistryEntry(
|
|
308
308
|
'layout.get_box_model',
|
|
309
309
|
'inspect',
|
|
@@ -312,7 +312,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
312
312
|
'low'
|
|
313
313
|
),
|
|
314
314
|
'layout.hit_test': createRegistryEntry('layout.hit_test', 'inspect', true, ['x', 'y'], 'low'),
|
|
315
|
-
// styles
|
|
315
|
+
// styles - low (computed), moderate (matched rules)
|
|
316
316
|
'styles.get_computed': createRegistryEntry(
|
|
317
317
|
'styles.get_computed',
|
|
318
318
|
'inspect',
|
|
@@ -327,7 +327,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
327
327
|
['elementRef'],
|
|
328
328
|
'moderate'
|
|
329
329
|
),
|
|
330
|
-
// viewport
|
|
330
|
+
// viewport - low (scroll), moderate (resize, debugger-backed)
|
|
331
331
|
'viewport.scroll': createRegistryEntry(
|
|
332
332
|
'viewport.scroll',
|
|
333
333
|
'navigate',
|
|
@@ -342,7 +342,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
342
342
|
['width', 'height', 'deviceScaleFactor', 'reset'],
|
|
343
343
|
'moderate'
|
|
344
344
|
),
|
|
345
|
-
// input
|
|
345
|
+
// input - low (simple), moderate (drag)
|
|
346
346
|
'input.click': createRegistryEntry(
|
|
347
347
|
'input.click',
|
|
348
348
|
'interact',
|
|
@@ -407,7 +407,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
407
407
|
['target'],
|
|
408
408
|
'low'
|
|
409
409
|
),
|
|
410
|
-
// capture
|
|
410
|
+
// capture - high (screenshots, CDP)
|
|
411
411
|
'screenshot.capture_region': createRegistryEntry(
|
|
412
412
|
'screenshot.capture_region',
|
|
413
413
|
'capture',
|
|
@@ -429,7 +429,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
429
429
|
[],
|
|
430
430
|
'high'
|
|
431
431
|
),
|
|
432
|
-
// patch
|
|
432
|
+
// patch - moderate (side effects)
|
|
433
433
|
'patch.apply_styles': createRegistryEntry(
|
|
434
434
|
'patch.apply_styles',
|
|
435
435
|
'patch',
|
|
@@ -453,7 +453,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
453
453
|
[],
|
|
454
454
|
'low'
|
|
455
455
|
),
|
|
456
|
-
// cdp
|
|
456
|
+
// cdp - high (raw protocol, large payloads)
|
|
457
457
|
'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
|
|
458
458
|
'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
|
|
459
459
|
'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
|
|
@@ -471,7 +471,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
471
471
|
['key', 'code', 'modifiers'],
|
|
472
472
|
'moderate'
|
|
473
473
|
),
|
|
474
|
-
// performance
|
|
474
|
+
// performance - moderate (debugger-backed)
|
|
475
475
|
'performance.get_metrics': createRegistryEntry(
|
|
476
476
|
'performance.get_metrics',
|
|
477
477
|
'performance',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
estimateSerializedPayloadCost,
|
|
5
|
+
getErrorRecovery,
|
|
5
6
|
getCostClass,
|
|
6
7
|
getProtocolVersion,
|
|
7
8
|
serializeJsonPayload,
|
|
@@ -26,7 +27,8 @@ import {
|
|
|
26
27
|
* @typedef {{
|
|
27
28
|
* ok: boolean,
|
|
28
29
|
* summary: string,
|
|
29
|
-
* evidence: unknown
|
|
30
|
+
* evidence: unknown,
|
|
31
|
+
* recovery?: import('./types.js').BridgeRecovery | null
|
|
30
32
|
* }} BridgeSummary
|
|
31
33
|
*/
|
|
32
34
|
|
|
@@ -49,7 +51,7 @@ import {
|
|
|
49
51
|
* approxTokens: number,
|
|
50
52
|
* meta?: Record<string, unknown>,
|
|
51
53
|
* error: unknown,
|
|
52
|
-
* response
|
|
54
|
+
* response?: unknown
|
|
53
55
|
* }} BatchItemSummary
|
|
54
56
|
*/
|
|
55
57
|
|
|
@@ -164,14 +166,15 @@ function getMetaCostClass(meta, field) {
|
|
|
164
166
|
export function summarizeBridgeResponse(response, method) {
|
|
165
167
|
const protocolWarning = getProtocolWarning(response.meta);
|
|
166
168
|
if (!response.ok) {
|
|
167
|
-
const
|
|
169
|
+
const recovery = response.error.recovery ?? getErrorRecovery(response.error.code);
|
|
168
170
|
return {
|
|
169
171
|
ok: false,
|
|
170
172
|
summary: appendProtocolWarning(
|
|
171
|
-
`${response.error.code}: ${response.error.message}${hint ? ` ${hint}` : ''}`,
|
|
173
|
+
`${response.error.code}: ${response.error.message}${recovery?.hint ? ` ${recovery.hint}` : ''}`,
|
|
172
174
|
protocolWarning
|
|
173
175
|
),
|
|
174
176
|
evidence: response.error.details ?? null,
|
|
177
|
+
recovery,
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -348,16 +351,38 @@ export function summarizeBridgeResponse(response, method) {
|
|
|
348
351
|
}
|
|
349
352
|
return entry;
|
|
350
353
|
});
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
const isFind = method === 'dom.find_by_text' || method === 'dom.find_by_role';
|
|
355
|
+
if (isFind) {
|
|
356
|
+
const count = typeof result.count === 'number' ? result.count : nodes.length;
|
|
357
|
+
const scanned = typeof result.scanned === 'number' ? result.scanned : null;
|
|
358
|
+
const truncated = result.truncated === true;
|
|
359
|
+
let summary;
|
|
360
|
+
if (count === 0) {
|
|
361
|
+
summary = truncated
|
|
362
|
+
? `No matching elements returned; search stopped${scanned === null ? '' : ` after scanning ${scanned}`}.`
|
|
363
|
+
: 'No matching elements found.';
|
|
364
|
+
} else if (truncated) {
|
|
365
|
+
summary = `Found ${count} matching element(s); additional matches were omitted.`;
|
|
366
|
+
} else {
|
|
367
|
+
summary = `Found ${count} matching element(s)${scanned === null ? '' : ` after scanning ${scanned}`}.`;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
summary: appendProtocolWarning(summary, protocolWarning),
|
|
372
|
+
evidence: {
|
|
373
|
+
found: count > 0,
|
|
374
|
+
count,
|
|
375
|
+
scanned,
|
|
376
|
+
truncated,
|
|
377
|
+
truncationReason: result.truncationReason ?? null,
|
|
378
|
+
nodes: compact,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
356
381
|
}
|
|
357
382
|
return {
|
|
358
383
|
ok: true,
|
|
359
384
|
summary: appendProtocolWarning(
|
|
360
|
-
|
|
385
|
+
`DOM query returned ${nodes.length} element(s)${nodes.length > 15 ? '; showing first 15' : ''}.`,
|
|
361
386
|
protocolWarning
|
|
362
387
|
),
|
|
363
388
|
evidence: compact,
|
|
@@ -666,9 +691,10 @@ export function summarizeBridgeResponse(response, method) {
|
|
|
666
691
|
|
|
667
692
|
/**
|
|
668
693
|
* @param {{ method: SummaryMethod, tabId: number | null, response: BridgeResponse, durationMs: number }} input
|
|
694
|
+
* @param {{ compact?: boolean }} [options]
|
|
669
695
|
* @returns {BatchItemSummary}
|
|
670
696
|
*/
|
|
671
|
-
export function summarizeBatchResponseItem({ method, tabId, response, durationMs }) {
|
|
697
|
+
export function summarizeBatchResponseItem({ method, tabId, response, durationMs }, options = {}) {
|
|
672
698
|
const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
|
|
673
699
|
return {
|
|
674
700
|
method,
|
|
@@ -676,17 +702,18 @@ export function summarizeBatchResponseItem({ method, tabId, response, durationMs
|
|
|
676
702
|
...summary,
|
|
677
703
|
durationMs,
|
|
678
704
|
approxTokens: summary.transportTokens,
|
|
679
|
-
meta: response.meta,
|
|
705
|
+
meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
|
|
680
706
|
error: response.ok ? null : response.error,
|
|
681
|
-
response: response.ok ? response.result : null,
|
|
707
|
+
...(options.compact ? {} : { response: response.ok ? response.result : null }),
|
|
682
708
|
};
|
|
683
709
|
}
|
|
684
710
|
|
|
685
711
|
/**
|
|
686
712
|
* @param {{ method: SummaryMethod, tabId: number | null, error: unknown, durationMs: number }} input
|
|
713
|
+
* @param {{ compact?: boolean }} [options]
|
|
687
714
|
* @returns {BatchItemSummary}
|
|
688
715
|
*/
|
|
689
|
-
export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
|
|
716
|
+
export function summarizeBatchErrorItem({ method, tabId, error, durationMs }, options = {}) {
|
|
690
717
|
const message = error instanceof Error ? error.message : String(error);
|
|
691
718
|
const response = /** @type {BridgeResponse} */ ({
|
|
692
719
|
id: 'batch_error',
|
|
@@ -706,12 +733,30 @@ export function summarizeBatchErrorItem({ method, tabId, error, durationMs }) {
|
|
|
706
733
|
...summary,
|
|
707
734
|
durationMs,
|
|
708
735
|
approxTokens: 0,
|
|
709
|
-
meta: response.meta,
|
|
736
|
+
meta: options.compact ? compactBatchMeta(response.meta) : response.meta,
|
|
710
737
|
error: response.error,
|
|
711
|
-
response: null,
|
|
738
|
+
...(options.compact ? {} : { response: null }),
|
|
712
739
|
};
|
|
713
740
|
}
|
|
714
741
|
|
|
742
|
+
/**
|
|
743
|
+
* @param {Record<string, unknown>} meta
|
|
744
|
+
* @returns {Record<string, unknown>}
|
|
745
|
+
*/
|
|
746
|
+
function compactBatchMeta(meta) {
|
|
747
|
+
/** @type {Record<string, unknown>} */
|
|
748
|
+
const compact = {};
|
|
749
|
+
for (const key of [
|
|
750
|
+
'protocol_version',
|
|
751
|
+
'protocol_warning',
|
|
752
|
+
'budget_truncated',
|
|
753
|
+
'continuation_hint',
|
|
754
|
+
]) {
|
|
755
|
+
if (meta[key] !== undefined && meta[key] !== null) compact[key] = meta[key];
|
|
756
|
+
}
|
|
757
|
+
return compact;
|
|
758
|
+
}
|
|
759
|
+
|
|
715
760
|
/**
|
|
716
761
|
* @param {unknown} value
|
|
717
762
|
* @returns {Record<string, unknown>}
|
|
@@ -758,20 +803,6 @@ function truncateUrl(url) {
|
|
|
758
803
|
}
|
|
759
804
|
}
|
|
760
805
|
|
|
761
|
-
/**
|
|
762
|
-
* @param {string} code
|
|
763
|
-
* @returns {string}
|
|
764
|
-
*/
|
|
765
|
-
function summarizeErrorHint(code) {
|
|
766
|
-
if (code === 'ELEMENT_STALE') {
|
|
767
|
-
return 'Re-query the current page after navigation or DOM updates.';
|
|
768
|
-
}
|
|
769
|
-
if (code === 'RESULT_TRUNCATED') {
|
|
770
|
-
return 'Narrow the query or raise the relevant budget if more detail is required.';
|
|
771
|
-
}
|
|
772
|
-
return '';
|
|
773
|
-
}
|
|
774
|
-
|
|
775
806
|
/**
|
|
776
807
|
* Check if an array looks like a list of patch entries.
|
|
777
808
|
* Returns true for empty arrays when method context suggests patch.list.
|
|
@@ -140,6 +140,7 @@ export interface BridgeRecovery {
|
|
|
140
140
|
hint: string;
|
|
141
141
|
retry?: boolean;
|
|
142
142
|
retryAfterMs?: number;
|
|
143
|
+
alternativeMethod?: string;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
export interface BridgeFailure {
|
|
@@ -192,6 +193,7 @@ export interface TruncateResult {
|
|
|
192
193
|
export interface DomQueryParams extends BudgetOptions {
|
|
193
194
|
selector?: string;
|
|
194
195
|
withinRef?: string | null;
|
|
196
|
+
budget?: BudgetOptions;
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
export interface NormalizedDomQuery extends BridgeParams {
|
|
@@ -200,6 +202,15 @@ export interface NormalizedDomQuery extends BridgeParams {
|
|
|
200
202
|
budget: Budget;
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
export interface DomFindResult {
|
|
206
|
+
found: boolean;
|
|
207
|
+
nodes: Record<string, unknown>[];
|
|
208
|
+
count: number;
|
|
209
|
+
scanned: number;
|
|
210
|
+
truncated: boolean;
|
|
211
|
+
truncationReason: 'maxResults' | 'scanLimit' | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
export interface StyleQueryParams {
|
|
204
215
|
elementRef?: string;
|
|
205
216
|
target?: InputTarget;
|
|
@@ -397,7 +408,7 @@ export interface NormalizedEvaluateParams extends BridgeParams {
|
|
|
397
408
|
expression: string;
|
|
398
409
|
awaitPromise: boolean;
|
|
399
410
|
timeoutMs: number;
|
|
400
|
-
returnByValue:
|
|
411
|
+
returnByValue: true;
|
|
401
412
|
}
|
|
402
413
|
|
|
403
414
|
export interface ConsoleParams {
|
|
@@ -554,6 +565,24 @@ export interface NormalizedNetworkParams extends BridgeParams {
|
|
|
554
565
|
urlPattern: string | null;
|
|
555
566
|
}
|
|
556
567
|
|
|
568
|
+
export type NetworkInterceptAction = 'fulfill' | 'continue' | 'block';
|
|
569
|
+
|
|
570
|
+
export interface NetworkInterceptAddParams {
|
|
571
|
+
urlPattern?: string;
|
|
572
|
+
action?: NetworkInterceptAction;
|
|
573
|
+
statusCode?: number;
|
|
574
|
+
body?: string;
|
|
575
|
+
headers?: Record<string, string>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export interface NormalizedNetworkInterceptAddParams extends BridgeParams {
|
|
579
|
+
urlPattern: string;
|
|
580
|
+
action: NetworkInterceptAction;
|
|
581
|
+
statusCode?: number;
|
|
582
|
+
body?: string;
|
|
583
|
+
headers?: Record<string, string>;
|
|
584
|
+
}
|
|
585
|
+
|
|
557
586
|
export interface PageTextParams {
|
|
558
587
|
textBudget?: number;
|
|
559
588
|
}
|
|
@@ -5,7 +5,7 @@ description: 'Token-efficient Chrome tab inspection, interaction, and patching v
|
|
|
5
5
|
|
|
6
6
|
# Browser Bridge
|
|
7
7
|
|
|
8
|
-
Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches
|
|
8
|
+
Token-efficient Chrome tab inspection, interaction, and CSS/DOM patching through a local native-messaging bridge. Reads structured DOM, styles, layout, console, storage, network, and applies reversible patches - all from the real tab the user already has open.
|
|
9
9
|
|
|
10
10
|
This CLI skill is for agents that can run shell commands and where direct `bbx` control fits better than MCP tools: manual debugging, terminal reproduction, install/doctor flows, raw protocol access, or environments without MCP.
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ For open-ended investigation, start with structured reads (`page.get_state`, `do
|
|
|
21
21
|
bbx status # daemon + extension health
|
|
22
22
|
bbx doctor # install/access readiness
|
|
23
23
|
bbx access-request # ask user to enable access for the focused window
|
|
24
|
-
bbx restart # start/restart the
|
|
24
|
+
bbx restart # start/restart the daemon and running MCP servers
|
|
25
25
|
bbx call <method> '{...}' # any RPC method (raw output)
|
|
26
26
|
bbx <method> '{...}' # direct alias for an exact bridge method such as page.get_state
|
|
27
27
|
bbx call --tab 123 <method> '{...}' # explicit tab override
|
|
@@ -235,7 +235,7 @@ bbx page-text 2000 # extract page content
|
|
|
235
235
|
| Inspect | `dom.query`, `dom.describe`, `dom.get_html`, `styles.get_computed`, `layout.get_box_model` |
|
|
236
236
|
| Find | `dom.find_by_text`, `dom.find_by_role`, `dom.wait_for`, `dom.get_accessibility_tree` |
|
|
237
237
|
| Page State | `page.get_console`, `page.get_storage`, `page.get_text`, `page.wait_for_load_state`, `page.evaluate` (debugger-backed) |
|
|
238
|
-
| Network | `page.get_network`, `network.intercept.add/remove/list/clear` (debugger-backed; rules are in-memory and drop if the debugger detaches
|
|
238
|
+
| Network | `page.get_network`, `network.intercept.add/remove/list/clear` (debugger-backed; rules are in-memory and drop if the debugger detaches - verify with `list`) |
|
|
239
239
|
| Interact | `input.click`, `input.type`, `input.fill`, `input.focus`, `input.press_key`, `cdp.dispatch_key_event`, `input.hover`, `input.drag`, `input.scroll_into_view` |
|
|
240
240
|
| Tabs | `tabs.list` (preferred), `tabs.create` (avoid unless necessary), `tabs.close`, `tabs.activate` |
|
|
241
241
|
| Patch | `patch.apply_styles`, `patch.apply_dom`, `patch.rollback` |
|
|
@@ -291,7 +291,7 @@ dom.find_by_role('button', 'Login') → input.click
|
|
|
291
291
|
|
|
292
292
|
`bbx a11y-tree` and `dom.get_accessibility_tree` are sensitive to `maxDepth` and `maxNodes`. Shallow runs can undercount interactive nodes on real pages, so widen those limits before treating a low interactive count as a bug.
|
|
293
293
|
|
|
294
|
-
> **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly
|
|
294
|
+
> **MCP mode:** If Browser Bridge is connected via MCP (tools named `browser_dom`, `browser_capture`, etc.), use the MCP tools directly - do not shell out to `bbx`. The MCP tools map 1:1 to CLI capabilities. In prompts, `BB MCP` and `Browser Bridge MCP` both work. Do not treat `bbx-mcp` as a skill alias.
|
|
295
295
|
>
|
|
296
296
|
> For open-ended MCP inspection tasks, prefer `browser_investigate` first. It is read-only, designed for cheaper delegated investigation, and falls back to a deterministic sequence when the client cannot delegate. Escalate to `browser_capture` only when the structured investigation is not enough.
|
|
297
297
|
|
|
@@ -195,7 +195,7 @@ bbx call input.select_option '{"target":{"elementRef":"el_456"},"values":["us"]}
|
|
|
195
195
|
|
|
196
196
|
Select by value, label, or index. Multiple values for multi-select.
|
|
197
197
|
|
|
198
|
-
**Text fields
|
|
198
|
+
**Text fields - `fill` vs `type`:**
|
|
199
199
|
|
|
200
200
|
```bash
|
|
201
201
|
bbx fill el_123 hello@example.com # set value instantly (preferred for forms)
|
|
@@ -121,7 +121,7 @@ bbx access-request
|
|
|
121
121
|
bbx call access.request
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically
|
|
124
|
+
If a tab-bound call returns `ACCESS_DENIED`, it also surfaces the Enable prompt automatically - so explicit `access.request` is optional but useful for proactive setup.
|
|
125
125
|
|
|
126
126
|
If access is already pending for a window, do not call `access.request` again. Ask the user to click `Enable` for the requested window and wait for confirmation before continuing.
|
|
127
127
|
|
|
@@ -139,7 +139,7 @@ bbx call page.evaluate '{"expression":"await fetch(\"/api/health\").then(r=>r.js
|
|
|
139
139
|
|
|
140
140
|
### page.get_console
|
|
141
141
|
|
|
142
|
-
Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections.
|
|
142
|
+
Read buffered console output. The console interceptor is auto-installed on first call. Captures `log`, `warn`, `error`, `info`, `debug` plus uncaught exceptions and unhandled rejections. `level` is a minimum severity, so `warn` includes errors, exceptions, and rejections; `exception` and `rejection` select only that exact stream.
|
|
143
143
|
|
|
144
144
|
To capture a reproduction reliably, call it once with `clear: true` before triggering the event. This installs the interceptor and removes older entries. Reproduce the issue, then read again without `clear`.
|
|
145
145
|
|
|
@@ -182,7 +182,7 @@ If `_registryPruned` is true, refresh previously cached refs before reusing them
|
|
|
182
182
|
|
|
183
183
|
### dom.wait_for
|
|
184
184
|
|
|
185
|
-
Wait for a DOM condition using MutationObserver + 250 ms polling fallback. Returns `{found, elementRef, duration}`.
|
|
185
|
+
Wait for a DOM condition using MutationObserver + 250 ms polling fallback. A text-only wait searches all elements. `hidden` succeeds when no matching visible element remains, including when it is absent. Returns `{found, elementRef, duration}`.
|
|
186
186
|
|
|
187
187
|
- `state`: `attached` (default), `detached`, `visible`, `hidden`
|
|
188
188
|
- `text`: optional text content filter
|