@browserbridge/bbx 1.1.0 → 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.
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+
3
+ import { callBridgeTool, getToolTokenBudget, summarizeToolError } from './handlers-utils.js';
4
+
5
+ /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
6
+ /** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
7
+
8
+ /**
9
+ * @param {{ action: string, url?: string, active?: boolean, tabId?: number }} args
10
+ * @returns {Promise<ToolResult>}
11
+ */
12
+ export async function handleTabsTool(args) {
13
+ if (args.action === 'list') {
14
+ return callBridgeTool('tabs.list');
15
+ }
16
+ if (args.action === 'create') {
17
+ return callBridgeTool('tabs.create', {
18
+ url: args.url,
19
+ active: args.active,
20
+ });
21
+ }
22
+ if (args.action === 'close') {
23
+ if (typeof args.tabId !== 'number') {
24
+ return summarizeToolError('tabId is required for tabs.close.');
25
+ }
26
+ return callBridgeTool('tabs.close', { tabId: args.tabId });
27
+ }
28
+ return summarizeToolError(`Unsupported tabs action "${args.action}".`);
29
+ }
30
+
31
+ /** @type {Record<string, { method: BridgeMethod, params: (a: Record<string, unknown>) => Record<string, unknown> }>} */
32
+ export const NAVIGATION_ACTIONS = {
33
+ navigate: {
34
+ method: 'navigation.navigate',
35
+ params: (a) => ({
36
+ url: a.url,
37
+ waitForLoad: a.waitForLoad,
38
+ timeoutMs: a.timeoutMs,
39
+ }),
40
+ },
41
+ reload: {
42
+ method: 'navigation.reload',
43
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
44
+ },
45
+ go_back: {
46
+ method: 'navigation.go_back',
47
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
48
+ },
49
+ go_forward: {
50
+ method: 'navigation.go_forward',
51
+ params: (a) => ({ waitForLoad: a.waitForLoad, timeoutMs: a.timeoutMs }),
52
+ },
53
+ scroll: {
54
+ method: 'viewport.scroll',
55
+ params: (a) => ({
56
+ top: a.top,
57
+ left: a.left,
58
+ behavior: a.behavior,
59
+ relative: a.relative,
60
+ }),
61
+ },
62
+ resize: {
63
+ method: 'viewport.resize',
64
+ params: (a) => ({ width: a.width, height: a.height, reset: a.reset }),
65
+ },
66
+ };
67
+
68
+ /**
69
+ * @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
70
+ * @returns {Promise<ToolResult>}
71
+ */
72
+ export async function handleNavigationTool(args) {
73
+ const entry = NAVIGATION_ACTIONS[args.action];
74
+ if (!entry) return summarizeToolError(`Unsupported navigation action "${args.action}".`);
75
+ return callBridgeTool(entry.method, entry.params(args), {
76
+ tabId: typeof args.tabId === 'number' ? args.tabId : null,
77
+ tokenBudget: getToolTokenBudget(args),
78
+ });
79
+ }
@@ -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
+ }