jev-cdp 0.1.8 → 0.1.10

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 +2 -2
  2. package/dist/cli.js +133 -34
  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.8 help run
31
- npx -y jev-cdp@0.1.8 help run
30
+ bunx jev-cdp@0.1.10 help run
31
+ npx -y jev-cdp@0.1.10 help run
32
32
  ```
33
33
 
34
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.
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.8",
6
+ version: "0.1.10",
7
7
  description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
8
8
  type: "module",
9
9
  license: "MIT",
@@ -348,16 +348,70 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
348
348
  }
349
349
  return null;
350
350
  };
351
+ const scrollContainers = () =>
352
+ [...document.querySelectorAll('*')].filter(e =>
353
+ e !== document.scrollingElement &&
354
+ visible(e) &&
355
+ /^(auto|scroll)$/.test(getComputedStyle(e).overflowY) &&
356
+ e.clientHeight > 0 &&
357
+ e.scrollHeight > e.clientHeight + 2
358
+ );
359
+ cache.scrollPoint = (e, delta) => {
360
+ if (!e?.isConnected || !visible(e)) return null;
361
+ if (delta > 0
362
+ ? e.scrollTop + e.clientHeight >= e.scrollHeight - 2
363
+ : e.scrollTop <= 0) return null;
364
+ const r = e.getBoundingClientRect();
365
+ let left = Math.max(0, r.left);
366
+ let right = Math.min(innerWidth, r.right);
367
+ let top = Math.max(0, r.top);
368
+ let bottom = Math.min(innerHeight, r.bottom);
369
+ for (let p = e.parentElement; p; p = p.parentElement) {
370
+ if (p === document.body || p === document.documentElement) continue;
371
+ const style = getComputedStyle(p);
372
+ const bounds = p.getBoundingClientRect();
373
+ if (/^(auto|scroll|hidden|clip)$/.test(style.overflowX)) {
374
+ left = Math.max(left, bounds.left);
375
+ right = Math.min(right, bounds.right);
376
+ }
377
+ if (/^(auto|scroll|hidden|clip)$/.test(style.overflowY)) {
378
+ top = Math.max(top, bounds.top);
379
+ bottom = Math.min(bottom, bounds.bottom);
380
+ }
381
+ }
382
+ if (right - left < 4 || bottom - top < 4) return null;
383
+ for (const fraction of [0.5, 0.9, 0.1]) {
384
+ const point = {
385
+ x: left + (right - left) * fraction,
386
+ y: top + (bottom - top) / 2,
387
+ };
388
+ let hit = document.elementFromPoint(point.x, point.y);
389
+ if (!hit || !e.contains(hit)) continue;
390
+ while (hit && hit !== e) {
391
+ if (
392
+ /^(auto|scroll)$/.test(getComputedStyle(hit).overflowY) &&
393
+ hit.scrollHeight > hit.clientHeight + 2
394
+ ) break;
395
+ hit = hit.parentElement;
396
+ }
397
+ if (hit === e) return point;
398
+ }
399
+ return null;
400
+ };
351
401
  cache.pageKey=()=>[performance.timeOrigin,location.href,scrollX,scrollY,innerWidth,innerHeight,
352
402
  [...document.querySelectorAll('input,textarea,select')].filter(safe)
353
- .map(e=>[identity(e),stateValue(e),e.checked,e.selectedIndex,e.disabled,e.readOnly])];
403
+ .map(e=>[identity(e),stateValue(e),e.checked,e.selectedIndex,e.disabled,e.readOnly]),
404
+ scrollContainers().map(e=>[
405
+ identity(e),e.scrollTop,e.scrollLeft,e.clientHeight,e.scrollHeight
406
+ ])];
354
407
  cache.guard=e=>{
355
408
  if (!e?.isConnected || !visible(e)) return null;
356
409
  const scope=e.closest('form,dialog,[role="dialog"],article,li,tr,[role="row"]') || e.parentElement;
357
410
  return [identity(e),role(e),name(e),sensitive(e)?Boolean(e.value):e.value??null,e.checked??null,e.selectedIndex??null,
358
411
  e.readOnly??null,e.matches(':disabled'),e.getAttribute('aria-disabled'),
359
412
  e.getAttribute('aria-expanded'),e.getAttribute('aria-checked'),e.getAttribute('aria-selected'),
360
- e.getAttribute('href'),scope?.innerText?.slice(0,6000)||''];
413
+ e.getAttribute('href'),scope?.innerText?.slice(0,6000)||'',
414
+ e.scrollTop,e.scrollLeft,e.clientHeight,e.scrollHeight];
361
415
  };
362
416
  const actions=[];
363
417
  for (const e of document.querySelectorAll(selector)) {
@@ -409,12 +463,35 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
409
463
  }
410
464
  }
411
465
  }
466
+ for (const e of scrollContainers()) {
467
+ const label =
468
+ e.getAttribute('aria-label') ||
469
+ e.closest('form,dialog,[role="dialog"]')
470
+ ?.querySelector('h1,h2,h3')?.textContent ||
471
+ 'panel';
472
+ for (const delta of [560, -560]) {
473
+ const point = cache.scrollPoint(e, delta);
474
+ if (!point) continue;
475
+ actions.push({
476
+ kind: 'scroll',
477
+ node: identity(e),
478
+ label: 'Scroll ' + (delta > 0 ? 'down' : 'up') +
479
+ ' in ' + label.trim().slice(0, 80),
480
+ delta,
481
+ point,
482
+ rect: { x: point.x - 1, y: point.y - 1, w: 2, h: 2 },
483
+ clickable: true,
484
+ coveredBy: null,
485
+ scrollTop: e.scrollTop,
486
+ });
487
+ }
488
+ }
412
489
  const text=words.join('\\n').slice(0,6000), height=document.documentElement.scrollHeight;
413
490
  const page_key=cache.pageKey(), guards={};
414
491
  for (const a of actions) if (!(a.node in guards)) guards[a.node]=cache.guard(cache.nodes.get(a.node));
415
492
  const semantics=actions.map(({rect,...action})=>action);
416
493
  const marker=[performance.timeOrigin,location.href,scrollX,scrollY,innerWidth,innerHeight,
417
- document.title,text,semantics,page_key[6]];
494
+ document.title,text,semantics,page_key[6],page_key[7]];
418
495
  const omitted_actions=Math.max(0,actions.length-250);
419
496
  actions.splice(250);
420
497
  actions.forEach((a,i)=>a.id='e'+(i+1));
@@ -723,8 +800,8 @@ class Browser {
723
800
  this.signalChange();
724
801
  if (params.frameId !== this.#mainFrameId || this.#pageLoadPromise)
725
802
  return;
726
- this.#pageLoadPromise = new Promise((resolve2) => {
727
- this.#resolvePageLoad = resolve2;
803
+ this.#pageLoadPromise = new Promise((resolve) => {
804
+ this.#resolvePageLoad = resolve;
728
805
  });
729
806
  };
730
807
  const loaded = () => {
@@ -769,11 +846,11 @@ class Browser {
769
846
  listener();
770
847
  }
771
848
  async waitForChange(ms) {
772
- await new Promise((resolve2) => {
849
+ await new Promise((resolve) => {
773
850
  const done = () => {
774
851
  clearTimeout(timer);
775
852
  this.#changeListeners.delete(done);
776
- resolve2();
853
+ resolve();
777
854
  };
778
855
  const timer = setTimeout(done, Math.min(ms, 150));
779
856
  this.#changeListeners.add(done);
@@ -782,11 +859,11 @@ class Browser {
782
859
  async waitForNewTarget(ms) {
783
860
  if (this.#newTargets.size)
784
861
  return;
785
- await new Promise((resolve2) => {
862
+ await new Promise((resolve) => {
786
863
  const done = () => {
787
864
  clearTimeout(timer);
788
865
  this.#popupListeners.delete(done);
789
- resolve2();
866
+ resolve();
790
867
  };
791
868
  const timer = setTimeout(done, ms);
792
869
  this.#popupListeners.add(done);
@@ -1255,9 +1332,9 @@ class Browser {
1255
1332
  if (waited)
1256
1333
  this.#waitSpentMs += performance.now() - waitStarted;
1257
1334
  if (this.#openedTabs.length) {
1258
- const targets2 = await listChromeTargets(this.#cdpUrl);
1335
+ const targets = await listChromeTargets(this.#cdpUrl);
1259
1336
  this.#openedTabs = this.#openedTabs.map((tab) => {
1260
- const current = targets2.find((target) => target.id === tab.id);
1337
+ const current = targets.find((target) => target.id === tab.id);
1261
1338
  return current ? { ...tab, url: current.url, title: current.title } : tab;
1262
1339
  });
1263
1340
  info.text = `${info.text}
