@vosjs/cli 0.70.0 → 0.71.0

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 CHANGED
@@ -173,7 +173,9 @@ A session never enters a take directory and never reaches vos.so: `vos push` ref
173
173
 
174
174
  **Rehearse before you record.** `vos record … --dry-run` runs every step against the real page, in order, because a later selector usually exists only after an earlier click. Nothing is captured and nothing is written: the pointer lands instead of travelling, every pause is cut to a beat, and the take directory beside it keeps its footage, its cut and its script exactly as they were (a real re-record moves `doc.json` aside; a rehearsal does not). Selector lookups keep their whole timeout, so a miss here is a miss in the take. It prints each step with the rect it resolved, in capture px, which are the rects a pin or `vos callout --step` reads, and exits 2 on any miss or a first load that never settled. Add `--dry-run` to the exact command you were about to run; `--storage-state` and `--browser-arg=` apply to it too, and the `Next:` line it prints carries them, so the command it hands you records what it rehearsed.
175
175
 
176
- **Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
176
+ **The planners read the flow's own structure.** On a scripted take the step timeline is a planner input: a `wait` step between the first and the last doing step, or a hover's dwell, in which the digest's activity measured nothing moving is proposed at 2× (`speedParams.stillRate`; the opening and closing waits are the clip's establishing and resting beats and stay at 1×), the presses inside one step (a click and the dialog it opened, a drag's presses) are one zoom while their targets share a window, and a park before the first step (the cursor arriving on the logo) is never a dwell. Manual and rejected spans stay as they are.
177
+
178
+ **Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), and after every click and typing run a `changed` moment, the region of the page that answered it (a dialog opening, a 200 landing, a panel re-laying out, a card going dark), with `seed` naming the moment it answers, its `rect` the changed region and its `focus` the centre, so a cut can hold on the consequence and a zoom can frame it, each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; `steps`, every script step with its source and output windows, the page it left and the words its `read` kept, so a layer is placed by the table and quotes the take; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
177
179
 
178
180
  **Many media in one take (concat).** `doc.json` may carry `media: [{ id, videoKey, cursor, meta, … }]`, the take's OTHER recordings or uploads, each the `source` shape with an `id`; a segment on one (`{ in, out, media: "m1" }`) plays it, and a zoom, tilt, speed, freeze or cam-move span names the media its source seconds belong to (absent = the primary, `source`). The Video row shows the clips in order, a span is drawn where its media plays, `vos plan` proposes zoom and speed spans on every media from its own cursor track, `vos validate` measures each span against its own media's length, and `vos push` and `vos pull --media` carry every media through the recording door. A media wears its OWN card: `media[].frame` carries the card-owned fields (its placement and size as `inset`, the browser bar, the corner, the shadow, the border, the cover fit and its focus) over the take's frame while it plays; the bar names that media's recorded page, an upload with no page wears none, and the frame-wide fields (the aspect, the padding, the ground, the backdrop, the card's animation) stay the take's. Sound: the primary's tracks play at the primary's moments; another media's own audio is not spliced into the cut yet. **Many cards**: an image or video overlay may show a document media by reference (`key: "media:<id>"`, `media:` alone the primary; its cursor track and recorded page come with it) and wear a card (`frame`: `browserBar`, a `lean` `{rx, ry}` in degrees, `shadow`, `shadowContact`, `shadowColor`, `cursor`), drawn by the card painter on its own plane above the primary card with the media's cursor dot and click rings inside it; a layer without `frame` stays the flat picture. The primary card stays primary: the sequence, the camera and the cut are its.
179
181
 
@@ -251,7 +253,7 @@ A re-record replaces the footage, the cursor track, the frames and everything de
251
253
  }
