agentreel 0.4.2 → 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.2",
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"
@@ -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,17 +213,20 @@ 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)
218
220
  # Collapse carriage-return overwrites (spinners, progress bars).
219
221
  # \r means "go back to line start" — keep only the final version of each line.
220
222
  collapsed_lines = []
221
223
  for line in clean.split('\n'):
222
224
  parts = line.split('\r')
223
- # Keep only the last non-empty segment (what's actually visible)
224
225
  final = parts[-1].strip() if parts else ""
225
- if final and (not collapsed_lines or final != collapsed_lines[-1]):
226
- collapsed_lines.append(final)
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)
227
230
  clean = '\n'.join(collapsed_lines)
228
231
 
229
232
  guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
@@ -231,49 +234,46 @@ def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> li
231
234
  # Demo mode: more chapters, more lines, show full flows
232
235
  is_demo = "demo" in guidelines.lower() if guidelines else False
233
236
 
234
- if is_demo:
235
- 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.)"
236
240
 
237
- ---
238
- {clean[:6000]}
239
- ---
241
+ if is_demo:
242
+ prompt = f"""You are creating chapter-based highlights for a demo walkthrough video.
243
+ {output_block}
240
244
 
241
245
  Context: {context}{guidelines_block}
242
246
 
243
- Create 4-6 chapters that walk through the full demo. Each chapter shows a complete command and its output. For each chapter, return:
244
- - "label": chapter name (1-3 words) like "Setup", "Run Command", "View Results", "Verify"
245
- - "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)
246
250
 
247
- Each chapter should have 12-20 lines. Show the COMPLETE command and output for each step.
248
- Include the prompt line (isPrompt: true) followed by the actual output.
249
- 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"
250
253
 
251
254
  Return ONLY a JSON array. No markdown fences."""
252
255
  else:
253
- prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
254
-
255
- ---
256
- {clean[:3000]}
257
- ---
256
+ prompt = f"""You are creating a highlights reel for a CLI demo video.
257
+ {output_block}
258
258
 
259
259
  Context: {context}{guidelines_block}
260
260
 
261
- Pick 3-4 highlight moments that would look impressive in a short video. For each highlight, return:
262
- - "label": short label (1-2 words) like "Initialize", "Configure", "Run", "Results"
263
- - "lines": array of objects, each with "text" (string), and optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool if it's a shell command)
264
- - "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
265
265
 
266
- Each highlight should have 4-8 lines max. Keep the text concise.
267
- 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"
268
268
 
269
- Return ONLY a JSON array of highlights. No markdown fences."""
269
+ Return ONLY a JSON array. No markdown fences."""
270
270
 
271
271
  claude = find_claude()
272
272
  result = subprocess.run(
273
273
  [claude, "-p", prompt, "--output-format", "text"],
274
274
  capture_output=True,
275
275
  text=True,
276
- timeout=120,
276
+ timeout=300,
277
277
  )
278
278
 
279
279
  text = result.stdout.strip()