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 +100 -5
- package/package.json +1 -1
- package/scripts/cli_demo.py +29 -29
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/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,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
|
|
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
|
|
226
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
244
|
-
- "label": chapter name (1-3 words)
|
|
245
|
-
- "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)
|
|
246
250
|
|
|
247
|
-
Each chapter
|
|
248
|
-
|
|
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
|
|
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
|
|
262
|
-
- "label":
|
|
263
|
-
- "lines": array of objects
|
|
264
|
-
- "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
|
|
265
265
|
|
|
266
|
-
Each highlight
|
|
267
|
-
|
|
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
|
|
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=
|
|
276
|
+
timeout=300,
|
|
277
277
|
)
|
|
278
278
|
|
|
279
279
|
text = result.stdout.strip()
|