agentreel 0.5.0 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # agentreel
2
2
 
3
- Turn your web apps and CLIs into viral clips — or demo what a PR does.
3
+ Turn your apps into launch videos.
4
4
 
5
5
  https://github.com/user-attachments/assets/474fd85d-3b35-48f4-82b8-1b337840fb51
6
6
 
@@ -15,63 +15,31 @@ 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
-
26
- # CLI demo:
18
+ # CLI launch video:
27
19
  agentreel --cmd "npx my-cli-tool"
28
20
 
29
- # Browser demo:
21
+ # Browser launch video:
30
22
  agentreel --url http://localhost:3000
31
23
  ```
32
24
 
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.
38
-
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
43
-
44
- Requires [`gh` CLI](https://cli.github.com) to be installed and authenticated.
45
-
46
- ### Marketing reel (`--cmd` / `--url`)
47
-
48
- The original mode — creates a short, polished clip for social media.
49
-
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
54
-
55
25
  ## How it works
56
26
 
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
27
+ 1. You provide a CLI command or URL
28
+ 2. AI plans and executes the demo (terminal or Playwright browser)
29
+ 3. AI generates a launch video with text slides, terminal highlights, diagrams, and panels
30
+ 4. Renders a polished **1080x1080** video with Remotion — ready for Twitter/X, LinkedIn, Reels
62
31
 
63
32
  ## Flags
64
33
 
65
34
  ```
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
35
  -c, --cmd <command> CLI command to demo
69
36
  -u, --url <url> URL to demo (browser mode)
70
37
  -t, --title <text> video title
38
+ -s, --subtitle <text> video subtitle
71
39
  -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
40
+ -a, --auth <file> Playwright auth state for browser demos
41
+ -g, --guidelines <text> highlight generation guidelines
42
+ --music <file> background music mp3
75
43
  --no-share skip the share prompt
76
44
  ```
77
45
 
@@ -79,11 +47,10 @@ The original mode — creates a short, polished clip for social media.
79
47
 
80
48
  - Node.js 18+
81
49
  - Claude CLI (`claude`) — plans and records demos
82
- - `gh` CLI — required for `--pr` mode
83
50
 
84
51
  ## Credits
85
52
 
86
- Default background music: ["Go Create"](https://uppbeat.io/track/all-good-folks/go-create) by All Good Folks (via [Uppbeat](https://uppbeat.io))
53
+ Default background music: "Boogie Funky" by Petrushka Sound
87
54
 
88
55
  ## License
89
56
 
package/bin/agentreel.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execFileSync, spawnSync, spawn } from "node:child_process";
3
+ import { execFileSync, spawnSync } from "node:child_process";
4
4
  import { readFileSync, writeFileSync, statSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
5
5
  import { join, dirname, resolve } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { createInterface } from "node:readline";
9
- import vm from "node:vm";
10
9
 
11
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
11
  const ROOT = resolve(__dirname, "..");
@@ -49,9 +48,8 @@ function parseArgs() {
49
48
  }
50
49
  if (a === "--cmd" || a === "-c") flags.cmd = args[++i];
51
50
  else if (a === "--url" || a === "-u") flags.url = args[++i];
52
- else if (a === "--pr") flags.pr = args[++i];
53
- else if (a === "--start") flags.start = args[++i];
54
51
  else if (a === "--title" || a === "-t") flags.title = args[++i];
52
+ else if (a === "--subtitle" || a === "-s") flags.subtitle = args[++i];
55
53
  else if (a === "--output" || a === "-o") flags.output = args[++i];
56
54
  else if (a === "--music") flags.music = args[++i];
57
55
  else if (a === "--auth" || a === "-a") flags.auth = args[++i];
@@ -62,19 +60,17 @@ function parseArgs() {
62
60
  }
63
61
 
64
62
  function printUsage() {
65
- console.log(`agentreel — Turn your apps into demo videos
63
+ console.log(`agentreel — Turn your apps into launch videos
66
64
 
67
65
  Usage:
68
- agentreel --pr 123 # demo a PR
69
- agentreel --cmd "npx my-tool" # CLI demo
70
- agentreel --url http://localhost:3000 # browser demo
66
+ agentreel --cmd "npx my-tool" # CLI launch video
67
+ agentreel --url http://localhost:3000 # browser launch video
71
68
 
72
69
  Flags:
73
- --pr <ref> PR number, owner/repo#N, or GitHub URL
74
- --start <cmd> start a dev server for browser PR demos
75
70
  -c, --cmd <cmd> CLI command to demo
76
71
  -u, --url <url> URL to demo (browser mode)
77
72
  -t, --title <text> video title
73
+ -s, --subtitle <text> video subtitle
78
74
  -o, --output <file> output file (default: agentreel.mp4)
79
75
  -a, --auth <file> Playwright auth state for browser demos
80
76
  -g, --guidelines <t> highlight generation guidelines
@@ -82,49 +78,7 @@ Flags:
82
78
  --no-share skip the share prompt`);
