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 +25 -6
- package/package.json +1 -1
- package/scripts/browser_demo.py +2 -2
- package/scripts/cli_demo.py +29 -7
- package/src/CastVideo.tsx +10 -23
- package/src/types.ts +0 -1
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
|
-
|
|
168
|
-
|
|
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
package/scripts/browser_demo.py
CHANGED
|
@@ -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=
|
|
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=
|
|
91
|
+
timeout=300,
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
text = result.stdout.strip()
|
package/scripts/cli_demo.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 *
|
|
709
|
-
displayText =
|
|
710
|
-
isTyping = chars <
|
|
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
|
-
|
|
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:
|
|
1160
|
+
fontSize: 16,
|
|
1174
1161
|
color: DIM,
|
|
1175
1162
|
letterSpacing: 3,
|
|
1176
1163
|
}}
|