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.
- package/README.md +3 -3
- package/dist/cli.js +105 -42
- 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.
|
|
31
|
-
npx -y jev-cdp@0.1.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
776
|
+
const manifest = join2(this.#recordingDirectory, "frames.ffconcat");
|
|
696
777
|
await Bun.write(manifest, `${lines.join(`
|
|
697
778
|
`)}
|
|
698
779
|
`);
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
1311
|
-
import { tmpdir as
|
|
1312
|
-
import { join as
|
|
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
|
|
1553
|
-
const outputPath =
|
|
1554
|
-
const schemaPath =
|
|
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
|
|
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
|
-
|
|
2311
|
-
|
|
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 {
|