jev-cdp 0.1.9 → 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.
- package/README.md +2 -2
- package/dist/cli.js +133 -34
- 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.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.
|
|
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((
|
|
727
|
-
this.#resolvePageLoad =
|
|
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((
|
|
849
|
+
await new Promise((resolve) => {
|
|
773
850
|
const done = () => {
|
|
774
851
|
clearTimeout(timer);
|
|
775
852
|
this.#changeListeners.delete(done);
|
|
776
|
-
|
|
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((
|
|
862
|
+
await new Promise((resolve) => {
|
|
786
863
|
const done = () => {
|
|
787
864
|
clearTimeout(timer);
|
|
788
865
|
this.#popupListeners.delete(done);
|
|
789
|
-
|
|
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
|
|
1335
|
+
const targets = await listChromeTargets(this.#cdpUrl);
|
|
1259
1336
|
this.#openedTabs = this.#openedTabs.map((tab) => {
|
|
1260
|
-
const current =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1671
|
-
indices.set(identity,
|
|
1672
|
-
const
|
|
1673
|
-
index
|
|
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
|
-
|
|
1784
|
+
element[key] = action[key];
|
|
1687
1785
|
}
|
|
1688
1786
|
if (action.kind === "select") {
|
|
1689
|
-
|
|
1690
|
-
|
|
1787
|
+
element.value = action.current_value ?? "";
|
|
1788
|
+
element.options = [];
|
|
1691
1789
|
}
|
|
1692
|
-
elements.push(
|
|
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
|
|
1722
|
-
operations[
|
|
1723
|
-
for (const [
|
|
1724
|
-
operations[
|
|
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 [
|
|
1731
|
-
questions[`${
|
|
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
|
|
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
|
};
|