@browserbridge/bbx 1.1.0 → 1.3.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.
Files changed (31) hide show
  1. package/README.md +6 -5
  2. package/package.json +1 -1
  3. package/packages/agent-client/src/cli.js +30 -20
  4. package/packages/agent-client/src/client.js +105 -42
  5. package/packages/agent-client/src/command-registry.js +4 -14
  6. package/packages/agent-client/src/detect.js +3 -3
  7. package/packages/agent-client/src/install.js +3 -7
  8. package/packages/agent-client/src/mcp-config.js +1 -3
  9. package/packages/agent-client/src/runtime.js +7 -41
  10. package/packages/agent-client/src/setup-status.js +3 -13
  11. package/packages/agent-client/src/types.ts +131 -0
  12. package/packages/mcp-server/src/handlers-capture.js +291 -0
  13. package/packages/mcp-server/src/handlers-dom.js +203 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +318 -0
  17. package/packages/mcp-server/src/handlers.js +59 -1176
  18. package/packages/mcp-server/src/server.js +2 -1
  19. package/packages/native-host/bin/bridge-daemon.js +2 -1
  20. package/packages/native-host/bin/install-manifest.js +8 -0
  21. package/packages/native-host/bin/postinstall.js +46 -9
  22. package/packages/native-host/src/daemon-logger.js +157 -0
  23. package/packages/native-host/src/daemon-process.js +43 -18
  24. package/packages/native-host/src/daemon.js +133 -12
  25. package/packages/native-host/src/framing.js +13 -0
  26. package/packages/native-host/src/native-host.js +7 -5
  27. package/packages/protocol/src/capabilities.js +1 -0
  28. package/packages/protocol/src/protocol.js +40 -0
  29. package/packages/protocol/src/registry.js +5 -9
  30. package/packages/protocol/src/types.ts +572 -0
  31. package/packages/protocol/src/types.js +0 -626
@@ -1,309 +1,70 @@
1
1
  // @ts-check
2
2
 
3
3
  import os from 'node:os';
