@vosjs/cli 0.77.1 → 0.79.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.
Files changed (41) hide show
  1. package/README.md +163 -21
  2. package/dist/{assetManifest-MQ7HO7RZ.js → assetManifest-U25FJER6.js} +3 -3
  3. package/dist/{chunk-UY2N42P2.js → chunk-4ZUHEXUX.js} +13 -4
  4. package/dist/chunk-4ZUHEXUX.js.map +1 -0
  5. package/dist/{chunk-B4U4URE3.js → chunk-J3KHIZCH.js} +466 -412
  6. package/dist/chunk-J3KHIZCH.js.map +1 -0
  7. package/dist/{chunk-2KEQ3LKH.js → chunk-OWYOJB7K.js} +22 -2
  8. package/dist/chunk-OWYOJB7K.js.map +1 -0
  9. package/dist/{chunk-IJZ3C435.js → chunk-TWAP4BKR.js} +4372 -587
  10. package/dist/chunk-TWAP4BKR.js.map +1 -0
  11. package/dist/{chunk-KDIRW72G.js → chunk-Z6TLHRFQ.js} +7 -4
  12. package/dist/chunk-Z6TLHRFQ.js.map +1 -0
  13. package/dist/cli.js +9 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/index.d.ts +41 -29
  16. package/dist/index.js +5 -5
  17. package/dist/manifest-VTOIEUHJ.js +8 -0
  18. package/dist/{run-UNOXIGZ3.js → run-UKWKAGDK.js} +7 -5
  19. package/dist/{shotList-MAQVLZBY.js → shotList-EE3S5LVF.js} +52 -7
  20. package/dist/shotList-EE3S5LVF.js.map +1 -0
  21. package/package.json +4 -4
  22. package/schema/actions.schema.json +489 -12
  23. package/schema/doc.schema.json +63 -3
  24. package/skills/VERSION +1 -1
  25. package/skills/launch-kit/SKILL.md +6 -1
  26. package/skills/product-video/SKILL.md +7 -0
  27. package/skills/vos-authoring/SKILL.md +8 -1
  28. package/skills/vos-create/SKILL.md +5 -0
  29. package/skills/vos-cut/SKILL.md +8 -0
  30. package/skills/vos-footage/SKILL.md +4 -0
  31. package/skills/vos-migrate/SKILL.md +4 -0
  32. package/dist/chunk-2KEQ3LKH.js.map +0 -1
  33. package/dist/chunk-B4U4URE3.js.map +0 -1
  34. package/dist/chunk-IJZ3C435.js.map +0 -1
  35. package/dist/chunk-KDIRW72G.js.map +0 -1
  36. package/dist/chunk-UY2N42P2.js.map +0 -1
  37. package/dist/manifest-QLQKVSML.js +0 -8
  38. package/dist/shotList-MAQVLZBY.js.map +0 -1
  39. /package/dist/{assetManifest-MQ7HO7RZ.js.map → assetManifest-U25FJER6.js.map} +0 -0
  40. /package/dist/{manifest-QLQKVSML.js.map → manifest-VTOIEUHJ.js.map} +0 -0
  41. /package/dist/{run-UNOXIGZ3.js.map → run-UKWKAGDK.js.map} +0 -0
package/README.md CHANGED
@@ -72,13 +72,15 @@ A **take** is a directory: the recording of a scripted browser flow, its exact c
72
72
 
73
73
  ```bash
74
74
  vos create --actions actions.json out.webm --strict # one shot: record, auto-plan, render
75
+ vos look https://your-app.example # SEE the page first: look.png + every control with a stable selector
75
76
  vos record --actions actions.json --out take --dry-run # rehearse the script first: which selectors resolve, their rects, in seconds
76
77
  vos record --actions actions.json --out take --strict # drive the page, record it with an exact cursor track, plan the cut
77
78
  vos digest take # SEE the recording before cutting: moments, frames, crops (an agent's eyes)
78
79
  # … edit take/doc.json (zoom spans, trims, speed, overlays) by hand or by agent …
79
- vos frames take --at-zooms --at-moments # PNG stills: contact sheet, every zoom apex, every moment
80
+ vos frames take --at-zooms --at-moments # PNG stills: every zoom apex on every media (each checked against its zoom-off twin), every moment
80
81
  vos render take check.webm --range 4..8 --draft # spot-check an edit in seconds (half res, low bitrate)
81
82
  vos render take out.webm # the polished render
83
+ vos check-video take/out.webm # the file itself: facts, scene changes, loudness, first and last frames
82
84
  vos frames take --frame 2.5 --size 1280x800 # one frame at an exact size, to check a moment
83
85
  vos open take # hand the take to the studio; a human drags every span
84
86
  ```
@@ -107,42 +109,73 @@ vos ingest loom.mp4 --as take --out take # a screen demo with no
107
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
111
 
