jev-cdp 0.1.5 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +105 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,11 +27,11 @@ This is an early experimental port. See [NOTICE.md](NOTICE.md) for source attrib
27
27
  Run the published CLI without adding it to a project. Bun must be installed for either command:
28
28
 
29
29
  ```bash
30
- bunx jev-cdp@0.1.5 help run
31
- npx -y jev-cdp@0.1.5 help run
30
+ bunx jev-cdp@0.1.6 help run
31
+ npx -y jev-cdp@0.1.6 help run
32
32
  ```
33
33
 
34
- Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg is required for `--recording`.
34
+ Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg with H.264 encoding (`libx264`) is required for `--recording`. Jev selects `-fps_mode vfr` when FFmpeg supports it and falls back to `-vsync vfr` for older builds. Run `jev-cdp doctor` to check the installed encoder with a short MP4 encode and decode.
35
35
 
36
36
  For source development:
37
37
 
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "jev-cdp",
6
- version: "0.1.5",
6
+ version: "0.1.6",
7
7
  description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -38,9 +38,9 @@ var package_default = {
38
38
 
39
39
  // src/browser.ts
40
40
  import { createHash } from "crypto";
41
- import { mkdir, mkdtemp, rm } from "fs/promises";
42
- import { tmpdir } from "os";
43
- import { dirname, join, resolve } from "path";
41
+ import { mkdir, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
42
+ import { tmpdir as tmpdir2 } from "os";
43
+ import { dirname, join as join2, resolve } from "path";
44
44
 
45
45
  // src/cdp.ts
46
46
  async function listChromeTargets(cdpUrl) {
@@ -143,6 +143,88 @@ class CdpClient {
143
143
  }
144
144
  }
145
145
 
146
+ // src/ffmpeg.ts
147
+ import { mkdtemp, rm } from "fs/promises";
148
+ import { tmpdir } from "os";
149
+ import { join } from "path";
150
+ function selectFrameSyncOption(help) {
151
+ if (/^\s*-fps_mode(?:\[:<stream_spec>\])?\s/m.test(help))
152
+ return "-fps_mode";
153
+ if (/^\s*-vsync\s/m.test(help))
154
+ return "-vsync";
155
+ throw new Error("FFmpeg supports neither -fps_mode nor -vsync");
156
+ }
157
+ async function run(command) {
158
+ const process2 = Bun.spawn(command, { stdout: "ignore", stderr: "pipe" });
159
+ const stderr = await new Response(process2.stderr).text();
160
+ return { code: await process2.exited, stderr };
161
+ }
162
+ async function recordingEncoder(ffmpeg = Bun.which("ffmpeg")) {
163
+ if (!ffmpeg)
164
+ throw new Error("FFmpeg not found on PATH");
165
+ const process2 = Bun.spawn([ffmpeg, "-hide_banner", "-h", "full"], { stdout: "pipe", stderr: "pipe" });
166
+ const [stdout, stderr, code] = await Promise.all([
167
+ new Response(process2.stdout).text(),
168
+ new Response(process2.stderr).text(),
169
+ process2.exited
170
+ ]);
171
+ if (code !== 0)
172
+ throw new Error(`Could not inspect FFmpeg options: ${stderr.slice(-500)}`);
173
+ return { path: ffmpeg, sync: selectFrameSyncOption(stdout + stderr) };
174
+ }
175
+ function recordingCommand(encoder, manifest, output) {
176
+ return [
177
+ encoder.path,
178
+ "-y",
179
+ "-f",
180
+ "concat",
181
+ "-safe",
182
+ "0",
183
+ "-i",
184
+ manifest,
185
+ encoder.sync,
186
+ "vfr",
187
+ "-c:v",
188
+ "libx264",
189
+ "-pix_fmt",
190
+ "yuv420p",
191
+ "-movflags",
192
+ "+faststart",
193
+ output
194
+ ];
195
+ }
196
+ async function renderRecording(encoder, manifest, output) {
197
+ const result = await run(recordingCommand(encoder, manifest, output));
198
+ if (result.code !== 0)
199
+ throw new Error(`Could not render recording: ${result.stderr.slice(-800)}`);
200
+ }
201
+ async function checkRecordingEncoder() {
202
+ const encoder = await recordingEncoder();
203
+ const directory = await mkdtemp(join(tmpdir(), "jev-cdp-ffmpeg-check-"));
204
+ try {
205
+ const frame = join(directory, "frame.ppm");
206
+ const manifest = join(directory, "frames.ffconcat");
207
+ const output = join(directory, "check.mp4");
208
+ await Bun.write(frame, Buffer.concat([Buffer.from(`P6
209
+ 16 16
210
+ 255
211
+ `), Buffer.alloc(16 * 16 * 3, 90)]));
212
+ const quoted = frame.replaceAll("'", "'\\''");
213
+ await Bun.write(manifest, `ffconcat version 1.0
214
+ file '${quoted}'
215
+ duration 0.12
216
+ file '${quoted}'
217
+ `);
218
+ await renderRecording(encoder, manifest, output);
219
+ const decoded = await run([encoder.path, "-v", "error", "-i", output, "-f", "null", "-"]);
220
+ if (decoded.code !== 0)
221
+ throw new Error(`Could not decode recording: ${decoded.stderr.slice(-500)}`);
222
+ return `FFmpeg at ${encoder.path}; ${encoder.sync} vfr and libx264 encode/decode passed`;
223
+ } finally {
224
+ await rm(directory, { recursive: true, force: true });
225
+ }
226
+ }
227
+
146
228
  // src/snapshot.js
147
229
  var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1ad2 under the MIT License.
148
230
  (() => {
@@ -631,11 +713,10 @@ class Browser {
631
713
  async startRecording() {
632
714
  if (!this.#recordingPath)
633
715
  return;
634
- if (!Bun.which("ffmpeg"))
635
- throw new Error("--recording requires ffmpeg on PATH");
716
+ await recordingEncoder();
636
717
  const firstSegment = !this.#recordingDirectory;
637
718
  if (firstSegment)
638
- this.#recordingDirectory = await mkdtemp(join(tmpdir(), "jev-cdp-recording-"));
719
+ this.#recordingDirectory = await mkdtemp2(join2(tmpdir2(), "jev-cdp-recording-"));
639
720
  await this.call("Page.enable");
640
721
  await this.call("Page.addScriptToEvaluateOnNewDocument", { source: RECORDING_CURSOR_INIT });
641
722
  const viewport = await this.evaluate("({width: innerWidth, height: innerHeight})");
@@ -643,7 +724,7 @@ class Browser {
643
724
  throw new Error("Could not read the recording viewport");
644
725
  await this.animateCursor(viewport.width / 2, viewport.height / 2);
645
726
  const initial = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 70 });
646
- const initialPath = join(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
727
+ const initialPath = join2(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
647
728
  await Bun.write(initialPath, Buffer.from(initial.data, "base64"));
648
729
  if (firstSegment)
649
730
  this.#recordingStartedAt = performance.now();
@@ -654,7 +735,7 @@ class Browser {
654
735
  return;
655
736
  });
656
737
  const sequence = this.#recordingSequence++;
657
- const path = join(this.#recordingDirectory, `${String(sequence).padStart(6, "0")}.jpg`);
738
+ const path = join2(this.#recordingDirectory, `${String(sequence).padStart(6, "0")}.jpg`);
658
739
  const elapsedMs = sequence ? performance.now() - this.#recordingStartedAt : 0;
659
740
  this.#recordingWrites = this.#recordingWrites.then(async () => {
660
741
  await Bun.write(path, Buffer.from(frame.data, "base64"));
@@ -692,33 +773,12 @@ class Browser {
692
773
  lines.push(`file '${quoted(frame.path)}'`, `duration ${duration.toFixed(4)}`);
693
774
  }
694
775
  lines.push(`file '${quoted(this.#recordingFrames.at(-1).path)}'`);
695
- const manifest = join(this.#recordingDirectory, "frames.ffconcat");
776
+ const manifest = join2(this.#recordingDirectory, "frames.ffconcat");
696
777
  await Bun.write(manifest, `${lines.join(`
697
778
  `)}
698
779
  `);
699
- const process2 = Bun.spawn([
700
- Bun.which("ffmpeg"),
701
- "-y",
702
- "-f",
703
- "concat",
704
- "-safe",
705
- "0",
706
- "-i",
707
- manifest,
708
- "-vsync",
709
- "vfr",
710
- "-c:v",
711
- "libx264",
712
- "-pix_fmt",
713
- "yuv420p",
714
- "-movflags",
715
- "+faststart",
716
- this.#recordingPath
717
- ], { stdout: "ignore", stderr: "pipe" });
718
- const stderr = await new Response(process2.stderr).text();
719
- if (await process2.exited !== 0)
720
- throw new Error(`Could not render recording: ${stderr.slice(-800)}`);
721
- await rm(this.#recordingDirectory, { recursive: true, force: true });
780
+ await renderRecording(await recordingEncoder(), manifest, this.#recordingPath);
781
+ await rm2(this.#recordingDirectory, { recursive: true, force: true });
722
782
  this.#recordingDirectory = undefined;
723
783
  }
724
784
  async saveFinalScreenshot() {
@@ -1307,9 +1367,9 @@ ${child.text}`.slice(0, 6000);
1307
1367
  }
1308
1368
 
1309
1369
  // src/model.ts
1310
- import { mkdtemp as mkdtemp2, readFile, rm as rm2 } from "fs/promises";
1311
- import { tmpdir as tmpdir2 } from "os";
1312
- import { join as join2 } from "path";
1370
+ import { mkdtemp as mkdtemp3, readFile, rm as rm3 } from "fs/promises";
1371
+ import { tmpdir as tmpdir3 } from "os";
1372
+ import { join as join3 } from "path";
1313
1373
 
1314
1374
  // src/questions.ts
1315
1375
  var NEXT_ACTION = `Advance the user's entire goal from the CURRENT page using one operation.
@@ -1549,9 +1609,9 @@ async function codexFieldText(context) {
1549
1609
  if (!["none", "low", "medium", "high", "xhigh", "max"].includes(reasoning)) {
1550
1610
  throw new Error("Unsupported Codex reasoning effort");
1551
1611
  }
1552
- const folder = await mkdtemp2(join2(tmpdir2(), "jev-codex-text-"));
1553
- const outputPath = join2(folder, "output.json");
1554
- const schemaPath = join2(folder, "schema.json");
1612
+ const folder = await mkdtemp3(join3(tmpdir3(), "jev-codex-text-"));
1613
+ const outputPath = join3(folder, "output.json");
1614
+ const schemaPath = join3(folder, "schema.json");
1555
1615
  await Bun.write(schemaPath, JSON.stringify(text_value_schema_default));
1556
1616
  const childEnvironment = Object.fromEntries(Object.entries(process.env).filter(([name, value]) => value !== undefined && !["TYPESAFE_API_KEY", "TEXT_MODEL_API_KEY"].includes(name)).map(([name, value]) => [name, value]));
1557
1617
  const command = [
@@ -1614,7 +1674,7 @@ ${JSON.stringify(context)}`;
1614
1674
  usage: {}
1615
1675
  }];
1616
1676
  } finally {
1617
- await rm2(folder, { recursive: true, force: true });
1677
+ await rm3(folder, { recursive: true, force: true });
1618
1678
  }
1619
1679
  }
1620
1680
  async function apiFieldText(context) {
@@ -2307,8 +2367,11 @@ async function doctor(args) {
2307
2367
  required: true
2308
2368
  });
2309
2369
  }
2310
- const ffmpeg = Bun.which("ffmpeg");
2311
- checks.push(ffmpeg ? { name: "recording", status: "ok", detail: `FFmpeg found at ${ffmpeg}`, required: false } : { name: "recording", status: "warning", detail: "FFmpeg not found; --recording will be unavailable", required: false });
2370
+ try {
2371
+ checks.push({ name: "recording", status: "ok", detail: await checkRecordingEncoder(), required: false });
2372
+ } catch (error) {
2373
+ checks.push({ name: "recording", status: "warning", detail: `${error instanceof Error ? error.message : String(error)}; --recording will be unavailable`, required: false });
2374
+ }
2312
2375
  if (process.env.TEXT_MODEL_PROVIDER === "api") {
2313
2376
  checks.push(process.env.TEXT_MODEL_API_KEY ? { name: "text-helper", status: "ok", detail: `API helper configured (${process.env.TEXT_MODEL ?? "deepseek-chat"})`, required: false } : { name: "text-helper", status: "warning", detail: "TEXT_MODEL_PROVIDER=api but TEXT_MODEL_API_KEY is not set", required: false });
2314
2377
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-cdp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
5
5
  "type": "module",
6
6
  "license": "MIT",