cdp-toolkit 1.9.0 → 1.9.4
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 +507 -0
- package/CONTRACT.md +62 -5
- package/README.md +74 -10
- package/dist/cli.js +2940 -138
- package/dist/index.js +994 -90
- package/dist/mcp.js +1221 -137
- package/package.json +2 -1
- package/src/backend.ts +156 -21
- package/src/bidi/client.ts +48 -10
- package/src/bidi/driver.ts +228 -33
- package/src/bidi/launch.ts +23 -13
- package/src/bidi-tools.ts +21 -0
- package/src/cdp/driver.ts +594 -9
- package/src/cli.ts +111 -12
- package/src/driver.ts +254 -3
- package/src/manifest.ts +42 -14
- package/src/mcp.ts +21 -12
- package/src/png.ts +809 -0
- package/src/shared-tools.ts +313 -11
- package/src/tools/console.ts +24 -7
- package/src/tools/network.ts +21 -7
- package/src/tools/recorder.ts +110 -10
- package/src/types.ts +8 -6
- package/src/workers.ts +69 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,512 @@ All notable changes to cdp-toolkit are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.4] - 2026-08-11
|
|
9
|
+
|
|
10
|
+
Firefox gains a second mode: **attach**. Until now `--browser firefox` always spawned a
|
|
11
|
+
throwaway-profile Firefox — a login wall for anything needing a real, already-authenticated
|
|
12
|
+
session. `--connect <port|host:port|ws-url>` (or env `CDP_FIREFOX_ENDPOINT`) instead connects to
|
|
13
|
+
a Firefox **you** already started with `--remote-debugging-port`, so tools see your real logged-in
|
|
14
|
+
profile. This process never launches or kills that browser: dispose only ends the BiDi session
|
|
15
|
+
(`session.end`), never the process. Also fixes a doc-nit (README/CONTRACT overclaimed "Firefox
|
|
16
|
+
cannot be attached to at all" — the real, narrower limitation is that you cannot relaunch the
|
|
17
|
+
`firefox` binary with the debug flag against an already-running instance) and a real hang (a
|
|
18
|
+
second attach against a Firefox that already has an active BiDi session used to hang forever;
|
|
19
|
+
it now fails fast with an actionable error).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`--connect <endpoint>` / `CDP_FIREFOX_ENDPOINT`** — Firefox attach mode. `<endpoint>` accepts
|
|
24
|
+
three spellings, all normalized to a ws URL: a bare port (`9223`), `host:port`
|
|
25
|
+
(`127.0.0.1:9223`), or a full `ws://`/`wss://` URL. Flag beats env, mirroring
|
|
26
|
+
`--browser`/`CDP_BROWSER` precedence. An endpoint **implies** the Firefox backend on its own —
|
|
27
|
+
there is nothing else it could mean — and errors against an explicit `--browser chrome` /
|
|
28
|
+
`CDP_BROWSER=chrome` rather than silently picking one side.
|
|
29
|
+
- `src/backend.ts`: `normalizeBidiEndpoint`, `resolveFirefoxEndpoint`, `stripConnectFlag`,
|
|
30
|
+
`resolveBackend` (the single backend-selection entry point `cli.ts`/`mcp.ts` now both call —
|
|
31
|
+
returns `{kind, endpoint?}`), and `startFirefoxSession`/`getOrCreateFirefoxSession` now accept
|
|
32
|
+
an `{ endpoint }` to attach instead of launch.
|
|
33
|
+
- `src/bidi/client.ts`: `connectBidiSessionUrl(wsUrl, opts?)` — connects directly to a ws URL,
|
|
34
|
+
no port-to-URL construction. `connectBidiSession(port, opts)` is now a thin wrapper over it
|
|
35
|
+
(kept exported, unchanged behavior).
|
|
36
|
+
- `src/bidi/driver.ts`: `createFirefoxDriverForEndpoint(wsUrl, opts?)` — the attach-mode driver
|
|
37
|
+
constructor. The driver's connection identity moved from a bare `port: number` to a ws
|
|
38
|
+
`endpoint: string`, so an attach endpoint can carry a different host/port/path than a launched
|
|
39
|
+
Firefox's `ws://127.0.0.1:<port>/session`.
|
|
40
|
+
- `firefox:attach:smoke` script (`test/firefox-attach-smoke.ts`) — drives the real attach path
|
|
41
|
+
against an out-of-band-spawned Firefox: attach, read a page, dispose (Firefox stays alive), a
|
|
42
|
+
second attach cycle to the same Firefox (proves `session.end` freed the slot), and confirms
|
|
43
|
+
`resolveBackend` auto-implies `firefox` from `--connect` alone.
|
|
44
|
+
- `test/backend-attach.test.ts` — pure-unit coverage of `normalizeBidiEndpoint`,
|
|
45
|
+
`resolveFirefoxEndpoint`, `stripConnectFlag`, and `resolveBackend`, including the
|
|
46
|
+
`--browser chrome` + `--connect` conflict.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **A second attach against a Firefox with an already-active BiDi session used to hang forever.**
|
|
51
|
+
Firefox allows exactly one active WebDriver BiDi session at a time; a colliding `session.new`
|
|
52
|
+
fails with the raw wire message `Maximum number of active sessions`, which reached no caller
|
|
53
|
+
as an error — it hung. `getConnection` (`src/bidi/driver.ts`) now catches that specific message
|
|
54
|
+
and raises a clear, actionable error naming the endpoint and the fix (close the other client,
|
|
55
|
+
or restart Firefox; a client killed without a clean dispose, e.g. `SIGKILL`, can leave a
|
|
56
|
+
session Firefox does not reap on its own).
|
|
57
|
+
- **Attach-mode dispose was undefined behavior before this release** (the mode did not exist):
|
|
58
|
+
`BidiBrowserDriver.dispose()` now sends `session.end` before closing the socket, which is what
|
|
59
|
+
actually frees Firefox's single session slot for the next attach — closing the socket alone
|
|
60
|
+
does not. Launch-mode dispose is unaffected: the spawned process is still killed after the
|
|
61
|
+
driver disposes, same as before.
|
|
62
|
+
- **`src/bidi/launch.ts` doc-nit** (the issue's explicit ask, code unchanged): the header/footer
|
|
63
|
+
said Firefox "cannot be attached to at all." That overstated a real, narrower fact — you cannot
|
|
64
|
+
*relaunch* the `firefox` binary with `--remote-debugging-port` against an already-running
|
|
65
|
+
instance and have it take effect (the relaunch hands off and exits, opening no port; still true,
|
|
66
|
+
still verified against Firefox 153.0.3). Attaching to a port a Firefox process was *originally*
|
|
67
|
+
launched with — by this toolkit or by hand — always worked and is now this release's first-class
|
|
68
|
+
path. README.md and CONTRACT.md carried the same overclaim and are corrected too.
|
|
69
|
+
|
|
70
|
+
### Notes
|
|
71
|
+
|
|
72
|
+
- Tool count unchanged at **45**; no tool added, renamed, or removed. Both Firefox modes expose
|
|
73
|
+
the same tool set as before (see README's "Tool availability is filtered per backend").
|
|
74
|
+
- The CLI is one process per invocation either way: an attach-mode CLI call still launches no
|
|
75
|
+
process and kills none, it just skips `launchFirefox` entirely. Multi-step attach workflows
|
|
76
|
+
still want the MCP server, same reasoning as launch mode.
|
|
77
|
+
|
|
78
|
+
## [1.9.3] - 2026-08-10
|
|
79
|
+
|
|
80
|
+
`cli.ts` had **no `--help` handling at all.** `--help` fell through the generic
|
|
81
|
+
`--key value` branch into `args.help = true`, and the named tool ran with that
|
|
82
|
+
as an inert extra property; `-h` doesn't even start with `--`, so it fell all
|
|
83
|
+
the way to the positional-token branch and was silently dropped once a tool
|
|
84
|
+
name was already set. Either way, **the tool ran.** Measured live this
|
|
85
|
+
session: `cdp take_screenshot --help` executed a real capture of whatever tab
|
|
86
|
+
happened to be active and wrote a 1.2 MB PNG to `/tmp/cdp-toolkit` — a "tell
|
|
87
|
+
me, don't do it" discovery gesture that silently photographed what the user
|
|
88
|
+
was looking at. Reproduced again here with the safe, non-capturing
|
|
89
|
+
`list_pages --help` against a `CDP_BASE` pointed at a closed port: pre-fix it
|
|
90
|
+
printed `{"error":"Unable to connect. Is the computer able to access the
|
|
91
|
+
url?"}` and exited 1 — proof the tool dispatched and tried to reach the
|
|
92
|
+
browser instead of printing help. Post-fix, the identical invocation against
|
|
93
|
+
the same dead port prints usage and exits 0, and a directory listing of the
|
|
94
|
+
artifact dir taken before and after is byte-identical: no file, no browser
|
|
95
|
+
touch.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- **`--help`/`-h` now short-circuit before any tool dispatch, recognised
|
|
100
|
+
anywhere in argv** (not just as the first token), and are never treated as
|
|
101
|
+
a tool argument or a tool name. With no tool named, `cdp --help`/`-h` prints
|
|
102
|
+
the existing top-level usage banner (now also documenting the flag itself).
|
|
103
|
+
With a tool named, `cdp <tool> --help` prints that tool's argument list —
|
|
104
|
+
name, type, required/optional, description — read directly from
|
|
105
|
+
`MANIFEST`'s `inputSchema` (`src/manifest.ts`), the exact JSON Schema the
|
|
106
|
+
MCP server advertises via `tools/list`, so there is no second
|
|
107
|
+
hand-maintained argument list to drift from it.
|
|
108
|
+
- An unknown tool name alongside `--help` (`cdp nope --help`) is reported as
|
|
109
|
+
`{"error":"unknown tool 'nope'. ..."}` and exits 1 — the same message the
|
|
110
|
+
CLI already gives for `cdp nope` without `--help` — rather than silently
|
|
111
|
+
falling back to top-level usage as though the name had been accepted.
|
|
112
|
+
- **Decision, stated explicitly:** an unknown `--flag` passed to a *known*
|
|
113
|
+
tool still passes through as a tool argument, unchanged from pre-1.9.3
|
|
114
|
+
behavior. Every `inputSchema` already declares `additionalProperties:
|
|
115
|
+
false`, but neither `cli.ts` nor `mcp.ts` has ever enforced that — each tool
|
|
116
|
+
validates its own args at runtime, extra keys are typically just ignored,
|
|
117
|
+
and an existing caller may already depend on that leniency for something
|
|
118
|
+
harmless. Rejecting unknown flags here would diverge the CLI from the MCP
|
|
119
|
+
server (whose validation this release does not touch) to fix a problem this
|
|
120
|
+
bug did not create. `--help`/`-h` are instead recognised by name,
|
|
121
|
+
specifically, ahead of generic flag parsing — narrow enough that it needs
|
|
122
|
+
none of that broader, riskier call.
|
|
123
|
+
|
|
124
|
+
### Added
|
|
125
|
+
|
|
126
|
+
- `test/cli-help.test.ts` (6 tests): spawns the real CLI as a subprocess with
|
|
127
|
+
`CDP_BASE` pointed at a closed port, so any invocation that still reaches
|
|
128
|
+
tool dispatch fails loudly with a connection error instead of silently
|
|
129
|
+
succeeding. Watched fail against the pre-fix code first — 5 of 6 red on
|
|
130
|
+
exit code 1 instead of 0 — before confirming green post-fix.
|
|
131
|
+
|
|
132
|
+
### Notes
|
|
133
|
+
|
|
134
|
+
- Tool count unchanged at **45**; no tool added, renamed, or removed.
|
|
135
|
+
`take_screenshot` is not among the files this release touches.
|
|
136
|
+
- 748 pass / 0 fail across 22 files, up from 742/21. `bun run typecheck` clean.
|
|
137
|
+
|
|
138
|
+
## [1.9.2] - 2026-08-10
|
|
139
|
+
|
|
140
|
+
`take_screenshot` grows four arguments — `scale`, `renderWidth`/`renderHeight`,
|
|
141
|
+
`tile` — and stops being able to hang the tab it is capturing. The constraint
|
|
142
|
+
worth learning from this release: **Chrome cannot encode a screenshot larger
|
|
143
|
+
than 16384 device px on either side, and past that it does not refuse politely
|
|
144
|
+
— `Page.captureScreenshot` never answers, and because `captureBeyondViewport`
|
|
145
|
+
resizes the renderer to the clip for the length of the capture, a call that
|
|
146
|
+
never returns never puts it back.** Output px are `ceil(css × scale ×
|
|
147
|
+
devicePixelRatio)`, so on an ordinary ratio-2 display that ceiling arrives at
|
|
148
|
+
about 8192 CSS px — a long article, a chat log, a docs page. A plain no-flag
|
|
149
|
+
`take_screenshot` of a 9000 CSS px page timed out (probed with an 8 s command
|
|
150
|
+
timeout) and left the tab wedged at 1390×9000, with every later capture on it
|
|
151
|
+
timing out too. It now returns a 2780×18000 px file in 1 second, as two
|
|
152
|
+
stitched bands. Tool count
|
|
153
|
+
stays 45 — no new tool, `take_screenshot` grew arguments. Five commits landed
|
|
154
|
+
it in sequence, on top of two measurement passes that drove a live
|
|
155
|
+
Chrome 151.0.7922.76 and wrote no code — the encode cap and the band-artifact
|
|
156
|
+
hunt. Every number below comes off a measurement, and where a measurement
|
|
157
|
+
contradicted the brief that ordered the work, the measurement is what is
|
|
158
|
+
written down.
|
|
159
|
+
|
|
160
|
+
### Added
|
|
161
|
+
|
|
162
|
+
- **`scale` — a per-capture output multiplier**, a finite number greater than
|
|
163
|
+
0 and at most 8 (default 1), Chrome only (capability `screenshot.scale`).
|
|
164
|
+
Measured exact on a live tab: a 2780×2128 default capture at `scale 3` is
|
|
165
|
+
8340×6384, a `--fullPage` at `scale 2` is 5560×13000, a 400×250 CSS element
|
|
166
|
+
at `scale 4` is 3200×2000 — `sips` agrees with the reported size on 9 of 9
|
|
167
|
+
artifacts, PNG and JPEG alike. It re-renders, it does not upsample, and it
|
|
168
|
+
changes nothing the page can see: `devicePixelRatio` read 2 and
|
|
169
|
+
`innerWidth`/`innerHeight` read 1390×1064 both before and after a `scale 3`
|
|
170
|
+
capture. Firefox refuses it rather than silently returning a 1x image the
|
|
171
|
+
caller could not tell apart from the one they wanted —
|
|
172
|
+
`browsingContext.captureScreenshot` has no scale parameter, and the refusal
|
|
173
|
+
points at `emulate {deviceScaleFactor}` instead.
|
|
174
|
+
|
|
175
|
+
- **`renderWidth`/`renderHeight` — emulate a viewport for exactly one capture,
|
|
176
|
+
then put the previous one back.** Required together, integers in
|
|
177
|
+
`[1, 16384]` CSS px, and declared by **both** backends (capability
|
|
178
|
+
`screenshot.renderSize`) — unlike `scale`, this is not a Chrome-only
|
|
179
|
+
argument. A 1390×1064 tab captured at 1920×1080 yields 3840×2160 device px
|
|
180
|
+
and reads back at 1390×1064 afterwards; captured at 420×900 it is
|
|
181
|
+
**byte-identical** (sha256 `93ffca74…`) to the same page genuinely emulated
|
|
182
|
+
at 420×900, media query flipped and all. That is the difference from `scale`
|
|
183
|
+
in one sentence: `scale` re-renders the same layout at more pixels,
|
|
184
|
+
`renderWidth`/`renderHeight` change what the page *believes* for one
|
|
185
|
+
capture. The restore holds on the success path, on the throwing path, and
|
|
186
|
+
over a caller's prior `emulate` (a tab emulated at 777×555 is 777×555 again
|
|
187
|
+
afterwards, not the real device). Two honest limits ride in the result
|
|
188
|
+
rather than in a comment: the restore can only put back an override **this
|
|
189
|
+
toolkit** applied — one set by the DevTools UI, another client, or an older
|
|
190
|
+
build leaves no record and is reset to the real device — and a capture that
|
|
191
|
+
TIMES OUT cannot be restored at all, because the `finally` runs while
|
|
192
|
+
Chrome's own command is still executing and re-resizes the page behind it.
|
|
193
|
+
`renderRestored: false` (plus `renderRestoreError`) is what the caller gets
|
|
194
|
+
when the tab is still emulated and they must fix it themselves.
|
|
195
|
+
|
|
196
|
+
- **`tile` — vertical band capture, stitched losslessly, and AUTO by
|
|
197
|
+
default.** Three-valued: omitted means one shot when the projection fits and
|
|
198
|
+
bands when it does not; `false` never bands and refuses instead (the exact
|
|
199
|
+
pre-1.9.2 behaviour); `true` always bands, even when one shot would have
|
|
200
|
+
fitted. Only `true` costs a capability (`screenshot.tile`, Chrome only).
|
|
201
|
+
The headline, a plain call with no flags on a 140,982 CSS px page:
|
|
202
|
+
**2780×281964 px, 18 bands, 10,818,058 bytes, 17–18 s**, tab healthy
|
|
203
|
+
afterwards (`[innerWidth, innerHeight, devicePixelRatio]` = `[1390, 1064,
|
|
204
|
+
2]`, `scrollY` 0) and two runs byte-identical. 281964 is 140982 × 2 exactly:
|
|
205
|
+
the whole document, nothing dropped. A banded capture is PNG-only and cannot
|
|
206
|
+
also `returnBase64`; both combinations are refused in 21–31 ms measured,
|
|
207
|
+
before a single band is captured, with a message naming the band count and
|
|
208
|
+
the region that forced the decision. Tiling is **vertical only** — a projected WIDTH
|
|
209
|
+
past the cap is refused (naming the largest scale, or renderWidth, that
|
|
210
|
+
would fit), not split into a grid, because PNG scanlines are the unit that
|
|
211
|
+
can be concatenated without decoding the image whole.
|
|
212
|
+
|
|
213
|
+
- **New result fields.** `scale` is always present. `width`/`height` are
|
|
214
|
+
**decoded from the encoded bytes** (PNG IHDR, JPEG SOF marker walk) rather
|
|
215
|
+
than projected, and are **omitted entirely** when the bytes cannot be
|
|
216
|
+
decoded — absent beats invented. `renderSize`, `renderRestored` and
|
|
217
|
+
`renderRestoreError` accompany a render-size capture; `tiled: true` and
|
|
218
|
+
`bands: <n>` are present only on a banded one, so a caller always knows when
|
|
219
|
+
an image was assembled rather than captured.
|
|
220
|
+
|
|
221
|
+
- **`src/png.ts`, a streaming lossless PNG band stitcher**, zero runtime
|
|
222
|
+
dependencies (`node:zlib` is a builtin). Peak RSS is **135.3 MB while
|
|
223
|
+
writing a 1.42 GB image** (368,640 rows) — 9× the raw pixels of a 157 MB one
|
|
224
|
+
for 11.7% more memory, because peak tracks band size, not image size (Bun's
|
|
225
|
+
own floor is 23.6 MB of that).
|
|
226
|
+
Its output is byte-identical under ffmpeg's raw decode to the source bands
|
|
227
|
+
concatenated, at every one of PNG's five row filters, and a 2-band stitch of
|
|
228
|
+
a page that fitted decodes to the identical sha256 as the single shot of the
|
|
229
|
+
same page. It carries `gAMA`/`cHRM`/`sRGB`/`iCCP`/`pHYs` from the first band
|
|
230
|
+
and throws on a mismatch: every Chrome capture measured carries an embedded
|
|
231
|
+
ICC profile, so a stitcher that dropped ancillary chunks — the obvious
|
|
232
|
+
default — would silently recolour every image it touched.
|
|
233
|
+
|
|
234
|
+
### Changed
|
|
235
|
+
|
|
236
|
+
- **Every capture now costs one `Page.getLayoutMetrics` round trip, including
|
|
237
|
+
the plain default one.** That is the change that closed the hazard. The
|
|
238
|
+
plain path was the one path that built no clip, so the encode guard never
|
|
239
|
+
ran on it, while `captureBeyondViewport: true` is hardcoded for every
|
|
240
|
+
capture — meaning an ordinary long page, hit with an ordinary flagless
|
|
241
|
+
screenshot, was already capturing beyond the viewport with nothing standing
|
|
242
|
+
between it and the hang. Nothing else about that path moved: it still sends
|
|
243
|
+
**no `clip` key at all**, and every step of this release A/B'd the common
|
|
244
|
+
path against its own parent commit's binaries on the same live tab —
|
|
245
|
+
default, `fullPage`, `scale 2`, `jpeg`, element-clip and `tile:false` all
|
|
246
|
+
came back byte-identical.
|
|
247
|
+
|
|
248
|
+
- **`savePath` outside the artifact dir now works.** The destination directory
|
|
249
|
+
is created from the output path's own dirname rather than always
|
|
250
|
+
`CDP_ARTIFACT_DIR`; previously a `savePath` in a non-existent directory
|
|
251
|
+
failed `ENOENT` while `CDP_ARTIFACT_DIR` was created for nobody.
|
|
252
|
+
|
|
253
|
+
- **The default filename's stamp is the capture's START**, not its end: a
|
|
254
|
+
banded capture writes as bands arrive and has no finished image to name a
|
|
255
|
+
file after, and one naming path is easier to trust than two.
|
|
256
|
+
|
|
257
|
+
- **A new state-file type**, `${CDP_ARTIFACT_DIR}/viewport-<scheme>-<targetId>.json`,
|
|
258
|
+
a sibling of the lease and origin files. A render-size capture must restore
|
|
259
|
+
the caller's *previous* override, and a `PageDriver` lives for exactly one
|
|
260
|
+
call (under the CLI, one process), so the record has to outlive the
|
|
261
|
+
connection the same way a lease does — measured: an `emulate` override set
|
|
262
|
+
in one process is still in force when read from another.
|
|
263
|
+
|
|
264
|
+
### Fixed
|
|
265
|
+
|
|
266
|
+
- **An element-clipped `take_screenshot` returned the wrong region of the page
|
|
267
|
+
whenever the element was not already on screen.** `--selector` and `--uid`
|
|
268
|
+
both, at every page height tried. The mechanism is a frame mismatch worth
|
|
269
|
+
learning on its own: `DOM.getBoxModel` answers in the **viewport** frame (its
|
|
270
|
+
quad matched `getBoundingClientRect()` to the pixel every time), but
|
|
271
|
+
`captureBeyondViewport: true` — hardcoded on every capture here — makes
|
|
272
|
+
Chrome read `clip.x`/`clip.y` as **document** coordinates. The driver scrolled
|
|
273
|
+
the element into view with `DOM.scrollIntoViewIfNeeded` and then handed
|
|
274
|
+
Chrome the element's now-small *on-screen* y as if it were its position in
|
|
275
|
+
the document, so the capture landed within one viewport height of the top of
|
|
276
|
+
the page no matter how far down the element really was. Measured on
|
|
277
|
+
Chrome 151.0.7922.76, a 10,000 CSS px page with the target at y=5000: the
|
|
278
|
+
scroll settled at `scrollY` 4593 with `rectTop` 407, and the returned
|
|
279
|
+
2780×500 PNG was `document[407..657]` — a flat slab of page background with
|
|
280
|
+
no part of the element in it. Identical failure at the two sizes this bug was
|
|
281
|
+
first reported at, 16,578 px (`scrollY` 14593) and 140,982 px (`scrollY`
|
|
282
|
+
69593). Starting the same call from `scrollY` 4950 instead — element already
|
|
283
|
+
visible at `rectTop` 50, so the driver's own scroll is a no-op — returned
|
|
284
|
+
`document[50..300]`: the document's top banner for its first 100 CSS px, then
|
|
285
|
+
background, boundary on the predicted pixel. **The fix is the frame
|
|
286
|
+
conversion**: the page's scroll offset is added to the box-model quad, in CSS
|
|
287
|
+
px (`cssVisualViewport.pageX`/`pageY`), read **after** the scroll settles and
|
|
288
|
+
off the same single `Page.getLayoutMetrics` every capture already pays for —
|
|
289
|
+
reading it before the scroll would correct by 0 on a freshly navigated tab
|
|
290
|
+
and change nothing. Those same three page heights now return the element
|
|
291
|
+
itself, and the three starting scroll positions 0 / 4950 / 9000 return one
|
|
292
|
+
identical sha256 `54b96705…` instead of two different wrong images. `scale`
|
|
293
|
+
(5560×1000 at `scale 2`), `renderWidth`/`renderHeight` (1600×500 at 800×600,
|
|
294
|
+
`renderRestored: true`), `tile` and the `uid` path all ride the corrected
|
|
295
|
+
rect unchanged, and the short-page control — element inside the initial
|
|
296
|
+
viewport, no scroll needed, offset 0 — is byte-for-byte what it was.
|
|
297
|
+
**Two honest notes.** First, the units are a live trap: on a scrolled page
|
|
298
|
+
`visualViewport.pageX`/`pageY` come back in **CSS** px (700/4593) while
|
|
299
|
+
`clientWidth`/`clientHeight` on that very same rect are **device** px
|
|
300
|
+
(2780×2128), and only `layoutViewport` reports the offset in device px
|
|
301
|
+
(1400/9186) — so "no `css` prefix means device px" is false for exactly the
|
|
302
|
+
fields this fix reads, and a plausible implementation that divided them by
|
|
303
|
+
the pixel ratio would have halved the correction. Second, this release's own
|
|
304
|
+
regression check could not have caught it: the element-clip A/B recorded
|
|
305
|
+
under *Changed* above compared each commit against its parent's binaries and
|
|
306
|
+
came back byte-identical **because both sides were wrong the same way** — a
|
|
307
|
+
same-vs-same check is blind to a bug older than both of its arms.
|
|
308
|
+
`DOM.scrollIntoViewIfNeeded` was kept rather than dropped, and that was
|
|
309
|
+
measured too, not assumed: with `captureBeyondViewport` nothing has to be on
|
|
310
|
+
screen to be captured, but on a fixture whose target fills itself from an
|
|
311
|
+
`IntersectionObserver`, the capture with the scroll reads "LAZY CONTENT
|
|
312
|
+
LOADED" and the same capture without it returns the unloaded placeholder in
|
|
313
|
+
the correct region — the same caveat the tiling notes below record, from the
|
|
314
|
+
other side. WebDriver BiDi never had this defect: it passes
|
|
315
|
+
`clip: {type: "element"}` and does no coordinate arithmetic of its own.
|
|
316
|
+
|
|
317
|
+
- **`emulate {clearOverrides: true}` reported `"cleared": true` while clearing
|
|
318
|
+
nothing.** Reproduced across three separate CLI processes: process A set
|
|
319
|
+
802×601, process B cleared and reported success, process C read back
|
|
320
|
+
`[802, 601]`. `Emulation.clearDeviceMetricsOverride` only reverts to the real
|
|
321
|
+
device when the clearing connection is also the connection that *set* the
|
|
322
|
+
override — and under this toolkit's per-call connection lifetime it
|
|
323
|
+
essentially never is, so this was the common case, not an edge case. The
|
|
324
|
+
driver now reads the tab's currently effective size, re-asserts that exact
|
|
325
|
+
size on its own connection (a value the caller cannot observe changing,
|
|
326
|
+
whose only job is to take ownership), and then clears; the same trace now
|
|
327
|
+
reads back `[1390, 1064]`, the real device. If any step of that sequence
|
|
328
|
+
throws, the call throws too rather than returning a false success, and the
|
|
329
|
+
viewport record is deliberately left in place so a later capture does not
|
|
330
|
+
read "no record" as "nothing to restore".
|
|
331
|
+
|
|
332
|
+
### Measured, not assumed — the numbers this release ships on
|
|
333
|
+
|
|
334
|
+
- **16384 device px per side, both axes, clipped and unclipped alike.**
|
|
335
|
+
16384×1600 encodes in 266 ms; 16400×1600 — sixteen pixels more — never
|
|
336
|
+
answers (60 s probe cap). With no `clip` key at all: 2560×16384 succeeds in
|
|
337
|
+
217 ms, 2560×24000 hangs. **It is not an area limit:** 8192×8192 (67,108,864
|
|
338
|
+
px) succeeds in 1.3 s while 16400×1600 (26,240,000 px, 2.6× less) hangs. The
|
|
339
|
+
cap is also in DEVICE px, so a CSS clip of 8200×800 at `deviceScaleFactor: 2`
|
|
340
|
+
hangs identically to asking for 16400×1600 directly.
|
|
341
|
+
- **Band tiling is visually clean, and this was falsification-tested rather
|
|
342
|
+
than assumed.** A `position: fixed` header present in the top band appears
|
|
343
|
+
**zero** times in a band 8000 px down; a `position: sticky` header appears
|
|
344
|
+
only in the band containing its true document position. Seams are row-exact
|
|
345
|
+
— band A ends on ROW 039, band B begins on ROW 040, no duplicate, no gap.
|
|
346
|
+
Clipped bands are byte-identical across three different real scroll
|
|
347
|
+
positions, so a stitcher never has to manage scroll. `clip.y` is always CSS
|
|
348
|
+
px and `scale` only multiplies resolution, so the two compose without
|
|
349
|
+
rescaling offsets. Content-verified end to end on the 140,982 px page: all
|
|
350
|
+
**71** label bands land at exactly their expected rows across 9 bands and 8
|
|
351
|
+
seams.
|
|
352
|
+
- **The honest caveat.** A page that loads content only on real scroll —
|
|
353
|
+
lazy images, virtualized lists, infinite scroll — renders **blank** in bands
|
|
354
|
+
the real viewport never reached. Measured directly: a page whose
|
|
355
|
+
`IntersectionObserver` had loaded 9 of 300 rows still reported 9 after a
|
|
356
|
+
deep-band capture, and the deep band was uniform `#222`.
|
|
357
|
+
`captureBeyondViewport` does not trigger an `IntersectionObserver`. This is
|
|
358
|
+
a property of the source page, not a bug in the stitch, and it is not
|
|
359
|
+
fixable here — which is exactly why an auto-tiled capture reports `tiled`,
|
|
360
|
+
so a caller knows to reason about it (pre-scroll the page, or disable
|
|
361
|
+
virtualization, before capturing).
|
|
362
|
+
- **Two corrections to earlier internal notes, kept rather than quietly
|
|
363
|
+
dropped.** (1) At 140,982 CSS px the *old* code did not hang: it failed in
|
|
364
|
+
about a second with `Page.captureScreenshot: Page is too large.` and left
|
|
365
|
+
the tab healthy. The hang-and-wedge lives at *modest* overshoot — the 9000
|
|
366
|
+
CSS px page above. Both are failures, but "it hangs at 140,982" is not what
|
|
367
|
+
this Chrome build does. (2) A wedged tab is **not** recovered by
|
|
368
|
+
`emulate --clearOverrides`: measured in order, clearing reports success and
|
|
369
|
+
the tab stays 1390×9000, an explicit `emulate --width/--height` recovers it,
|
|
370
|
+
and clearing again puts it *back*. Only a reload actually clears it. The
|
|
371
|
+
renderer's real viewport is what the abandoned capture resized, so
|
|
372
|
+
"clear to the real device" lands on the wrong size.
|
|
373
|
+
|
|
374
|
+
### Notes
|
|
375
|
+
|
|
376
|
+
- Tool count is unchanged at **45**. `take_screenshot` grew four arguments;
|
|
377
|
+
no tool was added, renamed, or removed.
|
|
378
|
+
- **Firefox/BiDi does not declare `screenshot.scale` or `screenshot.tile`**,
|
|
379
|
+
and for two different reasons stated plainly rather than dressed up as
|
|
380
|
+
design: `browsingContext.captureScreenshot` genuinely has no scale
|
|
381
|
+
parameter, while band tiling is *probably* implementable there
|
|
382
|
+
(`clip: {type: "box"}` exists) but every property it rests on — does a box
|
|
383
|
+
clip render past the viewport, is `clip.y` CSS px, do fixed elements repeat
|
|
384
|
+
per band, are seams pixel-exact — was measured on Chrome and nowhere else,
|
|
385
|
+
because no Firefox was driven for it. `screenshot.renderSize` **is**
|
|
386
|
+
declared by both. Auto-tiling never fires on a backend without
|
|
387
|
+
`screenshot.tile`, so Firefox behaviour is unchanged.
|
|
388
|
+
- New tests: **117**, in `test/screenshot-scale.test.ts` (81) and
|
|
389
|
+
`test/png.test.ts` (36) — 736 pass / 0 fail across 21 files, up from 619.
|
|
390
|
+
Both new suites were mutation-checked rather than merely green: 13 deliberate
|
|
391
|
+
mutants of `src/png.ts` and 6 of the tiling arithmetic were each killed,
|
|
392
|
+
and the three mutants that survived are documented in the source as provably
|
|
393
|
+
equivalent rather than silently "fixed" later.
|
|
394
|
+
- `docs/social-preview.html` and `docs/demo.tape` carry no version string and
|
|
395
|
+
needed no re-render for this release.
|
|
396
|
+
|
|
397
|
+
## [1.9.1] - 2026-08-10
|
|
398
|
+
|
|
399
|
+
`list_network_requests`, `get_network_request`, and `list_console_messages` now
|
|
400
|
+
observe MV3 background service workers, not just pages: the outbound fetch a
|
|
401
|
+
worker makes to a real backend is a first-class capture target, not something
|
|
402
|
+
you have to fake with an `evaluate_script` monkeypatch. Tool count stays 45 —
|
|
403
|
+
the three tools grow a `worker:` arm, no new tool. One seat (N1) landed it,
|
|
404
|
+
every protocol claim below probed against a live Chrome 151 before being built
|
|
405
|
+
on.
|
|
406
|
+
|
|
407
|
+
### Added
|
|
408
|
+
|
|
409
|
+
- **The three console/network reader tools accept `target: "worker:<substring>"`
|
|
410
|
+
or a bare worker target id**, alongside the page selectors they already took.
|
|
411
|
+
Chrome only (capability `worker.targets`) — Firefox refuses the arm from its
|
|
412
|
+
real capability set before launching a browser that could never have served
|
|
413
|
+
the call. The allowlist that grants the arm, `WORKER_CAPABLE_TOOLS` in
|
|
414
|
+
`src/workers.ts`, now names four tools (`evaluate_script` plus these three);
|
|
415
|
+
it is the single source of truth for both the refusal messages and
|
|
416
|
+
`test/manifest-grammar-drift.test.ts`'s exclusion property, so a fifth tool
|
|
417
|
+
cannot silently advertise `worker:` without a deliberate edit here.
|
|
418
|
+
|
|
419
|
+
- **A worker capture LISTENS instead of reloading.** Measured on Chrome
|
|
420
|
+
151.0.7922.109: `Network.enable` and `Runtime.consoleAPICalled` work
|
|
421
|
+
identically on a direct worker session to how they work on a page — all four
|
|
422
|
+
events the recorder persists (`requestWillBeSent`, `responseReceived`,
|
|
423
|
+
`loadingFinished`, `loadingFailed`) arrive, and `Network.getResponseBody`
|
|
424
|
+
serves the body — but `Page.enable`/`Page.reload` **do not exist** on a
|
|
425
|
+
worker session (`-32601 'Page.enable' wasn't found`). A page capture is
|
|
426
|
+
reload-driven; a worker capture cannot be, so it opens the worker's own CDP
|
|
427
|
+
session and listens for `durationMs` instead. `captureWindow()`'s branch is
|
|
428
|
+
keyed off the resolved target's type, not the selector's shape, so a bare
|
|
429
|
+
worker id takes the listen path too.
|
|
430
|
+
|
|
431
|
+
- **Recording a worker keeps it alive for the length of the capture — a
|
|
432
|
+
deliberate side effect, not a bug.** A held CDP session suppresses MV3 idle
|
|
433
|
+
eviction; measured in both directions with the same polling loop so
|
|
434
|
+
"stayed alive" isn't an artifact of how the poll works: with the recorder's
|
|
435
|
+
session held, the worker is still present in `/json/list` and still emits a
|
|
436
|
+
fresh `Network.requestWillBeSent` for a triggered fetch at t+60s; once the
|
|
437
|
+
session detaches, the same worker idle-evicts within ~30s. Documented in the
|
|
438
|
+
three tools' manifest descriptions and asserted in both directions by the
|
|
439
|
+
live smoke, not left implicit.
|
|
440
|
+
|
|
441
|
+
- **`wake` (default `true` on a capture) is REFUSED on a read-only call and on
|
|
442
|
+
a bare target id, rather than accepted and ignored.** Measured: a worker
|
|
443
|
+
that idle-evicts and restarts comes back under a brand-new target id (probed
|
|
444
|
+
restart: `C43F0A18...` → `1F7D3C59...` for the same extension). The recorder
|
|
445
|
+
keys its buffer file by target id, so waking a worker on a *read* would hand
|
|
446
|
+
back a live worker with an empty buffer — "0 requests" for a capture that
|
|
447
|
+
really did record some, a zero shaped like an answer. A wake therefore only
|
|
448
|
+
makes sense when a capture is being started, and only from a
|
|
449
|
+
`worker:<substring>` selector, since an evicted worker's old id no longer
|
|
450
|
+
exists to name. The refusal messages (`workerWakeMisuseMessage`,
|
|
451
|
+
`workerBufferReadMissMessage` in `src/workers.ts`) teach the id-re-mint fact
|
|
452
|
+
rather than just rejecting the argument.
|
|
453
|
+
|
|
454
|
+
- **Known limitation, stated rather than silently absent: a worker woken BY a
|
|
455
|
+
capture has already run its top-level code by the time the recorder
|
|
456
|
+
attaches**, so a fetch the worker makes at startup can be missed. Trigger
|
|
457
|
+
the request you want to observe (a message, an alarm) after the capture
|
|
458
|
+
starts rather than relying on the worker's own boot-time fetch. Fixing this
|
|
459
|
+
needs `Target.setAutoAttach{waitForDebuggerOnStart}` on a held browser
|
|
460
|
+
session, a different feature; the limitation is documented in
|
|
461
|
+
`recorder.ts`'s header and in the three manifest descriptions instead.
|
|
462
|
+
|
|
463
|
+
- **`get_console_message` reads the same worker-keyed buffer (so its miss
|
|
464
|
+
message matches what it reads) but is deliberately NOT in
|
|
465
|
+
`WORKER_CAPABLE_TOOLS`** — it already resolved worker targets through the
|
|
466
|
+
1.9.0 arm, routing it through the read-path resolver only sharpens its miss
|
|
467
|
+
message; advertising it as worker-capable was out of scope, and the drift
|
|
468
|
+
test pins the advertised list at four.
|
|
469
|
+
|
|
470
|
+
### Measured, not assumed — the protocol facts this release ships on
|
|
471
|
+
|
|
472
|
+
- **`Page.enable`/`Page.reload` are `-32601` on a worker session** — the
|
|
473
|
+
single measurement that shapes the implementation: a worker window cannot
|
|
474
|
+
be reload-driven, so it listens.
|
|
475
|
+
- **A held session suppresses MV3 idle eviction; detach restores it** —
|
|
476
|
+
confirmed in both directions with the same polling loop, including a
|
|
477
|
+
post-hold evaluate and a fresh `Network.requestWillBeSent` at t+60s, so
|
|
478
|
+
"alive" means genuinely live, not a stale listing entry.
|
|
479
|
+
- **A worker's target id is re-minted on every restart, never stable** — this
|
|
480
|
+
is why `wake` is refused on reads and on bare ids rather than silently doing
|
|
481
|
+
the wrong thing.
|
|
482
|
+
- **Two things a caller might reach for instead don't work, for ordinary
|
|
483
|
+
JavaScript reasons, not a service-worker-specific one.** A value bound at
|
|
484
|
+
module top level is not visible from `evaluate_script`'s global scope
|
|
485
|
+
(plain lexical scoping, identical in a page); assigning `self.fetch`
|
|
486
|
+
afterward cannot rebind a `fetch` reference the module already captured
|
|
487
|
+
(closure semantics, also identical in a page). `Network`-domain recording
|
|
488
|
+
on the worker's own session observes what the code ACTUALLY SENT and needs
|
|
489
|
+
neither. (An earlier field report attributing this to service-worker eval
|
|
490
|
+
semantics was a misdiagnosis of an outdated loaded extension build, not a
|
|
491
|
+
real capability gap — see the stale-build tip below.)
|
|
492
|
+
|
|
493
|
+
### Notes
|
|
494
|
+
|
|
495
|
+
- Tool count is unchanged at **45** — this release grew the `target` grammar
|
|
496
|
+
on three existing tools, not the tool list.
|
|
497
|
+
- `docs/social-preview.html` and `docs/demo.tape` carry no version string and
|
|
498
|
+
needed no re-render for this release.
|
|
499
|
+
- Before deep-debugging an extension that "the toolkit can't see": verify the
|
|
500
|
+
loaded build is current with
|
|
501
|
+
`evaluate_script {target:"worker:<id>", expression:"chrome.runtime.getManifest().version"}`.
|
|
502
|
+
An outdated unpacked build left over from a prior load mimics every symptom
|
|
503
|
+
in this release's "measured facts" section above.
|
|
504
|
+
- New tests: 20 in `test/worker-capture.test.ts`, 3 more in
|
|
505
|
+
`test/manifest-grammar-drift.test.ts` (allowlist rescoped from an
|
|
506
|
+
evaluate_script-only exclusion to the four-tool `WORKER_CAPABLE_TOOLS`
|
|
507
|
+
allowlist), and new live scenarios in `test/extension-smoke.ts` covering the
|
|
508
|
+
worker fetch/console capture, the eviction pair in both directions, and the
|
|
509
|
+
capture-path wake (proven by the fixture's own start counter, `starts: 2 ->
|
|
510
|
+
3`, never by `ServiceWorker.startWorker`'s result, which reports empty
|
|
511
|
+
success even for a scope that doesn't exist) — 31/31 assertions, up from the
|
|
512
|
+
1.9.0 baseline of 20.
|
|
513
|
+
|
|
8
514
|
## [1.9.0] - 2026-08-09
|
|
9
515
|
|
|
10
516
|
Four complaints from a field agent driving an MV3 extension, closed without adding
|
|
@@ -870,6 +1376,7 @@ First public release.
|
|
|
870
1376
|
- Zero runtime dependencies in the CDP/CLI layer (Node's global `WebSocket` + `fetch`). The MCP server adds only `@modelcontextprotocol/sdk`; `lighthouse_audit` is the sole non-CDP tool and shells out to `npx lighthouse`.
|
|
871
1377
|
- Runtime: Bun ≥ 1.1 (recommended) or Node ≥ 22 (for the global `WebSocket`). Requires Chrome/Chromium started with `--remote-debugging-port=9222`.
|
|
872
1378
|
|
|
1379
|
+
[1.9.1]: https://github.com/sblattj/cdp-toolkit/releases/tag/v1.9.1
|
|
873
1380
|
[1.9.0]: https://github.com/sblattj/cdp-toolkit/releases/tag/v1.9.0
|
|
874
1381
|
[1.8.0]: https://github.com/sblattj/cdp-toolkit/releases/tag/v1.8.0
|
|
875
1382
|
[1.7.0]: https://github.com/sblattj/cdp-toolkit/releases/tag/v1.7.0
|