cdp-toolkit 1.0.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/CHANGELOG.md +76 -9
- package/CONTRACT.md +7 -5
- package/README.md +99 -28
- package/dist/cli.js +3248 -1033
- package/dist/index.js +1418 -898
- package/dist/mcp.js +3447 -1064
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +851 -0
- package/src/bidi/launch.ts +192 -0
- package/src/bidi/protocol.ts +782 -0
- package/src/bidi/snapshot.ts +419 -0
- package/src/bidi-tools.ts +193 -0
- package/src/capabilities.ts +43 -0
- package/src/cdp/driver.ts +660 -0
- package/src/cli.ts +97 -14
- package/src/client.ts +25 -7
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +17 -0
- package/src/index.ts +55 -42
- package/src/leases-tools.ts +101 -0
- package/src/leases.ts +533 -0
- package/src/manifest.ts +211 -13
- package/src/mcp.ts +73 -15
- package/src/shared-tools.ts +466 -0
- package/src/tools/console.ts +3 -3
- package/src/tools/evaluate.ts +2 -2
- package/src/tools/heap.ts +3 -3
- package/src/tools/lighthouse.ts +4 -4
- package/src/tools/navigation.ts +1 -1
- package/src/tools/network.ts +7 -7
- package/src/tools/network_mock.ts +8 -8
- package/src/tools/performance.ts +11 -11
- package/src/tools/recorder.ts +5 -5
- package/src/tools/snapshot.ts +2 -2
- package/src/tools/dialogs.ts +0 -236
- package/src/tools/emulation.ts +0 -212
- package/src/tools/input.ts +0 -399
- package/src/tools/pages.ts +0 -159
- package/src/tools/screenshot.ts +0 -180
package/src/tools/input.ts
DELETED
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interaction tools: click, hover, drag, fill, fill_form, type_text,
|
|
3
|
-
* press_key, upload_file.
|
|
4
|
-
*
|
|
5
|
-
* Every element-targeting tool accepts exactly one of `{ uid }` (a
|
|
6
|
-
* backendDOMNodeId from take_snapshot) or `{ selector }` (a CSS selector).
|
|
7
|
-
* uid resolves via resolveUid (DOM.resolveNode); selector resolves via
|
|
8
|
-
* Runtime.evaluate(document.querySelector). Both end up as a JS object handle
|
|
9
|
-
* (objectId), then Runtime.callFunctionOn scrolls it into view + reads its
|
|
10
|
-
* bounding-rect center, and we dispatch real input events via
|
|
11
|
-
* Input.dispatchMouseEvent / Input.dispatchKeyEvent. This drives the page the
|
|
12
|
-
* same way a user would, so framework event handlers fire correctly.
|
|
13
|
-
*/
|
|
14
|
-
import type { CdpConnection } from "../client.ts";
|
|
15
|
-
import { CdpError, withPage } from "../client.ts";
|
|
16
|
-
import type { TargetSelector, Uid } from "../types.ts";
|
|
17
|
-
import { resolveUid } from "./snapshot.ts";
|
|
18
|
-
|
|
19
|
-
/* --------------------------------- targeting --------------------------------- */
|
|
20
|
-
|
|
21
|
-
interface ElementTarget {
|
|
22
|
-
uid?: Uid;
|
|
23
|
-
selector?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Resolve {uid|selector} (exactly one) to a JS object handle on the page. */
|
|
27
|
-
async function resolveElement(conn: CdpConnection, t: ElementTarget): Promise<{ objectId: string }> {
|
|
28
|
-
const hasUid = t.uid !== undefined && t.uid !== null;
|
|
29
|
-
const hasSelector = typeof t.selector === "string" && t.selector.length > 0;
|
|
30
|
-
if (hasUid === hasSelector) {
|
|
31
|
-
throw new CdpError("provide exactly one of { uid } or { selector }");
|
|
32
|
-
}
|
|
33
|
-
if (hasUid) {
|
|
34
|
-
return resolveUid(conn, t.uid as Uid);
|
|
35
|
-
}
|
|
36
|
-
// selector path
|
|
37
|
-
const { result, exceptionDetails } = await conn.send<{
|
|
38
|
-
result: { objectId?: string; subtype?: string };
|
|
39
|
-
exceptionDetails?: { text?: string };
|
|
40
|
-
}>("Runtime.evaluate", {
|
|
41
|
-
expression: `document.querySelector(${JSON.stringify(t.selector)})`,
|
|
42
|
-
returnByValue: false,
|
|
43
|
-
});
|
|
44
|
-
if (exceptionDetails) {
|
|
45
|
-
throw new CdpError(`selector '${t.selector}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
|
|
46
|
-
}
|
|
47
|
-
if (!result.objectId || result.subtype === "null") {
|
|
48
|
-
throw new CdpError(`selector '${t.selector}' matched no element`);
|
|
49
|
-
}
|
|
50
|
-
return { objectId: result.objectId };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface Point {
|
|
54
|
-
x: number;
|
|
55
|
-
y: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Scroll the element into view and return its viewport-space center point. */
|
|
59
|
-
async function centerOf(conn: CdpConnection, objectId: string): Promise<Point> {
|
|
60
|
-
const { result, exceptionDetails } = await conn.send<{
|
|
61
|
-
result: { value?: Point | null };
|
|
62
|
-
exceptionDetails?: { text?: string };
|
|
63
|
-
}>("Runtime.callFunctionOn", {
|
|
64
|
-
objectId,
|
|
65
|
-
functionDeclaration:
|
|
66
|
-
"function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}",
|
|
67
|
-
returnByValue: true,
|
|
68
|
-
});
|
|
69
|
-
if (exceptionDetails) {
|
|
70
|
-
throw new CdpError(`could not measure element: ${exceptionDetails.text ?? "error"}`);
|
|
71
|
-
}
|
|
72
|
-
if (!result.value) {
|
|
73
|
-
throw new CdpError("element has zero size / is not visible; cannot compute a click point");
|
|
74
|
-
}
|
|
75
|
-
return result.value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Focus an element via its object handle (used by fill / type_text). */
|
|
79
|
-
async function focusElement(conn: CdpConnection, objectId: string): Promise<void> {
|
|
80
|
-
await conn.send("Runtime.callFunctionOn", {
|
|
81
|
-
objectId,
|
|
82
|
-
functionDeclaration: "function(){this.focus&&this.focus();}",
|
|
83
|
-
returnByValue: true,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* ----------------------------------- click ----------------------------------- */
|
|
88
|
-
|
|
89
|
-
export interface ClickArgs extends ElementTarget {
|
|
90
|
-
target?: TargetSelector;
|
|
91
|
-
/** "left" (default) | "right" | "middle". */
|
|
92
|
-
button?: "left" | "right" | "middle";
|
|
93
|
-
/** 1 = single (default), 2 = double-click. */
|
|
94
|
-
clickCount?: number;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function click(args: ClickArgs): Promise<{ clicked: true; x: number; y: number }> {
|
|
98
|
-
const button = args.button ?? "left";
|
|
99
|
-
const clickCount = args.clickCount ?? 1;
|
|
100
|
-
return withPage(args.target, async (conn) => {
|
|
101
|
-
const { objectId } = await resolveElement(conn, args);
|
|
102
|
-
const { x, y } = await centerOf(conn, objectId);
|
|
103
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
104
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
|
|
105
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
|
|
106
|
-
return { clicked: true as const, x, y };
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* ----------------------------------- hover ----------------------------------- */
|
|
111
|
-
|
|
112
|
-
export interface HoverArgs extends ElementTarget {
|
|
113
|
-
target?: TargetSelector;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function hover(args: HoverArgs): Promise<{ hovered: true; x: number; y: number }> {
|
|
117
|
-
return withPage(args.target, async (conn) => {
|
|
118
|
-
const { objectId } = await resolveElement(conn, args);
|
|
119
|
-
const { x, y } = await centerOf(conn, objectId);
|
|
120
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
121
|
-
return { hovered: true as const, x, y };
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* ----------------------------------- drag ------------------------------------ */
|
|
126
|
-
|
|
127
|
-
export interface DragArgs {
|
|
128
|
-
target?: TargetSelector;
|
|
129
|
-
/** Source element (exactly one of from.uid / from.selector). */
|
|
130
|
-
from: ElementTarget;
|
|
131
|
-
/** Destination element (exactly one of to.uid / to.selector). */
|
|
132
|
-
to: ElementTarget;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export async function drag(args: DragArgs): Promise<{ dragged: true; from: Point; to: Point }> {
|
|
136
|
-
if (!args.from || !args.to) throw new CdpError("drag requires { from } and { to }");
|
|
137
|
-
return withPage(args.target, async (conn) => {
|
|
138
|
-
const { objectId: srcId } = await resolveElement(conn, args.from);
|
|
139
|
-
const from = await centerOf(conn, srcId);
|
|
140
|
-
const { objectId: dstId } = await resolveElement(conn, args.to);
|
|
141
|
-
const to = await centerOf(conn, dstId);
|
|
142
|
-
|
|
143
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from.x, y: from.y });
|
|
144
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
145
|
-
type: "mousePressed",
|
|
146
|
-
x: from.x,
|
|
147
|
-
y: from.y,
|
|
148
|
-
button: "left",
|
|
149
|
-
clickCount: 1,
|
|
150
|
-
});
|
|
151
|
-
// Intermediate move so dragstart/dragover fire on the page.
|
|
152
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
153
|
-
type: "mouseMoved",
|
|
154
|
-
x: (from.x + to.x) / 2,
|
|
155
|
-
y: (from.y + to.y) / 2,
|
|
156
|
-
button: "left",
|
|
157
|
-
});
|
|
158
|
-
await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: to.x, y: to.y, button: "left" });
|
|
159
|
-
await conn.send("Input.dispatchMouseEvent", {
|
|
160
|
-
type: "mouseReleased",
|
|
161
|
-
x: to.x,
|
|
162
|
-
y: to.y,
|
|
163
|
-
button: "left",
|
|
164
|
-
clickCount: 1,
|
|
165
|
-
});
|
|
166
|
-
return { dragged: true as const, from, to };
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* ------------------------------ fill / type_text ------------------------------ */
|
|
171
|
-
|
|
172
|
-
/** Set an element's value by clearing it then inserting text (focus + insertText). */
|
|
173
|
-
async function setValue(conn: CdpConnection, objectId: string, value: string): Promise<void> {
|
|
174
|
-
await focusElement(conn, objectId);
|
|
175
|
-
// Clear existing content so fill is an overwrite, then insert the new text.
|
|
176
|
-
await conn.send("Runtime.callFunctionOn", {
|
|
177
|
-
objectId,
|
|
178
|
-
functionDeclaration:
|
|
179
|
-
"function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}",
|
|
180
|
-
returnByValue: true,
|
|
181
|
-
});
|
|
182
|
-
if (value.length) {
|
|
183
|
-
await conn.send("Input.insertText", { text: value });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface FillArgs extends ElementTarget {
|
|
188
|
-
target?: TargetSelector;
|
|
189
|
-
value: string;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function fill(args: FillArgs): Promise<{ filled: true; value: string }> {
|
|
193
|
-
if (typeof args.value !== "string") throw new CdpError("fill requires a string { value }");
|
|
194
|
-
return withPage(args.target, async (conn) => {
|
|
195
|
-
const { objectId } = await resolveElement(conn, args);
|
|
196
|
-
await setValue(conn, objectId, args.value);
|
|
197
|
-
return { filled: true as const, value: args.value };
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface TypeTextArgs extends ElementTarget {
|
|
202
|
-
target?: TargetSelector;
|
|
203
|
-
text: string;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* type_text — focus then insert text. Unlike fill it does NOT clear first, so
|
|
208
|
-
* it appends to whatever is already focused/present (closest to "typing").
|
|
209
|
-
*/
|
|
210
|
-
export async function typeText(args: TypeTextArgs): Promise<{ typed: true; text: string }> {
|
|
211
|
-
if (typeof args.text !== "string") throw new CdpError("type_text requires a string { text }");
|
|
212
|
-
return withPage(args.target, async (conn) => {
|
|
213
|
-
const { objectId } = await resolveElement(conn, args);
|
|
214
|
-
await focusElement(conn, objectId);
|
|
215
|
-
if (args.text.length) await conn.send("Input.insertText", { text: args.text });
|
|
216
|
-
return { typed: true as const, text: args.text };
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/* ---------------------------------- fill_form --------------------------------- */
|
|
221
|
-
|
|
222
|
-
export interface FillFormField extends ElementTarget {
|
|
223
|
-
value: string;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface FillFormArgs {
|
|
227
|
-
target?: TargetSelector;
|
|
228
|
-
fields: FillFormField[];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function fillForm(args: FillFormArgs): Promise<{ filled: number; fields: number }> {
|
|
232
|
-
if (!Array.isArray(args.fields) || args.fields.length === 0) {
|
|
233
|
-
throw new CdpError("fill_form requires a non-empty { fields } array");
|
|
234
|
-
}
|
|
235
|
-
return withPage(args.target, async (conn) => {
|
|
236
|
-
let filled = 0;
|
|
237
|
-
for (const field of args.fields) {
|
|
238
|
-
if (typeof field.value !== "string") {
|
|
239
|
-
throw new CdpError("each fill_form field requires a string value");
|
|
240
|
-
}
|
|
241
|
-
const { objectId } = await resolveElement(conn, field);
|
|
242
|
-
await setValue(conn, objectId, field.value);
|
|
243
|
-
filled++;
|
|
244
|
-
}
|
|
245
|
-
return { filled, fields: args.fields.length };
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/* ---------------------------------- press_key --------------------------------- */
|
|
250
|
-
|
|
251
|
-
interface KeySpec {
|
|
252
|
-
key: string;
|
|
253
|
-
code: string;
|
|
254
|
-
keyCode: number;
|
|
255
|
-
/** text to commit (printable keys / Enter newline) */
|
|
256
|
-
text?: string;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Named-key table: maps friendly names to CDP dispatchKeyEvent fields. */
|
|
260
|
-
const NAMED_KEYS: Record<string, KeySpec> = {
|
|
261
|
-
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
262
|
-
return: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
263
|
-
tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
264
|
-
escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
265
|
-
esc: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
266
|
-
backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
267
|
-
delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
268
|
-
space: { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
269
|
-
arrowup: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
270
|
-
arrowdown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
271
|
-
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
272
|
-
arrowright: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
273
|
-
up: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
274
|
-
down: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
275
|
-
left: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
276
|
-
right: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
277
|
-
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
278
|
-
end: { key: "End", code: "End", keyCode: 35 },
|
|
279
|
-
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
280
|
-
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 },
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const MODIFIER_BITS: Record<string, number> = {
|
|
284
|
-
alt: 1,
|
|
285
|
-
control: 2,
|
|
286
|
-
ctrl: 2,
|
|
287
|
-
meta: 4,
|
|
288
|
-
cmd: 4,
|
|
289
|
-
command: 4,
|
|
290
|
-
shift: 8,
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
/** Resolve a key name (named or single printable char) to a KeySpec. */
|
|
294
|
-
function resolveKey(key: string): KeySpec {
|
|
295
|
-
const named = NAMED_KEYS[key.toLowerCase()];
|
|
296
|
-
if (named) return named;
|
|
297
|
-
if (key.length === 1) {
|
|
298
|
-
const upper = key.toUpperCase();
|
|
299
|
-
const isLetter = upper >= "A" && upper <= "Z";
|
|
300
|
-
const isDigit = key >= "0" && key <= "9";
|
|
301
|
-
const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
|
|
302
|
-
const keyCode = upper.charCodeAt(0);
|
|
303
|
-
return { key, code, keyCode, text: key };
|
|
304
|
-
}
|
|
305
|
-
throw new CdpError(`unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export interface PressKeyArgs {
|
|
309
|
-
target?: TargetSelector;
|
|
310
|
-
/** Key name: "Enter", "Tab", "a", "ArrowDown", etc. */
|
|
311
|
-
key: string;
|
|
312
|
-
/** Modifier names: "Control"/"Ctrl", "Shift", "Alt", "Meta"/"Cmd". */
|
|
313
|
-
modifiers?: string[];
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export async function pressKey(args: PressKeyArgs): Promise<{ pressed: string; modifiers: string[] }> {
|
|
317
|
-
if (typeof args.key !== "string" || !args.key.length) {
|
|
318
|
-
throw new CdpError("press_key requires a non-empty { key }");
|
|
319
|
-
}
|
|
320
|
-
const spec = resolveKey(args.key);
|
|
321
|
-
let modBits = 0;
|
|
322
|
-
const mods = args.modifiers ?? [];
|
|
323
|
-
for (const m of mods) {
|
|
324
|
-
const bit = MODIFIER_BITS[m.toLowerCase()];
|
|
325
|
-
if (bit === undefined) throw new CdpError(`unknown modifier '${m}'`);
|
|
326
|
-
modBits |= bit;
|
|
327
|
-
}
|
|
328
|
-
// When a modifier other than Shift is held, suppress text (it's a chord).
|
|
329
|
-
const suppressText = (modBits & ~MODIFIER_BITS.shift!) !== 0;
|
|
330
|
-
|
|
331
|
-
return withPage(args.target, async (conn) => {
|
|
332
|
-
const down: Record<string, unknown> = {
|
|
333
|
-
type: "keyDown",
|
|
334
|
-
key: spec.key,
|
|
335
|
-
code: spec.code,
|
|
336
|
-
windowsVirtualKeyCode: spec.keyCode,
|
|
337
|
-
nativeVirtualKeyCode: spec.keyCode,
|
|
338
|
-
modifiers: modBits,
|
|
339
|
-
};
|
|
340
|
-
if (spec.text && !suppressText) {
|
|
341
|
-
// A printable char dispatches as keyDown-with-text, which commits the
|
|
342
|
-
// character (verified: x/y/space produce value "xy "). `type` is already
|
|
343
|
-
// "keyDown" from construction above.
|
|
344
|
-
down.text = spec.text;
|
|
345
|
-
}
|
|
346
|
-
await conn.send("Input.dispatchKeyEvent", down);
|
|
347
|
-
await conn.send("Input.dispatchKeyEvent", {
|
|
348
|
-
type: "keyUp",
|
|
349
|
-
key: spec.key,
|
|
350
|
-
code: spec.code,
|
|
351
|
-
windowsVirtualKeyCode: spec.keyCode,
|
|
352
|
-
nativeVirtualKeyCode: spec.keyCode,
|
|
353
|
-
modifiers: modBits,
|
|
354
|
-
});
|
|
355
|
-
return { pressed: spec.key, modifiers: mods };
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/* --------------------------------- upload_file -------------------------------- */
|
|
360
|
-
|
|
361
|
-
export interface UploadFileArgs extends ElementTarget {
|
|
362
|
-
target?: TargetSelector;
|
|
363
|
-
/** Absolute path(s) to the file(s) to attach to the <input type=file>. */
|
|
364
|
-
files: string | string[];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export async function uploadFile(args: UploadFileArgs): Promise<{ uploaded: string[] }> {
|
|
368
|
-
const files = Array.isArray(args.files) ? args.files : [args.files];
|
|
369
|
-
if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
|
|
370
|
-
throw new CdpError("upload_file requires { files } as a non-empty path or array of paths");
|
|
371
|
-
}
|
|
372
|
-
return withPage(args.target, async (conn) => {
|
|
373
|
-
const { objectId } = await resolveElement(conn, args);
|
|
374
|
-
// setFileInputFiles needs a backendNodeId/objectId reference to the <input>.
|
|
375
|
-
await conn.send("DOM.setFileInputFiles", { files, objectId });
|
|
376
|
-
return { uploaded: files };
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/* ------------------------------------------------------------------------------
|
|
381
|
-
* CDP methods used:
|
|
382
|
-
* DOM.resolveNode (via resolveUid), Runtime.evaluate (selector resolution),
|
|
383
|
-
* Runtime.callFunctionOn (scrollIntoView + rect center, focus, clear value),
|
|
384
|
-
* Input.dispatchMouseEvent (click/hover/drag),
|
|
385
|
-
* Input.insertText (fill/type_text/fill_form),
|
|
386
|
-
* Input.dispatchKeyEvent (press_key, named keys + modifiers),
|
|
387
|
-
* DOM.setFileInputFiles (upload_file).
|
|
388
|
-
* Parity gaps vs chrome-devtools-mcp interaction tools:
|
|
389
|
-
* - press_key supports a curated named-key table + single chars; it is not the
|
|
390
|
-
* full Puppeteer KeyInput enum (function keys F1-F12, numpad, IME, etc.).
|
|
391
|
-
* - fill/type_text use Input.insertText (atomic paste-like commit) rather than
|
|
392
|
-
* per-character keystrokes, so per-char keydown handlers / input masks that
|
|
393
|
-
* depend on individual keystrokes won't see each character.
|
|
394
|
-
* - drag is a synthetic mouse press/move/release; native HTML5 drag-and-drop
|
|
395
|
-
* (DataTransfer) is approximated, matching CDP's mouse-driven model but not
|
|
396
|
-
* guaranteed for every custom DnD library.
|
|
397
|
-
* - no implicit auto-wait/retry: tools resolve and act once. Callers re-snapshot
|
|
398
|
-
* between steps (MCP wraps each action in a Puppeteer locator auto-wait).
|
|
399
|
-
* ---------------------------------------------------------------------------- */
|
package/src/tools/pages.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Page-lifecycle tools — operate on the BROWSER endpoint (Target.* domain).
|
|
3
|
-
*
|
|
4
|
-
* These four tools mirror chrome-devtools-mcp's page management:
|
|
5
|
-
* list_pages -> listPages (enumerate page targets)
|
|
6
|
-
* new_page -> newPage (Target.createTarget)
|
|
7
|
-
* close_page -> closePage (Target.closeTarget)
|
|
8
|
-
* select_page -> selectPage (Target.activateTarget + persist selected state)
|
|
9
|
-
*
|
|
10
|
-
* Unlike most modules these do NOT use withPage/openPage (which attach to a
|
|
11
|
-
* single page target's WebSocket). They use openBrowser() to reach the
|
|
12
|
-
* browser-level Target.* domain so we can create/close/activate any target.
|
|
13
|
-
*
|
|
14
|
-
* select_page persists the chosen targetId to the selected-state file
|
|
15
|
-
* (CDP_STATE_DIR/selected) per CONTRACT.md so other tools can treat it as the
|
|
16
|
-
* default active page if they choose.
|
|
17
|
-
*/
|
|
18
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
19
|
-
import { dirname } from "node:path";
|
|
20
|
-
import { CdpError, listTargets, openBrowser, resolveTarget } from "../client.ts";
|
|
21
|
-
import type { Target, TargetSelector } from "../types.ts";
|
|
22
|
-
|
|
23
|
-
/** Selected-target state file: a bare targetId written by select_page. */
|
|
24
|
-
const STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
25
|
-
const SELECTED_FILE = `${STATE_DIR}/selected`;
|
|
26
|
-
|
|
27
|
-
/** Compact page descriptor returned to callers. */
|
|
28
|
-
interface PageInfo {
|
|
29
|
-
id: string;
|
|
30
|
-
url: string;
|
|
31
|
-
title: string;
|
|
32
|
-
type: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function toPageInfo(t: Target): PageInfo {
|
|
36
|
-
return { id: t.id, url: t.url, title: t.title, type: t.type };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/* --------------------------------- list_pages -------------------------------- */
|
|
40
|
-
|
|
41
|
-
export interface ListPagesArgs {
|
|
42
|
-
/** include non-page targets (workers, background pages) when true */
|
|
43
|
-
all?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ListPagesResult {
|
|
47
|
-
pages: PageInfo[];
|
|
48
|
-
count: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Enumerate page targets via GET /json/list. */
|
|
52
|
-
export async function listPages(args: ListPagesArgs = {}): Promise<ListPagesResult> {
|
|
53
|
-
const targets = await listTargets();
|
|
54
|
-
const filtered = args.all ? targets : targets.filter((t) => t.type === "page");
|
|
55
|
-
const pages = filtered.map(toPageInfo);
|
|
56
|
-
return { pages, count: pages.length };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* ---------------------------------- new_page --------------------------------- */
|
|
60
|
-
|
|
61
|
-
export interface NewPageArgs {
|
|
62
|
-
/** URL to open in the new tab; defaults to about:blank */
|
|
63
|
-
url?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface NewPageResult {
|
|
67
|
-
targetId: string;
|
|
68
|
-
url: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Create a new page target (Target.createTarget). */
|
|
72
|
-
export async function newPage(args: NewPageArgs = {}): Promise<NewPageResult> {
|
|
73
|
-
const url = args.url ?? "about:blank";
|
|
74
|
-
const conn = await openBrowser();
|
|
75
|
-
try {
|
|
76
|
-
const { targetId } = await conn.send<{ targetId: string }>("Target.createTarget", { url });
|
|
77
|
-
if (!targetId) throw new CdpError("Target.createTarget returned no targetId");
|
|
78
|
-
return { targetId, url };
|
|
79
|
-
} finally {
|
|
80
|
-
conn.close();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* --------------------------------- close_page -------------------------------- */
|
|
85
|
-
|
|
86
|
-
export interface ClosePageArgs {
|
|
87
|
-
target: TargetSelector;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface ClosePageResult {
|
|
91
|
-
closed: string;
|
|
92
|
-
success: boolean;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Close a page target (Target.closeTarget). Refuses with a clear error if the
|
|
97
|
-
* selector cannot be resolved to a concrete target.
|
|
98
|
-
*/
|
|
99
|
-
export async function closePage(args: ClosePageArgs): Promise<ClosePageResult> {
|
|
100
|
-
if (args.target === undefined || args.target === "") {
|
|
101
|
-
throw new CdpError("close_page requires an explicit target; refusing to guess which page to close");
|
|
102
|
-
}
|
|
103
|
-
// resolveTarget throws a descriptive CdpError if the selector matches nothing.
|
|
104
|
-
const target = await resolveTarget(args.target);
|
|
105
|
-
const conn = await openBrowser();
|
|
106
|
-
try {
|
|
107
|
-
const res = await conn.send<{ success?: boolean }>("Target.closeTarget", { targetId: target.id });
|
|
108
|
-
// Target.closeTarget returns { success: boolean } on older builds; newer
|
|
109
|
-
// builds may return {}. Absence of an error means the close was accepted.
|
|
110
|
-
return { closed: target.id, success: res.success ?? true };
|
|
111
|
-
} finally {
|
|
112
|
-
conn.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* -------------------------------- select_page -------------------------------- */
|
|
117
|
-
|
|
118
|
-
export interface SelectPageArgs {
|
|
119
|
-
target: TargetSelector;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface SelectPageResult {
|
|
123
|
-
selected: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Activate (focus) a page target (Target.activateTarget) and persist its bare
|
|
128
|
-
* targetId to the selected-state file so other tools can default to it.
|
|
129
|
-
*/
|
|
130
|
-
export async function selectPage(args: SelectPageArgs): Promise<SelectPageResult> {
|
|
131
|
-
if (args.target === undefined || args.target === "") {
|
|
132
|
-
throw new CdpError("select_page requires an explicit target");
|
|
133
|
-
}
|
|
134
|
-
const target = await resolveTarget(args.target);
|
|
135
|
-
const conn = await openBrowser();
|
|
136
|
-
try {
|
|
137
|
-
await conn.send("Target.activateTarget", { targetId: target.id });
|
|
138
|
-
} finally {
|
|
139
|
-
conn.close();
|
|
140
|
-
}
|
|
141
|
-
await mkdir(dirname(SELECTED_FILE), { recursive: true });
|
|
142
|
-
await writeFile(SELECTED_FILE, target.id, "utf8");
|
|
143
|
-
return { selected: target.id };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/*
|
|
147
|
-
* CDP methods/domains used:
|
|
148
|
-
* - GET /json/list (via listTargets) — list_pages
|
|
149
|
-
* - Target.createTarget — new_page
|
|
150
|
-
* - Target.closeTarget — close_page
|
|
151
|
-
* - Target.activateTarget — select_page
|
|
152
|
-
* - resolveTarget() — selector resolution for close/select
|
|
153
|
-
*
|
|
154
|
-
* Parity gaps vs chrome-devtools-mcp:
|
|
155
|
-
* - new_page returns {targetId,url}; MCP returns the page's eventual title/URL after load. We do not await navigation here (navigate_page covers that).
|
|
156
|
-
* - select_page writes a flat-file selected target; the MCP keeps an in-process active-page handle. resolveTarget does NOT read this file, so "active" still means index:0 unless a tool opts in.
|
|
157
|
-
* - close_page reports success:true when newer Chromium returns an empty result (no explicit success field); it relies on the absence of a CDP error rather than the legacy boolean.
|
|
158
|
-
* - list_pages "all" flag exposes worker/background targets; the MCP only lists page-type tabs.
|
|
159
|
-
*/
|