@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.
- package/README.md +6 -5
- package/package.json +1 -1
- package/packages/agent-client/src/cli.js +30 -20
- package/packages/agent-client/src/client.js +105 -42
- package/packages/agent-client/src/command-registry.js +4 -14
- package/packages/agent-client/src/detect.js +3 -3
- package/packages/agent-client/src/install.js +3 -7
- package/packages/agent-client/src/mcp-config.js +1 -3
- package/packages/agent-client/src/runtime.js +7 -41
- package/packages/agent-client/src/setup-status.js +3 -13
- package/packages/agent-client/src/types.ts +131 -0
- package/packages/mcp-server/src/handlers-capture.js +291 -0
- package/packages/mcp-server/src/handlers-dom.js +203 -0
- package/packages/mcp-server/src/handlers-navigation.js +79 -0
- package/packages/mcp-server/src/handlers-page.js +365 -0
- package/packages/mcp-server/src/handlers-utils.js +318 -0
- package/packages/mcp-server/src/handlers.js +59 -1176
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/bin/bridge-daemon.js +2 -1
- package/packages/native-host/bin/install-manifest.js +8 -0
- package/packages/native-host/bin/postinstall.js +46 -9
- package/packages/native-host/src/daemon-logger.js +157 -0
- package/packages/native-host/src/daemon-process.js +43 -18
- package/packages/native-host/src/daemon.js +133 -12
- package/packages/native-host/src/framing.js +13 -0
- package/packages/native-host/src/native-host.js +7 -5
- package/packages/protocol/src/capabilities.js +1 -0
- package/packages/protocol/src/protocol.js +40 -0
- package/packages/protocol/src/registry.js +5 -9
- package/packages/protocol/src/types.ts +572 -0
- package/packages/protocol/src/types.js +0 -626
|
@@ -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
|
+
applyPageTextBudgetPreset,
|
|
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 = applyPageTextBudgetPreset(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,318 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
bridgeMethodNeedsTab,
|
|
5
|
+
DEFAULT_MAX_HTML_LENGTH,
|
|
6
|
+
DEFAULT_PAGE_TEXT_BUDGET,
|
|
7
|
+
estimateJsonPayloadCost,
|
|
8
|
+
getBudgetPreset,
|
|
9
|
+
getErrorRecovery,
|
|
10
|
+
isBudgetPresetName,
|
|
11
|
+
summarizeBatchErrorItem,
|
|
12
|
+
summarizeBatchResponseItem,
|
|
13
|
+
} from '../../protocol/src/index.js';
|
|
14
|
+
import {
|
|
15
|
+
getDoctorReport,
|
|
16
|
+
requestBridge,
|
|
17
|
+
resolveRef,
|
|
18
|
+
withBridgeClient,
|
|
19
|
+
} from '../../agent-client/src/runtime.js';
|
|
20
|
+
import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-client/src/subagent.js';
|
|
21
|
+
|
|
22
|
+
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
23
|
+
/** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
|
|
24
|
+
|
|
25
|
+
export const REQUEST_SOURCE = 'mcp';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {{
|
|
29
|
+
* content: Array<{ type: 'text', text: string }>,
|
|
30
|
+
* structuredContent: Record<string, unknown>,
|
|
31
|
+
* isError?: boolean
|
|
32
|
+
* }} ToolResult
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} summary
|
|
37
|
+
* @param {Record<string, unknown>} [structuredContent={}]
|
|
38
|
+
* @param {boolean} [isError=false]
|
|
39
|
+
* @returns {ToolResult}
|
|
40
|
+
*/
|
|
41
|
+
export function createToolResult(summary, structuredContent = {}, isError = false) {
|
|
42
|
+
const toolResult = {
|
|
43
|
+
content: [{ type: /** @type {'text'} */ ('text'), text: summary }],
|
|
44
|
+
structuredContent,
|
|
45
|
+
...(isError ? { isError: true } : {}),
|
|
46
|
+
};
|
|
47
|
+
const delivered = estimateJsonPayloadCost(toolResult);
|
|
48
|
+
return {
|
|
49
|
+
...toolResult,
|
|
50
|
+
structuredContent: {
|
|
51
|
+
...structuredContent,
|
|
52
|
+
deliveredBytes: delivered.bytes,
|
|
53
|
+
deliveredTokens: delivered.approxTokens,
|
|
54
|
+
deliveredCostClass: delivered.costClass,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {BridgeResponse} response
|
|
61
|
+
* @param {string} [method]
|
|
62
|
+
* @returns {ToolResult}
|
|
63
|
+
*/
|
|
64
|
+
export function summarizeToolResponse(response, method) {
|
|
65
|
+
const summary = annotateBridgeSummary(summarizeBridgeResponse(response, method), response);
|
|
66
|
+
return createToolResult(summary.summary, summary, !summary.ok);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {unknown} error
|
|
71
|
+
* @returns {ToolResult}
|
|
72
|
+
*/
|
|
73
|
+
export function summarizeToolError(error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return createToolResult(
|
|
76
|
+
`ERROR: ${message}`,
|
|
77
|
+
{
|
|
78
|
+
ok: false,
|
|
79
|
+
evidence: null,
|
|
80
|
+
},
|
|
81
|
+
true
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {(client: import('../../agent-client/src/client.js').BridgeClient) => Promise<ToolResult>} callback
|
|
87
|
+
* @returns {Promise<ToolResult>}
|
|
88
|
+
*/
|
|
89
|
+
export async function withToolClient(callback) {
|
|
90
|
+
try {
|
|
91
|
+
return await withBridgeClient(callback);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return summarizeToolError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {import('../../agent-client/src/client.js').BridgeClient} client
|
|
99
|
+
* @param {{ elementRef?: string | undefined, selector?: string | undefined }} input
|
|
100
|
+
* @param {number | null | undefined} [tabId]
|
|
101
|
+
* @returns {Promise<string>}
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveToolRef(client, input, tabId = null) {
|
|
104
|
+
if (typeof input.elementRef === 'string' && input.elementRef) {
|
|
105
|
+
return input.elementRef;
|
|
106
|
+
}
|
|
107
|
+
if (typeof input.selector === 'string' && input.selector) {
|
|
108
|
+
return resolveRef(client, input.selector, tabId, REQUEST_SOURCE);
|
|
109
|
+
}
|
|
110
|
+
throw new Error('Provide either elementRef or selector.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {'quick' | 'normal' | 'deep' | null}
|
|
116
|
+
*/
|
|
117
|
+
export function getBudgetPresetName(value) {
|
|
118
|
+
return isBudgetPresetName(value) ? value : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {{ budgetPreset?: unknown, selector?: unknown, elementRef?: unknown }} args
|
|
123
|
+
* @returns {'quick' | 'normal' | 'deep' | null}
|
|
124
|
+
*/
|
|
125
|
+
export function inferBudgetFromSelector(args) {
|
|
126
|
+
if (getBudgetPresetName(args.budgetPreset)) return null;
|
|
127
|
+
if (typeof args.elementRef === 'string' && args.elementRef) return 'quick';
|
|
128
|
+
const sel = typeof args.selector === 'string' ? args.selector.trim() : '';
|
|
129
|
+
if (!sel || sel === '*' || sel === 'body') return null;
|
|
130
|
+
if (/^#[\w-]+$/.test(sel)) return 'quick';
|
|
131
|
+
if ((sel.match(/\s/g) || []).length >= 3) return 'deep';
|
|
132
|
+
return 'normal';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {{ budgetPreset?: unknown }} args
|
|
137
|
+
* @returns {number | null}
|
|
138
|
+
*/
|
|
139
|
+
export function getToolTokenBudget(args) {
|
|
140
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
141
|
+
return presetName ? getBudgetPreset(presetName).tokenBudget : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @template {{ budgetPreset?: unknown, maxNodes?: unknown, maxDepth?: unknown, textBudget?: unknown }} T
|
|
146
|
+
* @param {T} args
|
|
147
|
+
* @returns {T}
|
|
148
|
+
*/
|
|
149
|
+
export function applyTreeBudgetPreset(args) {
|
|
150
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
151
|
+
if (!presetName) {
|
|
152
|
+
return args;
|
|
153
|
+
}
|
|
154
|
+
const preset = getBudgetPreset(presetName);
|
|
155
|
+
return /** @type {T} */ ({
|
|
156
|
+
...args,
|
|
157
|
+
maxNodes: args.maxNodes ?? preset.maxNodes,
|
|
158
|
+
maxDepth: args.maxDepth ?? preset.maxDepth,
|
|
159
|
+
textBudget: args.textBudget ?? preset.textBudget,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
|
|
165
|
+
* @param {T} args
|
|
166
|
+
* @returns {T}
|
|
167
|
+
*/
|
|
168
|
+
export function applyTextBudgetPreset(args) {
|
|
169
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
170
|
+
if (!presetName) {
|
|
171
|
+
return args;
|
|
172
|
+
}
|
|
173
|
+
const preset = getBudgetPreset(presetName);
|
|
174
|
+
return /** @type {T} */ ({
|
|
175
|
+
...args,
|
|
176
|
+
textBudget: args.textBudget ?? preset.textBudget,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
|
|
182
|
+
* @param {T} args
|
|
183
|
+
* @returns {T}
|
|
184
|
+
*/
|
|
185
|
+
export function applyPageTextBudgetPreset(args) {
|
|
186
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
187
|
+
if (!presetName) {
|
|
188
|
+
return args;
|
|
189
|
+
}
|
|
190
|
+
const textBudgetByPreset = {
|
|
191
|
+
quick: 2000,
|
|
192
|
+
normal: DEFAULT_PAGE_TEXT_BUDGET,
|
|
193
|
+
deep: DEFAULT_PAGE_TEXT_BUDGET * 2,
|
|
194
|
+
};
|
|
195
|
+
return /** @type {T} */ ({
|
|
196
|
+
...args,
|
|
197
|
+
textBudget: args.textBudget ?? textBudgetByPreset[presetName],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @template {{ budgetPreset?: unknown, limit?: unknown }} T
|
|
203
|
+
* @param {T} args
|
|
204
|
+
* @param {{ quick: number, normal: number, deep: number }} defaults
|
|
205
|
+
* @returns {T}
|
|
206
|
+
*/
|
|
207
|
+
export function applyLimitBudgetPreset(args, defaults) {
|
|
208
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
209
|
+
if (!presetName) {
|
|
210
|
+
return args;
|
|
211
|
+
}
|
|
212
|
+
return /** @type {T} */ ({
|
|
213
|
+
...args,
|
|
214
|
+
limit: args.limit ?? defaults[presetName],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @template {{ budgetPreset?: unknown, maxLength?: unknown }} T
|
|
220
|
+
* @param {T} args
|
|
221
|
+
* @returns {T}
|
|
222
|
+
*/
|
|
223
|
+
export function applyHtmlBudgetPreset(args) {
|
|
224
|
+
const presetName = getBudgetPresetName(args.budgetPreset);
|
|
225
|
+
if (!presetName) {
|
|
226
|
+
return args;
|
|
227
|
+
}
|
|
228
|
+
const maxLengthByPreset = {
|
|
229
|
+
quick: 600,
|
|
230
|
+
normal: DEFAULT_MAX_HTML_LENGTH,
|
|
231
|
+
deep: 6000,
|
|
232
|
+
};
|
|
233
|
+
return /** @type {T} */ ({
|
|
234
|
+
...args,
|
|
235
|
+
maxLength: args.maxLength ?? maxLengthByPreset[presetName],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {import('../../agent-client/src/client.js').BridgeClient} client
|
|
241
|
+
* @param {BridgeMethod} method
|
|
242
|
+
* @param {Record<string, unknown>} params
|
|
243
|
+
* @param {{ tabId?: number | null, source?: import('../../protocol/src/types.js').BridgeRequestSource, tokenBudget?: number | null }} options
|
|
244
|
+
* @returns {Promise<BridgeResponse>}
|
|
245
|
+
*/
|
|
246
|
+
export async function requestBridgeWithRetry(client, method, params, options) {
|
|
247
|
+
const response = await requestBridge(client, method, params, options);
|
|
248
|
+
const recovery = !response.ok && response.error ? getErrorRecovery(response.error.code) : null;
|
|
249
|
+
if (!response.ok && recovery?.retry) {
|
|
250
|
+
const delay = recovery.retryAfterMs ?? 1000;
|
|
251
|
+
process.stderr.write(
|
|
252
|
+
`[bbx-mcp] Retrying ${method} after ${delay}ms (${response.error.code})\n`
|
|
253
|
+
);
|
|
254
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
255
|
+
return requestBridge(client, method, params, options);
|
|
256
|
+
}
|
|
257
|
+
return response;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {BridgeMethod} method
|
|
262
|
+
* @param {Record<string, unknown>} [params={}]
|
|
263
|
+
* @param {{ tabId?: number | null, summaryMethod?: string, tokenBudget?: number | null }} [options]
|
|
264
|
+
* @returns {Promise<ToolResult>}
|
|
265
|
+
*/
|
|
266
|
+
export async function callBridgeTool(method, params = {}, options = {}) {
|
|
267
|
+
return withToolClient(async (client) => {
|
|
268
|
+
const response = await requestBridgeWithRetry(client, method, params, {
|
|
269
|
+
tabId: options.tabId ?? null,
|
|
270
|
+
source: REQUEST_SOURCE,
|
|
271
|
+
tokenBudget: options.tokenBudget ?? null,
|
|
272
|
+
});
|
|
273
|
+
return summarizeToolResponse(response, options.summaryMethod || method);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @typedef {{ ref: boolean, method: BridgeMethod, params: (args: Record<string, unknown>, ref?: string) => Record<string, unknown> }} ToolAction
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {Record<string, ToolAction>} actions
|
|
283
|
+
* @param {Record<string, unknown> & { action: string }} args
|
|
284
|
+
* @param {string} toolName
|
|
285
|
+
* @returns {Promise<ToolResult>}
|
|
286
|
+
*/
|
|
287
|
+
export async function dispatchToolAction(actions, args, toolName) {
|
|
288
|
+
const entry = actions[args.action];
|
|
289
|
+
if (!entry) return summarizeToolError(`Unsupported ${toolName} action "${args.action}".`);
|
|
290
|
+
return withToolClient(async (client) => {
|
|
291
|
+
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
292
|
+
const ref = entry.ref
|
|
293
|
+
? await resolveToolRef(
|
|
294
|
+
client,
|
|
295
|
+
/** @type {{ elementRef?: string, selector?: string }} */ (args),
|
|
296
|
+
requestedTabId
|
|
297
|
+
)
|
|
298
|
+
: undefined;
|
|
299
|
+
const response = await requestBridgeWithRetry(client, entry.method, entry.params(args, ref), {
|
|
300
|
+
tabId: requestedTabId,
|
|
301
|
+
source: REQUEST_SOURCE,
|
|
302
|
+
tokenBudget: getToolTokenBudget(/** @type {{ budgetPreset?: unknown }} */ (args)),
|
|
303
|
+
});
|
|
304
|
+
return summarizeToolResponse(response, entry.method);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export {
|
|
309
|
+
bridgeMethodNeedsTab,
|
|
310
|
+
getDoctorReport,
|
|
311
|
+
requestBridge,
|
|
312
|
+
resolveRef,
|
|
313
|
+
withBridgeClient,
|
|
314
|
+
annotateBridgeSummary,
|
|
315
|
+
summarizeBridgeResponse,
|
|
316
|
+
summarizeBatchErrorItem,
|
|
317
|
+
summarizeBatchResponseItem,
|
|
318
|
+
};
|