4
+ import { DEFAULT_CONSOLE_LIMIT, createRuntimeContext } from '../../protocol/src/index.js';
5
+ import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
4
6
  import {
5
- bridgeMethodNeedsTab,
6
- createRuntimeContext,
7
- DEFAULT_CONSOLE_LIMIT,
8
- DEFAULT_MAX_HTML_LENGTH,
9
- DEFAULT_NETWORK_LIMIT,
10
- estimateJsonPayloadCost,
11
- getBudgetPreset,
12
- getErrorRecovery,
13
- isBudgetPresetName,
14
- METHOD_SET,
15
- summarizeBatchErrorItem,
16
- summarizeBatchResponseItem,
17
- } from '../../protocol/src/index.js';
18
- import {
7
+ callBridgeTool,
8
+ createToolResult,
19
9
  getDoctorReport,
20
- requestBridge,
21
- resolveRef,
22
- withBridgeClient,
23
- } from '../../agent-client/src/runtime.js';
24
- import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-client/src/subagent.js';
25
- import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
26
-
27
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
28
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
10
+ getToolTokenBudget,
11
+ applyLimitBudgetPreset,
12
+ summarizeToolError,
13
+ summarizeToolResponse,
14
+ withToolClient,
15
+ REQUEST_SOURCE,
16
+ } from './handlers-utils.js';
17
+
18
+ export {
19
+ createToolResult,
20
+ summarizeToolError,
21
+ summarizeToolResponse,
22
+ withToolClient,
23
+ resolveToolRef,
24
+ getBudgetPresetName,
25
+ inferBudgetFromSelector,
26
+ getToolTokenBudget,
27
+ applyTreeBudgetPreset,
28
+ applyTextBudgetPreset,
29
+ applyLimitBudgetPreset,
30
+ applyHtmlBudgetPreset,
31
+ requestBridgeWithRetry,
32
+ callBridgeTool,
33
+ dispatchToolAction,
34
+ REQUEST_SOURCE,
35
+ } from './handlers-utils.js';
36
+
37
+ export {
38
+ DOM_ACTIONS,
39
+ STYLES_LAYOUT_ACTIONS,
40
+ PATCH_ACTIONS,
41
+ handleDomTool,
42
+ handleStylesLayoutTool,
43
+ handlePatchTool,
44
+ } from './handlers-dom.js';
45
+
46
+ export { NAVIGATION_ACTIONS, handleTabsTool, handleNavigationTool } from './handlers-navigation.js';
47
+
48
+ export {
49
+ CAPTURE_ACTIONS,
50
+ INPUT_ACTION_METHODS,
51
+ handleCaptureTool,
52
+ handleInputTool,
53
+ } from './handlers-capture.js';
54
+
55
+ export {
56
+ PAGE_ACTIONS,
57
+ handlePageTool,
58
+ handleBatchTool,
59
+ handleRawCallTool,
60
+ handleInvestigateTool,
61
+ } from './handlers-page.js';
62
+
63
+ /** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
64
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
29
65
 
30
- const REQUEST_SOURCE = 'mcp';
31
66
  const HOME_DIR = os.homedir();
32
67
 
33
- /**
34
- * @typedef {{
35
- * content: Array<{ type: 'text', text: string }>,
36
- * structuredContent: Record<string, unknown>,
37
- * isError?: boolean
38
- * }} ToolResult
39
- */
40
-
41
- /**
42
- * @param {string} summary
43
- * @param {Record<string, unknown>} [structuredContent={}]
44
- * @param {boolean} [isError=false]
45
- * @returns {ToolResult}
46
- */
47
- function createToolResult(summary, structuredContent = {}, isError = false) {
48
- const toolResult = {
49
- content: [{ type: /** @type {'text'} */ ('text'), text: summary }],
50
- structuredContent,
51
- ...(isError ? { isError: true } : {}),
52
- };
53
- const delivered = estimateJsonPayloadCost(toolResult);
54
- return {
55
- ...toolResult,
56
- structuredContent: {
57
- ...structuredContent,
58
- deliveredBytes: delivered.bytes,
59
- deliveredTokens: delivered.approxTokens,
60
- deliveredCostClass: delivered.costClass,
61
- },
62
- };
63
- }
64
-
65
- /**
66
- * @param {BridgeResponse} response
67
- * @param {string} [method]
68
- * @returns {ToolResult}
69
- */
70
- function summarizeToolResponse(response, method) {
71
- const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
72
- return createToolResult(summary.summary, summary, !summary.ok);
73
- }
74
-
75
- /**
76
- * @param {unknown} error
77
- * @returns {ToolResult}
78
- */
79
- function summarizeToolError(error) {
80
- const message = error instanceof Error ? error.message : String(error);
81
- return createToolResult(
82
- `ERROR: ${message}`,
83
- {
84
- ok: false,
85
- evidence: null,
86
- },
87
- true
88
- );
89
- }
90
-
91
- /**
92
- * @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
93
- * @returns {Promise<ToolResult>}
94
- */
95
- async function withToolClient(callback) {
96
- try {
97
- return await withBridgeClient(callback);
98
- } catch (error) {
99
- return summarizeToolError(error);
100
- }
101
- }
102
-
103
- /**
104
- * @param {import('../../agent-client/src/client.js').BridgeClient} client
105
- * @param {{ elementRef?: string | undefined, selector?: string | undefined }} input
106
- * @param {number | null | undefined} [tabId]
107
- * @returns {Promise<string>}
108
- */
109
- async function resolveToolRef(client, input, tabId = null) {
110
- if (typeof input.elementRef === 'string' && input.elementRef) {
111
- return input.elementRef;
112
- }
113
- if (typeof input.selector === 'string' && input.selector) {
114
- return resolveRef(client, input.selector, tabId, REQUEST_SOURCE);
115
- }
116
- throw new Error('Provide either elementRef or selector.');
117
- }
118
-
119
- /**
120
- * @param {unknown} value
121
- * @returns {'quick' | 'normal' | 'deep' | null}
122
- */
123
- function getBudgetPresetName(value) {
124
- return isBudgetPresetName(value) ? value : null;
125
- }
126
-
127
- /**
128
- * Infer a budget preset from selector specificity when the caller hasn't
129
- * provided an explicit budgetPreset.
130
- *
131
- * - ID selectors (#foo) or single element refs -> quick
132
- * - Class / tag / attribute selectors -> normal
133
- * - Universal (*), deeply nested, or missing selector -> deep
134
- *
135
- * @param {{ budgetPreset?: unknown, selector?: unknown, elementRef?: unknown }} args
136
- * @returns {'quick' | 'normal' | 'deep' | null}
137
- */
138
- function inferBudgetFromSelector(args) {
139
- if (getBudgetPresetName(args.budgetPreset)) return null; // explicit preset wins
140
- if (typeof args.elementRef === 'string' && args.elementRef) return 'quick';
141
- const sel = typeof args.selector === 'string' ? args.selector.trim() : '';
142
- if (!sel || sel === '*' || sel === 'body') return null; // keep default
143
- if (/^#[\w-]+$/.test(sel)) return 'quick';
144
- if ((sel.match(/\s/g) || []).length >= 3) return 'deep';
145
- return 'normal';
146
- }
147
-
148
- /**
149
- * @param {{ budgetPreset?: unknown }} args
150
- * @returns {number | null}
151
- */
152
- function getToolTokenBudget(args) {
153
- const presetName = getBudgetPresetName(args.budgetPreset);
154
- return presetName ? getBudgetPreset(presetName).tokenBudget : null;
155
- }
156
-
157
- /**
158
- * @template {{ budgetPreset?: unknown, maxNodes?: unknown, maxDepth?: unknown, textBudget?: unknown }} T
159
- * @param {T} args
160
- * @returns {T}
161
- */
162
- function applyTreeBudgetPreset(args) {
163
- const presetName = getBudgetPresetName(args.budgetPreset);
164
- if (!presetName) {
165
- return args;
166
- }
167
- const preset = getBudgetPreset(presetName);
168
- return /** @type {T} */ ({
169
- ...args,
170
- maxNodes: args.maxNodes ?? preset.maxNodes,
171
- maxDepth: args.maxDepth ?? preset.maxDepth,
172
- textBudget: args.textBudget ?? preset.textBudget,
173
- });
174
- }
175
-
176
- /**
177
- * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
178
- * @param {T} args
179
- * @returns {T}
180
- */
181
- function applyTextBudgetPreset(args) {
182
- const presetName = getBudgetPresetName(args.budgetPreset);
183
- if (!presetName) {
184
- return args;
185
- }
186
- const preset = getBudgetPreset(presetName);
187
- return /** @type {T} */ ({
188
- ...args,
189
- textBudget: args.textBudget ?? preset.textBudget,
190
- });
191
- }
192
-
193
- /**
194
- * @template {{ budgetPreset?: unknown, limit?: unknown }} T
195
- * @param {T} args
196
- * @param {{ quick: number, normal: number, deep: number }} defaults
197
- * @returns {T}
198
- */
199
- function applyLimitBudgetPreset(args, defaults) {
200
- const presetName = getBudgetPresetName(args.budgetPreset);
201
- if (!presetName) {
202
- return args;
203
- }
204
- return /** @type {T} */ ({
205
- ...args,
206
- limit: args.limit ?? defaults[presetName],
207
- });
208
- }
209
-
210
- /**
211
- * @template {{ budgetPreset?: unknown, maxLength?: unknown }} T
212
- * @param {T} args
213
- * @returns {T}
214
- */
215
- function applyHtmlBudgetPreset(args) {
216
- const presetName = getBudgetPresetName(args.budgetPreset);
217
- if (!presetName) {
218
- return args;
219
- }
220
- const maxLengthByPreset = {
221
- quick: 600,
222
- normal: DEFAULT_MAX_HTML_LENGTH,
223
- deep: 6000,
224
- };
225
- return /** @type {T} */ ({
226
- ...args,
227
- maxLength: args.maxLength ?? maxLengthByPreset[presetName],
228
- });
229
- }
230
-
231
- /**
232
- * Call requestBridge and, on a retriable error, wait and retry once.
233
- * Logs retries to stderr so they are visible in MCP server output.
234
- *
235
- * @param {import('../../agent-client/src/client.js').BridgeClient} client
236
- * @param {BridgeMethod} method
237
- * @param {Record<string, unknown>} params
238
- * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null }} options
239
- * @returns {Promise<BridgeResponse>}
240
- */
241
- async function requestBridgeWithRetry(client, method, params, options) {
242
- const response = await requestBridge(client, method, params, options);
243
- const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
244
- if (!response.ok && recovery?.retry) {
245
- const delay = recovery.retryAfterMs ?? 1000;
246
- process.stderr.write(
247
- `[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
248
- );
249
- await new Promise((r) => setTimeout(r, delay));
250
- return requestBridge(client, method, params, options);
251
- }
252
- return response;
253
- }
254
-
255
- /**
256
- * @param {BridgeMethod} method
257
- * @param {Record<string, unknown>} [params={}]
258
- * @param {{ tabId?: number | null, summaryMethod?: string, tokenBudget?: number | null }} [options]
259
- * @returns {Promise<ToolResult>}
260
- */
261
- async function callBridgeTool(method, params = {}, options = {}) {
262
- return withToolClient(async (client) => {
263
- const response = await requestBridgeWithRetry(client, method, params, {
264
- tabId: options.tabId ?? null,
265
- source: REQUEST_SOURCE,
266
- tokenBudget: options.tokenBudget ?? null,
267
- });
268
- return summarizeToolResponse(response, options.summaryMethod || method);
269
- });
270
- }
271
-
272
- /**
273
- * @typedef {{ ref: boolean, method: BridgeMethod, params: (args: Record<string, unknown>, ref?: string) => Record<string, unknown> }} ToolAction
274
- */
275
-
276
- /**
277
- * Generic dispatcher for table-driven tool handlers. Each action entry
278
- * declares a bridge method, whether it needs an element ref, and a function
279
- * mapping args (and optional ref) to bridge params.
280
- *
281
- * @param {Record<string, ToolAction>} actions
282
- * @param {Record<string, unknown> & { action: string }} args
283
- * @param {string} toolName
284
- * @returns {Promise<ToolResult>}
285
- */
286
- async function dispatchToolAction(actions, args, toolName) {
287
- const entry = actions[args.action];
288
- if (!entry) return summarizeToolError(`Unsupported ${toolName} action "${args.action}".`);
289
- return withToolClient(async (client) => {
290
- const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
291
- const ref = entry.ref
292
- ? await resolveToolRef(
293
- client,
294
- /** @type {{ elementRef?: string, selector?: string }} */ (args),
295
- requestedTabId
296
- )
297
- : undefined;
298
- const response = await requestBridgeWithRetry(client, entry.method, entry.params(args, ref), {
299
- tabId: requestedTabId,
300
- source: REQUEST_SOURCE,
301
- tokenBudget: getToolTokenBudget(/** @type {{ budgetPreset?: unknown }} */ (args)),
302
- });
303
- return summarizeToolResponse(response, entry.method);
304
- });
305
- }
306
-
307
68
  /**
308
69
  * @returns {Promise<ToolResult>}
309
70
  */
@@ -324,596 +85,6 @@ export async function handleStatusTool() {
324
85
  }
325
86
 
326
87
  /**
327
- * @param {{ action: string, url?: string, active?: boolean, tabId?: number }} args
328
- * @returns {Promise<ToolResult>}
329
- */
330
- export async function handleTabsTool(args) {
331
- if (args.action === 'list') {
332
- return callBridgeTool('tabs.list');
333
- }
334
- if (args.action === 'create') {
335
- return callBridgeTool('tabs.create', {
336
- url: args.url,
337
- active: args.active,
338
- });
339
- }
340
- if (args.action === 'close') {
341
- if (typeof args.tabId !== 'number') {
342
- return summarizeToolError('tabId is required for tabs.close.');
343
- }
344
- return callBridgeTool('tabs.close', { tabId: args.tabId });
345
- }
346
- return summarizeToolError(`Unsupported tabs action "${args.action}".`);
347
- }
348
-
349
- /**
350
- /** @type {Record<string, ToolAction>} */
351
- export const DOM_ACTIONS = {
352
- query: {
353
- ref: false,
354
- method: 'dom.query',
355
- params: (a) => ({
356
- selector: a.selector || 'body',
357
- withinRef: a.withinRef,
358
- maxNodes: a.maxNodes,
359
- maxDepth: a.maxDepth,
360
- textBudget: a.textBudget,
361
- includeBbox: a.includeBbox,
362
- attributeAllowlist: a.attributeAllowlist,
363
- }),
364
- },
365
- describe: {
366
- ref: true,
367
- method: 'dom.describe',
368
- params: (_, r) => ({ elementRef: r }),
369
- },
370
- text: {
371
- ref: true,
372
- method: 'dom.get_text',
373
- params: (a, r) => ({ elementRef: r, textBudget: a.textBudget }),
374
- },
375
- attributes: {
376
- ref: true,
377
- method: 'dom.get_attributes',
378
- params: (a, r) => ({ elementRef: r, attributes: a.attributes || [] }),
379
- },
380
- wait: {
381
- ref: false,
382
- method: 'dom.wait_for',
383
- params: (a) => ({
384
- selector: a.selector,
385
- text: a.text,
386
- state: a.state,
387
- timeoutMs: a.timeoutMs,
388
- }),
389
- },
390
- find_text: {
391
- ref: false,
392
- method: 'dom.find_by_text',
393
- params: (a) => ({
394
- text: a.text,
395
- exact: a.exact,
396
- selector: a.selector,
397
- maxResults: a.maxResults,
398
- }),
399
- },
400
- find_role: {
401
- ref: false,
402
- method: 'dom.find_by_role',
403
- params: (a) => ({
404
- role: a.role,
405
- name: a.name,
406
- selector: a.selector,
407
- maxResults: a.maxResults,
408
- }),
409
- },
410
- html: {
411
- ref: true,
412
- method: 'dom.get_html',
413
- params: (a, r) => ({
414
- elementRef: r,
415
- outer: a.outer,
416
- maxLength: a.maxLength,
417
- }),
418
- },
419
- accessibility_tree: {
420
- ref: false,
421
- method: 'dom.get_accessibility_tree',
422
- params: (a) => ({ maxNodes: a.maxNodes, maxDepth: a.maxDepth }),
423
- },
424
- };
425
-
426
- /**
427
- * @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
428
- * @returns {Promise<ToolResult>}
429
- */
430
- export async function handleDomTool(args) {
431
- if (args.action === 'query' || args.action === 'accessibility_tree') {
432
- const inferred = inferBudgetFromSelector(args);
433
- const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
434
- return dispatchToolAction(DOM_ACTIONS, applyTreeBudgetPreset(withBudget), 'DOM');
435
- }
436
- if (args.action === 'text') {
437
- return dispatchToolAction(DOM_ACTIONS, applyTextBudgetPreset(args), 'DOM');
438
- }
439
- if (args.action === 'html') {
440
- return dispatchToolAction(DOM_ACTIONS, applyHtmlBudgetPreset(args), 'DOM');
441
- }
442
- return dispatchToolAction(DOM_ACTIONS, args, 'DOM');
443
- }
444
-
445
- /** @type {Record<string, ToolAction>} */
446
- export const STYLES_LAYOUT_ACTIONS = {
447
- computed: {
448
- ref: true,
449
- method: 'styles.get_computed',
450
- params: (a, r) => ({ elementRef: r, properties: a.properties }),
451
- },
452
- matched_rules: {
453
- ref: true,
454
- method: 'styles.get_matched_rules',
455
- params: (_, r) => ({ elementRef: r }),
456
- },
457
- box_model: {
458
- ref: true,
459
- method: 'layout.get_box_model',
460
- params: (_, r) => ({ elementRef: r }),
461
- },
462
- hit_test: {
463
- ref: false,
464
- method: 'layout.hit_test',
465
- params: (a) => ({ x: a.x, y: a.y }),
466
- },
467
- };
468
-
469
- /**
470
- * @param {{ action: string, elementRef?: string, selector?: string, properties?: string[], x?: number, y?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
471
- * @returns {Promise<ToolResult>}
472
- */
473
- export async function handleStylesLayoutTool(args) {
474
- return dispatchToolAction(STYLES_LAYOUT_ACTIONS, args, 'styles/layout');
475
- }
476
-
477
- /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
478
- export const PAGE_ACTIONS = {
479
- state: { method: 'page.get_state', params: () => ({}) },
480
- evaluate: {
481
- method: 'page.evaluate',
482
- params: (a) => ({
483
- expression: a.expression,
484
- awaitPromise: a.awaitPromise,
485
- timeoutMs: a.timeoutMs,
486
- returnByValue: a.returnByValue,
487
- }),
488
- },
489
- console: {
490
- method: 'page.get_console',
491
- params: (a) => ({ level: a.level, clear: a.clear, limit: a.limit }),
492
- },
493
- wait_for_load: {
494
- method: 'page.wait_for_load_state',
495
- params: (a) => ({ timeoutMs: a.timeoutMs }),
496
- },
497
- storage: {
498
- method: 'page.get_storage',
499
- params: (a) => ({ type: a.type, keys: a.keys }),
500
- },
501
- text: {
502
- method: 'page.get_text',
503
- params: (a) => ({ textBudget: a.textBudget }),
504
- },
505
- network: {
506
- method: 'page.get_network',
507
- params: (a) => ({
508
- clear: a.clear,
509
- limit: a.limit,
510
- urlPattern: a.urlPattern,
511
- }),
512
- },
513
- performance: { method: 'performance.get_metrics', params: () => ({}) },
514
- };
515
-
516
- /**
517
- * @param {{ action: string, expression?: string, awaitPromise?: boolean, timeoutMs?: number, returnByValue?: boolean, level?: string, clear?: boolean, limit?: number, type?: string, keys?: string[], textBudget?: number, urlPattern?: string, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
518
- * @returns {Promise<ToolResult>}
519
- */
520
- export async function handlePageTool(args) {
521
- let normalizedArgs = args;
522
- if (args.action === 'text') {
523
- normalizedArgs = applyTextBudgetPreset(args);
524
- } else if (args.action === 'console') {
525
- normalizedArgs = applyLimitBudgetPreset(args, {
526
- quick: 10,
527
- normal: DEFAULT_CONSOLE_LIMIT,
528
- deep: 100,
529
- });
530
- } else if (args.action === 'network') {
531
- normalizedArgs = applyLimitBudgetPreset(args, {
532
- quick: 10,
533
- normal: DEFAULT_NETWORK_LIMIT,
534
- deep: 100,
535
- });
536
- }
537
- const entry = PAGE_ACTIONS[normalizedArgs.action];
538
- if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
539
- return callBridgeTool(entry.method, entry.params(normalizedArgs), {
540
- tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
541
- tokenBudget: getToolTokenBudget(normalizedArgs),
542
- });
543
- }
544
-
545
- /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
546
- export const NAVIGATION_ACTIONS = {
547
- navigate: {
548
- method: 'navigation.navigate',
549
- params: (a) => ({
550
- url: a.url,
551
- waitForLoad: a.waitForLoad,
552
- timeoutMs: a.timeoutMs,
553
- }),
554
- },
555
- reload: {
556
- method: 'navigation.reload',
557
- params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
558
- },
559
- go_back: {
560
- method: 'navigation.go_back',
561
- params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
562
- },
563
- go_forward: {
564
- method: 'navigation.go_forward',
565
- params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
566
- },
567
- scroll: {
568
- method: 'viewport.scroll',
569
- params: (a) => ({
570
- top: a.top,
571
- left: a.left,
572
- behavior: a.behavior,
573
- relative: a.relative,
574
- }),
575
- },
576
- resize: {
577
- method: 'viewport.resize',
578
- params: (a) => ({ width: a.width, height: a.height, reset: a.reset }),
579
- },
580
- };
581
-
582
- /**
583
- * @param {{ action: string, url?: string, waitForLoad?: boolean, timeoutMs?: number, top?: number, left?: number, behavior?: string, relative?: boolean, width?: number, height?: number, reset?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
584
- * @returns {Promise<ToolResult>}
585
- */
586
- export async function handleNavigationTool(args) {
587
- const entry = NAVIGATION_ACTIONS[args.action];
588
- if (!entry) return summarizeToolError(`Unsupported navigation action "${args.action}".`);
589
- return callBridgeTool(entry.method, entry.params(args), {
590
- tabId: typeof args.tabId === 'number' ? args.tabId : null,
591
- tokenBudget: getToolTokenBudget(args),
592
- });
593
- }
594
-
595
- /** @type {Record<string, BridgeMethod>} */
596
- export const INPUT_ACTION_METHODS = {
597
- click: 'input.click',
598
- focus: 'input.focus',
599
- type: 'input.type',
600
- press_key: 'input.press_key',
601
- cdp_press_key: 'cdp.dispatch_key_event',
602
- set_checked: 'input.set_checked',
603
- select_option: 'input.select_option',
604
- hover: 'input.hover',
605
- drag: 'input.drag',
606
- scroll_into_view: 'input.scroll_into_view',
607
- };
608
-
609
- /**
610
- * @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
611
- * @returns {Promise<ToolResult>}
612
- */
613
- export async function handleInputTool(args) {
614
- return withToolClient(async (client) => {
615
- const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
616
- const elementTarget = async () => ({
617
- elementRef: await resolveToolRef(client, args, requestedTabId),
618
- });
619
-
620
- switch (args.action) {
621
- case 'click': {
622
- const response = await requestBridge(
623
- client,
624
- 'input.click',
625
- {
626
- target: await elementTarget(),
627
- button: args.button,
628
- clickCount: args.clickCount,
629
- },
630
- {
631
- tabId: requestedTabId,
632
- source: REQUEST_SOURCE,
633
- tokenBudget: getToolTokenBudget(args),
634
- }
635
- );
636
- return summarizeToolResponse(response, 'input.click');
637
- }
638
- case 'focus': {
639
- const response = await requestBridge(
640
- client,
641
- 'input.focus',
642
- {
643
- target: await elementTarget(),
644
- },
645
- {
646
- tabId: requestedTabId,
647
- source: REQUEST_SOURCE,
648
- tokenBudget: getToolTokenBudget(args),
649
- }
650
- );
651
- return summarizeToolResponse(response, 'input.focus');
652
- }
653
- case 'type': {
654
- const response = await requestBridge(
655
- client,
656
- 'input.type',
657
- {
658
- target: await elementTarget(),
659
- text: args.text,
660
- clear: args.clear,
661
- submit: args.submit,
662
- },
663
- {
664
- tabId: requestedTabId,
665
- source: REQUEST_SOURCE,
666
- tokenBudget: getToolTokenBudget(args),
667
- }
668
- );
669
- return summarizeToolResponse(response, 'input.type');
670
- }
671
- case 'press_key': {
672
- const target = args.elementRef || args.selector ? await elementTarget() : undefined;
673
- const response = await requestBridge(
674
- client,
675
- 'input.press_key',
676
- {
677
- target,
678
- key: args.key,
679
- modifiers: args.modifiers,
680
- },
681
- {
682
- tabId: requestedTabId,
683
- source: REQUEST_SOURCE,
684
- tokenBudget: getToolTokenBudget(args),
685
- }
686
- );
687
- return summarizeToolResponse(response, 'input.press_key');
688
- }
689
- case 'cdp_press_key': {
690
- const response = await requestBridge(
691
- client,
692
- 'cdp.dispatch_key_event',
693
- {
694
- key: args.key,
695
- code: args.code,
696
- modifiers: args.modifiers,
697
- },
698
- {
699
- tabId: requestedTabId,
700
- source: REQUEST_SOURCE,
701
- tokenBudget: getToolTokenBudget(args),
702
- }
703
- );
704
- return summarizeToolResponse(response, 'cdp.dispatch_key_event');
705
- }
706
- case 'set_checked': {
707
- const response = await requestBridge(
708
- client,
709
- 'input.set_checked',
710
- {
711
- target: await elementTarget(),
712
- checked: args.checked,
713
- },
714
- {
715
- tabId: requestedTabId,
716
- source: REQUEST_SOURCE,
717
- tokenBudget: getToolTokenBudget(args),
718
- }
719
- );
720
- return summarizeToolResponse(response, 'input.set_checked');
721
- }
722
- case 'select_option': {
723
- const response = await requestBridge(
724
- client,
725
- 'input.select_option',
726
- {
727
- target: await elementTarget(),
728
- values: args.values,
729
- labels: args.labels,
730
- indexes: args.indexes,
731
- },
732
- {
733
- tabId: requestedTabId,
734
- source: REQUEST_SOURCE,
735
- tokenBudget: getToolTokenBudget(args),
736
- }
737
- );
738
- return summarizeToolResponse(response, 'input.select_option');
739
- }
740
- case 'hover': {
741
- const response = await requestBridge(
742
- client,
743
- 'input.hover',
744
- {
745
- target: await elementTarget(),
746
- duration: args.duration,
747
- },
748
- {
749
- tabId: requestedTabId,
750
- source: REQUEST_SOURCE,
751
- tokenBudget: getToolTokenBudget(args),
752
- }
753
- );
754
- return summarizeToolResponse(response, 'input.hover');
755
- }
756
- case 'drag': {
757
- const source = {
758
- elementRef:
759
- args.sourceElementRef ||
760
- (args.sourceSelector
761
- ? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
762
- : ''),
763
- };
764
- const destination = {
765
- elementRef:
766
- args.destinationElementRef ||
767
- (args.destinationSelector
768
- ? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
769
- : ''),
770
- };
771
- if (!source.elementRef || !destination.elementRef) {
772
- return summarizeToolError(
773
- 'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
774
- );
775
- }
776
- const response = await requestBridge(
777
- client,
778
- 'input.drag',
779
- {
780
- source,
781
- destination,
782
- offsetX: args.offsetX,
783
- offsetY: args.offsetY,
784
- },
785
- {
786
- tabId: requestedTabId,
787
- source: REQUEST_SOURCE,
788
- tokenBudget: getToolTokenBudget(args),
789
- }
790
- );
791
- return summarizeToolResponse(response, 'input.drag');
792
- }
793
- case 'scroll_into_view': {
794
- const response = await requestBridge(
795
- client,
796
- 'input.scroll_into_view',
797
- {
798
- target: await elementTarget(),
799
- },
800
- {
801
- tabId: requestedTabId,
802
- source: REQUEST_SOURCE,
803
- tokenBudget: getToolTokenBudget(args),
804
- }
805
- );
806
- return summarizeToolResponse(response, 'input.scroll_into_view');
807
- }
808
- default:
809
- return summarizeToolError(`Unsupported input action "${args.action}".`);
810
- }
811
- });
812
- }
813
-
814
- /** @type {Record<string, ToolAction>} */
815
- export const PATCH_ACTIONS = {
816
- apply_styles: {
817
- ref: true,
818
- method: 'patch.apply_styles',
819
- params: (a, r) => ({
820
- target: { elementRef: r },
821
- declarations: a.declarations,
822
- important: a.important,
823
- verify: a.verify,
824
- }),
825
- },
826
- apply_dom: {
827
- ref: true,
828
- method: 'patch.apply_dom',
829
- params: (a, r) => {
830
- const operation = typeof a.operation === 'string' ? a.operation : '';
831
- /** @type {Record<string, string>} */
832
- const opMap = {
833
- setAttribute: 'set_attribute',
834
- removeAttribute: 'remove_attribute',
835
- addClass: 'toggle_class',
836
- removeClass: 'toggle_class',
837
- setTextContent: 'set_text',
838
- setProperty: 'set_attribute',
839
- };
840
- return {
841
- target: { elementRef: r },
842
- operation: opMap[operation] || operation,
843
- value: a.value,
844
- name: a.name,
845
- verify: a.verify,
846
- };
847
- },
848
- },
849
- list: { ref: false, method: 'patch.list', params: () => ({}) },
850
- rollback: {
851
- ref: false,
852
- method: 'patch.rollback',
853
- params: (a) => ({ patchId: a.patchId }),
854
- },
855
- commit_baseline: {
856
- ref: false,
857
- method: 'patch.commit_session_baseline',
858
- params: () => ({}),
859
- },
860
- };
861
-
862
- /**
863
- * @param {{ action: string, elementRef?: string, selector?: string, declarations?: Record<string, string>, important?: boolean, operation?: string, value?: unknown, name?: string, patchId?: string, verify?: boolean, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
864
- * @returns {Promise<ToolResult>}
865
- */
866
- export async function handlePatchTool(args) {
867
- return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
868
- }
869
-
870
- /** @type {Record<string, ToolAction>} */
871
- export const CAPTURE_ACTIONS = {
872
- element: {
873
- ref: true,
874
- method: 'screenshot.capture_element',
875
- params: (_, r) => ({ elementRef: r }),
876
- },
877
- region: {
878
- ref: false,
879
- method: 'screenshot.capture_region',
880
- params: (a) => /** @type {Record<string, unknown>} */ (a.rect || {}),
881
- },
882
- full_page: {
883
- ref: false,
884
- method: 'screenshot.capture_full_page',
885
- params: () => ({}),
886
- },
887
- cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
888
- cdp_dom_snapshot: {
889
- ref: false,
890
- method: 'cdp.get_dom_snapshot',
891
- params: () => ({}),
892
- },
893
- cdp_box_model: {
894
- ref: true,
895
- method: 'cdp.get_box_model',
896
- params: (_, r) => ({ elementRef: r }),
897
- },
898
- cdp_computed_styles: {
899
- ref: true,
900
- method: 'cdp.get_computed_styles_for_node',
901
- params: (_, r) => ({ elementRef: r }),
902
- },
903
- };
904
-
905
- /**
906
- * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
907
- * @returns {Promise<ToolResult>}
908
- */
909
- export async function handleCaptureTool(args) {
910
- return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
911
- }
912
-
913
- /**
914
- * Returns the live runtime context: budget presets, method groups, and active limits.
915
- * Equivalent to `bbx skill`. Use this first to discover safe defaults before inspecting.
916
- *
917
88
  * @returns {Promise<ToolResult>}
918
89
  */
919
90
  export async function handleSkillTool() {
@@ -929,8 +100,6 @@ export async function handleSkillTool() {
929
100
  }
930
101
 
931
102
  /**
932
- * Check MCP config and CLI skill installation status.
933
- *
934
103
  * @param {{ global?: boolean }} args
935
104
  * @returns {Promise<ToolResult>}
936
105
  */
@@ -948,8 +117,6 @@ export async function handleSetupTool(args) {
948
117
  }
949
118
 
950
119
  /**
951
- * Tail recent bridge logs for debugging.
952
- *
953
120
  * @param {{ limit?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
954
121
  * @returns {Promise<ToolResult>}
955
122
  */
@@ -971,8 +138,6 @@ export async function handleLogTool(args) {
971
138
  }
972
139
 
973
140
  /**
974
- * Ping the bridge to check connectivity.
975
- *
976
141
  * @returns {Promise<ToolResult>}
977
142
  */
978
143
  export async function handleHealthTool() {
@@ -986,290 +151,8 @@ export async function handleHealthTool() {
986
151
  }
987
152
 
988
153
  /**
989
- * @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
990
- * @returns {Promise<ToolResult>}
991
- */
992
- export async function handleBatchTool(args) {
993
- if (!Array.isArray(args.calls) || args.calls.length === 0) {
994
- return summarizeToolError('calls must be a non-empty array.');
995
- }
996
-
997
- const calls = args.calls;
998
- return withToolClient(async (client) => {
999
- const results = await Promise.all(
1000
- calls.map(async (call) => {
1001
- if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
1002
- return {
1003
- method: '',
1004
- tabId: null,
1005
- ok: false,
1006
- summary: 'INVALID_REQUEST: Each batch call needs a method.',
1007
- evidence: null,
1008
- error: {
1009
- code: 'INVALID_REQUEST',
1010
- message: 'Each batch call needs a method.',
1011
- },
1012
- response: null,
1013
- };
1014
- }
1015
-
1016
- if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
1017
- return {
1018
- method: call.method,
1019
- tabId: null,
1020
- ok: false,
1021
- summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
1022
- evidence: null,
1023
- error: {
1024
- code: 'INVALID_REQUEST',
1025
- message: `Unknown bridge method "${call.method}".`,
1026
- },
1027
- response: null,
1028
- };
1029
- }
1030
-
1031
- const method = /** @type {BridgeMethod} */ (call.method);
1032
- const tabId = bridgeMethodNeedsTab(method)
1033
- ? typeof call.tabId === 'number'
1034
- ? call.tabId
1035
- : null
1036
- : null;
1037
- const tokenBudget = getToolTokenBudget(call);
1038
-
1039
- const startTime = Date.now();
1040
- try {
1041
- const response = await client.request({
1042
- method,
1043
- params: call.params || {},
1044
- tabId,
1045
- meta: {
1046
- source: REQUEST_SOURCE,
1047
- ...(tokenBudget != null ? { token_budget: tokenBudget } : {}),
1048
- },
1049
- });
1050
- return summarizeBatchResponseItem({
1051
- method,
1052
- tabId,
1053
- response,
1054
- durationMs: Date.now() - startTime,
1055
- });
1056
- } catch (error) {
1057
- return summarizeBatchErrorItem({
1058
- method,
1059
- tabId,
1060
- error,
1061
- durationMs: Date.now() - startTime,
1062
- });
1063
- }
1064
- })
1065
- );
1066
-
1067
- const failureCount = results.filter((result) => !result.ok).length;
1068
- const summary =
1069
- failureCount === 0
1070
- ? `Batch executed ${results.length} call(s).`
1071
- : `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
1072
- return createToolResult(
1073
- summary,
1074
- {
1075
- ok: failureCount === 0,
1076
- results,
1077
- },
1078
- failureCount > 0
1079
- );
1080
- });
1081
- }
1082
-
1083
- /**
1084
- * @param {{ method: string, params?: Record<string, unknown>, tabId?: number }} args
1085
- * @returns {Promise<ToolResult>}
1086
- */
1087
- export async function handleRawCallTool(args) {
1088
- if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
1089
- return summarizeToolError(`Unknown bridge method "${args.method}".`);
1090
- }
1091
-
1092
- return withToolClient(async (client) => {
1093
- const response = await requestBridge(
1094
- client,
1095
- /** @type {BridgeMethod} */ (args.method),
1096
- args.params || {},
1097
- {
1098
- tabId: typeof args.tabId === 'number' ? args.tabId : null,
1099
- source: REQUEST_SOURCE,
1100
- }
1101
- );
1102
-
1103
- if (!response.ok) {
1104
- return createToolResult(
1105
- response.error.message,
1106
- {
1107
- ok: false,
1108
- error: response.error,
1109
- response,
1110
- },
1111
- true
1112
- );
1113
- }
1114
-
1115
- return createToolResult(`Called ${args.method}.`, {
1116
- ok: true,
1117
- response: response.result,
1118
- });
1119
- });
1120
- }
1121
-
1122
- /**
1123
- * Explicitly request Browser Bridge access for the current browser window.
1124
- * Surfaces an Enable cue in the extension popup or side panel.
1125
- *
1126
154
  * @returns {Promise<ToolResult>}
1127
155
  */