112
+ **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
+
114
+ ```bash
115
+ vos join intro-take settings-take outro.mp4 --out film --captions
116
+ ```
117
+
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).
119
+
120
+ **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
+
122
+ ```bash
123
+ vos score film --track fresh-focus --at m1:share --dry-run # a catalog slug, a step on media m1: print the plan and the envelope
124
+ vos score film --track ./theme.wav --at 12 --gain 0.4 # a local file (copied into film/music/), output seconds
125
+ ```
126
+
127
+ The track is decoded (mediabunny in Node for PCM; a compressed track in the headless browser) and measured as a short-term loudness curve, a 1 s window every 0.5 s: K-weighted LUFS (BS.1770) for mono and stereo audio, RMS dBFS otherwise, and the output says which. Its strongest swell is the largest sustained rise to a peak (at least 3 dB, holding within 3 dB of the peak for 1.5 s), measured once the track has established its level: a rise starts no earlier than 5 s after the track first comes within 12 dB of its median level, and from no lower than that, so a fade in from silence at the top is the track starting, never its swell; it lands where it ARRIVES, within 1 dB of the peak. `--at` is where it should land: a step (its id, `step:<index>` by its index, `<media>:<step>` for another media's), resolved through the cut exactly as an anchored caption is, or output seconds. The clip's `in` is set so the swell plays there, `start` is 0, `out` is `in` plus the film's length, with `--gain` (0.5), `--fade-out` (1.5 s) and, whenever the track is entered mid-song (`in` > 0), a 0.8 s fade-in (`--fade-in` sets it, 0 for none); when the track cannot both hold the swell there and cover the film, the clamp is said in words. Without `--at` the track starts at its first moment above its median level. A second run replaces the clip; `--dry-run` writes nothing and prints the swell, the offset and the curve as one line of blocks (`--json` carries every point). The clip carries what the track measures (`loudness`, below) and a loudness target, `normalize: -16` (`--normalize <lufs>` sets another, `--normalize off` none), with `--gain` a trim on the target: a catalog track, mastered at about -16 LUFS, plays at the default 0.5 as it did before it had a target, and a file of your own lands at the same level.
128
+
110
129
  **No browser here?** A cloud agent, a CI runner or a chat host records on vos.so instead: the same script goes up, the fleet records, plans and lands the take as a private vos with its doc and its digest, and the take comes home into `--out` with its media, the directory every other verb reads.
111
130
 
112
131
  ```bash
113
132
  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
114
133
  ```
115
134
 
116
- 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.
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 or a `download` is refused on `--hosted` in words: nobody can confirm a step that spends on the fleet, and a hosted take has no `outputs/` to keep a file in.
117
136
 
118
- | Verb | Flags |
119
- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` or `--session <name>` `--header name=value`... `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` `--hosted` (record on vos.so; with `--title` `--folder <id>`) |
121
- | `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 |
122
- | `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` |
123
- | `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) |
124
- | `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 …`; writes `stills.json` |
125
- | `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>` |
126
- | `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` |
127
- | `open` | `--studio <url>` (the studio; `https://vos.so` by default, `VOS_ORIGIN` moves it) `--print` (print the URL, do not launch a browser) |
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) |
150
+
151
+ **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.
128
152
 
129
153
  **The wall check.** Once the first navigation settles, and before a frame is captured, `record`, `create` and `--dry-run` ask whether the recorder landed where it was sent. A take that met a sign-in instead is refused with exit 4 and a sentence (`asked for /dashboard, landed on /login: no session for app.acme.com`): the asked URL answered 401 or 403, the recorder was sent to an identity provider or a sign-in path, or the page is a sign-in form (one password field, or a one-time-code field) rendered in place. A redirect somewhere else with no sign-in in sight, which is what a site that shows strangers a public page looks like, is refused under `--strict` and in a rehearsal, and said as a warning otherwise. The check runs before a re-record clears anything, so a session that expired since the last take never costs the footage it failed to replace. The way past a wall is a session: `--storage-state <file>`, minted from the test auth the project already has wherever that exists. `--allow-wall` records the page anyway (a video OF a sign-in page is a legitimate take), and the take's `meta.wall` and its digest then say so.
130
154
 
131
155
  **`vos session`: takeover mode, local.** A production app behind an emailed code, SSO, a passkey or a CAPTCHA has no form to script and no test auth to mint from, so a person signs in once and the recorder reuses that session:
132
156
 
133
157
  ```bash
134
- vos session open https://app.acme.com --name acme # a plain Chrome window opens; sign in, quit it (⌘Q on a Mac, closing the window is not quitting)
158
+ 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)
135
159
  vos session check acme --url https://app.acme.com/dashboard # does it still open the page? exit 0, or 4
136
160
  vos record --actions actions.json --out take --session acme --strict
137
161
  vos session list · vos session rm acme
138
162
  ```
139
163
 
