agentreel 0.6.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,8 +48,6 @@ 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];
55
52
  else if (a === "--subtitle" || a === "-s") flags.subtitle = args[++i];
56
53
  else if (a === "--output" || a === "-o") flags.output = args[++i];
@@ -63,16 +60,13 @@ function parseArgs() {
63
60
  }
64
61
 
65
62
  function printUsage() {
66
- console.log(`agentreel — Turn your apps into demo videos
63
+ console.log(`agentreel — Turn your apps into launch videos
67
64
 
68
65
  Usage:
69
- agentreel --pr 123 # demo a PR
70
- agentreel --cmd "npx my-tool" # CLI demo
71
- 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
72
68
 
73
69
  Flags:
74
- --pr <ref> PR number, owner/repo#N, or GitHub URL
75
- --start <cmd> start a dev server for browser PR demos
76
70
  -c, --cmd <cmd> CLI command to demo
77
71
  -u, --url <url> URL to demo (browser mode)
78
72
  -t, --title <text> video title
@@ -84,49 +78,7 @@ Flags:
84
78
  --no-share skip the share prompt`);
85
79
  }
86
80
 
87
- // ── PR Context ─────────────────────────────────────────────
88
-
89
- function fetchPRContext(prRef) {
90
- try { execFileSync("gh", ["--version"], { stdio: "ignore" }); }
91
- catch {
92
- console.error("Error: `gh` CLI required for --pr mode. Install from https://cli.github.com");
93
- process.exit(1);
94
- }
95
- const pr = JSON.parse(execFileSync("gh", [
96
- "pr", "view", String(prRef), "--json", "title,body,headRefName,url,number",
97
- ], { encoding: "utf-8", timeout: 30000 }));
98
-
99
- let diff = "";
100
- try { diff = execFileSync("gh", ["pr", "diff", String(prRef)], { encoding: "utf-8", timeout: 30000 }); }
101
- catch {}
102
-
103
- let readme = "";
104
- for (const name of ["README.md", "readme.md", "README"]) {
105
- const p = join(process.cwd(), name);
106
- if (existsSync(p)) { readme = readFileSync(p, "utf-8"); break; }
107
- }
108
- return { ...pr, diff, readme };
109
- }
110
-
111
- function planDemoFromPR(prContext, guidelines) {
112
- const extra = guidelines ? `\nAdditional guidelines: ${guidelines}` : "";
113
- const result = claude(`You are planning a demo for a Pull Request.
114
-
115
- PR Title: ${prContext.title}
116
- PR Description: ${prContext.body || "(none)"}
117
-
118
- Diff (truncated):
119
- ${prContext.diff.slice(0, 8000)}
120
-
121
- README (truncated):
122
- ${prContext.readme.slice(0, 3000)}${extra}
123
-
124
- Return JSON: {"type":"cli"|"browser", "command":"..." or null, "url":"..." or null, "description":"one sentence", "title":"2-4 words", "guidelines":"what to demo"}
125
- Show actual changes honestly. Return ONLY JSON.`);
126
- return parseJSON(result, { type: "cli", command: prContext.title, description: prContext.title, title: prContext.title, guidelines: "" });
127
- }
128
-
129
- // ── CLI Demo ───────────────────────────────────────────────
81
+ // ── CLI Recording ─────────────────────────────────────────
130
82
 
131
83
  function planDemoSteps(command, context, guidelines) {
132
84
  const extra = guidelines ? `\nIMPORTANT guidelines:\n${guidelines}` : "";
@@ -157,7 +109,7 @@ function executeSteps(steps, workDir) {
157
109
  return outputs;
158
110
  }
159
111
 
160
- function extractHighlights(outputs, context, guidelines, isDemo) {
112
+ function extractHighlights(outputs, context, guidelines) {
161
113
  const session = outputs.map(o =>
162
114
  `$ ${o.command}\n${o.stdout}${o.stderr ? `\n(stderr: ${o.stderr})` : ""}`
163
115
  ).join("\n\n");
@@ -167,46 +119,50 @@ function extractHighlights(outputs, context, guidelines, isDemo) {
167
119
  ? `Terminal output:\n---\n${session.slice(0, 6000)}\n---`
168
120
  : "(No terminal output captured — generate representative output from context.)";
169
121
 
170
- let prompt;
171
- if (isDemo) {
172
- prompt = `Create chapter-based highlights for a demo video.
173
- ${outputBlock}
174
-
175
- 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"}]}}
176
136
 
177
- Return JSON array. Each: {"label":"Chapter Name", "lines":[{"text":"...", "isPrompt":true|false, "color":"#hex", "bold":true|false, "dim":true|false}]}
178
- 4-6 chapters, 12-20 lines each. Show complete commands + output.
179
- Colors: green="#50fa7b" yellow="#f1fa8c" purple="#bd93f9" red="#ff5555" dim="#6272a4" white="#f8f8f2"
180
- Return ONLY JSON array.`;
181
- } else {
182
- prompt = `Create highlights for a CLI demo video.
183
137
  ${outputBlock}
184
138
 
185
139
  Context: ${context}${extra}
186
140
 
187
- Return JSON array. Each: {"label":"Name", "lines":[{"text":"...", "isPrompt":true|false, "color":"#hex", "bold":true|false, "dim":true|false}]}
188
- 3-4 highlights, 4-8 lines each.
189
- 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.
190
145
  Return ONLY JSON array.`;
191
- }
192
146
 
193
147
  const result = parseJSON(claude(prompt), null);
194
148
  if (result) return result;
195
149
 
196
150
  console.error(" Retrying highlight extraction...");
197
- const retry = parseJSON(claude(`Generate ${isDemo ? 4 : 3} terminal highlights as JSON.
198
- Context: ${context}
199
- Each: {"label":"Name", "lines":[{"text":"cmd", "isPrompt":true}, {"text":"output", "color":"#50fa7b"}]}
200
- 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);
201
154
  if (retry) return retry;
202
155
 
203
- return [{ label: "Run", lines: [
204
- { text: context || "demo", isPrompt: true },
205
- { text: " Done.", color: "#50fa7b" },
206
- ]}];
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
+ ];
207
163
  }