1128
156
  export async function handleAccessTool() {
1129
157
  return callBridgeTool('access.request');
1130
158
  }
1131
-
1132
- // ---------------------------------------------------------------------------
1133
- // browser_investigate — heuristic fallback for subagent-delegatable inspection
1134
- // ---------------------------------------------------------------------------
1135
-
1136
- /**
1137
- * @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
1138
- */
1139
-
1140
- /** @type {Record<string, { label: string, steps: InvestigateStep[] }>} */
1141
- const INVESTIGATE_SCOPES = {
1142
- quick: {
1143
- label: 'quick',
1144
- steps: [
1145
- { method: 'page.get_state', params: () => ({}) },
1146
- {
1147
- method: 'dom.query',
1148
- params: (a) => ({
1149
- selector: a.selector || 'body',
1150
- maxNodes: 10,
1151
- maxDepth: 2,
1152
- textBudget: 300,
1153
- }),
1154
- },
1155
- ],
1156
- },
1157
- normal: {
1158
- label: 'normal',
1159
- steps: [
1160
- { method: 'page.get_state', params: () => ({}) },
1161
- {
1162
- method: 'dom.query',
1163
- params: (a) => ({
1164
- selector: a.selector || 'body',
1165
- maxNodes: 25,
1166
- maxDepth: 4,
1167
- textBudget: 600,
1168
- }),
1169
- },
1170
- { method: 'page.get_text', params: () => ({ textBudget: 4000 }) },
1171
- ],
1172
- },
1173
- deep: {
1174
- label: 'deep',
1175
- steps: [
1176
- { method: 'page.get_state', params: () => ({}) },
1177
- {
1178
- method: 'dom.query',
1179
- params: (a) => ({
1180
- selector: a.selector || 'body',
1181
- maxNodes: 50,
1182
- maxDepth: 6,
1183
- textBudget: 1000,
1184
- }),
1185
- },
1186
- { method: 'page.get_text', params: () => ({ textBudget: 8000 }) },
1187
- {
1188
- method: 'page.get_console',
1189
- params: () => ({ level: 'warn', limit: 20 }),
1190
- },
1191
- { method: 'page.get_network', params: () => ({ limit: 20 }) },
1192
- ],
1193
- },
1194
- };
1195
-
1196
- /**
1197
- * Investigate a page to answer a question or verify a condition.
1198
- * Runs a deterministic heuristic inspection sequence and returns a
1199
- * structured summary. Designed so smart orchestrators can delegate
1200
- * this to a cheaper subagent instead.
1201
- *
1202
- * @param {{ objective: string, scope?: 'quick' | 'normal' | 'deep', tabId?: number, selector?: string }} args
1203
- * @returns {Promise<ToolResult>}
1204
- */
1205
- export async function handleInvestigateTool(args) {
1206
- const objective = typeof args.objective === 'string' ? args.objective.trim() : '';
1207
- if (!objective) {
1208
- return summarizeToolError('objective is required for browser_investigate.');
1209
- }
1210
-
1211
- const scopeName = args.scope || 'normal';
1212
- const scope = INVESTIGATE_SCOPES[scopeName];
1213
- if (!scope) {
1214
- return summarizeToolError(`Unsupported investigation scope "${scopeName}".`);
1215
- }
1216
-
1217
- return withToolClient(async (client) => {
1218
- const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
1219
-
1220
- /** @type {Array<{ method: string, ok: boolean, summary: string, evidence: unknown, durationMs: number }>} */
1221
- const stepResults = [];
1222
-
1223
- for (const step of scope.steps) {
1224
- const startTime = Date.now();
1225
- try {
1226
- const response = await requestBridgeWithRetry(client, step.method, step.params(args), {
1227
- tabId: requestedTabId,
1228
- source: REQUEST_SOURCE,
1229
- tokenBudget: null,
1230
- });
1231
- const bridgeSummary = annotateBridgeSummary(
1232
- summarizeBridgeResponse(response, step.method),
1233
- response
1234
- );
1235
- stepResults.push({
1236
- method: step.method,
1237
- ok: bridgeSummary.ok,
1238
- summary: bridgeSummary.summary,
1239
- evidence: bridgeSummary.evidence,
1240
- durationMs: Date.now() - startTime,
1241
- });
1242
- } catch (error) {
1243
- const message = error instanceof Error ? error.message : String(error);
1244
- stepResults.push({
1245
- method: step.method,
1246
- ok: false,
1247
- summary: `ERROR: ${message}`,
1248
- evidence: null,
1249
- durationMs: Date.now() - startTime,
1250
- });
1251
- }
1252
- }
1253
-
1254
- const failedSteps = stepResults.filter((s) => !s.ok);
1255
- const allOk = failedSteps.length === 0;
1256
- const totalDuration = stepResults.reduce((sum, s) => sum + s.durationMs, 0);
1257
-
1258
- const summaryText = allOk
1259
- ? `Investigation complete (${scope.label}, ${stepResults.length} steps, ${totalDuration}ms). Objective: ${objective}`
1260
- : `Investigation partial (${scope.label}, ${stepResults.length} steps, ${failedSteps.length} failed, ${totalDuration}ms). Objective: ${objective}`;
1261
-
1262
- return createToolResult(
1263
- summaryText,
1264
- {
1265
- ok: allOk,
1266
- objective,
1267
- scope: scopeName,
1268
- heuristicFallback: true,
1269
- steps: stepResults,
1270
- failedSteps,
1271
- },
1272
- !allOk
1273
- );
1274
- });
1275
- }