@vosjs/cli 0.79.0 → 0.81.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 +92 -32
- package/dist/{assetManifest-U25FJER6.js → assetManifest-27NKSS7N.js} +2 -2
- package/dist/{chunk-4ZUHEXUX.js → chunk-352AC3V6.js} +235 -27
- package/dist/chunk-352AC3V6.js.map +1 -0
- package/dist/{chunk-Z6TLHRFQ.js → chunk-B373NVJE.js} +6 -2
- package/dist/chunk-B373NVJE.js.map +1 -0
- package/dist/{chunk-TWAP4BKR.js → chunk-S3RI2SHH.js} +2866 -715
- package/dist/chunk-S3RI2SHH.js.map +1 -0
- package/dist/cli.js +13 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +29 -29
- package/dist/index.js +3 -3
- package/dist/{run-UKWKAGDK.js → run-MEDD2Q74.js} +8 -4
- package/dist/{shotList-EE3S5LVF.js → shotList-PL7XP6AC.js} +35 -3
- package/dist/shotList-PL7XP6AC.js.map +1 -0
- package/package.json +5 -5
- package/schema/actions.schema.json +256 -49
- package/schema/doc.schema.json +36 -5
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +6 -2
- package/skills/product-video/SKILL.md +27 -0
- package/skills/product-video/references/taste.md +7 -3
- package/skills/vos-cut/SKILL.md +17 -2
- package/dist/chunk-4ZUHEXUX.js.map +0 -1
- package/dist/chunk-TWAP4BKR.js.map +0 -1
- package/dist/chunk-Z6TLHRFQ.js.map +0 -1
- package/dist/shotList-EE3S5LVF.js.map +0 -1
- /package/dist/{assetManifest-U25FJER6.js.map → assetManifest-27NKSS7N.js.map} +0 -0
- /package/dist/{run-UKWKAGDK.js.map → run-MEDD2Q74.js.map} +0 -0
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ vos logout # remove ~/.config/vos/credentials
|
|
|
50
50
|
```bash
|
|
51
51
|
vos render <config.json|url|take> [out] # config → video; the name picks mp4/webm; a program document's sound is mixed in
|
|
52
52
|
vos still <config.json|url> [out.webp] # config → frames: --times 0,1.5,50% in one browser; .png/.jpg re-encoded
|
|
53
|
-
vos info <config.json|url>
|
|
53
|
+
vos info <config.json|url|take> # inspect a config; a take: its steps and its cut in output seconds
|
|
54
54
|
vos check <config.json|url> # migrate → schema → syntax → compile → determinism, dialect and declared-file lints, all local
|
|
55
55
|
vos preview <config.json|url> [--port N] # serve a local playback page
|
|
56
56
|
vos versions # installed @vosjs/* versions
|
|
@@ -88,8 +88,7 @@ vos open take # hand the take to the s
|
|
|
88
88
|
When the product ships again, re-record the same script and keep the cut:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
vos record --actions actions.json --out take --strict # footage replaced
|
|
92
|
-
vos plan take --reuse # re-time that cut onto the new recording; what could not follow is named
|
|
91
|
+
vos record --actions actions.json --out take --strict --reuse # footage replaced, the cut re-timed onto it; what could not follow is named
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
Every manual span follows the step it was made on (the gesture whose window held it, or the one just before it), keeping its offset exactly; a span whose step is missing or skipped in the new recording is dropped and named with what it framed, never re-timed to a place with nothing under it. A layer pinned to a step (`pin: { step }`) plays when that step plays in the new cut; a layer that is not pinned keeps its output second, and when the step under it moved the report names it with the step's new time and the pin to write. A take fetched from vos.so can be recorded again: the script rides the document and `vos fetch --media` writes it back as `actions.json`.
|
|
@@ -107,7 +106,7 @@ vos ingest loom.mp4 --as take --out take # a screen demo with no
|
|
|
107
106
|
|
|
108
107
|
**A file with no trace is finished footage by default.** Nothing but a cursor trace says a file is a screen recording, so without one the take opens bare: no card, no browser bar, no drawn cursor or webcam, exported at the footage's own frame rate. `--as take` gives a screen demo the card; `--as footage` says the opposite outright.
|
|
109
108
|
|
|
110
|
-
What a trace cannot give is named, never guessed: agent-browser's own log carries neither timestamps nor coordinates (measured), so stamp `ts` and a `point` per command when you keep one, or replay the walk with `vos actions from-agent-browser` and `vos record` instead. `--offset <ms>` shifts a trace whose zero is not the video's; `--viewport WxH` names the coordinate space when the video was scaled. A headed capture with the real pointer baked in shows two cursors under a trace's drawn one; turn the drawn cursor off in `doc.json` for those.
|
|
109
|
+
What a trace cannot give is named, never guessed: agent-browser's own log carries neither timestamps nor coordinates (measured), so stamp `ts` and a `point` per command when you keep one, or replay the walk with `vos actions from-agent-browser` and `vos record` instead. `--offset <ms>` shifts a trace whose zero is not the video's; `--viewport WxH` names the coordinate space when the video was scaled. A headed capture with the real pointer baked in shows two cursors under a trace's drawn one; turn the drawn cursor off in `doc.json` for those (`cursor.visible: false`; for part of the film, `cursor.off: [{ in, out? }]` in output seconds, an absent `out` running to the end, which `vos plan` also writes under its end card).
|
|
111
110
|
|
|
112
111
|
**Several takes, one film.** `vos join` cuts takes (and plain video files) into one take, in argument order, and the result is one document to edit, render and push like any other:
|
|
113
112
|
|
|
@@ -115,7 +114,19 @@ What a trace cannot give is named, never guessed: agent-browser's own log carrie
|
|
|
115
114
|
vos join intro-take settings-take outro.mp4 --out film --captions
|
|
116
115
|
```
|
|
117
116
|
|
|
118
|
-
The first input is the primary: its frame, cursor style, export, sound and cut come along as they are. Each next input becomes a media of the document (`m1`, `m2`, …): its footage and sidecars are copied under `film/m1/…`, its segments are appended (so the output plays the inputs one after the other), every source-anchored span it carries (`zoom`, `speed`, `tilt`, `camMotion`, `freeze`, `rejected`) is tagged with its media and renamed `m1-…`, and its layers move to where its footage now starts. A layer it pinned to one of its steps keeps its moment through an `anchor` to that step (a pin places a layer on the primary's footage only, so the pin itself is dropped, and the done line says so); its sound clips stay behind. A layer that starts after an input's footage ends (an end card, a closing title) is that input's ENDING: every input's ending but the last one's is dropped, named in a note, because it would land over the next clip; the last input's stays at the end of the film. A plain video file becomes a media with no cursor track, a `meta` probed from the file, a full-bleed card of its own (`inset` 0, no corners, no shadow) and one segment over its whole length; its own sound is part of what it is, so it plays as an audio clip keyed to the copied file (`<id>-sound`, starting where the file plays, trimmed to its clip, a short fade at each end), and `--mute <file>` leaves it out. A file whose audio Node cannot decode (AAC) is copied as it is and the join says so in one line; the render reads it in the browser. The joined document must pass `vos validate` before a byte is written. `--out` is refused when it exists, unless `--force` names a take directory to replace; `--out` may not overlap an input. `--captions` lands every step's `caption` (from `meta.steps` or each take's `actions.json`) as a caption anchored to its step, in the house style, marked `from: "steps"` so a second run replaces only those; each ends with the clip it starts in (never over the next input's footage, and a step in the last second of its clip gets none), a step a caption is already anchored to is left alone, and an input none of whose steps carries a caption is named. `vos plan <take> --captions` does the same for a single take (`--captions none` keeps its meaning: the plan proposes none).
|
|
117
|
+
The first input is the primary: its frame, cursor style, export, sound and cut come along as they are. Each next input becomes a media of the document (`m1`, `m2`, …): its footage and sidecars are copied under `film/m1/…`, its segments are appended (so the output plays the inputs one after the other), every source-anchored span it carries (`zoom`, `speed`, `tilt`, `camMotion`, `freeze`, `rejected`) is tagged with its media and renamed `m1-…`, and its layers move to where its footage now starts. A layer it pinned to one of its steps keeps its moment through an `anchor` to that step (a pin places a layer on the primary's footage only, so the pin itself is dropped, and the done line says so); its sound clips stay behind. A layer that starts after an input's footage ends (an end card, a closing title) is that input's ENDING: every input's ending but the last one's is dropped, named in a note, because it would land over the next clip; the last input's stays at the end of the film. A plain video file becomes a media with no cursor track, a `meta` probed from the file, a full-bleed card of its own (`inset` 0, no corners, no shadow) and one segment over its whole length; its own sound is part of what it is, so it plays as an audio clip keyed to the copied file (`<id>-sound`, starting where the file plays, trimmed to its clip, a short fade at each end), and `--mute <file>` leaves it out. A file whose audio Node cannot decode (AAC) is copied as it is and the join says so in one line; the render reads it in the browser. The joined document must pass `vos validate` before a byte is written. `--out` is refused when it exists, unless `--force` names a take directory to replace; `--out` may not overlap an input. `--captions` lands every step's `caption` (from `meta.steps` or each take's `actions.json`) as a caption anchored to its step, starting as the step BEGINS (`anchor.at: "start"`, no offset, so the words name the beat while it plays; set `at: "end"` and an `offset` on a caption to land it after its gesture instead), in the house style, marked `from: "steps"` so a second run replaces only those; each ends with the clip it starts in (never over the next input's footage, and a step in the last second of its clip gets none), a step a caption is already anchored to is left alone, and an input none of whose steps carries a caption is named. `vos plan <take> --captions` does the same for a single take (`--captions none` keeps its meaning: the plan proposes none).
|
|
118
|
+
|
|
119
|
+
**Join again without losing the cut.** Re-running a join used to rebuild the document from its inputs, so adding a cold open or a scene recorded later lost every trim, speed span, kept or rejected zoom, caption, frame, sound and still made since. `--keep-edits` joins over the edited take instead:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
vos join intro-take settings-take outro.mp4 cold-open-take --out film --keep-edits --captions --zooms per-beat
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every join writes `join.json` beside the document: each input's path (relative to the take) and the sha256 of its footage, and the media id it became. A re-join matches inputs to media by that SOURCE FILE, never by position: the same footage at the same path is the same media (the same footage at a new path too), and a repeated input pairs in order. A kept input keeps its media id and everything on it as edited (its segments, in their edited order; every span on it, `rejected` included; the layers anchored to its steps; its sound clip; its media entry), and the film-wide fields (frame, cursor, cam, export, styles, still) are the edited document's. A new input is joined as a fresh join would join it, under an id no media of the take ever held, and `--captions` and `--zooms` act on the new inputs only (the done line counts what they did on those inputs, or says there was no new input). The done line names an input by its path from where the command runs. A RE-RECORDED input keeps its edits too, re-timed onto the new footage by its steps the way `vos plan --reuse` re-times a cut (the same mapping): a take whose footage changed at the same path, or a take at a new path that recorded the same script (more than half of the old take's named steps are in the new one, and the url agrees when both say one). It comes in under a new media id; its segments take the old ones' place in the cut, re-timed; its manual spans and rejections follow the steps they sat on (an unnamed step is found by its place beside the nearest named one); the planner's fresh proposals fill in where no kept span or rejection stands; a layer anchored to one of its steps stays on that step; a layer placed by output second over its footage keeps its place in the scene. What cannot be placed (a span or a layer on a step the new recording lacks) is dropped and named under `re-recorded …` in the done line. A plain video file has no steps to re-time by, so a changed file is a removal plus an addition. An input that is gone takes its edits with it, and the done line names them (`dropped …: 2 segments, 3 spans, layers c6, c6b went with it`). While the kept inputs keep their relative order, the edited segment list stands (a cold open moved to the front stays there) and a new input's segments go just before the first segment of the next kept input in argument order, else at the end; when the kept inputs are reordered, the segments are regrouped in argument order, each input's own segments in their edited order, and the done line says so. A layer, prop, sound clip or still placed at an output second (no anchor) follows the footage it played over; one past the footage stays past the new end; a film-wide clip such as a score keeps its seconds, and the done line names the changed length (`vos score` places a score again). An ending anchored to a step that is no longer last is named too. The first input must stay the take's primary, its footage unchanged: the primary carries the frame and the camera, and another one is another film (`--force` replaces the take). The take is updated in place, so its `vos.json` and everything else in it stays; a removed media's directory goes. A take joined before `join.json` existed is matched by the footage it holds (a take's footage is copied byte for byte); a video file joined then was re-encoded on the way in and reads as new.
|
|
126
|
+
|
|
127
|
+
**One zoom per beat.** The zoom planner proposes a span per gesture, which is right for a form and far too busy for a canvas, where every drag, label and copy is one. `vos plan <take> --zooms per-beat` (and `vos join … --zooms per-beat`) keeps at most one zoom per BEAT, and the script says where the beats are and where they pay off: a step with a `caption` starts a beat, which runs until the next captioned step of the same media starts (the last one to the media's end), and what plays before the first captioned step is one more beat, the opening. A step marked `"zoom": true` is its beat's PAYOFF: the beat keeps, for each marked step, the proposal that overlaps that step's own span (`tStart`..`tEnd` in `meta.steps`) most (ties: the one the cut plays longest, then the longer, the higher level, the earlier), never two that overlap, so a beat may keep two zooms on two marked steps. A beat with no marked step keeps NO zoom, which is how a beat stays wide. When no step of the take carries `"zoom": true`, each beat's LAST gesture (its last `click`, `drag`, `type` or `paste`; never a key press, which closes a beat more often than it pays it off) stands in for the mark, the opening keeps none, and the done line says `no step is marked "zoom": falling back to each beat's last gesture`. A step marked `"zoom": false` is never zoomed on, in either case, and a proposal on it is rejected with the note `zoom: false in the script`, whichever beat it sits in. A mark never invents a zoom: the planner still decides the level and the focus, and a marked step it proposed nothing for is named (`marked #4 click, but no proposal on it`). A span belongs to the beat its midpoint falls in. The done line names each beat, the step it zoomed on and the span kept (`beat "Draw the flow": marked #3 click, kept z1`), and lists a captioned beat the planner proposed nothing in (`beat "…": no proposal in this beat`). The kept span is promoted to `manual`, and every other proposal in the beat is written into `doc.rejected` the way a person deletes one (note `one zoom per beat`), so no re-plan brings it back. A beat that already holds a manual zoom keeps its manual ones and loses its proposals. A media with no captioned step keeps its zooms as planned, and the done line says so. A mark is read from the take's `actions.json` first, so a mark added after recording takes effect on the next `vos plan` with no re-record (and `vos plan`, `vos join` and `vos info` warn, never refuse, when the file no longer matches what was recorded: `actions.json has 122 steps and the take recorded 119 (meta.steps)`, with any recorded step whose verb or selector changed); then from the step the recorder kept (`meta.steps[].zoom`), then from the script a pushed or joined take carries. A recorded step is matched to its script step by `id`; one the recording has no id for (an id added to the script afterwards does not count, the recording never saw it) is found beside the nearest recorded named steps before and after it, else at its own index, and only where the verb (and the selector, when both say one) agrees, so a check or a step added to the script after recording hands no other step its caption or its mark. Steps with ids recorded are found wherever they move. After a join, each media's captions and marks travel in its `script`, so `vos plan` on the joined take finds them.
|
|
128
|
+
|
|
129
|
+
**Where everything lands.** `vos info <take>` prints the take's two clocks, read through studio-core's own functions (`ratedSegments`, `segmentOutputExtents`, `spanOutputExtents`, `longestOutputExtent`, `cameraSpanExtents`, `resolveOverlayAnchors`, `docOutputDuration`): per media, every step with its id, what it did, its SOURCE seconds (`meta.json`'s `tStart`..`tEnd`), its caption and where it plays in the cut; then the OUTPUT timeline after trims, speed and freezes: each segment, speed span, freeze, zoom and tilt span with its output extent (`(cut)` when the trims leave it out; on a cut that replays its seconds, a cold open, every other place it plays as `also`: a step's own place is the pass an anchor on it lands in, its longest, and a zoom's or a tilt's are the passes where it moves the camera, with `at rest in` for a clip that rests it), every layer with its anchor resolved, each sound clip, the still and the film's end. `--json` gives the same as one done event. It writes nothing.
|
|
119
130
|
|
|
120
131
|
**Music timed to the film.** `vos score` places ONE audio clip, id `score`, so that the strongest swell of a track lands on the film's payoff:
|
|
121
132
|
|
|
@@ -132,21 +143,21 @@ The track is decoded (mediabunny in Node for PCM; a compressed track in the head
|
|
|
132
143
|
vos record --hosted --actions actions.json --out take # needs a credential (vos login, or VOS_API_KEY); a public URL, or a preview behind --header
|
|
133
144
|
```
|
|
134
145
|
|
|
135
|
-
What travels: the script, the viewport, every `--header` value (a preview's bypass token), and the value of every `{ "env": "NAME" }` a `setup` step reads, from this shell. What never travels: a session (`--hosted` refuses `--session` and `--storage-state`, because none of a person's ever reaches vos.so). A page behind a sign-in fails on the fleet in words naming the local ladder, exit 4; a private-network URL is refused before a job exists. The fleet records up to ten minutes. `--dry-run` stays a local act. A script with a guarded step
|
|
136
|
-
|
|
137
|
-
| Verb | Flags
|
|
138
|
-
| -------- |
|
|
139
|
-
| `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` or `--session <name>` `--header name=value`... `--confirm <id>`... `--browser-arg=<switch>`... `--max-duration <s>` `--ready dom\|networkidle` `--background <slug\|url\|none>` `--hosted` (record on vos.so; with `--title` `--folder <id>`)
|
|
140
|
-
| `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
|
|
141
|
-
| `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` (bare: the step captions, anchored; `none`: no beat captions) `--clicks` `--release` |
|
|
142
|
-
| `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)
|
|
143
|
-
| `frames` | `--times 0,25%,50%,75%,100%` (the default selector, output seconds or percent) `--frame <t>` `--at-zooms` `--at-moments` `--size WxH` `--out <take>/stills` `--background` `--set …` `--strict`; writes `stills.json`
|
|
144
|
-
| `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>`
|
|
145
|
-
| `ingest` | `<video>` (positional) `--cursor <trace.zip\|steps.jsonl\|cursor.csv>` `--out take` `--offset <ms>` `--viewport WxH` `--background <slug\|url\|none>`; a `.zip` is a Playwright trace, `.jsonl`/`.json` are records (or a Playwright `.trace` by its lines), `.csv` is `t,x,y,type`
|
|
146
|
-
| `join` | `<take> <take\|file.mp4>...` (positional, in order) `--out <dir>` `--force` (replace a take directory) `--captions` (land the step captions, anchored) `--mute <file>`... (leave a file's own sound out)
|
|
147
|
-
| `score` | `<take>` `--track <slug\|file>` `--at <step id\|step:<index>\|media:step\|seconds>` `--gain 0..1` (0.5) `--normalize <lufs\|off>` (-16) `--fade-in <s>` (0.8 when entered mid-song) `--fade-out <s>` (1.5) `--dry-run`
|
|
148
|
-
| `audio` | `measure <take>` (write every audio clip's `loudness`, the fact a `normalize` target plays it to)
|
|
149
|
-
| `open` | `--studio <url>` (the studio; `https://vos.so` by default, `VOS_ORIGIN` moves it) `--print` (print the URL, do not launch a browser)
|
|
146
|
+
What travels: the script, the viewport, every `--header` value (a preview's bypass token), and the value of every `{ "env": "NAME" }` a `setup` or `teardown` step reads, from this shell. What never travels: a session (`--hosted` refuses `--session` and `--storage-state`, because none of a person's ever reaches vos.so). A page behind a sign-in fails on the fleet in words naming the local ladder, exit 4; a private-network URL is refused before a job exists. The fleet records up to ten minutes. `--dry-run` stays a local act. A script with a guarded step, a `download`, a `paste`, a `js` check or `peers` is refused on `--hosted` in words: nobody can confirm a step that spends on the fleet, a hosted take has no `outputs/` to keep a file in and no clipboard to paste from, a fleet browser runs no code it was handed, and it runs one browser.
|
|
147
|
+
|
|
148
|
+
| Verb | Flags |
|
|
149
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
150
|
+
| `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` or `--session <name>` `--header name=value`... `--confirm <id>`... `--browser-arg=<switch>`... `--max-duration <s>` `--ready dom\|networkidle` `--background <slug\|url\|none>` `--hosted` (record on vos.so; with `--title` `--folder <id>`) |
|
|
151
|
+
| `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 |
|
|
152
|
+
| `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` (bare: the step captions, anchored; `none`: no beat captions) `--clicks` `--release` `--zooms per-beat` (at most one zoom per captioned step) |
|
|
153
|
+
| `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) |
|
|
154
|
+
| `frames` | `--times 0,25%,50%,75%,100%` (the default selector, output seconds or percent) `--frame <t>` `--at-zooms` `--at-moments` `--size WxH` `--out <take>/stills` `--background` `--set …` `--strict`; writes `stills.json` |
|
|
155
|
+
| `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>` |
|
|
156
|
+
| `ingest` | `<video>` (positional) `--cursor <trace.zip\|steps.jsonl\|cursor.csv>` `--out take` `--offset <ms>` `--viewport WxH` `--background <slug\|url\|none>`; a `.zip` is a Playwright trace, `.jsonl`/`.json` are records (or a Playwright `.trace` by its lines), `.csv` is `t,x,y,type` |
|
|
157
|
+
| `join` | `<take> <take\|file.mp4>...` (positional, in order) `--out <dir>` `--keep-edits` (join again over an edited joined take, keeping its edits) or `--force` (replace a take directory) `--captions` (land the step captions, anchored) `--zooms per-beat` `--mute <file>`... (leave a file's own sound out) |
|
|
158
|
+
| `score` | `<take>` `--track <slug\|file>` `--at <step id\|step:<index>\|media:step\|seconds>` `--gain 0..1` (0.5) `--normalize <lufs\|off>` (-16) `--fade-in <s>` (0.8 when entered mid-song) `--fade-out <s>` (1.5) `--dry-run` |
|
|
159
|
+
| `audio` | `measure <take>` (write every audio clip's `loudness`, the fact a `normalize` target plays it to) |
|
|
160
|
+
| `open` | `--studio <url>` (the studio; `https://vos.so` by default, `VOS_ORIGIN` moves it) `--print` (print the URL, do not launch a browser) |
|
|
150
161
|
|
|
151
162
|
**When the take starts.** Step 1 runs once the first page is ready, and by default that means it has stopped changing: after `load`, the page must make no DOM change for 800 ms, at most 8 s (`ready: "dom"`). Network idle is the opt-in (`"ready": "networkidle"` in `actions.json`, or `--ready networkidle`), at most 45 s, because a page that holds a socket open or keeps an analytics beacon going never goes network-idle and every take of it would start after the whole timeout. The same mode opens a `setup` step's `goto`, the page after the setup, and an `await`'s reload. The done event says how the first load settled, `ready: { mode, ms, capped }`; `capped` is a page still changing at the cap (an animation, a ticker), recorded anyway, and `navTimeout: true` now means only that network idle was asked for and never came.
|
|
152
163
|
|
|
@@ -158,6 +169,7 @@ What travels: the script, the viewport, every `--header` value (a preview's bypa
|
|
|
158
169
|
vos session open https://app.acme.com --name acme # a plain Chrome window opens; sign in, set the account up, close the window (or press Enter)
|
|
159
170
|
vos session check acme --url https://app.acme.com/dashboard # does it still open the page? exit 0, or 4
|
|
160
171
|
vos record --actions actions.json --out take --session acme --strict
|
|
172
|
+
vos session exec acme list-keys.mjs --url https://app.acme.com/settings # your own Playwright code, inside the session
|
|
161
173
|
vos session list · vos session rm acme
|
|
162
174
|
```
|
|
163
175
|
|
|
@@ -165,6 +177,8 @@ vos session list · vos session rm acme
|
|
|
165
177
|
|
|
166
178
|
Measured before this was written: a profile plain Chrome signed in reopens signed in ONLY under the system Chrome with the real keychain. Playwright's bundled Chromium keeps its own keychain entry and can never decrypt Chrome's cookies, so every reader of a session here is the system Chrome (`VOS_BROWSER_PATH` overrides it) with Playwright's `--use-mock-keychain` removed. Two things `check` says because they cost real runs: an app whose session cookie has no expiry leaves nothing to reuse once the window closes, and a Chrome that was killed rather than quit never wrote its store.
|
|
167
179
|
|
|
180
|
+
**Never open a session's profile yourself.** A browser that cannot decrypt the profile's cookies does not merely fail to read them: it starts the cookie store over, and the session is signed out for good. Measured: a cookie the system Chrome kept through every reopen was gone after ONE launch of Playwright's bundled Chromium on the profile, for the system Chrome too. So a script that needs the signed-in browser (a state a step cannot reach yet, a list of what the account holds, cleaning up after a flow) runs inside the session with `vos session exec <name> <script.mjs>`: an ES module whose default export receives `{ context, page, url, name }` (the page already on `--url` when one is given), run on the profile exactly as `record` opens it, headless unless `--headed`, `--viewport WxH` when it matters. What the function returns is printed (`result` in the `--json` done event), the profile is closed cleanly whatever the script does, and a script that throws exits 1 with its error. `check` names a profile opened another way as a cause of a session that no longer opens signed in.
|
|
181
|
+
|
|
168
182
|
A session never enters a take directory and never reaches vos.so: `vos push` refuses a take holding a storage-state-shaped JSON, naming the file.
|
|
169
183
|
|
|
170
184
|
**`vos look`: see a page before scripting it.** An agent writing `actions.json` needs the page and a selector for each control it will touch, and behind a login the only other way to see the page is to record it. `vos look <url>` opens the page on the same browser and the same door `record` would use (`--session <name>` or `--storage-state <file>`, `--header`, `--browser-arg=`; headless unless `--headed`, `--viewport WxH` 1280x720 by default), waits until the DOM has held still for 0.8 s (never for network idle, which many apps never reach), runs any `--click <selector>` and `--type <selector>=<text>` in the order given, each followed by the same settle, and looks:
|
|
@@ -187,7 +201,17 @@ It writes `<out>/look.png` (the viewport; `--out look` by default) and lists eve
|
|
|
187
201
|
]
|
|
188
202
|
```
|
|
189
203
|
|
|
190
|
-
**`
|
|
204
|
+
**`teardown`: the steps that run after it.** A flow that CREATES something in the account (an API key, a project, an invite) creates it on every rehearsal and every take, since `--dry-run` clicks for real. `teardown: [...]` beside `setup` removes it again: the same verbs and the same rules (a `{ "env": "NAME" }` text, no literal in a password field, http and https only), run off camera once the screencast has stopped, on the take's own page and session, so nothing of it is captured or lands in `meta.steps`. It runs after a rehearsal too, and when the take stops early (a failed `await`, `--strict` on a page that stopped answering, the cap) or throws once its first step ran; not when the take never began (a setup that failed, a wall). The steps together get 60 s. A teardown never fails the take or changes the exit code: it stops at the first step that fails, the done event carries `teardown: { ran, of, failed?: { step, do, selector?, error } }`, and the summary says `teardown: 3 step(s) ran` or `TEARDOWN FAILED at teardown[1] (click #confirm): <reason>. What the take created may still be in the product; remove it by hand.` A `{ env }` the shell does not hold refuses the take before it begins, since the take would create what its teardown cannot remove.
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
"teardown": [
|
|
208
|
+
{ "do": "goto", "url": "http://localhost:3000/settings/keys" },
|
|
209
|
+
{ "do": "click", "selector": "[data-testid=key-row]:first-child button[aria-label=Revoke]" },
|
|
210
|
+
{ "do": "click", "selector": "role=button[name=\"Revoke key\"]" }
|
|
211
|
+
]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**`vos actions script <actions.json>`: the flow as a shot list.** When no rung of the session ladder holds and a person has to record the take themselves (in their own signed-in browser, with the vosso extension), the agent hands over the flow it worked out rather than an apology: numbered beats in plain words with the holds the script asked for, the page to start on and about how long. A step's `caption` leads its beat, a `text=` or `:has-text()` selector is said as its words, and a step's `id` is said as a name (`new-order` reads as "new order"), so give steps ids and captions that a person could follow. A script's `setup` is said as what to do before pressing record, and its `teardown` as what to undo after stopping. `--json` carries the beats beside the text. The take comes back through the ordinary handoff, and the agent cuts it (`vos pull --media`, then the `vos-cut` skill).
|
|
191
215
|
|
|
192
216
|
**`--header name=value`**, repeatable: a request header on every request the recording browser makes, the way past a preview deployment protected by a bypass header alone (`--header x-vercel-protection-bypass=$TOKEN`). The rehearsal's `Next:` line carries it.
|
|
193
217
|
|
|
@@ -214,7 +238,7 @@ It writes `<out>/look.png` (the viewport; `--out look` by default) and lists eve
|
|
|
214
238
|
|
|
215
239
|
**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.
|
|
216
240
|
|
|
217
|
-
**A series shares its look by data.** `vos plan <take> --style <seed doc.json | vosId>` copies the seed's `zoomStyle`, `zoomParams`, `speedParams`, `tiltStyle`, `frame`, `cursor`, `cam` and `export` onto a new take (never its spans, overlays or
|
|
241
|
+
**A series shares its look by data.** `vos plan <take> --style <seed doc.json | vosId>` copies the seed's `zoomStyle`, `zoomParams`, `speedParams`, `tiltStyle`, `frame`, `cursor`, `cam` and `export` onto a new take (never its spans, overlays, audio or cursor-off ranges) and re-plans the automatic spans under them. A seed that is a POSTER carries its layout too: its card placement, its `stage-*` clips by id (with the release's words from LAUNCH.md's `headline` and `kicker` roles, BRAND.md's `wordmark`, or `--headline` and `--kicker` patched in; the brand's mark from BRAND.md `logoUrl` fetched into `<take>/brand/` for `stage-mark`), its rest lean, its trailing freeze. What could not follow is said in words.
|
|
218
242
|
|
|
219
243
|
**The cut's motion is the document's, in one vocabulary.** Every visual thing carries `anim` (`enter`, `exit`, `idle`; a kind, or a step with its seconds and, for words, `unit`, `direction`, `stagger`): the card (`frame.anim`, entering by `tilt-in`, `pull-out`, `rise`, `fade` or `slide` and leaving by `recede` or `fade`), a text, image or video clip, a prop, and a FOOTAGE clip at its boundaries (`segments[].anim`: `exit` is how it leaves at the boundary after it, `enter` how it arrives at the one before, by `slide`, `fade` or `scale`; a slide names its `side`, a step its `seconds`, never longer than half the shorter clip). An exit and the next clip's enter at one boundary run together as a push: the incoming clip plays live while the outgoing, frozen on its last frame, moves away, the camera rests through the window, and one cursor dot crosses from the outgoing card's last point to the incoming's. A page change inside one recording is the same primitive: two clips of one media, the load between them cut. The output lasts until the last clip ends; the card is on screen exactly while its clip runs (the footage, freezes included) and leaves at its end, so its `exit` plays over the clip's last seconds like every clip's; past the footage the clips play over the ground alone. A FREEZE (`freeze: [{id, at, seconds}]`, a source moment held for output seconds, beside the speed spans on the retime lane) freezes the footage anywhere, the middle of the take included; a segment's older `hold` is read as a freeze at its end. **The frame that STANDS for a take** is `still` (output seconds; `vos plan --still <t>`, LAUNCH.md `still:`), else the take's own last freeze (a poster's rest; a template's freezes, stamped `from`, never count), else the hero moment after the card and the opening clips have entered: the shelf's cover and `vos frames --at-still` both read that one derivation, and a plan never moves a still the author set. There is no end-card field and no entrance field. A COMPONENT is a TEMPLATE: a plain take on a shelf whose clips carry stable ids, laid onto a take at an anchor and stamped `from` with where its clips came from. A fresh `vos plan` proposes the card's enter, the templates the recipe names (`LAUNCH.md` `with: <ref>[@end|@start|@step:<id>|@<seconds>], …` or `--with`, repeatable; `endCard: <ref>` names one at the end), the END CARD when `endCard` is on or absent: the official `End card` template on vos.so (the platform's `Templates` project, promoted; any official template resolves by its title, `--with "End card"`, `--style "Split cover, landscape"`), laid at the end with the release's words and BRAND.md's mark, stamped `from: endcard`; offline, the house clips stand in (a freeze of the last frame for the card's seconds, the card receding over it, the headline, the release line, the wordmark and the mark as clips over the freeze), a caption per `actions.json` step, a music bed and click sounds, a transition at every page change the recorder marked (`meta.steps[].navigated`: the outgoing clip ends on the frame before the change, the incoming begins once the page has settled; `transitions: slide|fade|scale|none`, slide by default), from LAUNCH.md's `entrance`, `transitions`, `endCard`, `with`, `captions`, `music` and `clicks` roles (or the flags), as data in doc.json with stable ids (`bed`, `click-<n>`, `caption-<step>`), so the studio shows what the kit will render; a refresh never re-proposes (a deleted clip stays deleted) and `--motion` re-proposes on purpose, replacing only its own work. A document written in the older spellings (`frame.entrance`, `endCard`, a clip's `enter`/`exit`/`fx`, a prop's `animation`) is read into the vocabulary and `vos validate` says so.
|
|
220
244
|
|
|
@@ -249,7 +273,10 @@ take/
|
|
|
249
273
|
meta.json RecordingMeta (producer: "cli", per-step source extents)
|
|
250
274
|
actions.json the script that produced it, with the resolved url; pushed inside the document and written back by a fetch or pull with --media
|
|
251
275
|
doc.json ProjectDoc, the editable cut
|
|
252
|
-
doc.prev.json the previous cut, kept by a re-record for
|
|
276
|
+
doc.prev.json the previous cut, kept by a re-record for `--reuse`
|
|
277
|
+
.doc-planned the hash of the plan the last recording wrote, so a re-record
|
|
278
|
+
knows an unedited doc.json from a cut
|
|
279
|
+
join.json a joined take's inputs: each one's path, footage sha256 and media id, read by `vos join --keep-edits`
|
|
253
280
|
outputs/ the files `download` steps kept, listed in meta.json's outputs
|
|
254
281
|
page-sound/ the sound the page played during the take, listed in meta.json's media
|
|
255
282
|
vos.json hosted tracking (vos id + base version), written by push, fetch and pull
|
|
@@ -266,7 +293,7 @@ claimed. Every verb resolves the name, and a push declares the container it
|
|
|
266
293
|
finds in the bytes, so a file never reaches vos.so under the wrong type.
|
|
267
294
|
`vos validate <take>` warns when an existing take holds one that does.
|
|
268
295
|
|
|
269
|
-
A re-record replaces the footage, the cursor track, the frames, `outputs/` and everything derived from them, moves `doc.json` to `doc.prev.json`, and keeps `actions.json` and `vos.json`.
|
|
296
|
+
A re-record replaces the footage, the cursor track, the frames, `outputs/` and everything derived from them, moves `doc.json` to `doc.prev.json`, and keeps `actions.json` and `vos.json`. With `--reuse` the cut goes onto the new footage in the same command (what `vos plan <take> --reuse` does afterwards); without it the take is planned fresh, and its last line and its done event (`previousCut`, `next_step`) say where the cut is and how to apply it. A plan nobody edited never takes the cut's place: recording again over a take whose `doc.json` is still the plan its recording wrote keeps the cut already in `doc.prev.json`.
|
|
270
297
|
|
|
271
298
|
## actions.json
|
|
272
299
|
|
|
@@ -290,7 +317,7 @@ A re-record replaces the footage, the cursor track, the frames, `outputs/` and e
|
|
|
290
317
|
}
|
|
291
318
|
```
|
|
292
319
|
|
|
293
|
-
Nine verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150; `count: 2 | 3` for a double or triple click, `button: "right" | "middle"`, `modifiers` held for the press), `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; or `keys`, a chord, a plain key or a sequence, below; `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; `modifiers` and `path`, below), `paste` (below). 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.
|
|
320
|
+
Nine verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150; `count: 2 | 3` for a double or triple click, `button: "right" | "middle"`, `modifiers` held for the press), `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; or `keys`, a chord, a plain key or a sequence, below; `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; `modifiers` and `path`, below), `paste` (below). 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`. A gesture step (`click`, `drag`, `type`, `paste`, `press`, `hover`) takes `"zoom": true` to mark it as its beat's payoff, the step `--zooms per-beat` keeps its zoom on, or `"zoom": false` for never zoom here: a plain `vos plan` rejects the planner's proposal on a `false` step for good, and names a `true` step it proposed nothing for (no zoom is invented); the recorder keeps the mark on `meta.steps`, `vos push` carries it in the script and `vos fetch` writes it back. 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.
|
|
294
321
|
|
|
295
322
|
**Canvas gestures**: what a whiteboard, a design tool or a timeline is recorded with.
|
|
296
323
|
|
|
@@ -333,11 +360,11 @@ Nine verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle afte
|
|
|
333
360
|
- **`drag` `path`** draws: viewport points inline (`[x, y]`, `[x, y, tMs]`, `[x, y, tMs, pressure]` or `{ "x", "y", "t", "pressure" }`, every point timed or none), or a file beside the actions file by a plain name (`"path": "loop.json"`, an array of points or `{ "points": [...] }`). The pointer travels to the first point, presses there, moves through every later point in order and releases at the last, replayed by the points' own times, or evenly over `ms` when they carry none. Every point is sent: a page that answers slowly makes the stroke late (an overrun, below), never a different stroke. A point that carries `pressure` draws the whole path with a pen, so the page's pointer events carry that pressure; without, the mouse draws it. A path takes no `selector`, `x`, `y`, `tx` or `ty`. The cursor track holds the press, every point and the release. The take keeps its script self-contained: a path a file held is written inline (with its times and pressure) in the take's `actions.json`, so the push embeds it, a fetch writes it back and a hosted take runs it, and a re-record draws the same strokes with the file gone; `--hosted` sends a path file's points inline too.
|
|
334
361
|
- **`click` `count`** 2 or 3 is a real double or triple click (that many presses inside the platform's double-click interval, each with its click count); `button` presses the right or middle one; `modifiers` (`Alt`, `Shift`, `Meta`, `Control`, `Mod`) are held for the press and released after it (a Shift-click adds to a selection). Every press is a down and an up on the cursor track.
|
|
335
362
|
- **`click` and `hover` at a point**: `x` and `y` (viewport CSS px) instead of a `selector`, for a spot no element names: a place on a canvas, a handle, empty space to click on to focus or deselect. The cursor track carries the press at that point with no element rect, so the planner treats it as a point. A drawn shape's DOM box is not always where it is drawn (an arrow's container sits at its anchor), so a point is often the truer target on a canvas.
|
|
336
|
-
- **`paste`** puts `text`, or a `file` beside the actions file (an image, an SVG, a text file; at most 10 MB), on the clipboard and pastes it the way a person's ⌘V does, into `selector` (clicked first) or where the focus is. The browser's own clipboard holds the item (a headless browser keeps its own, so this machine's clipboard is untouched), and the recorder presses the platform's paste chord, so the page gets a trusted keydown and a trusted `paste` event, and a page that reads `navigator.clipboard.read()` itself finds the item there. An SVG rides as its markup too. An IMAGE file is dispatched in the page with its own bytes instead (the focused element gets a `paste` event holding the file, and `navigator.clipboard.read` answers it for that paste only: shortly after it is first read, or after 3 s unread, the browser's own reads come back, so a later paste goes through the real clipboard), because the browser's clipboard re-encodes a picture and drops what it carries, a screenshot's density among it, so a 2x capture would land at twice its size; the log says which path ran, and that those events are untrusted. Words keep the trusted path, and fall back to the same dispatch in the page when the browser refuses them. A paste is a typing ping, never its contents. The take keeps a pasted file beside its script as `paste-<name>`, and its `actions.json` names the copy, so the take re-records on this machine with the original gone. A
|
|
363
|
+
- **`paste`** puts `text`, or a `file` beside the actions file (an image, an SVG, a text file; at most 10 MB), on the clipboard and pastes it the way a person's ⌘V does, into `selector` (clicked first) or where the focus is. The browser's own clipboard holds the item (a headless browser keeps its own, so this machine's clipboard is untouched), and the recorder presses the platform's paste chord, so the page gets a trusted keydown and a trusted `paste` event, and a page that reads `navigator.clipboard.read()` itself finds the item there. An SVG rides as its markup too. An IMAGE file is dispatched in the page with its own bytes instead (the focused element gets a `paste` event holding the file, and `navigator.clipboard.read` answers it for that paste only: shortly after it is first read, or after 3 s unread, the browser's own reads come back, so a later paste goes through the real clipboard), because the browser's clipboard re-encodes a picture and drops what it carries, a screenshot's density among it, so a 2x capture would land at twice its size; the log says which path ran, and that those events are untrusted. Words keep the trusted path, and fall back to the same dispatch in the page when the browser refuses them. A paste is a typing ping, never its contents. The take keeps a pasted file beside its script as `paste-<name>`, and its `actions.json` names the copy, so the take re-records on this machine with the original gone. **A pasted file travels with the take**: `vos push` sends each one with it and the document lists them (`source.scriptFiles`, the name the script pastes it by and where its bytes are), a picture through the same upload door as every other file the document names and a text file inside the document itself (at most 256 kB; the platform stores no text files), and `vos fetch --media` and `vos pull --media` write each one back beside `actions.json` under the name the script uses, never over a file already there, so a fetched take records again from anywhere. A paste file the take does not hold is said and left out of the push; one that resolves outside the take directory (a link out of it) is refused before anything is sent. A claimable push carries a pasted text file and a pasted picture the take fetched unchanged, and says so of any other. A drag's `path` needs none of this: the recorder writes a path file's points inline into the take's `actions.json`, so the pushed script carries the stroke itself.
|
|
337
364
|
- **`settle: true`** on a gesture step (or at the top of the script, for every one; a step's `settle: false` opts out) waits after the gesture until the page has made no DOM change and painted no new frame for 300 ms, counted from the page's last change (a page already still for longer goes on at once), at most 4 s: a heavy canvas that falls behind under load finishes the last stroke before the next one starts. `meta.steps[].settleMs` says how long it waited, and a page still changing at the cap is said in the log. A rehearsal captures no frames, so there it waits on the DOM alone.
|
|
338
365
|
- **Overruns**: a gesture the script paces (a straight drag's travel, a path's timing, typing, a key sequence, whose plan holds each key's round trip as well as its gaps) that took more than a fifth longer than planned, and at least 100 ms longer, is reported: `meta.steps[].overrunMs`, `overruns: [{ step, id, do, plannedMs, tookMs, overrunMs }]` in the done event, and one line (`3 steps ran 2.1× slower than scripted (…): the page could not keep up; add settle or raise ms`).
|
|
339
366
|
|
|
340
|
-
A hosted take (`--hosted`) refuses a `paste` (a fleet browser has no clipboard)
|
|
367
|
+
A hosted take (`--hosted`) refuses a `paste` (a fleet browser has no clipboard), a `path` read from a file (the script travels alone; write the points inline) and a `js` check (code a fleet browser would run on a stranger's say).
|
|
341
368
|
|
|
342
369
|
Three steps that are not gestures, and a guard:
|
|
343
370
|
|
|
@@ -362,8 +389,9 @@ Three steps that are not gestures, and a guard:
|
|
|
362
389
|
}
|
|
363
390
|
```
|
|
364
391
|
|
|
365
|
-
- **`expect`** checks the page on camera, with no cursor move: exactly one of `selector` (visible; with `enabled: true | false`, enabled or disabled), `text` (some visible text contains it)
|
|
366
|
-
- **`await`** waits OFF CAMERA for something slow: a render, a generated result, a deploy. The capture stops, the cursor clock stops with it, and `until` (`{ selector }`, `{ text }` or `{
|
|
392
|
+
- **`expect`** checks the page on camera, with no cursor move: exactly one of `selector` (visible; with `enabled: true | false`, enabled or disabled), `text` (some visible text contains it), `gone` (no longer visible) or `js` (below), waiting up to `timeout` seconds (5). A condition that never holds is a skip with its reason (`#10 expect: 'text=Generate video' stayed disabled for 5 s`), and `--strict` turns it into exit 2. Separately, every `click` asks whether its target is disabled: the press still happens, and the done event lists it under `disabled: [{ step, id, selector }]`, the rehearsal marks it, and `--strict` fails on it.
|
|
393
|
+
- **`await`** waits OFF CAMERA for something slow: a render, a generated result, a deploy. The capture stops, the cursor clock stops with it, and `until` (`{ selector }`, `{ text }`, `{ gone }` or `{ js }`, with `equals` beside a `js`) is polled every `poll` seconds (2) for up to `timeout` (600), reloading the page between looks with `reload: true` (after a reload the recorder lets the page settle, no DOM change for 0.8 s, and wants the condition on two reads in a row, so a list drawn after the network went quiet cannot end the wait early). When it holds, the capture resumes, so the footage runs straight from the moment before the wait to the moment after: no gap, no frozen stretch. The cut is recorded in `meta.skips[]` (`{ step, id, at, ms }`: `at` the footage second of the cut, `ms` the wall time that did not reach the footage) and on the step's `meta.steps[]` entry (`offCameraMs`), and the planned `doc.json` starts a segment at every `at`. On timeout, `onTimeout: "fail"` (the default) stops the take there with exit 1 and a sentence naming the step and the time it waited, keeping the footage up to it; `"continue"` resumes and marks the skip `timedOut`. A rehearsal waits for real and prints how long it took, which is how you learn the timing. A wait that holds within 250 ms cut nothing and records no skip or segment boundary (the step still reports it). A predicate that may already be true before the action (an older item with the same text, a spinner not yet shown) holds at once: wait for something only the new result has. Under `--json` an await reports `{ event: "await", step, id, phase: "start" }`, a `phase: "wait"` heartbeat with `elapsedMs` every 30 s, and `phase: "end"` with `ms`.
|
|
394
|
+
- **`js`** checks what an app keeps in SCRIPT rather than in the DOM: a canvas app's shapes, a selection, a document model, so a take can say "the board has 14 shapes" the way it says "the button is enabled". `{ "do": "expect", "js": "window.app.count() === 14" }` holds when the expression, evaluated in the page (a promise is awaited), is truthy; `{ "do": "expect", "js": "window.app.count()", "equals": 14 }` holds when its value equals `equals` (compared as JSON, any key order), and is the form to prefer, because a miss then says what the app had: ``#3 board expect: js `window.app.count()` was 15, not 14, for 5 s``, where a bare comparison can only say it `was false`. An expression that throws (the app not loaded yet) is read again until the timeout, and a miss says what it threw. `await` takes the same in `until` (`{ "js": "window.app.status()", "equals": "done" }`). The value the expression last answered is kept on the step, `meta.steps[].value` (its JSON, at most 2 KB, else that JSON cut short), so the take carries the app's state at that beat as `text` carries an element's words, and a rehearsal prints it beside the step. Timeouts and `--strict` are an `expect`'s and an `await`'s; an expression that does not parse is refused before a browser opens (`vos validate actions.json` says so too). It is the script's own code run on its own page: write only what reads state, never what changes it, and a hosted take refuses it.
|
|
367
395
|
|
|
368
396
|
**Every step is bounded, and says when it runs.** A page that stops answering (a busy loop, a renderer stuck after a reload the page made itself) would leave a step waiting on it for good, so every call the recorder makes into the page is held to the step's own bound: an `expect` its `timeout` plus 5 s, an `await` its `timeout` plus 30 s, a `download` its 30 s plus 30 s, any other step its planned time plus 30 s. A step that runs into its bound ends as a skip whose reason carries the real time it took (`the page stopped answering: this expect ran 6 s, past its bound of 6 s`); under `--strict`, and always in a rehearsal, the take stops there, keeping the footage up to it. Under `--json` every step reports `{ event: "step", phase: "start", i, id, do }` as it begins and `phase: "end"` with `ms` (and `skipped`, `hung`) as it ends, so a slow take is never minutes of silence.
|
|
369
397
|
|
|
@@ -372,6 +400,38 @@ Three steps that are not gestures, and a guard:
|
|
|
372
400
|
|
|
373
401
|
`vos actions script` says these too: `Check that … is enabled`, `Wait until … (off camera: pause the recording meanwhile)`, `Download …`, `Press ⌘/Ctrl+Z`, `Press v then Escape`, `Hold Alt/Option and drag …`, `Draw the stroke in loop.json, in one press`, `Double-click …`, `Copy the file logo.svg, then paste it (⌘/Ctrl+V)`, and a guarded step is marked as one that spends.
|
|
374
402
|
|
|
403
|
+
**A second person on the page: `peers`.** A collaboration product demos best with someone else on the canvas: their cursor gliding in, the shape they drew, the comment they left. `peers` adds other people to a take, each a browser context of its own (its own cookies, so its own user) on the same page, driven by its own steps and recorded OFF CAMERA:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"url": "https://app.example.com/board/42",
|
|
408
|
+
"steps": [
|
|
409
|
+
{ "id": "open", "do": "wait", "ms": 600 },
|
|
410
|
+
{ "id": "see-bob", "do": "expect", "text": "Bob is here" },
|
|
411
|
+
{ "id": "draw", "do": "drag", "x": 300, "y": 220, "tx": 420, "ty": 300 },
|
|
412
|
+
{ "id": "bob-replies", "do": "await", "until": { "text": "Bob commented" } }
|
|
413
|
+
],
|
|
414
|
+
"peers": [
|
|
415
|
+
{
|
|
416
|
+
"id": "bob",
|
|
417
|
+
"storageState": "bob.json",
|
|
418
|
+
"steps": [
|
|
419
|
+
{ "after": "draw", "do": "click", "selector": "text=Comment" },
|
|
420
|
+
{ "do": "type", "selector": "textarea", "text": "Ship it" },
|
|
421
|
+
{ "do": "press", "key": "Enter" }
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
- **Only the take's page is filmed**, and only the take's own steps make the cursor track: a peer's page is never screencast, scanned, masked or listened to, and a peer's gestures never become a cursor event, a click ring, a zoom or a dwell. What a peer does reaches the footage the way a viewer would see it, through the app's own sync (its presence, its remote cursors, the work it made). A peer's pointer glides and its typing is paced on the take's clock, so an app that draws remote cursors shows a person, not a jump.
|
|
429
|
+
- **Ordering is the take's.** A peer step names a take step by id: `with` starts it as that step starts (the two run together), `after` once that step has ended (its settle and its reads included). A step naming neither follows the peer's previous step at once, and a peer's first step with neither starts as the camera rolls. The take never waits on a peer: it waits on what the peer's work SHOWS, with its own `expect` or `await`, so the footage holds exactly the moment the sync landed (above, the take's `await` cuts out however long Bob's comment takes to arrive). A trigger that has already fired when the peer reaches it runs at once, marked `late`. When the take's steps are over, a peer step still waiting never runs (said, `notRun`), and one already running finishes, bounded, before the trailing hold.
|
|
430
|
+
- **A peer takes the take's gestures and its check**: `wait`, `move`, `hover`, `click`, `type`, `press`, `scroll`, `drag` (a `path` inline only) and `expect`, in exactly the take's shapes and words, each bounded like a take step. A miss is a skip listed with the take's (`skipped[]` entries carry `peer`), which `--strict` fails on. A `paste`, a `download`, an `await`, a guard, a `read`, a `caption`, `settle` and a `zoom` mark belong to the filmed page and are refused on a peer.
|
|
431
|
+
- **Each peer is its own user**: `storageState` names a Playwright storage state of the second person's sign-in (`context.storageState({ path })`), by a path from the actions file's directory or an absolute one; absent, the peer is signed out. It is opened by the browser and nothing else: never copied into the take, never uploaded, and a pushed script keeps its file name only. A peer that lands on a sign-in refuses the take before anything is captured, and so does a storage state that is not there. `url` (the take's by default) and `viewport` (the take's) may differ per peer; `--header` applies to each peer's own origin as to the take's. At most 4 peers, 200 steps each.
|
|
432
|
+
- **What it records**: `meta.peers[]` (`{ id, steps: [{ step, id, do, with | after, selector, tStart, tEnd, skipped, reason, late, value }], notRun }`, in the take's footage seconds; an `expect` whose check reads the app's state keeps what it last answered in `value`, as the take's own steps do) and `peers` in the done event; a rehearsal (`--dry-run`) runs the peers too and lists their steps under the take's. `vos actions script` says a peer's steps as a second person's beats against the take's (`Once beat 3 is done: Click "Comment"`).
|
|
433
|
+
- **Not supported**: a peer on a `--session` profile (a peer signs in from a storage state), a peer recorded by the hosted recorder (`--hosted` refuses peers: a fleet take runs one browser), a take step that waits on a peer step directly (wait on what it shows), and determinism a network cannot give: a peer's effect lands when the app's sync delivers it, which is why the take checks for it rather than sleeping.
|
|
434
|
+
|
|
375
435
|
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.
|
|
376
436
|
|
|
377
437
|
Not sure of a selector? `vos look <url>` (with the take's `--session` or `--storage-state`) prints every control on screen with one proposed selector, counted UNIQUE or AMBIGUOUS by the recorder's own engine, and `--click` walks it to the next screen; take a UNIQUE one into a step as it is, then rehearse with `--dry-run`.
|
|
@@ -380,7 +440,7 @@ Not sure of a selector? `vos look <url>` (with the take's `--session` or `--stor
|
|
|
380
440
|
|
|
381
441
|
`doc.json` is a `ProjectDoc` from [`@vosjs/studio-core`](../studio-core), all plain JSON. Zoom is `zoom: [{ id, in, out, level, cx, cy, source }]`, trims are `segments`, pacing is `speed`. Edit and re-render; nothing re-runs the browser. The full shape ships as a JSON Schema at [`schema/doc.schema.json`](./schema/doc.schema.json) (a `oneOf`: the recording document and the program document, sharing the layer definitions), and `vos validate <dir>` lints the semantics of either (span overlap, footage bounds, coordinate ranges, export honesty) before you spend a render.
|
|
382
442
|
|
|
383
|
-
**Contracts that bite.** Time is source seconds in `zoom`, `segments`, `speed` and `tilt`, and output seconds in `overlays` and `audio`. Zoom `cx`/`cy`, overlay `transform.x`/`y` and a pin's `rect` are normalized fractions of the frame in `[0, 1]` (`0.5, 0.5` is the centre), never pixels. `level` is 1..5. The planners write `source: "auto"`; spans you add or edit carry `source: "manual"`, which a re-plan never touches, and a deleted automatic span is recorded in `doc.rejected` so a re-plan does not bring it back.
|
|
443
|
+
**Contracts that bite.** Time is source seconds in `zoom`, `segments`, `speed` and `tilt`, and output seconds in `overlays` and `audio`. The output plays `segments` in list order, and a segment may repeat seconds an earlier one played (a cold open of the final state, then the recording from its start, needs no second copy of the file): a zoom, tilt, cam move, speed span, freeze or click over those seconds plays in every occurrence, and a step-anchored layer starts in the pass where its step plays the longest. A segment marked `"camera": "rest"` keeps the camera at rest through its pass (no zoom, tilt, cam move or follow recenter there; the cursor, clicks, speed spans and freezes still play, and a span running into or out of it ramps at its edge), so a cold open can show the settled whole while the main play zooms; `vos validate` names, for each replay, the passes the camera rests in. Zoom `cx`/`cy`, overlay `transform.x`/`y` and a pin's `rect` are normalized fractions of the frame in `[0, 1]` (`0.5, 0.5` is the centre), never pixels. `level` is 1..5. The planners write `source: "auto"`; spans you add or edit carry `source: "manual"`, which a re-plan never touches, and a deleted automatic span is recorded in `doc.rejected` so a re-plan does not bring it back.
|
|
384
444
|
|
|
385
445
|
**Camera styles.** `doc.zoomStyle` is one of `glide` (default: one steady zoom that travels between clicks), `focus` (a zoom per click, framed and released), `cinema` (slow moves and long holds), `snappy` (quick cycles), `cut` (instant zoom-ins) or `none`. Every style times the tilt track to its own zoom tempo, so a lean lands with its zoom; how far the card leans is `tiltStyle` (`off` | `subtle` | `medium` | `strong`), which the studio stamps when a style is picked and `doc.json` sets itself, with `tilt` spans. `keynote` and `drift` are retired names that still read: `glide` + `tiltStyle: medium` and `cinema` + `tiltStyle: subtle`.
|
|
386
446
|
|
|
@@ -392,7 +452,7 @@ Not sure of a selector? `vos look <url>` (with the take's `--session` or `--stor
|
|
|
392
452
|
|
|
393
453
|
**Pinned layers.** A layer ABOUT something on the page names it, and the lowering keeps the two together: `"pin": { "step": "copy", "side": "auto", "mark": "ring", "leader": true }` on any overlay kind places the layer a gap off one side of that step's element and carries it with the element as the camera moves (the layer keeps its screen size; only its place follows). One of `step` (a recorder step's `id`, else its index), `press` (SOURCE seconds; the nearest press names the element, for a human recording) or `rect` (video fractions) names the referent. `side` is `auto` (the first of right, left, below, above whose box fits inside the frame through the layer's life) or a side by name; `gap` is design px (24); `mark` is `ring` or `underline`, a standing highlight on the referent for the layer's life; `leader` draws a hairline from the layer to the referent; `color` is their ink. `transform.x/y` stay the fallback for a pin that cannot resolve, and `vos validate` says why (an unknown step, a step that touched nothing, a scroll inside the layer's window). A layer that cannot name its referent is a caption: place it in the margin, never over the app.
|
|
394
454
|
|
|
395
|
-
**Captions anchored to a step.** A layer's TIME can name a step the way a pin names its place: `"anchor": { "step": "share", "media": "m1", "at": "end", "offset": 0.2 }` starts the layer where that step plays in the cut. The lowering maps the step's moment (its `tStart`, or `tEnd` with `at: "end"`, in `meta.steps` of the media `media` names; absent = the primary) through the segments, speed spans and freezes exactly as a source-anchored span is mapped, then adds `offset` in OUTPUT seconds, so a trim, a speed change or a re-record moves the caption with its step and nobody recomputes a `start`. `start` is the fallback when the step is missing, skipped or cut out, and `duration` is the layer's own. A text layer's `{elapsed}` fills with the anchored step's off-camera wait when the recorder kept one (the entry of `meta.skips` that names it, else `offCameraMs` on the step; never a step's own `ms`, which is its reading time), and a step that waited for nothing itself (the beat that lands after an await) reads the nearest earlier wait on its media, which validate notes, as a rounded duration: `"{elapsed} later"` renders `12 minutes later`. `vos validate` says when an anchor cannot resolve (and which fallback plays), when `{elapsed}` has no wait at or before its step to fill from,
|
|
455
|
+
**Captions anchored to a step.** A layer's TIME can name a step the way a pin names its place: `"anchor": { "step": "share", "media": "m1", "at": "end", "offset": 0.2 }` starts the layer where that step plays in the cut. The lowering maps the step's moment (its `tStart`, or `tEnd` with `at: "end"`, in `meta.steps` of the media `media` names; absent = the primary) through the segments, speed spans and freezes exactly as a source-anchored span is mapped, then adds `offset` in OUTPUT seconds, so a trim, a speed change or a re-record moves the caption with its step and nobody recomputes a `start`. `start` is the fallback when the step is missing, skipped or cut out, and an anchored layer may leave it out: one whose step then resolves nowhere plays nowhere, which `vos validate` refuses, naming the step. `duration` is the layer's own; on a cut that replays the step's seconds the layer starts in the pass where the step plays the longest (the first on a tie), so a caption about a step lands in the main play rather than a short cold open. A text layer's `{elapsed}` fills with the anchored step's off-camera wait when the recorder kept one (the entry of `meta.skips` that names it, else `offCameraMs` on the step; never a step's own `ms`, which is its reading time), and a step that waited for nothing itself (the beat that lands after an await) reads the nearest earlier wait on its media, which validate notes, as a rounded duration: `"{elapsed} later"` renders `12 minutes later`. `vos validate` says when an anchor cannot resolve (and which fallback plays), when `{elapsed}` has no wait at or before its step to fill from, when two captions are on screen at the same time, naming both, and when a layer anchored to one media's step starts over another media's footage (a caption landed after its gesture, then the footage after that step trimmed away). A document without an anchor lowers exactly as before. Not the re-record `anchor` on spans, which only `vos plan --reuse` reads.
|
|
396
456
|
|
|
397
457
|
**Image and video overlays.** Media kinds on the same lane: `{ "id": "m0", "kind": "image" | "video", "start": 2, "duration": 4, "key": "/logo.png", "width": 0.35, "radius": 12, "opacity": 1, "loop": false, "transform": { … } }`. `key` is a file inside the take directory (`"/logo.png"`) or a URL; `width` is a fraction of the frame width, height follows the media's aspect; corners in design px. Video time is clip-local and muted by design; soundtracks belong to `doc.audio`.
|
|
398
458
|
|
|
@@ -423,7 +483,7 @@ Not sure of a selector? `vos look <url>` (with the take's `--session` or `--stor
|
|
|
423
483
|
|
|
424
484
|
**Background.** `doc.frame.backgroundMedia` puts a looping video or a still behind the card: `{ "kind": "video", "key": "<url or /file>", "duration": 10, "dim": 0.2 }`. Time is output-anchored modulo `duration`, so trims and speed never retime the ambience; `dim` is a black scrim for legibility; `frame.parallax` (0..1, 0.6 reads well) counter-pans it with the zoom and `backgroundMedia.blur` softens it in design px. The hand-picked set is `GET https://vos.so/api/backdrops` (no auth): copy `key` from a row's `urls["1080p"]`, `duration` from the row, and `frame.background` from its `ground`. Drawn under the card, outside the zoom, fail-open to the CSS `frame.background`.
|
|
425
485
|
|
|
426
|
-
**Checking what rendered.** A still is only proof of what it shows. `vos frames --at-zooms` reads every zoom span on every media of the take through the cut to the output time it plays (a span the cut keeps none of is skipped and named), stills its apex, and stills the same time again with the zooms off (`--set zoom=[]`): a zoom whose frame matches its zoom-off twin did not render, and it is said, `zoom z3 at 4.50s did not render (its frame matches the zoom-off frame)`, with the twin kept beside it. Each zoom entry in `stills.json` and the `--json` done event carries `zoomId`, `media` when it is not the primary, `rendered` and `difference` (the mean absolute difference from the twin over a small grayscale, 0..1). `vos check-video <file>` checks the rendered file itself, decoding it in the browser the CLI already drives: duration, bytes, codecs, resolution and frame rate; full-frame scene changes (a large change that stands out from the frames around it, or a smaller one over at least a quarter of the frame
|
|
486
|
+
**Checking what rendered.** A still is only proof of what it shows. `vos frames --at-zooms` reads every zoom span on every media of the take through the cut to the output time it plays (a span the cut keeps none of is skipped and named), stills its apex, and stills the same time again with the zooms off (`--set zoom=[]`): a zoom whose frame matches its zoom-off twin did not render, and it is said, `zoom z3 at 4.50s did not render (its frame matches the zoom-off frame)`, with the twin kept beside it. Each zoom entry in `stills.json` and the `--json` done event carries `zoomId`, `media` when it is not the primary, `rendered` and `difference` (the mean absolute difference from the twin over a small grayscale, 0..1). `vos check-video <file>` checks the rendered file itself, decoding it in the browser the CLI already drives: duration, bytes, codecs, resolution and frame rate; full-frame scene changes (a large change that stands out from the frames around it, or a smaller one that stands alone, over at least a quarter of the frame or scattered across it, as when a camera jumps between two views of one dark picture or a light board of thin lines cuts to the empty canvas; a zoom, a pan, a cross-fade or an app's own change in one block, a panel or an embed appearing, is none of these), with the count per 5 s and a warning when any 5 s holds more than one; loudness per ITU-R BS.1770 (short-term every 3 s, integrated, the sample peak), with a warning below -23 LUFS (too quiet for a feed) or above -9, and a silent track named; and the first and last frames as PNGs, with a warning when either is near-uniform, an empty or faded open or close.
|
|
427
487
|
|
|
428
488
|
## Overrides on render and frames
|
|
429
489
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
localManifestRefs,
|
|
5
5
|
pullManifest,
|
|
6
6
|
uploadManifest
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-352AC3V6.js";
|
|
8
8
|
import "./chunk-J3KHIZCH.js";
|
|
9
9
|
export {
|
|
10
10
|
hostedLiteralWarnings,
|
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
pullManifest,
|
|
13
13
|
uploadManifest
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=assetManifest-
|
|
15
|
+
//# sourceMappingURL=assetManifest-27NKSS7N.js.map
|