@vosjs/cli 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,101 +1,123 @@
1
1
  # @vosjs/cli
2
2
 
3
+ > The `vos` binary. Render vos configs to video and stills, record the real product from a scripted browser flow, plan zooms and pacing from the cursor track, cut as data, deliver a release's media per channel spec, and sync with vos.so. One package, every verb, MIT.
4
+
3
5
  [![npm](https://img.shields.io/npm/v/@vosjs/cli)](https://www.npmjs.com/package/@vosjs/cli)
4
- [![license](https://img.shields.io/npm/l/@vosjs/cli)](../../LICENSE)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/vosjs/vos/blob/main/LICENSE)
5
7
 
6
- The `vos` binary. Record a scripted browser flow into a **take**, plan zoom and cursor effects with the same planner the vosso studio uses, cut as data in `doc.json`, render deterministic video and stills headlessly, deliver a release's media per destination spec, and sync your work with [vos.so](https://vos.so). Designed to be driven by AI agents (Claude Code, Codex) as well as humans. One package, every verb, MIT.
8
+ Part of [vos](https://github.com/vosjs/vos), the open programmatic video engine behind [vos.so](https://vos.so). Designed to be driven by coding agents (Claude Code, Codex, Cursor) as well as by hand: logs on stderr, results on stdout, `--json` everywhere, and every editing decision in a JSON file.
7
9
 
8
10
  ```bash
9
- npm i -D @vosjs/cli
10
- npx vos render animation.json out.webm
11
+ npm i -g @vosjs/cli # or: npm i -D @vosjs/cli && npx vos …
12
+ vos render animation.json out.webm
11
13
  ```
12
14
 
13
- Deterministic: the preview is the render, and every edit is a data patch to `doc.json`, never a re-record. Export is free at every resolution up to 4K, no watermark. Everything that runs on your machine is open source; vos.so, the hosted platform, is the product. Workflow skills for coding agents: `npx skills add vosjs/skills` (`launch-kit` ships the media with a release; `product-video` and `vos-cut` make and cut one video).
15
+ Deterministic: the preview is the render, and every edit is a data patch to `doc.json`, never a re-record. Rendering is local and free at every resolution up to 4K, no watermark. The workflow skills for agents install with `npx skills add vosjs/skills` (`product-video` records and cuts one video, `vos-cut` cuts an existing recording, `launch-kit` ships the media with a release).
16
+
17
+ Until 0.9 the take pipeline and the vos.so verbs shipped separately as `@vosso/vos-plugin`, and before that as `@vosso/cli` and `@vosso/voila-cli`; those names are deprecated on npm and forward here.
18
+
19
+ ## Contents
20
+
21
+ - [Requirements](#requirements)
22
+ - [Engine verbs](#engine-verbs)
23
+ - [The take pipeline](#the-take-pipeline)
24
+ - [The take directory](#the-take-directory)
25
+ - [actions.json](#actionsjson)
26
+ - [doc.json](#docjson)
27
+ - [Overrides on render and frames](#overrides-on-render-and-frames)
28
+ - [Delivering a release](#delivering-a-release)
29
+ - [The vos.so loop](#the-vosso-loop)
30
+ - [CI](#ci)
31
+ - [For scripts and agents](#for-scripts-and-agents)
32
+ - [Programmatic use](#programmatic-use)
33
+
34
+ ## Requirements
14
35
 
15
- (Until 0.9 the take pipeline and the vos.so verbs shipped separately as `@vosso/vos-plugin`, and before that as `@vosso/cli` and `@vosso/voila-cli`; those names are deprecated on npm and forward here.)
36
+ Node 18 or newer and a Chromium-family browser. A system Chrome is used when present; otherwise `npx playwright install chromium` once, or set `VOS_BROWSER_PATH`. mp4 output needs Chrome (Chromium ships no AVC encoder). Render pages load `three`, `gsap` and mediabunny from a CDN, so rendering needs network access.
16
37
 
17
38
  ## Engine verbs
18
39
 
19
40
  ```bash
20
- vos render <config.json|url|take> [out] # config → video; a take directory renders through the take pipeline
21
- vos still <config.json|url> [out] # config → single frame (WebP)
22
- vos info <config.json|url> # inspect a config
23
- vos check <config.json|url> # migrate → schema → syntax → compile → determinism/dialect lints, all local
24
- vos preview <config.json|url> # serve a local playback page
25
- vos versions # installed @vosjs/* versions
41
+ vos render <config.json|url|take> [out] # config → video (a take directory renders through the take pipeline)
42
+ vos still <config.json|url> [out.webp] # config → one frame, WebP
43
+ vos info <config.json|url> # inspect a config
44
+ vos check <config.json|url> # migrate → schema → syntax → compile → determinism and dialect lints, all local
45
+ vos preview <config.json|url> [--port N] # serve a local playback page
46
+ vos versions # installed @vosjs/* versions
26
47
  ```
27
48
 
28
- `vos render` accepts `--width` / `--height` / `--fps` / `--duration` / `--format webm|mp4`; `vos still` accepts `--time` / `--width` / `--height`. Configs can be local files or URLs, and API `{ "config": … }` envelopes are unwrapped automatically. Old config versions are migrated before rendering. `vos check` runs the full local validation pipeline — the same compiler a hosted push runs server-side, so a clean check is a config that will compile anywhere. `vos render` is polymorphic by a deterministic sniff, never a flag: a take directory is recognized by its `doc.json`; anything else renders as an engine config.
49
+ | Verb | Flags and defaults |
50
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `render` | `--width 1920` `--height 1080` `--fps 30` `--duration <config.duration>` `--format webm\|mp4` (webm); `out` defaults to `<name>.<format>` |
52
+ | `still` | `--time 0` `--width 1280` `--height 720`; the output is always WebP (a `.png` name is refused) |
53
+ | `check` | Exits 1 on any error. Runs the same compiler a hosted push runs, so a clean check is a config that compiles anywhere |
29
54
 
30
- Rendering runs the same deterministic pipeline everywhere: the config is compiled with `@vosjs/core`, wrapped in the engine's capture template, and encoded frame-by-frame (WebCodecs) in headless Chromium. Same input, same video locally, in CI, or on a server.
55
+ Configs can be local files or URLs; a platform `{ "config": … }` envelope is unwrapped. Rendering compiles the config with `@vosjs/core`, wraps it in the engine's capture template, and encodes frame by frame (WebCodecs) in headless Chromium. Same input, same video: locally, in CI, or on a server. Every engine verb takes a directory too: a take (its `doc.json` is a recording document) renders through the take pipeline, and a program directory (`config.json`, composed with the program document beside it when there is one) renders as what the studio plays. A deterministic sniff of the document, never a flag.
31
56
 
32
57
  ## The take pipeline
33
58
 
59
+ A **take** is a directory: the recording of a scripted browser flow, its exact cursor track, and `doc.json`, the editable cut.
60
+
34
61
  ```bash
35
- vos create --actions actions.json out.webm --strict # one-shot: record + auto-plan + render
36
- vos record --actions actions.json --out take --strict # drive the page, synthesize the cursor track, encode + plan
37
- # the product ships a new version: re-record the SAME script, keep the cut
38
- vos record --actions actions.json --out take --strict # footage replaced; the cut survives as doc.prev.json
39
- vos plan take --reuse # re-time the previous cut onto the new recording (flags what could not follow)
40
- vos digest take # SEE the recording before cutting: moments (clicks, typing, scrolls, idle, scenes) + footage frames + crops
41
- # edit take/doc.json (zoom spans, trims, speed, styling) — or let your agent …
42
- vos frames take --at-zooms --at-moments # PNG stills: contact sheet, every zoom apex, every moment (the agent's eyes)
43
- vos render take check.webm --range 4..8 --draft # spot-check a doc edit in seconds (half res, low bitrate)
44
- vos render take out.webm # deterministic polished render
45
- vos frames take --frame 2.5 --size 1280x800 # exact-size still — posters, OG cards, store screenshots
46
- vos deliver take --to cws,producthunt,og --release "v2.1" # the release's assets per channel spec + verified kit.json
47
- vos deliver take --to cws,og,linkedin --poster poster.json # + the CARD half: covers composed by your poster program, this release's shot baked in
48
- vos validate take/kit/kit.json # re-measure every kit asset from its bytes against the channel specs
49
- vos brand https://your.app --out BRAND.md # the brand kit, witnessed: /design.md, /llms.txt, then the page (palette, faces, marks, the avoid list)
50
- vos actions from-agent-browser steps.jsonl # the walk an agent made in agent-browser → actions.json (no second script; what cannot follow is named)
51
- vos open take # hand the take to the studio — a human can drag every zoom span
52
- vos push take --yes --note "first pass" # host it: private vos + version history (recording uploads once)
53
- vos pull take # take the human's studio edits back: typed changelog + fresh doc.json
54
- vos fetch <vosId> --media # a take you did not record: doc.json + the recording, ready for digest/frames/render
62
+ vos create --actions actions.json out.webm --strict # one shot: record, auto-plan, render
63
+ vos record --actions actions.json --out take --strict # drive the page, record it with an exact cursor track, plan the cut
64
+ vos digest take # SEE the recording before cutting: moments, frames, crops (an agent's eyes)
65
+ # edit take/doc.json (zoom spans, trims, speed, overlays) by hand or by agent …
66
+ vos frames take --at-zooms --at-moments # PNG stills: contact sheet, every zoom apex, every moment
67
+ vos render take check.webm --range 4..8 --draft # spot-check an edit in seconds (half res, low bitrate)
68
+ vos render take out.webm # the polished render
69
+ vos frames take --frame 2.5 --size 1280x800 # an exact-size still: posters, OG cards, store screenshots
70
+ vos open take # hand the take to the studio; a human drags every span
55
71
  ```
56
72
 
57
- **Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video: it writes `digest/digest.json` (one MOMENT per thing the cursor track says mattered: click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail and frame-diff scene changes, each with source + output extents, a normalized `focus`/`rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids) plus one FOOTAGE frame and a crop around the target per moment and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide; `--full`/`--crop` set the image long edges (the token budget; the done event estimates it). `--transcript` merges Whisper-shaped segments as `said`; `--style` reports a reference doc's style fields. **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`/`export` onto a new take (never its spans, overlays or audio) and re-plans the auto spans under them, so the folder's `CUT.md` says only what a number cannot. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
58
-
59
- ## Platform verbs — the vos.so loop
60
-
61
- All vos.so traffic lives behind one client: one origin (`VOS_ORIGIN`, default `https://vos.so`), one credential ladder (`--key` → `VOS_API_KEY` → `~/.config/vos/credentials`, written by `vos login`; a `vos_rg_` remix grant is just a key), one tracking file (`vos.json` beside the artifact; the legacy `push.json`/`meta.json` still read). Credentials are never printed. Keys can never publish — pushes are **private**; humans publish on vos.so.
73
+ When the product ships again, re-record the same script and keep the cut:
62
74
 
63
75
  ```bash
64
- vos login # browser sign-in: prints a code + vos.so/cli/auth URL, a human
65
- # approves, the key stores itself (works headless; --key skips it)
66
- vos fetch bright-loop # a program: writes config.json + vos.json (public needs no auth)
67
- vos check bright-loop/config.json # full local validation
68
- vos push bright-loop/config.json # create a PRIVATE vos (lineage from vos.json / --remix-of)
69
- vos push bright-loop/config.json --vos <id> # add a version against your tracked base
70
- vos push bright-loop/config.json --claimable # NO credential: a 72h claim link instead (programs only)
71
- vos pull bright-loop # what changed on vos.so; syncs config.json (backup kept)
72
- vos duplicate <vosId> # a private sibling of your OWN vos (someone else's is remixed: fetch + push --remix-of)
76
+ vos record --actions actions.json --out take --strict # footage replaced; the previous cut survives as doc.prev.json
77
+ vos plan take --reuse # re-time that cut onto the new recording; what could not follow is named
73
78
  ```
74
79
 
75
- `push` is polymorphic by a deterministic sniff, never a flag: a take **directory** (a `doc.json` carrying `source`) pushes recording + doc 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`/`pull` write a program document back the same way (config.json from the document's own config, doc.json without it). Both paths share the same base tracking and the same two 409 shapes: `stale_base` replays the platform's typed changelog (run `vos pull`, re-apply, push again); `protected_conflict` lists human-edited nodes — keep their values, or re-push with `--override <id>` ONLY when the user asked for that exact change. The first push of a take asks before uploading (`--yes` for headless): agents never upload unprompted. Every push should carry `--label` (what changed, one line) and `--note` (why: the user's ask) — the version history reads as a conversation, and an unlabelled push is a turn the human cannot read.
80
+ | Verb | Flags |
81
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--max-duration <s>` `--background <slug\|url\|none>` |
83
+ | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
84
+ | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--background` |
85
+ | `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) |
86
+ | `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` |
87
+ | `render` | `--width` `--height` `--fps` `--format webm\|mp4` `--parallel N` (1..16 sessions) `--range a..b` (output seconds; keeps its audio) `--draft` `--frame <kind>` `--background` `--set …`; `out` defaults to `<take>/out.<format>` |
88
+ | `open` | `--studio http://localhost:6060` `--print` (print the URL, do not launch a browser) |
89
+
90
+ **Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
76
91
 
77
- `--claimable` is the credential-free rung, programs only: no key is resolved, no `vos.json` is written, and the response is a claim URL (72h; unclaimed work is deleted — deliberate cleanup, not data loss). Hand the link to the user and **nowhere else** it is the only reference and the only credential. Claiming moves the vos into the user's library; iteration after claim rides their key (`vos push --vos <id>`). Limits: config ≤200KB, 5 pushes per day per network.
92
+ **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 audio) and re-plans the automatic spans under them.
93
+
94
+ **A fresh take opens on a backdrop.** `create`, `record` and `plan` put the first ready loop from `GET /api/backdrops` behind the card (its ground colour as `frame.background`); `--background <slug|url|none>` overrides it, and offline the frame stays bare with a note.
95
+
96
+ **The human handoff.** `vos open <take>` serves the take directory (CORS-open, ephemeral port) and opens the studio at `?take=<server>`. `doc.json` hydrates directly, so an agent's edits arrive intact and every span is draggable; it keeps serving until Ctrl-C. Inside the studio, `window.__vos` (`openTake`, `getDoc`, `edit`, `undo`, `redo`, `setSelection`) is the sanctioned scripting surface; edits go through the patch store, undoable like any user edit.
97
+
98
+ **Render time** (measured on an M-series laptop, 1080p): about 1.5x real time single-flight, of which several seconds are fixed browser launch and CDN module cost, so short takes are overhead-dominated and `--parallel` pays off on takes past roughly 30 s. 2K roughly doubles per-frame cost. Recording is real time plus a few seconds of encode.
78
99
 
79
100
  ## The take directory
80
101
 
81
102
  ```
82
103
  take/
83
- recording.webm encoded footage (CFR WebM)
84
- frames/ raw screencast JPEGs (kept for re-encode)
85
- cursor.json synthesized CursorTrack (exact coords, element rects)
86
- meta.json RecordingMeta (producer: "cli")
87
- actions.json the script that produced it the replay recipe
88
- doc.json ProjectDoc the agent-editable surface
89
- vos.json hosted tracking (vos id + base version), written by push/pull
104
+ recording.webm encoded footage (30 fps CFR WebM)
105
+ frames/ raw screencast JPEGs, indexed by frames.json (kept for re-encode)
106
+ cursor.json the synthesized CursorTrack: exact coordinates, element rects
107
+ meta.json RecordingMeta (producer: "cli", per-step source extents)
108
+ actions.json the script that produced it, with the resolved url
109
+ doc.json ProjectDoc, the editable cut
110
+ doc.prev.json the previous cut, kept by a re-record for `vos plan --reuse`
111
+ vos.json hosted tracking (vos id + base version), written by push, fetch and pull
112
+ stills/ `vos frames` output + stills.json
113
+ digest/ `vos digest` output: digest.json, <id>.full.png, <id>.crop.png, sheet.png
114
+ kit/ `vos deliver` output: kit.json + the assets
115
+ mic.webm, cam.webm sidecar tracks, present on takes pulled from vos.so with --media
90
116
  ```
91
117
 
92
- `doc.json` is the product's superpower: zoom/pan is `zoom: [{in, out, level, cx, cy, source}]`, trims are `segments`, pacing is `speed` all plain JSON. Edit and re-render; nothing re-runs the browser. `plan` honors the wand contract: `source:"manual"` spans are preserved, only `source:"auto"` suggestions regenerate.
93
-
94
- Contracts that bite: time is **SOURCE seconds** everywhere in `zoom`/`segments`/`speed`; zoom `cx`/`cy` are **normalized [0..1] video-frame fractions** (0.5, 0.5 = center), never pixels; `level` is 1..5. 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, coord ranges, export honesty) before you spend a render.
95
-
96
- The package also ships [`schema/channel-specs.json`](./schema/channel-specs.json): per-channel launch-asset specs (store screenshots, social cuts, README loops — dimensions, byte and duration ceilings). `vos deliver <take> --to <channels>` loops them in one pass — stills at each spec's exact pixels, video cuts, every artifact verified (misses land in `skipped[]` with the reason) — and writes `kit.json`, the manifest the `launch-kit` agent skill at [github.com/vosjs/skills](https://github.com/vosjs/skills) builds the rest of the release around.
118
+ 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`.
97
119
 
98
- ## Actions
120
+ ## actions.json
99
121
 
100
122
  ```json
101
123
  {
@@ -104,63 +126,120 @@ The package also ships [`schema/channel-specs.json`](./schema/channel-specs.json
104
126
  "steps": [
105
127
  { "do": "wait", "ms": 700 },
106
128
  { "do": "hover", "selector": "a[href='/pricing']", "ms": 700 },
107
- { "do": "click", "selector": "#signup" },
129
+ { "id": "signup", "do": "click", "selector": "#signup" },
108
130
  {
109
131
  "do": "type",
110
132
  "selector": "input[name=email]",
111
133
  "text": "demo@example.com"
112
134
  },
113
135
  { "do": "scroll", "dy": 400 },
114
- { "do": "move", "x": 640, "y": 320 }
136
+ { "do": "move", "x": 640, "y": 320 },
137
+ { "do": "drag", "selector": ".knob", "tx": 300, "ty": 0, "ms": 700 }
115
138
  ]
116
139
  }
117
140
  ```
118
141
 
119
- Verbs: `wait` · `hover` · `click` · `type` · `scroll` · `move` · `drag` (press-move-release — slide a range input, drag a canvas element, move a timeline clip). Because the CLI issues every input itself, the cursor track is synthesized exact coordinates, exact timing, fresh element rects which is what powers element-aware auto-zoom and click effects downstream. `vos validate` checks a script (or a take) without running anything. Verified the feature in agent-browser already? `vos actions from-agent-browser steps.jsonl` 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.
142
+ Seven verbs: `wait`, `hover` (`ms` 700), `click`, `type` (`delayMs` 40 per key; `focus: false` skips the focusing click, for a submitting Enter), `scroll`, `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip). 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. 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.
120
143
 
121
- ## CI: the release loop on every tag
144
+ 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.
122
145
 
123
- Everything above is headless, so a tag push can produce the release's media. Keep the source in the repo (`media/actions.json` + the signed-off `media/doc.json` + `media/vos.json` tracking + `LAUNCH.md`), then on each tag: `vos record --actions media/actions.json --out /tmp/take --strict --json` → `vos plan /tmp/take --reuse --from media/doc.json --json` (the committed cut re-times onto the new footage; the flagged list in the job log is the release's real to-do) → `vos deliver /tmp/take --to <channels> --release "$TAG" --json` → `vos push /tmp/take --label "$TAG launch" --yes --json` with `VOS_API_KEY` as a repository secret. [vosjs/action](https://github.com/vosjs/action) runs that loop as one step and keeps a comment on the pull request with the watch page and the kit; the hand-written workflow lives on the docs page: [vos.so/docs/guides/every-release](https://vos.so/docs/guides/every-release). GitHub's `ubuntu-latest` ships Google Chrome, which mp4 renders need; a stale-base push fails with the shelf's changelog rather than resolving a human race silently. Cloud render jobs (`POST /api/render/jobs`) take a `callbackUrl` POSTed the job JSON once at completed/failed — a doorbell, not the truth (poll the job for that).
146
+ ## doc.json
124
147
 
125
- ## For scripts and agents
148
+ `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.
149
+
150
+ **Contracts that bite.** Time is source seconds in `zoom`, `segments`, `speed` and `tilt`, and output seconds in `overlays` and `audio`. Zoom `cx`/`cy` and overlay `transform.x`/`y` 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.
151
+
152
+ **Camera styles.** `doc.zoomStyle` is one of `glide` (default), `focus`, `cinema`, `snappy`, `cut`, `keynote` (gliding zooms plus a medium lean toward each focus, the launch-film register), `drift` (slow ambient zooms with a subtle lean) or `none`. In the studio, picking a style also stamps `tiltStyle` and plans the tilt spans; in `doc.json` set `tiltStyle` and `tilt` yourself.
153
+
154
+ **Tilt.** `doc.tilt`: source-anchored, non-overlapping spans where the card leans to a pose and returns to rest: `[{ "id": "u0", "in": 4, "out": 8, "rx": 6, "ry": -9, "source": "manual" }]`. `rx`/`ry` are degrees (±45 hard limit; ±5..18 reads well); `+rx` brings the top edge toward the camera, `+ry` the left edge, so a lean toward a right-side focus is a negative `ry`. Rest is flat. `doc.tiltStyle` (`off`, `subtle`, `medium`, `strong`) is the intensity the planner derives automatic spans from.
126
155
 
127
- Logs on stderr, results on stdout; `--json` = NDJSON events ending with `{"event":"done",…}`; exit codes 0/1/2/3 (3 = no browser). Requires a Chromium-family browser (system Chrome is used automatically; `npx playwright install chromium` or `VOS_BROWSER_PATH` otherwise) and network access (render pages load three/mediabunny from the CDN).
156
+ **Text overlays.** `doc.overlays`: screen-space clips above the card, outside the zoom, output-anchored. `{ "id": "t0", "kind": "text", "start": 1, "duration": 3, "text": "Ship it", "preset": "title", "transform": { "x": 0.5, "y": 0.82, "scale": 1, "rotation": 0 }, "enter": "rise", "exit": "fade" }`. Presets `title`, `caption`, `label`, overridable with `size` (12..200 design px) and `color`; `\n` breaks lines; a lower third sits at `y` ≈ 0.82. Enter and exit: `rise`, `fade`, `none`. Fonts load at render start, fail-open to system stacks.
128
157
 
129
- Pass `--strict` to `record` (agents: always): a skipped selector or a page that never reaches networkidle exits 2 and lists `skipped[]` in the done event the default is lenient (exit 0, `skipped[]` still reported) for exploratory runs.
158
+ **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`.
130
159
 
131
- `record` and `create` take `--max-duration <seconds>` (default `1800`, the hosted 30 min cap): the capture stops there and the done event says so (`capped: true`, the remaining steps did not run); with `--strict` that exits 2. `vos push` refuses a take over 30 min (413), so cut the flow rather than raising the cap.
160
+ **3D props.** `doc.objects`: world-space props between the card and the overlays. `{ "id": "p0", "asset": { "kind": "primitive", "shape": "knot", "color": "#ffb03a" }, "span": { "start": 1, "duration": 3 }, "transform3d": { "x": 0.8, "y": 0.28, "z": 0.5, "rx": 0, "ry": 40, "rz": 0, "scale": 0.18 }, "animation": "spin" }`. Shapes `cube`, `sphere`, `torus`, `knot`; `kind: "gltf"` with `key` loads a GLB (bounding-box normalized so `scale` means the same for every model; a bad model fails open). `x`/`y` are frame fractions, `z` world units toward the camera from the card plane, `scale` a fraction of the frame height; `animation` is `spin` or `float`, deterministic.
132
161
 
133
- `create` is the one-shot verb record, auto-plan, and render in one command and one browser session. The take dir still lands on disk, so the full quality loop (frames edit `doc.json` → re-render) remains open afterwards. With `--strict` an incomplete recording aborts BEFORE the render (exit 2, nothing spent on a broken flow). It accepts the render flags (`--width/--height/--fps/--format/--parallel/--draft/--frame/--background/--set`); there is no `--range` (a fresh take renders whole).
162
+ **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.
134
163
 
135
- **Audio**: `doc.audio` clips (music/SFX; `key` may name a file inside the take dir, e.g. `"/music.mp3"`) are mixed with gain/fade/loop envelopes and muxed into full renders Opus for webm, AAC (Opus fallback) for mp4. Audio forces single-flight (`--parallel` is ignored with a note); a `--range` render keeps its audio (the full-timeline mix is sliced to the range window).
164
+ **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`.
136
165
 
137
- **Background**: `doc.frame.backgroundMedia` puts a vos animation (or a still image) behind the recording card — `{ "kind": "video", "key": "https://assets.vos.so/backgrounds/ember-drift-1080p.webm", "duration": 10, "dim": 0.2 }`. `key` is a media URL or a take-dir file (`"/bg.webm"`); a video needs `duration` (the loop length — time is OUTPUT-anchored modulo it, so trims/speed never retime the ambience), `dim` (0..1) is a black scrim for legibility. The hand-picked set is `GET https://vos.so/api/backdrops` (no auth): write `key` from `urls["1080p"]`, `duration` from the row and `frame.background` from its `ground`; `--background <slug>` on `render`/`frames` does exactly that. Drawn under the card, outside the zoom transform; fail-open (the CSS `frame.background` still shows if it can't load). Take-dir video backgrounds seek via the server's Range support. Depth dials: `frame.parallax` 0..1 (the media counter-pans with the zoom — 0.6 reads well) and `frame.backgroundMedia.blur` in design px.
166
+ ## Overrides on render and frames
138
167
 
139
- **Tilt-forward camera styles**: `doc.zoomStyle: "keynote" | "drift"` bundle zoom AND lean into one camera sentence `keynote` (the launch-film register: gliding session zooms + a medium lean toward each zoom's focus, tilt ramps matched to the zoom ramps) and `drift` (calm ambient depth: slow fluid zooms + a subtle lean over 1.6s ramps, long chains). In the studio, picking one stamps `tiltStyle` and plans the auto tilt spans; in doc.json set `tiltStyle` + `tilt` spans yourself — the style still shapes tilt motion at render.
168
+ 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`.
140
169
 
141
- **Tilt moments** (tilt spans): `doc.tilt` SOURCE-anchored, non-overlapping regions where the 3D card leans to a pose and returns to rest: `[{ "id": "u0", "in": 4, "out": 8, "rx": 6, "ry": -9, "source": "manual" }]`. `rx`/`ry` are DEGREES (±45 hard; ±5..18 reads premium): `+rx` brings the TOP edge toward the camera, `+ry` the LEFT edge — lean toward a right-side focus with NEGATIVE `ry`. Rest is FLAT (there is no static card pose). Ramps ~0.9s in / ~0.8s out; spans ≤ ~1.35s apart swing pose-to-pose. `doc.tiltStyle` (`"off"|"subtle"|"medium"|"strong"`) records the Dynamic-tilt wand intensity — the studio derives `source:"auto"` spans from the zoom spans (lean toward each zoom's focus); spans you add or edit should carry `source:"manual"` so a re-plan never touches them. Tilt is punctuation: one pose change per ~5s beat, paired with zoom moments.
170
+ - `--set <path>=<value>` (repeatable) patches any field. The value is JSON when it parses (numbers, booleans, `null`, objects, arrays), else a string; array indices work: `--set zoom[0].level=3`, `--set frame.padding=120`, `--set 'frame.backgroundMedia={"kind":"video","key":"","duration":10}'`.
171
+ - `--frame <kind>` on `render` sets the browser chrome: `macos` (`mac`, `mac-light`), `mac-dark`, `windows` (`windows-light`), `windows-dark`, `minimal`, `none` (`hidden`). On `frames`, `--frame <t>` is the still's time; set the chrome there with `--set frame.browserBar.kind=…`.
172
+ - `--background <slug|url|none>`: a slug is resolved against `GET /api/backdrops` (its 1080p loop and duration); a URL or take-directory path is used as is, an image extension meaning a still and anything else a video looping every 10 s; `none` clears it.
142
173
 
143
- **Text overlays**: `doc.overlays` screen-space text clips ABOVE the card, outside the zoom transform, **OUTPUT-anchored** (trims/speed never retime a title). `{ "id": "t0", "kind": "text", "start": 1, "duration": 3, "text": "Ship it", "preset": "title", "transform": { "x": 0.5, "y": 0.82, "scale": 1, "rotation": 0 }, "enter": "rise", "exit": "fade" }`. Presets: `title` (Lexend 600 · 64px) / `caption` (Lexend 400 · 32px) / `label` (JetBrains Mono · 22px), overridable with `size` (12–200 design px) and `color`; `\n` in `text` breaks lines. **`transform.x/y` are FRACTIONS of the frame [0..1]** the zoom `cx/cy` convention: `0.5/0.5` = center at ANY aspect ratio, lower-third y ≈ `0.82`; positions survive aspect switches. NOT pixels (validate errors on pixel-looking values, warns off-frame). Enter/exit: `rise` / `fade` / `none` over ~0.35s. Fonts load from the CDN at render start (fail-open to system stacks). Quick check without editing doc.json: `vos frames take --set 'overlays=[…]'`.
174
+ So `vos frames take --frame 2.0 --set frame.browserBar.kind=mac-light --set tilt[0].rx=8` previews a macOS-framed, tilted card as a still, and `vos render take out.webm --frame macos --background soft-beams` renders it on a loop from the set. Verifying a document field and offering it as a feature are the same act here: reach for a flag, not a bespoke harness.
144
175
 
145
- **Image/video overlays**: media kinds on the same `doc.overlays` lane — `{ "id": "m0", "kind": "image"|"video", "start": 2, "duration": 4, "key": "/logo.png", "width": 0.35, "radius": 12, "opacity": 1, "loop": false, "transform": { "x": 0.5, "y": 0.5, "scale": 1, "rotation": 0 } }`. `key` rides the same plumbing as every media key (take-dir file, URL); `width` is a FRACTION of the frame width (height follows the media's aspect, × `transform.scale`); corners in design px (default 12, drawn as a shadowed media card). Video time is CLIP-LOCAL (t − start), muted by design — soundtracks belong to `doc.audio`; `loop: true` loops, else the last frame holds. Same enter/exit transitions as text.
176
+ ## Delivering a release
146
177
 
147
- **3D props**: `doc.objects` — world-space props between the card and the overlays. `{ "id": "p0", "asset": { "kind": "primitive", "shape": "knot", "color": "#ffb03a" }, "span": { "start": 1, "duration": 3 }, "transform3d": { "x": 0.8, "y": 0.28, "z": 0.5, "rx": 0, "ry": 40, "rz": 0, "scale": 0.18 }, "animation": "spin" }`. Shapes: `cube | sphere | torus | knot` (curated, fleet-safe). `x/y` are FRACTIONS of the frame; `z` = world units toward the camera from the card plane (0 = on it, 0.5 floats clearly in front — props can overhang the card); `scale` = fraction of the frame height; `animation`: `spin | float` (deterministic). `span` gates visibility with soft fades; absent = whole timeline. `kind: "gltf"` + `key` loads a real GLB (URL or take-dir file like `"/model.glb"`), bbox-NORMALIZED so `scale` means the same as primitives; loads fail-open (a bad model just doesn't render).
178
+ ```bash
179
+ vos deliver take --to cws,producthunt,og --release v2.1 # stills at each spec's pixels, video cuts, everything verified into kit.json
180
+ vos deliver take --to cws,og,linkedin --poster poster.json # the card half: covers composed by your poster program with this release's shot baked in
181
+ vos validate take/kit/kit.json # re-measure every asset from its bytes against the channel specs
182
+ vos brand https://your.app --out BRAND.md # the brand kit, witnessed: /design.md, /llms.txt, then the page
183
+ ```
184
+
185
+ [`schema/channel-specs.json`](./schema/channel-specs.json) holds per-channel launch-asset specs: dimensions, byte and duration ceilings, and a genre per image destination (`screenshot` is the real page from the take, full bleed; `card` is a composed cover). Channels: `cws`, `producthunt` (`ph`), `x`, `linkedin` (`li`), `og`, `github` (`gh`), `youtube` (`yt`), `shorts-linkedin` (`shorts`), or `all`. `vos deliver <take> --to <channels>` loops them in one pass and writes `kit.json`, the manifest the `launch-kit` skill builds the rest of the release around; an asset that misses its spec lands in `skipped[]` with the reason, and the verb exits 1 when nothing was produced. Flags: `--release <tag>`, `--out <take>/kit`, `--times`, `--range`, `--parallel`, `--poster <config.json|vosId>`, `--shot-time <t>` (the take moment baked into the poster), `--poster-time <t>` (the poster's own clock), `--composed` (keep the cut's camera and chrome on screenshot stills instead of the full-bleed page), `--set`, `--background`. `vos validate` reads a kit back from its bytes (a `.png` that is WebP, a lying size or duration, a set under its count, a byte ceiling).
186
+
187
+ `vos brand <url>` reads the site's `/design.md` first (the convention beside `/llms.txt`), then `/llms.txt`, then witnesses one page, and writes `BRAND.md`: the palette, faces, marks and the avoid list, with the provenance of every value, so a brand is resolved before any asset is authored.
188
+
189
+ ## The vos.so loop
190
+
191
+ 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.
192
+
193
+ ```bash
194
+ 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
195
+ vos fetch <vosId|url> [--out <slug>] [--media] # a program writes config.json + vos.json; a take writes doc.json (+ the recording with --media)
196
+ vos check bright-loop/config.json # full local validation
197
+ vos push bright-loop/config.json # create a PRIVATE vos (lineage from vos.json or --remix-of)
198
+ vos push bright-loop/config.json --vos <id> # add a version against your tracked base
199
+ vos push bright-loop/config.json --claimable # no credential: a 72 h claim link instead (programs only)
200
+ vos push take --yes --label "first pass" --note "…" # host a take: private vos + version history (the recording uploads once)
201
+ vos pull bright-loop [--since <versionId>] [--check] # what changed on vos.so since your base; syncs config.json (backup kept), or doc.json for a take
202
+ # --check reports without writing; --since walks from a base you name
203
+ vos duplicate <vosId> # a private sibling of your OWN vos (someone else's is remixed: fetch, then push --remix-of)
204
+ vos folder list | create <name> [--parent] [--desc] | move <ids…> --to <folder|none> | pull <ref> [--media]
205
+ vos asset push <file…> [--folder <slug>] | rename <id> <name.ext>
206
+ vos recipe push <FILE.md> --folder <slug> | --asset <id> # the one recipe write: create, or replace in place
207
+ ```
148
208
 
149
- **Doc overrides on `render` / `frames`** check any presentation from the CLI without hand-editing `doc.json` (the disk file is untouched; the patched doc is **lint-gated**, so a bad override fails exactly like a bad `doc.json`). This is deliberate: vos is both producer and consumer, so _verifying_ a doc field and _offering_ it as a feature are the same act reach for a flag, not a bespoke harness.
209
+ `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. 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` and `--yes`.
150
210
 
151
- - `--set <path>=<value>` (repeatable) patch any doc field. The value is JSON when it parses (numbers, booleans, `null`, objects, arrays), else a string. Array indices work: `--set zoom[0].level=3`. Examples: `--set tilt[0].rx=8`, `--set frame.padding=120`, `--set frame.browserBar.kind=mac-light`, `--set 'frame.backgroundMedia={"kind":"video","key":"…","duration":10,"dim":0.2}'`.
152
- - `--frame <kind>` (**render** only — `frames --frame <t>` is the still time selector; set the frame kind on `frames` via `--set frame.browserBar.kind=…`): `macos | mac-dark | windows | windows-dark | minimal | none`.
153
- - `--background <url>` — a background media layer; kind inferred from the URL (`.webm`/`.mp4` → video at a 10s loop, image otherwise); `none` clears it.
211
+ Both paths share the same base tracking and the same two 409 shapes. `stale_base` replays the platform's typed changelog: run `vos pull`, re-apply, push again. `protected_conflict` lists nodes a human edited in the studio: keep their values, or re-push with `--override <id>` only when the user asked for that exact change. The first push of a take asks before uploading (`--yes` for headless); agents never upload unprompted. The take's duration rides the upload, and the platform refuses a take over the hosted recording cap. Every push should carry `--label` (what changed, one line) and `--note` (why: the user's ask); the version history reads as a conversation, and an unlabelled push is a turn the human cannot read.
154
212
 
155
- So `vos frames take --frame 2.0 --set frame.browserBar.kind=mac-light --set tilt[0].rx=8` previews a macOS-framed, tilted card as a still; `vos render take out.webm --frame macos --background soft-beams` renders it on a backdrop from the set.
213
+ `--claimable` is the credential-free rung, programs only: no key is resolved, no `vos.json` is written, and the response is a claim URL (72 h; unclaimed work is deleted, which is deliberate cleanup). Hand the link to the user and nowhere else: it is the only reference and the only credential. Claiming moves the vos into the user's library, and iteration after claim rides their key (`vos push --vos <id>`). Limits are the platform's: config 200 KB, 5 pushes per day per network.
156
214
 
157
- **The human handoff**: `vos open <take>` serves the take dir (CORS-open, ephemeral port) and opens the studio at `?take=<server>` — `doc.json` hydrates directly, so the agent's zoom/trim/audio edits arrive intact and every span is draggable. Keeps serving until Ctrl-C (`--studio <url>` overrides the default `http://localhost:6060`; `--print` skips the browser launch). In the studio, agents also get a sanctioned scripting surface: `window.__vos` (`openTake / getDoc / edit / undo / redo / setSelection`) — edits go through the patch store, undoable like any user edit.
215
+ `vos folder pull <ref>` writes a folder's context package to disk (its recipes, the inherited ones, the exemplar programs and assets), which is what an agent reads to create in the owner's style. Recipes are `.md` files named in capitals (`CUT.md`, `BRAND.md`): the server uppercases an agent-filed name.
158
216
 
159
- **Render-time expectations** (M-series laptop, 1080p output, measured): a 12.5s take ≈ 19s single-flight, ≈ 15s with `--parallel 4` — call it **~1.5× real-time at 1080p**, of which several seconds are fixed browser-launch + CDN module cost, so short takes are overhead-dominated and `--parallel` pays off mainly on takes ≳ 30s. 2K roughly doubles per-frame cost. Recording is always real-time (the take's duration) plus a few seconds of encode.
217
+ ## CI
218
+
219
+ Everything above is headless, so a tag push can produce the release's media. Keep the source in the repo (`media/actions.json`, the signed-off `media/doc.json`, `media/vos.json`, `LAUNCH.md`), then on each tag:
220
+
221
+ ```bash
222
+ vos record --actions media/actions.json --out /tmp/take --strict --json
223
+ vos plan /tmp/take --reuse --from media/doc.json --json # the committed cut re-times onto the new footage; the flagged list is the release's to-do
224
+ vos deliver /tmp/take --to <channels> --release "$TAG" --json
225
+ vos push /tmp/take --label "$TAG launch" --yes --json # VOS_API_KEY as a repository secret
226
+ ```
227
+
228
+ [vosjs/action](https://github.com/vosjs/action) runs that loop as one step and keeps a comment on the pull request with the watch page and the kit; the hand-written workflow is on the docs page [vos.so/docs/guides/every-release](https://vos.so/docs/guides/every-release). GitHub's `ubuntu-latest` ships Google Chrome, which mp4 renders need. A stale-base push fails with the shelf's changelog rather than resolving a human race silently. Cloud render jobs (`POST /api/render/jobs`) take a `callbackUrl` that is POSTed the job JSON once at completed or failed: a doorbell, not the truth (poll the job for that).
229
+
230
+ ## For scripts and agents
231
+
232
+ - **Output.** Logs on stderr, results on stdout. `--json` turns every verb into NDJSON events ending with `{"event":"done",…}`.
233
+ - **Exit codes.** 0 ok, 1 error, 2 usage (including `--strict` failures), 3 no browser found.
234
+ - **`--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.
235
+ - **`--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.
236
+ - **`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.
237
+ - **`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.
238
+ - **Environment.** `VOS_ORIGIN`, `VOS_API_KEY`, `VOS_BROWSER_PATH`, `VOS_CLIENT` (the client string a push self-reports). `vos voila <verb>` is still accepted as an alias of `vos <verb>` and says so.
160
239
 
161
240
  ## Programmatic use
162
241
 
163
- The same core is available as a library:
242
+ The same functions are available as a library:
164
243
 
165
244
  ```ts
166
245
  import { launchBrowser, loadVosConfig, renderVideo } from '@vosjs/cli'
@@ -177,6 +256,8 @@ const { bytes } = await renderVideo(browser, {
177
256
  })
178
257
  ```
179
258
 
180
- `recordTake`, `planTake`, `digestTake`, `renderTake`, `validateActions`, `convertAgentBrowser` and the vos.so client helpers (`resolveCredential`, `readSyncState`) are exported the same way; `run(argv)` dispatches any take or platform verb.
259
+ Also exported: `renderStill`, `previewPages`, `configDuration`, `BrowserUnavailableError`; the take pipeline (`recordTake`, `encodeRecording`, `planTake`, `digestTake`, `parseTranscript`, `renderTake`, `renderAnimation`, `pullMedia`, `loadTake`, `takePaths`, `startTakeServer`, `validateActions`, `convertAgentBrowser`, `parseAgentBrowserLog`); the vos.so client helpers (`resolveCredential`, `platformOrigin`, `parseVosId`, `readSyncState`, `writeSyncState`, `apiJson`, `apiError`); `manifest`, the verb list `vos help` prints; and `run(argv)`, which dispatches any take or platform verb.
260
+
261
+ ## License
181
262
 
182
- Part of [vos](https://github.com/vosjs/vos). MIT.
263
+ [MIT](https://github.com/vosjs/vos/blob/main/LICENSE) © vosso
@@ -38,6 +38,10 @@ var manifest = {
38
38
  name: "validate",
39
39
  summary: "lint actions.json, a take dir (doc.json semantics), or re-measure a kit.json against the channel specs"
40
40
  },
41
+ {
42
+ name: "actions",
43
+ summary: "from-agent-browser: turn an agent-browser walk (steps.jsonl) into actions.json; what cannot follow is named"
44
+ },
41
45
  {
42
46
  name: "brand",
43
47
  summary: "write a product's BRAND.md, witnessed: /design.md, /llms.txt, then the page (palette, faces, marks, the avoid list)"
@@ -80,4 +84,4 @@ var manifest = {
80
84
  export {
81
85
  manifest
82
86
  };
83
- //# sourceMappingURL=chunk-5BFGODBI.js.map
87
+ //# sourceMappingURL=chunk-EFI3WUQB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/manifest.ts"],"sourcesContent":["/**\n * The verb manifest of the take pipeline and the vos.so verbs — what\n * `vos help` lists under the engine verbs. It kept the shape a separately\n * installed plugin once handed the host (name + host range), so a script\n * reading it keeps working.\n */\nexport interface PluginVerb {\n name: string\n summary: string\n}\n\nexport interface PluginManifest {\n name: string\n /** Host versions this plugin speaks the run(argv) contract with. */\n hostRange: string\n verbs: PluginVerb[]\n}\n\nexport const manifest: PluginManifest = {\n name: '@vosjs/cli',\n hostRange: '>=0.9.0',\n verbs: [\n {\n name: 'create',\n summary: 'record + auto-plan + render, one command (--strict)',\n },\n {\n name: 'record',\n summary: 'drive actions.json into a take (screencast + cursor track)',\n },\n {\n name: 'plan',\n summary:\n 'plan zoom/cursor effects into doc.json (wand contract); --reuse re-times a previous cut onto a re-recording',\n },\n {\n name: 'render',\n summary: 'render a take directory (engine configs render in the host)',\n },\n {\n name: 'frames',\n summary:\n 'PNG stills at output times / zoom apexes / moments / exact sizes',\n },\n {\n name: 'deliver',\n summary:\n 'render a take to release destinations (CWS, Product Hunt, X, LinkedIn, OG…) + verified kit.json',\n },\n {\n name: 'digest',\n summary:\n 'see a recording before cutting it: moments (clicks, typing, scrolls, idle, scenes) + footage frames + crops, in doc units',\n },\n { name: 'open', summary: 'serve the take into the vos.so studio' },\n {\n name: 'validate',\n summary:\n 'lint actions.json, a take dir (doc.json semantics), or re-measure a kit.json against the channel specs',\n },\n {\n name: 'actions',\n summary:\n 'from-agent-browser: turn an agent-browser walk (steps.jsonl) into actions.json; what cannot follow is named',\n },\n {\n name: 'brand',\n summary:\n \"write a product's BRAND.md, witnessed: /design.md, /llms.txt, then the page (palette, faces, marks, the avoid list)\",\n },\n {\n name: 'fetch',\n summary: 'download a hosted program: config.json + vos.json tracking',\n },\n {\n name: 'push',\n summary: 'push a config.json or take to vos.so (private; versioned)',\n },\n {\n name: 'duplicate',\n summary:\n 'a private sibling of your OWN vos (someone else\\u2019s is remixed: fetch + push --remix-of)',\n },\n {\n name: 'pull',\n summary:\n 'sync what changed on vos.so since your base (--media brings a take’s footage home)',\n },\n {\n name: 'folder',\n summary:\n 'list/create/pull folders, move voses and assets into them (pull = the context package on disk)',\n },\n {\n name: 'asset',\n summary: 'rename one of your assets in place (recipes included)',\n },\n {\n name: 'recipe',\n summary:\n 'push a recipe .md into a folder, or replace one in place (prior body kept)',\n },\n {\n name: 'login',\n summary: 'sign in via the browser (or --key); stores a content key',\n },\n ],\n}\n"],"mappings":";;;AAkBO,IAAM,WAA2B;AAAA,EACtC,MAAM;AAAA,EACN,WAAW;AAAA,EACX,OAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA,EAAE,MAAM,QAAQ,SAAS,wCAAwC;AAAA,IACjE;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
@@ -1,104 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/args.ts
4
- var UsageError = class extends Error {
5
- };
6
- function parseArgs(argv, booleanFlags) {
7
- const positionals = [];
8
- const flags = {};
9
- for (let i = 0; i < argv.length; i++) {
10
- const arg = argv[i];
11
- if (arg === "--") {
12
- positionals.push(...argv.slice(i + 1));
13
- break;
14
- }
15
- if (arg.startsWith("--")) {
16
- const eq = arg.indexOf("=");
17
- if (eq !== -1) {
18
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
19
- continue;
20
- }
21
- const name = arg.slice(2);
22
- if (booleanFlags.has(name)) {
23
- flags[name] = true;
24
- continue;
25
- }
26
- const next = argv[i + 1];
27
- if (next === void 0 || next.startsWith("--")) {
28
- throw new UsageError(`--${name} expects a value`);
29
- }
30
- flags[name] = next;
31
- i++;
32
- continue;
33
- }
34
- positionals.push(arg);
35
- }
36
- return { positionals, flags };
37
- }
38
- function numFlag(flags, name, fallback) {
39
- const v = flags[name];
40
- if (v === void 0) return fallback;
41
- const n = Number(v);
42
- if (!Number.isFinite(n))
43
- throw new UsageError(`--${name} expects a number, got "${String(v)}"`);
44
- return n;
45
- }
46
-
47
- // src/loadConfig.ts
48
- import { readFile } from "fs/promises";
49
- import {
50
- CURRENT_CONFIG_VERSION,
51
- migrateConfig,
52
- vosConfigJsonSchema
53
- } from "@vosjs/core";
54
- async function loadVosConfig(source) {
55
- const warnings = [];
56
- let raw;
57
- if (/^https?:\/\//.test(source)) {
58
- const res = await fetch(source);
59
- if (!res.ok) throw new Error(`fetch ${source} \u2192 ${res.status}`);
60
- raw = await res.text();
61
- } else {
62
- raw = await readFile(source, "utf8");
63
- }
64
- let parsed;
65
- try {
66
- parsed = JSON.parse(raw);
67
- } catch (e) {
68
- throw new UsageError(`${source} is not valid JSON: ${e.message}`);
69
- }
70
- if (typeof parsed !== "object" || parsed === null) {
71
- throw new UsageError(`${source} does not contain a JSON object`);
72
- }
73
- let obj = parsed;
74
- if (typeof obj.config === "object" && obj.config !== null && !("createTimeline" in obj)) {
75
- obj = obj.config;
76
- warnings.push("unwrapped { config } envelope");
77
- }
78
- const version = obj.version;
79
- const migrated = migrateConfig(obj);
80
- if (version === void 0) {
81
- warnings.push(
82
- `config declares no "version". Add "version": ${CURRENT_CONFIG_VERSION} before pushing it.`
83
- );
84
- } else if (version !== CURRENT_CONFIG_VERSION) {
85
- warnings.push(
86
- `migrated config v${String(version)} to v${CURRENT_CONFIG_VERSION}`
87
- );
88
- }
89
- const check = vosConfigJsonSchema.safeParse(migrated);
90
- if (!check.success) {
91
- const issues = check.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
92
- throw new UsageError(`invalid vos config:
93
- ${issues}`);
94
- }
95
- return { config: migrated, warnings };
96
- }
97
- function configDuration(config) {
98
- const d = config.duration;
99
- return typeof d === "number" && Number.isFinite(d) && d > 0 ? d : void 0;
100
- }
101
-
102
3
  // src/render.ts
103
4
  import { compileVosConfig } from "@vosjs/core";
104
5
  import { generateRenderTemplate } from "@vosjs/core/runtime";
@@ -254,13 +155,8 @@ function previewPages(config) {
254
155
  }
255
156
 
256
157
  export {
257
- UsageError,
258
- parseArgs,
259
- numFlag,
260
- loadVosConfig,
261
- configDuration,
262
158
  renderVideo,
263
159
  renderStill,
264
160
  previewPages
265
161
  };
266
- //# sourceMappingURL=chunk-X2BKHZ56.js.map
162
+ //# sourceMappingURL=chunk-GCWE75OA.js.map