cdp-toolkit 1.0.0 → 1.1.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/CONTRACT.md +1 -1
- package/README.md +34 -0
- package/dist/cli.js +3156 -1290
- package/dist/index.js +1116 -927
- package/dist/mcp.js +4472 -2626
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +823 -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 +72 -14
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +14 -0
- package/src/index.ts +49 -41
- package/src/mcp.ts +59 -9
- package/src/shared-tools.ts +420 -0
- 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/screenshot.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* screenshot tool — `take_screenshot` over raw CDP.
|
|
3
|
-
*
|
|
4
|
-
* Captures the viewport, the full scrollable page, or a single element via
|
|
5
|
-
* Page.captureScreenshot. Full-page support computes the content size from
|
|
6
|
-
* Page.getLayoutMetrics and passes an explicit clip + captureBeyondViewport so
|
|
7
|
-
* we don't depend on Headless-only conveniences (Arc is headful). Element
|
|
8
|
-
* capture resolves the node (uid -> backendNodeId, or CSS selector) and clips
|
|
9
|
-
* to its box. Artifacts are written under ARTIFACT_DIR; the base64 bytes are
|
|
10
|
-
* only returned when explicitly requested.
|
|
11
|
-
*/
|
|
12
|
-
import { writeFile, mkdir } from "node:fs/promises";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import type { CdpConnection } from "../client.ts";
|
|
15
|
-
import { CdpError, withPage } from "../client.ts";
|
|
16
|
-
import type { Target, TargetSelector, Uid } from "../types.ts";
|
|
17
|
-
|
|
18
|
-
const ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
19
|
-
|
|
20
|
-
export interface ScreenshotArgs {
|
|
21
|
-
target?: TargetSelector;
|
|
22
|
-
format?: "png" | "jpeg";
|
|
23
|
-
/** JPEG/quality 0-100. Ignored for png. */
|
|
24
|
-
quality?: number;
|
|
25
|
-
/** Capture the full scrollable content height, not just the viewport. */
|
|
26
|
-
fullPage?: boolean;
|
|
27
|
-
/** Element to clip to (a backendDOMNodeId). Mutually exclusive with selector. */
|
|
28
|
-
uid?: Uid;
|
|
29
|
-
/** CSS selector to clip to. Mutually exclusive with uid. */
|
|
30
|
-
selector?: string;
|
|
31
|
-
/** Override the artifact path. Default: ARTIFACT_DIR/<stamp>.<ext>. */
|
|
32
|
-
savePath?: string;
|
|
33
|
-
/** Also return the raw base64 image in the result. */
|
|
34
|
-
returnBase64?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ScreenshotResult {
|
|
38
|
-
path: string;
|
|
39
|
-
bytes: number;
|
|
40
|
-
format: "png" | "jpeg";
|
|
41
|
-
target: { id: string; url: string; title: string };
|
|
42
|
-
base64?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface Viewport {
|
|
46
|
-
x: number;
|
|
47
|
-
y: number;
|
|
48
|
-
width: number;
|
|
49
|
-
height: number;
|
|
50
|
-
scale: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface LayoutMetrics {
|
|
54
|
-
cssContentSize?: { x: number; y: number; width: number; height: number };
|
|
55
|
-
contentSize?: { x: number; y: number; width: number; height: number };
|
|
56
|
-
cssLayoutViewport?: { clientWidth: number; clientHeight: number };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface BoxModel {
|
|
60
|
-
model: {
|
|
61
|
-
/** content quad: [x1,y1,x2,y2,x3,y3,x4,y4] (top-left, top-right, bottom-right, bottom-left). */
|
|
62
|
-
content: number[];
|
|
63
|
-
width: number;
|
|
64
|
-
height: number;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function shortId(id: string): string {
|
|
69
|
-
return id.slice(0, 8);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function stamp(): string {
|
|
73
|
-
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Compute a full-content clip from layout metrics (CSS pixels). */
|
|
77
|
-
async function fullPageClip(conn: CdpConnection): Promise<Viewport> {
|
|
78
|
-
const m = await conn.send<LayoutMetrics>("Page.getLayoutMetrics");
|
|
79
|
-
const size = m.cssContentSize ?? m.contentSize;
|
|
80
|
-
if (!size) throw new CdpError("Page.getLayoutMetrics returned no content size");
|
|
81
|
-
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height), scale: 1 };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Compute an element clip from its box model (uid or CSS selector). */
|
|
85
|
-
async function elementClip(conn: CdpConnection, uid: Uid | undefined, selector: string | undefined): Promise<Viewport> {
|
|
86
|
-
let backendNodeId: number;
|
|
87
|
-
if (uid != null) {
|
|
88
|
-
backendNodeId = uid;
|
|
89
|
-
} else if (selector) {
|
|
90
|
-
const { root } = await conn.send<{ root: { nodeId: number } }>("DOM.getDocument", { depth: 0 });
|
|
91
|
-
const { nodeId } = await conn.send<{ nodeId: number }>("DOM.querySelector", {
|
|
92
|
-
nodeId: root.nodeId,
|
|
93
|
-
selector,
|
|
94
|
-
});
|
|
95
|
-
if (!nodeId) throw new CdpError(`selector matched no element: ${selector}`);
|
|
96
|
-
const desc = await conn.send<{ node: { backendNodeId: number } }>("DOM.describeNode", { nodeId });
|
|
97
|
-
backendNodeId = desc.node.backendNodeId;
|
|
98
|
-
} else {
|
|
99
|
-
throw new CdpError("elementClip requires uid or selector");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Scroll the element into view so the clip is within the captured region.
|
|
103
|
-
try {
|
|
104
|
-
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
|
|
105
|
-
} catch {
|
|
106
|
-
/* not all builds expose this; box-model coordinates are absolute regardless */
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const box = await conn.send<BoxModel>("DOM.getBoxModel", { backendNodeId });
|
|
110
|
-
const q = box.model.content;
|
|
111
|
-
const q0 = q[0];
|
|
112
|
-
const q1 = q[1];
|
|
113
|
-
if (q0 == null || q1 == null) throw new CdpError("DOM.getBoxModel returned an empty content quad");
|
|
114
|
-
const xs = [q[0], q[2], q[4], q[6]].filter((n): n is number => n != null);
|
|
115
|
-
const ys = [q[1], q[3], q[5], q[7]].filter((n): n is number => n != null);
|
|
116
|
-
const minX = Math.min(...xs);
|
|
117
|
-
const minY = Math.min(...ys);
|
|
118
|
-
const maxX = Math.max(...xs);
|
|
119
|
-
const maxY = Math.max(...ys);
|
|
120
|
-
const width = Math.ceil(maxX - minX);
|
|
121
|
-
const height = Math.ceil(maxY - minY);
|
|
122
|
-
if (width <= 0 || height <= 0) throw new CdpError("resolved element has zero area");
|
|
123
|
-
return { x: minX, y: minY, width, height, scale: 1 };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* take_screenshot — capture viewport / full page / element to a file.
|
|
128
|
-
* Returns { path, bytes, format } (+ base64 when requested).
|
|
129
|
-
*/
|
|
130
|
-
export async function takeScreenshot(args: ScreenshotArgs = {}): Promise<ScreenshotResult> {
|
|
131
|
-
const { target, fullPage, uid, selector, savePath, returnBase64 } = args;
|
|
132
|
-
if (uid != null && selector) throw new CdpError("provide exactly one of uid or selector, not both");
|
|
133
|
-
const format: "png" | "jpeg" = args.format ?? "png";
|
|
134
|
-
const quality = format === "jpeg" ? (args.quality ?? 80) : undefined;
|
|
135
|
-
|
|
136
|
-
return withPage(target, async (conn: CdpConnection, t: Target) => {
|
|
137
|
-
await conn.send("Page.enable");
|
|
138
|
-
|
|
139
|
-
const params: Record<string, unknown> = { format, captureBeyondViewport: true };
|
|
140
|
-
if (quality != null) params.quality = quality;
|
|
141
|
-
|
|
142
|
-
if (uid != null || selector) {
|
|
143
|
-
params.clip = await elementClip(conn, uid, selector);
|
|
144
|
-
} else if (fullPage) {
|
|
145
|
-
params.clip = await fullPageClip(conn);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { data } = await conn.send<{ data: string }>("Page.captureScreenshot", params);
|
|
149
|
-
const buf = Buffer.from(data, "base64");
|
|
150
|
-
if (buf.byteLength === 0) throw new CdpError("Page.captureScreenshot returned empty data");
|
|
151
|
-
|
|
152
|
-
await mkdir(ARTIFACT_DIR, { recursive: true });
|
|
153
|
-
const ext = format === "jpeg" ? "jpg" : "png";
|
|
154
|
-
const path = savePath ?? join(ARTIFACT_DIR, `screenshot-${shortId(t.id)}-${stamp()}.${ext}`);
|
|
155
|
-
await writeFile(path, buf);
|
|
156
|
-
|
|
157
|
-
const result: ScreenshotResult = {
|
|
158
|
-
path,
|
|
159
|
-
bytes: buf.byteLength,
|
|
160
|
-
format,
|
|
161
|
-
target: { id: t.id, url: t.url, title: t.title },
|
|
162
|
-
};
|
|
163
|
-
if (returnBase64) result.base64 = data;
|
|
164
|
-
return result;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/*
|
|
169
|
-
* CDP methods/domains used:
|
|
170
|
-
* - Page.enable
|
|
171
|
-
* - Page.captureScreenshot { format, quality, clip, captureBeyondViewport }
|
|
172
|
-
* - Page.getLayoutMetrics (full-page content clip)
|
|
173
|
-
* - DOM.getDocument / DOM.querySelector / DOM.describeNode (selector -> backendNodeId)
|
|
174
|
-
* - DOM.scrollIntoViewIfNeeded (best-effort, ignored if unsupported)
|
|
175
|
-
* - DOM.getBoxModel (element clip)
|
|
176
|
-
* Parity gaps vs chrome-devtools-mcp take_screenshot:
|
|
177
|
-
* - MCP refs are its own snapshot uids; here uid IS a backendDOMNodeId (toolkit ref scheme).
|
|
178
|
-
* - No device-pixel upscaling control beyond emulation-set deviceScaleFactor (clip scale fixed at 1).
|
|
179
|
-
* - Full-page on headful Arc relies on captureBeyondViewport + layout-metrics clip rather than headless auto-sizing.
|
|
180
|
-
*/
|