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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +112 -12
  3. 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.2 help run
31
- npx -y jev-cdp@0.1.2 help run
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 in milliseconds after moving to a click target and before pressing the mouse; Jev does not choose or observe this delay. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
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` | Wait after moving to a click target, before mousedown |
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.2",
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
- let cursor=document.getElementById('__jev-recording-cursor');
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
- if (await this.evaluate("document.readyState") === "complete")
506
- return;
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 moving to a click target, before mousedown.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-cdp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
5
5
  "type": "module",
6
6
  "license": "MIT",