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