83
79
  }
84
80
 
85
- // ── PR Context ─────────────────────────────────────────────
86
-
87
- function fetchPRContext(prRef) {
88
- try { execFileSync("gh", ["--version"], { stdio: "ignore" }); }
89
- catch {
90
- console.error("Error: `gh` CLI required for --pr mode. Install from https://cli.github.com");
91
- process.exit(1);
92
- }
93
- const pr = JSON.parse(execFileSync("gh", [
94
- "pr", "view", String(prRef), "--json", "title,body,headRefName,url,number",
95
- ], { encoding: "utf-8", timeout: 30000 }));
96
-
97
- let diff = "";
98
- try { diff = execFileSync("gh", ["pr", "diff", String(prRef)], { encoding: "utf-8", timeout: 30000 }); }
99
- catch {}
100
-
101
- let readme = "";
102
- for (const name of ["README.md", "readme.md", "README"]) {
103
- const p = join(process.cwd(), name);
104
- if (existsSync(p)) { readme = readFileSync(p, "utf-8"); break; }
105
- }
106
- return { ...pr, diff, readme };
107
- }
108
-
109
- function planDemoFromPR(prContext, guidelines) {
110
- const extra = guidelines ? `\nAdditional guidelines: ${guidelines}` : "";
111
- const result = claude(`You are planning a demo for a Pull Request.
112
-
113
- PR Title: ${prContext.title}
114
- PR Description: ${prContext.body || "(none)"}
115
-
116
- Diff (truncated):
117
- ${prContext.diff.slice(0, 8000)}
118
-
119
- README (truncated):
120
- ${prContext.readme.slice(0, 3000)}${extra}
121
-
122
- Return JSON: {"type":"cli"|"browser", "command":"..." or null, "url":"..." or null, "description":"one sentence", "title":"2-4 words", "guidelines":"what to demo"}
123
- Show actual changes honestly. Return ONLY JSON.`);
124
- return parseJSON(result, { type: "cli", command: prContext.title, description: prContext.title, title: prContext.title, guidelines: "" });
125
- }
126
-
127
- // ── CLI Demo ───────────────────────────────────────────────
81
+ // ── CLI Recording ─────────────────────────────────────────
128
82
 
