@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
@@ -0,0 +1,365 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ DEFAULT_CONSOLE_LIMIT,
5
+ DEFAULT_NETWORK_LIMIT,
6
+ METHOD_SET,
7
+ } from '../../protocol/src/index.js';
8
+ import {
9
+ annotateBridgeSummary,
10
+ applyLimitBudgetPreset,
11
+ applyTextBudgetPreset,
12
+ bridgeMethodNeedsTab,
13
+ callBridgeTool,
14
+ createToolResult,
15
+ getToolTokenBudget,
16
+ requestBridge,
17
+ requestBridgeWithRetry,
18
+ summarizeBatchErrorItem,
19
+ summarizeBatchResponseItem,
20
+ summarizeBridgeResponse,
21
+ summarizeToolError,
22
+ withToolClient,
23
+ REQUEST_SOURCE,
24
+ } from './handlers-utils.js';
25
+
26
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
27
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
28
+
29
+ /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
30
+ export const PAGE_ACTIONS = {
31
+ state: { method: 'page.get_state', params: () => ({}) },
32
+ evaluate: {
33
+ method: 'page.evaluate',
34
+ params: (a) => ({
35
+ expression: a.expression,
36
+ awaitPromise: a.awaitPromise,
37
+ timeoutMs: a.timeoutMs,
38
+ returnByValue: a.returnByValue,
39
+ }),
40
+ },
41
+ console: {
42
+ method: 'page.get_console',
43
+ params: (a) => ({ level: a.level, clear: a.clear, limit: a.limit }),
44
+ },
45
+ wait_for_load: {
46
+ method: 'page.wait_for_load_state',
47
+ params: (a) => ({ timeoutMs: a.timeoutMs }),
48
+ },
49
+ storage: {
50
+ method: 'page.get_storage',
51
+ params: (a) => ({ type: a.type, keys: a.keys }),
52
+ },
53
+ text: {
54
+ method: 'page.get_text',
55
+ params: (a) => ({ textBudget: a.textBudget }),
56
+ },
57
+ network: {
58
+ method: 'page.get_network',
59
+ params: (a) => ({
60
+ clear: a.clear,
61
+ limit: a.limit,
62
+ urlPattern: a.urlPattern,
63
+ }),
64
+ },
65
+ performance: { method: 'performance.get_metrics', params: () => ({}) },
66
+ };
67
+
68
+ /**
69
+ * @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
70
+ * @returns {Promise<ToolResult>}
71
+ */
72
+ export async function handlePageTool(args) {
73
+ let normalizedArgs = args;
74
+ if (args.action === 'text') {
75
+ normalizedArgs = applyTextBudgetPreset(args);
76
+ } else if (args.action === 'console') {
77
+ normalizedArgs = applyLimitBudgetPreset(args, {
78
+ quick: 10,
79
+ normal: DEFAULT_CONSOLE_LIMIT,
80
+ deep: 100,
81
+ });
82
+ } else if (args.action === 'network') {
83
+ normalizedArgs = applyLimitBudgetPreset(args, {
84
+ quick: 10,
85
+ normal: DEFAULT_NETWORK_LIMIT,
86
+ deep: 100,
87
+ });
88
+ }
89
+ const entry = PAGE_ACTIONS[normalizedArgs.action];
90
+ if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
91
+ return callBridgeTool(entry.method, entry.params(normalizedArgs), {
92
+ tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
93
+ tokenBudget: getToolTokenBudget(normalizedArgs),
94
+ });
95
+ }
96
+
97
+ /**
98
+ * @param {{ calls?: Array<{ method?: string, params?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }> }} args
99
+ * @returns {Promise<ToolResult>}
100
+ */
101
+ export async function handleBatchTool(args) {
102
+ if (!Array.isArray(args.calls) || args.calls.length === 0) {
103
+ return summarizeToolError('calls must be a non-empty array.');
104
+ }
105
+
106
+ const calls = args.calls;
107
+ return withToolClient(async (client) => {
108
+ const results = await Promise.all(
109
+ calls.map(async (call) => {
110
+ if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
111
+ return {
112
+ method: '',
113
+ tabId: null,
114
+ ok: false,
115
+ summary: 'INVALID_REQUEST: Each batch call needs a method.',
116
+ evidence: null,
117
+ error: {
118
+ code: 'INVALID_REQUEST',
119
+ message: 'Each batch call needs a method.',
120
+ },
121
+ response: null,
122
+ };
123
+ }
124
+
125
+ if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
126
+ return {
127
+ method: call.method,
128
+ tabId: null,
129
+ ok: false,
130
+ summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
131
+ evidence: null,
132
+ error: {
133
+ code: 'INVALID_REQUEST',
134
+ message: `Unknown bridge method "${call.method}".`,
135
+ },
136
+ response: null,
137
+ };
138
+ }
139
+
140
+ const method = /** @type {BridgeMethod} */ (call.method);
141
+ const tabId = bridgeMethodNeedsTab(method)
142
+ ? typeof call.tabId === 'number'
143
+ ? call.tabId
144
+ : null
145
+ : null;
146
+ const tokenBudget = getToolTokenBudget(call);
147
+
148
+ const startTime = Date.now();
149
+ try {
150
+ const response = await client.request({
151
+ method,
152
+ params: call.params || {},
153
+ tabId,
154
+ meta: {
155
+ source: REQUEST_SOURCE,
156
+ ...(tokenBudget != null ? { token_budget: tokenBudget } : {}),
157
+ },
158
+ });
159
+ return summarizeBatchResponseItem({
160
+ method,
161
+ tabId,
162
+ response,
163
+ durationMs: Date.now() - startTime,
164
+ });
165
+ } catch (error) {
166
+ return summarizeBatchErrorItem({
167
+ method,
168
+ tabId,
169
+ error,
170
+ durationMs: Date.now() - startTime,
171
+ });
172
+ }
173
+ })
174
+ );
175
+
176
+ const failureCount = results.filter((result) => !result.ok).length;
177
+ const summary =
178
+ failureCount === 0
179
+ ? `Batch executed ${results.length} call(s).`
180
+ : `Batch executed ${results.length} call(s) with ${failureCount} error(s).`;
181
+ return createToolResult(
182
+ summary,
183
+ {
184
+ ok: failureCount === 0,
185
+ results,
186
+ },
187
+ failureCount > 0
188
+ );
189
+ });
190
+ }
191
+
192
+ /**
193
+ * @param {{ method: string, params?: Record<string, unknown>, tabId?: number }} args
194
+ * @returns {Promise<ToolResult>}
195
+ */
196
+ export async function handleRawCallTool(args) {
197
+ if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
198
+ return summarizeToolError(`Unknown bridge method "${args.method}".`);
199
+ }
200
+
201
+ return withToolClient(async (client) => {
202
+ const response = await requestBridge(
203
+ client,
204
+ /** @type {BridgeMethod} */ (args.method),
205
+ args.params || {},
206
+ {
207
+ tabId: typeof args.tabId === 'number' ? args.tabId : null,
208
+ source: REQUEST_SOURCE,
209
+ }
210
+ );
211
+
212
+ if (!response.ok) {
213
+ return createToolResult(
214
+ response.error.message,
215
+ {
216
+ ok: false,
217
+ error: response.error,
218
+ response,
219
+ },
220
+ true
221
+ );
222
+ }
223
+
224
+ return createToolResult(`Called ${args.method}.`, {
225
+ ok: true,
226
+ response: response.result,
227
+ });
228
+ });
229
+ }
230
+
231
+ /**
232
+ * @typedef {{ method: BridgeMethod, params: (args: Record<string, unknown>) => Record<string, unknown> }} InvestigateStep
233
+ */
234
+
235
+ /** @type {Record<string, { label: string, steps: InvestigateStep[] }>} */
236
+ const INVESTIGATE_SCOPES = {
237
+ quick: {
238
+ label: 'quick',
239
+ steps: [
240
+ { method: 'page.get_state', params: () => ({}) },
241
+ {
242
+ method: 'dom.query',
243
+ params: (a) => ({
244
+ selector: a.selector || 'body',
245
+ maxNodes: 10,
246
+ maxDepth: 2,
247
+ textBudget: 300,
248
+ }),
249
+ },
250
+ ],
251
+ },
252
+ normal: {
253
+ label: 'normal',
254
+ steps: [
255
+ { method: 'page.get_state', params: () => ({}) },
256
+ {
257
+ method: 'dom.query',
258
+ params: (a) => ({
259
+ selector: a.selector || 'body',
260
+ maxNodes: 25,
261
+ maxDepth: 4,
262
+ textBudget: 600,
263
+ }),
264
+ },
265
+ { method: 'page.get_text', params: () => ({ textBudget: 4000 }) },
266
+ ],
267
+ },
268
+ deep: {
269
+ label: 'deep',
270
+ steps: [
271
+ { method: 'page.get_state', params: () => ({}) },
272
+ {
273
+ method: 'dom.query',
274
+ params: (a) => ({
275
+ selector: a.selector || 'body',
276
+ maxNodes: 50,
277
+ maxDepth: 6,
278
+ textBudget: 1000,
279
+ }),
280
+ },
281
+ { method: 'page.get_text', params: () => ({ textBudget: 8000 }) },
282
+ {
283
+ method: 'page.get_console',
284
+ params: () => ({ level: 'warn', limit: 20 }),
285
+ },
286
+ { method: 'page.get_network', params: () => ({ limit: 20 }) },
287
+ ],
288
+ },
289
+ };
290
+
291
+ /**
292
+ * @param {{ objective: string, scope?: 'quick' | 'normal' | 'deep', tabId?: number, selector?: string }} args
293
+ * @returns {Promise<ToolResult>}
294
+ */
295
+ export async function handleInvestigateTool(args) {
296
+ const objective = typeof args.objective === 'string' ? args.objective.trim() : '';
297
+ if (!objective) {
298
+ return summarizeToolError('objective is required for browser_investigate.');
299
+ }
300
+
301
+ const scopeName = args.scope || 'normal';
302
+ const scope = INVESTIGATE_SCOPES[scopeName];
303
+ if (!scope) {
304
+ return summarizeToolError(`Unsupported investigation scope "${scopeName}".`);
305
+ }
306
+
307
+ return withToolClient(async (client) => {
308
+ const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
309
+
310
+ /** @type {Array<{ method: string, ok: boolean, summary: string, evidence: unknown, durationMs: number }>} */
311
+ const stepResults = [];
312
+
313
+ for (const step of scope.steps) {
314
+ const startTime = Date.now();
315
+ try {
316
+ const response = await requestBridgeWithRetry(client, step.method, step.params(args), {
317
+ tabId: requestedTabId,
318
+ source: REQUEST_SOURCE,
319
+ tokenBudget: null,
320
+ });
321
+ const bridgeSummary = annotateBridgeSummary(
322
+ summarizeBridgeResponse(response, step.method),
323
+ response
324
+ );
325
+ stepResults.push({
326
+ method: step.method,
327
+ ok: bridgeSummary.ok,
328
+ summary: bridgeSummary.summary,
329
+ evidence: bridgeSummary.evidence,
330
+ durationMs: Date.now() - startTime,
331
+ });
332
+ } catch (error) {
333
+ const message = error instanceof Error ? error.message : String(error);
334
+ stepResults.push({
335
+ method: step.method,
336
+ ok: false,
337
+ summary: `ERROR: ${message}`,
338
+ evidence: null,
339
+ durationMs: Date.now() - startTime,
340
+ });
341
+ }
342
+ }
343
+
344
+ const failedSteps = stepResults.filter((s) => !s.ok);
345
+ const allOk = failedSteps.length === 0;
346
+ const totalDuration = stepResults.reduce((sum, s) => sum + s.durationMs, 0);
347
+
348
+ const summaryText = allOk
349
+ ? `Investigation complete (${scope.label}, ${stepResults.length} steps, ${totalDuration}ms). Objective: ${objective}`
350
+ : `Investigation partial (${scope.label}, ${stepResults.length} steps, ${failedSteps.length} failed, ${totalDuration}ms). Objective: ${objective}`;
351
+
352
+ return createToolResult(
353
+ summaryText,
354
+ {
355
+ ok: allOk,
356
+ objective,
357
+ scope: scopeName,
358
+ heuristicFallback: true,
359
+ steps: stepResults,
360
+ failedSteps,
361
+ },
362
+ !allOk
363
+ );
364
+ });
365
+ }
@@ -0,0 +1,296 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ bridgeMethodNeedsTab,
5
+ DEFAULT_MAX_HTML_LENGTH,
6
+ estimateJsonPayloadCost,
7
+ getBudgetPreset,
8
+ getErrorRecovery,
9
+ isBudgetPresetName,
10
+ summarizeBatchErrorItem,
11
+ summarizeBatchResponseItem,
12
+ } from '../../protocol/src/index.js';
13
+ import {
14
+ getDoctorReport,
15
+ requestBridge,
16
+ resolveRef,
17
+ withBridgeClient,
18
+ } from '../../agent-client/src/runtime.js';
19
+ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-client/src/subagent.js';
20
+
21
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
22
+ /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
23
+
24
+ export const REQUEST_SOURCE = 'mcp';
25
+
26
+ /**
27
+ * @typedef {{
28
+ * content: Array<{ type: 'text', text: string }>,
29
+ * structuredContent: Record<string, unknown>,
30
+ * isError?: boolean
31
+ * }} ToolResult
32
+ */
33
+
34
+ /**
35
+ * @param {string} summary
36
+ * @param {Record<string, unknown>} [structuredContent={}]
37
+ * @param {boolean} [isError=false]
38
+ * @returns {ToolResult}
39
+ */
40
+ export function createToolResult(summary, structuredContent = {}, isError = false) {
41
+ const toolResult = {
42
+ content: [{ type: /** @type {'text'} */ ('text'), text: summary }],
43
+ structuredContent,
44
+ ...(isError ? { isError: true } : {}),
45
+ };
46
+ const delivered = estimateJsonPayloadCost(toolResult);
47
+ return {
48
+ ...toolResult,
49
+ structuredContent: {
50
+ ...structuredContent,
51
+ deliveredBytes: delivered.bytes,
52
+ deliveredTokens: delivered.approxTokens,
53
+ deliveredCostClass: delivered.costClass,
54
+ },
55
+ };
56
+ }
57
+
58
+ /**
59
+ * @param {BridgeResponse} response
60
+ * @param {string} [method]
61
+ * @returns {ToolResult}
62
+ */
63
+ export function summarizeToolResponse(response, method) {
64
+ const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
65
+ return createToolResult(summary.summary, summary, !summary.ok);
66
+ }
67
+
68
+ /**
69
+ * @param {unknown} error
70
+ * @returns {ToolResult}
71
+ */
72
+ export function summarizeToolError(error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ return createToolResult(
75
+ `ERROR: ${message}`,
76
+ {
77
+ ok: false,
78
+ evidence: null,
79
+ },
80
+ true
81
+ );
82
+ }
83
+
84
+ /**
85
+ * @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
86
+ * @returns {Promise<ToolResult>}
87
+ */
88
+ export async function withToolClient(callback) {
89
+ try {
90
+ return await withBridgeClient(callback);
91
+ } catch (error) {
92
+ return summarizeToolError(error);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {import('../../agent-client/src/client.js').BridgeClient} client
98
+ * @param {{ elementRef?: string | undefined, selector?: string | undefined }} input
99
+ * @param {number | null | undefined} [tabId]
100
+ * @returns {Promise<string>}
101
+ */
102
+ export async function resolveToolRef(client, input, tabId = null) {
103
+ if (typeof input.elementRef === 'string' && input.elementRef) {
104
+ return input.elementRef;
105
+ }
106
+ if (typeof input.selector === 'string' && input.selector) {
107
+ return resolveRef(client, input.selector, tabId, REQUEST_SOURCE);
108
+ }
109
+ throw new Error('Provide either elementRef or selector.');
110
+ }
111
+
112
+ /**
113
+ * @param {unknown} value
114
+ * @returns {'quick' | 'normal' | 'deep' | null}
115
+ */
116
+ export function getBudgetPresetName(value) {
117
+ return isBudgetPresetName(value) ? value : null;
118
+ }
119
+
120
+ /**
121
+ * @param {{ budgetPreset?: unknown, selector?: unknown, elementRef?: unknown }} args
122
+ * @returns {'quick' | 'normal' | 'deep' | null}
123
+ */
124
+ export function inferBudgetFromSelector(args) {
125
+ if (getBudgetPresetName(args.budgetPreset)) return null;
126
+ if (typeof args.elementRef === 'string' && args.elementRef) return 'quick';
127
+ const sel = typeof args.selector === 'string' ? args.selector.trim() : '';
128
+ if (!sel || sel === '*' || sel === 'body') return null;
129
+ if (/^#[\w-]+$/.test(sel)) return 'quick';
130
+ if ((sel.match(/\s/g) || []).length >= 3) return 'deep';
131
+ return 'normal';
132
+ }
133
+
134
+ /**
135
+ * @param {{ budgetPreset?: unknown }} args
136
+ * @returns {number | null}
137
+ */
138
+ export function getToolTokenBudget(args) {
139
+ const presetName = getBudgetPresetName(args.budgetPreset);
140
+ return presetName ? getBudgetPreset(presetName).tokenBudget : null;
141
+ }
142
+
143
+ /**
144
+ * @template {{ budgetPreset?: unknown, maxNodes?: unknown, maxDepth?: unknown, textBudget?: unknown }} T
145
+ * @param {T} args
146
+ * @returns {T}
147
+ */
148
+ export function applyTreeBudgetPreset(args) {
149
+ const presetName = getBudgetPresetName(args.budgetPreset);
150
+ if (!presetName) {
151
+ return args;
152
+ }
153
+ const preset = getBudgetPreset(presetName);
154
+ return /** @type {T} */ ({
155
+ ...args,
156
+ maxNodes: args.maxNodes ?? preset.maxNodes,
157
+ maxDepth: args.maxDepth ?? preset.maxDepth,
158
+ textBudget: args.textBudget ?? preset.textBudget,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
164
+ * @param {T} args
165
+ * @returns {T}
166
+ */
167
+ export function applyTextBudgetPreset(args) {
168
+ const presetName = getBudgetPresetName(args.budgetPreset);
169
+ if (!presetName) {
170
+ return args;
171
+ }
172
+ const preset = getBudgetPreset(presetName);
173
+ return /** @type {T} */ ({
174
+ ...args,
175
+ textBudget: args.textBudget ?? preset.textBudget,
176
+ });
177
+ }
178
+
179
+ /**
180
+ * @template {{ budgetPreset?: unknown, limit?: unknown }} T
181
+ * @param {T} args
182
+ * @param {{ quick: number, normal: number, deep: number }} defaults
183
+ * @returns {T}
184
+ */
185
+ export function applyLimitBudgetPreset(args, defaults) {
186
+ const presetName = getBudgetPresetName(args.budgetPreset);
187
+ if (!presetName) {
188
+ return args;
189
+ }
190
+ return /** @type {T} */ ({
191
+ ...args,
192
+ limit: args.limit ?? defaults[presetName],
193
+ });
194
+ }
195
+
196
+ /**
197
+ * @template {{ budgetPreset?: unknown, maxLength?: unknown }} T
198
+ * @param {T} args
199
+ * @returns {T}
200
+ */
201
+ export function applyHtmlBudgetPreset(args) {
202
+ const presetName = getBudgetPresetName(args.budgetPreset);
203
+ if (!presetName) {
204
+ return args;
205
+ }
206
+ const maxLengthByPreset = {
207
+ quick: 600,
208
+ normal: DEFAULT_MAX_HTML_LENGTH,
209
+ deep: 6000,
210
+ };
211
+ return /** @type {T} */ ({
212
+ ...args,
213
+ maxLength: args.maxLength ?? maxLengthByPreset[presetName],
214
+ });
215
+ }
216
+
217
+ /**
218
+ * @param {import('../../agent-client/src/client.js').BridgeClient} client
219
+ * @param {BridgeMethod} method
220
+ * @param {Record<string, unknown>} params
221
+ * @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null }} options
222
+ * @returns {Promise<BridgeResponse>}
223
+ */
224
+ export async function requestBridgeWithRetry(client, method, params, options) {
225
+ const response = await requestBridge(client, method, params, options);
226
+ const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
227
+ if (!response.ok && recovery?.retry) {
228
+ const delay = recovery.retryAfterMs ?? 1000;
229
+ process.stderr.write(
230
+ `[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
231
+ );
232
+ await new Promise((r) => setTimeout(r, delay));
233
+ return requestBridge(client, method, params, options);
234
+ }
235
+ return response;
236
+ }
237
+
238
+ /**
239
+ * @param {BridgeMethod} method
240
+ * @param {Record<string, unknown>} [params={}]
241
+ * @param {{ tabId?: number | null, summaryMethod?: string, tokenBudget?: number | null }} [options]
242
+ * @returns {Promise<ToolResult>}
243
+ */
244
+ export async function callBridgeTool(method, params = {}, options = {}) {
245
+ return withToolClient(async (client) => {
246
+ const response = await requestBridgeWithRetry(client, method, params, {
247
+ tabId: options.tabId ?? null,
248
+ source: REQUEST_SOURCE,
249
+ tokenBudget: options.tokenBudget ?? null,
250
+ });
251
+ return summarizeToolResponse(response, options.summaryMethod || method);
252
+ });
253
+ }
254
+
255
+ /**
256
+ * @typedef {{ ref: boolean, method: BridgeMethod, params: (args: Record<string, unknown>, ref?: string) => Record<string, unknown> }} ToolAction
257
+ */
258
+
259
+ /**
260
+ * @param {Record<string, ToolAction>} actions
261
+ * @param {Record<string, unknown> & { action: string }} args
262
+ * @param {string} toolName
263
+ * @returns {Promise<ToolResult>}
264
+ */
265
+ export async function dispatchToolAction(actions, args, toolName) {
266
+ const entry = actions[args.action];
267
+ if (!entry) return summarizeToolError(`Unsupported ${toolName} action "${args.action}".`);
268
+ return withToolClient(async (client) => {
269
+ const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
270
+ const ref = entry.ref
271
+ ? await resolveToolRef(
272
+ client,
273
+ /** @type {{ elementRef?: string, selector?: string }} */ (args),
274
+ requestedTabId
275
+ )
276
+ : undefined;
277
+ const response = await requestBridgeWithRetry(client, entry.method, entry.params(args, ref), {
278
+ tabId: requestedTabId,
279
+ source: REQUEST_SOURCE,
280
+ tokenBudget: getToolTokenBudget(/** @type {{ budgetPreset?: unknown }} */ (args)),
281
+ });
282
+ return summarizeToolResponse(response, entry.method);
283
+ });
284
+ }
285
+
286
+ export {
287
+ bridgeMethodNeedsTab,
288
+ getDoctorReport,
289
+ requestBridge,
290
+ resolveRef,
291
+ withBridgeClient,
292
+ annotateBridgeSummary,
293
+ summarizeBridgeResponse,
294
+ summarizeBatchErrorItem,
295
+ summarizeBatchResponseItem,
296
+ };