Why
charmbracelet/vhs (19.5k stars) became a verb because of its .tape DSL — a plain-text format anyone can read and edit. No browser equivalent exists. Every competitor in this space (Screenwright, demosmith-mcp, demo-recorder) requires writing TypeScript/JavaScript shot functions. That's a barrier for the marketing/PM/founder audience who actually need demos most.
A 20-line `.demo` file should produce the same output as 60 lines of imperative JS.
What
A new file format democast accepts as input alongside the existing `democast.config.js`.
Example: `hello.demo`
```
democast 0.2 tape — Minty hello demo
Output demo.mp4
Url http://localhost:3456
Viewport 1280 720
Voice ~/.democast/voices/amy.onnx
Captions burn
Wait 1500ms
Say "Minty. Your network, unified."
Eval showView('today')
Wait 2200ms
Click .daily-focus-name
Wait 2400ms
Say "Open them. Everything they told you, in one place."
ScrollTo #facts-card-slot
Wait 2200ms
Eval showView('ask')
Type #ask-input "investors I should reach out to"
Click .ask-send-btn
Wait 2400ms
Say "Ask your network anything."
Eval showView('network')
Wait 2400ms
Say "Find clusters by company."
Eval showView('today')
Wait 1800ms
Say "All local. Open source."
```
CLI
```
democast record demo.tape # auto-detects .tape | .demo extension
democast all demo.tape
democast init --tape # scaffold a .tape instead of .config.js
```
If the user passes a `.tape` or `.demo` file, parse it. Otherwise fall back to the existing `.config.js` path. Both should remain first-class citizens.
Grammar (start small, expand later)
| Verb |
Args |
Maps to |
| `Output` |
`` |
`config.output` (or path) |
| `Url` |
`` |
`config.url` |
| `Viewport` |
` [@scale]` |
`config.viewport` |
| `Voice` |
`` |
`config.narration.voice` |
| `Captions` |
`burn` | `soft` | `srt` | `off` |
`config.narration.captions` (issue #1) |
| `Wait` |
`ms` or `s` |
`page.waitForTimeout` |
| `Say` |
`"text"` |
append a narration line at `current-video-time` |
| `Click` |
`` |
`page.click` |
| `Type` |
` "text"` |
`page.fill` |
| `Press` |
`` |
`page.keyboard.press` |
| `Eval` |
`` |
`page.evaluate(() => )` |
| `ScrollTo` |
`` |
`page.evaluate(() => el.scrollIntoView())` |
| `Set` |
`=` |
future config knobs (e.g. `Set TypingSpeed=80ms`) |
| `#` |
comment |
ignored |
The narration timing for `Say` is computed automatically: each `Say` records a line at the current cumulative wait time so far. The user doesn't have to count milliseconds; they just write the script in chronological order. (This is the actual elegance lift over the JS API.)
How
- New module `src/dsl.js` with one exported `parseTape(filepath) → config`. Pure function, no I/O outside reading the file. Fully unit-tested.
- `bin/democast.js` checks file extension on the config arg — `.tape`/`.demo` → `parseTape`, `.js`/`.config.js` → existing `require` path.
- Add an `init --tape` flag that writes `examples/hello.tape` (also create that example).
- README gets a new "Quick start" section right under the install instructions, leading with the .tape format because it's more readable.
Acceptance criteria
Out of scope
- Variables / templating (`$baseUrl`)
- Loops / conditionals
- Importing one .tape from another
- All of these can be added later without breaking the v1 grammar.
Effort estimate
~2-3 hours for an agent. The grammar is small but the parser needs care for quoted strings and helpful errors.
Why
charmbracelet/vhs (19.5k stars) became a verb because of its .tape DSL — a plain-text format anyone can read and edit. No browser equivalent exists. Every competitor in this space (Screenwright, demosmith-mcp, demo-recorder) requires writing TypeScript/JavaScript shot functions. That's a barrier for the marketing/PM/founder audience who actually need demos most.
A 20-line `.demo` file should produce the same output as 60 lines of imperative JS.
What
A new file format democast accepts as input alongside the existing `democast.config.js`.
Example: `hello.demo`
```
democast 0.2 tape — Minty hello demo
Output demo.mp4
Url http://localhost:3456
Viewport 1280 720
Voice ~/.democast/voices/amy.onnx
Captions burn
Wait 1500ms
Say "Minty. Your network, unified."
Eval showView('today')
Wait 2200ms
Click .daily-focus-name
Wait 2400ms
Say "Open them. Everything they told you, in one place."
ScrollTo #facts-card-slot
Wait 2200ms
Eval showView('ask')
Type #ask-input "investors I should reach out to"
Click .ask-send-btn
Wait 2400ms
Say "Ask your network anything."
Eval showView('network')
Wait 2400ms
Say "Find clusters by company."
Eval showView('today')
Wait 1800ms
Say "All local. Open source."
```
CLI
```
democast record demo.tape # auto-detects .tape | .demo extension
democast all demo.tape
democast init --tape # scaffold a .tape instead of .config.js
```
If the user passes a `.tape` or `.demo` file, parse it. Otherwise fall back to the existing `.config.js` path. Both should remain first-class citizens.
Grammar (start small, expand later)
The narration timing for `Say` is computed automatically: each `Say` records a line at the current cumulative wait time so far. The user doesn't have to count milliseconds; they just write the script in chronological order. (This is the actual elegance lift over the JS API.)
How
Acceptance criteria
Out of scope
Effort estimate
~2-3 hours for an agent. The grammar is small but the parser needs care for quoted strings and helpful errors.