252
254
  ```
253
255
 
254
- Seven verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150), `type` (`delayMs` 40 per key, paced by the clock; `focus: false` skips the focusing click, for a submitting Enter; `ms` the settle after, 150), `scroll` (`ms` the settle, 200), `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip; `ms` the travel, 700). The recorder's pace is the script's: a gesture is driven by the clock, so a page that answers each pointer sample slowly costs samples, never seconds, and the only pauses are the ones the script names (`wait`, a hover's dwell, a settle). `vos record` ends with a pace line, what the script asked, what the gestures added, what the page cost, and the steps that ran slow, and the `--json` done event carries it as `pace`. Every step takes an optional unique `id`; give steps ids so a span anchored to a step survives script edits and `vos plan --reuse` can follow it, and so a layer can name the element a step touched (`overlays[].pin.step`; the recorder keeps each hover, click, type and drag step's element rect on `meta.steps[].rect`). Because the CLI issues every input itself, the cursor track is synthesized with exact coordinates, exact timing and fresh element rects, which is what powers element-aware auto-zoom and click effects downstream. The schema is [`schema/actions.schema.json`](./schema/actions.schema.json); `vos validate actions.json` checks a script without running anything.
256
+ Eight verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150), `type` (`delayMs` 40 per key, paced by the clock; `focus: false` skips the focusing click; `replace: true` selects the field's value first so the typing replaces it; `at: "end" | "start"` puts the caret there before the first character, for a code editor line; `ms` the settle after, 150), `press` (one named key, `Escape`, `Enter`, `Tab`, `Backspace`, `Delete`, the arrows, `Home`, `End`, `PageUp`, `PageDown`, `Space`, recorded as a typing ping where the pointer is or on `selector` with no click, so an Escape closes what a click opened and an Enter submits what was typed without a second click ringing on the field; `ms` the settle, 150), `scroll` (`ms` the settle, 200), `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip; `ms` the travel, 700). The recorder's pace is the script's: a gesture is driven by the clock, so a page that answers each pointer sample slowly costs samples, never seconds, and the only pauses are the ones the script names (`wait`, a hover's dwell, a settle). `vos record` ends with a pace line, what the script asked, what the gestures added, what the page cost, and the steps that ran slow, and the `--json` done event carries it as `pace`. Every step takes an optional unique `id`; give steps ids so a span anchored to a step survives script edits and `vos plan --reuse` can follow it, and so a layer can name the element a step touched (`overlays[].pin.step`; the recorder keeps each hover, click, type and drag step's element rect on `meta.steps[].rect`). **The take carries its payload**: every step keeps the page it left (`meta.steps[].url`, origin and path), and a step with `read: "<selector>"` keeps that element's words once the step has settled (`meta.steps[].text`, at most 2 KB; on a click, what the click opened: the URL a share button minted, the code a dialog printed), so a layer quotes the take instead of a replay of the flow. Because the CLI issues every input itself, the cursor track is synthesized with exact coordinates, exact timing and fresh element rects, which is what powers element-aware auto-zoom and click effects downstream. The schema is [`schema/actions.schema.json`](./schema/actions.schema.json); `vos validate actions.json` checks a script without running anything.
255
257
 
256
258
  Verified the flow in agent-browser already? `vos actions from-agent-browser steps.jsonl [--out actions.json] [--url] [--viewport WxH]` writes the script from that walk (each command kept beside its `--json` result, the batch record shape; refs resolve through the last `snapshot -i`), and names every step the recorder cannot follow rather than dropping it.
257
259
 
@@ -2710,15 +2710,21 @@ import { mkdir, readFile as readFile2, rename, writeFile as writeFile2 } from "f
2710
2710
  import { existsSync as existsSync2 } from "fs";
2711
2711
  import { join as join2, resolve } from "path";
2712
2712
  import {
2713
+ CHANGE_GRID,
2714
+ CHANGE_LEAD,
2715
+ CHANGE_OFFSETS,
2713
2716
  DIGEST_CROP_MAX,
2714
2717
  DIGEST_FULL_MAX,
2715
2718
  DIGEST_VERSION,
2716
2719
  MOTION_DELTA,
2720
+ ambientDrift,
2717
2721
  buildDigest,
2722
+ changedRegion,
2718
2723
  cropBox,
2719
2724
  frameGeometry,
2720
2725
  momentsFromDoc,
2721
2726
  outputDurationOf,
2727
+ pickChange,
2722
2728
  planForDigest,
2723
2729
  sceneChanges
2724
2730
  } from "@vosjs/studio-core";
@@ -2762,7 +2768,40 @@ async function binsInPage(opts) {
2762
2768
  prev = luma;
2763
2769
  w.__progress = (i + 1) / n;
2764
2770
  }
2765
- w.__done = { ok: true, bins, width: size.width, height: size.height };
2771
+ const g = document.createElement("canvas");
2772
+ g.width = opts.grid.w;
2773
+ g.height = opts.grid.h;
2774
+ const gctx = g.getContext("2d", { willReadFrequently: true });
2775
+ const gridAt = async (t) => {
2776
+ const el = await getFrame(Math.max(0, Math.min(opts.durationS, t)));
2777
+ gctx.drawImage(el, 0, 0, opts.grid.w, opts.grid.h);
2778
+ const d = gctx.getImageData(0, 0, opts.grid.w, opts.grid.h).data;
2779
+ const out = new Array(opts.grid.w * opts.grid.h);
2780
+ for (let p = 0; p < out.length; p++) {
2781
+ const o = p * 4;
2782
+ out[p] = (d[o] * 299 + d[o + 1] * 587 + d[o + 2] * 114) / 1e3;
2783
+ }
2784
+ return out;
2785
+ };
2786
+ const changes = [];
2787
+ for (const seed of opts.seeds) {
2788
+ const samples = [];
2789
+ if (seed.t - opts.lead >= 0)
2790
+ samples.push({ dt: -opts.lead, luma: await gridAt(seed.t - opts.lead) });
2791
+ samples.push({ dt: 0, luma: await gridAt(seed.t) });
2792
+ for (const dt of opts.offsets) {
2793
+ if (seed.t + dt > opts.durationS) break;
2794
+ samples.push({ dt, luma: await gridAt(seed.t + dt) });
2795
+ }
2796
+ changes.push({ id: seed.id, samples });
2797
+ }
2798
+ w.__done = {
2799
+ ok: true,
2800
+ bins,
2801
+ width: size.width,
2802
+ height: size.height,
2803
+ changes
2804
+ };
2766
2805
  } catch (e) {
2767
2806
  w.__error = String(e instanceof Error && e.stack || e);
2768
2807
  }