140
- `open` runs the SYSTEM Chrome as an ordinary child process on a profile under `~/.config/vos/sessions/<name>`, with no automation switch and no debugging pipe, so a sign-in that refuses automated browsers (Google's) goes through, and it waits for the window to EXIT, because Chrome writes its cookie store when it quits and not before. It then prints what the session holds as counts and dates (`holds cookies for app.acme.com (7), accounts.google.com (12); newest expires in 29 days`), never a value, and `--json` does not either. `check` reads the session through the same Chrome and, with `--url`, asks the wall check whether the page still opens signed in: a session that expired says so in words with the command to sign in again, and exits 4. `record --session` records in that profile; it is one door and `--storage-state` is the other, never both.
164
+ `open` runs the SYSTEM Chrome as an ordinary child process on a profile under `~/.config/vos/sessions/<name>`, with no automation switch and no debugging pipe, so a sign-in that refuses automated browsers (Google's) goes through, and it waits for the person to finish, however they finish: the last window closed, Chrome quit (⌘Q), or Enter in the terminal. It never finishes on a sign-in it sees, because the person often is not done (a second factor or a single sign-on hop lands later, and the workspace picked and the demo data made in that window are what every take on the session finds). Chrome writes its cookie store when it quits, so a closed window or Enter quits that Chrome, by its process id (never by name: the everyday Chrome is the same app), and only then is the session read. The window is watched from outside the browser: a debugging endpoint, even one that only lists tabs, makes Chrome tell every page `navigator.webdriver === true`, which is what a sign-in that refuses automated browsers reads. So it counts the window's page renderers in the process table (they all exit when the last window closes, while a minimized window or one on another desktop keeps its own), and reads the profile's cookie store for counts only, saying `app.acme.com set 12 cookies (analytics not counted)` when the host's own cookies change, leaving out measurement cookies a stranger's visit sets too (`_ga*`, `_gid`, `ph_*`, `g_state`, `ajs_*`, `_fbp`, `_hj*`, `__stripe_*` and the like); Chrome commits that store about every thirty seconds, so the line lags a little, and it needs Node 22.5 or later, staying quiet before. A cookie count is never called a sign-in. Once the session is saved, `open` checks it the way `check --url` does: the URL opens headless on the saved profile AND for a stranger (a fresh, empty profile), through the wall check. Walled on the profile is `NOT signed in: https://app.acme.com/projects sends a stranger to /login …` with the command to try again (exit 4); open on the profile and walled for the stranger is `signed in: https://app.acme.com/projects opens as you`; open for both is `can't tell: … opens the same for a stranger`, a warning (exit 4 under `--strict`), because a public page proves nothing; and a sign-in page (`/login`, `/signin`, an identity provider) is not opened at all, since a stranger reaches it too. Name a page only a signed-in person sees. Only a session that passed is offered for recording. It also prints what the session holds as counts and dates (`holds cookies for app.acme.com (7), accounts.google.com (12); newest expires in 29 days`), never a value, and `--json` does not either. `check` reads the session through the same Chrome and, with `--url`, asks the wall check whether the page still opens signed in: a session that expired says so in words with the command to sign in again, and exits 4. `record --session` records in that profile; it is one door and `--storage-state` is the other, never both.
141
165
 
142
166
  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.
143
167
 
144
168
  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.
145
169
 
170
+ **`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:
171
+
172
+ ```bash
173
+ vos look https://app.acme.com/projects --session acme
174
+ vos look https://app.acme.com/projects --session acme --click '[data-testid="new-project"]' --out look
175
+ ```
176
+
177
+ It writes `<out>/look.png` (the viewport; `--out look` by default) and lists every control in view (buttons, links, tabs, menu items, checkboxes, switches, options, fields, editable regions, and anything with a test id) with its tag, role, accessible name, test id, link path and query, rect in viewport px, whether it is disabled, inside an open dialog or covered by something on top of it, and ONE proposed selector: its test id; else its `aria-label`, inside the open dialog it sits in; else a link's own `href`, query kept (two tiles to `?vos=a` and `?vos=b` are two links); else role and name (`role=button[name="Continue"]`, what `page.locator` reads), narrowed by its dialog or landmark when that is what makes it one; else a field's own `name` or `placeholder`, a written id, or its words inside its dialog or landmark. Never a position in the tree. Each proposal is counted with the recorder's own selector engine and marked `UNIQUE` (it reaches this element and no other) or `AMBIGUOUS` (it reaches others too, and the recorder would take the first). A control whose words hold an address, a key or a card number is listed with those words withheld, and the frame's exposures are said the way a rehearsal says them. Looking never spends or destroys: a `--click` whose target's words begin with a stem that does (delet, remov, revok, eras, discard, publish, deploy, purchas, subscri, unsubscri, pay, buy, generat, submit, send, transfer, upgrad, confirm, leav, destroy, so `Allow deleting` and `Revoke` are caught with `Delete`), or say move to trash, empty trash or cancel subscription, is refused in words before it is pressed (exit 2), and `--type` fills a field without pressing Enter. A look cannot know every spend: `Make a video` or `Create` on a primary button may start paid work and passes, because refusing every Make would refuse half of every app. The list is a floor under looking; in a recorded flow, a step's `guard: "spend"` in `actions.json` is the fence. A sign-in in front of the page is the wall check's verdict, exit 4; the sign-in page is still looked at, since it is what a `setup` step scripts past. `--wait <ms>` adds a pause before the look for a page that animates in; `--json` prints the whole look as one done event (`url`, `screenshot`, `controls`, `exposures`, `wall`).
178
+
146
179
  **`setup`: the steps that run before the camera rolls.** A sign-in form, a cookie banner, the "choose your editor" modal, an onboarding tour: things a take must get past and must not show. `actions.json` takes `setup: [...]` beside `steps`, with the verbs `goto`, `click`, `type`, `press`, `wait`. They run after the first navigation and before a frame is captured, as plain actions with no cursor, no frames, no pace and nothing in `meta.steps`; then the recorder opens `url` again and the take begins where the setup left it. A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`, read from the shell at run time and never logged or stored (the log names the field, never the value; a literal typed into a password field is refused by `validate`, because `actions.json` is committed and pushed with the take). A selector that never appears fails the take before anything is recorded (exit 2), because a take that begins at a half-finished sign-in is the wall by another name; rehearse the setup with `--dry-run` like everything else. This is rung 2 of the session ladder made scriptable: a local or self-hosted instance with a seeded user, no state file needed.
147
180
 
148
181
  ```json
@@ -158,7 +191,9 @@ A session never enters a take directory and never reaches vos.so: `vos push` ref
158
191
 
159
192
  **`--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.
160
193
 
161
- **What the frame shows.** Getting past a login puts the account's own data in the picture, and asking for a demo account does not hold that line: a person asked to sign in signs in as themselves. So the recorder looks. After the page opens and after every step it reads the text visible in the viewport and reports the KIND of thing it saw and where, never the string: an email address (one on `example.com` or a `.test`, `.example`, `.invalid` or `.localhost` domain is demo data and is not reported), something shaped like an API key or a JWT, a card number that passes the Luhn check, a masked card's visible tail. They land in `meta.exposures[]` (`step`, `kind`, `selector`, `rect`, `seen`), in the `record` and `create` done events, at the end of a rehearsal (before anything is recorded), as warnings in `vos validate <take>`, and in the digest's `take.exposures`. They warn; they do not fail a take, because a product may legitimately show addresses. The `selector` reaches that element and no other, so it can be pasted into a mask.
194
+ **The sound a page plays.** The recorder captures frames, so sound the product makes (a voice preview, a video playing in the page) would be lost. It watches every `<video>` and `<audio>` the page plays, in the page and its same-origin frames, `new Audio()` included: each one heard (not muted, volume above 0) lands in `meta.media[]` as `{ step, src, tag, duration, volume, intervals: [{ at, from, to }] }`, footage seconds `at` while it played media seconds `from`..`to`, split wherever an `await` took the camera away; one that played muted is listed with `muted: true` and no intervals. At the end of the take each heard source is fetched with the page's own cookies (a `blob:` or `data:` inside the page) into `page-sound/`, and the plan places one audio clip per interval at the output time the footage reaches it, `source: "page"`, at the element's volume. An auto speed span over a heard stretch is dropped, or the footage would race ahead of the sound. A re-plan, and `plan --reuse` after a re-record, replace only the `page` clips. A source that cannot be fetched is said, and its entry carries `fetchError` instead of `file`. A hosted take records `meta.media` and places nothing, since it has nowhere to keep the file. Web Audio a page synthesizes is not heard.
195
+
196
+ **What the frame shows.** Getting past a login puts the account's own data in the picture, and asking for a demo account does not hold that line: a person asked to sign in signs in as themselves. So the recorder looks. After the page opens and after every step it reads the text visible in the viewport and reports the KIND of thing it saw and where, never the string: an email address (one on `example.com` or a `.test`, `.example`, `.invalid` or `.localhost` domain is demo data and is not reported), something shaped like an API key or a JWT, a card number that passes the Luhn check, a masked card's visible tail. They land in `meta.exposures[]` (`step`, `kind`, `selector`, `rect`, `seen`), in the `record` and `create` done events, at the end of a rehearsal (before anything is recorded), as warnings in `vos validate <take>`, and in the digest's `take.exposures`. They warn; they do not fail a take, because a product may legitimately show addresses. The `selector` reaches that element and no other, so it can be pasted into a mask. Beside them the scan keeps two NOTICES, information that is never a warning, never in `exposures` and never a `--strict` failure: the signed-in account's own name, read conservatively from where apps say whose account it is (a "Signed in as …" or "Welcome back, …" line, an account menu's label, or the name-like words on the account menu's own button, the one holding the avatar; a name-like label on an avatar is reported as what MAY be the name, and a control's generic label, "Account menu" or "Profile settings", never is), with every place in the frame it showed; and a list of the account's recent items (a "Recents", "Recent" or "History" heading over three or more links) with the one-line `mask` that hides it. They land in `meta.notices[]` (`step`, `kind`: `account-name` with `places` and `sure`, or `personal-list` with `heading`, `selector`, `rect`, `items`; `seen`), the `record` and `create` done events, the end of a rehearsal, and a `noticed` section of `vos validate <take>`. The name itself is never printed, not even inside a selector: a line reads `the account's name (3 places, as the page opened): …`.
162
197
 
163
198
  **`mask`** in `actions.json` hides a selector BEFORE the first frame is captured and keeps it hidden across navigations and re-renders, so the real value is never in a frame, never in the recording, never pushed:
164
199
 
@@ -171,7 +206,7 @@ A session never enters a take directory and never reaches vos.so: `vos push` ref
171
206
 
172
207
  `as: "blur"` (the default) blurs the element; `as: "text"` swaps its words, which reads as a product where a blur reads as a redaction. Use `text` for IDENTIFIERS (an email, a name, an account id), never for product copy or numbers: the video stays true to the product. A form control is always blurred, since writing into an input would change what the app submits. A mask whose selector reached no element hid nothing, so it fails `--strict` and a rehearsal (exit 2) and is named; `meta.masks[]` records each mask's `hits`.
173
208
 
174
- **Rehearse before you record.** `vos record … --dry-run` runs every step against the real page, in order, because a later selector usually exists only after an earlier click. Nothing is captured and nothing is written: the pointer lands instead of travelling, every pause is cut to a beat, and the take directory beside it keeps its footage, its cut and its script exactly as they were (a real re-record moves `doc.json` aside; a rehearsal does not). Selector lookups keep their whole timeout, so a miss here is a miss in the take. It prints each step with the rect it resolved, in capture px, which are the rects a pin or `vos callout --step` reads, and exits 2 on any miss or a first load that never settled. Add `--dry-run` to the exact command you were about to run; `--storage-state` and `--browser-arg=` apply to it too, and the `Next:` line it prints carries them, so the command it hands you records what it rehearsed.
209
+ **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 (a first load that never went network-idle is only a note; a rehearsal captures no frames, so it says nothing of dead time). A step that spends is hovered and never pressed, so an `expect` or `await` after it is not waited on (`not waited: it follows #N (guarded, not pressed)`) and counts as resolved. Add `--dry-run` to the exact command you were about to run; `--storage-state` and `--browser-arg=` apply to it too, and the `Next:` line it prints carries them, so the command it hands you records what it rehearsed. The page's own console errors are said one line each, cut short, with a repeat counted (`[page error ×4]`) rather than printed again; `--verbose` prints every one whole, and the `--json` done event carries them all as `pageErrors` (`{ text, count }`).
175
210
 
176
211
  **The planners read the flow's own structure.** On a scripted take the step timeline is a planner input: a `wait` step between the first and the last doing step, or a hover's dwell, in which the digest's activity measured nothing moving is proposed at 2× (`speedParams.stillRate`; the opening and closing waits are the clip's establishing and resting beats and stay at 1×), the presses inside one step (a click and the dialog it opened, a drag's presses) are one zoom while their targets share a window, and a park before the first step (the cursor arriving on the logo) is never a dwell. Manual and rejected spans stay as they are.
177
212
 
@@ -215,6 +250,8 @@ take/
215
250
  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
216
251
  doc.json ProjectDoc, the editable cut
217
252
  doc.prev.json the previous cut, kept by a re-record for `vos plan --reuse`
253
+ outputs/ the files `download` steps kept, listed in meta.json's outputs
254
+ page-sound/ the sound the page played during the take, listed in meta.json's media
218
255
  vos.json hosted tracking (vos id + base version), written by push, fetch and pull
219
256
  stills/ `vos frames` output + stills.json
220
257
  digest/ `vos digest` output: digest.json, <id>.full.png, <id>.crop.png, sheet.png
@@ -229,7 +266,7 @@ claimed. Every verb resolves the name, and a push declares the container it
229
266
  finds in the bytes, so a file never reaches vos.so under the wrong type.
230
267
  `vos validate <take>` warns when an existing take holds one that does.
231
268
 
232
- A re-record replaces the footage, the cursor track, the frames and everything derived from them, moves `doc.json` to `doc.prev.json`, and keeps `actions.json` and `vos.json`.
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`.
233
270
 
234
271
  ## actions.json
235
272
 
@@ -253,10 +290,92 @@ A re-record replaces the footage, the cursor track, the frames and everything de
253
290
  }
254
291
  ```
255
292
 
256
- Eight verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150), `type` (`delayMs` 40 per key, paced by the clock; `focus: false` skips the focusing click; `replace: true` selects the field's value first so the typing replaces it; `at: "end" | "start"` puts the caret there before the first character, for a code editor line; `ms` the settle after, 150), `press` (one named key, `Escape`, `Enter`, `Tab`, `Backspace`, `Delete`, the arrows, `Home`, `End`, `PageUp`, `PageDown`, `Space`, recorded as a typing ping where the pointer is or on `selector` with no click, so an Escape closes what a click opened and an Enter submits what was typed without a second click ringing on the field; `ms` the settle, 150), `scroll` (`ms` the settle, 200), `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip; `ms` the travel, 700). The recorder's pace is the script's: a gesture is driven by the clock, so a page that answers each pointer sample slowly costs samples, never seconds, and the only pauses are the ones the script names (`wait`, a hover's dwell, a settle). `vos record` ends with a pace line, what the script asked, what the gestures added, what the page cost, and the steps that ran slow, and the `--json` done event carries it as `pace`. Every step takes an optional unique `id`; give steps ids so a span anchored to a step survives script edits and `vos plan --reuse` can follow it, and so a layer can name the element a step touched (`overlays[].pin.step`; the recorder keeps each hover, click, type and drag step's element rect on `meta.steps[].rect`). **The take carries its payload**: every step keeps the page it left (`meta.steps[].url`, origin and path), and a step with `read: "<selector>"` keeps that element's words once the step has settled (`meta.steps[].text`, at most 2 KB; on a click, what the click opened: the URL a share button minted, the code a dialog printed), so a layer quotes the take instead of a replay of the flow. Because the CLI issues every input itself, the cursor track is synthesized with exact coordinates, exact timing and fresh element rects, which is what powers element-aware auto-zoom and click effects downstream. The schema is [`schema/actions.schema.json`](./schema/actions.schema.json); `vos validate actions.json` checks a script without running anything.
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.
294
+
295
+ **Canvas gestures**: what a whiteboard, a design tool or a timeline is recorded with.
296
+
297
+ ```json
298
+ {
299
+ "settle": true,
300
+ "steps": [
301
+ { "do": "press", "keys": "r" },
302
+ { "do": "drag", "x": 200, "y": 160, "tx": 420, "ty": 300 },
303
+ { "do": "press", "keys": ["v", "Escape"] },
304
+ {
305
+ "do": "drag",
306
+ "selector": ".shape",
307
+ "tx": 600,
308
+ "ty": 300,
309
+ "modifiers": ["Alt"]
310
+ },
311
+ { "do": "drag", "path": "signature.json" },
312
+ {
313
+ "do": "drag",
314
+ "path": [
315
+ [300, 400],
316
+ [340, 420],
317
+ [380, 400]
318
+ ],
319
+ "ms": 500
320
+ },
321
+ { "do": "click", "selector": "text=Title", "count": 2 },
322
+ { "do": "click", "selector": ".layer", "button": "right" },
323
+ { "do": "click", "x": 640, "y": 420, "modifiers": ["Shift"] },
324
+ { "do": "click", "x": 300, "y": 260, "count": 2 },
325
+ { "do": "paste", "file": "logo.svg" },
326
+ { "do": "press", "keys": "Mod+Z" }
327
+ ]
328
+ }
329
+ ```
330
+
331
+ - **`press` `keys`** takes a chord (`"Shift+D"`, `"Meta+Shift+z"`, `"Mod+Z"`: `Mod` is Meta on macOS and Control elsewhere, wherever the browser runs), a plain key (`"r"`, a tool's shortcut) or a sequence (`["v", "Escape"]`, 150 ms apart). A key is a single character, a named key (`Escape`, `ArrowLeft`, `Space`), `F1` to `F24`, or a code (`KeyA`, `Digit1`); the modifiers are `Shift`, `Control`, `Alt`, `Meta`, `Mod`. Each key is a typing ping on the cursor track, when and where, never which key. The older `key` form still works.
332
+ - **`drag` `modifiers`** (`Alt`, `Shift`, `Meta`, `Control`, `Mod`) are held from the press to the release: Alt-drag to duplicate, Shift to constrain.
333
+ - **`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
+ - **`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
+ - **`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 pushed take carries its script but not a pasted file, so a script fetched from vos.so that pastes a file needs that file put beside it again, and says so.
337
+ - **`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
+ - **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
+
340
+ A hosted take (`--hosted`) refuses a `paste` (a fleet browser has no clipboard) and a `path` read from a file (the script travels alone; write the points inline).
341
+
342
+ Three steps that are not gestures, and a guard:
343
+
344
+ ```json
345
+ {
346
+ "steps": [
347
+ { "do": "expect", "selector": "text=Generate video", "enabled": true },
348
+ {
349
+ "id": "generate",
350
+ "do": "click",
351
+ "selector": "text=Generate video",
352
+ "guard": "spend"
353
+ },
354
+ {
355
+ "id": "ready",
356
+ "do": "await",
357
+ "until": { "text": "Your video is ready" },
358
+ "timeout": 900
359
+ },
360
+ { "do": "download", "selector": "text=Download", "as": "demo.mp4" }
361
+ ]
362
+ }
363
+ ```
364
+
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) or `gone` (no longer visible), 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.
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 `{ gone }`) 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`.
367
+
368
+ **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
+
370
+ - **`download`** keeps a file the page makes, in the take's `outputs/`: `selector` clicks a control and waits up to 30 s for the browser's download (the name the page sends in `Content-Disposition`); `src` names a `<video>`, `<audio>` or `<img>` (any selector the other steps take: `video >> nth=0`, `text=…`) and fetches its `currentSrc` with the page's own cookies. `as` names the file (a plain file name: no path separator, no `..`); absent, the page's name is used, made plain and unique. Each file is listed in `meta.outputs[]` (`{ step, id, name, bytes, contentType }`). A download that never arrives is a skip with its reason. A rehearsal fetches the file and keeps nothing.
371
+ - **`"guard": "spend"`** marks a `click`, `press` or `type` that costs something real: credits, a purchase, a message sent. It needs an `id`. `vos record --dry-run` moves the pointer to its target and never presses it (`#9 generate: guarded (spend), hovered, not pressed`); `vos record` presses it only when the command names it with `--confirm <id>` (repeatable), and otherwise refuses before capturing anything, naming the step and the flag (under `--json` also as `{ event: "refused", reason, step, id }`). The done event's `confirmed` and the step's `meta.steps[].confirmed` say which guarded steps were pressed.
372
+
373
+ `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.
257
374
 
258
375
  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.
259
376
 
377
+ 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`.
378
+
260
379
  ## doc.json
261
380
 
262
381
  `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.
@@ -273,6 +392,8 @@ Verified the flow in agent-browser already? `vos actions from-agent-browser step
273
392
 
274
393
  **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.
275
394
 
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, and when two captions are on screen at the same time, naming both. A document without an anchor lowers exactly as before. Not the re-record `anchor` on spans, which only `vos plan --reuse` reads.
396
+
276
397
  **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`.
277
398
 
278
399
  **HTML layers.** A UI component authored as DOM on the same lane, sharp at any export size because it is rasterized from source, not magnified from the footage: `{ "id": "h0", "kind": "html", "start": 2, "duration": 4, "html": "<div class=\"card\">Open Code</div>", "css": ".card{font-family:'Inter';background:#0b0b0d;border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.45)}", "box": { "width": 452, "height": 132 }, "transform": { … } }`. The markup is well-formed XML (`<br />`, `&amp;`, quoted attributes; `vos validate` refuses the rest in words, because a malformed layer decodes to nothing and says nothing). `box` is the design box in 1080p px; `width` defaults to the design size. The CSS is the chrome (shadow, radius, border; `bleed` is derived from the shadows, or stated), a `font-family` it names resolves against the hosted catalog at every `font-weight` it names (a face outside the catalog rides `fonts: [{ family, url }]`) (a face's `url` is an https URL on `https://assets.vos.so/`, a hosted file, or a `.woff2` beside the document, which `vos push` uploads like any layer file and a local render serves; a face on any other host is refused, like an image), and an image inside is inlined for you when its URL is on `https://assets.vos.so/` (`vos asset push` puts it there; any other host paints blank, so inline it as a `data:` URI). A still layer's `animation` runs on the wall clock rather than the timeline, so animate the layer with `anim` and `motion`; a LIVE layer (`"live": true`) is re-rasterized per frame at clip-local time: its `@keyframes` are scrubbed to `t` (paused, delayed by `-t`; fold any `animation-delay` into the keyframes) and `{{t}}` / `{{data.<name>}}` placeholders in the markup and CSS fill from the clock and the clip's `data`, so a counter, a progress bar or a typed line come out on the timeline's clock in the preview and in every export chunk alike. It is never re-hosted and never an upload.
@@ -281,8 +402,29 @@ Verified the flow in agent-browser already? `vos actions from-agent-browser step
281
402
 
282
403
  **Audio.** `doc.audio` clips (music, effects, `key` a take-directory file or a URL) are mixed with gain, fade and loop envelopes and muxed into every full render: Opus for webm, AAC (Opus fallback) for mp4. Audio forces single-flight (`--parallel` is ignored with a note); a `--range` render keeps its audio.
283
404
 
405
+ **Loudness as a target.** `gain` is 0..1, so it cannot lift a quiet file; a clip may say how loud it should play instead: `normalize`, an integrated loudness in LUFS from -30 to -6 (`-16` sits a score under speech without burying it). The lowering plays the clip at `10^((normalize - lufs) / 20) × gain`, which may exceed 1, from what the file measures, a FACT stored beside the clip and written by tooling, never by hand:
406
+
407
+ ```json
408
+ {
409
+ "id": "score",
410
+ "key": "music/theme.wav",
411
+ "gain": 1,
412
+ "normalize": -16,
413
+ "loudness": {
414
+ "lufs": -24.3,
415
+ "peakDb": -9.1,
416
+ "at": "2026-10-22T10:04:00.000Z",
417
+ "src": "music/theme.wav"
418
+ }
419
+ }
420
+ ```
421
+
422
+ `vos audio measure <take>` decodes every clip's file and writes its `loudness`: BS.1770 integrated loudness (K-weighted, gated) over the whole file and its sample peak, by the meter `vos check-video` reads a render with. `vos score` writes it for the score and `vos join` for the sound it adds. The gain is CAPPED, never limited: no target puts the file's sample peak above -1 dBFS, none lifts more than +24 dB, and `vos validate` says when a cap holds a clip under its target. A target applies only while `loudness.src` is the clip's own `key`, so a clip pointed at another file plays at its gain alone until it is measured again (`vos validate` warns: measure it: `vos audio measure <take>`); a document with no target lowers byte for byte as before. `vos push` and `vos pull --media` move `loudness.src` with the key when a file is uploaded or brought home.
423
+
284
424
  **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`.
285
425
 
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 that stands alone, as when a camera jumps between two views of one dark picture; a zoom, a pan or a cross-fade is neither), 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
+
286
428
  ## Overrides on render and frames
