agentreel 0.3.2 → 0.3.4

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
@@ -30,6 +30,7 @@ function parseArgs() {
30
30
  else if (arg === "--output" || arg === "-o") flags.output = args[++i];
31
31
  else if (arg === "--music") flags.music = args[++i];
32
32
  else if (arg === "--auth" || arg === "-a") flags.auth = args[++i];
33
+ else if (arg === "--guidelines" || arg === "-g") flags.guidelines = args[++i];
33
34
  else if (arg === "--no-share") flags.noShare = true;
34
35
  }
35
36
  return flags;
@@ -49,6 +50,7 @@ Flags:
49
50
  -t, --title <text> video title
50
51
  -o, --output <file> output file (default: agentreel.mp4)
51
52
  -a, --auth <file> Playwright storage state (cookies/auth) for browser demos
53
+ -g, --guidelines <text> guidelines for highlight generation (e.g. "focus on speed")
52
54
  --music <file> path to background music mp3
53
55
  --no-share skip the share prompt
54
56
  -h, --help show help
@@ -111,13 +113,14 @@ function recordCLI(command, workDir, context) {
111
113
  return outFile;
112
114
  }
113
115
 
114
- function extractHighlightsFromCast(castPath, context) {
116
+ function extractHighlightsFromCast(castPath, context, guidelines) {
115
117
  const python = findPython();
116
118
  const script = join(ROOT, "scripts", "cli_demo.py");
117
119
  const outFile = castPath + "-highlights.json";
118
120
 
119
121
  const args = [script, "--highlights", castPath, outFile];
120
122
  if (context) args.push(context);
123
+ if (guidelines) args.push(guidelines);
121
124
 
122
125
  execFileSync(python, args, { stdio: ["ignore", "inherit", "inherit"], env: process.env });
123
126
  return outFile;
@@ -160,12 +163,28 @@ function extractBrowserHighlights(videoPath, task) {
160
163
 
161
164
  // ── Browser Highlight Builder ───────────────────────────────
162
165
 
163
- function buildBrowserHighlights(clicks, videoPath, task) {
166
+ function buildBrowserHighlights(clicks, videoPath, task, guidelines) {
164
167
  const CLIP_DUR = 7;
165
168
  const MIN_HIGHLIGHTS = 3;
166
169
  const MAX_HIGHLIGHTS = 4;
167
- const labels = ["Overview", "Interact", "Navigate", "Result"];
168
- const overlays = ["**First look**", "**Key action**", "**Exploring**", "**The result**"];
170
+ // Ask Claude to generate labels/overlays based on the task
171
+ let labels, overlays;
172
+ try {
173
+ const guidelinesLine = guidelines ? `\nGuidelines: ${guidelines}` : "";
174
+ const genPrompt = `Generate exactly 4 highlight labels and overlay captions for a short browser demo video.
175
+ Task: ${task}${guidelinesLine}
176
+
177
+ Return a JSON object: {"labels": ["word1", "word2", "word3", "word4"], "overlays": ["**caption1**", "**caption2**", "**caption3**", "**caption4**"]}
178
+ Labels: 1-2 words each, specific to this app (not generic). Overlays: short punchy captions with **markdown bold** for emphasis. Return ONLY JSON.`;
179
+ const result = execFileSync("claude", ["-p", genPrompt, "--output-format", "text"], {
180
+ encoding: "utf-8", timeout: 30000, stdio: ["ignore", "pipe", "ignore"],
181
+ }).trim();
182
+ const parsed = JSON.parse(result.replace(/```json?\n?/g, "").replace(/```/g, "").trim());
183
+ labels = parsed.labels?.length >= 4 ? parsed.labels : null;
184
+ overlays = parsed.overlays?.length >= 4 ? parsed.overlays : null;
185
+ } catch { /* fall through */ }
186
+ if (!labels) labels = ["Overview", "Interact", "Navigate", "Result"];
187
+ if (!overlays) overlays = ["**First look**", "**Key action**", "**Exploring**", "**The result**"];
169
188
 
170
189
  // Estimate video duration from last click or default to 25s
171
190
  const lastClickTime = clicks.length > 0 ? clicks[clicks.length - 1].timeSec : 0;
@@ -409,7 +428,7 @@ async function main() {
409
428
  const castPath = recordCLI(demoCmd, process.cwd(), prompt);
410
429
 
411
430
  console.error("Step 2/3: Extracting highlights...");
412
- const highlightsPath = extractHighlightsFromCast(castPath, prompt);
431
+ const highlightsPath = extractHighlightsFromCast(castPath, prompt, flags.guidelines);
413
432
  const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
414
433
  console.error(` ${highlights.length} highlights extracted`);
415
434
 
@@ -449,7 +468,7 @@ async function main() {
449
468
  console.error(` ${allClicks.length} clicks captured`);
450
469
  }
451
470
 
452
- const highlights = buildBrowserHighlights(allClicks, videoPath, task);
471
+ const highlights = buildBrowserHighlights(allClicks, videoPath, task, flags.guidelines);
453
472
 
454
473
  console.error("Step 3/3: Rendering video...");
455
474
  await renderVideo({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
@@ -54,7 +54,7 @@ def generate_playwright_script(url, task):
54
54
  [claude, "-p", prompt, "--output-format", "text"],
55
55
  capture_output=True,
56
56
  text=True,
57
- timeout=120,
57
+ timeout=300,
58
58
  )
59
59
 
60
60
  text = result.stdout.strip()
@@ -88,7 +88,7 @@ def extract_highlights(video_path, task):
88
88
  [claude, "-p", prompt, "--output-format", "text"],
89
89
  capture_output=True,
90
90
  text=True,
91
- timeout=120,
91
+ timeout=300,
92
92
  )
93
93
 
94
94
  text = result.stdout.strip()
@@ -4,10 +4,13 @@ CLI demo recorder. Uses `claude` CLI to plan the demo, then records it.
4
4
  Usage:
5
5
  python cli_demo.py <command> <workdir> <output_cast> [context]
6
6
  """
7
+ import getpass
7
8
  import json
8
9
  import os
9
10
  import pty
11
+ import re
10
12
  import select
13
+ import socket
11
14
  import subprocess
12
15
  import sys
13
16
  import time
@@ -97,7 +100,24 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
97
100
  }
98
101
  f.write(json.dumps(header) + "\n")
99
102
 
100
- def write_event(event_type: str, data: str):
103
+ # Build patterns to strip user identity from output
104
+ _user = getpass.getuser()
105
+ _host = socket.gethostname().split(".")[0]
106
+ _home = os.path.expanduser("~")
107
+ _title_seq = re.compile(r'\x1b\][\d;]*[^\x07\x1b]*(?:\x07|\x1b\\)')
108
+ _identity = re.compile(
109
+ r'|'.join(re.escape(s) for s in {_user, _host, _home} if s),
110
+ re.IGNORECASE,
111
+ )
112
+
113
+ def _sanitize(text: str) -> str:
114
+ text = _title_seq.sub('', text)
115
+ text = _identity.sub('user', text)
116
+ return text
117
+
118
+ def write_event(event_type: str, data: str, sanitize: bool = False):
119
+ if sanitize:
120
+ data = _sanitize(data)
101
121
  elapsed = time.time() - start_time
102
122
  f.write(json.dumps([round(elapsed, 6), event_type, data]) + "\n")
103
123
  f.flush()
@@ -140,7 +160,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
140
160
  try:
141
161
  data = os.read(fd, 4096)
142
162
  if data:
143
- write_event("o", data.decode("utf-8", errors="replace"))
163
+ write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
144
164
  else:
145
165
  break
146
166
  except OSError:
@@ -154,7 +174,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
154
174
  if r:
155
175
  data = os.read(fd, 4096)
156
176
  if data:
157
- write_event("o", data.decode("utf-8", errors="replace"))
177
+ write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
158
178
  else:
159
179
  break
160
180
  else:
@@ -175,7 +195,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
175
195
  print(f"Saved: {output_path}", file=sys.stderr)
176
196
 
177
197
 
178
- def extract_highlights(cast_path: str, context: str) -> list[dict]:
198
+ def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> list[dict]:
179
199
  """Ask Claude to pick 3-4 highlight moments from the recorded session."""
180
200
  # Read the asciicast and strip to just the text content
181
201
  lines_output = []
@@ -192,16 +212,17 @@ def extract_highlights(cast_path: str, context: str) -> list[dict]:
192
212
 
193
213
  raw_output = "".join(lines_output)
194
214
  # Clean ANSI for Claude to read, but keep the raw for display
195
- import re
196
215
  clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_output)
197
216
 
217
+ guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
218
+
198
219
  prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
199
220
 
200
221
  ---
201
222
  {clean[:3000]}
202
223
  ---
203
224
 
204
- Context: {context}
225
+ Context: {context}{guidelines_block}
205
226
 
206
227
  Pick 3-4 highlight moments that would look impressive in a short video. For each highlight, return:
207
228
  - "label": short label (1-2 words) like "Initialize", "Configure", "Run", "Results"
@@ -255,8 +276,9 @@ if __name__ == "__main__":
255
276
  cast_file = sys.argv[2]
256
277
  output = sys.argv[3]
257
278
  context = sys.argv[4] if len(sys.argv) > 4 else ""
279
+ guidelines = sys.argv[5] if len(sys.argv) > 5 else ""
258
280
  print(f"Extracting highlights from: {cast_file}", file=sys.stderr)
259
- highlights = extract_highlights(cast_file, context)
281
+ highlights = extract_highlights(cast_file, context, guidelines)
260
282
  with open(output, "w") as f:
261
283
  json.dump(highlights, f, indent=2)
262
284
  print(f"Saved {len(highlights)} highlights to: {output}", file=sys.stderr)
package/src/CastVideo.tsx CHANGED
@@ -121,22 +121,6 @@ export const CastVideo: React.FC<CastProps> = ({
121
121
  {/* Subtle animated glow blobs in background */}
122
122
  <AnimatedBackground frame={frame} duration={durationInFrames} />
123
123
 
124
- {/* Global watermark — always visible */}
125
- <div
126
- style={{
127
- position: "absolute",
128
- top: 16,
129
- right: 20,
130
- zIndex: 5,
131
- fontFamily: MONO,
132
- fontSize: 11,
133
- color: "rgba(255,255,255,0.2)",
134
- letterSpacing: 2,
135
- }}
136
- >
137
- made with agentreel
138
- </div>
139
-
140
124
  <MusicTrack />
141
125
 
142
126
  <Sequence durationInFrames={titleFrames}>
@@ -694,7 +678,9 @@ const HighlightClip: React.FC<{
694
678
  const lineOpacity = interpolate(lineSpring, [0, 1], [0, 1]);
695
679
  const lineX = interpolate(lineSpring, [0, 1], [12, 0]);
696
680
 
697
- let displayText = line.text;
681
+ // Strip leading "$ " from text — renderer adds its own $ prefix
682
+ const cleanText = line.isPrompt ? line.text.replace(/^\$\s*/, "") : line.text;
683
+ let displayText = cleanText;
698
684
  let isTyping = false;
699
685
  if (line.isPrompt) {
700
686
  const typingEnd = lineFrame + fps * 0.6;
@@ -705,9 +691,9 @@ const HighlightClip: React.FC<{
705
691
  [0, 1],
706
692
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
707
693
  );
708
- const chars = Math.floor(progress * line.text.length);
709
- displayText = line.text.slice(0, chars);
710
- isTyping = chars < line.text.length;
694
+ const chars = Math.floor(progress * cleanText.length);
695
+ displayText = cleanText.slice(0, chars);
696
+ isTyping = chars < cleanText.length;
711
697
  }
712
698
  }
713
699
 
@@ -1166,11 +1152,12 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
1166
1152
  <div
1167
1153
  style={{
1168
1154
  position: "absolute",
1169
- bottom: 40,
1155
+ top: 24,
1156
+ right: 28,
1170
1157
  opacity: brandSpring * 0.4,
1171
- transform: `translateY(${interpolate(brandSpring, [0, 1], [10, 0])}px)`,
1158
+ transform: `translateY(${interpolate(brandSpring, [0, 1], [-10, 0])}px)`,
1172
1159
  fontFamily: MONO,
1173
- fontSize: 13,
1160
+ fontSize: 16,
1174
1161
  color: DIM,
1175
1162
  letterSpacing: 3,
1176
1163
  }}
package/src/types.ts CHANGED
@@ -82,6 +82,5 @@ export const defaultProps: CastProps = {
82
82
  },
83
83
  ],
84
84
  endText: "npx agentreel",
85
- endUrl: "github.com/islo-labs/agentreel",
86
85
  gradient: ["#0f0f1a", "#1a0f2e"],
87
86
  };