jev-cdp 0.1.2 → 0.1.3
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 +4 -4
- package/dist/cli.js +112 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ 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.3 help run
|
|
31
|
+
npx -y jev-cdp@0.1.3 help run
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg is required for `--recording`.
|
|
@@ -96,7 +96,7 @@ bun run run -- run \
|
|
|
96
96
|
--final-state
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay
|
|
99
|
+
`--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay after opening or loading a page, switching to an attached tab, or changing the URL within a page. Jev chooses during that delay, and the adapter waits only for any time left before acting. The same flag also pauses after moving to a click target and before pressing the mouse. Jev does not choose or observe these delays. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
|
|
100
100
|
|
|
101
101
|
`--final-state` adds an AI-oriented semantic snapshot to the final JSON on standard output. It includes the final URL, title, visible text, viewport, scroll state, actionable elements, accessible labels, and control state such as `pressed`, `checked`, `selected`, and `expanded`. Progress remains on standard error, so a coding agent can parse standard output as one JSON object and choose the next bounded goal without another browser observation.
|
|
102
102
|
|
|
@@ -181,7 +181,7 @@ The context is disposed after evidence capture by default. Combine it with `--ke
|
|
|
181
181
|
| `--tab TARGET_ID` | — | create a new tab | Attach to one exact existing Chrome page target |
|
|
182
182
|
| `tabs` | — | — | Print target IDs, titles, and URLs for open page tabs |
|
|
183
183
|
| `--recording PATH.mp4` | — | off | Record the complete goal with an animated cursor |
|
|
184
|
-
| `--interaction-pauses MS` | — | `0` |
|
|
184
|
+
| `--interaction-pauses MS` | — | `0` | Pause after page loads and before clicks, overlapping page pauses with Jev decisions |
|
|
185
185
|
| `--screenshot PATH.jpg` | — | off | Save the final browser viewport |
|
|
186
186
|
| `--final-state` | — | off | Include the final semantic page state in stdout JSON |
|
|
187
187
|
| `--field-value LABEL=VALUE` | — | Luna fallback | Type caller-provided test data into the exactly labeled field |
|
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.3",
|
|
7
7
|
description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
|
|
8
8
|
type: "module",
|
|
9
9
|
license: "MIT",
|
|
@@ -257,6 +257,24 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
|
|
|
257
257
|
|
|
258
258
|
// src/browser.ts
|
|
259
259
|
var MARKER = `(() => { const state=${snapshot_default}; return state?.marker ?? null; })()`;
|
|
260
|
+
var RECORDING_CURSOR_INIT = `(() => {
|
|
261
|
+
if (window !== window.top) return;
|
|
262
|
+
const mount = () => {
|
|
263
|
+
if (!document.documentElement) return false;
|
|
264
|
+
if (document.getElementById('__jev-recording-cursor')) return true;
|
|
265
|
+
const cursor = document.createElement('div');
|
|
266
|
+
cursor.id = '__jev-recording-cursor';
|
|
267
|
+
cursor.setAttribute('aria-hidden', 'true');
|
|
268
|
+
cursor.innerHTML = '<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
|
|
269
|
+
Object.assign(cursor.style, {position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
|
|
270
|
+
document.documentElement.append(cursor);
|
|
271
|
+
return true;
|
|
272
|
+
};
|
|
273
|
+
if (!mount()) {
|
|
274
|
+
const observer = new MutationObserver(() => { if (mount()) observer.disconnect(); });
|
|
275
|
+
observer.observe(document, { childList: true });
|
|
276
|
+
}
|
|
277
|
+
})()`;
|
|
260
278
|
|
|
261
279
|
class StalePageError extends Error {
|
|
262
280
|
}
|
|
@@ -294,6 +312,11 @@ class Browser {
|
|
|
294
312
|
#recordingSequence = 0;
|
|
295
313
|
#recordingWrites = Promise.resolve();
|
|
296
314
|
#stopRecordingEvents;
|
|
315
|
+
#stopPageEvents = [];
|
|
316
|
+
#mainFrameId;
|
|
317
|
+
#pageLoadPromise = null;
|
|
318
|
+
#resolvePageLoad;
|
|
319
|
+
#pauseUntil = 0;
|
|
297
320
|
constructor(cdp, sessionId, targetId, ownsTarget, browserContextId, options) {
|
|
298
321
|
this.#cdp = cdp;
|
|
299
322
|
this.#sessionId = sessionId;
|
|
@@ -361,9 +384,11 @@ class Browser {
|
|
|
361
384
|
mobile: false
|
|
362
385
|
});
|
|
363
386
|
await browser.call("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
387
|
+
await browser.watchPageLoads();
|
|
364
388
|
if (options.url)
|
|
365
389
|
await browser.call("Page.navigate", { url: options.url });
|
|
366
390
|
await browser.waitForReady();
|
|
391
|
+
browser.#pauseUntil = performance.now() + browser.#interactionPauses;
|
|
367
392
|
await browser.startRecording();
|
|
368
393
|
return browser;
|
|
369
394
|
} catch (error) {
|
|
@@ -377,6 +402,63 @@ class Browser {
|
|
|
377
402
|
get targetId() {
|
|
378
403
|
return this.#targetId;
|
|
379
404
|
}
|
|
405
|
+
async watchPageLoads() {
|
|
406
|
+
if (!this.#interactionPauses)
|
|
407
|
+
return;
|
|
408
|
+
await this.call("Page.enable");
|
|
409
|
+
const tree = await this.call("Page.getFrameTree");
|
|
410
|
+
this.#mainFrameId = tree.frameTree.frame.id;
|
|
411
|
+
const loading = (params) => {
|
|
412
|
+
if (params.frameId !== this.#mainFrameId || this.#pageLoadPromise)
|
|
413
|
+
return;
|
|
414
|
+
this.#pageLoadPromise = new Promise((resolve2) => {
|
|
415
|
+
this.#resolvePageLoad = resolve2;
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
const loaded = () => {
|
|
419
|
+
if (!this.#pageLoadPromise)
|
|
420
|
+
return;
|
|
421
|
+
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
422
|
+
this.#resolvePageLoad?.();
|
|
423
|
+
this.#resolvePageLoad = undefined;
|
|
424
|
+
this.#pageLoadPromise = null;
|
|
425
|
+
};
|
|
426
|
+
this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
|
|
427
|
+
const frame = params.frame;
|
|
428
|
+
if (frame && frame.id === this.#mainFrameId && !frame.parentId)
|
|
429
|
+
loading({ frameId: frame.id });
|
|
430
|
+
}), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
|
|
431
|
+
if (params.frameId === this.#mainFrameId)
|
|
432
|
+
loaded();
|
|
433
|
+
}), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
|
|
434
|
+
if (params.frameId === this.#mainFrameId) {
|
|
435
|
+
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
436
|
+
this.resetRecordingCursor().catch(() => {
|
|
437
|
+
return;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
async waitForInteractionPause() {
|
|
443
|
+
if (!this.#interactionPauses)
|
|
444
|
+
return;
|
|
445
|
+
while (true) {
|
|
446
|
+
const pageLoad = this.#pageLoadPromise;
|
|
447
|
+
if (pageLoad) {
|
|
448
|
+
await Promise.race([
|
|
449
|
+
pageLoad,
|
|
450
|
+
Bun.sleep(15000).then(() => {
|
|
451
|
+
throw new Error("Page did not finish loading within 15 seconds");
|
|
452
|
+
})
|
|
453
|
+
]);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const remaining = this.#pauseUntil - performance.now();
|
|
457
|
+
if (remaining <= 0)
|
|
458
|
+
return;
|
|
459
|
+
await Bun.sleep(remaining);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
380
462
|
async startRecording() {
|
|
381
463
|
if (!this.#recordingPath)
|
|
382
464
|
return;
|
|
@@ -384,6 +466,7 @@ class Browser {
|
|
|
384
466
|
throw new Error("--recording requires ffmpeg on PATH");
|
|
385
467
|
this.#recordingDirectory = await mkdtemp(join(tmpdir(), "jev-cdp-recording-"));
|
|
386
468
|
await this.call("Page.enable");
|
|
469
|
+
await this.call("Page.addScriptToEvaluateOnNewDocument", { source: RECORDING_CURSOR_INIT });
|
|
387
470
|
const viewport = await this.evaluate("({width: innerWidth, height: innerHeight})");
|
|
388
471
|
if (!viewport)
|
|
389
472
|
throw new Error("Could not read the recording viewport");
|
|
@@ -485,25 +568,37 @@ class Browser {
|
|
|
485
568
|
async animateCursor(x, y, click = false) {
|
|
486
569
|
if (!this.#recordingPath)
|
|
487
570
|
return;
|
|
571
|
+
await this.evaluate(RECORDING_CURSOR_INIT);
|
|
488
572
|
await this.evaluate(`(point => {
|
|
489
|
-
|
|
490
|
-
if (!cursor)
|
|
491
|
-
cursor=document.createElement('div');
|
|
492
|
-
cursor.id='__jev-recording-cursor'; cursor.setAttribute('aria-hidden','true');
|
|
493
|
-
cursor.innerHTML='<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
|
|
494
|
-
Object.assign(cursor.style,{position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
|
|
495
|
-
document.documentElement.append(cursor);
|
|
496
|
-
}
|
|
573
|
+
const cursor=document.getElementById('__jev-recording-cursor');
|
|
574
|
+
if (!cursor) return;
|
|
497
575
|
cursor.style.left=point.x+'px'; cursor.style.top=point.y+'px';
|
|
498
576
|
if (point.click) cursor.animate([{transform:'translate(-3px,-3px) scale(1)'},{transform:'translate(-3px,-3px) scale(.72)'},{transform:'translate(-3px,-3px) scale(1)'}],{duration:260,easing:'ease-out'});
|
|
499
577
|
})(${JSON.stringify({ x, y, click })})`);
|
|
500
578
|
await Bun.sleep(click ? 80 : 200);
|
|
501
579
|
}
|
|
580
|
+
async resetRecordingCursor() {
|
|
581
|
+
if (!this.#recordingPath)
|
|
582
|
+
return;
|
|
583
|
+
await this.evaluate(`${RECORDING_CURSOR_INIT}; (() => {
|
|
584
|
+
const cursor = document.getElementById('__jev-recording-cursor');
|
|
585
|
+
if (!cursor) return;
|
|
586
|
+
cursor.style.transition = 'none';
|
|
587
|
+
cursor.style.left = '50vw';
|
|
588
|
+
cursor.style.top = '50vh';
|
|
589
|
+
requestAnimationFrame(() => { cursor.style.transition = 'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)'; });
|
|
590
|
+
})()`);
|
|
591
|
+
}
|
|
502
592
|
async waitForReady() {
|
|
503
593
|
const deadline = Date.now() + 15000;
|
|
504
594
|
while (Date.now() < deadline) {
|
|
505
|
-
|
|
506
|
-
|
|
595
|
+
try {
|
|
596
|
+
if (!this.#pageLoadPromise && await this.evaluate("document.readyState") === "complete")
|
|
597
|
+
return;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
if (!(error instanceof StalePageError))
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
507
602
|
await Bun.sleep(20);
|
|
508
603
|
}
|
|
509
604
|
throw new Error("Page did not finish loading within 15 seconds");
|
|
@@ -670,6 +765,9 @@ class Browser {
|
|
|
670
765
|
if (this.#closed)
|
|
671
766
|
return;
|
|
672
767
|
this.#closed = true;
|
|
768
|
+
for (const stop of this.#stopPageEvents)
|
|
769
|
+
stop();
|
|
770
|
+
this.#stopPageEvents = [];
|
|
673
771
|
let failure;
|
|
674
772
|
try {
|
|
675
773
|
if (this.#recordingPath)
|
|
@@ -1150,6 +1248,7 @@ class Agent {
|
|
|
1150
1248
|
this.#textCalls.push({ ...helper, field: action.label, value: text });
|
|
1151
1249
|
}
|
|
1152
1250
|
}
|
|
1251
|
+
await this.#browser.waitForInteractionPause();
|
|
1153
1252
|
await this.#browser.act(action, page, text ?? undefined);
|
|
1154
1253
|
this.#pendingText = null;
|
|
1155
1254
|
const entry = {
|
|
@@ -1279,7 +1378,8 @@ Browser behavior:
|
|
|
1279
1378
|
[env: JEV_BROWSER_VISIBLE=1]
|
|
1280
1379
|
--keep-open Leave a runner-created tab or context open.
|
|
1281
1380
|
[env: JEV_BROWSER_KEEP_OPEN=1]
|
|
1282
|
-
--interaction-pauses <ms> Pause after
|
|
1381
|
+
--interaction-pauses <ms> Pause after page loads and before clicks. Jev decisions
|
|
1382
|
+
run during page pauses, so only remaining time is waited.
|
|
1283
1383
|
|
|
1284
1384
|
Known field values:
|
|
1285
1385
|
--field-value <label=value> Type an exact non-secret value when that accessible
|