agentreel 0.2.7 → 0.3.2
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 +81 -37
- package/package.json +1 -1
- package/public/browser-demo.mp4 +0 -0
- package/scripts/browser_demo.py +18 -4
- package/scripts/cli_demo.py +8 -3
- package/src/CastVideo.tsx +1 -11
- package/src/types.ts +25 -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,
|
|
@@ -158,11 +162,18 @@ function extractBrowserHighlights(videoPath, task) {
|
|
|
158
162
|
|
|
159
163
|
function buildBrowserHighlights(clicks, videoPath, task) {
|
|
160
164
|
const CLIP_DUR = 7;
|
|
165
|
+
const MIN_HIGHLIGHTS = 3;
|
|
166
|
+
const MAX_HIGHLIGHTS = 4;
|
|
161
167
|
const labels = ["Overview", "Interact", "Navigate", "Result"];
|
|
162
168
|
const overlays = ["**First look**", "**Key action**", "**Exploring**", "**The result**"];
|
|
163
169
|
|
|
164
|
-
//
|
|
165
|
-
|
|
170
|
+
// Estimate video duration from last click or default to 25s
|
|
171
|
+
const lastClickTime = clicks.length > 0 ? clicks[clicks.length - 1].timeSec : 0;
|
|
172
|
+
const videoDur = Math.max(25, lastClickTime + 5);
|
|
173
|
+
|
|
174
|
+
// Build click-based highlights
|
|
175
|
+
const clickHighlights = [];
|
|
176
|
+
if (clicks.length >= 1) {
|
|
166
177
|
// Group clicks that are within 3s of each other
|
|
167
178
|
const clusters = [];
|
|
168
179
|
let cluster = [clicks[0]];
|
|
@@ -177,14 +188,14 @@ function buildBrowserHighlights(clicks, videoPath, task) {
|
|
|
177
188
|
}
|
|
178
189
|
clusters.push(cluster);
|
|
179
190
|
|
|
180
|
-
// Take
|
|
191
|
+
// Take top clusters by density, sorted by time
|
|
181
192
|
const ranked = clusters
|
|
182
|
-
.map((c
|
|
193
|
+
.map((c) => ({ cluster: c }))
|
|
183
194
|
.sort((a, b) => b.cluster.length - a.cluster.length)
|
|
184
|
-
.slice(0,
|
|
195
|
+
.slice(0, MAX_HIGHLIGHTS)
|
|
185
196
|
.sort((a, b) => a.cluster[0].timeSec - b.cluster[0].timeSec);
|
|
186
197
|
|
|
187
|
-
const
|
|
198
|
+
for (const r of ranked) {
|
|
188
199
|
const first = r.cluster[0];
|
|
189
200
|
const last = r.cluster[r.cluster.length - 1];
|
|
190
201
|
const center = (first.timeSec + last.timeSec) / 2;
|
|
@@ -200,41 +211,50 @@ function buildBrowserHighlights(clicks, videoPath, task) {
|
|
|
200
211
|
const focusX = hlClicks.reduce((s, c) => s + c.x, 0) / hlClicks.length / 1280;
|
|
201
212
|
const focusY = hlClicks.reduce((s, c) => s + c.y, 0) / hlClicks.length / 800;
|
|
202
213
|
|
|
203
|
-
|
|
204
|
-
label: labels[i % labels.length],
|
|
205
|
-
overlay: overlays[i % overlays.length],
|
|
214
|
+
clickHighlights.push({
|
|
206
215
|
videoSrc: "browser-demo.mp4",
|
|
207
216
|
videoStartSec: Math.round(startSec * 10) / 10,
|
|
208
217
|
videoEndSec: Math.round(endSec * 10) / 10,
|
|
209
218
|
focusX,
|
|
210
219
|
focusY,
|
|
211
220
|
clicks: hlClicks,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
console.error(` ${highlights.length} highlights from ${clicks.length} clicks`);
|
|
216
|
-
return highlights;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
217
223
|
}
|
|
218
224
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
// Pad to MIN_HIGHLIGHTS with evenly-spaced filler clips
|
|
226
|
+
const highlights = [...clickHighlights];
|
|
227
|
+
if (highlights.length < MIN_HIGHLIGHTS) {
|
|
228
|
+
// Find time gaps not covered by existing highlights
|
|
229
|
+
const covered = highlights.map(h => [h.videoStartSec, h.videoEndSec]);
|
|
230
|
+
const fillerCount = MIN_HIGHLIGHTS - highlights.length;
|
|
231
|
+
|
|
232
|
+
// Divide full video into slots, pick uncovered ones
|
|
233
|
+
const slotDur = videoDur / (fillerCount + covered.length + 1);
|
|
234
|
+
for (let i = 0; i < fillerCount; i++) {
|
|
235
|
+
const candidate = slotDur * (i + 1);
|
|
236
|
+
// Skip if overlaps with existing highlight
|
|
237
|
+
const overlaps = covered.some(([s, e]) => candidate >= s && candidate <= e);
|
|
238
|
+
const startSec = overlaps
|
|
239
|
+
? Math.max(0, videoDur - CLIP_DUR * (fillerCount - i))
|
|
240
|
+
: Math.max(0, candidate - CLIP_DUR / 2);
|
|
241
|
+
highlights.push({
|
|
242
|
+
videoSrc: "browser-demo.mp4",
|
|
243
|
+
videoStartSec: Math.round(startSec * 10) / 10,
|
|
244
|
+
videoEndSec: Math.round((startSec + CLIP_DUR) * 10) / 10,
|
|
245
|
+
});
|
|
226
246
|
}
|
|
227
|
-
} catch {
|
|
228
|
-
// Claude failed, use defaults
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
// Sort by start time and assign labels
|
|
250
|
+
highlights.sort((a, b) => a.videoStartSec - b.videoStartSec);
|
|
251
|
+
for (let i = 0; i < highlights.length; i++) {
|
|
252
|
+
highlights[i].label = labels[i % labels.length];
|
|
253
|
+
highlights[i].overlay = overlays[i % overlays.length];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.error(` ${highlights.length} highlights (${clickHighlights.length} from clicks, ${highlights.length - clickHighlights.length} filler)`);
|
|
257
|
+
return highlights;
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
// ── Render ──────────────────────────────────────────────────
|
|
@@ -323,10 +343,11 @@ async function shareFlow(outputPath, title, prompt) {
|
|
|
323
343
|
const shouldShare = await askYesNo("Share to Twitter? [Y/n] ");
|
|
324
344
|
if (!shouldShare) return;
|
|
325
345
|
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
346
|
+
const name = title || "this";
|
|
347
|
+
const desc = prompt || "";
|
|
348
|
+
const text = desc
|
|
349
|
+
? `Introducing ${name} — ${desc}\n\nMade with https://github.com/islo-labs/agentreel`
|
|
350
|
+
: `Introducing ${name}\n\nMade with https://github.com/islo-labs/agentreel`;
|
|
330
351
|
const tweetText = encodeURIComponent(text);
|
|
331
352
|
const intentURL = `https://twitter.com/intent/tweet?text=${tweetText}`;
|
|
332
353
|
|
|
@@ -341,6 +362,22 @@ async function shareFlow(outputPath, title, prompt) {
|
|
|
341
362
|
}
|
|
342
363
|
}
|
|
343
364
|
|
|
365
|
+
// ── Auto-describe ──────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
function autoDescribe(cmd, url) {
|
|
368
|
+
const target = cmd || url;
|
|
369
|
+
try {
|
|
370
|
+
const result = execFileSync("claude", [
|
|
371
|
+
"-p",
|
|
372
|
+
`Describe what this tool/app does in one short sentence (under 10 words). No quotes, no period. Just the description.\n\n${target}`,
|
|
373
|
+
"--output-format", "text",
|
|
374
|
+
], { encoding: "utf-8", timeout: 30000, stdio: ["ignore", "pipe", "ignore"] });
|
|
375
|
+
const desc = result.trim();
|
|
376
|
+
if (desc && desc.length < 100) return desc;
|
|
377
|
+
} catch { /* fall through */ }
|
|
378
|
+
return cmd ? cmd.split(/\s+/).pop() : "Web app demo";
|
|
379
|
+
}
|
|
380
|
+
|
|
344
381
|
// ── Main ────────────────────────────────────────────────────
|
|
345
382
|
|
|
346
383
|
async function main() {
|
|
@@ -352,6 +389,13 @@ async function main() {
|
|
|
352
389
|
let demoURL = flags.url;
|
|
353
390
|
let prompt = flags.prompt;
|
|
354
391
|
|
|
392
|
+
// Auto-generate description if not provided
|
|
393
|
+
if (!prompt) {
|
|
394
|
+
console.error("Generating description...");
|
|
395
|
+
prompt = autoDescribe(demoCmd, demoURL);
|
|
396
|
+
console.error(` "${prompt}"`);
|
|
397
|
+
}
|
|
398
|
+
|
|
355
399
|
if (!demoCmd && !demoURL) {
|
|
356
400
|
console.error("Please provide --cmd or --url.\n");
|
|
357
401
|
printUsage();
|
|
@@ -388,7 +432,7 @@ async function main() {
|
|
|
388
432
|
|
|
389
433
|
ensureBrowserDeps();
|
|
390
434
|
console.error("Step 1/3: Recording browser demo...");
|
|
391
|
-
const videoPath = recordBrowser(demoURL, task);
|
|
435
|
+
const videoPath = recordBrowser(demoURL, task, flags.auth);
|
|
392
436
|
|
|
393
437
|
// Copy video to Remotion public dir so it can be served
|
|
394
438
|
const publicDir = join(ROOT, "public");
|
package/package.json
CHANGED
package/public/browser-demo.mp4
CHANGED
|
Binary file
|
package/scripts/browser_demo.py
CHANGED
|
@@ -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
|
@@ -114,18 +114,23 @@ def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
|
114
114
|
write_event("o", f"\x1b[38;5;245m# {desc}\x1b[0m\r\n")
|
|
115
115
|
time.sleep(0.3)
|
|
116
116
|
|
|
117
|
-
# Type the command character by character
|
|
118
|
-
write_event("o", "
|
|
117
|
+
# Type the command character by character (no $ prompt — the renderer adds one)
|
|
118
|
+
write_event("o", "")
|
|
119
119
|
for char in cmd:
|
|
120
120
|
write_event("o", char)
|
|
121
121
|
time.sleep(0.04)
|
|
122
122
|
write_event("o", "\r\n")
|
|
123
123
|
time.sleep(0.2)
|
|
124
124
|
|
|
125
|
-
# Execute in PTY
|
|
125
|
+
# Execute in PTY with sanitized env (hide username/hostname)
|
|
126
126
|
pid, fd = pty.fork()
|
|
127
127
|
if pid == 0:
|
|
128
128
|
os.chdir(workdir)
|
|
129
|
+
os.environ["PS1"] = "$ "
|
|
130
|
+
os.environ["PROMPT_COMMAND"] = ""
|
|
131
|
+
os.environ.pop("BASH_COMMAND", None)
|
|
132
|
+
# Suppress terminal title sequences (user@host)
|
|
133
|
+
os.environ["TERM"] = "dumb"
|
|
129
134
|
os.execvp("/bin/sh", ["/bin/sh", "-c", cmd])
|
|
130
135
|
else:
|
|
131
136
|
deadline = time.time() + 15
|
package/src/CastVideo.tsx
CHANGED
|
@@ -673,17 +673,7 @@ const HighlightClip: React.FC<{
|
|
|
673
673
|
backgroundColor: "#50fa7b",
|
|
674
674
|
}}
|
|
675
675
|
/>
|
|
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>
|
|
676
|
+
<div style={{ flex: 1 }} />
|
|
687
677
|
</div>
|
|
688
678
|
|
|
689
679
|
{/* Terminal body */}
|
package/src/types.ts
CHANGED
|
@@ -35,57 +35,53 @@ 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/
|
|
84
|
+
endText: "npx agentreel",
|
|
85
|
+
endUrl: "github.com/islo-labs/agentreel",
|
|
90
86
|
gradient: ["#0f0f1a", "#1a0f2e"],
|
|
91
87
|
};
|