@browserbridge/bbx 1.2.0 → 1.4.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/README.md +8 -5
- package/package.json +2 -2
- package/packages/agent-client/src/cli.js +56 -31
- package/packages/agent-client/src/client.js +81 -65
- package/packages/agent-client/src/command-registry.js +4 -15
- package/packages/agent-client/src/detect.js +3 -3
- package/packages/agent-client/src/install.js +3 -7
- package/packages/agent-client/src/mcp-config.js +20 -5
- package/packages/agent-client/src/runtime.js +7 -41
- package/packages/agent-client/src/setup-status.js +3 -13
- package/packages/agent-client/src/types.ts +139 -0
- package/packages/mcp-server/src/guidance.js +241 -0
- package/packages/mcp-server/src/handlers-capture.js +91 -16
- package/packages/mcp-server/src/handlers-dom.js +59 -4
- package/packages/mcp-server/src/handlers-navigation.js +22 -2
- package/packages/mcp-server/src/handlers-page.js +6 -11
- package/packages/mcp-server/src/handlers-utils.js +69 -1
- package/packages/mcp-server/src/server.js +111 -28
- package/packages/native-host/bin/postinstall.js +42 -21
- package/packages/native-host/src/auth-token.js +92 -0
- package/packages/native-host/src/daemon-process.js +1 -2
- package/packages/native-host/src/daemon.js +199 -30
- package/packages/native-host/src/framing.js +13 -0
- package/packages/native-host/src/native-host.js +25 -7
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/json-lines.js +29 -1
- package/packages/protocol/src/protocol.js +43 -0
- package/packages/protocol/src/registry.js +3 -9
- package/packages/protocol/src/types.ts +574 -0
- package/skills/browser-bridge/SKILL.md +21 -5
- package/skills/browser-bridge/agents/openai.yaml +1 -1
- package/skills/browser-bridge/references/interaction.md +6 -6
- package/skills/browser-bridge/references/protocol.md +57 -54
- package/skills/browser-bridge/references/ui-workflows.md +1 -1
- package/packages/protocol/src/types.js +0 -626
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
|
|
5
|
-
import { createFailure, ERROR_CODES } from '../../protocol/src/index.js';
|
|
5
|
+
import { createFailure, ERROR_CODES, MAX_JSON_LINE_BYTES } from '../../protocol/src/index.js';
|
|
6
|
+
import { readBridgeAuthToken } from './auth-token.js';
|
|
6
7
|
import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
|
|
7
8
|
import { spawnBridgeDaemonProcess } from './daemon-process.js';
|
|
8
|
-
import { createNativeMessageReader,
|
|
9
|
+
import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } from './framing.js';
|
|
9
10
|
|
|
10
11
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
11
12
|
|
|
@@ -117,11 +118,13 @@ export async function runNativeHost({
|
|
|
117
118
|
socketPath = undefined,
|
|
118
119
|
} = {}) {
|
|
119
120
|
const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
121
|
+
const writeNativeMessageQueued = createNativeMessageWriter(process.stdout);
|
|
122
|
+
|
|
120
123
|
let socket;
|
|
121
124
|
try {
|
|
122
125
|
socket = await connectWithBootstrap(resolvedTransport);
|
|
123
126
|
} catch (error) {
|
|
124
|
-
await
|
|
127
|
+
await writeNativeMessageQueued({
|
|
125
128
|
type: 'agent.response',
|
|
126
129
|
response: createFailure(
|
|
127
130
|
'native_bootstrap',
|
|
@@ -144,11 +147,21 @@ export async function runNativeHost({
|
|
|
144
147
|
socket.once('close', cleanupStdinEndListener);
|
|
145
148
|
socket.once('end', cleanupStdinEndListener);
|
|
146
149
|
socket.once('error', cleanupStdinEndListener);
|
|
147
|
-
|
|
150
|
+
const authToken = resolvedTransport.type === 'tcp' ? await readBridgeAuthToken() : null;
|
|
151
|
+
await writeJsonLine(socket, {
|
|
152
|
+
type: 'register',
|
|
153
|
+
role: 'extension',
|
|
154
|
+
...(authToken ? { authToken } : {}),
|
|
155
|
+
});
|
|
148
156
|
|
|
149
157
|
let lineBuffer = '';
|
|
150
158
|
socket.on('data', (chunk) => {
|
|
151
159
|
lineBuffer += chunk;
|
|
160
|
+
if (!lineBuffer.includes('\n') && Buffer.byteLength(lineBuffer, 'utf8') > MAX_JSON_LINE_BYTES) {
|
|
161
|
+
console.error(`native-host: daemon JSON line exceeded ${MAX_JSON_LINE_BYTES} bytes`);
|
|
162
|
+
socket.destroy();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
152
165
|
while (lineBuffer.includes('\n')) {
|
|
153
166
|
const index = lineBuffer.indexOf('\n');
|
|
154
167
|
const line = lineBuffer.slice(0, index).trim();
|
|
@@ -156,6 +169,11 @@ export async function runNativeHost({
|
|
|
156
169
|
if (!line) {
|
|
157
170
|
continue;
|
|
158
171
|
}
|
|
172
|
+
if (Buffer.byteLength(line, 'utf8') > MAX_JSON_LINE_BYTES) {
|
|
173
|
+
console.error(`native-host: daemon JSON line exceeded ${MAX_JSON_LINE_BYTES} bytes`);
|
|
174
|
+
socket.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
159
177
|
let message;
|
|
160
178
|
try {
|
|
161
179
|
message = JSON.parse(line);
|
|
@@ -164,11 +182,11 @@ export async function runNativeHost({
|
|
|
164
182
|
}
|
|
165
183
|
void (async () => {
|
|
166
184
|
if (message.type === 'extension.request') {
|
|
167
|
-
await
|
|
185
|
+
await writeNativeMessageQueued(message.request);
|
|
168
186
|
return;
|
|
169
187
|
}
|
|
170
188
|
if (message.type === 'agent.response') {
|
|
171
|
-
await
|
|
189
|
+
await writeNativeMessageQueued({
|
|
172
190
|
type: 'host.bridge_response',
|
|
173
191
|
response: message.response,
|
|
174
192
|
});
|
|
@@ -178,7 +196,7 @@ export async function runNativeHost({
|
|
|
178
196
|
message.type === 'extension.setup_status.response' ||
|
|
179
197
|
message.type === 'extension.setup_status.error'
|
|
180
198
|
) {
|
|
181
|
-
await
|
|
199
|
+
await writeNativeMessageQueued({
|
|
182
200
|
type:
|
|
183
201
|
message.type === 'extension.setup_status.response'
|
|
184
202
|
? 'host.setup_status.response'
|
|
@@ -24,6 +24,9 @@ export const DEFAULT_DEVICE_SCALE_FACTOR = 0;
|
|
|
24
24
|
/** Maximum size of a Chrome native messaging message in bytes. */
|
|
25
25
|
export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;
|
|
26
26
|
|
|
27
|
+
/** Maximum size of one newline-delimited daemon socket message in bytes. */
|
|
28
|
+
export const MAX_JSON_LINE_BYTES = MAX_NATIVE_MESSAGE_BYTES;
|
|
29
|
+
|
|
27
30
|
/** Default timeout for a bridge request awaiting an extension response (ms). */
|
|
28
31
|
export const DEFAULT_DAEMON_PENDING_TIMEOUT_MS = 30_000;
|
|
29
32
|
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { MAX_JSON_LINE_BYTES } from './defaults.js';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Install a newline-delimited JSON parser on a socket's `data` event.
|
|
5
7
|
*
|
|
6
8
|
* @param {import('node:net').Socket} socket
|
|
7
9
|
* @param {(message: unknown) => void} onMessage
|
|
10
|
+
* @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void }} [options]
|
|
8
11
|
* @returns {void}
|
|
9
12
|
*/
|
|
10
|
-
export function parseJsonLines(socket, onMessage) {
|
|
13
|
+
export function parseJsonLines(socket, onMessage, options = {}) {
|
|
11
14
|
let buffer = '';
|
|
15
|
+
const maxLineBytes =
|
|
16
|
+
typeof options.maxLineBytes === 'number' && Number.isFinite(options.maxLineBytes)
|
|
17
|
+
? Math.max(1, Math.floor(options.maxLineBytes))
|
|
18
|
+
: MAX_JSON_LINE_BYTES;
|
|
12
19
|
socket.setEncoding('utf8');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Error} error
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function fail(error) {
|
|
26
|
+
options.onProtocolError?.(error);
|
|
27
|
+
const destroy = /** @type {{ destroy?: (() => void) | undefined }} */ (socket).destroy;
|
|
28
|
+
if (typeof destroy === 'function') {
|
|
29
|
+
destroy.call(socket);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
/** @param {string} chunk */
|
|
14
34
|
socket.on('data', (chunk) => {
|
|
15
35
|
buffer += chunk;
|
|
36
|
+
if (!buffer.includes('\n') && Buffer.byteLength(buffer, 'utf8') > maxLineBytes) {
|
|
37
|
+
fail(new Error(`JSON line exceeds ${maxLineBytes} bytes.`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
16
40
|
while (buffer.includes('\n')) {
|
|
17
41
|
const index = buffer.indexOf('\n');
|
|
18
42
|
const line = buffer.slice(0, index).trim();
|
|
@@ -20,6 +44,10 @@ export function parseJsonLines(socket, onMessage) {
|
|
|
20
44
|
if (!line) {
|
|
21
45
|
continue;
|
|
22
46
|
}
|
|
47
|
+
if (Buffer.byteLength(line, 'utf8') > maxLineBytes) {
|
|
48
|
+
fail(new Error(`JSON line exceeds ${maxLineBytes} bytes.`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
23
51
|
try {
|
|
24
52
|
onMessage(JSON.parse(line));
|
|
25
53
|
} catch {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DEFAULT_A11Y_MAX_NODES,
|
|
8
8
|
DEFAULT_CONSOLE_LIMIT,
|
|
9
9
|
DEFAULT_EVAL_TIMEOUT_MS,
|
|
10
|
+
DEFAULT_LOG_TAIL_LIMIT,
|
|
10
11
|
DEFAULT_MAX_DEPTH,
|
|
11
12
|
DEFAULT_MAX_HTML_LENGTH,
|
|
12
13
|
DEFAULT_MAX_NODES,
|
|
@@ -29,6 +30,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
29
30
|
/** @typedef {import('./types.js').BridgeSuccessResponse} BridgeSuccessResponse */
|
|
30
31
|
/** @typedef {import('./types.js').CheckedActionParams} CheckedActionParams */
|
|
31
32
|
/** @typedef {import('./types.js').CdpDispatchKeyEventParams} CdpDispatchKeyEventParams */
|
|
33
|
+
/** @typedef {import('./types.js').CdpNodeIdParams} CdpNodeIdParams */
|
|
32
34
|
/** @typedef {import('./types.js').ConsoleParams} ConsoleParams */
|
|
33
35
|
/** @typedef {import('./types.js').DomQueryParams} DomQueryParams */
|
|
34
36
|
/** @typedef {import('./types.js').DragParams} DragParams */
|
|
@@ -38,11 +40,13 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
38
40
|
/** @typedef {import('./types.js').GetHtmlParams} GetHtmlParams */
|
|
39
41
|
/** @typedef {import('./types.js').HoverParams} HoverParams */
|
|
40
42
|
/** @typedef {import('./types.js').InputActionParams} InputActionParams */
|
|
43
|
+
/** @typedef {import('./types.js').LogTailParams} LogTailParams */
|
|
41
44
|
/** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
|
|
42
45
|
/** @typedef {import('./types.js').NetworkParams} NetworkParams */
|
|
43
46
|
/** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
|
|
44
47
|
/** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
|
|
45
48
|
/** @typedef {import('./types.js').NormalizedCdpDispatchKeyEventParams} NormalizedCdpDispatchKeyEventParams */
|
|
49
|
+
/** @typedef {import('./types.js').NormalizedCdpNodeIdParams} NormalizedCdpNodeIdParams */
|
|
46
50
|
/** @typedef {import('./types.js').NormalizedConsoleParams} NormalizedConsoleParams */
|
|
47
51
|
/** @typedef {import('./types.js').NormalizedDomQuery} NormalizedDomQuery */
|
|
48
52
|
/** @typedef {import('./types.js').NormalizedDragParams} NormalizedDragParams */
|
|
@@ -52,6 +56,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
52
56
|
/** @typedef {import('./types.js').NormalizedGetHtmlParams} NormalizedGetHtmlParams */
|
|
53
57
|
/** @typedef {import('./types.js').NormalizedHoverParams} NormalizedHoverParams */
|
|
54
58
|
/** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
|
|
59
|
+
/** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
|
|
55
60
|
/** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
|
|
56
61
|
/** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
|
|
57
62
|
/** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
|
|
@@ -209,6 +214,12 @@ export function validateBridgeRequest(request) {
|
|
|
209
214
|
'session_id is no longer supported. Use tab_id or window-scoped default routing.'
|
|
210
215
|
);
|
|
211
216
|
}
|
|
217
|
+
if (
|
|
218
|
+
candidate.params != null &&
|
|
219
|
+
(typeof candidate.params !== 'object' || Array.isArray(candidate.params))
|
|
220
|
+
) {
|
|
221
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'Request params must be an object.');
|
|
222
|
+
}
|
|
212
223
|
const parsedTabId = Number(candidate.tab_id);
|
|
213
224
|
|
|
214
225
|
const method = /** @type {BridgeMethod} */ (candidate.method);
|
|
@@ -242,6 +253,8 @@ export function validateBridgeRequest(request) {
|
|
|
242
253
|
*/
|
|
243
254
|
function normalizeRequestParams(method, params) {
|
|
244
255
|
switch (method) {
|
|
256
|
+
case 'log.tail':
|
|
257
|
+
return normalizeLogTailParams(params);
|
|
245
258
|
case 'dom.query':
|
|
246
259
|
return normalizeDomQuery(params);
|
|
247
260
|
case 'page.evaluate':
|
|
@@ -285,6 +298,9 @@ function normalizeRequestParams(method, params) {
|
|
|
285
298
|
return normalizeInputAction(params);
|
|
286
299
|
case 'cdp.dispatch_key_event':
|
|
287
300
|
return normalizeCdpDispatchKeyEventParams(params);
|
|
301
|
+
case 'cdp.get_box_model':
|
|
302
|
+
case 'cdp.get_computed_styles_for_node':
|
|
303
|
+
return normalizeCdpNodeIdParams(params);
|
|
288
304
|
case 'input.set_checked':
|
|
289
305
|
return normalizeCheckedAction(params);
|
|
290
306
|
case 'input.select_option':
|
|
@@ -429,6 +445,20 @@ export function normalizeCdpDispatchKeyEventParams(params = {}) {
|
|
|
429
445
|
};
|
|
430
446
|
}
|
|
431
447
|
|
|
448
|
+
/**
|
|
449
|
+
* @param {CdpNodeIdParams} [params={}]
|
|
450
|
+
* @returns {NormalizedCdpNodeIdParams}
|
|
451
|
+
*/
|
|
452
|
+
export function normalizeCdpNodeIdParams(params = {}) {
|
|
453
|
+
if (typeof params.nodeId !== 'number' || !Number.isFinite(params.nodeId)) {
|
|
454
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'nodeId must be a finite number.');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
nodeId: params.nodeId,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
432
462
|
/**
|
|
433
463
|
* @param {CheckedActionParams} [params={}]
|
|
434
464
|
* @returns {NormalizedCheckedAction}
|
|
@@ -642,6 +672,9 @@ export function normalizeHoverParams(params = {}) {
|
|
|
642
672
|
: undefined
|
|
643
673
|
),
|
|
644
674
|
duration: clampInt(params.duration, 0, 5_000, 0),
|
|
675
|
+
modifiers: Array.isArray(params.modifiers)
|
|
676
|
+
? params.modifiers.filter((modifier) => typeof modifier === 'string' && modifier.trim())
|
|
677
|
+
: [],
|
|
645
678
|
};
|
|
646
679
|
}
|
|
647
680
|
|
|
@@ -763,6 +796,16 @@ export function normalizePageTextParams(params = {}) {
|
|
|
763
796
|
};
|
|
764
797
|
}
|
|
765
798
|
|
|
799
|
+
/**
|
|
800
|
+
* @param {LogTailParams} [params={}]
|
|
801
|
+
* @returns {NormalizedLogTailParams}
|
|
802
|
+
*/
|
|
803
|
+
export function normalizeLogTailParams(params = {}) {
|
|
804
|
+
return {
|
|
805
|
+
limit: clampInt(params.limit, 1, 200, DEFAULT_LOG_TAIL_LIMIT),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
766
809
|
/**
|
|
767
810
|
* @param {ViewportResizeParams} [params={}]
|
|
768
811
|
* @returns {NormalizedViewportResizeParams}
|
|
@@ -123,7 +123,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
123
123
|
['kind', 'target'],
|
|
124
124
|
'trivial'
|
|
125
125
|
),
|
|
126
|
-
'log.tail': createRegistryEntry('log.tail', 'system', false, [], 'trivial'),
|
|
126
|
+
'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
|
|
127
127
|
'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
|
|
128
128
|
'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
|
|
129
129
|
// tabs — trivial
|
|
@@ -413,18 +413,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
413
413
|
// cdp — high (raw protocol, large payloads)
|
|
414
414
|
'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
|
|
415
415
|
'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
|
|
416
|
-
'cdp.get_box_model': createRegistryEntry(
|
|
417
|
-
'cdp.get_box_model',
|
|
418
|
-
'cdp',
|
|
419
|
-
true,
|
|
420
|
-
['elementRef'],
|
|
421
|
-
'high'
|
|
422
|
-
),
|
|
416
|
+
'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
|
|
423
417
|
'cdp.get_computed_styles_for_node': createRegistryEntry(
|
|
424
418
|
'cdp.get_computed_styles_for_node',
|
|
425
419
|
'cdp',
|
|
426
420
|
true,
|
|
427
|
-
['
|
|
421
|
+
['nodeId'],
|
|
428
422
|
'high'
|
|
429
423
|
),
|
|
430
424
|
'cdp.dispatch_key_event': createRegistryEntry(
|