agentreel 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.
- package/README.md +49 -23
- package/bin/agentreel.mjs +263 -36
- package/package.json +1 -1
- package/scripts/browser_demo.py +10 -4
- package/scripts/cli_demo.py +31 -6
- package/src/CastVideo.tsx +187 -215
- package/src/Root.tsx +18 -7
- package/src/types.ts +1 -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
|
|
@@ -100,13 +104,14 @@ function ensureBrowserDeps() {
|
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
function recordCLI(command, workDir, context) {
|
|
107
|
+
function recordCLI(command, workDir, context, guidelines) {
|
|
104
108
|
const python = findPython();
|
|
105
109
|
const script = join(ROOT, "scripts", "cli_demo.py");
|
|
106
110
|
const outFile = join(tmpdir(), "agentreel-cli-demo.cast");
|
|
107
111
|
|
|
108
112
|
const args = [script, command, workDir, outFile];
|
|
109
113
|
if (context) args.push(context);
|
|
114
|
+
if (guidelines) args.push(guidelines);
|
|
110
115
|
|
|
111
116
|
console.error(`Agent planning CLI demo for: ${command}`);
|
|
112
117
|
execFileSync(python, args, { stdio: ["ignore", "inherit", "inherit"], env: process.env });
|
|
@@ -133,7 +138,7 @@ function browserEnv() {
|
|
|
133
138
|
return { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browsersDir };
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
function recordBrowser(url, task, authState) {
|
|
141
|
+
function recordBrowser(url, task, authState, guidelines) {
|
|
137
142
|
const python = findPython();
|
|
138
143
|
const script = join(ROOT, "scripts", "browser_demo.py");
|
|
139
144
|
const outFile = join(tmpdir(), "agentreel-browser-demo.mp4");
|
|
@@ -141,6 +146,7 @@ function recordBrowser(url, task, authState) {
|
|
|
141
146
|
console.error(`Agent demoing browser app: ${url}`);
|
|
142
147
|
const args = [script, url, outFile, task];
|
|
143
148
|
if (authState) args.push("--auth", authState);
|
|
149
|
+
if (guidelines) args.push("--guidelines", guidelines);
|
|
144
150
|
execFileSync(python, args, {
|
|
145
151
|
stdio: ["ignore", "inherit", "inherit"],
|
|
146
152
|
env: browserEnv(),
|
|
@@ -397,6 +403,152 @@ function autoDescribe(cmd, url) {
|
|
|
397
403
|
return cmd ? cmd.split(/\s+/).pop() : "Web app demo";
|
|
398
404
|
}
|
|
399
405
|
|
|
406
|
+
// ── PR Context ─────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
function fetchPRContext(prRef) {
|
|
409
|
+
try {
|
|
410
|
+
execFileSync("gh", ["--version"], { stdio: "ignore" });
|
|
411
|
+
} catch {
|
|
412
|
+
console.error("Error: `gh` CLI is required for --pr mode. Install it from https://cli.github.com");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const prJson = execFileSync("gh", [
|
|
417
|
+
"pr", "view", String(prRef),
|
|
418
|
+
"--json", "title,body,headRefName,baseRefName,url,number",
|
|
419
|
+
], { encoding: "utf-8", timeout: 30000 });
|
|
420
|
+
const pr = JSON.parse(prJson);
|
|
421
|
+
|
|
422
|
+
let diff = "";
|
|
423
|
+
try {
|
|
424
|
+
diff = execFileSync("gh", ["pr", "diff", String(prRef)], {
|
|
425
|
+
encoding: "utf-8", timeout: 30000,
|
|
426
|
+
});
|
|
427
|
+
} catch (e) {
|
|
428
|
+
console.error(` Warning: could not fetch PR diff: ${e.message}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Read README from cwd (the agent already has the repo checked out)
|
|
432
|
+
let readme = "";
|
|
433
|
+
for (const name of ["README.md", "readme.md", "README", "README.rst"]) {
|
|
434
|
+
const p = join(process.cwd(), name);
|
|
435
|
+
if (existsSync(p)) {
|
|
436
|
+
readme = readFileSync(p, "utf-8");
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { ...pr, diff, readme };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function planDemoFromPR(prContext, guidelines) {
|
|
445
|
+
const guidelinesBlock = guidelines
|
|
446
|
+
? `\nAdditional guidelines: ${guidelines}`
|
|
447
|
+
: "";
|
|
448
|
+
|
|
449
|
+
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.
|
|
450
|
+
|
|
451
|
+
PR Title: ${prContext.title}
|
|
452
|
+
PR Description: ${prContext.body || "(no description)"}
|
|
453
|
+
|
|
454
|
+
Diff (truncated):
|
|
455
|
+
${prContext.diff.slice(0, 8000)}
|
|
456
|
+
|
|
457
|
+
README (truncated):
|
|
458
|
+
${prContext.readme.slice(0, 3000)}${guidelinesBlock}
|
|
459
|
+
|
|
460
|
+
Return a JSON object with these fields:
|
|
461
|
+
{
|
|
462
|
+
"type": "cli" or "browser",
|
|
463
|
+
"command": "the command to run" (for CLI demos, e.g. "npx my-tool --help") or null,
|
|
464
|
+
"url": "http://localhost:3000/relevant-page" (for browser demos) or null,
|
|
465
|
+
"description": "one-sentence summary of what the PR does",
|
|
466
|
+
"title": "short video title (2-4 words)",
|
|
467
|
+
"guidelines": "specific instructions for the demo recorder about what steps to show and what to focus on"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
Rules:
|
|
471
|
+
- If the PR changes a CLI tool, script, or backend logic that can be demonstrated in a terminal, use "cli".
|
|
472
|
+
- If the PR changes a web UI, frontend, or something best shown in a browser, use "browser".
|
|
473
|
+
- The "guidelines" field should tell the demo recorder exactly what to demonstrate — the specific feature or fix from this PR.
|
|
474
|
+
- The demo should show the actual changes working honestly, not market the product.
|
|
475
|
+
- Return ONLY the JSON object, no markdown fences.`;
|
|
476
|
+
|
|
477
|
+
const result = execFileSync("claude", ["-p", prompt, "--output-format", "text"], {
|
|
478
|
+
encoding: "utf-8",
|
|
479
|
+
timeout: 60000,
|
|
480
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
481
|
+
}).trim();
|
|
482
|
+
|
|
483
|
+
// Strip markdown fences if present
|
|
484
|
+
let text = result;
|
|
485
|
+
if (text.includes("```")) {
|
|
486
|
+
const parts = text.split("```");
|
|
487
|
+
for (let part of parts) {
|
|
488
|
+
part = part.trim();
|
|
489
|
+
if (part.startsWith("json")) part = part.slice(4).trim();
|
|
490
|
+
if (part.startsWith("{")) { text = part; break; }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return JSON.parse(text);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Dev Server ─────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
function startDevServer(command) {
|
|
500
|
+
console.error(` Starting dev server: ${command}`);
|
|
501
|
+
const proc = spawn("sh", ["-c", command], {
|
|
502
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
503
|
+
detached: true,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Wait for server to be ready (look for common ready signals in output)
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const timeout = setTimeout(() => {
|
|
509
|
+
console.error(" Dev server ready (timeout — assuming started)");
|
|
510
|
+
resolve(proc);
|
|
511
|
+
}, 30000);
|
|
512
|
+
|
|
513
|
+
const onData = (data) => {
|
|
514
|
+
const text = data.toString();
|
|
515
|
+
if (/localhost|ready|started|listening|compiled/i.test(text)) {
|
|
516
|
+
clearTimeout(timeout);
|
|
517
|
+
// Give it a moment to fully start
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
console.error(" Dev server ready");
|
|
520
|
+
resolve(proc);
|
|
521
|
+
}, 2000);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
proc.stdout.on("data", onData);
|
|
526
|
+
proc.stderr.on("data", onData);
|
|
527
|
+
|
|
528
|
+
proc.on("error", (err) => {
|
|
529
|
+
clearTimeout(timeout);
|
|
530
|
+
reject(new Error(`Dev server failed to start: ${err.message}`));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
proc.on("exit", (code) => {
|
|
534
|
+
clearTimeout(timeout);
|
|
535
|
+
if (code !== null && code !== 0) {
|
|
536
|
+
reject(new Error(`Dev server exited with code ${code}`));
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function stopDevServer(proc) {
|
|
543
|
+
if (!proc || proc.killed) return;
|
|
544
|
+
try {
|
|
545
|
+
// Kill the process group (detached process + children)
|
|
546
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
547
|
+
} catch {
|
|
548
|
+
try { proc.kill("SIGTERM"); } catch { /* already dead */ }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
400
552
|
// ── Main ────────────────────────────────────────────────────
|
|
401
553
|
|
|
402
554
|
async function main() {
|
|
@@ -404,83 +556,158 @@ async function main() {
|
|
|
404
556
|
const output = flags.output || "agentreel.mp4";
|
|
405
557
|
const noShare = flags.noShare;
|
|
406
558
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
let prompt = flags.prompt;
|
|
410
|
-
|
|
411
|
-
// Auto-generate description if not provided
|
|
412
|
-
if (!prompt) {
|
|
413
|
-
console.error("Generating description...");
|
|
414
|
-
prompt = autoDescribe(demoCmd, demoURL);
|
|
415
|
-
console.error(` "${prompt}"`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!demoCmd && !demoURL) {
|
|
419
|
-
console.error("Please provide --cmd or --url.\n");
|
|
559
|
+
if (!flags.cmd && !flags.url && !flags.pr) {
|
|
560
|
+
console.error("Please provide --pr, --cmd, or --url.\n");
|
|
420
561
|
printUsage();
|
|
421
562
|
process.exit(1);
|
|
422
563
|
}
|
|
423
564
|
|
|
424
|
-
|
|
565
|
+
// ── PR mode ──────────────────────────────────────────────
|
|
566
|
+
if (flags.pr) {
|
|
567
|
+
console.error("Fetching PR context...");
|
|
568
|
+
const prContext = fetchPRContext(flags.pr);
|
|
569
|
+
console.error(` PR #${prContext.number}: ${prContext.title}`);
|
|
570
|
+
|
|
571
|
+
console.error("Planning demo...");
|
|
572
|
+
const plan = planDemoFromPR(prContext, flags.guidelines);
|
|
573
|
+
console.error(` Type: ${plan.type}, "${plan.description}"`);
|
|
574
|
+
|
|
575
|
+
const videoTitle = flags.title || plan.title || prContext.title;
|
|
576
|
+
const description = plan.description;
|
|
577
|
+
// Prepend "demo" to guidelines so downstream scripts know to use chapter-based extraction
|
|
578
|
+
const demoGuidelines = `[demo] ${plan.guidelines || ""}`.trim();
|
|
579
|
+
|
|
580
|
+
if (plan.type === "browser") {
|
|
581
|
+
const url = plan.url || "http://localhost:3000";
|
|
582
|
+
let serverProc = null;
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
if (flags.start) {
|
|
586
|
+
serverProc = await startDevServer(flags.start);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
ensureBrowserDeps();
|
|
590
|
+
console.error("Step 1/3: Recording browser demo...");
|
|
591
|
+
const videoPath = recordBrowser(url, demoGuidelines, flags.auth, demoGuidelines);
|
|
592
|
+
|
|
593
|
+
const publicDir = join(ROOT, "public");
|
|
594
|
+
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
595
|
+
copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
|
|
596
|
+
|
|
597
|
+
console.error("Step 2/3: Building highlights...");
|
|
598
|
+
const clicksPath = videoPath.replace(".mp4", "-clicks.json");
|
|
599
|
+
let allClicks = [];
|
|
600
|
+
if (existsSync(clicksPath)) {
|
|
601
|
+
allClicks = JSON.parse(readFileSync(clicksPath, "utf-8"));
|
|
602
|
+
console.error(` ${allClicks.length} clicks captured`);
|
|
603
|
+
}
|
|
604
|
+
const highlights = buildBrowserHighlights(allClicks, videoPath, demoGuidelines, demoGuidelines);
|
|
605
|
+
|
|
606
|
+
console.error("Step 3/3: Rendering video...");
|
|
607
|
+
await renderVideo({
|
|
608
|
+
title: videoTitle,
|
|
609
|
+
subtitle: description,
|
|
610
|
+
highlights,
|
|
611
|
+
endText: prContext.title,
|
|
612
|
+
endUrl: prContext.url,
|
|
613
|
+
mode: "demo",
|
|
614
|
+
}, output, flags.music);
|
|
615
|
+
} finally {
|
|
616
|
+
stopDevServer(serverProc);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
// CLI demo
|
|
620
|
+
if (!plan.command) {
|
|
621
|
+
console.error("Error: Claude could not determine a command to demo for this PR.");
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.error("Step 1/3: Recording CLI demo...");
|
|
626
|
+
const castPath = recordCLI(plan.command, process.cwd(), description, demoGuidelines);
|
|
627
|
+
|
|
628
|
+
console.error("Step 2/3: Extracting highlights...");
|
|
629
|
+
const highlightsPath = extractHighlightsFromCast(castPath, description, demoGuidelines);
|
|
630
|
+
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
631
|
+
console.error(` ${highlights.length} highlights extracted`);
|
|
632
|
+
|
|
633
|
+
console.error("Step 3/3: Rendering video...");
|
|
634
|
+
await renderVideo({
|
|
635
|
+
title: videoTitle,
|
|
636
|
+
subtitle: description,
|
|
637
|
+
highlights,
|
|
638
|
+
endText: plan.command,
|
|
639
|
+
endUrl: prContext.url,
|
|
640
|
+
mode: "demo",
|
|
641
|
+
}, output, flags.music);
|
|
642
|
+
}
|
|
425
643
|
|
|
426
|
-
|
|
644
|
+
if (!noShare) {
|
|
645
|
+
await shareFlow(resolve(output), videoTitle, description);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── Manual modes (--cmd / --url) ─────────────────────────
|
|
651
|
+
console.error("Generating description...");
|
|
652
|
+
const description = autoDescribe(flags.cmd, flags.url);
|
|
653
|
+
console.error(` "${description}"`);
|
|
654
|
+
|
|
655
|
+
let videoTitle = flags.title || flags.cmd || flags.url;
|
|
656
|
+
|
|
657
|
+
if (flags.cmd) {
|
|
427
658
|
console.error("Step 1/3: Recording CLI demo...");
|
|
428
|
-
const castPath = recordCLI(
|
|
659
|
+
const castPath = recordCLI(flags.cmd, process.cwd(), description, flags.guidelines);
|
|
429
660
|
|
|
430
661
|
console.error("Step 2/3: Extracting highlights...");
|
|
431
|
-
const highlightsPath = extractHighlightsFromCast(castPath,
|
|
662
|
+
const highlightsPath = extractHighlightsFromCast(castPath, description, flags.guidelines);
|
|
432
663
|
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
433
664
|
console.error(` ${highlights.length} highlights extracted`);
|
|
434
665
|
|
|
435
666
|
console.error("Step 3/3: Rendering video...");
|
|
436
667
|
await renderVideo({
|
|
437
668
|
title: videoTitle,
|
|
438
|
-
subtitle:
|
|
669
|
+
subtitle: description,
|
|
439
670
|
highlights,
|
|
440
|
-
endText:
|
|
671
|
+
endText: flags.cmd,
|
|
441
672
|
}, output, flags.music);
|
|
442
673
|
|
|
443
674
|
if (!noShare) {
|
|
444
|
-
await shareFlow(resolve(output), videoTitle,
|
|
675
|
+
await shareFlow(resolve(output), videoTitle, description);
|
|
445
676
|
}
|
|
446
677
|
return;
|
|
447
678
|
}
|
|
448
679
|
|
|
449
|
-
if (
|
|
450
|
-
const task =
|
|
680
|
+
if (flags.url) {
|
|
681
|
+
const task = description || "Explore the main features of this app";
|
|
451
682
|
|
|
452
683
|
ensureBrowserDeps();
|
|
453
684
|
console.error("Step 1/3: Recording browser demo...");
|
|
454
|
-
const videoPath = recordBrowser(
|
|
685
|
+
const videoPath = recordBrowser(flags.url, task, flags.auth, flags.guidelines);
|
|
455
686
|
|
|
456
|
-
// Copy video to Remotion public dir so it can be served
|
|
457
687
|
const publicDir = join(ROOT, "public");
|
|
458
688
|
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
459
689
|
copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
|
|
460
690
|
|
|
461
691
|
console.error("Step 2/3: Building highlights...");
|
|
462
|
-
|
|
463
|
-
// Read click data — this is the primary signal for highlights
|
|
464
692
|
const clicksPath = videoPath.replace(".mp4", "-clicks.json");
|
|
465
693
|
let allClicks = [];
|
|
466
694
|
if (existsSync(clicksPath)) {
|
|
467
695
|
allClicks = JSON.parse(readFileSync(clicksPath, "utf-8"));
|
|
468
696
|
console.error(` ${allClicks.length} clicks captured`);
|
|
469
697
|
}
|
|
470
|
-
|
|
471
698
|
const highlights = buildBrowserHighlights(allClicks, videoPath, task, flags.guidelines);
|
|
472
699
|
|
|
473
700
|
console.error("Step 3/3: Rendering video...");
|
|
474
701
|
await renderVideo({
|
|
475
702
|
title: videoTitle,
|
|
476
|
-
subtitle:
|
|
703
|
+
subtitle: description,
|
|
477
704
|
highlights,
|
|
478
|
-
endText:
|
|
479
|
-
endUrl:
|
|
705
|
+
endText: flags.url,
|
|
706
|
+
endUrl: flags.url,
|
|
480
707
|
}, output, flags.music);
|
|
481
708
|
|
|
482
709
|
if (!noShare) {
|
|
483
|
-
await shareFlow(resolve(output), videoTitle,
|
|
710
|
+
await shareFlow(resolve(output), videoTitle, description);
|
|
484
711
|
}
|
|
485
712
|
return;
|
|
486
713
|
}
|
package/package.json
CHANGED
package/scripts/browser_demo.py
CHANGED
|
@@ -30,11 +30,13 @@ def find_claude():
|
|
|
30
30
|
return "claude"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def generate_playwright_script(url, task):
|
|
33
|
+
def generate_playwright_script(url, task, guidelines=""):
|
|
34
34
|
"""Use claude CLI to generate a Playwright demo script."""
|
|
35
|
+
guidelines_part = f"IMPORTANT guidelines: {guidelines}. " if guidelines else ""
|
|
35
36
|
prompt = (
|
|
36
37
|
f"Generate a Playwright Python async function that demos a web app at {url}. "
|
|
37
38
|
f"Task: {task}. "
|
|
39
|
+
f"{guidelines_part}"
|
|
38
40
|
f"The function signature is: async def demo(page). "
|
|
39
41
|
f"Navigate to the URL, wait for load, interact with key features — "
|
|
40
42
|
f"click buttons, fill forms, scroll. Take about 20 seconds total. "
|
|
@@ -124,12 +126,12 @@ def extract_highlights(video_path, task):
|
|
|
124
126
|
return highlights
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
async def record_browser_demo(url, task, output_path, auth_state=None):
|
|
129
|
+
async def record_browser_demo(url, task, output_path, auth_state=None, guidelines=""):
|
|
128
130
|
"""Generate and run a Playwright demo with video recording."""
|
|
129
131
|
from playwright.async_api import async_playwright
|
|
130
132
|
|
|
131
133
|
print(f"Generating demo script for {url}...", file=sys.stderr)
|
|
132
|
-
script_code = generate_playwright_script(url, task)
|
|
134
|
+
script_code = generate_playwright_script(url, task, guidelines)
|
|
133
135
|
print(f"Script ready ({len(script_code)} chars)", file=sys.stderr)
|
|
134
136
|
|
|
135
137
|
video_dir = tempfile.mkdtemp()
|
|
@@ -269,12 +271,16 @@ if __name__ == "__main__":
|
|
|
269
271
|
# Parse remaining args: [task] [--auth <state_file>]
|
|
270
272
|
task = "Explore the main features"
|
|
271
273
|
auth_state = None
|
|
274
|
+
guidelines = ""
|
|
272
275
|
i = 3
|
|
273
276
|
while i < len(sys.argv):
|
|
274
277
|
if sys.argv[i] == "--auth" and i + 1 < len(sys.argv):
|
|
275
278
|
auth_state = sys.argv[i + 1]
|
|
276
279
|
i += 2
|
|
280
|
+
elif sys.argv[i] == "--guidelines" and i + 1 < len(sys.argv):
|
|
281
|
+
guidelines = sys.argv[i + 1]
|
|
282
|
+
i += 2
|
|
277
283
|
else:
|
|
278
284
|
task = sys.argv[i]
|
|
279
285
|
i += 1
|
|
280
|
-
asyncio.run(record_browser_demo(url, task, output, auth_state=auth_state))
|
|
286
|
+
asyncio.run(record_browser_demo(url, task, output, auth_state=auth_state, guidelines=guidelines))
|
package/scripts/cli_demo.py
CHANGED
|
@@ -33,12 +33,14 @@ def find_claude():
|
|
|
33
33
|
return "claude"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def generate_demo_plan(command: str, context: str) -> list[dict]:
|
|
36
|
+
def generate_demo_plan(command: str, context: str, guidelines: str = "") -> list[dict]:
|
|
37
37
|
"""Use claude CLI to plan a demo sequence."""
|
|
38
|
+
guidelines_block = f"\n\nIMPORTANT guidelines you MUST follow:\n{guidelines}" if guidelines else ""
|
|
39
|
+
|
|
38
40
|
prompt = f"""You are planning a terminal demo for a CLI tool. The tool is invoked with: {command}
|
|
39
41
|
|
|
40
42
|
Context about what this tool does:
|
|
41
|
-
{context}
|
|
43
|
+
{context}{guidelines_block}
|
|
42
44
|
|
|
43
45
|
Generate a JSON array of demo steps. Each step is an object with:
|
|
44
46
|
- "type": "command" (run a shell command)
|
|
@@ -196,7 +198,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
196
198
|
|
|
197
199
|
|
|
198
200
|
def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> list[dict]:
|
|
199
|
-
"""Ask Claude to pick
|
|
201
|
+
"""Ask Claude to pick highlight moments from the recorded session."""
|
|
200
202
|
# Read the asciicast and strip to just the text content
|
|
201
203
|
lines_output = []
|
|
202
204
|
with open(cast_path) as f:
|
|
@@ -216,7 +218,29 @@ def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> li
|
|
|
216
218
|
|
|
217
219
|
guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
|
|
218
220
|
|
|
219
|
-
|
|
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:
|
|
220
244
|
|
|
221
245
|
---
|
|
222
246
|
{clean[:3000]}
|
|
@@ -289,9 +313,10 @@ if __name__ == "__main__":
|
|
|
289
313
|
workdir = sys.argv[2]
|
|
290
314
|
output = sys.argv[3]
|
|
291
315
|
context = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
316
|
+
guidelines = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
292
317
|
|
|
293
318
|
print(f"Planning demo for: {command}", file=sys.stderr)
|
|
294
|
-
steps = generate_demo_plan(command, context)
|
|
319
|
+
steps = generate_demo_plan(command, context, guidelines)
|
|
295
320
|
print(f"Generated {len(steps)} steps:", file=sys.stderr)
|
|
296
321
|
for s in steps:
|
|
297
322
|
print(f" $ {s['value']} — {s.get('description', '')}", file=sys.stderr)
|
|
@@ -302,7 +327,7 @@ if __name__ == "__main__":
|
|
|
302
327
|
# Extract highlights from the recording
|
|
303
328
|
highlights_path = output.replace(".cast", "-highlights.json")
|
|
304
329
|
print("Extracting highlights...", file=sys.stderr)
|
|
305
|
-
highlights = extract_highlights(output, context)
|
|
330
|
+
highlights = extract_highlights(output, context, guidelines)
|
|
306
331
|
with open(highlights_path, "w") as f:
|
|
307
332
|
json.dump(highlights, f, indent=2)
|
|
308
333
|
print(f"Saved {len(highlights)} highlights to: {highlights_path}", file=sys.stderr)
|