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 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
- execFileSync(python, [script, url, outFile, task], {
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
- // If we have clicks, cluster them into highlights
165
- if (clicks.length >= 2) {
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 up to 4 clusters, pick the ones with most clicks
191
+ // Take top clusters by density, sorted by time
181
192
  const ranked = clusters
182
- .map((c, i) => ({ cluster: c, idx: i }))
193
+ .map((c) => ({ cluster: c }))
183
194
  .sort((a, b) => b.cluster.length - a.cluster.length)
184
- .slice(0, 4)
195
+ .slice(0, MAX_HIGHLIGHTS)
185
196
  .sort((a, b) => a.cluster[0].timeSec - b.cluster[0].timeSec);
186
197
 
187
- const highlights = ranked.map((r, i) => {
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
- return {
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
- // Fallback: try Claude extraction, or use evenly-spaced defaults
220
- try {
221
- const highlightsPath = extractBrowserHighlights(videoPath, task);
222
- const highlights = JSON.parse(readFileSync(highlightsPath, "utf-8"));
223
- if (highlights.length > 0) {
224
- console.error(` ${highlights.length} highlights from Claude`);
225
- return highlights;
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
- // Last resort: evenly-spaced clips
232
- console.error(" Using default highlights (no clicks, no Claude)");
233
- return [
234
- { label: "Overview", overlay: "**Quick look**", videoSrc: "browser-demo.mp4", videoStartSec: 1, videoEndSec: 8 },
235
- { label: "Features", overlay: "**Key features**", videoSrc: "browser-demo.mp4", videoStartSec: 8, videoEndSec: 15 },
236
- { label: "Result", overlay: "**See it work**", videoSrc: "browser-demo.mp4", videoStartSec: 15, videoEndSec: 22 },
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
- // Use prompt for tweet text if available, otherwise title
327
- const tweetBody = prompt || title;
328
-
329
- const text = `${tweetBody}\n\nMade with agentreel`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.2.7",
3
+ "version": "0.3.2",
4
4
  "description": "Turn your web apps and CLIs into viral clips",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
Binary file
@@ -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
- context = await browser.new_context(
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
- task = sys.argv[3] if len(sys.argv) > 3 else "Explore the main features"
266
- asyncio.run(record_browser_demo(url, task, output))
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))
@@ -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", "\x1b[38;5;76m$\x1b[0m ")
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. "npm install itsovertime"
39
- endUrl?: string; // URL shown under CTA, e.g. "github.com/islo-labs/overtime"
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: "itsovertime",
45
- subtitle: "Cron for AI agents",
44
+ title: "agentreel",
45
+ subtitle: "Turn your apps into viral clips",
46
46
  highlights: [
47
47
  {
48
- label: "Initialize",
48
+ label: "Record",
49
49
  overlay: "One command.",
50
50
  lines: [
51
- { text: "npx @islo-labs/overtime init", isPrompt: true },
51
+ { text: "npx agentreel --cmd 'my-cli-tool'", isPrompt: true },
52
52
  { text: "" },
53
- { text: " itsovertime Cron for AI agents", bold: true, color: "#bd93f9" },
53
+ { text: " agentreel Turn your apps into viral clips", bold: true, color: "#bd93f9" },
54
54
  { text: "" },
55
- { text: " ✓ Created overtime.yml", color: "#50fa7b" },
55
+ { text: " ✓ Recording CLI demo...", color: "#50fa7b" },
56
56
  ],
57
57
  },
58
58
  {
59
- label: "Configure",
60
- overlay: "Plain English schedules.",
59
+ label: "Highlight",
60
+ overlay: "AI picks the best moments.",
61
61
  lines: [
62
- { text: "cat overtime.yml", isPrompt: true },
63
- { text: "shifts:", dim: true },
64
- { text: " - name: pr-review", color: "#f8f8f2" },
65
- { text: ' schedule: "every hour"', color: "#50fa7b" },
66
- { text: ' task: "Review open PRs..."', color: "#50fa7b" },
67
- { text: " notify: slack", color: "#f8f8f2" },
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: 3,
69
+ zoomLine: 2,
70
70
  },
71
71
  {
72
- label: "Run",
73
- overlay: "Fully autonomous.",
72
+ label: "Share",
73
+ overlay: "Ready to post.",
74
74
  lines: [
75
- { text: "npx @islo-labs/overtime", isPrompt: true },
75
+ { text: "Rendering video...", dim: true },
76
76
  { text: "" },
77
- { text: "┌─ itsovertime ───────────────────────────┐", color: "#bd93f9" },
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: " PR #42 reviewed — approved", color: "#50fa7b" },
83
- { text: " ✓ PR #43 reviewed — changes requested", color: "#f1fa8c" },
79
+ { text: " Share to Twitter? [Y/n]", color: "#f8f8f2" },
84
80
  ],
85
- zoomLine: 3,
81
+ zoomLine: 2,
86
82
  },
87
83
  ],
88
- endText: "npx @islo-labs/overtime",
89
- endUrl: "github.com/islo-labs/overtime",
84
+ endText: "npx agentreel",
85
+ endUrl: "github.com/islo-labs/agentreel",
90
86
  gradient: ["#0f0f1a", "#1a0f2e"],
91
87
  };