@vosjs/cli 0.46.1 → 0.48.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 (39) hide show
  1. package/README.md +15 -5
  2. package/dist/{chunk-UE66DC2K.js → chunk-ANKECV7F.js} +658 -60
  3. package/dist/chunk-ANKECV7F.js.map +1 -0
  4. package/dist/{chunk-ID4OUS5L.js → chunk-AUPGJJTL.js} +18 -1
  5. package/dist/chunk-AUPGJJTL.js.map +1 -0
  6. package/dist/{chunk-EFI3WUQB.js → chunk-P3X5KVA6.js} +15 -2
  7. package/dist/{chunk-EFI3WUQB.js.map → chunk-P3X5KVA6.js.map} +1 -1
  8. package/dist/cli.js +4 -4
  9. package/dist/index.js +3 -3
  10. package/dist/manifest-A4R367EM.js +8 -0
  11. package/dist/{run-2OZS25ER.js → run-CYHH56KA.js} +3 -3
  12. package/dist/{shotList-JC4A3FBT.js → shotList-7F2FE3RU.js} +2 -2
  13. package/package.json +9 -7
  14. package/schema/actions.schema.json +2 -2
  15. package/skills/VERSION +1 -0
  16. package/skills/launch-kit/SKILL.md +356 -0
  17. package/skills/launch-kit/references/channel-specs.md +56 -0
  18. package/skills/product-video/SKILL.md +263 -0
  19. package/skills/product-video/references/destinations.md +71 -0
  20. package/skills/product-video/references/sessions.md +226 -0
  21. package/skills/product-video/references/taste.md +115 -0
  22. package/skills/product-video/references/troubleshooting.md +116 -0
  23. package/skills/vos-authoring/SKILL.md +191 -0
  24. package/skills/vos-authoring/references/examples.md +239 -0
  25. package/skills/vos-authoring/references/schema-reference.md +468 -0
  26. package/skills/vos-create/SKILL.md +258 -0
  27. package/skills/vos-cut/SKILL.md +241 -0
  28. package/skills/vos-footage/SKILL.md +98 -0
  29. package/skills/vos-migrate/SKILL.md +108 -0
  30. package/skills/vos-remix/SKILL.md +136 -0
  31. package/skills/vos-remix/references/3d-recipe.md +37 -0
  32. package/skills/vos-remix/references/params-knobs.md +80 -0
  33. package/skills/vos-remix/references/remix-contract.md +81 -0
  34. package/dist/chunk-ID4OUS5L.js.map +0 -1
  35. package/dist/chunk-UE66DC2K.js.map +0 -1
  36. package/dist/manifest-UCS6OVWZ.js +0 -8
  37. /package/dist/{manifest-UCS6OVWZ.js.map → manifest-A4R367EM.js.map} +0 -0
  38. /package/dist/{run-2OZS25ER.js.map → run-CYHH56KA.js.map} +0 -0
  39. /package/dist/{shotList-JC4A3FBT.js.map → shotList-7F2FE3RU.js.map} +0 -0
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: vos-migrate
3
+ description: Turn an existing Loom, Screen Studio, Cap, or plain mp4/webm screen demo into an editable vosso take — re-encode the file, synthesize the take's metadata, plan, and push, so the old demo becomes a living document you re-cut instead of a file you re-record. Use when asked to "convert this Loom", "migrate our old demos", "re-edit this mp4", "make our Screen Studio recording editable", or to bring any already-recorded demo into vosso.
4
+ license: MIT
5
+ ---
6
+
7
+ # Migrate a recorded demo into an editable take
8
+
9
+ You are turning a FINISHED video file into a vosso take: a document whose
10
+ zooms, trims, speed and layers are data, hosted with version history, and
11
+ editable in the studio. What the file loses by being foreign is honesty
12
+ you must carry: it has NO cursor track, so nothing can be auto-zoomed and
13
+ the digest sees little — your eyes are the contact sheet, and every zoom
14
+ is placed by eye.
15
+
16
+ Two doors:
17
+
18
+ - **The human door**: drop the file on https://vos.so/studio — it enters as
19
+ the browser-recorder shape and opens in the take editor directly. When a
20
+ human is present, this is the shortest path; hand them the link.
21
+ - **The agent door** (this skill): build the take directory yourself and
22
+ run the normal pipeline. Proven end to end below.
23
+
24
+ ## 1. Probe the source
25
+
26
+ ```bash
27
+ ffprobe -v quiet -print_format json -show_streams -show_format demo.mp4
28
+ ```
29
+
30
+ Keep: width, height, duration, fps, whether an audio stream exists. A
31
+ Loom/Screen Studio export is typically 1080p H.264 with the camera bubble
32
+ and any edits BAKED IN — they migrate as pixels, not as layers. Say so in
33
+ the handoff: the migration makes the file editable from here on, it does
34
+ not un-bake old edits.
35
+
36
+ ## 2. Build the take directory
37
+
38
+ ```bash
39
+ mkdir take
40
+ ffmpeg -i demo.mp4 -c:v libvpx-vp9 -crf 34 -b:v 0 -row-mt 1 \
41
+ -c:a libopus take/recording.webm # drop -c:a if no audio stream
42
+ ```
43
+
44
+ Then write `take/meta.json` from the probe (every field required; times in
45
+ ms; `producer: "migrated"` marks provenance):
46
+
47
+ ```json
48
+ {
49
+ "producer": "migrated",
50
+ "dpr": 1, "zoom": 1,
51
+ "t0": 0,
52
+ "durationMs": 36766,
53
+ "width": 1920, "height": 1080,
54
+ "fps": 30,
55
+ "hasAudio": false
56
+ }
57
+ ```
58
+
59
+ `t0` may be any epoch ms; `width`/`height` are the video pixels (there was
60
+ no browser viewport). Set `hasAudio` true only when the webm actually
61
+ carries the track.
62
+
63
+ ## 3. Plan, and see it honestly
64
+
65
+ ```bash
66
+ vos plan take --fresh # builds doc.json; cursorKept false, zoomAuto 0
67
+ vos frames take --times 0,10%,25%,50%,75%,90%,100% # the contact sheet IS your eyes
68
+ vos digest take # runs, but expect little: head/tail and only
69
+ # hard scene cuts - no clicks, no typing, no dwells
70
+ ```
71
+
72
+ No auto-zoom is possible and none should be faked. Read the stills, find
73
+ the 2–4 moments that matter, and write manual spans by eye:
74
+
75
+ ```bash
76
+ # doc.json - every span you add carries "source": "manual"
77
+ # zoom: [{ "id": "m1", "in": 10.5, "out": 14.0, "level": 1.6,
78
+ # "cx": 0.52, "cy": 0.35, "source": "manual" }]
79
+ # segments: trim dead heads/tails; speed: only where footage truly idles
80
+ ```
81
+
82
+ Set `export.resolution` to MATCH the footage (a 1080p source is `1080p` —
83
+ migrating does not add pixels). `vos validate take` must pass; spot-check
84
+ with `vos render take check.webm --range a..b --draft`.
85
+
86
+ ## 4. Push — the old demo becomes a living document
87
+
88
+ ```bash
89
+ vos push take --folder <project> --label "migrated from <source>" \
90
+ --note "<what it shows; that old edits are baked; what you re-cut>"
91
+ ```
92
+
93
+ End on the loop: the watch page plays it, the studio edits every span you
94
+ wrote plus everything the file never had (backdrops, text layers, speed),
95
+ and `vos pull` brings human edits back down. The pitch belongs in the
96
+ handoff, in one line: **this is the last demo you edit blind — record the
97
+ next one with `vos record` and the camera plans itself.**
98
+
99
+ ## Honest limits (say them, never paper over them)
100
+
101
+ - No cursor track ⇒ no auto-zoom, no click effects, no typing zooms, and
102
+ no framing lint — every zoom is yours, by eye, and you say so.
103
+ - Old edits, camera bubbles and captions are baked pixels.
104
+ - The migration re-encodes once (VP9); a badly compressed source stays
105
+ badly compressed — migrating does not restore quality.
106
+ - A spec-size deliverable ladder (store stills, posters) works from the
107
+ migrated take exactly as from a native one — the `launch-kit` skill
108
+ takes over when the ask is a release.
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: vos-remix
3
+ description: Remix a vos.so gallery program into your own — fetch its config, edit the declared knobs or code locally, validate with vos check, and push a private copy back with lineage that iterates as versions. Carries the params/Looks doctrine and the GLB 3D-showcase recipe. Use when asked to remix, customize or personalize a gallery animation, swap a 3D model into a showcase, or iterate a fetched program as versions.
4
+ license: MIT
5
+ ---
6
+
7
+ # Remix a vos program
8
+
9
+ Every video on [vos.so/gallery](https://vos.so/gallery) is a program:
10
+ inputs + a deterministic function → video. Remixing is editing that
11
+ program's JSON config and pushing the result back as a **private** vos on
12
+ the user's account, with lineage. You edit locally; the platform validates,
13
+ compiles, and renders a preview.
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ npm i -D @vosjs/cli # the fetch/check/push/pull loop
19
+ ```
20
+
21
+ No browser needed for this loop (rendering previews happens on the
22
+ platform). Everything also works as plain HTTP if the CLI is unavailable:
23
+ `references/remix-contract.md` documents every endpoint.
24
+
25
+ ## Credentials (never print them)
26
+
27
+ Two shapes, both used as `Authorization: Bearer`:
28
+
29
+ 1. **Remix grant** (`vos_rg_…`) — if the user's prompt contains one, use it.
30
+ 24h lifetime, bound to ONE source vos, max 5 pushes.
31
+ 2. **Durable key** (`vos_sk_…`) — resolution order before asking:
32
+ `VOS_API_KEY` env, then the first line of `~/.config/vos/credentials`.
33
+ Humans mint keys at https://vos.so/app/api.
34
+ 3. **No credential at all** — the claimable push (programs only):
35
+ ```bash
36
+ vos push my-remix.json --claimable --title "My remix"
37
+ # → claim: https://vos.so/claim/… expires: <72h from now>
38
+
39
+ # No CLI, or an older one? The same thing over plain HTTP:
40
+ curl -s -X POST https://vos.so/api/claim \
41
+ -H 'content-type: application/json' \
42
+ -d '{"title": "My remix", "config": <the VosConfigJson>}'
43
+ # → { "claimUrl": "https://vos.so/claim/…", "expiresAt": "…" }
44
+ ```
45
+ Hand `claimUrl` to the user and nowhere else — it is the only reference
46
+ and the only credential. The link lasts 72 hours; unclaimed work is
47
+ deleted after that (deliberate cleanup, not data loss — re-push if it
48
+ lapses). Claiming moves the vos into the user's library and starts its
49
+ hosted preview; iteration after claim rides their key (the loop below).
50
+ Limits: config ≤200KB, 5 pushes per day per network.
51
+
52
+ The CLI resolves grants and keys automatically (`export VOS_API_KEY=…`). No
53
+ shape can publish: pushed voses stay private until a human publishes on
54
+ vos.so.
55
+
56
+ ## The loop
57
+
58
+ 1. **Fetch** the source program (public programs need no auth):
59
+ ```bash
60
+ vos fetch https://vos.so/vos/<id> # or the bare id
61
+ # → <slug>/config.json (params preserved) + <slug>/vos.json (tracking)
62
+ ```
63
+
64
+ 2. **Edit** `config.json` locally. The function fields (`setup`,
65
+ `createContent`, `createTimeline`, `onFrame`) are JavaScript **as
66
+ strings** — no TypeScript syntax, no `${}` template interpolation.
67
+ Change 1–2 axes decisively (palette, motion grammar, density) — never a
68
+ 10%-nudge duplicate. If the program declares `params`, prefer retuning
69
+ or extending those knobs over surgery on function strings.
70
+
71
+ 3. **Declare knobs and Looks** — the remix's control surface. 2–4
72
+ intent-named params plus 2–3 presets. This is a craft with rules:
73
+ `references/params-knobs.md`.
74
+
75
+ 4. **Check** locally before pushing:
76
+ ```bash
77
+ vos check <slug>/config.json # migrate → schema → syntax → compile → lints
78
+ vos still <slug>/config.json probe.webp --time 2 # RUN it — check compiles
79
+ # but never executes the module; a runtime
80
+ # throw (a TDZ against the function's own
81
+ # declarations) ships invisibly on check
82
+ # alone. Render a still after every code
83
+ # edit, before every push.
84
+ ```
85
+ Fix every error; treat determinism warnings seriously (a config that
86
+ renders differently per run is broken by definition).
87
+
88
+ 5. **Push** as a private vos with lineage (the CLI reads `vos.json` beside
89
+ the config for the `remixOfId` credit):
90
+ ```bash
91
+ vos push <slug>/config.json --title "Aurora Ribbons — dusk"
92
+ # → Created private vos <id>
93
+ # watch: https://vos.so/vos/<id>
94
+ # studio: https://vos.so/studio?vos=<id>
95
+ ```
96
+
97
+ 6. **Iterate** — further edits become versions of the same vos:
98
+ ```bash
99
+ vos push <slug>/config.json --vos <id> --note "cooler palette, slower drift"
100
+ ```
101
+ The directory TRACKS its vos through `vos.json`, so the base version is
102
+ carried automatically: if the human edited in the studio since your last
103
+ push, the push is rejected WITH their typed changelog. `--note` is your
104
+ handoff line — it is what the human reads first; `--label` names the
105
+ version.
106
+
107
+ 7. **Pull before every editing round** — the human may have fine-tuned:
108
+ ```bash
109
+ vos pull <slug> # what changed since your base, then sync
110
+ ```
111
+ Prints each version attributed (`v3 (studio · warmer): knob hue 0.2→0.35`
112
+ plus their notes), syncs `config.json` to the head (your previous copy is
113
+ kept as `config.backup.json`), and repoints the base. **Human-edited
114
+ nodes are protected**: a push that touches them is rejected unless you
115
+ pass `--override <id>` — do that ONLY when the user's instruction
116
+ explicitly targets that node. Their turns of your knobs are preference
117
+ data: read the changelog before deciding what to do next.
118
+
119
+ 8. **Hand back BOTH links** — the watch page (preview, knobs, code) and the
120
+ studio (`https://vos.so/studio?vos=<id>`) where the user fine-tunes your
121
+ knobs live. Publishing is their act, on vos.so.
122
+
123
+ ## Rules that keep pushes honest
124
+
125
+ - **Private is the contract.** Never try `visibility: "public"` — it clamps
126
+ to private, and publishing by key is rejected by design.
127
+ - **Quota-aware**: durable keys create ≤50 voses/24h; grants push ≤5. Iterate
128
+ versions on one vos instead of creating many voses.
129
+ - **Server-render constraints** (the preview fleet is software-GL): no
130
+ `THREE.DoubleSide` on transmission materials, no `dispersion`, and every
131
+ fetched asset must be an absolute `https` URL the fleet can reach.
132
+ - **Every knob must act.** A param the code never reads is worse than no
133
+ param — see the honesty section of `references/params-knobs.md`.
134
+ - 3D model swaps (GLB into a showcase program): `references/3d-recipe.md`.
135
+ - Full endpoint reference, quotas, and error table:
136
+ `references/remix-contract.md`.
@@ -0,0 +1,37 @@
1
+ # The 3D recipe — showcase a model
2
+
3
+ The 3D showcase programs (gallery tag `3d` — e.g. Studio Turntable, Dolly
4
+ Reveal: https://vos.so/gallery?tag=3d) are built to take a model swap.
5
+
6
+ ## Steps
7
+
8
+ 1. **Fetch** a 3d-tagged program: `vos fetch <its watch URL>`.
9
+ 2. **Point it at the model**, either way:
10
+ - Add or replace the async `setup` field to load a GLB by URL:
11
+ ```js
12
+ "setup": "async (ctx) => { const gltf = await new ctx.loaders.GLTFLoader().loadAsync('https://…/model.glb'); return { model: gltf.scene } }"
13
+ ```
14
+ then replace the `buildProduct()` body — it is commented as **THE SWAP
15
+ POINT** in these programs — with the loaded `setupData.model`,
16
+ bbox-normalized to ~1.7 units and grounded at `y = 0` (the templates
17
+ ship the normalization snippet; keep it so `scale` knobs stay
18
+ model-independent).
19
+ - Or keep the template's own product and only retune params.
20
+ 3. **The model URL must be CORS-clean and absolute `https`.** Assets
21
+ uploaded to vos.so (`POST /api/assets/upload`, browser-session auth,
22
+ `.glb`/`.gltf` ≤50MB) serve public+immutable with `ACAO: *` at
23
+ `/api/assets/{id}/file` — bake that absolute URL into the config so the
24
+ platform's preview render can fetch it. Any other host must send
25
+ `Access-Control-Allow-Origin` and be publicly reachable.
26
+ 4. **Knob honesty on GLBs**: the template's material knobs (hue, finish) do
27
+ nothing on a GLB's own materials — declare only params that act
28
+ (backdrop, light mood, camera pace, and whatever you wire yourself).
29
+ Verify per `params-knobs.md` rule 2.
30
+ 5. **Check and push** as usual: `vos check` → `vos push`. Humans do the
31
+ same flow UI-side by dropping a GLB on https://vos.so/gallery.
32
+
33
+ ## Server-render cautions (the preview fleet)
34
+
35
+ - No `THREE.DoubleSide` on transmission materials; no `dispersion`.
36
+ - Big GLBs slow the preview render — prefer draco-compressed models and
37
+ keep textures ≤2K for the showcase context.
@@ -0,0 +1,80 @@
1
+ # Params and Looks — the control surface you ship
2
+
3
+ A remix delivers output *plus the controls you considered while making it*.
4
+ The human's next instruction can be a knob turn, a prompt in knob
5
+ vocabulary, or both: human instructs → agent creates (program + knobs +
6
+ Looks) → human feels the space and settles or redirects → settled values
7
+ bake in, the surface renegotiates, repeat. **You ship an interface, not
8
+ just an artifact.**
9
+
10
+ ## Format
11
+
12
+ ```json
13
+ "params": [{ "key": "hue", "label": "Hue", "kind": "number",
14
+ "min": 0, "max": 1, "default": 0.6,
15
+ "hint": "Shifts every color around the wheel",
16
+ "unit": "°", "group": "Color", "order": 1 }]
17
+ ```
18
+
19
+ - kinds: `number` (min/max/step) | `color` | `select` (options) | `toggle`
20
+ - `hint`: ONE sentence on what visibly changes — always write it
21
+ - `unit` shows inside the number field (`px` `%` `s` `×` `°`);
22
+ `group`/`order` cluster related knobs into their own panel card
23
+ - The program reads each key from `ctx.data` where it animates:
24
+ ```js
25
+ const d = ctx.data || {}
26
+ if (typeof d.hue === 'number') u.uHue.value = d.hue
27
+ ```
28
+
29
+ Looks are named points in param space — free variants (one program, three
30
+ directions, zero extra render cost):
31
+
32
+ ```json
33
+ "presets": [{ "name": "Vivid", "values": { "hue": 0.9, "speed": 2 } },
34
+ { "name": "Calm", "values": { "hue": 0.3, "speed": 0.6 } }]
35
+ ```
36
+
37
+ Values must reference declared param keys with matching types (anything
38
+ else is dropped on save; max 8 Looks, names ≤24 chars). Ship 2–3 Looks
39
+ whenever the program has 4+ params.
40
+
41
+ ## The five rules
42
+
43
+ 1. **The knob budget is a negotiation, not an accumulation.** Curate 2–4
44
+ knobs (the schema caps at 12). A knob earns its slot by being touched or
45
+ asked about. **Collapse**: when the human settles a value and stops
46
+ touching it, bake it into the code as the new default and free the slot —
47
+ the surface is a cache of live decisions, not an archive. **Promotion**:
48
+ recurring prompt themes become knobs ("you keep circling pacing — I
49
+ added `pace`").
50
+ 2. **Knob honesty is renderable.** Every knob must visibly change the
51
+ output. Verify: render frames at several points of the knob's range and
52
+ compare — min, MIDPOINT, max for numbers. The midpoint matters:
53
+ circular quantities (hue is the classic) look identical at the two range
54
+ ends (±half a turn is the same rotation), so a min-vs-max check calls a
55
+ working knob dead. If the best pair is near-identical, the knob is dead;
56
+ wire it or drop it. Fake agency poisons the whole paradigm.
57
+ 3. **Respect human-set values.** A value the human adjusted is theirs — when
58
+ you regenerate code, preserve what each knob does. Renaming or retiring
59
+ a knob is something you SAY in the iteration note ("replaced `speed`
60
+ with `tempo`"), never something that happens silently. This rule is
61
+ ENFORCED: the platform rejects a push that touches nodes a studio
62
+ version edited since your last push, until you pass `--overrides` — the
63
+ consent switch for "the user asked me to change exactly this".
64
+ 4. **Name knobs by intent, not implementation** — `mood`, `drama`, `pace`;
65
+ never `blurRadius`. The knob is language between two authors; its name
66
+ becomes the vocabulary of the next prompt.
67
+ 5. **State the boundary.** Knobs cover the aesthetic space (continuous or
68
+ enumerable dimensions). Structural change — a new scene, a new object, a
69
+ different narrative — stays prompt territory. Knobs are for feeling;
70
+ prompts are for asking.
71
+
72
+ ## Craft notes
73
+
74
+ - A `select` of curated combinations often beats three independent numbers
75
+ (e.g. `mood: dusk | noon | neon` driving palette + light together).
76
+ - Defaults must reproduce the pushed output exactly: knobs at defaults =
77
+ the video you shipped.
78
+ - On GLB/model swaps, template material knobs usually stop acting (the
79
+ model brings its own materials) — see `3d-recipe.md`; declare only knobs
80
+ you have verified act.
@@ -0,0 +1,81 @@
1
+ # The remix contract (HTTP)
2
+
3
+ Mirror of https://vos.so/llms-remix.txt — the canonical, always-current
4
+ copy. Everything the CLI's `fetch`/`check`/`push` do rides these endpoints;
5
+ use them directly when the CLI is unavailable.
6
+
7
+ ## Auth
8
+
9
+ Two credential shapes; NEVER print either.
10
+
11
+ 1. **Remix grant** (`vos_rg_…`) — ephemeral, zero-setup. The watch page's
12
+ "remix with your agent" prompt embeds one. Grants expire in 24h and are
13
+ bound to ONE source vos: they can only POST remixes of it (`remixOfId`
14
+ required, max 5 pushes), iterate the voses they created, and read those.
15
+ 2. **Durable content key** (`vos_sk_…`) — minted at https://vos.so/app/api.
16
+ Resolution order before asking the user: `VOS_API_KEY` env, then
17
+ `~/.config/vos/credentials` (first line). If the user pastes a key into
18
+ chat, use it — and suggest rotating it afterward.
19
+
20
+ ```
21
+ Authorization: Bearer <token>
22
+ ```
23
+
24
+ Both shapes can create/read/iterate voses. They can NEVER publish
25
+ (`visibility: "public"` is clamped or rejected — publishing stays a human
26
+ act on vos.so) and never touch the render API. Durable-key creates are
27
+ quota'd (50/24h).
28
+
29
+ ## Endpoints
30
+
31
+ | Call | What |
32
+ | --- | --- |
33
+ | `GET /api/vos/{id}` | metadata: title, slug, params, `contentUrls`, `forkedFrom`, `currentVersionId` |
34
+ | `GET /api/vos/{id}/config` | `{ "config": VosConfigJson }` — the raw stored config, params included |
35
+ | `POST /api/vos` | create: `{ title, slug, visibility: "private", config, remixOfId? }` → `201 { vos: { id, … } }`; preview render auto-queues |
36
+ | `POST /api/vos/{id}/versions` | iterate: `{ config, baseVersionId?, label?, note?, overrides? }` → `201 { version: { id, versionNumber } }`; stale base or protected nodes → 409 (see Errors) |
37
+ | `GET /api/vos/{id}/changes?since={versionId}` | owner-only: the typed changelog after your base — attributed versions (origin/label/note), semantic `summary` + ops per version, and the `protected` human-edited node set |
38
+ | `PATCH /api/vos/{id}` | `{ title \| config \| tags \| visibility: "private"\|"unlisted" }` |
39
+ | `POST /api/claim` | **no auth** — the claimable push: `{ title, slug?, config }` → `201 { claimUrl, expiresAt, vos: { id } }`. Programs only, config ≤200KB, 5/24h per network. The claimUrl goes to the user and nowhere else; unclaimed work is deleted after 72h |
40
+
41
+ Base URL `https://vos.so`. `slug` is `[a-z0-9-]`, unique per user, ≤50
42
+ chars. `remixOfId` stamps the "remixed from" credit — always set it when
43
+ the work started from someone's program.
44
+
45
+ ## Config rules (enforced server-side)
46
+
47
+ - Function fields are JS **strings**: no TypeScript syntax; escape
48
+ backticks/`${}` that must survive into output.
49
+ - `createContent` returns `{ objects, refs, dispose }`; `createTimeline`
50
+ reaches custom handles via `content.refs.<x>`.
51
+ - Server-render constraints: no `THREE.DoubleSide` on transmission
52
+ materials, no `dispersion`, assets by absolute `https` URL only.
53
+ - The platform compiles on every push — a push that 400s with a compile
54
+ error was going to render nothing; fix and re-push. `vos check` runs the
55
+ same compiler locally first.
56
+
57
+ ## Params and presets (preserved by contract)
58
+
59
+ `config.params` (max 12 declared; curate 2–4) and `config.presets` (max 8,
60
+ names ≤24 chars) survive every create/version/PATCH — the platform
61
+ re-attaches them even though its schema strips unknown fields. Format and
62
+ craft rules: `params-knobs.md`.
63
+
64
+ ## Hand-back
65
+
66
+ Give the user both URLs every time:
67
+
68
+ - watch: `https://vos.so/vos/{id}` — preview, knobs, the program
69
+ - studio: `https://vos.so/studio?vos={id}` — live fine-tuning of your knobs
70
+
71
+ ## Errors
72
+
73
+ | Status | Meaning | Do |
74
+ | --- | --- | --- |
75
+ | 400 | Invalid input / Failed to compile config | fix locally (`vos check`), re-push |
76
+ | 401 | Invalid or revoked key | re-resolve credentials; ask the user |
77
+ | 403 | missing `content` scope · cannot publish · grant off its source vos | respect the boundary; don't retry |
78
+ | 409 | slug exists (on create) | pick another slug (the CLI retries derived slugs automatically) |
79
+ | 409 | `stale_base` (on versions) | the platform copy moved since your base — the body EMBEDS the typed changes you missed; `vos pull`, re-apply, push with the fresh base |
80
+ | 409 | `protected_conflict` (on versions) | your push touches nodes a studio version edited since your last push — keep the human's values, or include the listed ids in `overrides` ONLY when the user asked for that change |
81
+ | 429 | 50/24h create quota, or grant's 5-push cap | iterate versions instead of creating; or wait |
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/plugin/pace.ts"],"sourcesContent":["/**\n * The recorder's pace — the pure parts.\n *\n * A take of a scripted flow should run as long as the script asks, plus the\n * gestures a person makes between the asks (the pointer's travel, the press,\n * the settle after it) and no more. Measured before this module (§4.5 of\n * the utility-clip retrospective), hovers ran 2× their ask, drags 4.8× and\n * a 25 s script recorded 42 s, for two reasons the numbers here answer:\n *\n * - The motion loops counted STEPS (one per 16 ms of the asked duration)\n * and awaited a `mouse.move` round trip per step, so a page that\n * re-renders per move (a slider) stretched every gesture by the round\n * trip. A gesture is now driven by the CLOCK: its position is a function\n * of the elapsed time, and it ends when the asked duration has elapsed,\n * however many samples the page allowed.\n * - The settles after a gesture were fixed sleeps (500 ms after a click,\n * 250 after every selector lookup). A settle is DATA: `ms` on the step\n * overrides a small default, and the script's own `wait` steps carry\n * the intended pauses.\n */\n\n/** Pointer travel: ms per CSS px, floor and ceiling — a person's hand. */\nexport const POINTER_MS_PER_PX = 0.7\nexport const POINTER_MIN_MS = 250\nexport const POINTER_MAX_MS = 800\n/** The sample cadence a motion loop aims for. */\nexport const MOTION_TICK_MS = 16\n\n/** The settle after a gesture when the step names none, by verb. */\nexport const SETTLE_MS = {\n click: 150,\n type: 150,\n scroll: 200,\n drag: 80,\n} as const\n\n/** The press: the pause before the button goes down, and the hold. */\nexport const PRESS_LEAD_MS = 80\nexport const PRESS_HOLD_MS = 70\n/** The pause between a selector lookup that scrolled the page and the move. */\nexport const SCROLL_SETTLE_MS = 120\n/** The hold after the last step, so the take does not cut on a press. */\nexport const TRAILING_HOLD_MS = 400\n\n/** How long the pointer takes to travel `dist` CSS px. */\nexport function pointerTravelMs(dist: number): number {\n return Math.min(\n POINTER_MAX_MS,\n Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX)),\n )\n}\n\n/** The settle after a step: its own `ms` when it names one, else the verb's. */\nexport function settleMs(step: { do: string; ms?: number }): number {\n if (typeof step.ms === 'number' && step.ms >= 0) return step.ms\n const d = step.do as keyof typeof SETTLE_MS\n return SETTLE_MS[d] ?? 0\n}\n\nexport const easeInOutCubic = (u: number): number =>\n u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2\n\n/**\n * Drive a motion by the clock: `at(u)` is called with the eased progress\n * for each sample, and the loop ends when `dur` ms have elapsed, with the\n * last call at u = 1 exactly. Between samples it sleeps to the next tick\n * only when the sample came back early; a slow sample (a page busy\n * re-rendering) is followed at once, so the gesture keeps its length and\n * loses samples, never the other way round.\n */\nexport async function clockMotion(\n dur: number,\n at: (u: number) => Promise<void>,\n clock: { now: () => number; sleep: (ms: number) => Promise<void> },\n): Promise<number> {\n const start = clock.now()\n let samples = 0\n for (;;) {\n const elapsed = clock.now() - start\n if (elapsed >= dur) break\n await at(easeInOutCubic(Math.min(1, elapsed / dur)))\n samples++\n const next =\n start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS\n const wait = Math.min(next, start + dur) - clock.now()\n if (wait > 0) await clock.sleep(wait)\n }\n await at(1)\n return samples + 1\n}\n\n/**\n * Typing paced by the clock: the i-th character is due at `start + i·delay`;\n * after each keystroke the loop sleeps to the next due time only if the\n * keystroke came back early. A slow field (an editor re-rendering per key)\n * types as fast as it can and says so in the pace report.\n */\nexport async function clockTyping(\n chars: readonly string[],\n delay: number,\n type: (ch: string, index: number) => Promise<void>,\n clock: { now: () => number; sleep: (ms: number) => Promise<void> },\n): Promise<void> {\n const start = clock.now()\n for (let i = 0; i < chars.length; i++) {\n await type(chars[i], i)\n const due = start + (i + 1) * delay\n const wait = due - clock.now()\n if (wait > 0 && i < chars.length - 1) await clock.sleep(wait)\n }\n}\n\n/** One step's pace: what the script asked for it and what it took. */\nexport interface StepPace {\n step: number\n do: string\n /** The script's own ask: a wait's ms, a hover's dwell, a drag's ms, typing's chars × delay; 0 for a click or a scroll. */\n askedMs: number\n /** The gesture the recorder adds by design: pointer travel, the press, the settle. */\n gestureMs: number\n wallMs: number\n}\n\nexport interface PaceReport {\n askedMs: number\n gestureMs: number\n wallMs: number\n /** Wall time neither asked nor a gesture: what the page and the round trips cost. */\n overheadMs: number\n overheadPct: number\n /** The steps whose wall ran past their ask plus gesture by more than a third. */\n slow: { step: number; do: string; askedMs: number; wallMs: number }[]\n}\n\n/** What a script asks of a step, in ms: the part of its wall time that is the author's. */\nexport function askedMs(step: {\n do: string\n ms?: number\n text?: string\n delayMs?: number\n}): number {\n switch (step.do) {\n case 'wait':\n return step.ms ?? 0\n case 'hover':\n return step.ms ?? 700\n case 'drag':\n return step.ms ?? 700\n case 'type':\n return (step.text?.length ?? 0) * (step.delayMs ?? 40)\n default:\n return 0\n }\n}\n\nexport function paceReport(steps: readonly StepPace[]): PaceReport {\n const askedMs = steps.reduce((a, s) => a + s.askedMs, 0)\n const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0)\n const wallMs = steps.reduce((a, s) => a + s.wallMs, 0)\n const overheadMs = Math.max(0, wallMs - askedMs - gestureMs)\n const slow = steps\n .filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60)\n .map((s) => ({\n step: s.step,\n do: s.do,\n askedMs: s.askedMs,\n wallMs: s.wallMs,\n }))\n return {\n askedMs,\n gestureMs,\n wallMs,\n overheadMs,\n overheadPct: wallMs > 0 ? Math.round((overheadMs / wallMs) * 100) : 0,\n slow,\n }\n}\n\n/** The pace in one line for the log. */\nexport function paceLine(r: PaceReport): string {\n const s = (ms: number) => `${(ms / 1000).toFixed(1)} s`\n const slow = r.slow.length\n ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(', ')}`\n : ''\n return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`\n}\n\n/**\n * DEAD TIME, the smoothness fact that replaces the freeze share as the\n * warning. A still frame is not a defect: a page the viewer is reading is\n * content, and a workspace tour is mostly still. What is dead is a still\n * frame under a PARKED cursor past the beat it takes to read what changed:\n * after that the picture, the pointer and the camera all hold and nothing\n * is being said. So the derivation reads, per step, when the page settled\n * after the step's act, how long the settled frame was held, and whether\n * the cursor moved in that hold; a hold past the reading beat is dead.\n *\n * The beat is longer after a page changed (a navigation shows a whole new\n * page to take in) than after a control changed (one thing to see).\n */\nexport const READ_BEAT_MS = { page: 1000, control: 600 } as const\n/** A dead stretch this long is a warning on its own, whatever the share. */\nexport const DEAD_STRETCH_WARN_MS = 1500\n/** The share of the take past which dead time is a warning. */\nexport const DEAD_SHARE_WARN_PCT = 20\n\nexport interface DeadStep {\n step: number\n id?: string\n do: string\n /** ms from the step's act (its press, else its start) to the last frame change in the step. */\n settledMs: number\n /** ms the settled frame was held before the step ended. */\n heldMs: number\n /** the hold past the reading beat, while the cursor was parked; 0 when the hold was read or the pointer moved. */\n deadMs: number\n}\n\nexport interface DeadReport {\n ms: number\n pct: number\n /** the steps that carried any dead time, in order. */\n steps: DeadStep[]\n /** the longest single dead hold. */\n longestMs: number\n}\n\nexport function deadTime(\n steps: readonly {\n step: number\n id?: string\n do: string\n tStart: number\n tEnd: number\n navigated?: boolean\n }[],\n frames: readonly { tMs: number }[],\n events: readonly { t: number; type: string }[],\n durationMs: number,\n): DeadReport {\n const out: DeadStep[] = []\n let ms = 0\n let longestMs = 0\n for (const s of steps) {\n const t0 = s.tStart * 1000\n const t1 = s.tEnd * 1000\n if (!(t1 > t0)) continue\n // The act: the step's first press, else its start (a wait, a move).\n const press = events.find(\n (e) => e.type === 'down' && e.t >= t0 && e.t <= t1,\n )\n const act = press ? press.t : t0\n // The last visual change inside the step, after the act.\n let last = act\n for (const f of frames) {\n if (f.tMs > act && f.tMs <= t1) last = f.tMs\n }\n const settledMs = Math.round(last - act)\n const heldMs = Math.round(t1 - last)\n // Parked: no pointer motion while the settled frame was held.\n const moved = events.some(\n (e) =>\n (e.type === 'move' || e.type === 'scroll') && e.t > last && e.t <= t1,\n )\n // The opening step shows a page the viewer has not seen: a page beat.\n const beat =\n s.navigated || s.step === 0 ? READ_BEAT_MS.page : READ_BEAT_MS.control\n const deadMs = moved ? 0 : Math.max(0, heldMs - beat)\n if (deadMs > 0) {\n out.push({\n step: s.step,\n ...(s.id ? { id: s.id } : {}),\n do: s.do,\n settledMs,\n heldMs,\n deadMs,\n })\n ms += deadMs\n longestMs = Math.max(longestMs, deadMs)\n }\n }\n return {\n ms,\n pct: durationMs > 0 ? Math.round((ms / durationMs) * 100) : 0,\n steps: out,\n longestMs,\n }\n}\n\n/** Whether a dead report earns a warning: the share, or one long hold. */\nexport function deadWarns(r: DeadReport): boolean {\n return r.pct >= DEAD_SHARE_WARN_PCT || r.longestMs >= DEAD_STRETCH_WARN_MS\n}\n\n/** The dead time in words, naming the steps and what to cut. */\nexport function deadLine(r: DeadReport): string {\n const s = (ms: number) => `${(ms / 1000).toFixed(1)} s`\n if (!r.steps.length) return 'dead time: none'\n const parts = r.steps.map(\n (d) =>\n `#${d.step}${d.id ? ` (${d.id})` : ''} ${d.do} held ${s(d.heldMs)} after it settled, ${s(d.deadMs)} past the beat`,\n )\n return `dead time: ${s(r.ms)} (${r.pct} %), a still frame under a parked cursor past the reading beat: ${parts.join('; ')}. Cut those steps' ms; a hold is what it takes to read what changed.`\n}\n"],"mappings":"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","names":["askedMs"]}