@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 +4 -1
- package/dist/{chunk-LGRNHJ2U.js → chunk-RECQEBXN.js} +76 -27
- package/dist/chunk-RECQEBXN.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{run-ZNQKSGAF.js → run-BTBEJLWL.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-LGRNHJ2U.js.map +0 -1
- /package/dist/{run-ZNQKSGAF.js.map → run-BTBEJLWL.js.map} +0 -0
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
|
|
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
|
|
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
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
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
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
11743
|
+
//# sourceMappingURL=chunk-RECQEBXN.js.map
|