cdp-toolkit 1.9.5 → 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 CHANGED
@@ -5,6 +5,44 @@ 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
+
8
46
  ## [1.9.5] - 2026-08-11
9
47
 
10
48
  **Documentation and stale code-comment clarifications; NO behavior changes since 1.9.4.** Two
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
- var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3705
+ function artifactDir() {
3706
+ return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3707
+ }
3706
3708
  function recFile(targetId) {
3707
- return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
3709
+ return `${artifactDir()}/rec-${targetId}.jsonl`;
3708
3710
  }
3709
3711
  function captureFile(targetId, captureId) {
3710
- return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
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(ARTIFACT_DIR2, { recursive: true });
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 ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR3, { recursive: true });
4150
- const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
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 ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
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(ARTIFACT_DIR4, { recursive: true });
4670
- const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
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 ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR5, { recursive: true });
4731
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
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 ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR6, { recursive: true });
4807
- const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
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 ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4929
- var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
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
- return c;
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: "Absolute path, or array of absolute paths, to the file(s) to attach to the <input type=file>. Accepts a single string or an array of strings."
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
- var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3703
+ function artifactDir() {
3704
+ return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3705
+ }
3704
3706
  function recFile(targetId) {
3705
- return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
3707
+ return `${artifactDir()}/rec-${targetId}.jsonl`;
3706
3708
  }
3707
3709
  function captureFile(targetId, captureId) {
3708
- return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
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(ARTIFACT_DIR2, { recursive: true });
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 ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR3, { recursive: true });
4148
- const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
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 ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
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(ARTIFACT_DIR4, { recursive: true });
4668
- const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
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 ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR5, { recursive: true });
4729
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
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 ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR6, { recursive: true });
4805
- const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
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 ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4927
- var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
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
- var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3705
+ function artifactDir() {
3706
+ return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3707
+ }
3706
3708
  function recFile(targetId) {
3707
- return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
3709
+ return `${artifactDir()}/rec-${targetId}.jsonl`;
3708
3710
  }
3709
3711
  function captureFile(targetId, captureId) {
3710
- return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
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(ARTIFACT_DIR2, { recursive: true });
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 ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR3, { recursive: true });
4150
- const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
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 ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
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(ARTIFACT_DIR4, { recursive: true });
4670
- const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
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 ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR5, { recursive: true });
4731
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
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 ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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(ARTIFACT_DIR6, { recursive: true });
4807
- const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
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 ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4929
- var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
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: "Absolute path, or array of absolute paths, to the file(s) to attach to the <input type=file>. Accepts a single string or an array of strings."
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
- return c;
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.5";
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.5",
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",
@@ -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);
@@ -322,7 +322,13 @@ const BIDI_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
322
322
  const connections = new Map<string, BidiConnection>();
323
323
  async function getConnection(endpoint: string, timeoutMs?: number): Promise<BidiConnection> {
324
324
  const existing = connections.get(endpoint);
325
- 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);
326
332
  // Firefox 153's default unhandledPromptBehavior is "dismiss": a page-blocking alert()/confirm()
327
333
  // gets auto-dismissed before handleDialog() ever runs, and Firefox's own userPromptOpened event
328
334
  // reports handler:"dismiss" on it (verified: a bare session.new left "no such alert" on the very
@@ -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) return 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, or array of absolute paths, to the file(s) to attach to the <input type=file>. Accepts a single string or an array of strings."
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.5";
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
@@ -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
- export const ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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 `${ARTIFACT_DIR}/rec-${targetId}.jsonl`;
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 `${ARTIFACT_DIR}/rec-${targetId}-${captureId}.jsonl`;
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(ARTIFACT_DIR, { recursive: true });
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