208
164
 
209
- // ── Browser Demo ───────────────────────────────────────────
165
+ // ── Browser Recording ─────────────────────────────────────
210
166
 
211
167
  async function ensurePlaywright() {
212
168
  try {
@@ -219,26 +175,72 @@ async function ensurePlaywright() {
219
175
  }
220
176
  }
221
177
 
222
- async function recordBrowser(url, task, authState, guidelines) {
178
+ async function recordBrowser(url, authState, guidelines) {
223
179
  const { chromium } = await import("playwright");
224
180
  const fs = await import("node:fs");
225
181
  const { mkdtemp } = await import("node:fs/promises");
226
182
  const videoDir = await mkdtemp(join(tmpdir(), "agentreel-"));
227
- const outFile = join(tmpdir(), "agentreel-browser-demo.mp4");
183
+ const outFile = join(tmpdir(), "agentreel-browser.mp4");
228
184
 
229
- 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();
230
205
 
231
- const extra = guidelines ? `\nGuidelines: ${guidelines}` : "";
232
- const scriptCode = claude(`Generate a Playwright JS async function body that demos ${url}.
233
- Task: ${task}${extra}
234
- The code will run inside: async (page) => { YOUR_CODE_HERE }
235
- Navigate, click buttons, fill forms, scroll. ~20 seconds total.
236
- Add await page.waitForTimeout(1500) between actions.
237
- Use timeout:5000, force:true on clicks. Wrap actions in try/catch.
238
- 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)}`;
239
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}...`);
240
243
  const recordingStartMs = Date.now();
241
- const browser = await chromium.launch({ headless: true });
242
244
  const ctxOpts = {
243
245
  viewport: { width: 1280, height: 800 },
244
246
  recordVideo: { dir: videoDir, size: { width: 1280, height: 800 } },
@@ -258,16 +260,21 @@ Return ONLY the function body, no function declaration, no imports.`);
258
260
  const page = await context.newPage();
259
261
  try { await page.goto(url, { waitUntil: "networkidle", timeout: 15000 }); }
260
262
  catch { await page.goto(url, { timeout: 15000 }); }
261
- await page.waitForTimeout(1000);
263
+ await page.waitForTimeout(1500);
262
264
 
263
- // Run the generated demo script in a sandboxed VM context
264
265
  try {
265
266
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
266
267
  const demoFn = new AsyncFunction("page", scriptCode);
267
268
  await demoFn(page);
268
269
  } catch (e) {
269
270
  console.error(` Demo script error: ${e.message}`);
270
- 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 {}
271
278
  }
272
279
 
273
280
  let clicks = [];
@@ -290,7 +297,7 @@ Return ONLY the function body, no function declaration, no imports.`);
290
297
  }
291
298
  }
292
299
 
293
- return { videoPath: outFile, clicks };
300
+ return { videoPath: outFile, clicks, siteContext };
294
301
  }
295
302
 
296
303
  function buildBrowserHighlights(clicks, task, guidelines) {
@@ -346,6 +353,53 @@ Labels: 1-2 words. Overlays: short with **bold**. Return ONLY JSON.`, 30000);
346
353
  return highlights;
347
354
  }
