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