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 +12 -45
- package/bin/agentreel.mjs +175 -185
- package/package.json +1 -1
- package/public/browser-demo.mp4 +0 -0
- package/public/music.mp3 +0 -0
- package/src/CastVideo.tsx +981 -638
- package/src/Root.tsx +16 -12
- package/src/types.ts +100 -48
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# agentreel
|
|
2
2
|
|
|
3
|
-
Turn your
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
58
|
-
2.
|
|
59
|
-
3. AI
|
|
60
|
-
4.
|
|
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
|
|
73
|
-
-g, --guidelines <text>
|
|
74
|
-
--music <file>
|
|
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:
|
|
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
|
|
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
|
|
63
|
+
console.log(`agentreel — Turn your apps into launch videos
|
|
66
64
|
|
|
67
65
|
Usage:
|
|
68
|
-
agentreel --
|
|
69
|
-
agentreel --
|
|
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
|
-
// ──
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
Colors: green="#
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
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 [
|
|
202
|
-
{
|
|
203
|
-
{
|
|
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
|
|
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,
|
|
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
|
|
183
|
+
const outFile = join(tmpdir(), "agentreel-browser.mp4");
|
|
226
184
|
|
|
227
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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="#
|
|
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="#
|
|
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
|
-
|
|
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="
|
|
370
|
-
blocks += `<circle cx="${PAD + 16}" cy="${y + BAR_H / 2}" r="
|
|
371
|
-
blocks += `<rect x="${PAD}" y="${y + BAR_H}" width="${W - PAD * 2}" height="${bodyH}" fill="#
|
|
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 ? "#
|
|
375
|
-
const prefix = line.isPrompt ? `<tspan fill="#
|
|
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="#
|
|
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="#
|
|
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
|
|
470
|
-
console.error("Please provide --
|
|
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
|
|
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/
|
|
541
|
-
const { videoPath, clicks } = await recordBrowser(flags.url,
|
|
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/
|
|
546
|
-
const
|
|
547
|
-
console.error("Step 3/
|
|
548
|
-
|
|
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
|
Binary file
|
package/public/music.mp3
CHANGED
|
Binary file
|