@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
package/package.json
CHANGED
|
@@ -613,16 +613,18 @@ async function main() {
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
if (command === 'screenshot') {
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
616
|
+
const parsed = extractTabFlag(rest);
|
|
617
|
+
const [refOrSelector, outputPath] = parsed.rest;
|
|
618
|
+
if (!refOrSelector)
|
|
619
|
+
throw new Error('Usage: screenshot [--tab <tabId>] <ref|selector> [path]');
|
|
620
|
+
const elementRef = await resolveRef(client, refOrSelector, parsed.tabId, REQUEST_SOURCE);
|
|
619
621
|
const response = await requestBridge(
|
|
620
622
|
client,
|
|
621
623
|
'screenshot.capture_element',
|
|
622
624
|
{
|
|
623
625
|
elementRef,
|
|
624
626
|
},
|
|
625
|
-
{ source: REQUEST_SOURCE }
|
|
627
|
+
{ tabId: parsed.tabId, source: REQUEST_SOURCE }
|
|
626
628
|
);
|
|
627
629
|
if (!response.ok) {
|
|
628
630
|
await printSummary(response);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getBridgeTransport,
|
|
16
16
|
getSocketPath,
|
|
17
17
|
} from '../../native-host/src/config.js';
|
|
18
|
+
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
18
19
|
|
|
19
20
|
/** @typedef {import('../../native-host/src/config.js').BridgeTransport} BridgeTransport */
|
|
20
21
|
|
|
@@ -23,7 +24,9 @@ import {
|
|
|
23
24
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
24
25
|
/**
|
|
25
26
|
* @typedef {{
|
|
27
|
+
* extensionConnected?: boolean,
|
|
26
28
|
* supported_versions?: string[],
|
|
29
|
+
* daemon_supported_versions?: string[],
|
|
27
30
|
* deprecated_since?: string,
|
|
28
31
|
* migration_hint?: string
|
|
29
32
|
* }} ProtocolHealthResult
|
|
@@ -55,9 +58,29 @@ import {
|
|
|
55
58
|
* clientId?: string,
|
|
56
59
|
* defaultTimeoutMs?: number,
|
|
57
60
|
* autoReconnect?: boolean,
|
|
61
|
+
* restartDaemonOnVersionMismatch?: boolean,
|
|
62
|
+
* restartDaemonFn?: typeof restartBridgeDaemon,
|
|
58
63
|
* }} BridgeClientOptions
|
|
59
64
|
*/
|
|
60
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} left
|
|
68
|
+
* @param {string} right
|
|
69
|
+
* @returns {number}
|
|
70
|
+
*/
|
|
71
|
+
function compareProtocolVersions(left, right) {
|
|
72
|
+
const leftParts = left.split('.').map((part) => Number(part) || 0);
|
|
73
|
+
const rightParts = right.split('.').map((part) => Number(part) || 0);
|
|
74
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
75
|
+
for (let index = 0; index < length; index += 1) {
|
|
76
|
+
const delta = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
77
|
+
if (delta !== 0) {
|
|
78
|
+
return delta > 0 ? 1 : -1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
/**
|
|
62
85
|
* @param {string} method
|
|
63
86
|
* @param {number} timeoutMs
|
|
@@ -81,6 +104,8 @@ export class BridgeClient extends EventEmitter {
|
|
|
81
104
|
clientId = `agent_${randomUUID()}`,
|
|
82
105
|
defaultTimeoutMs = DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
83
106
|
autoReconnect = false,
|
|
107
|
+
restartDaemonOnVersionMismatch = true,
|
|
108
|
+
restartDaemonFn = restartBridgeDaemon,
|
|
84
109
|
} = {}) {
|
|
85
110
|
super();
|
|
86
111
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -89,6 +114,8 @@ export class BridgeClient extends EventEmitter {
|
|
|
89
114
|
this.clientId = clientId;
|
|
90
115
|
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
91
116
|
this.autoReconnect = autoReconnect;
|
|
117
|
+
this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
|
|
118
|
+
this.restartDaemonFn = restartDaemonFn;
|
|
92
119
|
this.socket = null;
|
|
93
120
|
this.connected = false;
|
|
94
121
|
this.protocolCompatibility = null;
|
|
@@ -96,6 +123,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
96
123
|
/** @type {Map<string, PendingRequest>} */
|
|
97
124
|
this.waiting = new Map();
|
|
98
125
|
this._reconnecting = false;
|
|
126
|
+
this._attemptedVersionMismatchRestart = false;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
/**
|
|
@@ -173,19 +201,38 @@ export class BridgeClient extends EventEmitter {
|
|
|
173
201
|
});
|
|
174
202
|
});
|
|
175
203
|
|
|
204
|
+
this.protocolCompatibility = null;
|
|
205
|
+
this.protocolWarning = null;
|
|
206
|
+
|
|
207
|
+
/** @type {ProtocolHealthResult | null} */
|
|
208
|
+
let healthResult = null;
|
|
176
209
|
try {
|
|
177
210
|
const healthResponse = await this.request({
|
|
178
211
|
method: 'health.ping',
|
|
179
212
|
});
|
|
180
213
|
if (healthResponse.ok) {
|
|
181
|
-
|
|
182
|
-
/** @type {ProtocolHealthResult} */ (healthResponse.result)
|
|
183
|
-
);
|
|
184
|
-
this.protocolWarning = this.protocolCompatibility.warning ?? null;
|
|
214
|
+
healthResult = /** @type {ProtocolHealthResult} */ (healthResponse.result);
|
|
185
215
|
}
|
|
186
216
|
} catch {
|
|
187
217
|
this.protocolCompatibility = null;
|
|
188
218
|
this.protocolWarning = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!healthResult) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.protocolCompatibility = BridgeClient.checkProtocolVersion(healthResult);
|
|
227
|
+
this.protocolWarning = this.protocolCompatibility.warning ?? null;
|
|
228
|
+
if (this.protocolCompatibility.compatible) {
|
|
229
|
+
this._attemptedVersionMismatchRestart = false;
|
|
230
|
+
}
|
|
231
|
+
if (this.shouldRestartDaemonForProtocolMismatch(healthResult)) {
|
|
232
|
+
this._attemptedVersionMismatchRestart = true;
|
|
233
|
+
await this.disconnectForDaemonRestart();
|
|
234
|
+
await this.restartDaemonFn({ transport: this.transport });
|
|
235
|
+
await this.connect();
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
|
|
@@ -305,6 +352,60 @@ export class BridgeClient extends EventEmitter {
|
|
|
305
352
|
this._reconnecting = false;
|
|
306
353
|
}
|
|
307
354
|
|
|
355
|
+
/**
|
|
356
|
+
* @param {ProtocolHealthResult} healthResult
|
|
357
|
+
* @returns {boolean}
|
|
358
|
+
*/
|
|
359
|
+
shouldRestartDaemonForProtocolMismatch(healthResult) {
|
|
360
|
+
if (
|
|
361
|
+
!this.restartDaemonOnVersionMismatch ||
|
|
362
|
+
this._attemptedVersionMismatchRestart ||
|
|
363
|
+
!this.protocolCompatibility ||
|
|
364
|
+
this.protocolCompatibility.compatible
|
|
365
|
+
) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const remoteVersions = Array.isArray(healthResult?.daemon_supported_versions)
|
|
370
|
+
? healthResult.daemon_supported_versions
|
|
371
|
+
: healthResult?.extensionConnected === true
|
|
372
|
+
? []
|
|
373
|
+
: Array.isArray(healthResult?.supported_versions)
|
|
374
|
+
? healthResult.supported_versions
|
|
375
|
+
: [];
|
|
376
|
+
const latestRemote = remoteVersions[0];
|
|
377
|
+
return (
|
|
378
|
+
typeof latestRemote === 'string' &&
|
|
379
|
+
compareProtocolVersions(latestRemote, PROTOCOL_VERSION) < 0
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Drop the current socket before forcing a daemon restart so the next
|
|
385
|
+
* connect() call observes a fresh local process rather than the existing one.
|
|
386
|
+
*
|
|
387
|
+
* @returns {Promise<void>}
|
|
388
|
+
*/
|
|
389
|
+
async disconnectForDaemonRestart() {
|
|
390
|
+
const socket = this.socket;
|
|
391
|
+
if (!socket) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const previousAutoReconnect = this.autoReconnect;
|
|
396
|
+
this.autoReconnect = false;
|
|
397
|
+
this.connected = false;
|
|
398
|
+
this.socket = null;
|
|
399
|
+
|
|
400
|
+
if (!socket.destroyed) {
|
|
401
|
+
const closed = once(socket, 'close').catch(() => {});
|
|
402
|
+
socket.destroy();
|
|
403
|
+
await closed;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.autoReconnect = previousAutoReconnect;
|
|
407
|
+
}
|
|
408
|
+
|
|
308
409
|
/**
|
|
309
410
|
* Check whether the remote side supports our protocol version.
|
|
310
411
|
* Call after a successful health.ping to get early warnings about version drift.
|
|
@@ -347,7 +347,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
347
347
|
{
|
|
348
348
|
title: 'Capture',
|
|
349
349
|
lines: [
|
|
350
|
-
'bbx screenshot <ref|selector> [path]
|
|
350
|
+
'bbx screenshot [--tab <tabId>] <ref|selector> [path] Capture partial element screenshot',
|
|
351
351
|
],
|
|
352
352
|
},
|
|
353
353
|
]);
|
|
@@ -0,0 +1,279 @@
|
|
|
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: true,
|
|
44
|
+
method: 'cdp.get_box_model',
|
|
45
|
+
params: (_, r) => ({ elementRef: r }),
|
|
46
|
+
},
|
|
47
|
+
cdp_computed_styles: {
|
|
48
|
+
ref: true,
|
|
49
|
+
method: 'cdp.get_computed_styles_for_node',
|
|
50
|
+
params: (_, r) => ({ elementRef: r }),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
56
|
+
* @returns {Promise<ToolResult>}
|
|
57
|
+
*/
|
|
58
|
+
export async function handleCaptureTool(args) {
|
|
59
|
+
return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {Record<string, BridgeMethod>} */
|
|
63
|
+
export const INPUT_ACTION_METHODS = {
|
|
64
|
+
click: 'input.click',
|
|
65
|
+
focus: 'input.focus',
|
|
66
|
+
type: 'input.type',
|
|
67
|
+
press_key: 'input.press_key',
|
|
68
|
+
cdp_press_key: 'cdp.dispatch_key_event',
|
|
69
|
+
set_checked: 'input.set_checked',
|
|
70
|
+
select_option: 'input.select_option',
|
|
71
|
+
hover: 'input.hover',
|
|
72
|
+
drag: 'input.drag',
|
|
73
|
+
scroll_into_view: 'input.scroll_into_view',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @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
|
|
78
|
+
* @returns {Promise<ToolResult>}
|
|
79
|
+
*/
|
|
80
|
+
export async function handleInputTool(args) {
|
|
81
|
+
return withToolClient(async (client) => {
|
|
82
|
+
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
83
|
+
const elementTarget = async () => ({
|
|
84
|
+
elementRef: await resolveToolRef(client, args, requestedTabId),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
switch (args.action) {
|
|
88
|
+
case 'click': {
|
|
89
|
+
const response = await requestBridge(
|
|
90
|
+
client,
|
|
91
|
+
'input.click',
|
|
92
|
+
{
|
|
93
|
+
target: await elementTarget(),
|
|
94
|
+
button: args.button,
|
|
95
|
+
clickCount: args.clickCount,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
tabId: requestedTabId,
|
|
99
|
+
source: REQUEST_SOURCE,
|
|
100
|
+
tokenBudget: getToolTokenBudget(args),
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
return summarizeToolResponse(response, 'input.click');
|
|
104
|
+
}
|
|
105
|
+
case 'focus': {
|
|
106
|
+
const response = await requestBridge(
|
|
107
|
+
client,
|
|
108
|
+
'input.focus',
|
|
109
|
+
{
|
|
110
|
+
target: await elementTarget(),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
tabId: requestedTabId,
|
|
114
|
+
source: REQUEST_SOURCE,
|
|
115
|
+
tokenBudget: getToolTokenBudget(args),
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
return summarizeToolResponse(response, 'input.focus');
|
|
119
|
+
}
|
|
120
|
+
case 'type': {
|
|
121
|
+
const response = await requestBridge(
|
|
122
|
+
client,
|
|
123
|
+
'input.type',
|
|
124
|
+
{
|
|
125
|
+
target: await elementTarget(),
|
|
126
|
+
text: args.text,
|
|
127
|
+
clear: args.clear,
|
|
128
|
+
submit: args.submit,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
tabId: requestedTabId,
|
|
132
|
+
source: REQUEST_SOURCE,
|
|
133
|
+
tokenBudget: getToolTokenBudget(args),
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
return summarizeToolResponse(response, 'input.type');
|
|
137
|
+
}
|
|
138
|
+
case 'press_key': {
|
|
139
|
+
const target = args.elementRef || args.selector ? await elementTarget() : undefined;
|
|
140
|
+
const response = await requestBridge(
|
|
141
|
+
client,
|
|
142
|
+
'input.press_key',
|
|
143
|
+
{
|
|
144
|
+
target,
|
|
145
|
+
key: args.key,
|
|
146
|
+
modifiers: args.modifiers,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
tabId: requestedTabId,
|
|
150
|
+
source: REQUEST_SOURCE,
|
|
151
|
+
tokenBudget: getToolTokenBudget(args),
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
return summarizeToolResponse(response, 'input.press_key');
|
|
155
|
+
}
|
|
156
|
+
case 'cdp_press_key': {
|
|
157
|
+
const response = await requestBridge(
|
|
158
|
+
client,
|
|
159
|
+
'cdp.dispatch_key_event',
|
|
160
|
+
{
|
|
161
|
+
key: args.key,
|
|
162
|
+
code: args.code,
|
|
163
|
+
modifiers: args.modifiers,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
tabId: requestedTabId,
|
|
167
|
+
source: REQUEST_SOURCE,
|
|
168
|
+
tokenBudget: getToolTokenBudget(args),
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
return summarizeToolResponse(response, 'cdp.dispatch_key_event');
|
|
172
|
+
}
|
|
173
|
+
case 'set_checked': {
|
|
174
|
+
const response = await requestBridge(
|
|
175
|
+
client,
|
|
176
|
+
'input.set_checked',
|
|
177
|
+
{
|
|
178
|
+
target: await elementTarget(),
|
|
179
|
+
checked: args.checked,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
tabId: requestedTabId,
|
|
183
|
+
source: REQUEST_SOURCE,
|
|
184
|
+
tokenBudget: getToolTokenBudget(args),
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
return summarizeToolResponse(response, 'input.set_checked');
|
|
188
|
+
}
|
|
189
|
+
case 'select_option': {
|
|
190
|
+
const response = await requestBridge(
|
|
191
|
+
client,
|
|
192
|
+
'input.select_option',
|
|
193
|
+
{
|
|
194
|
+
target: await elementTarget(),
|
|
195
|
+
values: args.values,
|
|
196
|
+
labels: args.labels,
|
|
197
|
+
indexes: args.indexes,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
tabId: requestedTabId,
|
|
201
|
+
source: REQUEST_SOURCE,
|
|
202
|
+
tokenBudget: getToolTokenBudget(args),
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
return summarizeToolResponse(response, 'input.select_option');
|
|
206
|
+
}
|
|
207
|
+
case 'hover': {
|
|
208
|
+
const response = await requestBridge(
|
|
209
|
+
client,
|
|
210
|
+
'input.hover',
|
|
211
|
+
{
|
|
212
|
+
target: await elementTarget(),
|
|
213
|
+
duration: args.duration,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
tabId: requestedTabId,
|
|
217
|
+
source: REQUEST_SOURCE,
|
|
218
|
+
tokenBudget: getToolTokenBudget(args),
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
return summarizeToolResponse(response, 'input.hover');
|
|
222
|
+
}
|
|
223
|
+
case 'drag': {
|
|
224
|
+
const source = {
|
|
225
|
+
elementRef:
|
|
226
|
+
args.sourceElementRef ||
|
|
227
|
+
(args.sourceSelector
|
|
228
|
+
? await resolveRef(client, args.sourceSelector, requestedTabId, REQUEST_SOURCE)
|
|
229
|
+
: ''),
|
|
230
|
+
};
|
|
231
|
+
const destination = {
|
|
232
|
+
elementRef:
|
|
233
|
+
args.destinationElementRef ||
|
|
234
|
+
(args.destinationSelector
|
|
235
|
+
? await resolveRef(client, args.destinationSelector, requestedTabId, REQUEST_SOURCE)
|
|
236
|
+
: ''),
|
|
237
|
+
};
|
|
238
|
+
if (!source.elementRef || !destination.elementRef) {
|
|
239
|
+
return summarizeToolError(
|
|
240
|
+
'sourceElementRef/sourceSelector and destinationElementRef/destinationSelector are required for drag.'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const response = await requestBridge(
|
|
244
|
+
client,
|
|
245
|
+
'input.drag',
|
|
246
|
+
{
|
|
247
|
+
source,
|
|
248
|
+
destination,
|
|
249
|
+
offsetX: args.offsetX,
|
|
250
|
+
offsetY: args.offsetY,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
tabId: requestedTabId,
|
|
254
|
+
source: REQUEST_SOURCE,
|
|
255
|
+
tokenBudget: getToolTokenBudget(args),
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
return summarizeToolResponse(response, 'input.drag');
|
|
259
|
+
}
|
|
260
|
+
case 'scroll_into_view': {
|
|
261
|
+
const response = await requestBridge(
|
|
262
|
+
client,
|
|
263
|
+
'input.scroll_into_view',
|
|
264
|
+
{
|
|
265
|
+
target: await elementTarget(),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
tabId: requestedTabId,
|
|
269
|
+
source: REQUEST_SOURCE,
|
|
270
|
+
tokenBudget: getToolTokenBudget(args),
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
return summarizeToolResponse(response, 'input.scroll_into_view');
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
return summarizeToolError(`Unsupported input action "${args.action}".`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
verify: a.verify,
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
apply_dom: {
|
|
155
|
+
ref: true,
|
|
156
|
+
method: 'patch.apply_dom',
|
|
157
|
+
params: (a, r) => {
|
|
158
|
+
const operation = typeof a.operation === 'string' ? a.operation : '';
|
|
159
|
+
/** @type {Record<string, string>} */
|
|
160
|
+
const opMap = {
|
|
161
|
+
setAttribute: 'set_attribute',
|
|
162
|
+
removeAttribute: 'remove_attribute',
|
|
163
|
+
addClass: 'toggle_class',
|
|
164
|
+
removeClass: 'toggle_class',
|
|
165
|
+
setTextContent: 'set_text',
|
|
166
|
+
setProperty: 'set_attribute',
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
target: { elementRef: r },
|
|
170
|
+
operation: opMap[operation] || operation,
|
|
171
|
+
value: a.value,
|
|
172
|
+
name: a.name,
|
|
173
|
+
verify: a.verify,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
list: { ref: false, method: 'patch.list', params: () => ({}) },
|
|
178
|
+
rollback: {
|
|
179
|
+
ref: false,
|
|
180
|
+
method: 'patch.rollback',
|
|
181
|
+
params: (a) => ({ patchId: a.patchId }),
|
|
182
|
+
},
|
|
183
|
+
commit_baseline: {
|
|
184
|
+
ref: false,
|
|
185
|
+
method: 'patch.commit_session_baseline',
|
|
186
|
+
params: () => ({}),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @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
|
|
192
|
+
* @returns {Promise<ToolResult>}
|
|
193
|
+
*/
|
|
194
|
+
export async function handlePatchTool(args) {
|
|
195
|
+
return dispatchToolAction(PATCH_ACTIONS, args, 'patch');
|
|
196
|
+
}
|