@vibeo/cli 0.3.4 → 0.4.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.
@@ -0,0 +1,442 @@
1
+ # Vibeo Core (`@vibeo/core`)
2
+
3
+ ## Overview
4
+
5
+ `@vibeo/core` is the foundation of the Vibeo video framework. It provides the timing engine, React context providers, hooks for frame-based animation, the interpolation engine, and the core components (`Composition`, `Sequence`, `Loop`) used to structure video timelines.
6
+
7
+ **When to use**: Any time you are building a Vibeo video project. This package is always required.
8
+
9
+ ---
10
+
11
+ ## API Reference
12
+
13
+ ### Components
14
+
15
+ #### `Composition<T>`
16
+ Registers a video composition with the framework.
17
+
18
+ ```tsx
19
+ <Composition
20
+ id="MyVideo"
21
+ component={MyScene}
22
+ width={1920}
23
+ height={1080}
24
+ fps={30}
25
+ durationInFrames={300}
26
+ defaultProps={{ title: "Hello" }}
27
+ calculateMetadata={async (props) => ({ durationInFrames: 600 })}
28
+ />
29
+ ```
30
+
31
+ **Props** (`CompositionProps<T>`):
32
+ | Prop | Type | Description |
33
+ |------|------|-------------|
34
+ | `id` | `string` | Unique identifier for the composition |
35
+ | `component` | `ComponentType<T>` | The React component to render |
36
+ | `width` | `number` | Width in pixels |
37
+ | `height` | `number` | Height in pixels |
38
+ | `fps` | `number` | Frames per second |
39
+ | `durationInFrames` | `number` | Total duration in frames |
40
+ | `defaultProps?` | `T` | Default props passed to the component |
41
+ | `calculateMetadata?` | `(props: T) => Promise<Partial<VideoConfig>>` | Async function to compute metadata dynamically |
42
+
43
+ #### `Sequence`
44
+ Time-shifts its children on the timeline. Supports arbitrary nesting with cumulative offsets.
45
+
46
+ ```tsx
47
+ <Sequence from={30} durationInFrames={60} name="Scene2">
48
+ <MyComponent />
49
+ </Sequence>
50
+ ```
51
+
52
+ **Props**:
53
+ | Prop | Type | Default | Description |
54
+ |------|------|---------|-------------|
55
+ | `from?` | `number` | `0` | Frame offset where this sequence begins |
56
+ | `durationInFrames?` | `number` | `Infinity` | How many frames this sequence lasts |
57
+ | `name?` | `string` | — | Debug label |
58
+ | `layout?` | `"none" \| "absolute-fill"` | `"absolute-fill"` | CSS layout mode |
59
+
60
+ Inside a Sequence, `useCurrentFrame()` returns the **relative** frame (i.e., frame 0 is when the Sequence starts).
61
+
62
+ #### `Loop`
63
+ Repeats its children for a given duration.
64
+
65
+ ```tsx
66
+ <Loop durationInFrames={30} times={4}>
67
+ <PulsingDot />
68
+ </Loop>
69
+ ```
70
+
71
+ **Props**:
72
+ | Prop | Type | Default | Description |
73
+ |------|------|---------|-------------|
74
+ | `durationInFrames` | `number` | — | Duration of one loop iteration |
75
+ | `times?` | `number` | `Infinity` | Number of loop iterations |
76
+ | `layout?` | `"none" \| "absolute-fill"` | — | CSS layout mode |
77
+
78
+ #### `VibeoRoot`
79
+ Top-level context provider tree. Wrap your composition registration in this.
80
+
81
+ ```tsx
82
+ <VibeoRoot>
83
+ <Composition id="MyVideo" ... />
84
+ </VibeoRoot>
85
+ ```
86
+
87
+ ### Hooks
88
+
89
+ #### `useCurrentFrame(): number`
90
+ Returns the current frame number **relative to the nearest parent Sequence**. This is the primary hook for animation.
91
+
92
+ #### `useVideoConfig(): VideoConfig`
93
+ Returns `{ width, height, fps, durationInFrames }` for the current composition.
94
+
95
+ #### `useTimelinePosition(): number`
96
+ Returns the **absolute** frame number (ignores Sequence offsets). Rarely needed directly.
97
+
98
+ #### `useSequenceContext(): SequenceContextType | null`
99
+ Returns the current Sequence context, or `null` if not inside a Sequence.
100
+
101
+ #### `useLoopContext(): LoopContextType | null`
102
+ Returns `{ iteration, durationInFrames }` for the current loop, or `null`.
103
+
104
+ ### Interpolation
105
+
106
+ #### `interpolate(input, inputRange, outputRange, options?): number`
107
+ The core animation primitive. Maps an input value through input/output ranges with easing.
108
+
109
+ ```ts
110
+ const opacity = interpolate(frame, [0, 30], [0, 1]);
111
+ const x = interpolate(frame, [0, 15, 30], [0, 200, 0], { easing: easeInOut });
112
+ ```
113
+
114
+ **Parameters**:
115
+ | Param | Type | Description |
116
+ |-------|------|-------------|
117
+ | `input` | `number` | The current value (usually the frame) |
118
+ | `inputRange` | `readonly number[]` | Breakpoints for the input |
119
+ | `outputRange` | `readonly number[]` | Corresponding output values |
120
+ | `options?` | `InterpolateOptions` | Easing and extrapolation config |
121
+
122
+ **`InterpolateOptions`**:
123
+ | Field | Type | Default | Description |
124
+ |-------|------|---------|-------------|
125
+ | `easing?` | `(t: number) => number` | `linear` | Easing function |
126
+ | `extrapolateLeft?` | `ExtrapolateType` | `"extend"` | Behavior below input range |
127
+ | `extrapolateRight?` | `ExtrapolateType` | `"extend"` | Behavior above input range |
128
+
129
+ **`ExtrapolateType`**: `"clamp" | "extend" | "identity"`
130
+
131
+ ### Easing Functions
132
+
133
+ ```ts
134
+ import { linear, easeIn, easeOut, easeInOut, bezier, steps } from "@vibeo/core";
135
+ ```
136
+
137
+ | Function | Description |
138
+ |----------|-------------|
139
+ | `linear(t)` | No easing, linear progression |
140
+ | `easeIn(t)` | Cubic ease-in (t^3) |
141
+ | `easeOut(t)` | Cubic ease-out |
142
+ | `easeInOut(t)` | Cubic ease-in-out |
143
+ | `bezier(x1, y1, x2, y2)` | Returns a custom cubic bezier easing function |
144
+ | `steps(n)` | Returns a step function with `n` discrete steps |
145
+
146
+ ### Timing Utilities
147
+
148
+ ```ts
149
+ import { msPerFrame, frameToTime, timeToFrame, getMediaTime } from "@vibeo/core";
150
+ ```
151
+
152
+ | Function | Signature | Description |
153
+ |----------|-----------|-------------|
154
+ | `msPerFrame` | `(fps) => number` | Milliseconds per frame: `1000 / fps` |
155
+ | `frameToTime` | `(frame, fps) => number` | Frame to seconds: `frame * msPerFrame(fps) / 1000` |
156
+ | `timeToFrame` | `(time, fps) => number` | Seconds to frame: `floor(time * fps)` |
157
+ | `getMediaTime` | `(frame, fps, playbackRate, startFrom) => number` | Compute media time accounting for playback rate |
158
+
159
+ ### Other Utilities
160
+
161
+ #### `calculateMediaDuration(options): number`
162
+ Computes the actual duration of media in frames, accounting for trim and playback rate.
163
+
164
+ #### `validateVideoConfig(config): void`
165
+ Validates a `VideoConfig` object, throwing on invalid values.
166
+
167
+ ### Types
168
+
169
+ ```ts
170
+ import type {
171
+ VideoConfig,
172
+ CompositionProps,
173
+ SequenceContextType,
174
+ LoopContextType,
175
+ ExtrapolateType,
176
+ InterpolateOptions,
177
+ } from "@vibeo/core";
178
+ ```
179
+
180
+ ### Context Providers (advanced)
181
+
182
+ - `TimelineProvider` / `TimelineContext` / `useTimelineContext` — manages the global frame state
183
+ - `CompositionProvider` / `CompositionContext` / `useCompositionContext` — manages composition registration
184
+ - `SequenceContext` / `useSequenceContext` — tracks cumulative Sequence offsets
185
+ - `LoopContext` / `useLoopContext` — tracks loop iteration state
186
+
187
+ ---
188
+
189
+ ## Key Timing Math
190
+
191
+ ### Frame <-> Time
192
+ ```
193
+ msPerFrame = 1000 / fps
194
+ timeInSeconds = frame * msPerFrame / 1000
195
+ frameFromTime = floor(timeInSeconds * fps)
196
+ ```
197
+
198
+ ### Media Time with Playback Rate
199
+ ```
200
+ mediaTime(frame, fps, playbackRate, startFrom) =
201
+ interpolate(frame, [-1, startFrom, startFrom+1], [-1, startFrom, startFrom+playbackRate]) * (1000/fps) / 1000
202
+ ```
203
+
204
+ ### Sequence Relative Frame
205
+ ```
206
+ relativeFrame = absoluteFrame - (cumulatedFrom + relativeFrom)
207
+ ```
208
+
209
+ ### Loop Iteration
210
+ ```
211
+ iteration = floor(currentFrame / durationInFrames)
212
+ loopFrame = currentFrame % durationInFrames
213
+ ```
214
+
215
+ ### Interpolation Engine
216
+ ```
217
+ 1. Find segment: which adjacent pair in inputRange brackets input
218
+ 2. Normalize: t = (input - inputRange[i]) / (inputRange[i+1] - inputRange[i])
219
+ 3. Ease: t = easing(t)
220
+ 4. Scale: output = t * (outputRange[i+1] - outputRange[i]) + outputRange[i]
221
+ 5. Extrapolate: clamp | extend | identity beyond range ends
222
+ ```
223
+
224
+ ### Media Duration with Trim & Playback Rate
225
+ ```
226
+ duration = trimAfter ?? totalDurationInFrames
227
+ duration -= trimBefore ?? 0
228
+ actualDuration = floor(duration / playbackRate)
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Platform Format Presets
234
+
235
+ Use these when the user mentions a platform. **Any time the user says "Short", "Reel", "TikTok", or "vertical" — use 1080x1920 (9:16), not landscape.**
236
+
237
+ | Format | Width | Height | FPS | Max Duration | Aliases |
238
+ |--------|-------|--------|-----|-------------|---------|
239
+ | **YouTube** | 1920 | 1080 | 30 | — | landscape, standard |
240
+ | **YouTube 4K** | 3840 | 2160 | 30-60 | — | 4K |
241
+ | **YouTube Short** | 1080 | 1920 | 30-60 | 3 min | vertical |
242
+ | **TikTok** | 1080 | 1920 | 30 | 10 min | |
243
+ | **Instagram Reel** | 1080 | 1920 | 30 | 20 min | |
244
+ | **Instagram Post** | 1080 | 1080 | 30 | 60s | square |
245
+ | **Twitter/X** | 1920 | 1080 | 30 | 2m 20s | 512MB max |
246
+ | **Twitter/X Short** | 1080 | 1920 | 30 | 2m 20s | vertical tweet |
247
+
248
+ ### Vertical video (9:16) layout tips
249
+
250
+ - **Code blocks**: max ~900px wide, font size 24-28 (larger than landscape)
251
+ - **No side-by-side**: use top/bottom stacks, not left/right splits
252
+ - **Text**: minimum 36px body, 64px+ titles
253
+ - **Safe zones**: avoid top 100px (status bar) and bottom 150px (nav gestures)
254
+ - **Single focus**: one idea per screen, no multi-column layouts
255
+
256
+ ---
257
+
258
+ ## Common Patterns
259
+
260
+ ### Multi-file project structure (use for 3+ scenes)
261
+
262
+ ```
263
+ src/
264
+ ├── index.tsx # Root + Composition registration
265
+ ├── Video.tsx # Scene orchestrator (Sequences)
266
+ ├── scenes/
267
+ │ ├── Intro.tsx
268
+ │ ├── Problem.tsx
269
+ │ ├── Solution.tsx
270
+ │ └── Outro.tsx
271
+ └── components/
272
+ ├── CodeBlock.tsx
273
+ └── AnimatedCard.tsx
274
+ ```
275
+
276
+ ### Centralized scene timing (best practice)
277
+
278
+ Define all timing in one place — never hardcode frame numbers in `<Sequence>`:
279
+
280
+ ```tsx
281
+ const SCENES = {
282
+ intro: { from: 0, duration: 120 },
283
+ problem: { from: 120, duration: 300 },
284
+ solution: { from: 420, duration: 450 },
285
+ outro: { from: 870, duration: 90 },
286
+ } as const;
287
+
288
+ const TOTAL = SCENES.outro.from + SCENES.outro.duration;
289
+
290
+ function MyVideo() {
291
+ return (
292
+ <>
293
+ <Sequence from={SCENES.intro.from} durationInFrames={SCENES.intro.duration}>
294
+ <IntroScene />
295
+ </Sequence>
296
+ <Sequence from={SCENES.problem.from} durationInFrames={SCENES.problem.duration}>
297
+ <ProblemScene />
298
+ </Sequence>
299
+ {/* ... */}
300
+ </>
301
+ );
302
+ }
303
+ ```
304
+
305
+ ### Looping/pulsing animation
306
+
307
+ Use `frame % N` for repeating effects (pulse, glow, rotate):
308
+
309
+ ```tsx
310
+ const frame = useCurrentFrame();
311
+ const pulse = interpolate(frame % 60, [0, 30, 60], [0.3, 0.6, 0.3]);
312
+ const rotation = (frame % 90) * 4; // 360° every 3 seconds
313
+ ```
314
+
315
+ ### Staggered list/card animation
316
+
317
+ Animate N items with increasing delay:
318
+
319
+ ```tsx
320
+ const frame = useCurrentFrame();
321
+ const items = ["Feature 1", "Feature 2", "Feature 3"];
322
+
323
+ {items.map((item, i) => {
324
+ const delay = 10 + i * 8;
325
+ const opacity = interpolate(frame, [delay, delay + 20], [0, 1], {
326
+ extrapolateLeft: "clamp", extrapolateRight: "clamp",
327
+ });
328
+ const y = interpolate(frame, [delay, delay + 20], [30, 0], {
329
+ easing: easeOut, extrapolateLeft: "clamp", extrapolateRight: "clamp",
330
+ });
331
+ return <div key={i} style={{ opacity, transform: `translateY(${y}px)` }}>{item}</div>;
332
+ })}
333
+ ```
334
+
335
+ ### Overlapping Sequences for manual transitions
336
+
337
+ Alternative to `<Transition>` when you need custom per-scene blend control:
338
+
339
+ ```tsx
340
+ <Sequence from={0} durationInFrames={90}>
341
+ <SceneA /> {/* fade out in last 15 frames */}
342
+ </Sequence>
343
+ <Sequence from={75} durationInFrames={90}>
344
+ <SceneB /> {/* fade in during first 15 frames */}
345
+ </Sequence>
346
+ ```
347
+
348
+ ### Multi-segment interpolation
349
+
350
+ ```tsx
351
+ const x = interpolate(frame, [0, 30, 60, 90], [0, 200, 0, 0]);
352
+ const y = interpolate(frame, [0, 30, 60, 90], [0, 0, 0, 200]);
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Gotchas and Tips
358
+
359
+ 1. **`useCurrentFrame()` is always relative to the nearest `<Sequence>`**. If you need the absolute frame, use `useTimelinePosition()`.
360
+
361
+ 2. **`interpolate()` requires at least 2 input range values** and input/output ranges must have the same length.
362
+
363
+ 3. **Extrapolation defaults to `"extend"`** (continues the slope beyond the range). Use `{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }` to clamp values at range boundaries.
364
+
365
+ 4. **`Sequence` children are only rendered when the absolute frame is within `[from, from + durationInFrames)`**. Outside that range, `Sequence` returns `null`.
366
+
367
+ 5. **Loop iterations**: Inside a `<Loop>`, `useCurrentFrame()` resets to 0 each iteration. Use `useLoopContext()` to get the current `iteration` number.
368
+
369
+ 6. **`calculateMetadata` is async** — use it for compositions whose duration depends on fetched data (e.g., audio file length).
370
+
371
+ 7. **All timing is frame-based, not time-based**. Convert with `frameToTime(frame, fps)` when needed.
372
+
373
+
374
+ ---
375
+
376
+ ## LLM & Agent Integration
377
+
378
+ Vibeo's CLI is built with [incur](https://github.com/wevm/incur), making it natively discoverable by AI agents and LLMs.
379
+
380
+ ### Discovering the API
381
+
382
+ ```bash
383
+ # Get a compact summary of all CLI commands (ideal for LLM system prompts)
384
+ bunx @vibeo/cli --llms
385
+
386
+ # Get the full manifest with schemas, examples, and argument details
387
+ bunx @vibeo/cli --llms-full
388
+
389
+ # Get JSON Schema for a specific command (useful for structured tool calls)
390
+ bunx @vibeo/cli render --schema
391
+ bunx @vibeo/cli create --schema
392
+ ```
393
+
394
+ ### Using as an MCP Server
395
+
396
+ ```bash
397
+ # Start Vibeo as an MCP (Model Context Protocol) server
398
+ bunx @vibeo/cli --mcp
399
+
400
+ # Register as a persistent MCP server for your agent
401
+ bunx @vibeo/cli mcp add
402
+ ```
403
+
404
+ This lets LLMs call `create`, `render`, `preview`, and `list` as structured tool calls through the MCP protocol.
405
+
406
+ ### Generating Skill Files
407
+
408
+ ```bash
409
+ # Sync skill files to your agent's skill directory
410
+ bunx @vibeo/cli skills add
411
+ ```
412
+
413
+ This generates markdown skill files that agents like Claude Code can discover and use to write Vibeo code without reading source.
414
+
415
+ ### Agent-Friendly Output
416
+
417
+ ```bash
418
+ # Output as JSON for programmatic consumption
419
+ bunx @vibeo/cli list --entry src/index.tsx --format json
420
+
421
+ # Output as YAML
422
+ bunx @vibeo/cli list --entry src/index.tsx --format yaml
423
+
424
+ # Filter output to specific keys
425
+ bunx @vibeo/cli list --entry src/index.tsx --filter-output compositions[0].id
426
+
427
+ # Count tokens in output (useful for context window planning)
428
+ bunx @vibeo/cli render --schema --token-count
429
+ ```
430
+
431
+ ### How LLMs Should Use Vibeo
432
+
433
+ 1. **Create a project**: `bunx @vibeo/cli create my-video --template basic`
434
+ 2. **Install deps**: `cd my-video && bun install`
435
+ 3. **Install Playwright** (required for render/list): `bunx playwright install chromium`
436
+ 4. **Edit `src/index.tsx`**: Write React components using `@vibeo/core` hooks and components
437
+ 5. **Preview**: `bunx @vibeo/cli preview --entry src/index.tsx`
438
+ 6. **Render**: `bunx @vibeo/cli render --entry src/index.tsx --composition MyComp`
439
+
440
+ Step 3 is mandatory — `vibeo render` and `vibeo list` will fail without Playwright browsers installed.
441
+
442
+ All commands accept `--format json` for structured output that LLMs can parse reliably.