@@ -1265,7 +1342,7 @@ ${this.#openedTabs.map((tab) => `Opened new tab: ${tab.title} ${tab.url} (target
1265
1342
  `)}`;
1266
1343
  }
1267
1344
  const frames = await this.frameStates();
1268
- for (const frame of frames.filter((frame2) => frame2.parentId !== null)) {
1345
+ for (const frame of frames.filter((frame) => frame.parentId !== null)) {
1269
1346
  const frameId = frame.id;
1270
1347
  try {
1271
1348
  const offset = await this.frameScreenOffset(frameId, frames);
@@ -1417,15 +1494,36 @@ ${child.text}`.slice(0, 6000);
1417
1494
  return { element: null, performedAt: performance.now() };
1418
1495
  }
1419
1496
  if (action.kind === "scroll") {
1420
- await this.animateCursor(550, 650);
1497
+ let point = { x: 550, y: 650 };
1498
+ if (typeof action.node === "number") {
1499
+ const expression = `window.__jevFast?.scrollPoint(
1500
+ window.__jevFast.nodes.get(${action.node}),
1501
+ ${action.delta ?? 0}
1502
+ )`;
1503
+ const target = action.frameId ? await this.evaluateFrame(action.frameId, expression) : await this.evaluate(expression);
1504
+ if (!target)
1505
+ throw new StalePageError("Scroll target changed or is covered");
1506
+ point = target;
1507
+ if (action.frameId) {
1508
+ const offset = await this.frameScreenOffset(action.frameId);
1509
+ point.x += offset.x;
1510
+ point.y += offset.y;
1511
+ const covered = await this.evaluate(`(() => {
1512
+ const e = document.elementFromPoint(${point.x}, ${point.y});
1513
+ return !e || e.tagName !== "IFRAME";
1514
+ })()`);
1515
+ if (covered)
1516
+ throw new StalePageError("Scroll target is covered");
1517
+ }
1518
+ }
1519
+ await this.animateCursor(point.x, point.y);
1421
1520
  await this.call("Input.dispatchMouseEvent", {
1422
1521
  type: "mouseWheel",
1423
- x: 550,
1424
- y: 650,
1522
+ ...point,
1425
1523
  deltaX: 0,
1426
1524
  deltaY: action.delta ?? 0
1427
1525
  });
1428
- return { element: null, performedAt: performance.now() };
1526
+ return { element: null, performedAt: performance.now(), scrollPoint: point };
1429
1527
  }
1430
1528
  if (typeof action.node !== "number")
1431
1529
  throw new Error("Invalid observed node");
@@ -1667,10 +1765,10 @@ function actionSpace(actions) {
1667
1765
  continue;
1668
1766
  const identity = `${action.frameId ?? "main"}:${action.node}`;
1669
1767
  if (!indices.has(identity)) {
1670
- const index2 = String(elements.length + 1);
1671
- indices.set(identity, index2);
1672
- const element2 = {
1673
- index: index2,
1768
+ const index = String(elements.length + 1);
1769
+ indices.set(identity, index);
1770
+ const element = {
1771
+ index,
1674
1772
  label: action.label.split(" \u2192 ")[0] ?? action.label,
1675
1773
  operations: [],
1676
1774
  frameId: action.frameId ?? null,
@@ -1683,13 +1781,13 @@ function actionSpace(actions) {
1683
1781
  };
1684
1782
  for (const key of ["role", "value", "checked", "selected", "expanded", "pressed", "sensitive"]) {
1685
1783
  if (action[key] !== undefined)
1686
- element2[key] = action[key];
1784
+ element[key] = action[key];
1687
1785
  }
1688
1786
  if (action.kind === "select") {
1689
- element2.value = action.current_value ?? "";
1690
- element2.options = [];
1787
+ element.value = action.current_value ?? "";
1788
+ element.options = [];
1691
1789
  }
1692
- elements.push(element2);
1790
+ elements.push(element);
1693
1791
  }
1694
1792
  const index = indices.get(identity);
1695
1793
  if (action.clickable === false)
@@ -1718,17 +1816,17 @@ async function choose(page, goal, history, providedFields = []) {
1718
1816
  SELECT: "Select an observed dropdown value."
1719
1817
  };
1720
1818
  const operations = {};
1721
- for (const operation2 of Object.keys(targets))
1722
- operations[operation2] = labels[operation2];
1723
- for (const [operation2, action] of Object.entries(controls))
1724
- operations[operation2] = action.label;
1819
+ for (const operation of Object.keys(targets))
1820
+ operations[operation] = labels[operation];
1821
+ for (const [operation, action] of Object.entries(controls))
1822
+ operations[operation] = action.label;
1725
1823
  operations.DONE = "Every requirement is visibly satisfied.";
1726
1824
  operations.BLOCKED = "No supported operation can progress.";
1727
1825
  const questions = {
1728
1826
  operation: { type: "choice", criteria: operations, instructions: { goal, rules: NEXT_ACTION } }
1729
1827
  };
1730
- for (const [operation2, candidates] of Object.entries(targets)) {
1731
- questions[`${operation2.toLowerCase()}_target`] = {
1828
+ for (const [operation, candidates] of Object.entries(targets)) {
1829
+ questions[`${operation.toLowerCase()}_target`] = {
1732
1830
  type: "choice",
1733
1831
  criteria: Object.fromEntries(Object.entries(candidates).map(([index, action]) => [index, {
1734
1832
  element: `[${index}] ${action.label}`,
@@ -1742,7 +1840,7 @@ async function choose(page, goal, history, providedFields = []) {
1742
1840
  current_value: action.current_value ?? action.value ?? "",
1743
1841
  ...Object.fromEntries(["role", "checked", "selected", "expanded", "pressed", "sensitive"].filter((name) => action[name] !== undefined).map((name) => [name, action[name]]))
1744
1842
  }])),
1745
- instructions: { goal, operation: operation2, rules: [NEXT_ACTION, TARGET] }
1843
+ instructions: { goal, operation, rules: [NEXT_ACTION, TARGET] }
1746
1844
  };
1747
1845
  }
1748
1846
  const body = {
@@ -2082,7 +2180,7 @@ class Agent {
2082
2180
  }
2083
2181
  await this.#browser.waitForInteractionPause();
2084
2182
  const fromTargetId = this.#browser.targetId;
2085
- const { element, performedAt } = await this.#browser.act(action, page, text ?? undefined);
2183
+ const { element, performedAt, scrollPoint } = await this.#browser.act(action, page, text ?? undefined);
2086
2184
  this.#pendingText = null;
2087
2185
  const entry = {
2088
2186
  step: this.#history.length + 1,
@@ -2106,6 +2204,7 @@ class Agent {
2106
2204
  element,
2107
2205
  value: action.kind === "select" ? action.value ?? null : null,
2108
2206
  delta_y: action.kind === "scroll" ? action.delta ?? 0 : null,
2207
+ scroll_point: scrollPoint ?? null,
2109
2208
  redacted: action.kind === "fill" && (Boolean(action.sensitive) || this.#sensitiveFieldLabels.has(action.label)),
2110
2209
  usage: decision.usage,
2111
2210
  executed_ms: Math.round(performedAt - this.#startedAt),
@@ -2450,7 +2549,7 @@ function actionEvent(entry, maxSteps) {
2450
2549
  element: entry.element,
2451
2550
  ...entry.kind === "fill" ? { text: entry.text, redacted: entry.redacted } : {},
2452
2551
  ...entry.kind === "select" ? { optionValue: entry.value } : {},
2453
- ...entry.kind === "scroll" ? { deltaY: entry.delta_y, point: { x: 550, y: 650 } } : {},
2552
+ ...entry.kind === "scroll" ? { deltaY: entry.delta_y, point: entry.scroll_point ?? { x: 550, y: 650 } } : {},
2454
2553
  ...entry.kind === "wait" ? { durationMs: 100 } : {}
2455
2554
  }
2456
2555
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-cdp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
5
5
  "type": "module",
6
6
  "license": "MIT",