@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,131 @@
|
|
|
1
|
+
import type { BridgeTransport } from '../../native-host/src/config.js';
|
|
2
|
+
import type {
|
|
3
|
+
BridgeMeta,
|
|
4
|
+
BridgeMethod,
|
|
5
|
+
BridgeRequestSource,
|
|
6
|
+
BridgeResponse,
|
|
7
|
+
} from '../../protocol/src/types.js';
|
|
8
|
+
import type { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
9
|
+
|
|
10
|
+
export type { BridgeMeta, BridgeMethod, BridgeRequestSource, BridgeResponse, BridgeTransport };
|
|
11
|
+
|
|
12
|
+
export type McpClientName =
|
|
13
|
+
| 'codex'
|
|
14
|
+
| 'claude'
|
|
15
|
+
| 'cursor'
|
|
16
|
+
| 'copilot'
|
|
17
|
+
| 'opencode'
|
|
18
|
+
| 'antigravity'
|
|
19
|
+
| 'windsurf'
|
|
20
|
+
| 'agents';
|
|
21
|
+
|
|
22
|
+
export type SupportedTarget = McpClientName;
|
|
23
|
+
|
|
24
|
+
export type Detector = () => boolean | Promise<boolean>;
|
|
25
|
+
|
|
26
|
+
export interface InstallAgentOptions {
|
|
27
|
+
targets: SupportedTarget[];
|
|
28
|
+
projectPath: string;
|
|
29
|
+
global: boolean;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SetupStatusOptions {
|
|
34
|
+
global?: boolean;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
projectPath?: string;
|
|
37
|
+
mcpDetectors?: Record<string, Detector>;
|
|
38
|
+
skillDetectors?: Record<string, Detector>;
|
|
39
|
+
access?: (targetPath: string) => Promise<void>;
|
|
40
|
+
readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProtocolHealthResult {
|
|
44
|
+
extensionConnected?: boolean;
|
|
45
|
+
supported_versions?: string[];
|
|
46
|
+
daemon_supported_versions?: string[];
|
|
47
|
+
deprecated_since?: string;
|
|
48
|
+
migration_hint?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ClientMessage =
|
|
52
|
+
| {
|
|
53
|
+
type: 'registered';
|
|
54
|
+
role: 'agent' | 'extension';
|
|
55
|
+
clientId?: string;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
type: 'agent.response';
|
|
59
|
+
response: BridgeResponse;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export interface PendingRequest {
|
|
63
|
+
resolve: (value: any) => void;
|
|
64
|
+
reject: (error: Error) => void;
|
|
65
|
+
timeoutId: NodeJS.Timeout;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BridgeClientOptions {
|
|
69
|
+
transport?: BridgeTransport;
|
|
70
|
+
socketPath?: string;
|
|
71
|
+
clientId?: string;
|
|
72
|
+
defaultTimeoutMs?: number;
|
|
73
|
+
autoReconnect?: boolean;
|
|
74
|
+
restartDaemonOnVersionMismatch?: boolean;
|
|
75
|
+
restartDaemonFn?: typeof restartBridgeDaemon;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ShortcutCommand {
|
|
79
|
+
method: BridgeMethod;
|
|
80
|
+
resolve?: boolean;
|
|
81
|
+
printMethod?: string;
|
|
82
|
+
usage: string;
|
|
83
|
+
description: string;
|
|
84
|
+
build: (r: string[], ref?: string) => Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BrowserManifestStatus {
|
|
88
|
+
browser: string;
|
|
89
|
+
manifestPath: string;
|
|
90
|
+
installed: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DoctorReport {
|
|
94
|
+
manifestInstalled: boolean;
|
|
95
|
+
manifestPath: string;
|
|
96
|
+
allowedOrigins: string[];
|
|
97
|
+
defaultExtensionId: string | null;
|
|
98
|
+
defaultExtensionIdSource: string;
|
|
99
|
+
daemonReachable: boolean;
|
|
100
|
+
extensionConnected: boolean;
|
|
101
|
+
accessEnabled: boolean;
|
|
102
|
+
enabledWindowId: number | null;
|
|
103
|
+
routeTabId: number | null;
|
|
104
|
+
routeReady: boolean;
|
|
105
|
+
routeReason: string;
|
|
106
|
+
issues: string[];
|
|
107
|
+
nextSteps: string[];
|
|
108
|
+
browserManifests: BrowserManifestStatus[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface DoctorReportOptions {
|
|
112
|
+
loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
|
|
113
|
+
manifestPath?: string;
|
|
114
|
+
defaultExtensionIdInfo?: { extensionId: string | null; source: string };
|
|
115
|
+
bridgeClientRunner?: <T>(
|
|
116
|
+
callback: (client: { request: BridgeClientRequest }) => Promise<T>
|
|
117
|
+
) => Promise<T>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type BridgeClientRequest = (options: {
|
|
121
|
+
method: BridgeMethod;
|
|
122
|
+
tabId?: number | null;
|
|
123
|
+
params?: Record<string, unknown>;
|
|
124
|
+
meta?: BridgeMeta;
|
|
125
|
+
timeoutMs?: number;
|
|
126
|
+
}) => Promise<BridgeResponse>;
|
|
127
|
+
|
|
128
|
+
export interface ScreenshotResult {
|
|
129
|
+
image: string;
|
|
130
|
+
rect: Record<string, unknown>;
|
|
131
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
dispatchToolAction,
|
|
5
|
+
getToolTokenBudget,
|
|
6
|
+
REQUEST_SOURCE,
|
|
7
|
+
requestBridge,
|
|
8
|
+
resolveToolRef,
|
|
9
|
+
resolveRef,
|
|
10
|
+
summarizeToolError,
|
|
11
|
+
summarizeToolResponse,
|
|
12
|
+
withToolClient,
|
|
13
|
+
} from './handlers-utils.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
16
|
+
/** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
|
|
17
|
+
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
18
|
+
|
|
19
|
+
/** @type {Record<string, ToolAction>} */
|
|
20
|
+
export const CAPTURE_ACTIONS = {
|
|
21
|
+
element: {
|
|
22
|
+
ref: true,
|
|
23
|
+
method: 'screenshot.capture_element',
|
|
24
|
+
params: (_, r) => ({ elementRef: r }),
|
|
25
|
+
},
|
|
26
|
+
region: {
|
|
27
|
+
ref: false,
|
|
28
|
+
method: 'screenshot.capture_region',
|
|
29
|
+
params: (a) => /** @type {Record<string, unknown>} */ (a.rect || {}),
|
|
30
|
+
},
|
|
31
|
+
full_page: {
|
|
32
|
+
ref: false,
|
|
33
|
+
method: 'screenshot.capture_full_page',
|
|
34
|
+
params: () => ({}),
|
|
35
|
+
},
|
|
36
|
+
cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
|
|
37
|
+
cdp_dom_snapshot: {
|
|
38
|
+
ref: false,
|
|
39
|
+
method: 'cdp.get_dom_snapshot',
|
|
40
|
+
params: () => ({}),
|
|
41
|
+
},
|
|
42
|
+
cdp_box_model: {
|
|
43
|
+
ref: false,
|
|
44
|
+
method: 'cdp.get_box_model',
|
|
45
|
+
params: (a) => ({ nodeId: a.nodeId }),
|
|
46
|
+
},
|
|
47
|
+
cdp_computed_styles: {
|
|
48
|
+
ref: false,
|
|
49
|
+
method: 'cdp.get_computed_styles_for_node',
|
|
50
|
+
params: (a) => ({ nodeId: a.nodeId }),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** @param {Record<string, unknown>} args */
|
|
55
|
+
function isCdpNodeCapture(args) {
|
|
56
|
+
return args.action === 'cdp_box_model' || args.action === 'cdp_computed_styles';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
61
|
+
* @returns {Promise<ToolResult>}
|
|
62
|
+
*/
|
|
63
|
+
export async function handleCaptureTool(args) {
|
|
64
|
+
if (
|
|
65
|
+
isCdpNodeCapture(args) &&
|
|
66
|
+
(typeof args.nodeId !== 'number' || !Number.isFinite(args.nodeId))
|
|
67
|
+
) {
|
|
68
|
+
return summarizeToolError('nodeId must be a finite number.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {Record<string, BridgeMethod>} */
|
|
75
|
+
export const INPUT_ACTION_METHODS = {
|
|
76
|
+
click: 'input.click',
|
|
77
|
+
focus: 'input.focus',
|
|
78
|
+
type: 'input.type',
|
|
79
|
+
press_key: 'input.press_key',
|
|
80
|
+
cdp_press_key: 'cdp.dispatch_key_event',
|
|
81
|
+
set_checked: 'input.set_checked',
|
|
82
|
+
select_option: 'input.select_option',
|
|
83
|
+
hover: 'input.hover',
|
|
84
|
+
drag: 'input.drag',
|
|
85
|
+
scroll_into_view: 'input.scroll_into_view',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @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
|
|
90
|
+
* @returns {Promise<ToolResult>}
|
|
91
|
+
*/
|
|
92
|
+
export async function handleInputTool(args) {
|
|
93
|
+
return withToolClient(async (client) => {
|
|
94
|
+
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
95
|
+
const elementTarget = async () => ({
|
|
96
|
+
elementRef: await resolveToolRef(client, args, requestedTabId),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
switch (args.action) {
|
|
100
|
+
case 'click': {
|
|
101
|
+
const response = await requestBridge(
|
|
102
|
+
client,
|
|
103
|
+
'input.click',
|
|
104
|
+
{
|
|
105
|
+
target: await elementTarget(),
|
|
106
|
+
button: args.button,
|
|
107
|
+
clickCount: args.clickCount,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
tabId: requestedTabId,
|
|
111
|
+
source: REQUEST_SOURCE,
|
|
112
|
+
tokenBudget: getToolTokenBudget(args),
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
return summarizeToolResponse(response, 'input.click');
|
|
116
|
+
}
|
|
117
|
+
case 'focus': {
|
|
118
|
+
const response = await requestBridge(
|
|
119
|
+
client,
|
|
120
|
+
'input.focus',
|
|
121
|
+
{
|
|
122
|
+
target: await elementTarget(),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
tabId: requestedTabId,
|
|
126
|
+
source: REQUEST_SOURCE,
|
|
127
|
+
tokenBudget: getToolTokenBudget(args),
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
return summarizeToolResponse(response, 'input.focus');
|
|
131
|
+
}
|
|
132
|
+
case 'type': {
|
|
133
|
+
const response = await requestBridge(
|
|
134
|
+
client,
|
|
135
|
+
'input.type',
|
|
136
|
+
{
|
|
137
|
+
target: await elementTarget(),
|
|
138
|
+
text: args.text,
|
|
139
|
+
clear: args.clear,
|
|
140
|
+
submit: args.submit,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
tabId: requestedTabId,
|
|
144
|
+
source: REQUEST_SOURCE,
|
|
145
|
+
tokenBudget: getToolTokenBudget(args),
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
return summarizeToolResponse(response, 'input.type');
|
|
149
|
+
}
|
|
150
|
+
case 'press_key': {
|
|
151
|
+
const target = args.elementRef || args.selector ? await elementTarget() : undefined;
|
|
152
|
+
const response = await requestBridge(
|
|
153
|
+
client,
|
|
154
|
+
'input.press_key',
|
|
155
|
+
{
|
|
156
|
+
target,
|
|
157
|
+
key: args.key,
|
|
158
|
+
modifiers: args.modifiers,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
tabId: requestedTabId,
|
|
162
|
+
source: REQUEST_SOURCE,
|
|
163
|
+
tokenBudget: getToolTokenBudget(args),
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
return summarizeToolResponse(response, 'input.press_key');
|
|
167
|
+
}
|
|
168
|
+
case 'cdp_press_key': {
|
|
169
|
+
const response = await requestBridge(
|
|
170
|
+
client,
|
|
171
|
+
'cdp.dispatch_key_event',
|
|
172
|
+
{
|
|
173
|
+
key: args.key,
|
|
174
|
+
code: args.code,
|
|
175
|
+
modifiers: args.modifiers,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
tabId: requestedTabId,
|
|
179
|
+
source: REQUEST_SOURCE,
|
|
180
|
+
tokenBudget: getToolTokenBudget(args),
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
return summarizeToolResponse(response, 'cdp.dispatch_key_event');
|
|
184
|
+
}
|
|
185
|
+
case 'set_checked': {
|
|
186
|
+
const response = await requestBridge(
|
|
187
|
+
client,
|
|
188
|
+
'input.set_checked',
|
|
189
|
+
{
|
|
190
|
+
target: await elementTarget(),
|
|
191
|
+
checked: args.checked,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
tabId: requestedTabId,
|
|
195
|
+
source: REQUEST_SOURCE,
|
|
196
|
+
tokenBudget: getToolTokenBudget(args),
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
return summarizeToolResponse(response, 'input.set_checked');
|
|
200
|
+
}
|
|
201
|
+
case 'select_option': {
|
|
202
|
+
const response = await requestBridge(
|
|
203
|
+
client,
|
|
204
|
+
'input.select_option',
|
|
205
|
+
{
|
|
206
|
+
target: await elementTarget(),
|
|
207
|
+
values: args.values,
|
|
208
|
+
labels: args.labels,
|
|
209
|
+
indexes: args.indexes,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
tabId: requestedTabId,
|
|
213
|
+
source: REQUEST_SOURCE,
|
|
214
|
+
tokenBudget: getToolTokenBudget(args),
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
return summarizeToolResponse(response, 'input.select_option');
|
|
218
|
+
}
|
|
219
|
+
case 'hover': {
|
|
220
|
+
const response = await requestBridge(
|
|
221
|
+
client,
|
|
222
|
+
'input.hover',
|
|
223
|
+
{
|
|
224
|
+
target: await elementTarget(),
|
|
225
|
+
duration: args.duration,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
tabId: requestedTabId,
|
|
229
|
+
source: REQUEST_SOURCE,
|
|
230
|
+
tokenBudget: getToolTokenBudget(args),
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
return summarizeToolResponse(response, 'input.hover');
|
|
234
|
+
}
|
|
235
|
+
case 'drag': {
|
|
236
|
+
const source = {
|
|
237
|
+
elementRef:
|
|
238
|
+
args.sourceElementRef ||
|
|
239
|
+
(args.sourceSelector
|
|
240
|
+
? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
|
|
241
|
+
: ''),
|
|
242
|
+
};
|
|
243
|
+
const destination = {
|
|
244
|
+
elementRef:
|
|
245
|
+
args.destinationElementRef ||
|
|
246
|
+
(args.destinationSelector
|
|
247
|
+
? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
|
|
248
|
+
: ''),
|
|
249
|
+
};
|
|
250
|
+
if (!source.elementRef || !destination.elementRef) {
|
|
251
|
+
return summarizeToolError(
|
|
252
|
+
'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const response = await requestBridge(
|
|
256
|
+
client,
|
|
257
|
+
'input.drag',
|
|
258
|
+
{
|
|
259
|
+
source,
|
|
260
|
+
destination,
|
|
261
|
+
offsetX: args.offsetX,
|
|
262
|
+
offsetY: args.offsetY,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
tabId: requestedTabId,
|
|
266
|
+
source: REQUEST_SOURCE,
|
|
267
|
+
tokenBudget: getToolTokenBudget(args),
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
return summarizeToolResponse(response, 'input.drag');
|
|
271
|
+
}
|
|
272
|
+
case 'scroll_into_view': {
|
|
273
|
+
const response = await requestBridge(
|
|
274
|
+
client,
|
|
275
|
+
'input.scroll_into_view',
|
|
276
|
+
{
|
|
277
|
+
target: await elementTarget(),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
tabId: requestedTabId,
|
|
281
|
+
source: REQUEST_SOURCE,
|
|
282
|
+
tokenBudget: getToolTokenBudget(args),
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
return summarizeToolResponse(response, 'input.scroll_into_view');
|
|
286
|
+
}
|
|
287
|
+
default:
|
|
288
|
+
return summarizeToolError(`Unsupported input action "${args.action}".`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyHtmlBudgetPreset,
|
|
5
|
+
applyTextBudgetPreset,
|
|
6
|
+
applyTreeBudgetPreset,
|
|
7
|
+
dispatchToolAction,
|
|
8
|
+
inferBudgetFromSelector,
|
|
9
|
+
} from './handlers-utils.js';
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
12
|
+
/** @typedef {import('./handlers-utils.js').ToolAction} ToolAction */
|
|
13
|
+
/** @typedef {import('./handlers-utils.js').ToolResult} ToolResult */
|
|
14
|
+
|
|
15
|
+
/** @type {Record<string, ToolAction>} */
|
|
16
|
+
export const DOM_ACTIONS = {
|
|
17
|
+
query: {
|
|
18
|
+
ref: false,
|
|
19
|
+
method: 'dom.query',
|
|
20
|
+
params: (a) => ({
|
|
21
|
+
selector: a.selector || 'body',
|
|
22
|
+
withinRef: a.withinRef,
|
|
23
|
+
maxNodes: a.maxNodes,
|
|
24
|
+
maxDepth: a.maxDepth,
|
|
25
|
+
textBudget: a.textBudget,
|
|
26
|
+
includeBbox: a.includeBbox,
|
|
27
|
+
attributeAllowlist: a.attributeAllowlist,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
describe: {
|
|
31
|
+
ref: true,
|
|
32
|
+
method: 'dom.describe',
|
|
33
|
+
params: (_, r) => ({ elementRef: r }),
|
|
34
|
+
},
|
|
35
|
+
text: {
|
|
36
|
+
ref: true,
|
|
37
|
+
method: 'dom.get_text',
|
|
38
|
+
params: (a, r) => ({ elementRef: r, textBudget: a.textBudget }),
|
|
39
|
+
},
|
|
40
|
+
attributes: {
|
|
41
|
+
ref: true,
|
|
42
|
+
method: 'dom.get_attributes',
|
|
43
|
+
params: (a, r) => ({ elementRef: r, attributes: a.attributes || [] }),
|
|
44
|
+
},
|
|
45
|
+
wait: {
|
|
46
|
+
ref: false,
|
|
47
|
+
method: 'dom.wait_for',
|
|
48
|
+
params: (a) => ({
|
|
49
|
+
selector: a.selector,
|
|
50
|
+
text: a.text,
|
|
51
|
+
state: a.state,
|
|
52
|
+
timeoutMs: a.timeoutMs,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
find_text: {
|
|
56
|
+
ref: false,
|
|
57
|
+
method: 'dom.find_by_text',
|
|
58
|
+
params: (a) => ({
|
|
59
|
+
text: a.text,
|
|
60
|
+
exact: a.exact,
|
|
61
|
+
selector: a.selector,
|
|
62
|
+
maxResults: a.maxResults,
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
find_role: {
|
|
66
|
+
ref: false,
|
|
67
|
+
method: 'dom.find_by_role',
|
|
68
|
+
params: (a) => ({
|
|
69
|
+
role: a.role,
|
|
70
|
+
name: a.name,
|
|
71
|
+
selector: a.selector,
|
|
72
|
+
maxResults: a.maxResults,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
html: {
|
|
76
|
+
ref: true,
|
|
77
|
+
method: 'dom.get_html',
|
|
78
|
+
params: (a, r) => ({
|
|
79
|
+
elementRef: r,
|
|
80
|
+
outer: a.outer,
|
|
81
|
+
maxLength: a.maxLength,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
accessibility_tree: {
|
|
85
|
+
ref: false,
|
|
86
|
+
method: 'dom.get_accessibility_tree',
|
|
87
|
+
params: (a) => ({ maxNodes: a.maxNodes, maxDepth: a.maxDepth }),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @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
|
|
93
|
+
* @returns {Promise<ToolResult>}
|
|
94
|
+
*/
|
|
95
|
+
export async function handleDomTool(args) {
|
|
96
|
+
if (args.action === 'query' || args.action === 'accessibility_tree') {
|
|
97
|
+
const inferred = inferBudgetFromSelector(args);
|
|
98
|
+
const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
|
|
99
|
+
return dispatchToolAction(DOM_ACTIONS, applyTreeBudgetPreset(withBudget), 'DOM');
|
|
100
|
+
}
|
|
101
|
+
if (args.action === 'text') {
|
|
102
|
+
return dispatchToolAction(DOM_ACTIONS, applyTextBudgetPreset(args), 'DOM');
|
|
103
|
+
}
|
|
104
|
+
if (args.action === 'html') {
|
|
105
|
+
return dispatchToolAction(DOM_ACTIONS, applyHtmlBudgetPreset(args), 'DOM');
|
|
106
|
+
}
|
|
107
|
+
return dispatchToolAction(DOM_ACTIONS, args, 'DOM');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @type {Record<string, ToolAction>} */
|
|
111
|
+
export const STYLES_LAYOUT_ACTIONS = {
|
|
112
|
+
computed: {
|
|
113
|
+
ref: true,
|
|
114
|
+
method: 'styles.get_computed',
|
|
115
|
+
params: (a, r) => ({ elementRef: r, properties: a.properties }),
|
|
116
|
+
},
|
|
117
|
+
matched_rules: {
|
|
118
|
+
ref: true,
|
|
119
|
+
method: 'styles.get_matched_rules',
|
|
120
|
+
params: (_, r) => ({ elementRef: r }),
|
|
121
|
+
},
|
|
122
|
+
box_model: {
|
|
123
|
+
ref: true,
|
|
124
|
+
method: 'layout.get_box_model',
|
|
125
|
+
params: (_, r) => ({ elementRef: r }),
|
|
126
|
+
},
|
|
127
|
+
hit_test: {
|
|
128
|
+
ref: false,
|
|
129
|
+
method: 'layout.hit_test',
|
|
130
|
+
params: (a) => ({ x: a.x, y: a.y }),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {{ action: string, elementRef?: string, selector?: string, properties?: string[], x?: number, y?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
136
|
+
* @returns {Promise<ToolResult>}
|
|
137
|
+
*/
|
|
138
|
+
export async function handleStylesLayoutTool(args) {
|
|
139
|
+
return dispatchToolAction(STYLES_LAYOUT_ACTIONS, args, 'styles/layout');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @type {Record<string, ToolAction>} */
|
|
143
|
+
export const PATCH_ACTIONS = {
|
|
144
|
+
apply_styles: {
|
|
145
|
+
ref: true,
|
|
146
|
+
method: 'patch.apply_styles',
|
|
147
|
+
params: (a, r) => ({
|
|
148
|
+
target: { elementRef: r },
|
|
149
|
+
declarations: a.declarations,
|
|
150
|
+
important: a.important,
|
|
151
|
+
patchId: a.patchId,
|
|
152
|
+
verify: a.verify,
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
apply_dom: {
|
|
156
|
+
ref: true,
|
|
157
|
+
method: 'patch.apply_dom',
|
|
158
|
+
params: (a, r) => {
|
|
159
|
+
const operation = typeof a.operation === 'string' ? a.operation : '';
|
|
160
|
+
/** @type {Record<string, string>} */
|
|
161
|
+
const opMap = {
|
|
162
|
+
setAttribute: 'set_attribute',
|
|
163
|
+
removeAttribute: 'remove_attribute',
|
|
164
|
+
addClass: 'add_class',
|
|
165
|
+
removeClass: 'remove_class',
|
|
166
|
+
setTextContent: 'set_text',
|
|
167
|
+
setProperty: 'set_attribute',
|
|
168
|
+
};
|
|
169
|
+
const normalizedOperation = opMap[operation] || operation;
|
|
170
|
+
const value =
|
|
171
|
+
normalizedOperation === 'add_class' || normalizedOperation === 'remove_class'
|
|
172
|
+
? (a.value ?? a.name)
|
|
173
|
+
: a.value;
|
|
174
|
+
return {
|
|
175
|
+
target: { elementRef: r },
|
|
176
|
+
operation: normalizedOperation,
|
|
177
|
+
value,
|
|
178
|
+
name: a.name,
|
|
179
|
+
patchId: a.patchId,
|
|
180
|
+
verify: a.verify,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
list: { ref: false, method: 'patch.list', params: () => ({}) },
|
|
185
|
+
rollback: {
|
|
186
|
+
ref: false,
|
|
187
|
+
method: 'patch.rollback',
|
|
188
|
+
params: (a) => ({ patchId: a.patchId }),
|
|
189
|
+
},
|
|
190
|
+
commit_baseline: {
|
|
191
|
+
ref: false,
|
|
192
|
+
method: 'patch.commit_session_baseline',
|
|
193
|
+
params: () => ({}),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @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
|
|
199
|
+
* @returns {Promise<ToolResult>}
|
|
200
|
+
*/
|
|
201
|
+
export async function handlePatchTool(args) {
|
|
202
|
+
return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
|
|
203
|
+
}
|
|
@@ -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
|
+
}
|