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 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
- > 🔊 Turn on sound
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
- ## How it works
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
- 1. You provide a CLI command or URL
31
- 2. AI plans and executes a demo (terminal or browser)
32
- 3. AI picks the 3-4 best highlight moments
33
- 4. Renders a polished video with music, transitions, and overlays
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
- ## What you get
44
+ Requires [`gh` CLI](https://cli.github.com) to be installed and authenticated.
37
45
 
38
- A 15-20 second 1080x1080 video with:
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
- Ready for Twitter/X, LinkedIn, Reels.
48
+ The original mode — creates a short, polished clip for social media.
47
49
 
48
- ## Supports
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
- - **CLI demos** — records your tool in a terminal, shows the highlights
51
- - **Browser demos** — records your web app via Playwright, shows the key moments
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 === "--prompt" || arg === "-p") flags.prompt = args[++i];
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
- let demoCmd = flags.cmd;
408
- let demoURL = flags.url;
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
- let videoTitle = flags.title || demoCmd || demoURL;
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
- if (demoCmd) {
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(demoCmd, process.cwd(), prompt);
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, prompt, flags.guidelines);
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: prompt,
669
+ subtitle: description,
439
670
  highlights,
440
- endText: demoCmd,
671
+ endText: flags.cmd,
441
672
  }, output, flags.music);
442
673
 
443
674
  if (!noShare) {
444
- await shareFlow(resolve(output), videoTitle, prompt);
675
+ await shareFlow(resolve(output), videoTitle, description);
445
676
  }
446
677
  return;
447
678
  }
448
679
 
449
- if (demoURL) {
450
- const task = prompt || "Explore the main features of this app";
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(demoURL, task, flags.auth);
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: prompt,
703
+ subtitle: description,
477
704
  highlights,
478
- endText: demoURL,
479
- endUrl: demoURL,
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, prompt);
710
+ await shareFlow(resolve(output), videoTitle, description);
484
711
  }
485
712
  return;
486
713
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
@@ -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))
@@ -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 3-4 highlight moments from the recorded session."""
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
- prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
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)