cdp-toolkit 1.9.4 → 1.9.7
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 +83 -0
- package/CONTRACT.md +1 -1
- package/README.md +16 -1
- package/dist/cli.js +36 -28
- package/dist/index.js +21 -19
- package/dist/mcp.js +37 -29
- package/package.json +1 -1
- package/src/bidi/client.ts +11 -0
- package/src/bidi/driver.ts +30 -22
- package/src/bidi/launch.ts +9 -2
- package/src/manifest.ts +2 -5
- package/src/mcp.ts +1 -1
- package/src/tools/recorder.ts +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,89 @@ 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.7] - 2026-08-14
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A dropped Firefox WebDriver BiDi connection is now re-dialed instead of erroring
|
|
13
|
+
permanently.** The backend memoizes one `BidiConnection` per endpoint, and `getConnection`
|
|
14
|
+
(`src/bidi/driver.ts`) returned the cached one unconditionally; once its socket dropped —
|
|
15
|
+
a Firefox restart, an idle/network drop, or a lost BiDi session slot — every later call
|
|
16
|
+
rejected forever with `connection not open`, for the entire life of the MCP server.
|
|
17
|
+
`getConnection` now evicts a dead cached connection (via a new `BidiConnection.isOpen`
|
|
18
|
+
getter) and re-creates the session on the next call, so Firefox tools recover on their own
|
|
19
|
+
instead of needing a server restart.
|
|
20
|
+
- **Firefox launch mode no longer returns the hardcoded macOS Firefox path on Linux.**
|
|
21
|
+
`resolveBinary` (`src/bidi/launch.ts`) returned the first truthy candidate without checking
|
|
22
|
+
it existed, so on Linux it handed back `/Applications/Firefox.app/Contents/MacOS/firefox`
|
|
23
|
+
and never fell back to `firefox` on `PATH` — `ENOENT` at spawn, in launch mode only. An
|
|
24
|
+
absolute candidate (`FIREFOX_BIN`, the macOS path) is now only returned if it exists on this
|
|
25
|
+
box; the bare `firefox` name is left for `spawn` to resolve against `PATH`.
|
|
26
|
+
- **Recorder artifact directory is now resolved per call, so `CDP_ARTIFACT_DIR` is honored at
|
|
27
|
+
runtime instead of being frozen at import time.** The buffer directory in `src/tools/recorder.ts`
|
|
28
|
+
was a module-level `const` bound the moment the module loaded, so a caller (or a test) that set
|
|
29
|
+
`CDP_ARTIFACT_DIR` afterward was ignored and `recFile`/`captureFile` still pointed at
|
|
30
|
+
`/tmp/cdp-toolkit`. On a clean CI runner that path does not exist, so the first buffer
|
|
31
|
+
`writeFile` threw `ENOENT` and the worker-capture suite failed there while passing on any box
|
|
32
|
+
where `/tmp/cdp-toolkit` happened to exist. It now reads the env var per call through a local
|
|
33
|
+
`artifactDir()` helper — the same "read per call, not at module load" pattern as `leaseDir()` in
|
|
34
|
+
`src/leases.ts` — so the directory is resolved at write time and a redirect takes effect.
|
|
35
|
+
|
|
36
|
+
## [1.9.6] - 2026-08-13
|
|
37
|
+
|
|
38
|
+
**Fix: `upload_file`'s `files` parameter is now declared array-only in the tool manifest** (was
|
|
39
|
+
`"type": ["string","array"]`). Google's Gemini function-calling API rejects union-typed parameter
|
|
40
|
+
schemas — the `any_of` branch's untyped `items` fails its `$type == Type.ARRAY` predicate — which
|
|
41
|
+
hard-fails the entire `GenerateContentRequest` and kills any Gemini-backed agent session the
|
|
42
|
+
moment this manifest is included in its toolset (observed as subagents returning empty results on
|
|
43
|
+
every dispatch). Runtime behavior is unchanged: the handler still accepts and wraps a bare string
|
|
44
|
+
for direct CDP/CLI callers; MCP/model callers now pass `[path]` per the declared schema.
|
|
45
|
+
|
|
46
|
+
## [1.9.5] - 2026-08-11
|
|
47
|
+
|
|
48
|
+
**Documentation and stale code-comment clarifications; NO behavior changes since 1.9.4.** Two
|
|
49
|
+
updates. First, the Firefox `fullPage` screenshot gap that 1.9.2's docs hedged as "unmeasured" is
|
|
50
|
+
now resolved: measured against a real Firefox 153.0.3 through the shipped CLI in attach mode, a
|
|
51
|
+
20,000px-tall page captured with `take_screenshot --fullPage true` came back as a single
|
|
52
|
+
1366×20000 PNG at scale 1 — verified complete top-to-bottom (a marker at y=0, a marker at y=20000,
|
|
53
|
+
an unbroken ruler through the middle, no truncation). Firefox has no equivalent of Chrome's
|
|
54
|
+
16384-device-px encode cap, so `browsingContext.captureScreenshot(origin:"document")` returns the
|
|
55
|
+
whole long page in one BiDi call and never needs the band-tiling Chrome's cap requires — tiling
|
|
56
|
+
exists solely to route around that cap, and Firefox doesn't have one, so `screenshot.tile` is a
|
|
57
|
+
non-issue for Firefox correctness rather than a gap. `screenshot.scale` remains a genuine,
|
|
58
|
+
unrelated Firefox gap: BiDi's `captureScreenshot` has no scale parameter at all, so Firefox
|
|
59
|
+
captures stay 1x only. That same measured result also replaces three stale "unmeasured"/"probably
|
|
60
|
+
implementable" comments in `src/bidi/driver.ts` describing `screenshot.tile` — comments only, no
|
|
61
|
+
logic changed, since `screenshot.tile` was already correctly left undeclared and `tile:true`
|
|
62
|
+
already correctly refused on Firefox; only the *reasoning* in the comments was stale. Second, this
|
|
63
|
+
release also gives a changelog entry to doc-only additions that landed on top of 1.9.4 without one:
|
|
64
|
+
Linux and remote-over-SSH attach guidance, and a `claude mcp add` one-liner, both in README.md's
|
|
65
|
+
setup instructions.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- README.md: Linux and remote-over-SSH `--connect`/`CDP_FIREFOX_ENDPOINT`
|
|
70
|
+
attach guidance, and a `claude mcp add cdp-toolkit --scope user -- bun run
|
|
71
|
+
"$(pwd)/src/mcp.ts"` one-liner in the quick-start block.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- README.md's "Honest capability gaps" list and CONTRACT.md's `screenshot.ts`
|
|
76
|
+
module row: the Firefox `tile:true`/`screenshot.tile` hedge ("unmeasured
|
|
77
|
+
gap... probably implementable") is replaced with the measured result above.
|
|
78
|
+
`scale`'s status is unchanged and still called out as a genuine gap.
|
|
79
|
+
- `src/bidi/driver.ts`: three comment blocks (the `BIDI_CAPABILITIES` header
|
|
80
|
+
comment, the `capture()` method's `tile:true` refusal comment, and the
|
|
81
|
+
per-capability doc-comment list) rewritten from "unmeasured"/"probably
|
|
82
|
+
implementable" to the measured result above. No capability declarations, no
|
|
83
|
+
refusal logic, and no error-message text changed — comments only.
|
|
84
|
+
|
|
85
|
+
### Notes
|
|
86
|
+
|
|
87
|
+
- Tool count unchanged at **45**; no tool added, renamed, or removed.
|
|
88
|
+
- Version bump only: `package.json` and `src/mcp.ts`'s `VERSION` constant move
|
|
89
|
+
1.9.4 → 1.9.5. No runtime behavior changed anywhere in this release.
|
|
90
|
+
|
|
8
91
|
## [1.9.4] - 2026-08-11
|
|
9
92
|
|
|
10
93
|
Firefox gains a second mode: **attach**. Until now `--browser firefox` always spawned a
|
package/CONTRACT.md
CHANGED
|
@@ -171,7 +171,7 @@ A `Uid` **is** a CDP `backendDOMNodeId` (a number). This makes refs stateless:
|
|
|
171
171
|
| `src/tools/evaluate.ts` | `evaluate_script`→`evaluateScript` | `Runtime.enable` + `Runtime.evaluate` (`returnByValue`, `awaitPromise`). Support optional `args` via `Runtime.callFunctionOn` on a uid/selector. Surface `exceptionDetails` as thrown error. |
|
|
172
172
|
| `src/tools/snapshot.ts` | `take_snapshot`→`takeSnapshot` **+ export `resolveUid`** | a11y tree + ref scheme above. |
|
|
173
173
|
| `src/tools/input.ts` | `click`→`click`, `hover`→`hover`, `drag`→`drag`, `fill`→`fill`, `fill_form`→`fillForm`, `type_text`→`typeText`, `press_key`→`pressKey`, `upload_file`→`uploadFile` | imports `resolveUid` from `./snapshot.ts`. `hover`=mouseMoved. `drag`=press→move→release. `fill`/`type_text`=focus + `Input.insertText` (fast) or `dispatchKeyEvent`. `press_key`=`Input.dispatchKeyEvent` (support modifiers + named keys). `upload_file`=`DOM.setFileInputFiles` on resolved node. `fill_form`=array of {uid|selector,value}. |
|
|
174
|
-
| `src/tools/screenshot.ts` | `take_screenshot`→`takeScreenshot` | `Page.captureScreenshot`. Support `{ format:'png'|'jpeg', quality, fullPage, uid?/selector? (element clip via getBoxModel), savePath? }`. Write to ARTIFACT_DIR, return `{ path, bytes, format }` and optionally base64 when `{ returnBase64:true }`. **As of 1.9.2 also `scale` (Chrome-only capability `screenshot.scale`, rides `clip.scale`), `renderWidth`/`renderHeight` (BOTH backends, `screenshot.renderSize`, an `Emulation.setDeviceMetricsOverride` held for one capture and undone in a `finally` against the viewport record above), and `tile` (three-valued, `true` costs `screenshot.tile`).** Two rules the size arithmetic imposes on anyone touching this path. **(a) Guard the projection, not the knob:** output px are `ceil(css × scale × devicePixelRatio)`, Chrome cannot encode past 16384 device px per side, and past that `Page.captureScreenshot` NEVER ANSWERS while `captureBeyondViewport` (hardcoded true) leaves the renderer resized to the clip — a wedged tab a `finally` cannot undo, because the client-side timeout fires while Chrome's command is still running. So `Page.getLayoutMetrics` is fetched and the projection checked on EVERY capture, including the plain no-clip one; a guard conditioned on the feature that motivated it (`scale !== 1`) has already let two wedges through. **(b) Bands are lazy and vertical:** past the cap the driver hands the tool layer a `BandedCapture` whose Nth `Page.captureScreenshot` runs only when the Nth band is pulled (accumulating them into an array would typecheck, pass on a small page, and hold 1.4 GB on a large one), stitched by `stitchPngBandsToFile` in `src/png.ts` — a zero-dependency streaming PNG stitcher (`node:zlib`), not an MCP tool and deliberately absent from the `TOOLS` registry. Vertical only: PNG scanlines are the one axis concatenable without decoding the image, so an over-WIDE projection is refused, never split. |
|
|
174
|
+
| `src/tools/screenshot.ts` | `take_screenshot`→`takeScreenshot` | `Page.captureScreenshot`. Support `{ format:'png'|'jpeg', quality, fullPage, uid?/selector? (element clip via getBoxModel), savePath? }`. Write to ARTIFACT_DIR, return `{ path, bytes, format }` and optionally base64 when `{ returnBase64:true }`. **As of 1.9.2 also `scale` (Chrome-only capability `screenshot.scale`, rides `clip.scale`), `renderWidth`/`renderHeight` (BOTH backends, `screenshot.renderSize`, an `Emulation.setDeviceMetricsOverride` held for one capture and undone in a `finally` against the viewport record above), and `tile` (three-valued, `true` costs `screenshot.tile`).** Firefox declares neither `screenshot.scale` (BiDi's `captureScreenshot` has no scale parameter, so Firefox captures are always 1x) nor `screenshot.tile` — but the tile gap is moot, not a limitation: Firefox has no Chrome-style 16384-device-px encode cap to band around, so `browsingContext.captureScreenshot(origin:"document")` returns an arbitrarily long `fullPage` capture in one shot. Measured against Firefox 153.0.3: a 20,000px-tall page came back as a single 1366×20000 PNG at scale 1, verified complete top-to-bottom (no truncation). Two rules the size arithmetic imposes on anyone touching this path. **(a) Guard the projection, not the knob:** output px are `ceil(css × scale × devicePixelRatio)`, Chrome cannot encode past 16384 device px per side, and past that `Page.captureScreenshot` NEVER ANSWERS while `captureBeyondViewport` (hardcoded true) leaves the renderer resized to the clip — a wedged tab a `finally` cannot undo, because the client-side timeout fires while Chrome's command is still running. So `Page.getLayoutMetrics` is fetched and the projection checked on EVERY capture, including the plain no-clip one; a guard conditioned on the feature that motivated it (`scale !== 1`) has already let two wedges through. **(b) Bands are lazy and vertical:** past the cap the driver hands the tool layer a `BandedCapture` whose Nth `Page.captureScreenshot` runs only when the Nth band is pulled (accumulating them into an array would typecheck, pass on a small page, and hold 1.4 GB on a large one), stitched by `stitchPngBandsToFile` in `src/png.ts` — a zero-dependency streaming PNG stitcher (`node:zlib`), not an MCP tool and deliberately absent from the `TOOLS` registry. Vertical only: PNG scanlines are the one axis concatenable without decoding the image, so an over-WIDE projection is refused, never split. |
|
|
175
175
|
| `src/tools/emulation.ts` | `emulate`→`emulate`, `resize_page`→`resizePage` | `Emulation.setDeviceMetricsOverride` (width,height,deviceScaleFactor,mobile), `Emulation.setUserAgentOverride`, `Emulation.setCPUThrottlingRate`, `Emulation.setEmulatedMedia`, `Network`-based throttling via `Network.emulateNetworkConditions`. `resize_page` = device metrics width/height. |
|
|
176
176
|
| `src/tools/dialogs.ts` | `handle_dialog`→`handleDialog` | `Page.enable`, subscribe `Page.javascriptDialogOpening`, respond with `Page.handleJavaScriptDialog({ accept, promptText })`. Support a "wait for next dialog then handle" mode and an "auto-handle for N ms" mode. |
|
|
177
177
|
| `src/tools/recorder.ts` + `src/tools/console.ts` + `src/tools/network.ts` | console: `list_console_messages`→`listConsoleMessages`, `get_console_message`→`getConsoleMessage`; network: `list_network_requests`→`listNetworkRequests`, `get_network_request`→`getNetworkRequest` | **Owned by one agent.** `recorder.ts` exports `startRecorder(target, {network,console}): Promise<{stop()}>` that opens a persistent `openPage` conn, enables `Network`+`Runtime`(+`Log`), and appends events to `rec-<targetId>.jsonl`. console/network `list_*` read that file (and support a one-shot `{ reload:true, durationMs }` mode that records fresh by reloading the page and capturing for a window, then returns). `get_*` filter by id/url. Document the recorder model at the top of `recorder.ts`. **As of 1.9.1, the same three tools also accept a `worker:<substring>` target** (Chrome only, capability `worker.targets`; MV3 service/shared workers), resolved via `resolveRecorderTarget`/`resolveWorkerSelector` in `src/workers.ts` rather than `resolveTarget`. Because `Page.enable`/`Page.reload` do not exist on a worker session (measured: `-32601`), `captureWindow()` skips the reload for a worker-typed resolution and LISTENS on the held connection for `durationMs` instead — keyed off the resolved target's type, not the selector's shape, so a bare worker id takes the same branch as a `worker:` selector. Holding that session is a deliberate side effect, the same pattern as the beacon sessions below: it SUPPRESSES the worker's MV3 idle eviction for as long as the capture runs (measured: alive and still emitting events at 60s held, evicted ~30s after detach). `wake` (default `true` on a capture) restarts an evicted worker first, but is refused on a read-only call and on a bare target id, because a restart mints a NEW target id and the buffer is keyed by the old one — see `workerWakeMisuseMessage` in `src/workers.ts`. |
|
package/README.md
CHANGED
|
@@ -265,6 +265,21 @@ cdp --connect ws://127.0.0.1:9223/session take_snapshot # or the full ws:// UR
|
|
|
265
265
|
|
|
266
266
|
`<endpoint>` accepts three spellings, all normalized to a ws URL: a bare port (`9223`), `host:port` (`127.0.0.1:9223`), or a full `ws://`/`wss://` URL. `--connect`/`CDP_FIREFOX_ENDPOINT` implies the Firefox backend on its own and errors against an explicit `--browser chrome`.
|
|
267
267
|
|
|
268
|
+
**On Linux, and over SSH to a remote host.** Attach is a plain loopback WebSocket, so it is byte-identical on every OS; only the *launch* half is platform-specific. On Linux the binary is `firefox` on `PATH` (or `/usr/bin/firefox`), and the same `--remote-debugging-port 9223` opens the endpoint. One catch worth stating plainly: if your everyday Firefox is already running, relaunching it with the flag just focuses the open window and opens no port (see "Attaching is not relaunching" below). To attach to your real logged-in profile, quit Firefox first and reopen it with the flag; to run beside your daily browser instead, use the separate `--no-remote -profile` instance shown above.
|
|
269
|
+
|
|
270
|
+
When Firefox runs on a different host than cdp-toolkit (a remote box, a container), its debug port binds to loopback *there*, so forward the port and attach to localhost:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
ssh -L 9223:127.0.0.1:9223 you@the-box # forward the remote debug port
|
|
274
|
+
cdp --connect 9223 take_snapshot # then attach as if it were local
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
For Claude Code, register the MCP server with the endpoint in one line (the env var implies the Firefox backend):
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
claude mcp add cdp-toolkit -e CDP_FIREFOX_ENDPOINT=9223 -- npx -y cdp-toolkit
|
|
281
|
+
```
|
|
282
|
+
|
|
268
283
|
**Firefox allows only one active WebDriver BiDi session at a time.** A second `session.new` against the same endpoint fails outright while a first session is open. Disposing cleanly (the normal exit path, both CLI and MCP shutdown) sends `session.end` first, which frees the slot for the next attach. A client that dies **without** disposing — a `SIGKILL`, a crash — leaves the slot occupied: Firefox does not reap an abandoned session on its own. A colliding attach against an occupied slot now fails fast with an actionable error naming the endpoint, rather than hanging forever (fixed in 1.9.4); recover by closing the other client or restarting Firefox.
|
|
269
284
|
|
|
270
285
|
**Attaching is not relaunching.** The one thing that is genuinely impossible is handing a debug port to an *already-running* Firefox process after the fact: the `--remote-debugging-port` flag only takes effect on a process's original launch, so relaunching the `firefox` binary against a running instance hands off to it and exits silently, opening no port (verified against Firefox 153.0.3). That is a real, narrow limitation of the Firefox binary itself. It is not the same claim as "Firefox cannot be attached to" — a Firefox that was launched *with* the debug port open, whether by this toolkit or by your own hand, exposes a plain BiDi endpoint that any number of fresh clients can connect to later, which is exactly what `--connect`/`CDP_FIREFOX_ENDPOINT` does.
|
|
@@ -289,7 +304,7 @@ Everything else, including `mock_request`/`list_mocks`/`clear_mocks` (Firefox's
|
|
|
289
304
|
- **`locate.text` is not available** (Firefox 153's `browsingContext.locateNodes` rejects the `innerText` locator type as unsupported), unlike Chrome, which has it via `DOM.performSearch`.
|
|
290
305
|
- **No modifier-key clicks.** `click`'s `modifiers` (Alt/Control/Meta/Shift) is a Chrome-only *parameter* on an otherwise-universal tool: a non-empty `modifiers` array throws under `--browser firefox` rather than being silently dropped. A plain click still works on both backends.
|
|
291
306
|
- **No real HTML5 drag-and-drop mode.** `drag`'s `mode:"html5"` requires capability `input.html5Drag` and is rejected with a clear error under Firefox; `mode:"mouse"` (the default) works on both.
|
|
292
|
-
- **No per-capture screenshot `scale
|
|
307
|
+
- **No per-capture screenshot `scale`; band tiling is a Chrome-only workaround Firefox has no use for.** Two *parameters* on an otherwise-universal tool, for two different reasons. `scale` (`screenshot.scale`) is a genuine protocol gap: `browsingContext.captureScreenshot` has no scale parameter at all, and the refusal points at `emulate {deviceScaleFactor}` + a scale-1 capture instead — Firefox captures are always 1x. `tile:true` (`screenshot.tile`) is **not** a Firefox gap: measured against Firefox 153.0.3, `take_screenshot --fullPage true` on a 20,000px-tall page returned one 1366×20000 PNG at scale 1 from a single BiDi `captureScreenshot(origin:"document")` call — verified complete top-to-bottom (a marker at y=0, a marker at y=20000, an unbroken ruler through the middle, no truncation). Chrome's banding exists solely to route around its 16384-device-px encode cap; Firefox has no such cap, so there is nothing for tiling to work around, and auto-tiling correctly never fires there — a Firefox `fullPage` capture is already the whole page in one shot, however long. **`renderWidth`/`renderHeight` (`screenshot.renderSize`) are available on both backends** — that one is not a gap.
|
|
293
308
|
|
|
294
309
|
## The tools (29 parity + 16 superset = 45)
|
|
295
310
|
|
package/dist/cli.js
CHANGED
|
@@ -3702,12 +3702,14 @@ import { readFile as readFile4 } from "node:fs/promises";
|
|
|
3702
3702
|
|
|
3703
3703
|
// src/tools/recorder.ts
|
|
3704
3704
|
import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
|
|
3705
|
-
|
|
3705
|
+
function artifactDir() {
|
|
3706
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3707
|
+
}
|
|
3706
3708
|
function recFile(targetId) {
|
|
3707
|
-
return `${
|
|
3709
|
+
return `${artifactDir()}/rec-${targetId}.jsonl`;
|
|
3708
3710
|
}
|
|
3709
3711
|
function captureFile(targetId, captureId) {
|
|
3710
|
-
return `${
|
|
3712
|
+
return `${artifactDir()}/rec-${targetId}-${captureId}.jsonl`;
|
|
3711
3713
|
}
|
|
3712
3714
|
function delay2(ms) {
|
|
3713
3715
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -3747,7 +3749,7 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
3747
3749
|
async function startRecorder(target, opts = {}) {
|
|
3748
3750
|
const network = opts.network ?? true;
|
|
3749
3751
|
const consoleCap = opts.console ?? true;
|
|
3750
|
-
await mkdir6(
|
|
3752
|
+
await mkdir6(artifactDir(), { recursive: true });
|
|
3751
3753
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
3752
3754
|
const file = opts.file ?? recFile(resolved.id);
|
|
3753
3755
|
if (opts.truncate) {
|
|
@@ -4107,7 +4109,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
4107
4109
|
// src/tools/performance.ts
|
|
4108
4110
|
import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
|
|
4109
4111
|
import { join as join5 } from "node:path";
|
|
4110
|
-
var
|
|
4112
|
+
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4111
4113
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
4112
4114
|
var DEFAULT_CATEGORIES = [
|
|
4113
4115
|
"devtools.timeline",
|
|
@@ -4146,8 +4148,8 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
4146
4148
|
return sink;
|
|
4147
4149
|
}
|
|
4148
4150
|
async function writeTrace(events, targetId) {
|
|
4149
|
-
await mkdir7(
|
|
4150
|
-
const path = join5(
|
|
4151
|
+
await mkdir7(ARTIFACT_DIR2, { recursive: true });
|
|
4152
|
+
const path = join5(ARTIFACT_DIR2, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
4151
4153
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
4152
4154
|
await writeFile6(path, body, "utf8");
|
|
4153
4155
|
const { size } = await stat(path);
|
|
@@ -4340,7 +4342,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
4340
4342
|
import { spawn } from "node:child_process";
|
|
4341
4343
|
import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
|
|
4342
4344
|
import { join as join6 } from "node:path";
|
|
4343
|
-
var
|
|
4345
|
+
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4344
4346
|
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
4345
4347
|
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
4346
4348
|
return ledger.map((entry, i) => {
|
|
@@ -4560,7 +4562,7 @@ async function startScreenRecording(args = {}) {
|
|
|
4560
4562
|
conn.close();
|
|
4561
4563
|
throw err;
|
|
4562
4564
|
}
|
|
4563
|
-
const spoolDir = join6(
|
|
4565
|
+
const spoolDir = join6(ARTIFACT_DIR3, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
4564
4566
|
try {
|
|
4565
4567
|
await mkdir8(spoolDir, { recursive: true });
|
|
4566
4568
|
} catch (err) {
|
|
@@ -4666,8 +4668,8 @@ async function stopScreenRecording(args = {}) {
|
|
|
4666
4668
|
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
4667
4669
|
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
4668
4670
|
}
|
|
4669
|
-
await mkdir8(
|
|
4670
|
-
const outPath = args.savePath ?? join6(
|
|
4671
|
+
await mkdir8(ARTIFACT_DIR3, { recursive: true });
|
|
4672
|
+
const outPath = args.savePath ?? join6(ARTIFACT_DIR3, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
4671
4673
|
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
4672
4674
|
const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
|
|
4673
4675
|
await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
|
|
@@ -4703,7 +4705,7 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
|
4703
4705
|
// src/tools/heap.ts
|
|
4704
4706
|
import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
|
|
4705
4707
|
import { join as join7 } from "node:path";
|
|
4706
|
-
var
|
|
4708
|
+
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4707
4709
|
function stamp4(targetId) {
|
|
4708
4710
|
const short = targetId.slice(0, 8);
|
|
4709
4711
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -4727,8 +4729,8 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4727
4729
|
if (snapshot.length === 0) {
|
|
4728
4730
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
4729
4731
|
}
|
|
4730
|
-
await mkdir9(
|
|
4731
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(
|
|
4732
|
+
await mkdir9(ARTIFACT_DIR4, { recursive: true });
|
|
4733
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR4, args.savePath) : join7(ARTIFACT_DIR4, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
4732
4734
|
await writeFile8(path, snapshot, "utf8");
|
|
4733
4735
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
4734
4736
|
return {
|
|
@@ -4743,7 +4745,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4743
4745
|
// src/tools/lighthouse.ts
|
|
4744
4746
|
import { spawn as spawn2 } from "node:child_process";
|
|
4745
4747
|
import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
4746
|
-
var
|
|
4748
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4747
4749
|
function cdpPort() {
|
|
4748
4750
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
4749
4751
|
const m = base.match(/:(\d+)/);
|
|
@@ -4803,8 +4805,8 @@ async function lighthouseAudit(args) {
|
|
|
4803
4805
|
const url = args.url.trim();
|
|
4804
4806
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
4805
4807
|
const port = cdpPort();
|
|
4806
|
-
await mkdir10(
|
|
4807
|
-
const outPath = args.savePath ?? `${
|
|
4808
|
+
await mkdir10(ARTIFACT_DIR5, { recursive: true });
|
|
4809
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
4808
4810
|
const formFactor = args.formFactor ?? "desktop";
|
|
4809
4811
|
const lhArgs = [
|
|
4810
4812
|
"--yes",
|
|
@@ -4925,8 +4927,8 @@ import { basename, extname, join as join9 } from "node:path";
|
|
|
4925
4927
|
// src/tools/browser-session.ts
|
|
4926
4928
|
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
4927
4929
|
import { join as join8 } from "node:path";
|
|
4928
|
-
var
|
|
4929
|
-
var DOWNLOADS_DIR = join8(
|
|
4930
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4931
|
+
var DOWNLOADS_DIR = join8(ARTIFACT_DIR6, "downloads");
|
|
4930
4932
|
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
4931
4933
|
var session;
|
|
4932
4934
|
function isDisconnect(e) {
|
|
@@ -5235,6 +5237,9 @@ class BidiConnection {
|
|
|
5235
5237
|
get holders() {
|
|
5236
5238
|
return this.refCount;
|
|
5237
5239
|
}
|
|
5240
|
+
get isOpen() {
|
|
5241
|
+
return !this.closed && this.ws !== undefined;
|
|
5242
|
+
}
|
|
5238
5243
|
connect() {
|
|
5239
5244
|
return new Promise((resolve, reject) => {
|
|
5240
5245
|
const ws = new WebSocket(this.wsUrl);
|
|
@@ -5969,10 +5974,12 @@ var BIDI_CAPABILITIES = new Set([
|
|
|
5969
5974
|
var connections = new Map;
|
|
5970
5975
|
async function getConnection(endpoint, timeoutMs) {
|
|
5971
5976
|
const existing = connections.get(endpoint);
|
|
5972
|
-
if (existing) {
|
|
5977
|
+
if (existing && existing.isOpen) {
|
|
5973
5978
|
existing.retain();
|
|
5974
5979
|
return existing;
|
|
5975
5980
|
}
|
|
5981
|
+
if (existing)
|
|
5982
|
+
connections.delete(endpoint);
|
|
5976
5983
|
const conn = await connectBidiSessionUrl(endpoint, {
|
|
5977
5984
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
5978
5985
|
capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } }
|
|
@@ -6768,7 +6775,7 @@ function createFirefoxDriverForEndpoint(wsUrl, opts) {
|
|
|
6768
6775
|
|
|
6769
6776
|
// src/bidi/launch.ts
|
|
6770
6777
|
import { spawn as spawn3 } from "node:child_process";
|
|
6771
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
6778
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
6772
6779
|
import { createConnection, createServer } from "node:net";
|
|
6773
6780
|
import { tmpdir } from "node:os";
|
|
6774
6781
|
import { join as join10 } from "node:path";
|
|
@@ -6779,8 +6786,12 @@ function resolveBinary() {
|
|
|
6779
6786
|
"firefox"
|
|
6780
6787
|
];
|
|
6781
6788
|
for (const c of candidates) {
|
|
6782
|
-
if (c)
|
|
6783
|
-
|
|
6789
|
+
if (!c)
|
|
6790
|
+
continue;
|
|
6791
|
+
const isAbsolute = c.startsWith("/");
|
|
6792
|
+
if (isAbsolute && !existsSync(c))
|
|
6793
|
+
continue;
|
|
6794
|
+
return c;
|
|
6784
6795
|
}
|
|
6785
6796
|
throw new Error("Firefox binary not found. Tried FIREFOX_BIN env var, " + "/Applications/Firefox.app/Contents/MacOS/firefox, and 'firefox' on PATH.");
|
|
6786
6797
|
}
|
|
@@ -8065,14 +8076,11 @@ var MANIFEST = [
|
|
|
8065
8076
|
description: "CSS selector for the file input element (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
8066
8077
|
},
|
|
8067
8078
|
files: {
|
|
8068
|
-
type:
|
|
8069
|
-
"string",
|
|
8070
|
-
"array"
|
|
8071
|
-
],
|
|
8079
|
+
type: "array",
|
|
8072
8080
|
items: {
|
|
8073
8081
|
type: "string"
|
|
8074
8082
|
},
|
|
8075
|
-
description:
|
|
8083
|
+
description: 'Absolute path(s) to the file(s) to attach to the <input type=file>, as an array of absolute paths. A single path must be wrapped in an array (e.g. ["/tmp/a.pdf"]). Declared array-only because some model APIs (Google Gemini function calling) reject union-typed parameters.'
|
|
8076
8084
|
}
|
|
8077
8085
|
},
|
|
8078
8086
|
required: [
|
package/dist/index.js
CHANGED
|
@@ -3700,12 +3700,14 @@ import { readFile as readFile4 } from "node:fs/promises";
|
|
|
3700
3700
|
|
|
3701
3701
|
// src/tools/recorder.ts
|
|
3702
3702
|
import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
|
|
3703
|
-
|
|
3703
|
+
function artifactDir() {
|
|
3704
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3705
|
+
}
|
|
3704
3706
|
function recFile(targetId) {
|
|
3705
|
-
return `${
|
|
3707
|
+
return `${artifactDir()}/rec-${targetId}.jsonl`;
|
|
3706
3708
|
}
|
|
3707
3709
|
function captureFile(targetId, captureId) {
|
|
3708
|
-
return `${
|
|
3710
|
+
return `${artifactDir()}/rec-${targetId}-${captureId}.jsonl`;
|
|
3709
3711
|
}
|
|
3710
3712
|
function delay2(ms) {
|
|
3711
3713
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -3745,7 +3747,7 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
3745
3747
|
async function startRecorder(target, opts = {}) {
|
|
3746
3748
|
const network = opts.network ?? true;
|
|
3747
3749
|
const consoleCap = opts.console ?? true;
|
|
3748
|
-
await mkdir6(
|
|
3750
|
+
await mkdir6(artifactDir(), { recursive: true });
|
|
3749
3751
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
3750
3752
|
const file = opts.file ?? recFile(resolved.id);
|
|
3751
3753
|
if (opts.truncate) {
|
|
@@ -4105,7 +4107,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
4105
4107
|
// src/tools/performance.ts
|
|
4106
4108
|
import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
|
|
4107
4109
|
import { join as join5 } from "node:path";
|
|
4108
|
-
var
|
|
4110
|
+
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4109
4111
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
4110
4112
|
var DEFAULT_CATEGORIES = [
|
|
4111
4113
|
"devtools.timeline",
|
|
@@ -4144,8 +4146,8 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
4144
4146
|
return sink;
|
|
4145
4147
|
}
|
|
4146
4148
|
async function writeTrace(events, targetId) {
|
|
4147
|
-
await mkdir7(
|
|
4148
|
-
const path = join5(
|
|
4149
|
+
await mkdir7(ARTIFACT_DIR2, { recursive: true });
|
|
4150
|
+
const path = join5(ARTIFACT_DIR2, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
4149
4151
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
4150
4152
|
await writeFile6(path, body, "utf8");
|
|
4151
4153
|
const { size } = await stat(path);
|
|
@@ -4338,7 +4340,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
4338
4340
|
import { spawn } from "node:child_process";
|
|
4339
4341
|
import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
|
|
4340
4342
|
import { join as join6 } from "node:path";
|
|
4341
|
-
var
|
|
4343
|
+
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4342
4344
|
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
4343
4345
|
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
4344
4346
|
return ledger.map((entry, i) => {
|
|
@@ -4558,7 +4560,7 @@ async function startScreenRecording(args = {}) {
|
|
|
4558
4560
|
conn.close();
|
|
4559
4561
|
throw err;
|
|
4560
4562
|
}
|
|
4561
|
-
const spoolDir = join6(
|
|
4563
|
+
const spoolDir = join6(ARTIFACT_DIR3, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
4562
4564
|
try {
|
|
4563
4565
|
await mkdir8(spoolDir, { recursive: true });
|
|
4564
4566
|
} catch (err) {
|
|
@@ -4664,8 +4666,8 @@ async function stopScreenRecording(args = {}) {
|
|
|
4664
4666
|
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
4665
4667
|
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
4666
4668
|
}
|
|
4667
|
-
await mkdir8(
|
|
4668
|
-
const outPath = args.savePath ?? join6(
|
|
4669
|
+
await mkdir8(ARTIFACT_DIR3, { recursive: true });
|
|
4670
|
+
const outPath = args.savePath ?? join6(ARTIFACT_DIR3, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
4669
4671
|
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
4670
4672
|
const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
|
|
4671
4673
|
await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
|
|
@@ -4701,7 +4703,7 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
|
4701
4703
|
// src/tools/heap.ts
|
|
4702
4704
|
import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
|
|
4703
4705
|
import { join as join7 } from "node:path";
|
|
4704
|
-
var
|
|
4706
|
+
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4705
4707
|
function stamp4(targetId) {
|
|
4706
4708
|
const short = targetId.slice(0, 8);
|
|
4707
4709
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -4725,8 +4727,8 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4725
4727
|
if (snapshot.length === 0) {
|
|
4726
4728
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
4727
4729
|
}
|
|
4728
|
-
await mkdir9(
|
|
4729
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(
|
|
4730
|
+
await mkdir9(ARTIFACT_DIR4, { recursive: true });
|
|
4731
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR4, args.savePath) : join7(ARTIFACT_DIR4, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
4730
4732
|
await writeFile8(path, snapshot, "utf8");
|
|
4731
4733
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
4732
4734
|
return {
|
|
@@ -4741,7 +4743,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4741
4743
|
// src/tools/lighthouse.ts
|
|
4742
4744
|
import { spawn as spawn2 } from "node:child_process";
|
|
4743
4745
|
import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
4744
|
-
var
|
|
4746
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4745
4747
|
function cdpPort() {
|
|
4746
4748
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
4747
4749
|
const m = base.match(/:(\d+)/);
|
|
@@ -4801,8 +4803,8 @@ async function lighthouseAudit(args) {
|
|
|
4801
4803
|
const url = args.url.trim();
|
|
4802
4804
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
4803
4805
|
const port = cdpPort();
|
|
4804
|
-
await mkdir10(
|
|
4805
|
-
const outPath = args.savePath ?? `${
|
|
4806
|
+
await mkdir10(ARTIFACT_DIR5, { recursive: true });
|
|
4807
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
4806
4808
|
const formFactor = args.formFactor ?? "desktop";
|
|
4807
4809
|
const lhArgs = [
|
|
4808
4810
|
"--yes",
|
|
@@ -4923,8 +4925,8 @@ import { basename, extname, join as join9 } from "node:path";
|
|
|
4923
4925
|
// src/tools/browser-session.ts
|
|
4924
4926
|
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
4925
4927
|
import { join as join8 } from "node:path";
|
|
4926
|
-
var
|
|
4927
|
-
var DOWNLOADS_DIR = join8(
|
|
4928
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4929
|
+
var DOWNLOADS_DIR = join8(ARTIFACT_DIR6, "downloads");
|
|
4928
4930
|
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
4929
4931
|
var session;
|
|
4930
4932
|
function isDisconnect(e) {
|
package/dist/mcp.js
CHANGED
|
@@ -3702,12 +3702,14 @@ import { readFile as readFile4 } from "node:fs/promises";
|
|
|
3702
3702
|
|
|
3703
3703
|
// src/tools/recorder.ts
|
|
3704
3704
|
import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
|
|
3705
|
-
|
|
3705
|
+
function artifactDir() {
|
|
3706
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3707
|
+
}
|
|
3706
3708
|
function recFile(targetId) {
|
|
3707
|
-
return `${
|
|
3709
|
+
return `${artifactDir()}/rec-${targetId}.jsonl`;
|
|
3708
3710
|
}
|
|
3709
3711
|
function captureFile(targetId, captureId) {
|
|
3710
|
-
return `${
|
|
3712
|
+
return `${artifactDir()}/rec-${targetId}-${captureId}.jsonl`;
|
|
3711
3713
|
}
|
|
3712
3714
|
function delay2(ms) {
|
|
3713
3715
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -3747,7 +3749,7 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
3747
3749
|
async function startRecorder(target, opts = {}) {
|
|
3748
3750
|
const network = opts.network ?? true;
|
|
3749
3751
|
const consoleCap = opts.console ?? true;
|
|
3750
|
-
await mkdir6(
|
|
3752
|
+
await mkdir6(artifactDir(), { recursive: true });
|
|
3751
3753
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
3752
3754
|
const file = opts.file ?? recFile(resolved.id);
|
|
3753
3755
|
if (opts.truncate) {
|
|
@@ -4107,7 +4109,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
4107
4109
|
// src/tools/performance.ts
|
|
4108
4110
|
import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
|
|
4109
4111
|
import { join as join5 } from "node:path";
|
|
4110
|
-
var
|
|
4112
|
+
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4111
4113
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
4112
4114
|
var DEFAULT_CATEGORIES = [
|
|
4113
4115
|
"devtools.timeline",
|
|
@@ -4146,8 +4148,8 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
4146
4148
|
return sink;
|
|
4147
4149
|
}
|
|
4148
4150
|
async function writeTrace(events, targetId) {
|
|
4149
|
-
await mkdir7(
|
|
4150
|
-
const path = join5(
|
|
4151
|
+
await mkdir7(ARTIFACT_DIR2, { recursive: true });
|
|
4152
|
+
const path = join5(ARTIFACT_DIR2, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
4151
4153
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
4152
4154
|
await writeFile6(path, body, "utf8");
|
|
4153
4155
|
const { size } = await stat(path);
|
|
@@ -4340,7 +4342,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
4340
4342
|
import { spawn } from "node:child_process";
|
|
4341
4343
|
import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
|
|
4342
4344
|
import { join as join6 } from "node:path";
|
|
4343
|
-
var
|
|
4345
|
+
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4344
4346
|
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
4345
4347
|
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
4346
4348
|
return ledger.map((entry, i) => {
|
|
@@ -4560,7 +4562,7 @@ async function startScreenRecording(args = {}) {
|
|
|
4560
4562
|
conn.close();
|
|
4561
4563
|
throw err;
|
|
4562
4564
|
}
|
|
4563
|
-
const spoolDir = join6(
|
|
4565
|
+
const spoolDir = join6(ARTIFACT_DIR3, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
4564
4566
|
try {
|
|
4565
4567
|
await mkdir8(spoolDir, { recursive: true });
|
|
4566
4568
|
} catch (err) {
|
|
@@ -4666,8 +4668,8 @@ async function stopScreenRecording(args = {}) {
|
|
|
4666
4668
|
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
4667
4669
|
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
4668
4670
|
}
|
|
4669
|
-
await mkdir8(
|
|
4670
|
-
const outPath = args.savePath ?? join6(
|
|
4671
|
+
await mkdir8(ARTIFACT_DIR3, { recursive: true });
|
|
4672
|
+
const outPath = args.savePath ?? join6(ARTIFACT_DIR3, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
4671
4673
|
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
4672
4674
|
const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
|
|
4673
4675
|
await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
|
|
@@ -4703,7 +4705,7 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
|
4703
4705
|
// src/tools/heap.ts
|
|
4704
4706
|
import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
|
|
4705
4707
|
import { join as join7 } from "node:path";
|
|
4706
|
-
var
|
|
4708
|
+
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4707
4709
|
function stamp4(targetId) {
|
|
4708
4710
|
const short = targetId.slice(0, 8);
|
|
4709
4711
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -4727,8 +4729,8 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4727
4729
|
if (snapshot.length === 0) {
|
|
4728
4730
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
4729
4731
|
}
|
|
4730
|
-
await mkdir9(
|
|
4731
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(
|
|
4732
|
+
await mkdir9(ARTIFACT_DIR4, { recursive: true });
|
|
4733
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR4, args.savePath) : join7(ARTIFACT_DIR4, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
4732
4734
|
await writeFile8(path, snapshot, "utf8");
|
|
4733
4735
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
4734
4736
|
return {
|
|
@@ -4743,7 +4745,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
4743
4745
|
// src/tools/lighthouse.ts
|
|
4744
4746
|
import { spawn as spawn2 } from "node:child_process";
|
|
4745
4747
|
import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
4746
|
-
var
|
|
4748
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4747
4749
|
function cdpPort() {
|
|
4748
4750
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
4749
4751
|
const m = base.match(/:(\d+)/);
|
|
@@ -4803,8 +4805,8 @@ async function lighthouseAudit(args) {
|
|
|
4803
4805
|
const url = args.url.trim();
|
|
4804
4806
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
4805
4807
|
const port = cdpPort();
|
|
4806
|
-
await mkdir10(
|
|
4807
|
-
const outPath = args.savePath ?? `${
|
|
4808
|
+
await mkdir10(ARTIFACT_DIR5, { recursive: true });
|
|
4809
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
4808
4810
|
const formFactor = args.formFactor ?? "desktop";
|
|
4809
4811
|
const lhArgs = [
|
|
4810
4812
|
"--yes",
|
|
@@ -4925,8 +4927,8 @@ import { basename, extname, join as join9 } from "node:path";
|
|
|
4925
4927
|
// src/tools/browser-session.ts
|
|
4926
4928
|
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
4927
4929
|
import { join as join8 } from "node:path";
|
|
4928
|
-
var
|
|
4929
|
-
var DOWNLOADS_DIR = join8(
|
|
4930
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4931
|
+
var DOWNLOADS_DIR = join8(ARTIFACT_DIR6, "downloads");
|
|
4930
4932
|
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
4931
4933
|
var session;
|
|
4932
4934
|
function isDisconnect(e) {
|
|
@@ -6072,14 +6074,11 @@ var MANIFEST = [
|
|
|
6072
6074
|
description: "CSS selector for the file input element (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
6073
6075
|
},
|
|
6074
6076
|
files: {
|
|
6075
|
-
type:
|
|
6076
|
-
"string",
|
|
6077
|
-
"array"
|
|
6078
|
-
],
|
|
6077
|
+
type: "array",
|
|
6079
6078
|
items: {
|
|
6080
6079
|
type: "string"
|
|
6081
6080
|
},
|
|
6082
|
-
description:
|
|
6081
|
+
description: 'Absolute path(s) to the file(s) to attach to the <input type=file>, as an array of absolute paths. A single path must be wrapped in an array (e.g. ["/tmp/a.pdf"]). Declared array-only because some model APIs (Google Gemini function calling) reject union-typed parameters.'
|
|
6083
6082
|
}
|
|
6084
6083
|
},
|
|
6085
6084
|
required: [
|
|
@@ -6913,6 +6912,9 @@ class BidiConnection {
|
|
|
6913
6912
|
get holders() {
|
|
6914
6913
|
return this.refCount;
|
|
6915
6914
|
}
|
|
6915
|
+
get isOpen() {
|
|
6916
|
+
return !this.closed && this.ws !== undefined;
|
|
6917
|
+
}
|
|
6916
6918
|
connect() {
|
|
6917
6919
|
return new Promise((resolve, reject) => {
|
|
6918
6920
|
const ws = new WebSocket(this.wsUrl);
|
|
@@ -7647,10 +7649,12 @@ var BIDI_CAPABILITIES = new Set([
|
|
|
7647
7649
|
var connections = new Map;
|
|
7648
7650
|
async function getConnection(endpoint, timeoutMs) {
|
|
7649
7651
|
const existing = connections.get(endpoint);
|
|
7650
|
-
if (existing) {
|
|
7652
|
+
if (existing && existing.isOpen) {
|
|
7651
7653
|
existing.retain();
|
|
7652
7654
|
return existing;
|
|
7653
7655
|
}
|
|
7656
|
+
if (existing)
|
|
7657
|
+
connections.delete(endpoint);
|
|
7654
7658
|
const conn = await connectBidiSessionUrl(endpoint, {
|
|
7655
7659
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
7656
7660
|
capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } }
|
|
@@ -8446,7 +8450,7 @@ function createFirefoxDriverForEndpoint(wsUrl, opts) {
|
|
|
8446
8450
|
|
|
8447
8451
|
// src/bidi/launch.ts
|
|
8448
8452
|
import { spawn as spawn3 } from "node:child_process";
|
|
8449
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
8453
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
8450
8454
|
import { createConnection, createServer } from "node:net";
|
|
8451
8455
|
import { tmpdir } from "node:os";
|
|
8452
8456
|
import { join as join10 } from "node:path";
|
|
@@ -8457,8 +8461,12 @@ function resolveBinary() {
|
|
|
8457
8461
|
"firefox"
|
|
8458
8462
|
];
|
|
8459
8463
|
for (const c of candidates) {
|
|
8460
|
-
if (c)
|
|
8461
|
-
|
|
8464
|
+
if (!c)
|
|
8465
|
+
continue;
|
|
8466
|
+
const isAbsolute = c.startsWith("/");
|
|
8467
|
+
if (isAbsolute && !existsSync(c))
|
|
8468
|
+
continue;
|
|
8469
|
+
return c;
|
|
8462
8470
|
}
|
|
8463
8471
|
throw new Error("Firefox binary not found. Tried FIREFOX_BIN env var, " + "/Applications/Firefox.app/Contents/MacOS/firefox, and 'firefox' on PATH.");
|
|
8464
8472
|
}
|
|
@@ -8874,7 +8882,7 @@ var FIREFOX_TOOLS = {
|
|
|
8874
8882
|
};
|
|
8875
8883
|
|
|
8876
8884
|
// src/mcp.ts
|
|
8877
|
-
var VERSION = "1.9.
|
|
8885
|
+
var VERSION = "1.9.7";
|
|
8878
8886
|
var { kind: BROWSER, endpoint: FIREFOX_ENDPOINT } = resolveBackend(process.argv.slice(2));
|
|
8879
8887
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
8880
8888
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-toolkit",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.7",
|
|
4
4
|
"description": "A lightweight chrome-devtools-mcp alternative: an MCP server + CLI that drives the tabs you name, one target per call over one direct WebSocket, with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. Chrome over CDP and Firefox over WebDriver BiDi, with tools a backend can't run absent from tools/list rather than throwing mid-task. 45 tools including built-in network mocking, a cookie group, tab leases with human-activity detection, real HTML5 drag-and-drop, raw mouse/scroll dispatch, downloads, permission grants, and tab-to-video screen recording so several agents can drive one browser without stealing each other's tabs or a human's. No Puppeteer, no all-target fan-out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/bidi/client.ts
CHANGED
|
@@ -88,6 +88,17 @@ export class BidiConnection {
|
|
|
88
88
|
return this.refCount;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* True while this connection still has a live socket. Goes false once the
|
|
93
|
+
* socket closes (ws.onclose) or dispose() runs — both set `closed` and
|
|
94
|
+
* reject all in-flight calls. getConnection uses this to skip and evict a
|
|
95
|
+
* dead cached connection so the next call re-dials a fresh session instead
|
|
96
|
+
* of reusing a socket that can only reject with "connection not open".
|
|
97
|
+
*/
|
|
98
|
+
get isOpen(): boolean {
|
|
99
|
+
return !this.closed && this.ws !== undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
connect(): Promise<this> {
|
|
92
103
|
return new Promise((resolve, reject) => {
|
|
93
104
|
const ws = new WebSocket(this.wsUrl);
|
package/src/bidi/driver.ts
CHANGED
|
@@ -301,11 +301,13 @@ const AWAIT_SCROLL_SETTLE_SOURCE = "function(){return window.__cdpScrollSettle;}
|
|
|
301
301
|
* above already rides, applied for one capture and undone afterwards. Refusing it would be dishonest
|
|
302
302
|
* in the opposite direction — claiming a missing primitive this driver demonstrably has. Contrast
|
|
303
303
|
* screenshot.scale, absent because captureScreenshot genuinely has no scale parameter.
|
|
304
|
-
* screenshot.tile
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* the
|
|
304
|
+
* screenshot.tile stays undeclared, but no longer for an unmeasured reason: live-fired against
|
|
305
|
+
* Firefox 153.0.3, a 20,000px-tall page captured with take_screenshot --fullPage true came back
|
|
306
|
+
* as a single 1366x20000 PNG at scale 1, verified complete top-to-bottom — no clip, no bands.
|
|
307
|
+
* Chrome's band tiling exists solely to route around its 16384-device-px encode cap; Firefox has
|
|
308
|
+
* no such cap, so screenshot.tile (the capability gating explicit tile:true forced banding) has
|
|
309
|
+
* no gap here to close. It stays undeclared because it would advertise a forced-tiling primitive
|
|
310
|
+
* this backend has never needed, not because band-tiling behavior is untested.
|
|
309
311
|
*/
|
|
310
312
|
const BIDI_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
311
313
|
"emulate.deviceMetrics", "screenshot.fullPage", "screenshot.element", "screenshot.renderSize", "network.intercept", "locate.xpath",
|
|
@@ -320,7 +322,13 @@ const BIDI_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
|
320
322
|
const connections = new Map<string, BidiConnection>();
|
|
321
323
|
async function getConnection(endpoint: string, timeoutMs?: number): Promise<BidiConnection> {
|
|
322
324
|
const existing = connections.get(endpoint);
|
|
323
|
-
if (existing) { existing.retain(); return existing; }
|
|
325
|
+
if (existing && existing.isOpen) { existing.retain(); return existing; }
|
|
326
|
+
// A cached connection whose socket has died (ws.onclose fired) can only reject every
|
|
327
|
+
// send() with "connection not open" — reusing it would strand the endpoint permanently.
|
|
328
|
+
// Evict it so the code below re-dials and creates a fresh BiDi session. No cleanup is
|
|
329
|
+
// needed: a dead connection already had `closed` set and rejectAll() cleared its per-call
|
|
330
|
+
// timers, and BidiConnection holds no heartbeat/keepalive resource to leak (see client.ts).
|
|
331
|
+
if (existing) connections.delete(endpoint);
|
|
324
332
|
// Firefox 153's default unhandledPromptBehavior is "dismiss": a page-blocking alert()/confirm()
|
|
325
333
|
// gets auto-dismissed before handleDialog() ever runs, and Firefox's own userPromptOpened event
|
|
326
334
|
// reports handler:"dismiss" on it (verified: a bare session.new left "no such alert" on the very
|
|
@@ -882,15 +890,14 @@ class BidiPageDriver implements PageDriver {
|
|
|
882
890
|
`deviceScaleFactor:${opts.scale} first and capture at scale 1, or use --browser chrome.`,
|
|
883
891
|
);
|
|
884
892
|
}
|
|
885
|
-
// Same ADR-001 param-level shape,
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
// never bands here) ask this backend for nothing and are untouched.
|
|
893
|
+
// Same ADR-001 param-level shape, but now a measured non-gap rather than an unmeasured one.
|
|
894
|
+
// Live-fired against Firefox 153.0.3: a 20,000px-tall page captured with take_screenshot
|
|
895
|
+
// --fullPage true came back as one 1366x20000 PNG at scale 1, verified complete top-to-bottom,
|
|
896
|
+
// via the single origin:"document" capture below — no clip, no bands. Chrome's band tiling
|
|
897
|
+
// exists only to route around its 16384-device-px encode cap; this backend has no such cap, so
|
|
898
|
+
// there is nothing for tile:true's forced banding to do here. The refusal below stays because
|
|
899
|
+
// there is no forced-tiling primitive to build, not because it is untested. `tile:false` and
|
|
900
|
+
// the default (auto, which never bands here) ask this backend for nothing and are untouched.
|
|
894
901
|
if (opts?.tile === true) {
|
|
895
902
|
throw driverError(
|
|
896
903
|
"unsupported",
|
|
@@ -1408,13 +1415,14 @@ export { BidiBrowserDriver, BidiPageDriver, resolveContext };
|
|
|
1408
1415
|
* Firefox is next driven: whether a null viewport reverts to the real window (CDP's clear does
|
|
1409
1416
|
* NOT, for an override another session set), and whether setViewport reflows synchronously the
|
|
1410
1417
|
* way Emulation.setDeviceMetricsOverride was measured to on Chrome.
|
|
1411
|
-
* - screenshot.tile: not declared,
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
* backend
|
|
1418
|
+
* - screenshot.tile: not declared, but no longer an unmeasured gap. Live-fired against Firefox
|
|
1419
|
+
* 153.0.3: take_screenshot --fullPage true on a 20,000px-tall page returned one 1366x20000 PNG
|
|
1420
|
+
* at scale 1, verified complete top-to-bottom, via a single origin:"document" capture — no
|
|
1421
|
+
* clip, no bands, no cap hit. Chrome's band tiling exists solely to route around its
|
|
1422
|
+
* 16384-device-px encode cap; Firefox has no such cap, so screenshot.tile (forced tiling via
|
|
1423
|
+
* tile:true) has nothing to do here. `tile:true` is still a refusal in screenshot() naming
|
|
1424
|
+
* --browser chrome; the default (auto) still never bands on this backend — both unchanged, now
|
|
1425
|
+
* for a documented reason instead of an untested one.
|
|
1418
1426
|
* - emulate.mediaFeatures / emulate.cpuThrottling / emulate.networkConditions: not declared.
|
|
1419
1427
|
* Firefox 153 does not implement setMediaFeaturesOverride; CPU throttling and network condition
|
|
1420
1428
|
* emulation have no verified-working BiDi path in this environment, so emulate() never claims
|
package/src/bidi/launch.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* node:os and node:path.
|
|
23
23
|
*/
|
|
24
24
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
25
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
25
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
26
26
|
import { createConnection, createServer } from "node:net";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
28
|
import { join } from "node:path";
|
|
@@ -58,7 +58,14 @@ function resolveBinary(): string {
|
|
|
58
58
|
"firefox",
|
|
59
59
|
];
|
|
60
60
|
for (const c of candidates) {
|
|
61
|
-
if (c)
|
|
61
|
+
if (!c) continue;
|
|
62
|
+
// An ABSOLUTE candidate (FIREFOX_BIN, the macOS app path) is only usable if it exists on this
|
|
63
|
+
// box — otherwise the hardcoded macOS path would win on Linux and shadow the "firefox" PATH
|
|
64
|
+
// fallback, yielding ENOENT at spawn. A bare name like "firefox" is not a filesystem path, so
|
|
65
|
+
// let spawn resolve it against PATH.
|
|
66
|
+
const isAbsolute = c.startsWith("/");
|
|
67
|
+
if (isAbsolute && !existsSync(c)) continue;
|
|
68
|
+
return c;
|
|
62
69
|
}
|
|
63
70
|
throw new Error(
|
|
64
71
|
"Firefox binary not found. Tried FIREFOX_BIN env var, " +
|
package/src/manifest.ts
CHANGED
|
@@ -891,14 +891,11 @@ export const MANIFEST: ToolSpec[] = [
|
|
|
891
891
|
"description": "CSS selector for the file input element (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
892
892
|
},
|
|
893
893
|
"files": {
|
|
894
|
-
"type":
|
|
895
|
-
"string",
|
|
896
|
-
"array"
|
|
897
|
-
],
|
|
894
|
+
"type": "array",
|
|
898
895
|
"items": {
|
|
899
896
|
"type": "string"
|
|
900
897
|
},
|
|
901
|
-
"description": "Absolute path
|
|
898
|
+
"description": "Absolute path(s) to the file(s) to attach to the <input type=file>, as an array of absolute paths. A single path must be wrapped in an array (e.g. [\"/tmp/a.pdf\"]). Declared array-only because some model APIs (Google Gemini function calling) reject union-typed parameters."
|
|
902
899
|
}
|
|
903
900
|
},
|
|
904
901
|
"required": [
|
package/src/mcp.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { toolAvailability } from "./capabilities.ts";
|
|
|
26
26
|
import { FIREFOX_TOOLS } from "./firefox-tools.ts";
|
|
27
27
|
import { leaseFromArgs, markLongLivedProcess, withLeaseScope } from "./leases.ts";
|
|
28
28
|
|
|
29
|
-
const VERSION = "1.9.
|
|
29
|
+
const VERSION = "1.9.7";
|
|
30
30
|
|
|
31
31
|
// Backend + (Firefox only) attach endpoint are read once at startup (MCP has no per-call notion
|
|
32
32
|
// of backend): --browser flag / CDP_BROWSER env, else "chrome" (zero behavior change for existing
|
package/src/tools/recorder.ts
CHANGED
|
@@ -80,17 +80,20 @@ import {
|
|
|
80
80
|
WORKER_EMPTY_NEEDLE_MESSAGE,
|
|
81
81
|
} from "../workers.ts";
|
|
82
82
|
|
|
83
|
-
/** Artifact / buffer directory (shared with the rest of the toolkit).
|
|
84
|
-
|
|
83
|
+
/** Artifact / buffer directory (shared with the rest of the toolkit). Read per
|
|
84
|
+
* call, not at module load, so a test can redirect the directory (as leases.ts). */
|
|
85
|
+
function artifactDir(): string {
|
|
86
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
87
|
+
}
|
|
85
88
|
|
|
86
89
|
/** Compute the shared "latest" JSONL buffer path for a given targetId. */
|
|
87
90
|
export function recFile(targetId: string): string {
|
|
88
|
-
return `${
|
|
91
|
+
return `${artifactDir()}/rec-${targetId}.jsonl`;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/** Compute a unique per-capture JSONL buffer path (isolates concurrent captures). */
|
|
92
95
|
export function captureFile(targetId: string, captureId: string): string {
|
|
93
|
-
return `${
|
|
96
|
+
return `${artifactDir()}/rec-${targetId}-${captureId}.jsonl`;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
/** Sleep helper (module runtime only; not the workflow-script sandbox). */
|
|
@@ -200,7 +203,7 @@ export async function startRecorder(target: TargetSelector, opts: RecorderOption
|
|
|
200
203
|
const network = opts.network ?? true;
|
|
201
204
|
const consoleCap = opts.console ?? true;
|
|
202
205
|
|
|
203
|
-
await mkdir(
|
|
206
|
+
await mkdir(artifactDir(), { recursive: true });
|
|
204
207
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
205
208
|
const file = opts.file ?? recFile(resolved.id);
|
|
206
209
|
|