storyshot 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boheling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,340 @@
1
- # Temporary Holding Version
1
+ # Storyshot
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ask your coding agent for a video of your web app, and get a polished 1080p
4
+ MP4: a promo clip, a narrated tutorial, a lesson, or step-by-step GIFs for your
5
+ docs.
6
+
7
+ > *"make a 15 s promo video of https://myapp.com"*
8
+
9
+ Storyshot gives Cursor, Claude Code and other coding agents what they need to
10
+ do this well. A real browser clicks through your site, and the recording gets
11
+ a smooth cursor, automatic zoom, captions, an optional AI voiceover, music
12
+ and an end card. Loading time is squeezed out, so a 30-second AI job takes a
13
+ second or two on screen.
14
+
15
+ The agent plans, writes and fixes the storyboard. Storyshot does the
16
+ recording and editing, and the same storyboard always renders the same video.
17
+
18
+ ## Quick start
19
+
20
+ Open your project in Cursor or Claude Code and paste this into the agent chat:
21
+
22
+ > Set up Storyshot in this project: run `npx -y storyshot setup cursor` (use
23
+ > `setup claude` in Claude Code) and do everything it says. Then make a 15 s
24
+ > promo video of https://myapp.com
25
+
26
+ The agent does the rest:
27
+ - `setup` writes the MCP server config and the Storyshot skill, adds
28
+ `.storyshot/` to `.gitignore`, and downloads the browser it needs.
29
+ - If ffmpeg is missing, setup prints the exact install command for your OS
30
+ (`brew install ffmpeg`, `winget install --id Gyan.FFmpeg -e`,
31
+ `sudo apt-get install -y ffmpeg`). The agent runs it after you approve.
32
+ - The agent reads the skill and starts on your video in the same chat, using
33
+ the CLI.
34
+
35
+ Reload Cursor (or restart Claude Code) when convenient. From the next chat on,
36
+ the agent uses the Storyshot MCP tools automatically, and you can just ask:
37
+
38
+ - *"make a 15 s promo video of https://myapp.com"*, or type `/storyshot`
39
+ - *"make a narrated tutorial showing how to export a report"*
40
+ - *"make a lesson video explaining how the AI editor works"*
41
+ - *"make docs GIFs for the onboarding flow"*
42
+
43
+ `npx storyshot doctor` re-checks everything at any time. Storyshot runs on
44
+ macOS, Linux and Windows.
45
+
46
+ ## What happens when you ask
47
+
48
+ 1. **The agent looks at your site** (about 5 s). It reads the page, and checks
49
+ whether the flow is behind a login and whether you have a voice API key.
50
+ 2. **It confirms the plan with you, once, before spending anything:**
51
+
52
+ ```
53
+ Here's the plan. Reply "ok", or change any line:
54
+ 1. Type: pitch: a 15–30 s promo
55
+ 2. Length: about 15 s, 6 shots
56
+ 3. Flow: hero → Examples → Try It → AI result → pricing → end card
57
+ 4. Voice: none (other options: free local English voice; ElevenLabs, key found in .env)
58
+ 5. Music: upbeat · Captions: English · Subtitle file (.srt): no
59
+ 6. Format: 16:9, 1080p
60
+ 7. Login: not needed
61
+ ```
62
+
63
+ 3. **If you need to log in**, a browser window opens and you log in there.
64
+ The agent never sees or types your password. See
65
+ [Logged-in apps](#logged-in-apps).
66
+ 4. **It writes `videos/<site>-<kind>.yaml`** (kind is promo, tutorial, lesson or docs, plus a topic for tutorials, e.g. `videos/acme-tutorial-export-pdf.yaml` → `.mp4`), using only click targets verified on
67
+ your live pages.
68
+ 5. **It renders, looks at the result, and fixes it.** Every render returns a
69
+ contact sheet with one frame per step, plus warnings: a step that is rushed
70
+ or slow, narration that doesn't fit, a caption covering what's being
71
+ clicked, a page still loading. The agent fixes these and re-renders. When
72
+ only text or timing changed, it re-renders in seconds without re-recording.
73
+ 6. **It hands you the MP4**, a shot list, and what was sped up. Depending on
74
+ the type, you also get `.srt` subtitles, YouTube chapters, or per-step
75
+ GIFs.
76
+
77
+ The storyboard is a plain file in your repo. Commit it, edit it, and ask the
78
+ agent for changes later ("make step 3 slower", "add a Chinese voiceover").
79
+ After your next release, re-render it and you have an up-to-date video.
80
+
81
+ ## What you can make
82
+
83
+ | ask for | type (`purpose`) | you get |
84
+ |---|---|---|
85
+ | a promo / launch / landing-page clip | `pitch` | 15–30 s, music, captions, tightly cut |
86
+ | a how-to or onboarding video | `tutorial` | narrated, a spotlight on each click, numbered steps, soft music, `.srt` + chapters |
87
+ | a lesson or explainer | `course` | narrated and explains the *why*, no music, `.srt` + chapters |
88
+ | help-center snippets | `docs` | silent, one GIF per step |
89
+
90
+ Any type can also be vertical (9:16) or in another language.
91
+
92
+ ## Voice and cost
93
+
94
+ Rendering is free and runs locally. Only the voiceover can cost money:
95
+
96
+ | voice | cost | notes |
97
+ |---|---|---|
98
+ | none | free | captions only |
99
+ | `kokoro` | free | local, English only; downloads a ~90 MB model on first use |
100
+ | ElevenLabs | your credits | the most natural voice; many languages including Chinese. Put `ELEVENLABS_API_KEY` in `.env`. |
101
+ | OpenAI | your credits | `OPENAI_API_KEY` |
102
+
103
+ With a paid voice, the agent iterates with the free voice (draft mode) and
104
+ only renders the paid voice once the video looks right. Each line is cached,
105
+ so a change only pays for the lines that changed.
106
+
107
+ ## Logged-in apps
108
+
109
+ When the flow is behind a login, the agent says so in its plan. Then:
110
+
111
+ 1. A browser window opens (the `login` tool, or `storyshot login <url>`).
112
+ 2. You log in there. 2FA and "Sign in with Google" work.
113
+ 3. The session is saved to `videos/<site>-login.json` once you're logged in, or
114
+ when you close the window. It includes cookies, localStorage and IndexedDB,
115
+ and the file is added to `.gitignore` because it holds your session.
116
+ 4. Every recording starts logged in, so the video never shows a login screen.
117
+
118
+ If the session expires, Storyshot says so ("the saved login has expired, run
119
+ login again") instead of failing in a confusing way. Use a demo account: the
120
+ video shows whatever is on screen.
121
+
122
+ ## For agents: tools and playbook
123
+
124
+ The playbook is [`skills/storyshot/SKILL.md`](skills/storyshot/SKILL.md). Agents
125
+ without skill support get the same text from the `storyboard_guide` tool or
126
+ from `storyshot guide`.
127
+
128
+ | MCP tool | what it does |
129
+ |---|---|
130
+ | `storyboard_guide` | The playbook and the full YAML reference. |
131
+ | `inspect_page` | Verified click targets and a screenshot, for a URL or for the state after step N of a storyboard. Reports login walls. |
132
+ | `login` | Opens a browser window where the **user** logs in, and saves the session. |
133
+ | `init_storyboard` | Writes a starter storyboard with verified targets. |
134
+ | `check_storyboard` | Validates in about a second: typos with "did you mean", action and target shapes, `TODO`s, keys, the session file. |
135
+ | `render_video` / `render_status` | Renders in the background and returns the MP4 path, warnings, a per-step speed log and the preview image. If the render isn't done within 45 s, it returns a `job_id`, so client tool timeouts never kill a render. |
136
+
137
+ Other MCP clients: run `npx -y storyshot mcp` (stdio). When run from a git
138
+ checkout, `setup` points to `node /path/to/checkout/bin/storyshot.mjs mcp` instead.
139
+
140
+ When a step fails, the error names the step and action, and says whether the
141
+ target matched nothing or only hidden elements. It also includes a screenshot
142
+ and the clickable targets on the page at that moment, so the agent can fix the
143
+ storyboard in one round.
144
+
145
+ ## CLI (scripts, CI, or by hand)
146
+
147
+ Everything the agent does is also a command:
148
+
149
+ | command | what it does |
150
+ |---|---|
151
+ | `storyshot init <url> [--purpose tutorial] [--topic export-pdf] [--storage videos/<site>-login.json]` | Write `videos/<site>-<kind>[-<topic>].yaml` with verified targets; the video gets the same name. |
152
+ | `storyshot login <url> [--save videos/<site>-login.json]` | Log in once in a browser window and save the session. |
153
+ | `storyshot inspect <url> [--storage auth.json]` | List verified targets and save screenshots. |
154
+ | `storyshot inspect videos/acme-promo.yaml --after 3` | The same, for the page after steps 1–3. |
155
+ | `storyshot check videos/acme-promo.yaml` | Validate without opening a browser. |
156
+ | `storyshot render videos/acme-promo.yaml [--draft]` | Record, then compose the video and soundtrack. |
157
+ | `storyshot compose videos/acme-promo.yaml [--draft]` | Rebuild from the last recording, in seconds. |
158
+ | `storyshot record videos/acme-promo.yaml` | Only record. |
159
+ | `storyshot guide` | Print the playbook and the reference. |
160
+ | `storyshot setup cursor\|claude` | Set up the MCP server and the skill in this project, and install or check what Storyshot needs. |
161
+ | `storyshot doctor` | Check Node, ffmpeg, Chromium and voices; install Chromium if it's missing. |
162
+ | `storyshot mcp` | Run the MCP server. |
163
+
164
+ Recordings, previews and voice lines are cached in `.storyshot/` next to the
165
+ YAML. Add it to `.gitignore`. `--draft` swaps a paid voice for the free one and
166
+ writes `<out>.draft.mp4`.
167
+
168
+ To avoid installing anything, use Docker (handy for CI). The image bundles
169
+ Chromium, ffmpeg and CJK fonts:
170
+
171
+ ```bash
172
+ docker build -t storyshot .
173
+ docker run --rm -v "$PWD:/work" -v storyshot-cache:/root/.cache --env-file .env storyshot render videos/acme-promo.yaml
174
+ ```
175
+
176
+ The examples in [`examples/`](examples/) are a 15 s pitch, a narrated tutorial
177
+ and a course lesson for the same app.
178
+
179
+ ## Storyboard reference
180
+
181
+ The agent writes this file for you; this reference is for reading or editing it
182
+ by hand.
183
+
184
+ ```yaml
185
+ url: https://example.com
186
+ out: acme-promo.mp4 # relative to this file; default: <file name>.mp4
187
+ viewport: { width: 1600, height: 900 } # CSS px the site is laid out at
188
+ output: { width: 1920, height: 1080, fps: 30 } # same aspect ratio; use 1080x1920 with a 900x1600 viewport for vertical
189
+ theme: { accent: "#d9f75b", ink: "#0f172a", font: Inter }
190
+ music: { bpm: 120, volume: 1 } # or { file: track.mp3 }, or false
191
+ sfx: true # click / whoosh / riser sounds
192
+ voice: { provider: elevenlabs, voice: hpp4J3VqNfWAUOO0d1Us } # optional voiceover, see below
193
+ storageState: auth.json # optional Playwright login state
194
+ captionPosition: bottom # or top; can also be set per step. A caption that would cover a step's click target or zoom subject moves to the other side automatically
195
+ purpose: pitch # pitch | tutorial | course | docs (sets the defaults below)
196
+ highlight: false # spotlight targets before click/type (per step or per action too)
197
+ stepNumbers: false # numbered caption badges
198
+ subtitles: false # write <out>.srt (narration, else captions)
199
+ chapters: false # write <out>.chapters.txt for YouTube
200
+ clips: null # gif | webm | mp4: one file per step in <out>-steps/
201
+ maxWait: 1.0 # with duration: auto, the longest a wait is shown
202
+
203
+ setup: # runs before recording starts
204
+ - dismiss: { role: button, name: Accept cookies }
205
+
206
+ steps:
207
+ - caption: Turn PDFs into **editable PowerPoint** # **bold** uses the accent color
208
+ say: Turn any PDF into an editable PowerPoint. # voiceover line (needs `voice`)
209
+ duration: 2.4 # seconds in the final video, or `auto` (fit the narration)
210
+ zoom: { target: h1, scale: 1.2, start: 0.2, end: 2.4, ramp: [1.0, 0.4], offset: { y: 40 } }
211
+ actions:
212
+ - move: { x: 0.5, y: 0.4 } # numbers ≤ 1 are viewport fractions
213
+ - click: { role: button, name: Try it }
214
+ - caption: AI does the work
215
+ duration: 1.3
216
+ sfx: riser # rising sound + chime (or `chime`)
217
+ actions:
218
+ - waitFor: { text: Done, timeout: 120000 } # waiting is compressed automatically
219
+ - hidden: true # cut, not shown in the video
220
+ actions:
221
+ - goto: /
222
+
223
+ end_card:
224
+ logo: { alt: Company logo } # captured from the page, or a path to an image
225
+ title: Acme
226
+ subtitle: Ship **faster**
227
+ cta: Start free at acme.com
228
+ duration: 1.0
229
+ ```
230
+
231
+ Steps that share a caption keep it on screen across both steps.
232
+
233
+ ### Targets
234
+
235
+ A target is either a CSS / Playwright selector string (`h1`, `nav a[href="#pricing"]`)
236
+ or an object with one of these fields:
237
+
238
+ `role` + `name` · `text` · `label` · `placeholder` · `alt` · `testId` · `css`
239
+
240
+ You can add `exact: true`, `last: true` or `nth: 2` to any of them.
241
+
242
+ ### Actions
243
+
244
+ | action | example |
245
+ |---|---|
246
+ | `click` | `click: { role: button, name: Convert, ms: 800, offset: { x: 0.3, y: 0.5 } }` |
247
+ | `move` / `hover` | `move: { text: "$0" }` or `move: { x: 0.5, y: 0.4, ms: 1200 }` |
248
+ | `type` | `type: { target: { label: Email }, text: hi@acme.com, delay: 60 }`, or `type: hello` to type into whatever has focus |
249
+ | `drag` | `drag: { from: { css: ".slider" }, by: [{ x: -300 }, { x: 450 }] }` (several moves while held), or `to: <target or point>` |
250
+ | `press` | `press: Enter`, `press: "^"`, `press: { key: Meta+K, show: true }`. When highlighting, shortcuts and special keys show an on-screen badge; plain characters don't. `show: false` hides it. |
251
+ | `pause` | `pause: 700` (ms) |
252
+ | `waitFor` | `waitFor: { role: button, name: Done, timeout: 120000 }` |
253
+ | `waitForUrl` | `waitForUrl: "**/dashboard"` |
254
+ | `networkIdle` | `networkIdle: true` |
255
+ | `goto` | `goto: /pricing` |
256
+ | `scrollTo` | `scrollTo: { css: "#pricing", ms: 1200 }` |
257
+ | `scroll` | `scroll: 600` |
258
+ | `dismiss` | `dismiss: { role: button, name: Close }` (clicks only if visible) |
259
+
260
+ `ms` sets how long the cursor takes to travel to the target (`dragMs` for each
261
+ drag move). `highlight: true/false` on `click`, `type` or `move` overrides the
262
+ step's spotlight setting.
263
+
264
+ After each `click`, `goto`, `waitFor`, `waitForUrl`, `networkIdle` and
265
+ `press: Enter`, Storyshot waits up to 5 s for the images on screen to load and
266
+ decode. That time counts as waiting, so it is compressed in the video. Compose
267
+ warns when a step shows near-blank frames, such as an empty page with a
268
+ loading spinner.
269
+
270
+ ### `duration: auto`
271
+
272
+ The cursor moves in real time. Waits are shown for at most `maxWait` seconds.
273
+ If the step's narration needs more time, the wait is first shown longer
274
+ (slower), and then the last frame is held until the line finishes. Nothing is
275
+ ever rushed, and the voice is never cut off. Use it for tutorials; for tightly
276
+ cut promos, use fixed durations.
277
+
278
+ Targets only ever match **visible** elements, so hidden copies (closed menus,
279
+ `<option>`s, mobile duplicates) are ignored. Elements below the fold are
280
+ scrolled into view automatically, including in apps that scroll an inner
281
+ container instead of the window. `dismiss` is a no-op when the element isn't
282
+ there, so it's safe for banners that only sometimes appear.
283
+
284
+ In-page anchor links (`href="#..."`) scroll smoothly instead of jumping.
285
+
286
+ ### Zoom
287
+
288
+ | field | meaning |
289
+ |---|---|
290
+ | `target` | What to center on: a target, or a point `{ x: 0.5, y: 0.3 }`. If a step action used the same target, its position at that moment is used (so you can zoom on a button that disappears after the click). Otherwise it is measured at the start of the step when the zoom begins in the first half, or at the end of the step otherwise. |
291
+ | `scale` | Zoom factor (default 1.3). 1.2–1.5 works well. |
292
+ | `start`, `end` | Seconds from the start of the step. The camera eases in from 1× at `start` and is back at 1× at `end` (default: the step's end). `end` may run past the step, e.g. to hold the zoom through a click. |
293
+ | `ramp` | Ease-in / ease-out seconds: `0.5` or `[in, out]`. |
294
+ | `offset` | Moves the camera center from the target's center, in viewport CSS px. `{ y: -120 }` centers 120 px above the target. |
295
+
296
+ The camera never shows anything outside the frame. When a target sits near an
297
+ edge, the zoomed view is clamped to that edge, so the target is off-center
298
+ rather than cut off.
299
+
300
+ ### Voiceover
301
+
302
+ Add a `voice` block, then give steps a `say:` line (and optionally
303
+ `end_card.say`). Each line starts as its step begins (`sayAt` shifts it) and may
304
+ run until the next line. A line that is up to 20% too long is sped up slightly.
305
+ A line longer than that is queued after the previous line, and you get a
306
+ warning telling you to shorten it. Music ducks under the voice, and the final
307
+ mix is normalized to -14 LUFS.
308
+
309
+ | provider | key (env or `.env`) | notes |
310
+ |---|---|---|
311
+ | `kokoro` (default) | none | Local and free; English voices only (`af_heart`, `am_michael`, `bf_emma`, …). Needs `npm i kokoro-js`; the first run downloads an ~90 MB model. |
312
+ | `elevenlabs` | `ELEVENLABS_API_KEY` | Most natural, many languages including Chinese. `voice` is a voice ID; `model` defaults to `eleven_multilingual_v2`. The key needs Text to Speech access, plus Voices → Read to list voices. |
313
+ | `openai` | `OPENAI_API_KEY` | `voice: alloy`, `model: gpt-4o-mini-tts`; optional `instructions` for tone. |
314
+ | `say` | none | macOS built-in voices, robotic; fine for drafts. |
315
+
316
+ Other fields: `speed` (default 1), `volume` (default 1), and `pronounce`, a
317
+ map of word → spoken form used only for TTS, e.g.
318
+ `pronounce: { pxGenius: P X Genius, PPTX: P P T X }`. Subtitles keep the real
319
+ spelling. Synthesized lines
320
+ are cached in `.storyshot/tts-cache/`, so `compose` only pays for lines you
321
+ changed. Write names the way they should be pronounced (`say: P X Genius.`),
322
+ and spell out prices (`eleven ninety-nine`) in `pronounce` or in the line itself. At normal pace that comes to
323
+ about 2.5 words per second, or roughly 35 words for a 15 s video.
324
+
325
+ ### End card
326
+
327
+ Every field is optional; leave out `end_card` entirely for no end card.
328
+
329
+ | field | meaning |
330
+ |---|---|
331
+ | `logo` | A target on the page (captured as an image, e.g. `{ alt: Acme logo }`) or a path to an image file. If the logo image already contains the wordmark, leave out `title`. |
332
+ | `title` | Large product name next to the logo. |
333
+ | `subtitle` | One line; `**bold**` gets an accent highlight. |
334
+ | `cta` | Dark call-to-action pill, e.g. `Start free at acme.com`. |
335
+ | `duration` | Seconds (default 1.0). |
336
+ | `say` | Voiceover line played over the end card. |
337
+
338
+ ## License
339
+
340
+ MIT
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process';
3
+ import { loadConfig, loadEnv } from '../src/config.mjs';
4
+ import { inspect } from '../src/inspect.mjs';
5
+ import { build } from '../src/build.mjs';
6
+ import { check, formatCheck } from '../src/check.mjs';
7
+
8
+ const USAGE = `storyshot — turn a YAML storyboard into a product demo, tutorial or course video
9
+
10
+ Usage:
11
+ storyshot init <url> [--purpose pitch|tutorial|course|docs] [--topic export-pdf] [--storage <site>-login.json]
12
+ write a starter storyboard using verified targets from the page,
13
+ named by type: videos/<site>-promo|tutorial|lesson|docs[-<topic>].yaml
14
+ storyshot login <url> [--save videos/<site>-login.json]
15
+ open a browser window to log in once; saves the session for
16
+ \`storageState:\` (and adds it to .gitignore)
17
+ storyshot inspect <url> [--storage <site>-login.json]
18
+ list a page's headings, sections and verified click targets
19
+ storyshot inspect <storyboard.yaml> [--after N]
20
+ same, for the page as it is after the storyboard's steps 1..N
21
+ storyshot check <storyboard.yaml> validate the storyboard in a second (no browser)
22
+ storyshot render <storyboard.yaml> record the site, then compose video + soundtrack
23
+ storyshot compose <storyboard.yaml> re-compose from the last recording
24
+ (after changing captions, durations, zoom, music, voice, end card)
25
+ storyshot record <storyboard.yaml> only (re-)record the browser session
26
+ storyshot guide print the agent playbook + storyboard reference
27
+ storyshot setup cursor|claude add the MCP server + playbook to this project for Cursor / Claude Code,
28
+ and install / check everything Storyshot needs
29
+ storyshot doctor check (and install what it can of) Chromium, ffmpeg, voices
30
+ storyshot mcp run as an MCP server (stdio)
31
+
32
+ Options:
33
+ --draft (render/compose) use the free local voice instead of a paid TTS provider and write
34
+ <out>.draft.mp4; when it looks right, run \`compose\` without --draft (reuses the recording)
35
+ --viewport 1600x900 (inspect, init)
36
+ `;
37
+
38
+ const args = process.argv.slice(2);
39
+ const [cmd, arg] = args;
40
+ const flag = (name) => { const i = args.indexOf(`--${name}`); return i > 0 ? args[i + 1] : undefined; };
41
+ const NEEDS_ARG = ['login', 'init', 'inspect', 'check', 'render', 'compose', 'record', 'setup'];
42
+ if (![...NEEDS_ARG, 'mcp', 'guide', 'doctor'].includes(cmd) || (NEEDS_ARG.includes(cmd) && !arg)) {
43
+ process.stdout.write(USAGE);
44
+ process.exit(['--help', '-h', 'help'].includes(cmd) ? 0 : 1);
45
+ }
46
+ const viewport = () => { const [w, h] = (flag('viewport') ?? '1600x900').split('x').map(Number); return { width: w, height: h }; };
47
+
48
+ try {
49
+ if (cmd === 'mcp') {
50
+ await import('../src/mcp.mjs');
51
+ } else if (cmd === 'guide') {
52
+ const { guide } = await import('../src/guide.mjs');
53
+ console.log(guide());
54
+ } else if (cmd === 'doctor') {
55
+ const { doctor } = await import('../src/doctor.mjs');
56
+ loadEnv(process.cwd());
57
+ const d = await doctor();
58
+ console.log(d.report);
59
+ if (d.todo.length) { console.log(`\nStill to do — run:\n${d.todo.map((t) => ` ${t}`).join('\n')}`); process.exit(1); }
60
+ } else if (cmd === 'setup') {
61
+ const { setup, cliCommand } = await import('../src/setup.mjs');
62
+ const { doctor } = await import('../src/doctor.mjs');
63
+ const files = setup(arg);
64
+ for (const f of files) console.log(`✔ wrote ${f}`);
65
+ loadEnv(process.cwd());
66
+ console.log('\nChecking what Storyshot needs:');
67
+ const d = await doctor();
68
+ console.log(d.report.replace(/^/gm, ' '));
69
+ const skill = files.find((f) => f.endsWith('SKILL.md'));
70
+ console.log([
71
+ '',
72
+ ...(d.todo.length ? ['STILL TO DO (agent: run these yourself, then `' + cliCommand() + ' doctor`):', ...d.todo.map((t) => ` ${t}`), ''] : []),
73
+ 'NEXT',
74
+ ` Agent: read ${skill} now and follow it for the user's request. Until the storyshot MCP tools`,
75
+ ` show up in this session, use the CLI instead: ${cliCommand()} <command>`,
76
+ arg === 'cursor'
77
+ ? ' User: reload Cursor (Cmd/Ctrl+Shift+P → "Reload Window") so the storyshot MCP tools load; future chats use them automatically.'
78
+ : ' User: restart Claude Code in this project and approve the storyshot MCP server; future sessions use it automatically.',
79
+ ].join('\n'));
80
+ } else if (cmd === 'login') {
81
+ const { login, authFileFor } = await import('../src/auth.mjs');
82
+ const r = await login(arg, { file: flag('save') ?? authFileFor(arg), viewport: viewport() });
83
+ console.log(`✔ saved the login session to ${r.file} (${r.how}, ${r.cookies} cookies)${r.gitignore ? `\n added it to ${r.gitignore}` : ''}` +
84
+ `\n use it: \`storageState: ${r.file.split(/[\\/]/).pop()}\` in the storyboard (path relative to the yaml), or \`--storage ${r.file}\` for inspect/init`);
85
+ } else if (cmd === 'init') {
86
+ const { initStoryboard } = await import('../src/init.mjs');
87
+ const r = await initStoryboard(arg, { purpose: flag('purpose') ?? 'pitch', topic: flag('topic'), file: flag('file'), storageState: flag('storage'), viewport: viewport() });
88
+ console.log(`✔ wrote ${r.file}\n screenshot: ${r.screenshot}\n next: edit it, then \`storyshot check ${r.file}\``);
89
+ } else if (cmd === 'inspect') {
90
+ await (await import('../src/doctor.mjs')).ensureBrowser();
91
+ if (/\.ya?ml$/.test(arg)) {
92
+ const { record } = await import('../src/record.mjs');
93
+ const cfg = loadConfig(arg);
94
+ const after = flag('after');
95
+ console.log((await record(cfg, { log: () => {}, inspectAfter: after != null ? Number(after) : cfg.steps.length })).text);
96
+ } else {
97
+ console.log((await inspect(arg, { viewport: viewport(), storageState: flag('storage') })).text);
98
+ }
99
+ } else if (cmd === 'check') {
100
+ const r = check(arg, { draft: args.includes('--draft') });
101
+ console.log(formatCheck(r));
102
+ process.exit(r.ok ? 0 : 1);
103
+ } else {
104
+ const draft = args.includes('--draft');
105
+ const c = check(arg, { draft });
106
+ if (!c.ok) { console.error(formatCheck(c)); process.exit(1); }
107
+ try { execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); } catch {
108
+ throw new Error('storyshot needs ffmpeg on your PATH (macOS: brew install ffmpeg · Ubuntu: apt install ffmpeg · Windows: winget install ffmpeg) — or use the Docker image');
109
+ }
110
+ const r = await build(loadConfig(arg, { draft }), cmd);
111
+ if (r.out) {
112
+ console.log(`\n✔ ${r.out} (${r.duration.toFixed(1)}s, built in ${r.seconds}s)`);
113
+ console.log(` preview: ${r.preview}`);
114
+ for (const f of r.extras ?? []) if (!/-steps[\\/]/.test(f)) console.log(` + ${f}`);
115
+ for (const w of r.warnings) console.log(` ⚠ ${w}`);
116
+ }
117
+ }
118
+ } catch (e) {
119
+ console.error(`\n✖ ${e.message}`);
120
+ if (/Executable doesn't exist/.test(e.message)) console.error(' run: npx playwright install chromium');
121
+ process.exit(1);
122
+ }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "storyshot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Turn a short YAML storyboard into a polished product demo video of your web app — smooth cursor, auto zoom, captions, music.",
5
+ "author": "boheling <boheling@gmail.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/boheling/storyshot.git"
9
+ },
10
+ "homepage": "https://github.com/boheling/storyshot#readme",
11
+ "type": "module",
12
+ "bin": {
13
+ "storyshot": "bin/storyshot.mjs"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "skills",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "demo",
26
+ "video",
27
+ "playwright",
28
+ "screen-recording",
29
+ "product-demo"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.32.1",
34
+ "playwright": "^1.64.0",
35
+ "yaml": "^2.9.1",
36
+ "zod": "^4.6.5"
37
+ },
38
+ "optionalDependencies": {
39
+ "kokoro-js": "^1.2.1"
40
+ }
41
+ }