agentreel 0.3.5 → 0.4.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 +49 -23
- package/bin/agentreel.mjs +250 -69
- package/package.json +1 -1
- package/scripts/cli_demo.py +24 -2
- package/src/CastVideo.tsx +187 -215
- package/src/Root.tsx +18 -7
- package/src/types.ts +1 -0
- package/public/browser-demo.mp4 +0 -0
- package/public/music.mp3 +0 -0
- package/public/screenshot.png +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# agentreel
|
|
2
2
|
|
|
3
|
-
Turn your web apps and CLIs into viral clips.
|
|
3
|
+
Turn your web apps and CLIs into viral clips — or demo what a PR does.
|
|
4
4
|
|
|
5
5
|
https://github.com/user-attachments/assets/474fd85d-3b35-48f4-82b8-1b337840fb51
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> Turn on sound
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -15,46 +15,72 @@ npx agentreel
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
+
# Demo a PR (reads context from GitHub):
|
|
19
|
+
agentreel --pr 123
|
|
20
|
+
agentreel --pr owner/repo#123
|
|
21
|
+
agentreel --pr https://github.com/owner/repo/pull/123
|
|
22
|
+
|
|
23
|
+
# PR demo with a dev server (for web UI changes):
|
|
24
|
+
agentreel --pr 123 --start "npm run dev"
|
|
25
|
+
|
|
18
26
|
# CLI demo:
|
|
19
27
|
agentreel --cmd "npx my-cli-tool"
|
|
20
28
|
|
|
21
29
|
# Browser demo:
|
|
22
30
|
agentreel --url http://localhost:3000
|
|
23
|
-
|
|
24
|
-
# With context for smarter demo planning:
|
|
25
|
-
agentreel --cmd "npx my-tool" --prompt "A CLI that manages cron jobs"
|
|
26
31
|
```
|
|
27
32
|
|
|
28
|
-
##
|
|
33
|
+
## Modes
|
|
34
|
+
|
|
35
|
+
### PR demo (`--pr`)
|
|
36
|
+
|
|
37
|
+
Point it at a pull request. It fetches the diff, description, and README from GitHub, then AI plans and records a demo showing what the PR actually does.
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
5. Prompts you to share on Twitter
|
|
39
|
+
- **1920x1080 landscape** — chapter-based walkthrough
|
|
40
|
+
- **4-6 chapters**, 12s each — full command + output flows
|
|
41
|
+
- No music, no marketing overlays — just the real demo
|
|
42
|
+
- Great for attaching to PRs so reviewers can see the change in action
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
Requires [`gh` CLI](https://cli.github.com) to be installed and authenticated.
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
- **Title card** with your project name
|
|
40
|
-
- **Highlight clips** — terminal or browser window on animated gradient
|
|
41
|
-
- **Text overlays** — bold captions that work on mute
|
|
42
|
-
- **Cursor + typing** — looks like someone's actually using it
|
|
43
|
-
- **Background music** with fade in/out
|
|
44
|
-
- **End CTA** — install command + URL
|
|
46
|
+
### Marketing reel (`--cmd` / `--url`)
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
The original mode — creates a short, polished clip for social media.
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
- **1080x1080 square** — optimized for Twitter/X, LinkedIn, Reels
|
|
51
|
+
- **3-4 highlight snippets**, 4.5s each — the best moments
|
|
52
|
+
- Music, animated transitions, text overlays, cursor animations
|
|
53
|
+
- Prompts you to share on Twitter
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
1. **PR mode**: Fetches PR diff + README from GitHub, AI decides CLI or browser demo, plans steps that show the actual changes
|
|
58
|
+
2. **Manual mode**: You provide a CLI command or URL, AI plans an impressive demo
|
|
59
|
+
3. AI executes the demo (terminal PTY or Playwright browser)
|
|
60
|
+
4. AI picks the best moments as highlights
|
|
61
|
+
5. Renders video with Remotion
|
|
62
|
+
|
|
63
|
+
## Flags
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
--pr <ref> PR number, owner/repo#N, or full GitHub URL
|
|
67
|
+
--start <cmd> command to start a dev server (for browser PR demos)
|
|
68
|
+
-c, --cmd <command> CLI command to demo
|
|
69
|
+
-u, --url <url> URL to demo (browser mode)
|
|
70
|
+
-t, --title <text> video title
|
|
71
|
+
-o, --output <file> output file (default: agentreel.mp4)
|
|
72
|
+
-a, --auth <file> Playwright storage state (cookies/auth) for browser demos
|
|
73
|
+
-g, --guidelines <text> guidelines for highlight generation
|
|
74
|
+
--music <file> path to background music mp3
|
|
75
|
+
--no-share skip the share prompt
|
|
76
|
+
```
|
|
52
77
|
|
|
53
78
|
## Requirements
|
|
54
79
|
|
|
55
80
|
- Node.js 18+
|
|
56
81
|
- Python 3.10+
|
|
57
82
|
- Claude CLI (`claude`) — used to plan demo sequences
|
|
83
|
+
- `gh` CLI — required for `--pr` mode
|
|
58
84
|
|
|
59
85
|
## Credits
|
|
60
86
|
|
package/bin/agentreel.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { readFileSync, statSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
5
5
|
import { join, dirname, resolve } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -25,7 +25,8 @@ function parseArgs() {
|
|
|
25
25
|
}
|
|
26
26
|
if (arg === "--cmd" || arg === "-c") flags.cmd = args[++i];
|
|
27
27
|
else if (arg === "--url" || arg === "-u") flags.url = args[++i];
|
|
28
|
-
else if (arg === "--
|
|
28
|
+
else if (arg === "--pr") flags.pr = args[++i];
|
|
29
|
+
else if (arg === "--start") flags.start = args[++i];
|
|
29
30
|
else if (arg === "--title" || arg === "-t") flags.title = args[++i];
|
|
30
31
|
else if (arg === "--output" || arg === "-o") flags.output = args[++i];
|
|
31
32
|
else if (arg === "--music") flags.music = args[++i];
|
|
@@ -40,13 +41,16 @@ function printUsage() {
|
|
|
40
41
|
console.log(`agentreel — Turn your web apps and CLIs into viral clips
|
|
41
42
|
|
|
42
43
|
Usage:
|
|
44
|
+
agentreel --pr 123 # demo a PR (reads context from GitHub)
|
|
45
|
+
agentreel --pr owner/repo#123 # demo a PR (explicit repo)
|
|
43
46
|
agentreel --cmd "npx my-cli-tool" # CLI demo
|
|
44
47
|
agentreel --url http://localhost:3000 # browser demo
|
|
45
48
|
|
|
46
49
|
Flags:
|
|
50
|
+
--pr <ref> PR number, owner/repo#N, or full GitHub URL
|
|
51
|
+
--start <cmd> command to start a dev server (for browser PR demos)
|
|
47
52
|
-c, --cmd <command> CLI command to demo
|
|
48
53
|
-u, --url <url> URL to demo (browser mode)
|
|
49
|
-
-p, --prompt <text> description of what the tool does
|
|
50
54
|
-t, --title <text> video title
|
|
51
55
|
-o, --output <file> output file (default: agentreel.mp4)
|
|
52
56
|
-a, --auth <file> Playwright storage state (cookies/auth) for browser demos
|
|
@@ -326,29 +330,7 @@ async function renderVideo(props, output, musicPath) {
|
|
|
326
330
|
console.error(`\nDone: ${output} (${Math.round(size / 1024)} KB)`);
|
|
327
331
|
}
|
|
328
332
|
|
|
329
|
-
// ──
|
|
330
|
-
|
|
331
|
-
// Video upload placeholder — will add agentreel.dev hosting later
|
|
332
|
-
async function uploadVideo(_filePath) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function openShareURL(videoURL, text) {
|
|
337
|
-
const tweetText = encodeURIComponent(text);
|
|
338
|
-
const encodedURL = encodeURIComponent(videoURL);
|
|
339
|
-
const intentURL = `https://twitter.com/intent/tweet?text=${tweetText}&url=${encodedURL}`;
|
|
340
|
-
|
|
341
|
-
console.error(`\n Share: ${videoURL}`);
|
|
342
|
-
console.error(` Tweet: ${intentURL}\n`);
|
|
343
|
-
|
|
344
|
-
// Open in browser
|
|
345
|
-
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
346
|
-
try {
|
|
347
|
-
execFileSync(cmd, [intentURL], { stdio: "ignore" });
|
|
348
|
-
} catch {
|
|
349
|
-
console.error(" (Could not open browser — copy the link above)");
|
|
350
|
-
}
|
|
351
|
-
}
|
|
333
|
+
// ── Share ───────────────────────────────────────────────────
|
|
352
334
|
|
|
353
335
|
function askYesNo(question) {
|
|
354
336
|
return new Promise((resolve) => {
|
|
@@ -383,20 +365,150 @@ async function shareFlow(outputPath, title, prompt) {
|
|
|
383
365
|
}
|
|
384
366
|
}
|
|
385
367
|
|
|
386
|
-
// ──
|
|
368
|
+
// ── PR Context ─────────────────────────────────────────────
|
|
387
369
|
|
|
388
|
-
function
|
|
389
|
-
const target = cmd || url;
|
|
370
|
+
function fetchPRContext(prRef) {
|
|
390
371
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
372
|
+
execFileSync("gh", ["--version"], { stdio: "ignore" });
|
|
373
|
+
} catch {
|
|
374
|
+
console.error("Error: `gh` CLI is required for --pr mode. Install it from https://cli.github.com");
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const prJson = execFileSync("gh", [
|
|
379
|
+
"pr", "view", String(prRef),
|
|
380
|
+
"--json", "title,body,headRefName,baseRefName,url,number",
|
|
381
|
+
], { encoding: "utf-8", timeout: 30000 });
|
|
382
|
+
const pr = JSON.parse(prJson);
|
|
383
|
+
|
|
384
|
+
let diff = "";
|
|
385
|
+
try {
|
|
386
|
+
diff = execFileSync("gh", ["pr", "diff", String(prRef)], {
|
|
387
|
+
encoding: "utf-8", timeout: 30000,
|
|
388
|
+
});
|
|
389
|
+
} catch (e) {
|
|
390
|
+
console.error(` Warning: could not fetch PR diff: ${e.message}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Read README from cwd (the agent already has the repo checked out)
|
|
394
|
+
let readme = "";
|
|
395
|
+
for (const name of ["README.md", "readme.md", "README", "README.rst"]) {
|
|
396
|
+
const p = join(process.cwd(), name);
|
|
397
|
+
if (existsSync(p)) {
|
|
398
|
+
readme = readFileSync(p, "utf-8");
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { ...pr, diff, readme };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function planDemoFromPR(prContext, guidelines) {
|
|
407
|
+
const guidelinesBlock = guidelines
|
|
408
|
+
? `\nAdditional guidelines: ${guidelines}`
|
|
409
|
+
: "";
|
|
410
|
+
|
|
411
|
+
const prompt = `You are planning a demo for a Pull Request. Your job is to decide whether this is a CLI or browser demo, and provide the details needed to record it.
|
|
412
|
+
|
|
413
|
+
PR Title: ${prContext.title}
|
|
414
|
+
PR Description: ${prContext.body || "(no description)"}
|
|
415
|
+
|
|
416
|
+
Diff (truncated):
|
|
417
|
+
${prContext.diff.slice(0, 8000)}
|
|
418
|
+
|
|
419
|
+
README (truncated):
|
|
420
|
+
${prContext.readme.slice(0, 3000)}${guidelinesBlock}
|
|
421
|
+
|
|
422
|
+
Return a JSON object with these fields:
|
|
423
|
+
{
|
|
424
|
+
"type": "cli" or "browser",
|
|
425
|
+
"command": "the command to run" (for CLI demos, e.g. "npx my-tool --help") or null,
|
|
426
|
+
"url": "http://localhost:3000/relevant-page" (for browser demos) or null,
|
|
427
|
+
"description": "one-sentence summary of what the PR does",
|
|
428
|
+
"title": "short video title (2-4 words)",
|
|
429
|
+
"guidelines": "specific instructions for the demo recorder about what steps to show and what to focus on"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
Rules:
|
|
433
|
+
- If the PR changes a CLI tool, script, or backend logic that can be demonstrated in a terminal, use "cli".
|
|
434
|
+
- If the PR changes a web UI, frontend, or something best shown in a browser, use "browser".
|
|
435
|
+
- The "guidelines" field should tell the demo recorder exactly what to demonstrate — the specific feature or fix from this PR.
|
|
436
|
+
- The demo should show the actual changes working honestly, not market the product.
|
|
437
|
+
- Return ONLY the JSON object, no markdown fences.`;
|
|
438
|
+
|
|
439
|
+
const result = execFileSync("claude", ["-p", prompt, "--output-format", "text"], {
|
|
440
|
+
encoding: "utf-8",
|
|
441
|
+
timeout: 60000,
|
|
442
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
443
|
+
}).trim();
|
|
444
|
+
|
|
445
|
+
// Strip markdown fences if present
|
|
446
|
+
let text = result;
|
|
447
|
+
if (text.includes("```")) {
|
|
448
|
+
const parts = text.split("```");
|
|
449
|
+
for (let part of parts) {
|
|
450
|
+
part = part.trim();
|
|
451
|
+
if (part.startsWith("json")) part = part.slice(4).trim();
|
|
452
|
+
if (part.startsWith("{")) { text = part; break; }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return JSON.parse(text);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Dev Server ─────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
function startDevServer(command) {
|
|
462
|
+
console.error(` Starting dev server: ${command}`);
|
|
463
|
+
const proc = spawn("sh", ["-c", command], {
|
|
464
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
465
|
+
detached: true,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Wait for server to be ready (look for common ready signals in output)
|
|
469
|
+
return new Promise((resolve, reject) => {
|
|
470
|
+
const timeout = setTimeout(() => {
|
|
471
|
+
console.error(" Dev server ready (timeout — assuming started)");
|
|
472
|
+
resolve(proc);
|
|
473
|
+
}, 30000);
|
|
474
|
+
|
|
475
|
+
const onData = (data) => {
|
|
476
|
+
const text = data.toString();
|
|
477
|
+
if (/localhost|ready|started|listening|compiled/i.test(text)) {
|
|
478
|
+
clearTimeout(timeout);
|
|
479
|
+
// Give it a moment to fully start
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
console.error(" Dev server ready");
|
|
482
|
+
resolve(proc);
|
|
483
|
+
}, 2000);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
proc.stdout.on("data", onData);
|
|
488
|
+
proc.stderr.on("data", onData);
|
|
489
|
+
|
|
490
|
+
proc.on("error", (err) => {
|
|
491
|
+
clearTimeout(timeout);
|
|
492
|
+
reject(new Error(`Dev server failed to start: ${err.message}`));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
proc.on("exit", (code) => {
|
|
496
|
+
clearTimeout(timeout);
|
|
497
|
+
if (code !== null && code !== 0) {
|
|
498
|
+
reject(new Error(`Dev server exited with code ${code}`));
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function stopDevServer(proc) {
|
|
505
|
+
if (!proc || proc.killed) return;
|
|
506
|
+
try {
|
|
507
|
+
// Kill the process group (detached process + children)
|
|
508
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
509
|
+
} catch {
|
|
510
|
+
try { proc.kill("SIGTERM"); } catch { /* already dead */ }
|
|
511
|
+
}
|
|
400
512
|
}
|
|
401
513
|
|
|
402
514
|
// ── Main ────────────────────────────────────────────────────
|
|
@@ -406,83 +518,152 @@ async function main() {
|
|
|
406
518
|
const output = flags.output || "agentreel.mp4";
|
|
407
519
|
const noShare = flags.noShare;
|
|
408
520
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
let prompt = flags.prompt;
|
|
412
|
-
|
|
413
|
-
// Auto-generate description if not provided
|
|
414
|
-
if (!prompt) {
|
|
415
|
-
console.error("Generating description...");
|
|
416
|
-
prompt = autoDescribe(demoCmd, demoURL);
|
|
417
|
-
console.error(` "${prompt}"`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!demoCmd && !demoURL) {
|
|
421
|
-
console.error("Please provide --cmd or --url.\n");
|
|
521
|
+
if (!flags.cmd && !flags.url && !flags.pr) {
|
|
522
|
+
console.error("Please provide --pr, --cmd, or --url.\n");
|
|
422
523
|
printUsage();
|
|
423
524
|
process.exit(1);
|
|
424
525
|
}
|
|
425
526
|
|
|
426
|
-
|
|
527
|
+
// ── PR mode ──────────────────────────────────────────────
|
|
528
|
+
if (flags.pr) {
|
|
529
|
+
console.error("Fetching PR context...");
|
|
530
|
+
const prContext = fetchPRContext(flags.pr);
|
|
531
|
+
console.error(` PR #${prContext.number}: ${prContext.title}`);
|
|
532
|
+
|
|
533
|
+
console.error("Planning demo...");
|
|
534
|
+
const plan = planDemoFromPR(prContext, flags.guidelines);
|
|
535
|
+
console.error(` Type: ${plan.type}, "${plan.description}"`);
|
|
536
|
+
|
|
537
|
+
const videoTitle = flags.title || plan.title || prContext.title;
|
|
538
|
+
const description = plan.description;
|
|
539
|
+
// Prepend "demo" to guidelines so downstream scripts know to use chapter-based extraction
|
|
540
|
+
const demoGuidelines = `[demo] ${plan.guidelines || ""}`.trim();
|
|
541
|
+
|
|
542
|
+
if (plan.type === "browser") {
|
|
543
|
+
const url = plan.url || "http://localhost:3000";
|
|
544
|
+
let serverProc = null;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
if (flags.start) {
|
|
548
|
+
serverProc = await startDevServer(flags.start);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
ensureBrowserDeps();
|
|
552
|
+
console.error("Step 1/3: Recording browser demo...");
|
|
553
|
+
const videoPath = recordBrowser(url, demoGuidelines, flags.auth, demoGuidelines);
|
|
554
|
+
|
|
555
|
+
const publicDir = join(ROOT, "public");
|
|
556
|
+
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
557
|
+
copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
|
|
558
|
+
|
|
559
|
+
console.error("Step 2/3: Building highlights...");
|
|
560
|
+
const clicksPath = videoPath.replace(".mp4", "-clicks.json");
|
|
561
|
+
let allClicks = [];
|
|
562
|
+
if (existsSync(clicksPath)) {
|
|
563
|
+
allClicks = JSON.parse(readFileSync(clicksPath, "utf-8"));
|
|
564
|
+
console.error(` ${allClicks.length} clicks captured`);
|
|
565
|
+
}
|
|
566
|
+
const highlights = buildBrowserHighlights(allClicks, videoPath, demoGuidelines, demoGuidelines);
|
|
567
|
+
|
|
568
|
+
console.error("Step 3/3: Rendering video...");
|
|
569
|
+
await renderVideo({
|
|
570
|
+
title: videoTitle,
|
|
571
|
+
subtitle: description,
|
|
572
|
+
highlights,
|
|
573
|
+
endText: prContext.title,
|
|
574
|
+
endUrl: prContext.url,
|
|
575
|
+
mode: "demo",
|
|
576
|
+
}, output, flags.music);
|
|
577
|
+
} finally {
|
|
578
|
+
stopDevServer(serverProc);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
// CLI demo
|
|
582
|
+
if (!plan.command) {
|
|
583
|
+
console.error("Error: Claude could not determine a command to demo for this PR.");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
console.error("Step 1/3: Recording CLI demo...");
|
|
588
|
+
const castPath = recordCLI(plan.command, process.cwd(), description, demoGuidelines);
|
|
589
|
+
|
|
590
|
+
console.error("Step 2/3: Extracting highlights...");
|
|
591
|
+
const highlightsPath = extractHighlightsFromCast(castPath, description, demoGuidelines);
|
|
592
|
+
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
593
|
+
console.error(` ${highlights.length} highlights extracted`);
|
|
594
|
+
|
|
595
|
+
console.error("Step 3/3: Rendering video...");
|
|
596
|
+
await renderVideo({
|
|
597
|
+
title: videoTitle,
|
|
598
|
+
subtitle: description,
|
|
599
|
+
highlights,
|
|
600
|
+
endText: plan.command,
|
|
601
|
+
endUrl: prContext.url,
|
|
602
|
+
mode: "demo",
|
|
603
|
+
}, output, flags.music);
|
|
604
|
+
}
|
|
427
605
|
|
|
428
|
-
|
|
606
|
+
if (!noShare) {
|
|
607
|
+
await shareFlow(resolve(output), videoTitle, description);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── Manual modes (--cmd / --url) ─────────────────────────
|
|
613
|
+
let videoTitle = flags.title || flags.cmd || flags.url;
|
|
614
|
+
|
|
615
|
+
if (flags.cmd) {
|
|
429
616
|
console.error("Step 1/3: Recording CLI demo...");
|
|
430
|
-
const castPath = recordCLI(
|
|
617
|
+
const castPath = recordCLI(flags.cmd, process.cwd(), flags.cmd, flags.guidelines);
|
|
431
618
|
|
|
432
619
|
console.error("Step 2/3: Extracting highlights...");
|
|
433
|
-
const highlightsPath = extractHighlightsFromCast(castPath,
|
|
620
|
+
const highlightsPath = extractHighlightsFromCast(castPath, flags.cmd, flags.guidelines);
|
|
434
621
|
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
435
622
|
console.error(` ${highlights.length} highlights extracted`);
|
|
436
623
|
|
|
437
624
|
console.error("Step 3/3: Rendering video...");
|
|
438
625
|
await renderVideo({
|
|
439
626
|
title: videoTitle,
|
|
440
|
-
subtitle: prompt,
|
|
441
627
|
highlights,
|
|
442
|
-
endText:
|
|
628
|
+
endText: flags.cmd,
|
|
443
629
|
}, output, flags.music);
|
|
444
630
|
|
|
445
631
|
if (!noShare) {
|
|
446
|
-
await shareFlow(resolve(output), videoTitle,
|
|
632
|
+
await shareFlow(resolve(output), videoTitle, flags.cmd);
|
|
447
633
|
}
|
|
448
634
|
return;
|
|
449
635
|
}
|
|
450
636
|
|
|
451
|
-
if (
|
|
452
|
-
const task =
|
|
637
|
+
if (flags.url) {
|
|
638
|
+
const task = "Explore the main features of this app";
|
|
453
639
|
|
|
454
640
|
ensureBrowserDeps();
|
|
455
641
|
console.error("Step 1/3: Recording browser demo...");
|
|
456
|
-
const videoPath = recordBrowser(
|
|
642
|
+
const videoPath = recordBrowser(flags.url, task, flags.auth, flags.guidelines);
|
|
457
643
|
|
|
458
|
-
// Copy video to Remotion public dir so it can be served
|
|
459
644
|
const publicDir = join(ROOT, "public");
|
|
460
645
|
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
461
646
|
copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
|
|
462
647
|
|
|
463
648
|
console.error("Step 2/3: Building highlights...");
|
|
464
|
-
|
|
465
|
-
// Read click data — this is the primary signal for highlights
|
|
466
649
|
const clicksPath = videoPath.replace(".mp4", "-clicks.json");
|
|
467
650
|
let allClicks = [];
|
|
468
651
|
if (existsSync(clicksPath)) {
|
|
469
652
|
allClicks = JSON.parse(readFileSync(clicksPath, "utf-8"));
|
|
470
653
|
console.error(` ${allClicks.length} clicks captured`);
|
|
471
654
|
}
|
|
472
|
-
|
|
473
655
|
const highlights = buildBrowserHighlights(allClicks, videoPath, task, flags.guidelines);
|
|
474
656
|
|
|
475
657
|
console.error("Step 3/3: Rendering video...");
|
|
476
658
|
await renderVideo({
|
|
477
659
|
title: videoTitle,
|
|
478
|
-
subtitle: prompt,
|
|
479
660
|
highlights,
|
|
480
|
-
endText:
|
|
481
|
-
endUrl:
|
|
661
|
+
endText: flags.url,
|
|
662
|
+
endUrl: flags.url,
|
|
482
663
|
}, output, flags.music);
|
|
483
664
|
|
|
484
665
|
if (!noShare) {
|
|
485
|
-
await shareFlow(resolve(output), videoTitle,
|
|
666
|
+
await shareFlow(resolve(output), videoTitle, flags.url);
|
|
486
667
|
}
|
|
487
668
|
return;
|
|
488
669
|
}
|
package/package.json
CHANGED
package/scripts/cli_demo.py
CHANGED
|
@@ -198,7 +198,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
198
198
|
|
|
199
199
|
|
|
200
200
|
def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> list[dict]:
|
|
201
|
-
"""Ask Claude to pick
|
|
201
|
+
"""Ask Claude to pick highlight moments from the recorded session."""
|
|
202
202
|
# Read the asciicast and strip to just the text content
|
|
203
203
|
lines_output = []
|
|
204
204
|
with open(cast_path) as f:
|
|
@@ -218,7 +218,29 @@ def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> li
|
|
|
218
218
|
|
|
219
219
|
guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
# Demo mode: more chapters, more lines, show full flows
|
|
222
|
+
is_demo = "demo" in guidelines.lower() if guidelines else False
|
|
223
|
+
|
|
224
|
+
if is_demo:
|
|
225
|
+
prompt = f"""You are creating chapter-based highlights for a demo walkthrough video. Here is the full terminal output:
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
{clean[:6000]}
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
Context: {context}{guidelines_block}
|
|
232
|
+
|
|
233
|
+
Create 4-6 chapters that walk through the full demo. Each chapter shows a complete command and its output. For each chapter, return:
|
|
234
|
+
- "label": chapter name (1-3 words) like "Setup", "Run Command", "View Results", "Verify"
|
|
235
|
+
- "lines": array of objects, each with "text" (string), and optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool if it's a shell command)
|
|
236
|
+
|
|
237
|
+
Each chapter should have 12-20 lines. Show the COMPLETE command and output for each step.
|
|
238
|
+
Include the prompt line (isPrompt: true) followed by the actual output.
|
|
239
|
+
Use these colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
|
|
240
|
+
|
|
241
|
+
Return ONLY a JSON array. No markdown fences."""
|
|
242
|
+
else:
|
|
243
|
+
prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
|
|
222
244
|
|
|
223
245
|
---
|
|
224
246
|
{clean[:3000]}
|