agentreel 0.1.1 → 0.1.3
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 +3 -1
- package/bin/agentreel.mjs +83 -52
- package/package.json +3 -1
- package/src/CastVideo.tsx +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Turn your Claude Code sessions into viral demo videos.
|
|
4
4
|
|
|
5
|
-
https://github.com/user-attachments/assets/
|
|
5
|
+
https://github.com/user-attachments/assets/474fd85d-3b35-48f4-82b8-1b337840fb51
|
|
6
|
+
|
|
7
|
+
> 🔊 Turn on sound
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
package/bin/agentreel.mjs
CHANGED
|
@@ -258,7 +258,7 @@ function extractBrowserHighlights(videoPath, task) {
|
|
|
258
258
|
|
|
259
259
|
// ── Render ──────────────────────────────────────────────────
|
|
260
260
|
|
|
261
|
-
function renderVideo(props, output, musicPath) {
|
|
261
|
+
async function renderVideo(props, output, musicPath) {
|
|
262
262
|
const publicDir = join(ROOT, "public");
|
|
263
263
|
if (!existsSync(publicDir)) mkdirSync(publicDir, { recursive: true });
|
|
264
264
|
if (musicPath && existsSync(musicPath)) {
|
|
@@ -267,11 +267,37 @@ function renderVideo(props, output, musicPath) {
|
|
|
267
267
|
|
|
268
268
|
const absOutput = resolve(output);
|
|
269
269
|
const propsJSON = JSON.stringify(props);
|
|
270
|
-
const remotion = join(ROOT, "node_modules", ".bin", "remotion");
|
|
271
270
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
// Render using Remotion's Node.js API — no CLI binary needed
|
|
272
|
+
const { bundle } = await import("@remotion/bundler");
|
|
273
|
+
const { renderMedia, selectComposition } = await import("@remotion/renderer");
|
|
274
|
+
|
|
275
|
+
const entryPoint = join(ROOT, "src", "index.ts");
|
|
276
|
+
|
|
277
|
+
console.error(" Bundling...");
|
|
278
|
+
const serveUrl = await bundle({
|
|
279
|
+
entryPoint,
|
|
280
|
+
webpackOverride: (config) => config,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
console.error(" Preparing renderer...");
|
|
284
|
+
const composition = await selectComposition({
|
|
285
|
+
serveUrl,
|
|
286
|
+
id: "CastVideo",
|
|
287
|
+
inputProps: props,
|
|
288
|
+
onBrowserDownload: () => {
|
|
289
|
+
console.error(" Downloading renderer (one-time, ~90MB)...");
|
|
290
|
+
return () => {}; // suppress progress logs
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
console.error(" Rendering...");
|
|
295
|
+
await renderMedia({
|
|
296
|
+
composition,
|
|
297
|
+
serveUrl,
|
|
298
|
+
codec: "h264",
|
|
299
|
+
outputLocation: absOutput,
|
|
300
|
+
inputProps: props,
|
|
275
301
|
});
|
|
276
302
|
|
|
277
303
|
const size = statSync(absOutput).size;
|
|
@@ -280,46 +306,40 @@ function renderVideo(props, output, musicPath) {
|
|
|
280
306
|
|
|
281
307
|
// ── Upload + Share ──────────────────────────────────────────
|
|
282
308
|
|
|
283
|
-
async function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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}`);
|
|
309
|
+
async function uploadVideo(filePath) {
|
|
310
|
+
// Try uploading via curl to Streamable (works if user has STREAMABLE_EMAIL/PASSWORD set)
|
|
311
|
+
const email = process.env.STREAMABLE_EMAIL;
|
|
312
|
+
const password = process.env.STREAMABLE_PASSWORD;
|
|
313
|
+
|
|
314
|
+
if (email && password) {
|
|
315
|
+
try {
|
|
316
|
+
const result = execFileSync("curl", [
|
|
317
|
+
"-s", "-u", `${email}:${password}`,
|
|
318
|
+
"-F", `file=@${filePath}`,
|
|
319
|
+
"https://api.streamable.com/upload",
|
|
320
|
+
], { timeout: 60000 });
|
|
321
|
+
const data = JSON.parse(result.toString());
|
|
322
|
+
if (data.shortcode) {
|
|
323
|
+
return `https://streamable.com/${data.shortcode}`;
|
|
324
|
+
}
|
|
325
|
+
} catch { /* fall through */ }
|
|
319
326
|
}
|
|
320
327
|
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
// Try Imgur as fallback (supports anonymous video upload)
|
|
329
|
+
try {
|
|
330
|
+
const result = execFileSync("curl", [
|
|
331
|
+
"-s",
|
|
332
|
+
"-H", "Authorization: Client-ID 546c25a59c58ad7",
|
|
333
|
+
"-F", `video=@${filePath}`,
|
|
334
|
+
"https://api.imgur.com/3/upload",
|
|
335
|
+
], { timeout: 120000 });
|
|
336
|
+
const data = JSON.parse(result.toString());
|
|
337
|
+
if (data.data?.link) {
|
|
338
|
+
return data.data.link;
|
|
339
|
+
}
|
|
340
|
+
} catch { /* fall through */ }
|
|
341
|
+
|
|
342
|
+
return null;
|
|
323
343
|
}
|
|
324
344
|
|
|
325
345
|
function openShareURL(videoURL, text) {
|
|
@@ -353,14 +373,25 @@ async function shareFlow(outputPath, title) {
|
|
|
353
373
|
const shouldShare = await askYesNo("Share to Twitter? [Y/n] ");
|
|
354
374
|
if (!shouldShare) return;
|
|
355
375
|
|
|
356
|
-
console.error("Uploading
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
376
|
+
console.error("Uploading video...");
|
|
377
|
+
const url = await uploadVideo(outputPath);
|
|
378
|
+
|
|
379
|
+
if (url) {
|
|
380
|
+
const text = `${title}\n\nMade with agentreel`;
|
|
360
381
|
openShareURL(url, text);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
console.error("
|
|
382
|
+
} else {
|
|
383
|
+
// No upload worked — open Twitter with just the text, user attaches video manually
|
|
384
|
+
console.error("Could not auto-upload. Opening Twitter — drag your video into the tweet.");
|
|
385
|
+
const text = `${title}\n\nMade with agentreel`;
|
|
386
|
+
const tweetText = encodeURIComponent(text);
|
|
387
|
+
const intentURL = `https://twitter.com/intent/tweet?text=${tweetText}`;
|
|
388
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
389
|
+
try {
|
|
390
|
+
execFileSync(cmd, [intentURL], { stdio: "ignore" });
|
|
391
|
+
} catch {
|
|
392
|
+
console.error(` Tweet link: ${intentURL}`);
|
|
393
|
+
}
|
|
394
|
+
console.error(` Video: ${resolve(outputPath)}`);
|
|
364
395
|
}
|
|
365
396
|
}
|
|
366
397
|
|
|
@@ -413,7 +444,7 @@ async function main() {
|
|
|
413
444
|
console.error(` ${highlights.length} highlights extracted`);
|
|
414
445
|
|
|
415
446
|
console.error("Step 3/3: Rendering video...");
|
|
416
|
-
renderVideo({
|
|
447
|
+
await renderVideo({
|
|
417
448
|
title: videoTitle,
|
|
418
449
|
subtitle: prompt,
|
|
419
450
|
highlights,
|
|
@@ -443,7 +474,7 @@ async function main() {
|
|
|
443
474
|
console.error(` ${highlights.length} highlights extracted`);
|
|
444
475
|
|
|
445
476
|
console.error("Step 3/3: Rendering video...");
|
|
446
|
-
renderVideo({
|
|
477
|
+
await renderVideo({
|
|
447
478
|
title: videoTitle,
|
|
448
479
|
subtitle: prompt,
|
|
449
480
|
highlights,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentreel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Turn Claude Code sessions into viral demo videos",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentreel": "./bin/agentreel.mjs"
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"render": "remotion render CastVideo out/cast.mp4"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@remotion/bundler": "^4",
|
|
14
|
+
"@remotion/renderer": "^4",
|
|
13
15
|
"@remotion/cli": "^4",
|
|
14
16
|
"remotion": "^4",
|
|
15
17
|
"react": "^18",
|
package/src/CastVideo.tsx
CHANGED
|
@@ -115,7 +115,7 @@ export const CastVideo: React.FC<CastProps> = ({
|
|
|
115
115
|
letterSpacing: 2,
|
|
116
116
|
}}
|
|
117
117
|
>
|
|
118
|
-
made with
|
|
118
|
+
made with agentreel
|
|
119
119
|
</div>
|
|
120
120
|
|
|
121
121
|
<MusicTrack />
|
|
@@ -1010,7 +1010,7 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1010
1010
|
letterSpacing: 3,
|
|
1011
1011
|
}}
|
|
1012
1012
|
>
|
|
1013
|
-
made with
|
|
1013
|
+
made with agentreel
|
|
1014
1014
|
</div>
|
|
1015
1015
|
</AbsoluteFill>
|
|
1016
1016
|
);
|