@@ -2985,23 +3024,66 @@ async function digestTake(browser, dir, opts = {}) {
2985
3024
  });
2986
3025
  await page.goto(`${server.base}/digest.html`);
2987
3026
  await page.waitForFunction("window.__pageReady__ === true");
2988
- void page.evaluate(binsInPage, { source, durationS: durS, delta: MOTION_DELTA }).catch(() => {
3027
+ const seeds = momentsFromDoc(doc, plan, {}).filter(
3028
+ (m) => m.at !== null && (m.kind === "click" || m.kind === "typing")
3029
+ ).map((m) => ({
3030
+ id: m.id,
3031
+ t: m.kind === "click" ? m.at : m.source.out
3032
+ }));
3033
+ void page.evaluate(binsInPage, {
3034
+ source,
3035
+ durationS: durS,
3036
+ delta: MOTION_DELTA,
3037
+ seeds,
3038
+ grid: { ...CHANGE_GRID },
3039
+ offsets: [...CHANGE_OFFSETS],
3040
+ lead: CHANGE_LEAD
3041
+ }).catch(() => {
2989
3042
  });
2990
3043
  const b = await waitForPageDone(
2991
3044
  page,
2992
3045
  "digest bins",
2993
3046
  () => {
2994
3047
  },
2995
- 6e4 + durS * 400
3048
+ 6e4 + durS * 400 + seeds.length * 2e3
2996
3049
  );
2997
3050
  bins = b.bins;
2998
3051
  plan = planForDigest(doc, bins);
2999
3052
  frameW = b.width;
3000
3053
  frameH = b.height;
3054
+ const sampled = b.changes ?? [];
3055
+ const changes = seeds.flatMap((seed) => {
3056
+ const row = sampled.find((c) => c.id === seed.id);
3057
+ const at2 = row?.samples.find((smp) => smp.dt === 0);
3058
+ const earlier = row?.samples.find((smp) => smp.dt < 0);
3059
+ const after = row?.samples.filter((smp) => smp.dt > 0) ?? [];
3060
+ if (!row || !at2 || !after.length) return [];
3061
+ const before = { ...CHANGE_GRID, luma: at2.luma };
3062
+ const rate = earlier ? ambientDrift(
3063
+ { ...CHANGE_GRID, luma: earlier.luma },
3064
+ before,
3065
+ -earlier.dt
3066
+ ) : null;
3067
+ const w = pickChange(
3068
+ { id: seed.id, at: seed.t },
3069
+ after.map((smp) => ({
3070
+ dt: smp.dt,
3071
+ region: changedRegion(
3072
+ before,
3073
+ { ...CHANGE_GRID, luma: smp.luma },
3074
+ MOTION_DELTA,
3075
+ 1,
3076
+ rate ? { rate, dt: smp.dt } : null
3077
+ )
3078
+ }))
3079
+ );
3080
+ return w ? [w] : [];
3081
+ });
3001
3082
  moments = momentsFromDoc(doc, plan, {
3002
3083
  bins,
3003
3084
  scenes: sceneChanges(bins),
3004
- transcript
3085
+ transcript,
3086
+ changes
3005
3087
  });
3006
3088
  const geo = frameGeometry(doc, frameW, frameH);
3007
3089
  const shots = moments.filter((m) => m.at !== null).map((m) => {
@@ -5699,7 +5781,8 @@ function planOtherMedia(doc, activity) {
5699
5781
  height: m.meta.height,
5700
5782
  duration: m.meta.durationMs / 1e3,
5701
5783
  style: doc.zoomStyle,
5702
- params: doc.zoomParams
5784
+ params: doc.zoomParams,
5785
+ steps: m.meta.steps
5703
5786
  }).filter((z) => !manualZoom.some((x) => overlaps(z, x))).filter((z) => !isRejected("zoom", z, rejected)).map((z) => ({ ...z, id: `${z.id}-${m.id}`, media: m.id }))
5704
5787
  );
5705
5788
  const manualSpeed = (doc.speed ?? []).filter(
@@ -5709,7 +5792,8 @@ function planOtherMedia(doc, activity) {
5709
5792
  ...planAutoSpeed(m.cursor, {
5710
5793
  durationMs: m.meta.durationMs,
5711
5794
  params: doc.speedParams,
5712
- activity
5795
+ activity,
5796
+ steps: m.meta.steps
5713
5797
  }).filter((x) => !manualSpeed.some((y) => x.in < y.out && x.out > y.in)).filter((x) => !isRejected("speed", x, rejected)).map((x) => ({ ...x, id: `${x.id}-${m.id}`, media: m.id }))
5714
5798
  );
5715
5799
  }
@@ -5742,14 +5826,16 @@ async function planTake(dir, opts = {}) {
5742
5826
  height: doc.source.meta.height,
5743
5827
  duration: doc.source.meta.durationMs / 1e3,
5744
5828
  style: doc.zoomStyle,
5745
- params: doc.zoomParams
5829
+ params: doc.zoomParams,
5830
+ steps: doc.source.meta.steps
5746
5831
  }).filter((z) => !manualZoom.some((m) => overlaps(z, m))).filter((z) => !isRejected("zoom", z, doc.rejected));
5747
5832
  doc.zoom = [...manualZoom, ...autoZoom].sort((a, b) => a.in - b.in);
5748
5833
  const manualSpeed = rt.speed;
5749
5834
  const autoSpeed = planAutoSpeed(doc.source.cursor, {
5750
5835
  durationMs: doc.source.meta.durationMs,
5751
5836
  params: doc.speedParams,
5752
- activity
5837
+ activity,
5838
+ steps: doc.source.meta.steps
5753
5839
  }).filter((s) => !manualSpeed.some((m) => s.in < m.out && s.out > m.in)).filter((s) => !isRejected("speed", s, doc.rejected));
5754
5840
  doc.speed = [...manualSpeed, ...autoSpeed].sort((a, b) => a.in - b.in);
5755
5841
  {
@@ -5797,7 +5883,8 @@ async function planTake(dir, opts = {}) {
5797
5883
  height: doc.source.meta.height,
5798
5884
  duration: doc.source.meta.durationMs / 1e3,
5799
5885
  style: doc.zoomStyle,
5800
- params: doc.zoomParams
5886
+ params: doc.zoomParams,
5887
+ steps: doc.source.meta.steps
5801
5888
  }).filter((z) => !manual.some((m) => overlaps(z, m))).filter((z) => !isRejected("zoom", z, doc.rejected));
5802
5889
  doc.zoom = [...manual, ...auto].sort((a, b) => a.in - b.in);
5803
5890
  const manualSpeed = (doc.speed ?? []).filter(
@@ -5806,7 +5893,8 @@ async function planTake(dir, opts = {}) {
5806
5893
  const autoSpeed = planAutoSpeed(doc.source.cursor, {
5807
5894
  durationMs: doc.source.meta.durationMs,
5808
5895
  params: doc.speedParams,
5809
- activity
5896
+ activity,
5897
+ steps: doc.source.meta.steps
5810
5898
  }).filter((s) => !manualSpeed.some((m) => s.in < m.out && s.out > m.in)).filter((s) => !isRejected("speed", s, doc.rejected));
5811
5899
  doc.speed = [...manualSpeed, ...autoSpeed].sort((a, b) => a.in - b.in);
5812
5900
  {
@@ -5837,12 +5925,14 @@ async function planTake(dir, opts = {}) {
5837
5925
  height: doc.source.meta.height,
5838
5926
  duration: doc.source.meta.durationMs / 1e3,
5839
5927
  style: doc.zoomStyle,
5840
- params: doc.zoomParams
5928
+ params: doc.zoomParams,
5929
+ steps: doc.source.meta.steps
5841
5930
  });
5842
5931
  doc.speed = planAutoSpeed(doc.source.cursor, {
5843
5932
  durationMs: doc.source.meta.durationMs,
5844
5933
  params: doc.speedParams,
5845
- activity
5934
+ activity,
5935
+ steps: doc.source.meta.steps
5846
5936
  });
5847
5937
  fresh = true;
5848
5938
  }
@@ -11386,7 +11476,7 @@ function summary2(result) {
11386
11476
  async function cmdActionsScript(input, flags) {
11387
11477
  const r = createReporter(flags.json === true);
11388
11478
  const { readFile: readFile14 } = await import("fs/promises");
11389
- const { shotList, formatShotList } = await import("./shotList-KH6FTX4L.js");
11479
+ const { shotList, formatShotList } = await import("./shotList-MAQVLZBY.js");
11390
11480
  const raw = JSON.parse(await readFile14(resolve11(input), "utf8"));
11391
11481
  const errors = validateActions(raw);
11392
11482
  if (errors.length) throw new UsageError(`${input}:
@@ -14238,4 +14328,4 @@ export {
14238
14328
  verbHelp,
14239
14329
  run
14240
14330
  };
14241
- //# sourceMappingURL=chunk-KIFXVPBR.js.map
14331
+ //# sourceMappingURL=chunk-XB5XRE4Q.js.map