@vosjs/cli 0.38.0 → 0.39.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
@@ -60,6 +60,7 @@ A **take** is a directory: the recording of a scripted browser flow, its exact c
60
60
 
61
61
  ```bash
62
62
  vos create --actions actions.json out.webm --strict # one shot: record, auto-plan, render
63
+ vos record --actions actions.json --out take --dry-run # rehearse the script first: which selectors resolve, their rects, in seconds
63
64
  vos record --actions actions.json --out take --strict # drive the page, record it with an exact cursor track, plan the cut
64
65
  vos digest take # SEE the recording before cutting: moments, frames, crops (an agent's eyes)
65
66
  # … edit take/doc.json (zoom spans, trims, speed, overlays) by hand or by agent …
@@ -79,7 +80,7 @@ vos plan take --reuse # re-time that cut onto
79
80
 
80
81
  | Verb | Flags |
81
82
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--max-duration <s>` `--background <slug\|url\|none>` |
83
+ | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--keep-frames` `--storage-state <file>` `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` |
83
84
  | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
84
85
  | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--with <doc.json\|take\|vosId>[@end\|@start\|@step:<id>\|@<seconds>]` (a template, repeatable) `--background` `--motion` (re-propose the motion) `--headline` `--kicker` `--launch` `--brand` `--music` `--entrance` `--transitions slide\|fade\|scale\|none` `--end-card on\|none\|<ref>` `--captions` `--clicks` `--release` |