287
429
 
288
430
  Check any presentation from the command line without hand-editing `doc.json`. The file on disk is untouched; the patched document is lint-gated, so a bad override fails exactly like a bad `doc.json`.
@@ -307,7 +449,7 @@ vos brand https://your.app --out BRAND.md # the brand kit,
307
449
 
308
450
  ## The vos.so loop
309
451
 
310
- Everything that runs on your machine is open source; [vos.so](https://vos.so) is the hosted platform: a studio for the human half of an edit, version history with a typed changelog, a shelf of projects and recipes. All vos.so traffic lives behind one client: one origin (`VOS_ORIGIN`, default `https://vos.so`; `--origin` per call), one credential ladder (`--key`, then `VOS_API_KEY`, then `~/.config/vos/credentials`, written by `vos login`; a `vos_rg_` remix grant is just a key), one tracking file (`vos.json` beside the artifact). Credentials are never printed. Keys can never publish: pushes are private, and humans publish on vos.so.
452
+ Everything that runs on your machine is open source; [vos.so](https://vos.so) is the hosted platform: a studio for the human half of an edit, version history with a typed changelog, a shelf of projects and recipes. All vos.so traffic lives behind one client: one origin (`VOS_ORIGIN`, default `https://vos.so`; `--origin` per call), one credential ladder (`--key`, then `VOS_API_KEY`, then `~/.config/vos/credentials`, written by `vos login` with the origin it was minted for; a `vos_rg_` remix grant is just a key), one tracking file (`vos.json` beside the artifact). Credentials are never printed, and the stored one is sent only to the origin it was minted for (a file written before the origin line is `https://vos.so`'s): a command aimed at another origin sends no key and says so, so pass `--key` or `VOS_API_KEY` for it, or `vos login --origin <it>`. Keys can never publish: pushes are private, and humans publish on vos.so.
311
453
 
312
454
  ```bash
313
455
  vos login [--key <k>] [--label <name>] [--no-browser] # browser sign-in: a code and a vos.so/cli/auth URL, a human approves, the key stores itself
@@ -335,7 +477,7 @@ vos assets ls [--kind <kind>] [--q <text>] [--sort size] | usage | why <id> #
335
477
  vos recipe push <FILE.md> --folder <slug> | --asset <id> # the one recipe write: create, or replace in place
336
478
  ```
337
479
 
338
- `push` is polymorphic by a deterministic sniff, never a flag: a take directory (a `doc.json` carrying `source`) pushes recording and document through the take pipeline; a `config.json` (or a directory holding one) pushes the program, and a `doc.json` beside it that carries `program` (a program document: overlays, objects, audio, speed, tween edits, its own length; `program.config` omitted on disk) rides along, lint-gated. `fetch` and `pull` write a program document back the same way. `pull` takes `--since <versionId>` (walk the changelog from that base instead of the tracked one) and `--check` (print what changed and stop: nothing on disk moves) on both paths. A program push LANDS where `vos.json` says: a directory that tracks a vos iterates it, so the fetch, edit, check, push, pull loop needs no flags, and making something separate from the same config is the explicit `--remix-of` door. Program pushes take `--vos`, `--title`, `--slug`, `--desc`, `--tags`, `--folder`, `--remix-of`, `--base`, `--label`, `--note`, `--override <id>` (repeatable) and `--claimable`; take pushes take `--title`, `--label`, `--note`, `--folder`, `--override`, `--yes` and `--claimable`. Both take `--wait`. A new vos pushed without `--folder` lands unfiled, at the root of the shelf, and the done line says so. `--folder` takes a project on your own shelf (its slug or id) or anyone's project by its address, `<handle>/<slug>` as its page on vos.so names it: the server answers where the push lands, CONTINUING that project when the key is its owner's and otherwise STARTING the person's own from it (a private project made once, named after it, with the recipes they may read copied in, never its videos). The push says which.
480
+ `push` is polymorphic by a deterministic sniff, never a flag: a take directory (a `doc.json` carrying `source`) pushes recording and document through the take pipeline; a `config.json` (or a directory holding one) pushes the program, and a `doc.json` beside it that carries `program` (a program document: overlays, objects, audio, speed, tween edits, its own length; `program.config` omitted on disk) rides along, lint-gated. `fetch` and `pull` write a program document back the same way. `pull` takes `--since <versionId>` (walk the changelog from that base instead of the tracked one) and `--check` (print what changed and stop: nothing on disk moves) on both paths. A program push LANDS where `vos.json` says: a directory that tracks a vos iterates it, so the fetch, edit, check, push, pull loop needs no flags, and making something separate from the same config is the explicit `--remix-of` door. Program pushes take `--vos`, `--title`, `--slug`, `--desc`, `--tags`, `--folder`, `--remix-of`, `--base`, `--label`, `--note`, `--override <id>` (repeatable) and `--claimable`; take pushes take `--title`, `--label`, `--note`, `--folder`, `--override`, `--yes`, `--bake` and `--claimable`. Both take `--wait`. A take push first asks the platform which `@vosjs/studio-core` it renders with (`GET /api/engine`, public, `{ studioCore, core }`) and refuses (exit 2), before anything uploads, a take that uses a field newer than it: the program a push compiles plays right, but the platform's studio lowers the stored document again with its own release, so it would show an anchored caption at its fallback start or play a clip with a loudness target at its gain, and every version saved from the studio would keep it that way. `--bake` sends a copy any renderer plays: every such field the take uses, said in what an older release reads (an anchored caption at the start it plays at in this cut with its `{elapsed}` filled, a loudness target as its gain, up to 1), whatever the platform answered, behind, current or nothing at all, and it never touches `doc.json`. The table of fields and their releases is `DOC_FEATURES` in `@vosjs/studio-core`. A platform that cannot say (no such endpoint, a refusal, no answer in 2 s) is warned of and pushed to. A new vos pushed without `--folder` lands unfiled, at the root of the shelf, and the done line says so. `--folder` takes a project on your own shelf (its slug or id) or anyone's project by its address, `<handle>/<slug>` as its page on vos.so names it: the server answers where the push lands, CONTINUING that project when the key is its owner's and otherwise STARTING the person's own from it (a private project made once, named after it, with the recipes they may read copied in, never its videos). The push says which.
339
481
 
340
482
  **A program's files are declared, never typed into a function.** `config.json` names each file it uses in `assets` (`"assets": { "logo": { "ref": "./logo.png", "kind": "image" }, "shots": { "ref": ["./shots/1.png", "./shots/2.png"] } }`) and reads `ctx.assets.logo`, or `"$assets.logo"` in an element, object or font. On disk a `ref` is a path in the config's own directory or below, so `vos render`, `vos still` and `vos preview` serve the file to the page from disk. A path that climbs out of that directory (`../`, an absolute path, a link that points away) is not served and not uploaded, and a config loaded from a URL reads no local file: a program's code runs in a page, and what the page is served the program can send anywhere. `vos push` uploads each local file (content-addressed, so a re-push sends nothing: pictures, SVG, video, sound, `.glb` models, fonts and HDR environments) and what it stores names the hosted file as `asset:<id>`, the one spelling the platform resolves for every render; the config on disk keeps its paths. A file an element names as `src` or a font names as `url` is lifted into the manifest on the way (the element then names `"$assets.<name>"`), and a path in `data` that is a file beside the config is uploaded and rewritten to the hosted file's path, so every place a program can name a file crosses. A file typed inside a function cannot be rewritten: `vos push` says so, with the manifest line to write. `vos fetch --media` brings a program's hosted files home under `assets/` and re-points the manifest at them (`assets/.hosted.json` remembers which hosted file each one is, so a later fetch replaces one that changed); without `--media` the manifest keeps `asset:<id>` and a local render reads each file with your key. `vos check` says when a `"$assets.<name>"` names nothing, when a declared path is not a file, and when a function names a hosted file in code instead of the manifest: a file named only in code does not load on a render of a private program and does not follow a remix. A `--claimable` push carries them too, through the claim's own upload session (below).
341
483
 
@@ -365,7 +507,7 @@ vos push /tmp/take --label "$TAG launch" --yes --json # VOS_API_KEY as a r
365
507
  - **Output.** Logs on stderr, results on stdout. `--json` turns every verb into NDJSON events ending with `{"event":"done",…}`.
366
508
  - **`vos <verb> --help`** prints that verb's usage lines and exits 0 (it used to be a usage error).
367
509
  - **Exit codes.** 0 ok, 1 error, 2 usage (including `--strict` failures), 3 no browser found, 4 the recorder met a wall (a sign-in, or under `--strict` a redirect away from the asked page) and nothing was recorded or cleared. `--json` carries the verdict as a `wall` event (`kind`, `level`, `asked`, `landed`, `refused`).
368
- - **`--strict`** on `record` and `create` (agents: always): a skipped selector, a page that never reaches network idle, or a take that hit `--max-duration` exits 2 and lists `skipped[]` (and `capped`) in the done event. The default is lenient (exit 0, `skipped[]` still reported) for exploratory runs.
510
+ - **`--strict`** on `record` and `create` (agents: always): a skipped step (a selector that never appeared, an `expect` that never held, a download that never came), a click on a disabled control, an `await` that timed out, or a take that hit `--max-duration` exits 2 and lists `skipped[]`, `disabled[]` (and `capped`) in the done event. A first load that never settles is a note, never a strict failure: `ready.capped` in the done event, and `navTimeout: true` when `ready: "networkidle"` was asked for and never came (many real apps hold a socket open and never go network-idle, which is why the default waits for the DOM instead). The default is lenient (exit 0, `skipped[]` still reported) for exploratory runs.
369
511
  - **`--max-duration <s>`** on `record` and `create` defaults to the hosted recording cap, read live from `GET /api/limits` (2 s, fail-open to 30 min when the origin is unreachable): the capture stops there and the done event says so. Cut the flow rather than raising the cap; the platform refuses a longer take.
370
512
  - **`create`** is the one-shot verb: record, auto-plan and render in one command and one browser session. The take directory still lands on disk, so the full loop (frames, edit `doc.json`, re-render) stays open afterwards.
371
513
  - **`vos validate <thing>`** takes an `actions.json`, a take directory, a program directory (`config.json`, plus its program document when present), or a `kit.json`; exit 1 on any problem.
@@ -4,12 +4,12 @@ import {
4
4
  localManifestRefs,
5
5
  pullManifest,
6
6
  uploadManifest
7
- } from "./chunk-UY2N42P2.js";
8
- import "./chunk-B4U4URE3.js";
7
+ } from "./chunk-4ZUHEXUX.js";
8
+ import "./chunk-J3KHIZCH.js";
9
9
  export {
10
10
  hostedLiteralWarnings,
11
11
  localManifestRefs,
12
12
  pullManifest,
13
13
  uploadManifest
14
14
  };
15
- //# sourceMappingURL=assetManifest-MQ7HO7RZ.js.map
15
+ //# sourceMappingURL=assetManifest-U25FJER6.js.map
@@ -3,6 +3,7 @@ import {
3
3
  MEDIA_HEAD_BYTES,
4
4
  TAKE_MEDIA_EXTENSIONS,
5
5
  apiJson,
6
+ bearerFor,
6
7
  extensionFor,
7
8
  hostedAssetId,
8
9
  localFile,
@@ -13,7 +14,7 @@ import {
13
14
  writeFileSafe,
14
15
  writeFileSafeSync,
15
16
  writeSyncState
16
- } from "./chunk-B4U4URE3.js";
17
+ } from "./chunk-J3KHIZCH.js";
17
18
 
18
19
  // src/plugin/assetManifest.ts
19
20
  import { existsSync as existsSync3, readFileSync, realpathSync, rmSync } from "fs";
@@ -113,7 +114,11 @@ async function prepareReRecord(dir) {
113
114
  p.framesIndex,
114
115
  p.framesDir,
115
116
  join(dir, "stills"),
116
- join(dir, "digest")
117
+ join(dir, "digest"),
118
+ // What the last run's download steps kept: this run downloads again.
119
+ join(dir, "outputs"),
120
+ // The sound the last run's page played: this run hears it again.
121
+ join(dir, "page-sound")
117
122
  ]) {
118
123
  await rm(stale, { recursive: true, force: true });
119
124
  }
