agentreel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/bin/agentreel.mjs +461 -0
- package/package.json +36 -0
- package/public/browser-demo.mp4 +0 -0
- package/public/music.mp3 +0 -0
- package/public/screenshot.png +0 -0
- package/scripts/browser_demo.py +215 -0
- package/scripts/cli_demo.py +272 -0
- package/src/CastVideo.tsx +1000 -0
- package/src/Root.tsx +26 -0
- package/src/index.ts +4 -0
- package/src/types.ts +82 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# agentreel
|
|
2
|
+
|
|
3
|
+
Turn your Claude Code sessions into viral demo videos.
|
|
4
|
+
|
|
5
|
+
https://github.com/user-attachments/assets/070ee610-298c-4989-8d7e-369ca495469e
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx agentreel
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# After Claude builds something, just run:
|
|
17
|
+
agentreel
|
|
18
|
+
|
|
19
|
+
# It reads your session, detects what was built, records a demo,
|
|
20
|
+
# picks the highlights, and renders a video. One command.
|
|
21
|
+
|
|
22
|
+
# Manual mode:
|
|
23
|
+
agentreel --cmd "npx my-cli-tool" # CLI demo
|
|
24
|
+
agentreel --url http://localhost:3000 # browser demo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
1. Reads your Claude Code session log
|
|
30
|
+
2. Detects what was built — CLI tool or web app
|
|
31
|
+
3. Claude plans and executes a demo (terminal or browser)
|
|
32
|
+
4. Claude picks the 3-4 best highlight moments
|
|
33
|
+
5. Renders a polished video with music, transitions, and overlays
|
|
34
|
+
6. Prompts you to share on Twitter
|
|
35
|
+
|
|
36
|
+
## What you get
|
|
37
|
+
|
|
38
|
+
A 15-20 second 1080x1080 video with:
|
|
39
|
+
- **Title card** with your project name
|
|
40
|
+
- **Highlight clips** — terminal or browser window on animated gradient
|
|
41
|
+
- **Text overlays** — bold captions that work on mute
|
|
42
|
+
- **Cursor + typing** — looks like someone's actually using it
|
|
43
|
+
- **Background music** with fade in/out
|
|
44
|
+
- **End CTA** — install command + URL
|
|
45
|
+
|
|
46
|
+
Ready for Twitter/X, LinkedIn, Reels.
|
|
47
|
+
|
|
48
|
+
## Supports
|
|
49
|
+
|
|
50
|
+
- **CLI demos** — records your tool in a terminal, shows the highlights
|
|
51
|
+
- **Browser demos** — records your web app via Playwright, shows the key moments
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
- Node.js 18+
|
|
56
|
+
- Python 3.10+
|
|
57
|
+
- Claude CLI (`claude`)
|
|
58
|
+
|
|
59
|
+
## Credits
|
|
60
|
+
|
|
61
|
+
Default background music: ["Go Create"](https://uppbeat.io/track/all-good-folks/go-create) by All Good Folks (via [Uppbeat](https://uppbeat.io))
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, copyFileSync, createReadStream } from "node:fs";
|
|
5
|
+
import { join, dirname, basename, resolve } from "node:path";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = resolve(__dirname, "..");
|
|
12
|
+
|
|
13
|
+
// ── CLI flags ───────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function parseArgs() {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const flags = {};
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg === "--help" || arg === "-h") { printUsage(); process.exit(0); }
|
|
21
|
+
if (arg === "--version" || arg === "-v") { console.log("0.1.0"); process.exit(0); }
|
|
22
|
+
if (arg === "--cmd" || arg === "-c") flags.cmd = args[++i];
|
|
23
|
+
else if (arg === "--url" || arg === "-u") flags.url = args[++i];
|
|
24
|
+
else if (arg === "--prompt" || arg === "-p") flags.prompt = args[++i];
|
|
25
|
+
else if (arg === "--title" || arg === "-t") flags.title = args[++i];
|
|
26
|
+
else if (arg === "--output" || arg === "-o") flags.output = args[++i];
|
|
27
|
+
else if (arg === "--music") flags.music = args[++i];
|
|
28
|
+
else if (arg === "--session") flags.session = args[++i];
|
|
29
|
+
else if (arg === "--no-share") flags.noShare = true;
|
|
30
|
+
}
|
|
31
|
+
return flags;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printUsage() {
|
|
35
|
+
console.log(`agentreel — Turn Claude Code sessions into viral demo videos
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
agentreel # auto-detect from session
|
|
39
|
+
agentreel --cmd "npx @islo-labs/overtime" # manual CLI demo
|
|
40
|
+
agentreel --url http://localhost:3000 # manual browser demo
|
|
41
|
+
|
|
42
|
+
Flags:
|
|
43
|
+
-c, --cmd <command> CLI command to demo
|
|
44
|
+
-u, --url <url> URL to demo (browser mode)
|
|
45
|
+
-p, --prompt <text> description of what the tool does
|
|
46
|
+
-t, --title <text> video title
|
|
47
|
+
-o, --output <file> output file (default: agentreel.mp4)
|
|
48
|
+
--music <file> path to background music mp3
|
|
49
|
+
--session <file> path to Claude Code session .jsonl
|
|
50
|
+
--no-share skip the share prompt
|
|
51
|
+
-h, --help show help
|
|
52
|
+
-v, --version show version`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Session parser ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function findLatestSession() {
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const projectKey = cwd.replaceAll("/", "-");
|
|
60
|
+
const projectDir = join(homedir(), ".claude", "projects", projectKey);
|
|
61
|
+
|
|
62
|
+
if (!existsSync(projectDir)) return null;
|
|
63
|
+
|
|
64
|
+
let newest = null;
|
|
65
|
+
let newestTime = 0;
|
|
66
|
+
for (const entry of readdirSync(projectDir)) {
|
|
67
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
68
|
+
const full = join(projectDir, entry);
|
|
69
|
+
const mtime = statSync(full).mtimeMs;
|
|
70
|
+
if (mtime > newestTime) { newestTime = mtime; newest = full; }
|
|
71
|
+
}
|
|
72
|
+
return newest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSession(path) {
|
|
76
|
+
const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean);
|
|
77
|
+
const session = { prompt: "", title: "", actions: [], startTime: null, endTime: null };
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
let obj;
|
|
81
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
82
|
+
|
|
83
|
+
const ts = obj.timestamp ? new Date(obj.timestamp) : null;
|
|
84
|
+
if (ts && !isNaN(ts)) {
|
|
85
|
+
if (!session.startTime || ts < session.startTime) session.startTime = ts;
|
|
86
|
+
if (!session.endTime || ts > session.endTime) session.endTime = ts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (obj.type === "user" && !session.prompt) {
|
|
90
|
+
session.prompt = extractPrompt(obj);
|
|
91
|
+
}
|
|
92
|
+
if (obj.type === "custom-title" && obj.customTitle) {
|
|
93
|
+
session.title = obj.customTitle;
|
|
94
|
+
}
|
|
95
|
+
if (obj.type === "assistant") {
|
|
96
|
+
const content = obj.message?.content;
|
|
97
|
+
if (!Array.isArray(content)) continue;
|
|
98
|
+
for (const block of content) {
|
|
99
|
+
if (block.type !== "tool_use") continue;
|
|
100
|
+
const action = parseToolUse(block.name, block.input, ts);
|
|
101
|
+
if (action) session.actions.push(action);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
session.actions.sort((a, b) => (a.time || 0) - (b.time || 0));
|
|
107
|
+
if (session.startTime && session.endTime) {
|
|
108
|
+
session.durationMs = session.endTime - session.startTime;
|
|
109
|
+
}
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractPrompt(obj) {
|
|
114
|
+
const content = obj.message?.content;
|
|
115
|
+
if (typeof content === "string") return cleanPrompt(content);
|
|
116
|
+
if (Array.isArray(content)) {
|
|
117
|
+
for (const block of content) {
|
|
118
|
+
if (block.type === "text" && block.text) return cleanPrompt(block.text);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cleanPrompt(s) {
|
|
125
|
+
for (const line of s.split("\n")) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed || /^[│├└─┌┐]/.test(trimmed)) continue;
|
|
128
|
+
return trimmed.slice(0, 200);
|
|
129
|
+
}
|
|
130
|
+
return s.slice(0, 200);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseToolUse(name, input, ts) {
|
|
134
|
+
if (!input) return null;
|
|
135
|
+
switch (name) {
|
|
136
|
+
case "Read": return { type: "read", filePath: input.file_path, time: ts };
|
|
137
|
+
case "Write": return { type: "write", filePath: input.file_path, size: input.content?.length || 0, time: ts };
|
|
138
|
+
case "Edit": return { type: "edit", filePath: input.file_path, time: ts };
|
|
139
|
+
case "Bash": return { type: "bash", command: input.command, time: ts };
|
|
140
|
+
case "Grep": case "Glob": return { type: "search", time: ts };
|
|
141
|
+
case "Agent": return { type: "agent", time: ts };
|
|
142
|
+
default: return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Detection ───────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function detectResult(session) {
|
|
149
|
+
const containsAny = (s, ...subs) => subs.some(sub => s.includes(sub));
|
|
150
|
+
|
|
151
|
+
for (const a of session.actions) {
|
|
152
|
+
if (a.type === "bash") {
|
|
153
|
+
const cmd = (a.command || "").toLowerCase();
|
|
154
|
+
if (containsAny(cmd, "npm run dev", "npm start", "npx next", "npx vite", "yarn dev", "pnpm dev", "flask run", "uvicorn")) {
|
|
155
|
+
const url = extractURL(a.command) || "http://localhost:3000";
|
|
156
|
+
return { type: "browser", command: url };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const a of session.actions) {
|
|
162
|
+
if ((a.type === "write" || a.type === "edit") && a.filePath?.endsWith("package.json")) {
|
|
163
|
+
try {
|
|
164
|
+
const pkg = JSON.parse(readFileSync(a.filePath, "utf-8"));
|
|
165
|
+
if (pkg.bin && pkg.name) return { type: "cli", command: `npx ${pkg.name} --help` };
|
|
166
|
+
} catch { /* skip */ }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const a of session.actions) {
|
|
171
|
+
if (a.type === "bash" && a.command?.includes("go build")) {
|
|
172
|
+
const parts = a.command.split(/\s+/);
|
|
173
|
+
const oIdx = parts.indexOf("-o");
|
|
174
|
+
if (oIdx !== -1 && parts[oIdx + 1]) return { type: "cli", command: `${parts[oIdx + 1]} --help` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (let i = session.actions.length - 1; i >= 0; i--) {
|
|
179
|
+
const a = session.actions[i];
|
|
180
|
+
if (a.type !== "bash") continue;
|
|
181
|
+
const cmd = (a.command || "").trim();
|
|
182
|
+
if (containsAny(cmd, "go build", "go test", "npm install", "npm test", "git ", "mkdir", "ls ", "cat ")) continue;
|
|
183
|
+
if (containsAny(cmd, "npx ", "./bin/", "./dist/", "go run", "python ", "node ")) {
|
|
184
|
+
return { type: "cli", command: cmd };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { type: "unknown" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractURL(cmd) {
|
|
192
|
+
for (const part of (cmd || "").split(/\s+/)) {
|
|
193
|
+
if (part.includes("localhost:")) return part.startsWith("http") ? part : `http://${part}`;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Recording + Highlights ──────────────────────────────────
|
|
199
|
+
|
|
200
|
+
function findPython() {
|
|
201
|
+
const venvPython = join(ROOT, "scripts", ".venv", "bin", "python");
|
|
202
|
+
if (existsSync(venvPython)) return venvPython;
|
|
203
|
+
return "python3";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function recordCLI(command, workDir, context) {
|
|
207
|
+
const python = findPython();
|
|
208
|
+
const script = join(ROOT, "scripts", "cli_demo.py");
|
|
209
|
+
const outFile = join(tmpdir(), "agentreel-cli-demo.cast");
|
|
210
|
+
|
|
211
|
+
const args = [script, command, workDir, outFile];
|
|
212
|
+
if (context) args.push(context);
|
|
213
|
+
|
|
214
|
+
console.error(`Agent planning CLI demo for: ${command}`);
|
|
215
|
+
execFileSync(python, args, { stdio: ["ignore", "inherit", "inherit"], env: process.env });
|
|
216
|
+
return outFile;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractHighlightsFromCast(castPath, context) {
|
|
220
|
+
const python = findPython();
|
|
221
|
+
const script = join(ROOT, "scripts", "cli_demo.py");
|
|
222
|
+
const outFile = castPath + "-highlights.json";
|
|
223
|
+
|
|
224
|
+
const args = [script, "--highlights", castPath, outFile];
|
|
225
|
+
if (context) args.push(context);
|
|
226
|
+
|
|
227
|
+
execFileSync(python, args, { stdio: ["ignore", "inherit", "inherit"], env: process.env });
|
|
228
|
+
return outFile;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Browser Recording ───────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function recordBrowser(url, task) {
|
|
234
|
+
const python = findPython();
|
|
235
|
+
const script = join(ROOT, "scripts", "browser_demo.py");
|
|
236
|
+
const outFile = join(tmpdir(), "agentreel-browser-demo.mp4");
|
|
237
|
+
|
|
238
|
+
console.error(`Agent demoing browser app: ${url}`);
|
|
239
|
+
execFileSync(python, [script, url, outFile, task], {
|
|
240
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
241
|
+
env: process.env,
|
|
242
|
+
timeout: 120000,
|
|
243
|
+
});
|
|
244
|
+
return outFile;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractBrowserHighlights(videoPath, task) {
|
|
248
|
+
const python = findPython();
|
|
249
|
+
const script = join(ROOT, "scripts", "browser_demo.py");
|
|
250
|
+
const outFile = videoPath + "-highlights.json";
|
|
251
|
+
|
|
252
|
+
execFileSync(python, [script, "--highlights", videoPath, outFile, task], {
|
|
253
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
254
|
+
env: process.env,
|
|
255
|
+
});
|
|
256
|
+
return outFile;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Render ──────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function renderVideo(props, output, musicPath) {
|
|
262
|
+
const publicDir = join(ROOT, "public");
|
|
263
|
+
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
264
|
+
if (musicPath && existsSync(musicPath)) {
|
|
265
|
+
copyFileSync(musicPath, join(publicDir, "music.mp3"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const absOutput = resolve(output);
|
|
269
|
+
const propsJSON = JSON.stringify(props);
|
|
270
|
+
const remotion = join(ROOT, "node_modules", ".bin", "remotion");
|
|
271
|
+
|
|
272
|
+
execFileSync(remotion, ["render", "CastVideo", absOutput, "--props", propsJSON], {
|
|
273
|
+
cwd: ROOT,
|
|
274
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const size = statSync(absOutput).size;
|
|
278
|
+
console.error(`\nDone: ${output} (${Math.round(size / 1024)} KB)`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Upload + Share ──────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function uploadToStreamable(filePath) {
|
|
284
|
+
const { FormData, File } = await import("node:buffer")
|
|
285
|
+
.then(() => globalThis)
|
|
286
|
+
.catch(() => globalThis);
|
|
287
|
+
|
|
288
|
+
const fileBuffer = readFileSync(filePath);
|
|
289
|
+
const fileName = basename(filePath);
|
|
290
|
+
|
|
291
|
+
// Use multipart form upload via fetch
|
|
292
|
+
const boundary = "----agentreel" + Date.now();
|
|
293
|
+
const CRLF = "\r\n";
|
|
294
|
+
|
|
295
|
+
const header = [
|
|
296
|
+
`--${boundary}`,
|
|
297
|
+
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
|
|
298
|
+
"Content-Type: video/mp4",
|
|
299
|
+
"",
|
|
300
|
+
].join(CRLF);
|
|
301
|
+
|
|
302
|
+
const footer = `${CRLF}--${boundary}--${CRLF}`;
|
|
303
|
+
|
|
304
|
+
const headerBuf = Buffer.from(header + CRLF);
|
|
305
|
+
const footerBuf = Buffer.from(footer);
|
|
306
|
+
const body = Buffer.concat([headerBuf, fileBuffer, footerBuf]);
|
|
307
|
+
|
|
308
|
+
const resp = await fetch("https://api.streamable.com/upload", {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: {
|
|
311
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
312
|
+
},
|
|
313
|
+
body,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (!resp.ok) {
|
|
317
|
+
const text = await resp.text();
|
|
318
|
+
throw new Error(`Streamable upload failed (${resp.status}): ${text}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const data = await resp.json();
|
|
322
|
+
return `https://streamable.com/${data.shortcode}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function openShareURL(videoURL, text) {
|
|
326
|
+
const tweetText = encodeURIComponent(text);
|
|
327
|
+
const encodedURL = encodeURIComponent(videoURL);
|
|
328
|
+
const intentURL = `https://twitter.com/intent/tweet?text=${tweetText}&url=${encodedURL}`;
|
|
329
|
+
|
|
330
|
+
console.error(`\n Share: ${videoURL}`);
|
|
331
|
+
console.error(` Tweet: ${intentURL}\n`);
|
|
332
|
+
|
|
333
|
+
// Open in browser
|
|
334
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
335
|
+
try {
|
|
336
|
+
execFileSync(cmd, [intentURL], { stdio: "ignore" });
|
|
337
|
+
} catch {
|
|
338
|
+
console.error(" (Could not open browser — copy the link above)");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function askYesNo(question) {
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
345
|
+
rl.question(question, (answer) => {
|
|
346
|
+
rl.close();
|
|
347
|
+
resolve(answer.trim().toLowerCase() !== "n");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function shareFlow(outputPath, title) {
|
|
353
|
+
const shouldShare = await askYesNo("Share to Twitter? [Y/n] ");
|
|
354
|
+
if (!shouldShare) return;
|
|
355
|
+
|
|
356
|
+
console.error("Uploading to Streamable...");
|
|
357
|
+
try {
|
|
358
|
+
const url = await uploadToStreamable(outputPath);
|
|
359
|
+
const text = `${title}\n\nMade with @agentreel`;
|
|
360
|
+
openShareURL(url, text);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error(`Upload failed: ${err.message}`);
|
|
363
|
+
console.error("You can manually upload the video and share it.");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Main ────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
async function main() {
|
|
370
|
+
const flags = parseArgs();
|
|
371
|
+
const output = flags.output || "agentreel.mp4";
|
|
372
|
+
const noShare = flags.noShare;
|
|
373
|
+
|
|
374
|
+
let demoCmd = flags.cmd;
|
|
375
|
+
let demoURL = flags.url;
|
|
376
|
+
let prompt = flags.prompt;
|
|
377
|
+
|
|
378
|
+
// Auto-detect from Claude session if no manual flags
|
|
379
|
+
if (!demoCmd && !demoURL) {
|
|
380
|
+
const sessionPath = flags.session || findLatestSession();
|
|
381
|
+
if (!sessionPath) {
|
|
382
|
+
console.error("No session found and no --cmd or --url provided.\n");
|
|
383
|
+
printUsage();
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.error(`Reading session: ${basename(sessionPath)}`);
|
|
388
|
+
const session = parseSession(sessionPath);
|
|
389
|
+
if (!prompt) prompt = session.prompt;
|
|
390
|
+
|
|
391
|
+
const detected = detectResult(session);
|
|
392
|
+
if (detected.type === "cli") {
|
|
393
|
+
demoCmd = detected.command;
|
|
394
|
+
console.error(`Detected CLI: ${demoCmd}`);
|
|
395
|
+
} else if (detected.type === "browser") {
|
|
396
|
+
demoURL = detected.command;
|
|
397
|
+
console.error(`Detected browser: ${demoURL}`);
|
|
398
|
+
} else {
|
|
399
|
+
console.error("Couldn't detect what was built. Use --cmd or --url.");
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let videoTitle = flags.title || demoCmd || demoURL;
|
|
405
|
+
|
|
406
|
+
if (demoCmd) {
|
|
407
|
+
console.error("Step 1/3: Recording CLI demo...");
|
|
408
|
+
const castPath = recordCLI(demoCmd, process.cwd(), prompt);
|
|
409
|
+
|
|
410
|
+
console.error("Step 2/3: Extracting highlights...");
|
|
411
|
+
const highlightsPath = extractHighlightsFromCast(castPath, prompt);
|
|
412
|
+
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
413
|
+
console.error(` ${highlights.length} highlights extracted`);
|
|
414
|
+
|
|
415
|
+
console.error("Step 3/3: Rendering video...");
|
|
416
|
+
renderVideo({
|
|
417
|
+
title: videoTitle,
|
|
418
|
+
subtitle: prompt,
|
|
419
|
+
highlights,
|
|
420
|
+
endText: demoCmd,
|
|
421
|
+
}, output, flags.music);
|
|
422
|
+
|
|
423
|
+
if (!noShare) {
|
|
424
|
+
await shareFlow(resolve(output), videoTitle);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (demoURL) {
|
|
430
|
+
const task = prompt || "Explore the main features of this app";
|
|
431
|
+
|
|
432
|
+
console.error("Step 1/3: Recording browser demo...");
|
|
433
|
+
const videoPath = recordBrowser(demoURL, task);
|
|
434
|
+
|
|
435
|
+
// Copy video to Remotion public dir so it can be served
|
|
436
|
+
const publicDir = join(ROOT, "public");
|
|
437
|
+
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
438
|
+
copyFileSync(videoPath, join(publicDir, "browser-demo.mp4"));
|
|
439
|
+
|
|
440
|
+
console.error("Step 2/3: Extracting highlights...");
|
|
441
|
+
const highlightsPath = extractBrowserHighlights(videoPath, task);
|
|
442
|
+
const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
|
|
443
|
+
console.error(` ${highlights.length} highlights extracted`);
|
|
444
|
+
|
|
445
|
+
console.error("Step 3/3: Rendering video...");
|
|
446
|
+
renderVideo({
|
|
447
|
+
title: videoTitle,
|
|
448
|
+
subtitle: prompt,
|
|
449
|
+
highlights,
|
|
450
|
+
endText: demoURL,
|
|
451
|
+
endUrl: demoURL,
|
|
452
|
+
}, output, flags.music);
|
|
453
|
+
|
|
454
|
+
if (!noShare) {
|
|
455
|
+
await shareFlow(resolve(output), videoTitle);
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentreel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn Claude Code sessions into viral demo videos",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agentreel": "./bin/agentreel.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "remotion studio",
|
|
10
|
+
"render": "remotion render CastVideo out/cast.mp4"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@remotion/cli": "^4",
|
|
14
|
+
"remotion": "^4",
|
|
15
|
+
"react": "^18",
|
|
16
|
+
"react-dom": "^18"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18",
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/",
|
|
25
|
+
"scripts/",
|
|
26
|
+
"public/",
|
|
27
|
+
"remotion.config.*",
|
|
28
|
+
"tsconfig.json"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/islo-labs/agentreel.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": ["cli", "demo", "video", "claude", "agent", "remotion"]
|
|
36
|
+
}
|
|
Binary file
|
package/public/music.mp3
ADDED
|
Binary file
|
|
Binary file
|