85
86
  | `digest` | `--out <take>/digest` `--full 960` `--crop 640` (image long edges, the token budget) `--no-frames` `--transcript <file>` (Whisper-shaped segments merged as `said`) `--style <ref>` (report a reference document's style fields) |
@@ -87,6 +88,8 @@ vos plan take --reuse # re-time that cut onto
87
88
  | `render` | `--width` `--height` `--fps` `--format webm\|mp4` `--parallel N` (1..16 sessions) `--range a..b` (output seconds; keeps its audio) `--draft` `--frame <kind>` `--background` `--set …`; `out` defaults to `<take>/out.<format>` |
88
89
  | `open` | `--studio http://localhost:6060` `--print` (print the URL, do not launch a browser) |
89
90
 
91
+ **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.
92
+
90
93
  **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.
91
94
 
92
95
  **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.
@@ -6629,7 +6629,8 @@ function paceLine(r) {
6629
6629
  }
6630
6630
 
6631
6631
  // src/plugin/recorder.ts
6632
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6632
+ var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
6633
+ var DRY_PAUSE_MS = 120;
6633
6634
  function jpegDims(buf) {
6634
6635
  let i = 2;
6635
6636
  while (i < buf.length - 9) {
@@ -6644,7 +6645,9 @@ function jpegDims(buf) {
6644
6645
  return null;
6645
6646
  }
6646
6647
  async function recordTake(browser, url, actions, paths, log, opts = {}) {
6647
- const maxSeconds = opts.maxDurationSeconds ?? Infinity;
6648
+ const dry = opts.dryRun === true;
6649
+ const sleep = dry ? (ms) => realSleep(Math.min(ms, DRY_PAUSE_MS)) : realSleep;
6650
+ const maxSeconds = dry ? Infinity : opts.maxDurationSeconds ?? Infinity;
6648
6651
  const vw = actions.viewport?.width ?? 1280;
6649
6652
  const vh = actions.viewport?.height ?? 720;
6650
6653
  const context = await browser.newContext({
@@ -6667,22 +6670,24 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6667
6670
  const frames = [];
6668
6671
  let frameIdx = 0;
6669
6672
  let t0 = 0;
6670
- cdp.on("Page.screencastFrame", (ev) => {
6671
- const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
6672
- const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
6673
- writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6674
- frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
6675
- cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
6673
+ if (!dry)
6674
+ cdp.on("Page.screencastFrame", (ev) => {
6675
+ const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
6676
+ const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
6677
+ writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6678
+ frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
6679
+ cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
6680
+ });
6676
6681
  });
6677
- });
6678
6682
  t0 = Date.now();
6679
- await cdp.send("Page.startScreencast", {
6680
- format: "jpeg",
6681
- quality: 90,
6682
- maxWidth: vw,
6683
- maxHeight: vh,
6684
- everyNthFrame: 1
6685
- });
6683
+ if (!dry)
6684
+ await cdp.send("Page.startScreencast", {
6685
+ format: "jpeg",
6686
+ quality: 90,
6687
+ maxWidth: vw,
6688
+ maxHeight: vh,
6689
+ everyNthFrame: 1
6690
+ });
6686
6691
  const events = [];
6687
6692
  const cur = { x: 48, y: 48 };
6688
6693
  const now = () => Date.now() - t0;
@@ -6694,7 +6699,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6694
6699
  const dist = Math.hypot(tx - cur.x, ty - cur.y);
6695
6700
  const from = { ...cur };
6696
6701
  await clockMotion(
6697
- pointerTravelMs(dist),
6702
+ dry ? 0 : pointerTravelMs(dist),
6698
6703
  async (u) => {
6699
6704
  cur.x = from.x + (tx - from.x) * u;
6700
6705
  cur.y = from.y + (ty - from.y) * u;
@@ -6969,12 +6974,14 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6969
6974
  ` WARNING: ${freezePct}% of the take is visually frozen (${freezes.length} freezes, longest ${Math.max(...freezes.map((f) => f.ms))}ms) \u2014 keep motion in frame or trim these spans in doc.json`
6970
6975
  );
6971
6976
  }
6972
- await writeJson(paths.cursor, events);
6973
- await writeJson(paths.meta, meta, true);
6974
- await writeJson(paths.framesIndex, frames);
6975
- await writeJson(paths.actions, { ...actions, url }, true);
6976
6977
  const pace = paceReport(paces);
6977
- log(` ${paceLine(pace)}`);
6978
+ if (!dry) {
6979
+ await writeJson(paths.cursor, events);
6980
+ await writeJson(paths.meta, meta, true);
6981
+ await writeJson(paths.framesIndex, frames);
6982
+ await writeJson(paths.actions, { ...actions, url }, true);
6983
+ log(` ${paceLine(pace)}`);
6984
+ }
6978
6985
  return {
6979
6986
  events,
6980
6987
  frames,
@@ -8345,12 +8352,12 @@ async function browserLogin(origin, r, opts) {
8345
8352
  r.log(`Sign in at ${verifyUrl}`);
8346
8353
  if (opts.openBrowser) void launchDefaultBrowser(verifyUrl);
8347
8354
  r.log("Waiting for approval\u2026 (Ctrl-C to cancel)");
8348
- const sleep2 = opts.sleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
8355
+ const sleep = opts.sleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
8349
8356
  let intervalMs = baseIntervalMs;
8350
8357
  let failures = 0;
8351
8358
  let capNoted = false;
8352
8359
  while (Date.now() < expiresAt) {
8353
- await sleep2(intervalMs);
8360
+ await sleep(intervalMs);
8354
8361
  let res;
8355
8362
  try {
8356
8363
  res = await apiJson(origin, "/api/cli/login/poll", {
@@ -10108,14 +10115,15 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
10108
10115
  "check",
10109
10116
  "motion",
10110
10117
  "leader",
10111
- "keep-frames"
10118
+ "keep-frames",
10119
+ "dry-run"
10112
10120
  ]);
10113
10121
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
10114
10122
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10115
10123
 
10116
10124
  Take pipeline
10117
10125
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10118
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10126
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10119
10127
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
10120
10128
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
10121
10129
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
@@ -10445,6 +10453,47 @@ async function cmdRecord(argv) {
10445
10453
  const maxDurationSeconds = await maxDuration(flags, r);
10446
10454
  const storageState = takeStorageState(flags);
10447
10455
  const browserArgs = takeBrowserArgs(multi);
10456
+ if (flags["dry-run"] === true) {
10457
+ void flags.strict;
10458
+ void flags["keep-frames"];
10459
+ const browser2 = await launchBrowser(browserArgs);
10460
+ try {
10461
+ r.log("rehearsing (nothing is captured or written)\u2026");
10462
+ r.event({ event: "phase", phase: "rehearse" });
10463
+ const started = Date.now();
10464
+ const rec = await recordTake(
10465
+ browser2,
10466
+ url,
10467
+ actions,
10468
+ takePaths(outDir),
10469
+ r.log,
10470
+ { storageState, dryRun: true }
10471
+ );
10472
+ const steps = rec.meta.steps ?? [];
10473
+ const lines = steps.map((s) => {
10474
+ const rect = s.rect ? ` rect ${s.rect.x},${s.rect.y} ${s.rect.w}\xD7${s.rect.h}` : "";
10475
+ const what = s.selector ? ` ${s.selector}` : "";
10476
+ return ` ${s.skipped ? "\u2717" : "\u2713"} #${s.step}${s.id ? ` (${s.id})` : ""} ${s.do}${what}${s.skipped ? " NOT FOUND" : rect}${s.navigated ? " \u2192 navigated" : ""}`;
10477
+ });
10478
+ const failed2 = rec.skipped.length > 0 || rec.navTimeout;
10479
+ r.done(
10480
+ {
10481
+ dryRun: true,
10482
+ ok: !failed2,
10483
+ seconds: +((Date.now() - started) / 1e3).toFixed(1),
10484
+ steps,
10485
+ skipped: rec.skipped,
10486
+ navTimeout: rec.navTimeout
10487
+ },
10488
+ `${failed2 ? "REHEARSAL FAILED" : "Rehearsal passed"}: ${steps.length - rec.skipped.length}/${steps.length} steps resolved in ${((Date.now() - started) / 1e3).toFixed(1)}s${rec.navTimeout ? " (the first load never reached networkidle)" : ""}
10489
+ ${lines.join("\n")}
10490
+ ${failed2 ? "Fix the script, rehearse again, then record." : `Rects are capture px (the step rects a pin or a callout reads). Next: vos record --actions ${actionsPath} --out ${outDir} --strict`}`
10491
+ );
10492
+ return failed2 ? EXIT_USAGE : EXIT_OK;
10493
+ } finally {
10494
+ await browser2.close();
10495
+ }
10496
+ }
10448
10497
  if (existsSync15(join18(outDir, "meta.json"))) {
10449
10498
  const { prevDoc, kept } = await prepareReRecord(outDir);
10450
10499
  r.log(
@@ -11691,4 +11740,4 @@ export {
11691
11740
  takeBrowserArgs,
11692
11741
  run
11693
11742
  };
11694
- //# sourceMappingURL=chunk-LGRNHJ2U.js.map
11743
+ //# sourceMappingURL=chunk-RECQEBXN.js.map