@@ -408,6 +413,7 @@ function docAudioRefs(doc, keep = isTakeRelativeKey) {
408
413
  where: `audio ${clip.id}`,
409
414
  key: clip.key,
410
415
  set: (next) => {
416
+ if (clip.loudness?.src === clip.key) clip.loudness.src = next;
411
417
  clip.key = next;
412
418
  }
413
419
  });
@@ -485,7 +491,7 @@ function downloadPlan(ctx, url) {
485
491
  async function downloadMedia(ctx, url, target) {
486
492
  const plan = downloadPlan(ctx, url);
487
493
  const res = await fetch(plan.url, {
488
- headers: plan.bearer && ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
494
+ headers: plan.bearer ? bearerFor(ctx.key, plan.url) : {}
489
495
  });
490
496
  if (!res.ok || !res.body) {
491
497
  throw new Error(
@@ -729,8 +735,11 @@ export {
729
735
  isTakeRelativeKey,
730
736
  takeRelativeFile,
731
737
  sourceSidecarRefs,
738
+ docObjectRefs,
739
+ docOverlayRefs,
732
740
  docLayerRefs,
733
741
  docMediaRefs,
742
+ docAudioRefs,
734
743
  uploadDocRefs,
735
744
  readMediaHead,
736
745
  pullMedia,
@@ -741,4 +750,4 @@ export {
741
750
  pullManifest,
742
751
  hostedLiteralWarnings
743
752
  };
744
- //# sourceMappingURL=chunk-UY2N42P2.js.map
753
+ //# sourceMappingURL=chunk-4ZUHEXUX.js.map