agentreel 0.4.1 → 0.4.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/bin/agentreel.mjs +100 -5
- package/package.json +1 -1
- package/public/music.mp3 +0 -0
- package/scripts/cli_demo.py +36 -26
package/bin/agentreel.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
-
import { readFileSync, statSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
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";
|
|
@@ -282,6 +282,101 @@ Labels: 1-2 words each, specific to this app (not generic). Overlays: short punc
|
|
|
282
282
|
return highlights;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
// ── SVG Fallback ───────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
function escSvg(s) {
|
|
288
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderSVG(props, output) {
|
|
292
|
+
const TERM_BG = "#282a36";
|
|
293
|
+
const TITLE_BAR = "#1e1f29";
|
|
294
|
+
const ACCENT = "#50fa7b";
|
|
295
|
+
const DIM = "#6272a4";
|
|
296
|
+
const WHITE = "#f8f8f2";
|
|
297
|
+
const FONT = '"SF Mono", "Fira Code", "Cascadia Code", monospace';
|
|
298
|
+
const SANS = '-apple-system, "SF Pro Display", system-ui, sans-serif';
|
|
299
|
+
|
|
300
|
+
const W = props.mode === "demo" ? 1200 : 700;
|
|
301
|
+
const PAD = 32;
|
|
302
|
+
const LINE_H = 22;
|
|
303
|
+
const TERM_PAD = 16;
|
|
304
|
+
const TITLE_BAR_H = 36;
|
|
305
|
+
const CHAPTER_GAP = 28;
|
|
306
|
+
const LABEL_H = 24;
|
|
307
|
+
const FONT_SIZE = 13;
|
|
308
|
+
|
|
309
|
+
let y = PAD;
|
|
310
|
+
let blocks = "";
|
|
311
|
+
|
|
312
|
+
// Title
|
|
313
|
+
blocks += `<text x="${W / 2}" y="${y + 28}" font-family="${escSvg(SANS)}" font-size="32" font-weight="800" fill="${WHITE}" text-anchor="middle">${escSvg(props.title)}</text>`;
|
|
314
|
+
y += 40;
|
|
315
|
+
if (props.subtitle) {
|
|
316
|
+
blocks += `<text x="${W / 2}" y="${y + 18}" font-family="${escSvg(SANS)}" font-size="16" fill="${DIM}" text-anchor="middle">${escSvg(props.subtitle)}</text>`;
|
|
317
|
+
y += 28;
|
|
318
|
+
}
|
|
319
|
+
y += 16;
|
|
320
|
+
|
|
321
|
+
for (const hl of props.highlights) {
|
|
322
|
+
if (!hl.lines || hl.lines.length === 0) continue;
|
|
323
|
+
|
|
324
|
+
// Chapter label
|
|
325
|
+
blocks += `<text x="${PAD}" y="${y + 14}" font-family="${escSvg(FONT)}" font-size="11" fill="${ACCENT}" letter-spacing="2" text-transform="uppercase">${escSvg(hl.label.toUpperCase())}</text>`;
|
|
326
|
+
y += LABEL_H;
|
|
327
|
+
|
|
328
|
+
const bodyH = TERM_PAD * 2 + hl.lines.length * LINE_H;
|
|
329
|
+
const termH = TITLE_BAR_H + bodyH;
|
|
330
|
+
|
|
331
|
+
// Terminal window
|
|
332
|
+
blocks += `<rect x="${PAD}" y="${y}" width="${W - PAD * 2}" height="${termH}" rx="8" fill="${TITLE_BAR}"/>`;
|
|
333
|
+
// Traffic lights
|
|
334
|
+
blocks += `<circle cx="${PAD + 16}" cy="${y + TITLE_BAR_H / 2}" r="5" fill="#ff5555"/>`;
|
|
335
|
+
blocks += `<circle cx="${PAD + 34}" cy="${y + TITLE_BAR_H / 2}" r="5" fill="#f1fa8c"/>`;
|
|
336
|
+
blocks += `<circle cx="${PAD + 52}" cy="${y + TITLE_BAR_H / 2}" r="5" fill="#50fa7b"/>`;
|
|
337
|
+
// Body
|
|
338
|
+
blocks += `<rect x="${PAD}" y="${y + TITLE_BAR_H}" width="${W - PAD * 2}" height="${bodyH}" fill="${TERM_BG}"/>`;
|
|
339
|
+
|
|
340
|
+
let lineY = y + TITLE_BAR_H + TERM_PAD;
|
|
341
|
+
for (const line of hl.lines) {
|
|
342
|
+
const color = line.dim ? DIM : line.color || WHITE;
|
|
343
|
+
const weight = line.bold ? "700" : "400";
|
|
344
|
+
const prefix = line.isPrompt ? `<tspan fill="${ACCENT}">$ </tspan>` : "";
|
|
345
|
+
const text = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
|
|
346
|
+
blocks += `<text x="${PAD + TERM_PAD}" y="${lineY + FONT_SIZE}" font-family="${escSvg(FONT)}" font-size="${FONT_SIZE}" font-weight="${weight}" fill="${color}">${prefix}${escSvg(text)}</text>`;
|
|
347
|
+
lineY += LINE_H;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
y += termH + CHAPTER_GAP;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// End text
|
|
354
|
+
if (props.endUrl) {
|
|
355
|
+
blocks += `<text x="${W / 2}" y="${y + 16}" font-family="${escSvg(SANS)}" font-size="14" fill="${DIM}" text-anchor="middle">${escSvg(props.endUrl)}</text>`;
|
|
356
|
+
y += 28;
|
|
357
|
+
}
|
|
358
|
+
y += PAD;
|
|
359
|
+
|
|
360
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${y}" viewBox="0 0 ${W} ${y}">
|
|
361
|
+
<rect width="${W}" height="${y}" fill="#0f0f1a"/>
|
|
362
|
+
${blocks}
|
|
363
|
+
</svg>`;
|
|
364
|
+
|
|
365
|
+
writeFileSync(output, svg);
|
|
366
|
+
console.error(`\nDone: ${output} (SVG fallback)`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function renderWithFallback(props, output, musicPath) {
|
|
370
|
+
try {
|
|
371
|
+
await renderVideo(props, output, musicPath);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(` Video rendering failed: ${e.message}`);
|
|
374
|
+
const svgOutput = output.replace(/\.[^.]+$/, ".svg");
|
|
375
|
+
console.error(` Falling back to SVG: ${svgOutput}`);
|
|
376
|
+
renderSVG(props, svgOutput);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
285
380
|
// ── Render ──────────────────────────────────────────────────
|
|
286
381
|
|
|
287
382
|
async function renderVideo(props, output, musicPath) {
|
|
@@ -566,7 +661,7 @@ async function main() {
|
|
|
566
661
|
const highlights = buildBrowserHighlights(allClicks, videoPath, demoGuidelines, demoGuidelines);
|
|
567
662
|
|
|
568
663
|
console.error("Step 3/3: Rendering video...");
|
|
569
|
-
await
|
|
664
|
+
await renderWithFallback({
|
|
570
665
|
title: videoTitle,
|
|
571
666
|
subtitle: description,
|
|
572
667
|
highlights,
|
|
@@ -593,7 +688,7 @@ async function main() {
|
|
|
593
688
|
console.error(` ${highlights.length} highlights extracted`);
|
|
594
689
|
|
|
595
690
|
console.error("Step 3/3: Rendering video...");
|
|
596
|
-
await
|
|
691
|
+
await renderWithFallback({
|
|
597
692
|
title: videoTitle,
|
|
598
693
|
subtitle: description,
|
|
599
694
|
highlights,
|
|
@@ -622,7 +717,7 @@ async function main() {
|
|
|
622
717
|
console.error(` ${highlights.length} highlights extracted`);
|
|
623
718
|
|
|
624
719
|
console.error("Step 3/3: Rendering video...");
|
|
625
|
-
await
|
|
720
|
+
await renderWithFallback({
|
|
626
721
|
title: videoTitle,
|
|
627
722
|
highlights,
|
|
628
723
|
endText: flags.cmd,
|
|
@@ -655,7 +750,7 @@ async function main() {
|
|
|
655
750
|
const highlights = buildBrowserHighlights(allClicks, videoPath, task, flags.guidelines);
|
|
656
751
|
|
|
657
752
|
console.error("Step 3/3: Rendering video...");
|
|
658
|
-
await
|
|
753
|
+
await renderWithFallback({
|
|
659
754
|
title: videoTitle,
|
|
660
755
|
highlights,
|
|
661
756
|
endText: flags.url,
|
package/package.json
CHANGED
package/public/music.mp3
ADDED
|
Binary file
|
package/scripts/cli_demo.py
CHANGED
|
@@ -63,7 +63,7 @@ Return ONLY the JSON array, no markdown, no explanation."""
|
|
|
63
63
|
[claude, "-p", prompt, "--output-format", "text"],
|
|
64
64
|
capture_output=True,
|
|
65
65
|
text=True,
|
|
66
|
-
timeout=
|
|
66
|
+
timeout=300,
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
if result.returncode != 0:
|
|
@@ -213,57 +213,67 @@ def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> li
|
|
|
213
213
|
continue
|
|
214
214
|
|
|
215
215
|
raw_output = "".join(lines_output)
|
|
216
|
-
# Clean ANSI
|
|
216
|
+
# Clean ANSI escape codes
|
|
217
217
|
clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_output)
|
|
218
|
+
# Strip other common escape sequences (title sets, OSC, etc.)
|
|
219
|
+
clean = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', clean)
|
|
220
|
+
# Collapse carriage-return overwrites (spinners, progress bars).
|
|
221
|
+
# \r means "go back to line start" — keep only the final version of each line.
|
|
222
|
+
collapsed_lines = []
|
|
223
|
+
for line in clean.split('\n'):
|
|
224
|
+
parts = line.split('\r')
|
|
225
|
+
final = parts[-1].strip() if parts else ""
|
|
226
|
+
if final:
|
|
227
|
+
# Deduplicate consecutive identical lines (spinner frames)
|
|
228
|
+
if not collapsed_lines or final != collapsed_lines[-1]:
|
|
229
|
+
collapsed_lines.append(final)
|
|
230
|
+
clean = '\n'.join(collapsed_lines)
|
|
218
231
|
|
|
219
232
|
guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
|
|
220
233
|
|
|
221
234
|
# Demo mode: more chapters, more lines, show full flows
|
|
222
235
|
is_demo = "demo" in guidelines.lower() if guidelines else False
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
# If terminal output is empty (e.g. tmux sessions, interactive tools),
|
|
238
|
+
# tell Claude to generate representative highlights from context alone
|
|
239
|
+
output_block = f"\nTerminal output:\n---\n{clean[:6000 if is_demo else 3000]}\n---" if clean.strip() else "\n(No terminal output captured — generate representative highlights from the context below.)"
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
241
|
+
if is_demo:
|
|
242
|
+
prompt = f"""You are creating chapter-based highlights for a demo walkthrough video.
|
|
243
|
+
{output_block}
|
|
230
244
|
|
|
231
245
|
Context: {context}{guidelines_block}
|
|
232
246
|
|
|
233
|
-
Create 4-6 chapters that walk through the
|
|
234
|
-
- "label": chapter name (1-3 words)
|
|
235
|
-
- "lines": array of objects
|
|
247
|
+
Create 4-6 chapters that walk through the demo. For each chapter, return:
|
|
248
|
+
- "label": chapter name (1-3 words)
|
|
249
|
+
- "lines": array of objects with "text" (string), optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool)
|
|
236
250
|
|
|
237
|
-
Each chapter
|
|
238
|
-
|
|
239
|
-
Use these colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
|
|
251
|
+
Each chapter: 12-20 lines. Include the prompt (isPrompt: true) and realistic output.
|
|
252
|
+
Colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
|
|
240
253
|
|
|
241
254
|
Return ONLY a JSON array. No markdown fences."""
|
|
242
255
|
else:
|
|
243
|
-
prompt = f"""You are creating a highlights reel for a CLI
|
|
244
|
-
|
|
245
|
-
---
|
|
246
|
-
{clean[:3000]}
|
|
247
|
-
---
|
|
256
|
+
prompt = f"""You are creating a highlights reel for a CLI demo video.
|
|
257
|
+
{output_block}
|
|
248
258
|
|
|
249
259
|
Context: {context}{guidelines_block}
|
|
250
260
|
|
|
251
|
-
Pick 3-4 highlight moments
|
|
252
|
-
- "label":
|
|
253
|
-
- "lines": array of objects
|
|
254
|
-
- "zoomLine": (optional) index of the most impressive line
|
|
261
|
+
Pick 3-4 highlight moments. For each, return:
|
|
262
|
+
- "label": 1-2 word label
|
|
263
|
+
- "lines": array of objects with "text", optionally "color" (hex), "bold", "dim", "isPrompt"
|
|
264
|
+
- "zoomLine": (optional) index of the most impressive line
|
|
255
265
|
|
|
256
|
-
Each highlight
|
|
257
|
-
|
|
266
|
+
Each highlight: 4-8 lines max.
|
|
267
|
+
Colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
|
|
258
268
|
|
|
259
|
-
Return ONLY a JSON array
|
|
269
|
+
Return ONLY a JSON array. No markdown fences."""
|
|
260
270
|
|
|
261
271
|
claude = find_claude()
|
|
262
272
|
result = subprocess.run(
|
|
263
273
|
[claude, "-p", prompt, "--output-format", "text"],
|
|
264
274
|
capture_output=True,
|
|
265
275
|
text=True,
|
|
266
|
-
timeout=
|
|
276
|
+
timeout=300,
|
|
267
277
|
)
|
|
268
278
|
|
|
269
279
|
text = result.stdout.strip()
|