@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.
Files changed (29) hide show
  1. package/package.json +7 -7
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-helpers.js +20 -3
  4. package/packages/agent-client/src/cli.js +273 -76
  5. package/packages/agent-client/src/command-registry.js +12 -6
  6. package/packages/agent-client/src/install.js +39 -4
  7. package/packages/agent-client/src/mcp-config.js +28 -14
  8. package/packages/agent-client/src/remotes.js +93 -15
  9. package/packages/agent-client/src/runtime.js +6 -3
  10. package/packages/mcp-server/src/bin.js +10 -1
  11. package/packages/mcp-server/src/guidance.js +2 -2
  12. package/packages/mcp-server/src/handlers-capture.js +62 -32
  13. package/packages/mcp-server/src/handlers-dom.js +2 -2
  14. package/packages/mcp-server/src/handlers-navigation.js +30 -9
  15. package/packages/mcp-server/src/handlers-page.js +170 -143
  16. package/packages/mcp-server/src/handlers-utils.js +277 -8
  17. package/packages/mcp-server/src/handlers.js +31 -10
  18. package/packages/mcp-server/src/lifecycle.js +413 -0
  19. package/packages/mcp-server/src/server.js +59 -30
  20. package/packages/native-host/src/daemon.js +3 -1
  21. package/packages/protocol/src/capabilities.js +1 -1
  22. package/packages/protocol/src/defaults.js +2 -0
  23. package/packages/protocol/src/protocol.js +122 -9
  24. package/packages/protocol/src/registry.js +14 -14
  25. package/packages/protocol/src/summary.js +61 -30
  26. package/packages/protocol/src/types.ts +30 -1
  27. package/skills/browser-bridge/SKILL.md +4 -4
  28. package/skills/browser-bridge/references/interaction.md +1 -1
  29. 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
- /** @param {BridgeClient | null} client */
122
- const executeBatch = async (client) => {
123
- const results = await mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, async (call) => {
124
- if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
125
- return {
126
- method: '',
127
- tabId: null,
128
- ok: false,
129
- summary: 'INVALID_REQUEST: Each batch call needs a method.',
130
- evidence: null,
131
- error: {
132
- code: 'INVALID_REQUEST',
133
- message: 'Each batch call needs a method.',
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
- const startTime = Date.now();
179
- const callClient = destinationId
180
- ? await createBridgeClientForDestination(destinationId)
181
- : client;
182
- if (!callClient) {
183
- return summarizeBatchErrorItem({
184
- method,
185
- tabId,
186
- error: new Error('A local bridge client is required for calls without destinationId.'),
187
- durationMs: Date.now() - startTime,
188
- });
189
- }
190
- try {
191
- if (destinationId) {
192
- await callClient.connect();
193
- }
194
- const response = await requestBridgeWithRetry(callClient, method, call.params || {}, {
195
- tabId,
196
- source: REQUEST_SOURCE,
197
- tokenBudget,
198
- });
199
- return {
200
- destinationId,
201
- ...summarizeBatchResponseItem({
202
- method,
203
- tabId,
204
- response,
205
- durationMs: Date.now() - startTime,
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
- const failureCount = results.filter((result) => !result.ok).length;
223
- const summary =
224
- failureCount === 0
225
- ? `Batch executed ${results.length} call(s).`
226
- : `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
227
- return createToolResult(
228
- summary,
229
- {
230
- ok: failureCount === 0,
231
- results,
232
- },
233
- failureCount > 0
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
- return calls.every((call) => typeof call?.destinationId === 'string')
238
- ? executeBatch(null)
239
- : withToolClient(executeBatch);
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 response = await requestBridgeWithRetry(
281
- client,
282
- /** @type {BridgeMethod} */ (args.method),
283
- args.params || {},
284
- {
285
- tabId: typeof args.tabId === 'number' ? args.tabId : null,
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
  );