348
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
+
349
403
  // ── SVG Fallback ───────────────────────────────────────────
350
404
 
351
405
  function escSvg(s) {
@@ -355,36 +409,35 @@ function escSvg(s) {
355
409
  function renderSVG(props, output) {
356
410
  const FONT = '"SF Mono", "Fira Code", monospace';
357
411
  const SANS = '-apple-system, system-ui, sans-serif';
358
- const W = props.mode === "demo" ? 1200 : 700;
412
+ const W = 700;
359
413
  const PAD = 32, LINE_H = 22, TERM_PAD = 16, BAR_H = 36, GAP = 28, FS = 13;
360
414
 
361
415
  let y = PAD, blocks = "";
362
- 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>`;
363
417
  y += props.subtitle ? 68 : 56;
364
- 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>`;
365
419
 
366
420
  for (const hl of props.highlights) {
367
421
  if (!hl.lines?.length) continue;
368
- blocks += `<text x="${PAD}" y="${y + 14}" font-family="${escSvg(FONT)}" font-size="11" fill="#50fa7b" letter-spacing="2">${escSvg(hl.label.toUpperCase())}</text>`;
369
- y += 24;
422
+ y += 8;
370
423
  const bodyH = TERM_PAD * 2 + hl.lines.length * LINE_H;
371
- blocks += `<rect x="${PAD}" y="${y}" width="${W - PAD * 2}" height="${BAR_H + bodyH}" rx="8" fill="#1e1f29"/>`;
372
- 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"/>`;
373
- 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"/>`;
374
427
  let ly = y + BAR_H + TERM_PAD;
375
428
  for (const line of hl.lines) {
376
- const color = line.dim ? "#6272a4" : line.color || "#f8f8f2";
377
- 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>` : "";
378
431
  const text = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
379
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>`;
380
433
  ly += LINE_H;
381
434
  }
382
435
  y += BAR_H + bodyH + GAP;
383
436
  }
384
- 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; }
385
438
  y += PAD;
386
439
 
387
- 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>`);
388
441
  console.error(`\nDone: ${output} (SVG)`);
389
442
  }
390
443
 
@@ -439,87 +492,18 @@ async function shareFlow(outputPath, title, desc) {
439
492
  catch { console.error(` Link: ${intentURL}`); }
440
493
  }
441
494
 
442
- // ── Dev Server ─────────────────────────────────────────────
443
-
444
- function startDevServer(command) {
445
- console.error(` Starting: ${command}`);
446
- const proc = spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"], detached: true });
447
- return new Promise((resolve, reject) => {
448
- const timeout = setTimeout(() => resolve(proc), 30000);
449
- const onData = (d) => {
450
- if (/localhost|ready|started|listening|compiled/i.test(d.toString())) {
451
- clearTimeout(timeout);
452
- setTimeout(() => resolve(proc), 2000);
453
- }
454
- };
455
- proc.stdout.on("data", onData);
456
- proc.stderr.on("data", onData);
457
- proc.on("error", e => { clearTimeout(timeout); reject(e); });
458
- });
459
- }
460
-
461
- function stopDevServer(proc) {
462
- if (!proc?.killed) try { process.kill(-proc.pid, "SIGTERM"); } catch { try { proc.kill(); } catch {} }
463
- }
464
-
465
495
  // ── Main ───────────────────────────────────────────────────
466
496
 
467
497
  async function main() {
468
498
  const flags = parseArgs();
469
499
  const output = flags.output || "agentreel.mp4";
470
500
 
471
- if (!flags.cmd && !flags.url && !flags.pr) {
472
- console.error("Please provide --pr, --cmd, or --url.\n");
501
+ if (!flags.cmd && !flags.url) {
502
+ console.error("Please provide --cmd or --url.\n");
473
503
  printUsage();
474
504
  process.exit(1);
475
505
  }
476
506
 
477
- // ── PR mode ──────────────────────────────────────────
478
- if (flags.pr) {
479
- console.error("Fetching PR context...");
480
- const pr = fetchPRContext(flags.pr);
481
- console.error(` PR #${pr.number}: ${pr.title}`);
482
-
483
- console.error("Planning demo...");
484
- const plan = planDemoFromPR(pr, flags.guidelines);
485
- console.error(` Type: ${plan.type}, "${plan.description}"`);
486
-
487
- const title = flags.title || plan.title || pr.title;
488
- const subtitle = flags.subtitle || plan.description;
489
- const demoGuidelines = `[demo] ${plan.guidelines || ""}`.trim();
490
-
491
- if (plan.type === "browser") {
492
- let serverProc = null;
493
- try {
494
- if (flags.start) serverProc = await startDevServer(flags.start);
495
- await ensurePlaywright();
496
- console.error("Step 1/3: Recording browser demo...");
497
- const { videoPath, clicks } = await recordBrowser(plan.url || "http://localhost:3000", demoGuidelines, flags.auth, demoGuidelines);
498
- const publicDir = join(ROOT, "public");
499
- if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
500
- copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
501
- console.error("Step 2/3: Building highlights...");
502
- const highlights = buildBrowserHighlights(clicks, demoGuidelines, demoGuidelines);
503
- console.error("Step 3/3: Rendering...");
504
- await render({ title, subtitle, highlights, endText: pr.title, endUrl: pr.url, mode: "demo" }, output, flags.music);
505
- } finally { stopDevServer(serverProc); }
506
- } else {
507
- if (!plan.command) { console.error("Error: could not determine command to demo."); process.exit(1); }
508
- console.error("Step 1/3: Recording CLI demo...");
509
- const steps = planDemoSteps(plan.command, plan.description, demoGuidelines);
510
- console.error(` ${steps.length} steps planned`);
511
- const outputs = executeSteps(steps, process.cwd());
512
- console.error("Step 2/3: Extracting highlights...");
513
- const highlights = extractHighlights(outputs, plan.description, demoGuidelines, true);
514
- console.error(` ${highlights.length} highlights`);
515
- console.error("Step 3/3: Rendering...");
516
- await render({ title, subtitle, highlights, endText: plan.command, endUrl: pr.url, mode: "demo" }, output, flags.music);
517
- }
518
-
519
- if (!flags.noShare) await shareFlow(resolve(output), title, plan.description);
520
- return;
521
- }
522
-
523
507
  // ── CLI mode ─────────────────────────────────────────
524
508
  if (flags.cmd) {
525
509
  const title = flags.title || flags.cmd;
@@ -528,7 +512,7 @@ async function main() {
528
512
  console.error(` ${steps.length} steps planned`);
529
513
  const outputs = executeSteps(steps, process.cwd());
530
514
  console.error("Step 2/3: Extracting highlights...");
531
- const highlights = extractHighlights(outputs, flags.cmd, flags.guidelines, false);
515
+ const highlights = extractHighlights(outputs, flags.cmd, flags.guidelines);
532
516
  console.error(` ${highlights.length} highlights`);
533
517
  console.error("Step 3/3: Rendering...");
534
518
  await render({ title, highlights, endText: flags.cmd }, output, flags.music);
@@ -538,17 +522,20 @@ async function main() {
538
522
 
539
523
  // ── Browser mode ─────────────────────────────────────
540
524
  if (flags.url) {
541
- const title = flags.title || flags.url;
542
525
  await ensurePlaywright();
543
- console.error("Step 1/3: Recording browser demo...");
544
- 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);
545
528
  const publicDir = join(ROOT, "public");
546
529
  if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
547
530
  copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
548
- console.error("Step 2/3: Building highlights...");
549
- const highlights = buildBrowserHighlights(clicks, "Explore the main features", flags.guidelines);
550
- console.error("Step 3/3: Rendering...");
551
- 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);
552
539
  if (!flags.noShare) await shareFlow(resolve(output), title, flags.url);
553
540
  }
554
541
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.6.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