129
83
  function planDemoSteps(command, context, guidelines) {
130
84
  const extra = guidelines ? `\nIMPORTANT guidelines:\n${guidelines}` : "";
@@ -155,7 +109,7 @@ function executeSteps(steps, workDir) {
155
109
  return outputs;
156
110
  }
157
111
 
158
- function extractHighlights(outputs, context, guidelines, isDemo) {
112
+ function extractHighlights(outputs, context, guidelines) {
159
113
  const session = outputs.map(o =>
160
114
  `$ ${o.command}\n${o.stdout}${o.stderr ? `\n(stderr: ${o.stderr})` : ""}`
161
115
  ).join("\n\n");
@@ -165,46 +119,50 @@ function extractHighlights(outputs, context, guidelines, isDemo) {
165
119
  ? `Terminal output:\n---\n${session.slice(0, 6000)}\n---`
166
120
  : "(No terminal output captured — generate representative output from context.)";
167
121
 
168
- let prompt;
169
- if (isDemo) {
170
- prompt = `Create chapter-based highlights for a demo video.
171
- ${outputBlock}
172
-
173
- Context: ${context}${extra}
122
+ const prompt = `Create highlights for a sleek launch video (like a product launch reel).
123
+ Mix these highlight types for maximum impact:
124
+
125
+ 1. Text slides — bold narrative statements:
126
+ {"label":"...", "statement":"Line one.\\nLine two."}
127
+ 2. Terminal highlights — actual CLI demo:
128
+ {"label":"...", "lines":[{"text":"...", "isPrompt":true|false, "color":"#hex", "bold":true|false, "dim":true|false}]}
129
+ 3. Animated tree — shows the tool's architecture/hierarchy as a branching visualization:
130
+ {"label":"...", "tree":{"root":"Root Label", "depth":4, "branching":[3,2,3], "nodeLabels":[["Child1","Child2","Child3"],["Grandchild1","Grandchild2"]]}}
131
+ The tree auto-generates a fractal structure. Make it CONTEXTUAL — root=the tool name, first-level children=its main modules/stages, second-level=sub-components. branching can be a number (uniform) or array (per-level). nodeLabels[0]=level 1 labels, nodeLabels[1]=level 2 labels (cycled if fewer than nodes).
132
+ 4. Side-by-side panels — two content cards comparing concepts:
133
+ {"label":"...", "panels":{"left":{"title":"...", "content":"Line1.\\nLine2."}, "right":{"title":"...", "content":"Line1.\\nLine2."}}}
134
+ 5. Diagram — manual node-and-edge flow (for pipelines/flows, not hierarchies):
135
+ {"label":"...", "diagram":{"nodes":[{"id":"...", "label":"...", "x":0.0-1.0, "y":0.0-1.0}], "edges":[{"from":"id", "to":"id"}]}}
174
136
 
175
- Return JSON array. Each: {"label":"Chapter Name", "lines":[{"text":"...", "isPrompt":true|false, "color":"#hex", "bold":true|false, "dim":true|false}]}
176
- 4-6 chapters, 12-20 lines each. Show complete commands + output.
177
- Colors: green="#50fa7b" yellow="#f1fa8c" purple="#bd93f9" red="#ff5555" dim="#6272a4" white="#f8f8f2"
178
- Return ONLY JSON array.`;
179
- } else {
180
- prompt = `Create highlights for a CLI demo video.
181
137
  ${outputBlock}
182
138
 
183
139
  Context: ${context}${extra}
184
140
 
185
- Return JSON array. Each: {"label":"Name", "lines":[{"text":"...", "isPrompt":true|false, "color":"#hex", "bold":true|false, "dim":true|false}]}
186
- 3-4 highlights, 4-8 lines each.
187
- Colors: green="#50fa7b" yellow="#f1fa8c" purple="#bd93f9" red="#ff5555" dim="#6272a4" white="#f8f8f2"
141
+ Structure: Open with a text slide (the hook), then 1-2 terminal highlights showing the tool in action, then a tree or panels to visualize the architecture, then close with a text slide (the payoff). 5-6 highlights total.
142
+ IMPORTANT: Trees and diagrams must reflect the ACTUAL tool being demoed — use real module names, real pipeline stages, real concepts from the context. Do NOT use generic labels.
143
+ Colors (light terminal): green="#16a34a" purple="#6d28d9" blue="#2563eb" red="#dc2626" dim="#9ca3af" default="#1a1a1a"
144
+ For text slides: keep statements punchy, 1-2 lines max.
188
145
  Return ONLY JSON array.`;
189
- }
190
146
 
191
147
  const result = parseJSON(claude(prompt), null);
192
148
  if (result) return result;
193
149
 
194
150
  console.error(" Retrying highlight extraction...");
195
- const retry = parseJSON(claude(`Generate ${isDemo ? 4 : 3} terminal highlights as JSON.
196
- Context: ${context}
197
- Each: {"label":"Name", "lines":[{"text":"cmd", "isPrompt":true}, {"text":"output", "color":"#50fa7b"}]}
198
- 8-15 lines per highlight. Return ONLY JSON array.`), null);
151
+ const retry = parseJSON(claude(`Generate a launch video with 4 highlights. Context: ${context}
152
+ Mix types: text slides {"label":"...", "statement":"Bold text"} and terminal {"label":"...", "lines":[{"text":"cmd", "isPrompt":true}, {"text":"output", "color":"#16a34a"}]}.
153
+ Start with a text slide hook, then terminal demos, end with a text slide payoff. Return ONLY JSON array.`), null);
199
154
  if (retry) return retry;
200
155
 
201
- return [{ label: "Run", lines: [
202
- { text: context || "demo", isPrompt: true },
203
- { text: " Done.", color: "#50fa7b" },
204
- ]}];
156
+ return [
157
+ { label: "Intro", statement: context || "Demo" },
158
+ { label: "Run", lines: [
159
+ { text: context || "demo", isPrompt: true },
160
+ { text: " Done.", color: "#16a34a" },
161
+ ]},
162
+ ];
205
163
  }
206
164
 
207
- // ── Browser Demo ───────────────────────────────────────────
165
+ // ── Browser Recording ─────────────────────────────────────
208
166
 
209
167
  async function ensurePlaywright() {
210
168
  try {
@@ -217,26 +175,72 @@ async function ensurePlaywright() {
217
175
  }
218
176
  }
219
177
 
220
- async function recordBrowser(url, task, authState, guidelines) {
178
+ async function recordBrowser(url, authState, guidelines) {
221
179
  const { chromium } = await import("playwright");
222
180
  const fs = await import("node:fs");
223
181
  const { mkdtemp } = await import("node:fs/promises");
224
182
  const videoDir = await mkdtemp(join(tmpdir(), "agentreel-"));
225
- const outFile = join(tmpdir(), "agentreel-browser-demo.mp4");
183
+ const outFile = join(tmpdir(), "agentreel-browser.mp4");
226
184
 
227
- console.error(` Recording ${url}...`);
185
+ // Step 1: Navigate and extract page content
186
+ console.error(` Loading ${url}...`);
187
+ const browser = await chromium.launch({ headless: true });
188
+ const scoutCtx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
189
+ const scoutPage = await scoutCtx.newPage();
190
+ try { await scoutPage.goto(url, { waitUntil: "networkidle", timeout: 15000 }); }
191
+ catch { await scoutPage.goto(url, { timeout: 15000 }); }
192
+ await scoutPage.waitForTimeout(1000);
193
+
194
+ // Extract visible text, title, headings
195
+ const pageContent = await scoutPage.evaluate(() => {
196
+ const title = document.title || "";
197
+ const meta = document.querySelector('meta[name="description"]')?.getAttribute("content") || "";
198
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3")).map(h => h.textContent?.trim()).filter(Boolean).slice(0, 10);
199
+ const buttons = Array.from(document.querySelectorAll("button, a[href]")).map(b => b.textContent?.trim()).filter(t => t && t.length < 40).slice(0, 10);
200
+ const body = document.body?.innerText?.slice(0, 3000) || "";
201
+ return { title, meta, headings, buttons, body };
202
+ });
203
+ await scoutPage.close();
204
+ await scoutCtx.close();
228
205
 
229
- const extra = guidelines ? `\nGuidelines: ${guidelines}` : "";
230
- const scriptCode = claude(`Generate a Playwright JS async function body that demos ${url}.
231
- Task: ${task}${extra}
232
- The code will run inside: async (page) => { YOUR_CODE_HERE }
233
- Navigate, click buttons, fill forms, scroll. ~20 seconds total.
234
- Add await page.waitForTimeout(1500) between actions.
235
- Use timeout:5000, force:true on clicks. Wrap actions in try/catch.
236
- Return ONLY the function body, no function declaration, no imports.`);
206
+ const siteContext = `Website: ${url}
207
+ Title: ${pageContent.title}
208
+ Description: ${pageContent.meta}
209
+ Headings: ${pageContent.headings.join(", ")}
210
+ Buttons/Links: ${pageContent.buttons.join(", ")}
211
+ Content (truncated): ${pageContent.body.slice(0, 1500)}`;
237
212
 
213
+ console.error(` Page: "${pageContent.title}" — ${pageContent.headings.slice(0, 3).join(", ")}`);
214
+
215
+ // Step 2: Generate Playwright demo script using actual page content
216
+ const extra = guidelines ? `\nGuidelines: ${guidelines}` : "";
217
+ console.error(` Generating demo script...`);
218
+ const scriptCode = claude(`Generate a Playwright JS async function body that demos this website.
219
+
220
+ ${siteContext}${extra}
221
+
222
+ The code runs inside: async (page) => { YOUR_CODE_HERE }
223
+
224
+ IMPORTANT RULES:
225
+ - page is already at ${url} — do NOT call page.goto()
226
+ - Start by scrolling down slowly to show the full page
227
+ - Use page.evaluate(() => window.scrollBy(0, 400)) for scrolling
228
+ - Click interesting buttons/links using page.click() with {timeout:5000, force:true}
229
+ - Add await page.waitForTimeout(2000) between actions
230
+ - Total ~25 seconds of activity
231
+ - Wrap each action in try/catch so failures don't stop the demo
232
+ - Return ONLY valid JS code — no comments before the first statement, no markdown
233
+
234
+ Example pattern:
235
+ await page.waitForTimeout(2000);
236
+ try { await page.evaluate(() => window.scrollBy({top: 500, behavior: 'smooth'})); } catch {}
237
+ await page.waitForTimeout(2000);
238
+ try { await page.click('text=Get Started', {timeout: 5000, force: true}); } catch {}
239
+ await page.waitForTimeout(2000);`);
240
+
241
+ // Step 3: Record with video
242
+ console.error(` Recording ${url}...`);
238
243
  const recordingStartMs = Date.now();
239
- const browser = await chromium.launch({ headless: true });
240
244
  const ctxOpts = {
241
245
  viewport: { width: 1280, height: 800 },
242
246
  recordVideo: { dir: videoDir, size: { width: 1280, height: 800 } },
@@ -256,16 +260,21 @@ Return ONLY the function body, no function declaration, no imports.`);
256
260
  const page = await context.newPage();
257
261
  try { await page.goto(url, { waitUntil: "networkidle", timeout: 15000 }); }
258
262
  catch { await page.goto(url, { timeout: 15000 }); }
259
- await page.waitForTimeout(1000);
263
+ await page.waitForTimeout(1500);
260
264
 
261
- // Run the generated demo script in a sandboxed VM context
262
265
  try {
263
266
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
264
267
  const demoFn = new AsyncFunction("page", scriptCode);
265
268
  await demoFn(page);
266
269
  } catch (e) {
267
270
  console.error(` Demo script error: ${e.message}`);
268
- await page.waitForTimeout(3000);
271
+ // Fallback: at least scroll the page
272
+ try {
273
+ for (let i = 0; i < 5; i++) {
274
+ await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" }));
275
+ await page.waitForTimeout(2000);
276
+ }
277
+ } catch {}
269
278
  }
270
279
 
271
280
  let clicks = [];
@@ -288,7 +297,7 @@ Return ONLY the function body, no function declaration, no imports.`);
288
297
  }
289
298
  }
290
299
 
291
- return { videoPath: outFile, clicks };
300
+ return { videoPath: outFile, clicks, siteContext };
292
301
  }
293
302
 
294
303
  function buildBrowserHighlights(clicks, task, guidelines) {
@@ -344,6 +353,53 @@ Labels: 1-2 words. Overlays: short with **bold**. Return ONLY JSON.`, 30000);
344
353
  return highlights;
345
354
  }
346
355
 
356
+ function wrapBrowserHighlights(browserHighlights, context, guidelines) {
357
+ const clipCount = browserHighlights.length;
358
+ const extra = guidelines ? `\nGuidelines: ${guidelines}` : "";
359
+
360
+ const prompt = `Create a launch video structure that wraps ${clipCount} browser demo clips.
361
+ The browser clips are already recorded — you need to create the narrative beats AROUND them.
362
+
363
+ Context: ${context}${extra}
364
+
365
+ Return a JSON array mixing these types:
366
+ 1. Text slides: {"label":"...", "statement":"Line one.\\nLine two."}
367
+ 2. Panels: {"label":"...", "panels":{"left":{"title":"...", "content":"..."}, "right":{"title":"...", "content":"..."}}}
368
+ 3. Trees: {"label":"...", "tree":{"root":"...", "depth":4, "branching":[4,3,2], "nodeLabels":[["child1","child2"]], "outro":"Closing text."}}
369
+ 4. Browser clip placeholder: {"_browserClip": true}
370
+
371
+ Structure: Open with a text slide hook, then alternate browser clips with narrative beats, close with a text slide or tree with outro. Use "_browserClip" as a placeholder where each recorded browser clip should go (use exactly ${clipCount} of them).
372
+
373
+ Return ONLY JSON array.`;
374
+
375
+ try {
376
+ const result = parseJSON(claude(prompt, 60000), null);
377
+ if (result && Array.isArray(result)) {
378
+ // Replace _browserClip placeholders with actual browser highlights
379
+ const final = [];
380
+ let clipIdx = 0;
381
+ for (const item of result) {
382
+ if (item._browserClip && clipIdx < browserHighlights.length) {
383
+ final.push(browserHighlights[clipIdx++]);
384
+ } else if (!item._browserClip) {
385
+ final.push(item);
386
+ }
387
+ }
388
+ // Append any remaining browser clips
389
+ while (clipIdx < browserHighlights.length) {
390
+ final.push(browserHighlights[clipIdx++]);
391
+ }
392
+ return final;
393
+ }
394
+ } catch {}
395
+
396
+ // Fallback: just wrap with a simple text slide
397
+ return [
398
+ { label: "Intro", statement: context || "Demo" },
399
+ ...browserHighlights,
400
+ ];
401
+ }
402
+
347
403
  // ── SVG Fallback ───────────────────────────────────────────
348
404
 
349
405
  function escSvg(s) {
@@ -353,36 +409,35 @@ function escSvg(s) {
353
409
  function renderSVG(props, output) {
354
410
  const FONT = '"SF Mono", "Fira Code", monospace';
355
411
  const SANS = '-apple-system, system-ui, sans-serif';
356
- const W = props.mode === "demo" ? 1200 : 700;
412
+ const W = 700;
357
413
  const PAD = 32, LINE_H = 22, TERM_PAD = 16, BAR_H = 36, GAP = 28, FS = 13;
358
414
 
359
415
  let y = PAD, blocks = "";
360
- blocks += `<text x="${W / 2}" y="${y + 28}" font-family="${escSvg(SANS)}" font-size="32" font-weight="800" fill="#f8f8f2" text-anchor="middle">${escSvg(props.title)}</text>`;
416
+ blocks += `<text x="${W / 2}" y="${y + 28}" font-family="${escSvg(SANS)}" font-size="32" font-weight="800" fill="#111" text-anchor="middle">${escSvg(props.title)}</text>`;
361
417
  y += props.subtitle ? 68 : 56;
362
- if (props.subtitle) blocks += `<text x="${W / 2}" y="${y - 22}" font-family="${escSvg(SANS)}" font-size="16" fill="#6272a4" text-anchor="middle">${escSvg(props.subtitle)}</text>`;
418
+ if (props.subtitle) blocks += `<text x="${W / 2}" y="${y - 22}" font-family="${escSvg(SANS)}" font-size="16" fill="#999" text-anchor="middle">${escSvg(props.subtitle)}</text>`;
363
419
 
364
420
  for (const hl of props.highlights) {
365
421
  if (!hl.lines?.length) continue;
366
- blocks += `<text x="${PAD}" y="${y + 14}" font-family="${escSvg(FONT)}" font-size="11" fill="#50fa7b" letter-spacing="2">${escSvg(hl.label.toUpperCase())}</text>`;
367
- y += 24;
422
+ y += 8;
368
423
  const bodyH = TERM_PAD * 2 + hl.lines.length * LINE_H;
369
- blocks += `<rect x="${PAD}" y="${y}" width="${W - PAD * 2}" height="${BAR_H + bodyH}" rx="8" fill="#1e1f29"/>`;
370
- blocks += `<circle cx="${PAD + 16}" cy="${y + BAR_H / 2}" r="5" fill="#ff5555"/><circle cx="${PAD + 34}" cy="${y + BAR_H / 2}" r="5" fill="#f1fa8c"/><circle cx="${PAD + 52}" cy="${y + BAR_H / 2}" r="5" fill="#50fa7b"/>`;
371
- blocks += `<rect x="${PAD}" y="${y + BAR_H}" width="${W - PAD * 2}" height="${bodyH}" fill="#282a36"/>`;
424
+ blocks += `<rect x="${PAD}" y="${y}" width="${W - PAD * 2}" height="${BAR_H + bodyH}" rx="12" fill="#f0f0f0"/>`;
425
+ blocks += `<circle cx="${PAD + 16}" cy="${y + BAR_H / 2}" r="4" fill="rgba(0,0,0,0.06)"/><circle cx="${PAD + 30}" cy="${y + BAR_H / 2}" r="4" fill="rgba(0,0,0,0.06)"/><circle cx="${PAD + 44}" cy="${y + BAR_H / 2}" r="4" fill="rgba(0,0,0,0.06)"/>`;
426
+ blocks += `<rect x="${PAD}" y="${y + BAR_H}" width="${W - PAD * 2}" height="${bodyH}" fill="#f8f8f8" rx="0"/>`;
372
427
  let ly = y + BAR_H + TERM_PAD;
373
428
  for (const line of hl.lines) {
374
- const color = line.dim ? "#6272a4" : line.color || "#f8f8f2";
375
- const prefix = line.isPrompt ? `<tspan fill="#50fa7b">$ </tspan>` : "";
429
+ const color = line.dim ? "#9ca3af" : line.color || "#1a1a1a";
430
+ const prefix = line.isPrompt ? `<tspan fill="#16a34a">$ </tspan>` : "";
376
431
  const text = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
377
432
  blocks += `<text x="${PAD + TERM_PAD}" y="${ly + FS}" font-family="${escSvg(FONT)}" font-size="${FS}" font-weight="${line.bold ? 700 : 400}" fill="${color}">${prefix}${escSvg(text)}</text>`;
378
433
  ly += LINE_H;
379
434
  }
380
435
  y += BAR_H + bodyH + GAP;
381
436
  }
382
- if (props.endUrl) { blocks += `<text x="${W / 2}" y="${y + 16}" font-family="${escSvg(SANS)}" font-size="14" fill="#6272a4" text-anchor="middle">${escSvg(props.endUrl)}</text>`; y += 28; }
437
+ if (props.endUrl) { blocks += `<text x="${W / 2}" y="${y + 16}" font-family="${escSvg(SANS)}" font-size="14" fill="#999" text-anchor="middle">${escSvg(props.endUrl)}</text>`; y += 28; }
383
438
  y += PAD;
384
439
 
385
- writeFileSync(output, `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${y}" viewBox="0 0 ${W} ${y}"><rect width="${W}" height="${y}" fill="#0f0f1a"/>${blocks}</svg>`);
440
+ writeFileSync(output, `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${y}" viewBox="0 0 ${W} ${y}"><rect width="${W}" height="${y}" fill="#fff"/>${blocks}</svg>`);
386
441
  console.error(`\nDone: ${output} (SVG)`);
387
442
  }
388
443
 
@@ -437,86 +492,18 @@ async function shareFlow(outputPath, title, desc) {
437
492
  catch { console.error(` Link: ${intentURL}`); }
438
493
  }
439
494
 
440
- // ── Dev Server ─────────────────────────────────────────────
441
-
442
- function startDevServer(command) {
443
- console.error(` Starting: ${command}`);
444
- const proc = spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"], detached: true });
445
- return new Promise((resolve, reject) => {
446
- const timeout = setTimeout(() => resolve(proc), 30000);
447
- const onData = (d) => {
448
- if (/localhost|ready|started|listening|compiled/i.test(d.toString())) {
449
- clearTimeout(timeout);
450
- setTimeout(() => resolve(proc), 2000);
451
- }
452
- };
453
- proc.stdout.on("data", onData);
454
- proc.stderr.on("data", onData);
455
- proc.on("error", e => { clearTimeout(timeout); reject(e); });
456
- });
457
- }
458
-
459
- function stopDevServer(proc) {
460
- if (!proc?.killed) try { process.kill(-proc.pid, "SIGTERM"); } catch { try { proc.kill(); } catch {} }
461
- }
462
-
463
495
  // ── Main ───────────────────────────────────────────────────
464
496
 
465
497
  async function main() {
466
498
  const flags = parseArgs();
467
499
  const output = flags.output || "agentreel.mp4";
468
500
 
469
- if (!flags.cmd && !flags.url && !flags.pr) {
470
- console.error("Please provide --pr, --cmd, or --url.\n");
501
+ if (!flags.cmd && !flags.url) {
502
+ console.error("Please provide --cmd or --url.\n");
471
503
  printUsage();
472
504
  process.exit(1);
473
505
  }
474
506
 
475
- // ── PR mode ──────────────────────────────────────────
476
- if (flags.pr) {
477
- console.error("Fetching PR context...");
478
- const pr = fetchPRContext(flags.pr);
479
- console.error(` PR #${pr.number}: ${pr.title}`);
480
-
481
- console.error("Planning demo...");
482
- const plan = planDemoFromPR(pr, flags.guidelines);
483
- console.error(` Type: ${plan.type}, "${plan.description}"`);
484
-
485
- const title = flags.title || plan.title || pr.title;
486
- const demoGuidelines = `[demo] ${plan.guidelines || ""}`.trim();
487
-
488
- if (plan.type === "browser") {
489
- let serverProc = null;
490
- try {
491
- if (flags.start) serverProc = await startDevServer(flags.start);
492
- await ensurePlaywright();
493
- console.error("Step 1/3: Recording browser demo...");
494
- const { videoPath, clicks } = await recordBrowser(plan.url || "http://localhost:3000", demoGuidelines, flags.auth, demoGuidelines);
495
- const publicDir = join(ROOT, "public");
496
- if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
497
- copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
498
- console.error("Step 2/3: Building highlights...");
499
- const highlights = buildBrowserHighlights(clicks, demoGuidelines, demoGuidelines);
500
- console.error("Step 3/3: Rendering...");
501
- await render({ title, subtitle: plan.description, highlights, endText: pr.title, endUrl: pr.url, mode: "demo" }, output, flags.music);
502
- } finally { stopDevServer(serverProc); }
503
- } else {
504
- if (!plan.command) { console.error("Error: could not determine command to demo."); process.exit(1); }
505
- console.error("Step 1/3: Recording CLI demo...");
506
- const steps = planDemoSteps(plan.command, plan.description, demoGuidelines);
507
- console.error(` ${steps.length} steps planned`);
508
- const outputs = executeSteps(steps, process.cwd());
509
- console.error("Step 2/3: Extracting highlights...");
510
- const highlights = extractHighlights(outputs, plan.description, demoGuidelines, true);
511
- console.error(` ${highlights.length} highlights`);
512
- console.error("Step 3/3: Rendering...");
513
- await render({ title, subtitle: plan.description, highlights, endText: plan.command, endUrl: pr.url, mode: "demo" }, output, flags.music);
514
- }
515
-
516
- if (!flags.noShare) await shareFlow(resolve(output), title, plan.description);
517
- return;
518
- }
519
-
520
507
  // ── CLI mode ─────────────────────────────────────────
521
508
  if (flags.cmd) {
522
509
  const title = flags.title || flags.cmd;
@@ -525,7 +512,7 @@ async function main() {
525
512
  console.error(` ${steps.length} steps planned`);
526
513
  const outputs = executeSteps(steps, process.cwd());
527
514
  console.error("Step 2/3: Extracting highlights...");
528
- const highlights = extractHighlights(outputs, flags.cmd, flags.guidelines, false);
515
+ const highlights = extractHighlights(outputs, flags.cmd, flags.guidelines);
529
516
  console.error(` ${highlights.length} highlights`);
530
517
  console.error("Step 3/3: Rendering...");
531
518
  await render({ title, highlights, endText: flags.cmd }, output, flags.music);
@@ -535,17 +522,20 @@ async function main() {
535
522
 
536
523
  // ── Browser mode ─────────────────────────────────────
537
524
  if (flags.url) {
538
- const title = flags.title || flags.url;
539
525
  await ensurePlaywright();
540
- console.error("Step 1/3: Recording browser demo...");
541
- const { videoPath, clicks } = await recordBrowser(flags.url, "Explore the main features", flags.auth, flags.guidelines);
526
+ console.error("Step 1/4: Recording browser demo...");
527
+ const { videoPath, clicks, siteContext } = await recordBrowser(flags.url, flags.auth, flags.guidelines);
542
528
  const publicDir = join(ROOT, "public");
543
529
  if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
544
530
  copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
545
- console.error("Step 2/3: Building highlights...");
546
- const highlights = buildBrowserHighlights(clicks, "Explore the main features", flags.guidelines);
547
- console.error("Step 3/3: Rendering...");
548
- await render({ title, highlights, endText: flags.url, endUrl: flags.url }, output, flags.music);
531
+ console.error("Step 2/4: Building browser clips...");
532
+ const browserClips = buildBrowserHighlights(clicks, siteContext, flags.guidelines);
533
+ console.error("Step 3/4: Generating launch video...");
534
+ const highlights = wrapBrowserHighlights(browserClips, siteContext, flags.guidelines);
535
+ console.error(` ${highlights.length} highlights (${browserClips.length} browser clips + narrative beats)`);
536
+ const title = flags.title || siteContext.split("\n")[1]?.replace("Title: ", "") || flags.url;
537
+ console.error("Step 4/4: Rendering...");
538
+ await render({ title, subtitle: flags.subtitle, highlights, endText: flags.url, endUrl: flags.url }, output, flags.music);
549
539
  if (!flags.noShare) await shareFlow(resolve(output), title, flags.url);
550
540
  }
551
541
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Turn your apps into demo videos",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
Binary file
package/public/music.mp3 CHANGED
Binary file