agentreel 0.3.1 → 0.3.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 +7 -3
- package/package.json +1 -1
- package/public/browser-demo.mp4 +0 -0
- package/scripts/browser_demo.py +20 -6
- package/scripts/cli_demo.py +31 -7
- package/src/CastVideo.tsx +11 -34
- package/src/types.ts +24 -29
package/bin/agentreel.mjs
CHANGED
|
@@ -29,6 +29,7 @@ function parseArgs() {
|
|
|
29
29
|
else if (arg === "--title" || arg === "-t") flags.title = args[++i];
|
|
30
30
|
else if (arg === "--output" || arg === "-o") flags.output = args[++i];
|
|
31
31
|
else if (arg === "--music") flags.music = args[++i];
|
|
32
|
+
else if (arg === "--auth" || arg === "-a") flags.auth = args[++i];
|
|
32
33
|
else if (arg === "--no-share") flags.noShare = true;
|
|
33
34
|
}
|
|
34
35
|
return flags;
|
|
@@ -47,6 +48,7 @@ Flags:
|
|
|
47
48
|
-p, --prompt <text> description of what the tool does
|
|
48
49
|
-t, --title <text> video title
|
|
49
50
|
-o, --output <file> output file (default: agentreel.mp4)
|
|
51
|
+
-a, --auth <file> Playwright storage state (cookies/auth) for browser demos
|
|
50
52
|
--music <file> path to background music mp3
|
|
51
53
|
--no-share skip the share prompt
|
|
52
54
|
-h, --help show help
|
|
@@ -128,13 +130,15 @@ function browserEnv() {
|
|
|
128
130
|
return { ...process.env, PLAYWRIGHT_BROWSERS_PATH: browsersDir };
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
function recordBrowser(url, task) {
|
|
133
|
+
function recordBrowser(url, task, authState) {
|
|
132
134
|
const python = findPython();
|
|
133
135
|
const script = join(ROOT, "scripts", "browser_demo.py");
|
|
134
136
|
const outFile = join(tmpdir(), "agentreel-browser-demo.mp4");
|
|
135
137
|
|
|
136
138
|
console.error(`Agent demoing browser app: ${url}`);
|
|
137
|
-
|
|
139
|
+
const args = [script, url, outFile, task];
|
|
140
|
+
if (authState) args.push("--auth", authState);
|
|
141
|
+
execFileSync(python, args, {
|
|
138
142
|
stdio: ["ignore", "inherit", "inherit"],
|
|
139
143
|
env: browserEnv(),
|
|
140
144
|
timeout: 300000,
|
|
@@ -428,7 +432,7 @@ async function main() {
|
|
|
428
432
|
|
|
429
433
|
ensureBrowserDeps();
|
|
430
434
|
console.error("Step 1/3: Recording browser demo...");
|
|
431
|
-
const videoPath = recordBrowser(demoURL, task);
|
|
435
|
+
const videoPath = recordBrowser(demoURL, task, flags.auth);
|
|
432
436
|
|
|
433
437
|
// Copy video to Remotion public dir so it can be served
|
|
434
438
|
const publicDir = join(ROOT, "public");
|
package/package.json
CHANGED
package/public/browser-demo.mp4
CHANGED
|
Binary file
|
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()
|
|
@@ -124,7 +124,7 @@ def extract_highlights(video_path, task):
|
|
|
124
124
|
return highlights
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
async def record_browser_demo(url, task, output_path):
|
|
127
|
+
async def record_browser_demo(url, task, output_path, auth_state=None):
|
|
128
128
|
"""Generate and run a Playwright demo with video recording."""
|
|
129
129
|
from playwright.async_api import async_playwright
|
|
130
130
|
|
|
@@ -137,11 +137,15 @@ async def record_browser_demo(url, task, output_path):
|
|
|
137
137
|
|
|
138
138
|
async with async_playwright() as p:
|
|
139
139
|
browser = await p.chromium.launch(headless=True)
|
|
140
|
-
|
|
140
|
+
ctx_opts = dict(
|
|
141
141
|
viewport={"width": 1280, "height": 800},
|
|
142
142
|
record_video_dir=video_dir,
|
|
143
143
|
record_video_size={"width": 1280, "height": 800},
|
|
144
144
|
)
|
|
145
|
+
if auth_state and os.path.isfile(auth_state):
|
|
146
|
+
ctx_opts["storage_state"] = auth_state
|
|
147
|
+
print(f"Using auth state: {auth_state}", file=sys.stderr)
|
|
148
|
+
context = await browser.new_context(**ctx_opts)
|
|
145
149
|
|
|
146
150
|
# Inject click tracker — persists across navigations
|
|
147
151
|
click_tracker_js = (
|
|
@@ -262,5 +266,15 @@ if __name__ == "__main__":
|
|
|
262
266
|
else:
|
|
263
267
|
url = sys.argv[1]
|
|
264
268
|
output = sys.argv[2]
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
# Parse remaining args: [task] [--auth <state_file>]
|
|
270
|
+
task = "Explore the main features"
|
|
271
|
+
auth_state = None
|
|
272
|
+
i = 3
|
|
273
|
+
while i < len(sys.argv):
|
|
274
|
+
if sys.argv[i] == "--auth" and i + 1 < len(sys.argv):
|
|
275
|
+
auth_state = sys.argv[i + 1]
|
|
276
|
+
i += 2
|
|
277
|
+
else:
|
|
278
|
+
task = sys.argv[i]
|
|
279
|
+
i += 1
|
|
280
|
+
asyncio.run(record_browser_demo(url, task, output, auth_state=auth_state))
|
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()
|
|
@@ -114,18 +134,23 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
114
134
|
write_event("o", f"\x1b[38;5;245m# {desc}\x1b[0m\r\n")
|
|
115
135
|
time.sleep(0.3)
|
|
116
136
|
|
|
117
|
-
# Type the command character by character
|
|
118
|
-
write_event("o", "
|
|
137
|
+
# Type the command character by character (no $ prompt — the renderer adds one)
|
|
138
|
+
write_event("o", "")
|
|
119
139
|
for char in cmd:
|
|
120
140
|
write_event("o", char)
|
|
121
141
|
time.sleep(0.04)
|
|
122
142
|
write_event("o", "\r\n")
|
|
123
143
|
time.sleep(0.2)
|
|
124
144
|
|
|
125
|
-
# Execute in PTY
|
|
145
|
+
# Execute in PTY with sanitized env (hide username/hostname)
|
|
126
146
|
pid, fd = pty.fork()
|
|
127
147
|
if pid == 0:
|
|
128
148
|
os.chdir(workdir)
|
|
149
|
+
os.environ["PS1"] = "$ "
|
|
150
|
+
os.environ["PROMPT_COMMAND"] = ""
|
|
151
|
+
os.environ.pop("BASH_COMMAND", None)
|
|
152
|
+
# Suppress terminal title sequences (user@host)
|
|
153
|
+
os.environ["TERM"] = "dumb"
|
|
129
154
|
os.execvp("/bin/sh", ["/bin/sh", "-c", cmd])
|
|
130
155
|
else:
|
|
131
156
|
deadline = time.time() + 15
|
|
@@ -135,7 +160,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
135
160
|
try:
|
|
136
161
|
data = os.read(fd, 4096)
|
|
137
162
|
if data:
|
|
138
|
-
write_event("o", data.decode("utf-8", errors="replace"))
|
|
163
|
+
write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
|
|
139
164
|
else:
|
|
140
165
|
break
|
|
141
166
|
except OSError:
|
|
@@ -149,7 +174,7 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
149
174
|
if r:
|
|
150
175
|
data = os.read(fd, 4096)
|
|
151
176
|
if data:
|
|
152
|
-
write_event("o", data.decode("utf-8", errors="replace"))
|
|
177
|
+
write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
|
|
153
178
|
else:
|
|
154
179
|
break
|
|
155
180
|
else:
|
|
@@ -187,7 +212,6 @@ def extract_highlights(cast_path: str, context: str) -> list[dict]:
|
|
|
187
212
|
|
|
188
213
|
raw_output = "".join(lines_output)
|
|
189
214
|
# Clean ANSI for Claude to read, but keep the raw for display
|
|
190
|
-
import re
|
|
191
215
|
clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_output)
|
|
192
216
|
|
|
193
217
|
prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
|
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}>
|
|
@@ -673,17 +657,7 @@ const HighlightClip: React.FC<{
|
|
|
673
657
|
backgroundColor: "#50fa7b",
|
|
674
658
|
}}
|
|
675
659
|
/>
|
|
676
|
-
<div
|
|
677
|
-
style={{
|
|
678
|
-
flex: 1,
|
|
679
|
-
textAlign: "center",
|
|
680
|
-
fontFamily: MONO,
|
|
681
|
-
fontSize: 12,
|
|
682
|
-
color: "rgba(255,255,255,0.25)",
|
|
683
|
-
}}
|
|
684
|
-
>
|
|
685
|
-
Terminal
|
|
686
|
-
</div>
|
|
660
|
+
<div style={{ flex: 1 }} />
|
|
687
661
|
</div>
|
|
688
662
|
|
|
689
663
|
{/* Terminal body */}
|
|
@@ -704,7 +678,9 @@ const HighlightClip: React.FC<{
|
|
|
704
678
|
const lineOpacity = interpolate(lineSpring, [0, 1], [0, 1]);
|
|
705
679
|
const lineX = interpolate(lineSpring, [0, 1], [12, 0]);
|
|
706
680
|
|
|
707
|
-
|
|
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;
|
|
708
684
|
let isTyping = false;
|
|
709
685
|
if (line.isPrompt) {
|
|
710
686
|
const typingEnd = lineFrame + fps * 0.6;
|
|
@@ -715,9 +691,9 @@ const HighlightClip: React.FC<{
|
|
|
715
691
|
[0, 1],
|
|
716
692
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
717
693
|
);
|
|
718
|
-
const chars = Math.floor(progress *
|
|
719
|
-
displayText =
|
|
720
|
-
isTyping = chars <
|
|
694
|
+
const chars = Math.floor(progress * cleanText.length);
|
|
695
|
+
displayText = cleanText.slice(0, chars);
|
|
696
|
+
isTyping = chars < cleanText.length;
|
|
721
697
|
}
|
|
722
698
|
}
|
|
723
699
|
|
|
@@ -1176,11 +1152,12 @@ const EndCard: React.FC<{ text: string; url?: string }> = ({ text, url }) => {
|
|
|
1176
1152
|
<div
|
|
1177
1153
|
style={{
|
|
1178
1154
|
position: "absolute",
|
|
1179
|
-
|
|
1155
|
+
top: 24,
|
|
1156
|
+
right: 28,
|
|
1180
1157
|
opacity: brandSpring * 0.4,
|
|
1181
|
-
transform: `translateY(${interpolate(brandSpring, [0, 1], [10, 0])}px)`,
|
|
1158
|
+
transform: `translateY(${interpolate(brandSpring, [0, 1], [-10, 0])}px)`,
|
|
1182
1159
|
fontFamily: MONO,
|
|
1183
|
-
fontSize:
|
|
1160
|
+
fontSize: 16,
|
|
1184
1161
|
color: DIM,
|
|
1185
1162
|
letterSpacing: 3,
|
|
1186
1163
|
}}
|
package/src/types.ts
CHANGED
|
@@ -35,57 +35,52 @@ export interface CastProps {
|
|
|
35
35
|
title: string; // big opening title
|
|
36
36
|
subtitle?: string; // smaller text under title
|
|
37
37
|
highlights: Highlight[];
|
|
38
|
-
endText?: string; // closing CTA command, e.g. "
|
|
39
|
-
endUrl?: string; // URL shown under CTA, e.g. "github.com/islo-labs/
|
|
38
|
+
endText?: string; // closing CTA command, e.g. "npx agentreel"
|
|
39
|
+
endUrl?: string; // URL shown under CTA, e.g. "github.com/islo-labs/agentreel"
|
|
40
40
|
gradient?: [string, string]; // background gradient colors
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export const defaultProps: CastProps = {
|
|
44
|
-
title: "
|
|
45
|
-
subtitle: "
|
|
44
|
+
title: "agentreel",
|
|
45
|
+
subtitle: "Turn your apps into viral clips",
|
|
46
46
|
highlights: [
|
|
47
47
|
{
|
|
48
|
-
label: "
|
|
48
|
+
label: "Record",
|
|
49
49
|
overlay: "One command.",
|
|
50
50
|
lines: [
|
|
51
|
-
{ text: "npx
|
|
51
|
+
{ text: "npx agentreel --cmd 'my-cli-tool'", isPrompt: true },
|
|
52
52
|
{ text: "" },
|
|
53
|
-
{ text: "
|
|
53
|
+
{ text: " agentreel Turn your apps into viral clips", bold: true, color: "#bd93f9" },
|
|
54
54
|
{ text: "" },
|
|
55
|
-
{ text: " ✓
|
|
55
|
+
{ text: " ✓ Recording CLI demo...", color: "#50fa7b" },
|
|
56
56
|
],
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
label: "
|
|
60
|
-
overlay: "
|
|
59
|
+
label: "Highlight",
|
|
60
|
+
overlay: "AI picks the best moments.",
|
|
61
61
|
lines: [
|
|
62
|
-
{ text: "
|
|
63
|
-
{ text: "
|
|
64
|
-
{ text: "
|
|
65
|
-
{ text: '
|
|
66
|
-
{ text: '
|
|
67
|
-
{ text: "
|
|
62
|
+
{ text: "Extracting highlights...", dim: true },
|
|
63
|
+
{ text: "" },
|
|
64
|
+
{ text: " ✓ 4 highlights extracted", color: "#50fa7b" },
|
|
65
|
+
{ text: ' "Initialize" — first run', color: "#f8f8f2" },
|
|
66
|
+
{ text: ' "Configure" — setup step', color: "#f8f8f2" },
|
|
67
|
+
{ text: ' "Run" — the wow moment', color: "#f1fa8c" },
|
|
68
68
|
],
|
|
69
|
-
zoomLine:
|
|
69
|
+
zoomLine: 2,
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
label: "
|
|
73
|
-
overlay: "
|
|
72
|
+
label: "Share",
|
|
73
|
+
overlay: "Ready to post.",
|
|
74
74
|
lines: [
|
|
75
|
-
{ text: "
|
|
75
|
+
{ text: "Rendering video...", dim: true },
|
|
76
76
|
{ text: "" },
|
|
77
|
-
{ text: "
|
|
78
|
-
{ text: "│ pr-review every hour ⟳ running │", color: "#f1fa8c" },
|
|
79
|
-
{ text: "│ dep-updates Mon at 2am idle │", dim: true },
|
|
80
|
-
{ text: "└──────────────────────────────────────────┘", color: "#bd93f9" },
|
|
77
|
+
{ text: " Done: agentreel.mp4 (2.4 MB)", color: "#50fa7b" },
|
|
81
78
|
{ text: "" },
|
|
82
|
-
{ text: "
|
|
83
|
-
{ text: " ✓ PR #43 reviewed — changes requested", color: "#f1fa8c" },
|
|
79
|
+
{ text: " Share to Twitter? [Y/n]", color: "#f8f8f2" },
|
|
84
80
|
],
|
|
85
|
-
zoomLine:
|
|
81
|
+
zoomLine: 2,
|
|
86
82
|
},
|
|
87
83
|
],
|
|
88
|
-
endText: "npx
|
|
89
|
-
endUrl: "github.com/islo-labs/overtime",
|
|
84
|
+
endText: "npx agentreel",
|
|
90
85
|
gradient: ["#0f0f1a", "#1a0f2e"],
|
|
91
86
|
};
|