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 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 renderVideo({
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 renderVideo({
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 renderVideo({
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 renderVideo({
753
+ await renderWithFallback({
659
754
  title: videoTitle,
660
755
  highlights,
661
756
  endText: flags.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
Binary file
@@ -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=120,
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 for Claude to read, but keep the raw for display
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
- if is_demo:
225
- prompt = f"""You are creating chapter-based highlights for a demo walkthrough video. Here is the full terminal output:
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
- {clean[:6000]}
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 full demo. Each chapter shows a complete command and its output. For each chapter, return:
234
- - "label": chapter name (1-3 words) like "Setup", "Run Command", "View Results", "Verify"
235
- - "lines": array of objects, each with "text" (string), and optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool if it's a shell command)
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 should have 12-20 lines. Show the COMPLETE command and output for each step.
238
- Include the prompt line (isPrompt: true) followed by the actual output.
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 tool demo video. Here is the full terminal output:
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 that would look impressive in a short video. For each highlight, return:
252
- - "label": short label (1-2 words) like "Initialize", "Configure", "Run", "Results"
253
- - "lines": array of objects, each with "text" (string), and optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool if it's a shell command)
254
- - "zoomLine": (optional) index of the most impressive line to zoom into
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 should have 4-8 lines max. Keep the text concise.
257
- Use these colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
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 of highlights. No markdown fences."""
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=120,
276
+ timeout=300,
267
277
  )
268
278
 
269
279
  text = result.stdout.strip()