@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
|
@@ -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 */
|
|
@@ -30,6 +36,51 @@ import {
|
|
|
30
36
|
export const MAX_BATCH_CALLS = 20;
|
|
31
37
|
export const MAX_BATCH_CONCURRENCY = 5;
|
|
32
38
|
|
|
39
|
+
/** @type {ReadonlySet<BridgeMethod>} */
|
|
40
|
+
const BATCH_SAFE_METHODS = new Set([
|
|
41
|
+
'health.ping',
|
|
42
|
+
'daemon.metrics',
|
|
43
|
+
'tabs.list',
|
|
44
|
+
'skill.get_runtime_context',
|
|
45
|
+
'setup.get_status',
|
|
46
|
+
'log.tail',
|
|
47
|
+
'page.get_state',
|
|
48
|
+
'page.get_console',
|
|
49
|
+
'page.wait_for_load_state',
|
|
50
|
+
'page.get_storage',
|
|
51
|
+
'page.get_text',
|
|
52
|
+
'page.get_network',
|
|
53
|
+
'dom.query',
|
|
54
|
+
'dom.describe',
|
|
55
|
+
'dom.get_text',
|
|
56
|
+
'dom.get_attributes',
|
|
57
|
+
'dom.wait_for',
|
|
58
|
+
'dom.find_by_text',
|
|
59
|
+
'dom.find_by_role',
|
|
60
|
+
'dom.get_html',
|
|
61
|
+
'dom.get_accessibility_tree',
|
|
62
|
+
'layout.get_box_model',
|
|
63
|
+
'layout.hit_test',
|
|
64
|
+
'styles.get_computed',
|
|
65
|
+
'styles.get_matched_rules',
|
|
66
|
+
'network.intercept.list',
|
|
67
|
+
'patch.list',
|
|
68
|
+
'performance.get_metrics',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {BridgeMethod} method
|
|
73
|
+
* @param {Record<string, unknown>} params
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isBatchSafeBridgeCall(method, params) {
|
|
77
|
+
if (!BATCH_SAFE_METHODS.has(method)) return false;
|
|
78
|
+
if ((method === 'page.get_console' || method === 'page.get_network') && params.clear === true) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
/** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
|
|
34
85
|
export const PAGE_ACTIONS = {
|
|
35
86
|
state: { method: 'page.get_state', params: () => ({}) },
|
|
@@ -118,125 +169,120 @@ export async function handleBatchTool(args) {
|
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
const calls = args.calls;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
response: null,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
140
|
-
return {
|
|
141
|
-
method: call.method,
|
|
142
|
-
tabId: null,
|
|
143
|
-
ok: false,
|
|
144
|
-
summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
|
|
145
|
-
evidence: null,
|
|
146
|
-
error: {
|
|
147
|
-
code: 'INVALID_REQUEST',
|
|
148
|
-
message: `Unknown bridge method "${call.method}".`,
|
|
149
|
-
},
|
|
150
|
-
response: null,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (call.method === 'setup.install') {
|
|
155
|
-
return {
|
|
156
|
-
method: call.method,
|
|
157
|
-
tabId: null,
|
|
158
|
-
ok: false,
|
|
159
|
-
summary: 'INVALID_REQUEST: Use browser_setup for setup changes.',
|
|
160
|
-
evidence: null,
|
|
161
|
-
error: {
|
|
162
|
-
code: 'INVALID_REQUEST',
|
|
163
|
-
message: 'Use browser_setup for setup changes.',
|
|
164
|
-
},
|
|
165
|
-
response: null,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const method = /** @type {BridgeMethod} */ (call.method);
|
|
170
|
-
const tabId = bridgeMethodNeedsTab(method)
|
|
171
|
-
? typeof call.tabId === 'number'
|
|
172
|
-
? call.tabId
|
|
173
|
-
: null
|
|
174
|
-
: null;
|
|
175
|
-
const tokenBudget = getToolTokenBudget(call);
|
|
176
|
-
const destinationId = typeof call.destinationId === 'string' ? call.destinationId : null;
|
|
172
|
+
for (const [index, call] of calls.entries()) {
|
|
173
|
+
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
174
|
+
return summarizeToolError(`calls[${index}] needs a method.`);
|
|
175
|
+
}
|
|
176
|
+
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
177
|
+
return summarizeToolError(`Unknown bridge method "${call.method}" at calls[${index}].`);
|
|
178
|
+
}
|
|
179
|
+
const method = /** @type {BridgeMethod} */ (call.method);
|
|
180
|
+
if (!isBatchSafeBridgeCall(method, call.params || {})) {
|
|
181
|
+
return summarizeToolError(
|
|
182
|
+
`${method} is not safe for parallel batch execution. Use its specialized tool or browser_call sequentially.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch (error) {
|
|
209
|
-
return summarizeBatchErrorItem({
|
|
210
|
-
method,
|
|
211
|
-
tabId,
|
|
212
|
-
error,
|
|
213
|
-
durationMs: Date.now() - startTime,
|
|
214
|
-
});
|
|
215
|
-
} finally {
|
|
216
|
-
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 {
|
|
217
217
|
await callClient.close();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
callError ??= error;
|
|
218
220
|
}
|
|
219
221
|
}
|
|
220
|
-
}
|
|
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
|
+
});
|
|
221
237
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|
|
236
255
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
/** @type {ReadonlySet<string>} */
|
|
257
|
+
const RECOGNIZED_ERROR_CODES = new Set(Object.values(ERROR_CODES));
|
|
258
|
+
|
|
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
|
+
};
|
|
240
286
|
}
|
|
241
287
|
|
|
242
288
|
/**
|
|
@@ -264,45 +310,26 @@ async function mapWithConcurrency(values, concurrency, callback) {
|
|
|
264
310
|
}
|
|
265
311
|
|
|
266
312
|
/**
|
|
267
|
-
* @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
|
|
268
314
|
* @returns {Promise<ToolResult>}
|
|
269
315
|
*/
|
|
270
316
|
export async function handleRawCallTool(args) {
|
|
271
317
|
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
272
318
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
273
319
|
}
|
|
274
|
-
if (args.method === 'setup.install') {
|
|
275
|
-
return summarizeToolError('Use browser_setup for setup changes.');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
320
|
return withToolClient(
|
|
279
321
|
async (client) => {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
args.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
source: REQUEST_SOURCE,
|
|
287
|
-
}
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
if (!response.ok) {
|
|
291
|
-
return createToolResult(
|
|
292
|
-
response.error.message,
|
|
293
|
-
{
|
|
294
|
-
ok: false,
|
|
295
|
-
error: response.error,
|
|
296
|
-
response,
|
|
297
|
-
},
|
|
298
|
-
true
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return createToolResult(`Called ${args.method}.`, {
|
|
303
|
-
ok: true,
|
|
304
|
-
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),
|
|
305
328
|
});
|
|
329
|
+
if (response.ok && method.startsWith('screenshot.')) {
|
|
330
|
+
return createScreenshotResult(response, method);
|
|
331
|
+
}
|
|
332
|
+
return summarizeToolResponse(response, method, params);
|
|
306
333
|
},
|
|
307
334
|
{ destinationId: args.destinationId ?? null }
|
|
308
335
|
);
|