agentreel 0.4.4 → 0.5.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentreel",
3
- "version": "0.4.4",
4
- "description": "Turn your web apps and CLIs into viral clips",
3
+ "version": "0.5.0",
4
+ "description": "Turn your apps into demo videos",
5
5
  "bin": {
6
6
  "agentreel": "./bin/agentreel.mjs"
7
7
  },
@@ -24,7 +24,6 @@
24
24
  "files": [
25
25
  "bin/",
26
26
  "src/",
27
- "scripts/",
28
27
  "public/",
29
28
  "remotion.config.*",
30
29
  "tsconfig.json"
@@ -1,286 +0,0 @@
1
- """
2
- Browser demo recorder. Uses claude CLI to generate a Playwright script,
3
- runs it with video recording, then extracts highlight timestamps.
4
-
5
- Usage:
6
- python browser_demo.py <url> <output_video> [task]
7
- python browser_demo.py --highlights <video_path> <output_json> [task]
8
- """
9
- import asyncio
10
- import json
11
- import os
12
- import subprocess
13
- import sys
14
- import tempfile
15
- import time
16
-
17
-
18
- def find_claude():
19
- """Find the claude CLI binary."""
20
- import shutil
21
- claude = shutil.which("claude")
22
- if claude:
23
- return claude
24
- for path in [
25
- os.path.expanduser("~/.local/bin/claude"),
26
- "/usr/local/bin/claude",
27
- ]:
28
- if os.path.isfile(path):
29
- return path
30
- return "claude"
31
-
32
-
33
- def generate_playwright_script(url, task, guidelines=""):
34
- """Use claude CLI to generate a Playwright demo script."""
35
- guidelines_part = f"IMPORTANT guidelines: {guidelines}. " if guidelines else ""
36
- prompt = (
37
- f"Generate a Playwright Python async function that demos a web app at {url}. "
38
- f"Task: {task}. "
39
- f"{guidelines_part}"
40
- f"The function signature is: async def demo(page). "
41
- f"Navigate to the URL, wait for load, interact with key features — "
42
- f"click buttons, fill forms, scroll. Take about 20 seconds total. "
43
- f"Add page.wait_for_timeout(1500) between actions so the viewer can see each step. "
44
- f"IMPORTANT rules for robust scripts: "
45
- f"- Use timeout=5000 on every click/fill/action so failures are fast. "
46
- f"- Use force=True on all click() calls to bypass overlapping labels/overlays. "
47
- f"- Click visible labels and buttons, never hidden inputs (like sr-only radio buttons). "
48
- f"- Wrap each action in try/except and continue on failure — the demo must finish. "
49
- f"Return ONLY the Python function code, no imports, no markdown fences."
50
- )
51
-
52
- claude = find_claude()
53
- print(f"Using claude at: {claude}", file=sys.stderr)
54
-
55
- result = subprocess.run(
56
- [claude, "-p", prompt, "--output-format", "text"],
57
- capture_output=True,
58
- text=True,
59
- timeout=300,
60
- )
61
-
62
- text = result.stdout.strip()
63
- if "```" in text:
64
- parts = text.split("```")
65
- for part in parts:
66
- part = part.strip()
67
- if part.startswith("python"):
68
- part = part[6:].strip()
69
- if "async def demo" in part:
70
- text = part
71
- break
72
- return text
73
-
74
-
75
- def extract_highlights(video_path, task):
76
- """Ask Claude to suggest highlight timestamps for a browser recording."""
77
- claude = find_claude()
78
-
79
- prompt = (
80
- f"I recorded a browser demo video of a web app. The task was: {task}. "
81
- f"The video is about 20 seconds long. "
82
- f"Suggest 3-4 highlight moments as a JSON array. Each highlight has: "
83
- f'"label" (1-2 words), "overlay" (short caption with **bold** for accent), '
84
- f'"videoStartSec" (start time in seconds), "videoEndSec" (end time). '
85
- f"Each clip should be 5-8 seconds to show the full interaction. Cover: page load, key interaction, result. "
86
- f"Return ONLY the JSON array."
87
- )
88
-
89
- result = subprocess.run(
90
- [claude, "-p", prompt, "--output-format", "text"],
91
- capture_output=True,
92
- text=True,
93
- timeout=300,
94
- )
95
-
96
- text = result.stdout.strip()
97
- if result.returncode != 0 or not text:
98
- print(f"Claude returned no output (exit {result.returncode}), using default highlights", file=sys.stderr)
99
- if result.stderr:
100
- print(f" stderr: {result.stderr[:200]}", file=sys.stderr)
101
- text = ""
102
-
103
- if "```" in text:
104
- parts = text.split("```")
105
- for part in parts:
106
- part = part.strip()
107
- if part.startswith("json"):
108
- part = part[4:].strip()
109
- if part.startswith("["):
110
- text = part
111
- break
112
-
113
- try:
114
- highlights = json.loads(text)
115
- except (json.JSONDecodeError, ValueError):
116
- print(f"Could not parse highlights, using defaults", file=sys.stderr)
117
- highlights = [
118
- {"label": "Overview", "overlay": "**Quick look**", "videoStartSec": 1, "videoEndSec": 7},
119
- {"label": "Features", "overlay": "**Key features**", "videoStartSec": 7, "videoEndSec": 14},
120
- {"label": "Result", "overlay": "**See it work**", "videoStartSec": 14, "videoEndSec": 20},
121
- ]
122
-
123
- for h in highlights:
124
- h["videoSrc"] = "browser-demo.mp4"
125
-
126
- return highlights
127
-
128
-
129
- async def record_browser_demo(url, task, output_path, auth_state=None, guidelines=""):
130
- """Generate and run a Playwright demo with video recording."""
131
- from playwright.async_api import async_playwright
132
-
133
- print(f"Generating demo script for {url}...", file=sys.stderr)
134
- script_code = generate_playwright_script(url, task, guidelines)
135
- print(f"Script ready ({len(script_code)} chars)", file=sys.stderr)
136
-
137
- video_dir = tempfile.mkdtemp()
138
- recording_start_ms = int(time.time() * 1000)
139
-
140
- async with async_playwright() as p:
141
- browser = await p.chromium.launch(headless=True)
142
- ctx_opts = dict(
143
- viewport={"width": 1280, "height": 800},
144
- record_video_dir=video_dir,
145
- record_video_size={"width": 1280, "height": 800},
146
- )
147
- if auth_state and os.path.isfile(auth_state):
148
- ctx_opts["storage_state"] = auth_state
149
- print(f"Using auth state: {auth_state}", file=sys.stderr)
150
- context = await browser.new_context(**ctx_opts)
151
-
152
- # Inject click tracker — persists across navigations
153
- click_tracker_js = (
154
- "if (!window.__agentreel_clicks) {"
155
- " window.__agentreel_clicks = [];"
156
- " document.addEventListener('click', function(e) {"
157
- " window.__agentreel_clicks.push({"
158
- " x: e.clientX,"
159
- " y: e.clientY,"
160
- f" timestamp: Date.now() - {recording_start_ms}"
161
- " });"
162
- " }, true);"
163
- "}"
164
- )
165
- await context.add_init_script(click_tracker_js)
166
-
167
- page = await context.new_page()
168
-
169
- # Navigate first
170
- print(f"Loading {url}...", file=sys.stderr)
171
- try:
172
- await page.goto(url, wait_until="networkidle", timeout=15000)
173
- except Exception as e:
174
- print(f"Navigation warning: {e}", file=sys.stderr)
175
- await page.goto(url, timeout=15000)
176
-
177
- await page.wait_for_timeout(1000)
178
-
179
- # Run the generated demo
180
- try:
181
- local_ns = {}
182
- full_code = "import asyncio\n" + script_code
183
- compiled = compile(full_code, "<demo>", "exec") # noqa: S102
184
- exec(compiled, local_ns) # noqa: S102
185
-
186
- if "demo" in local_ns:
187
- print("Running demo script...", file=sys.stderr)
188
- await local_ns["demo"](page)
189
- else:
190
- print("No demo() function found, waiting on page...", file=sys.stderr)
191
- await page.wait_for_timeout(10000)
192
- except Exception as e:
193
- print(f"Demo script error: {e}", file=sys.stderr)
194
- # Scroll around as fallback
195
- await page.wait_for_timeout(2000)
196
- await page.evaluate("window.scrollTo({ top: 500, behavior: 'smooth' })")
197
- await page.wait_for_timeout(2000)
198
- await page.evaluate("window.scrollTo({ top: 0, behavior: 'smooth' })")
199
- await page.wait_for_timeout(2000)
200
-
201
- # Extract click data before closing
202
- try:
203
- clicks_raw = await page.evaluate("window.__agentreel_clicks || []")
204
- except Exception:
205
- clicks_raw = []
206
-
207
- clicks = [
208
- {"x": c["x"], "y": c["y"], "timeSec": round(c["timestamp"] / 1000.0, 3)}
209
- for c in clicks_raw
210
- ]
211
- clicks_path = output_path.replace(".mp4", "-clicks.json")
212
- with open(clicks_path, "w") as f:
213
- json.dump(clicks, f, indent=2)
214
- print(f"Captured {len(clicks)} clicks -> {clicks_path}", file=sys.stderr)
215
-
216
- # Get the video path before closing
217
- video = page.video
218
- await page.close()
219
- await context.close()
220
- await browser.close()
221
-
222
- # Find the recorded video and convert to mp4
223
- for f in os.listdir(video_dir):
224
- if f.endswith(".webm"):
225
- webm_path = os.path.join(video_dir, f)
226
-
227
- # Convert webm to mp4 with ffmpeg
228
- print(f"Converting to mp4...", file=sys.stderr)
229
- try:
230
- subprocess.run(
231
- ["ffmpeg", "-y", "-i", webm_path, "-c:v", "libx264",
232
- "-preset", "fast", "-crf", "23", output_path],
233
- capture_output=True,
234
- timeout=60,
235
- )
236
- print(f"Saved: {output_path}", file=sys.stderr)
237
- except (subprocess.TimeoutExpired, FileNotFoundError):
238
- # ffmpeg not available, use webm directly
239
- import shutil
240
- mp4_path = output_path.replace(".mp4", ".webm")
241
- shutil.copy2(webm_path, mp4_path)
242
- print(f"Saved (webm): {mp4_path}", file=sys.stderr)
243
- return
244
-
245
- print("Error: no video file recorded", file=sys.stderr)
246
- sys.exit(1)
247
-
248
-
249
- if __name__ == "__main__":
250
- if len(sys.argv) < 3:
251
- print(
252
- "Usage:\n"
253
- " python browser_demo.py <url> <output_video> [task]\n"
254
- " python browser_demo.py --highlights <video_path> <output_json> [task]",
255
- file=sys.stderr,
256
- )
257
- sys.exit(1)
258
-
259
- if sys.argv[1] == "--highlights":
260
- video_path = sys.argv[2]
261
- output = sys.argv[3]
262
- task = sys.argv[4] if len(sys.argv) > 4 else "Demo the web app"
263
- print(f"Extracting highlights...", file=sys.stderr)
264
- highlights = extract_highlights(video_path, task)
265
- with open(output, "w") as f:
266
- json.dump(highlights, f, indent=2)
267
- print(f"Saved {len(highlights)} highlights to: {output}", file=sys.stderr)
268
- else:
269
- url = sys.argv[1]
270
- output = sys.argv[2]
271
- # Parse remaining args: [task] [--auth <state_file>]
272
- task = "Explore the main features"
273
- auth_state = None
274
- guidelines = ""
275
- i = 3
276
- while i < len(sys.argv):
277
- if sys.argv[i] == "--auth" and i + 1 < len(sys.argv):
278
- auth_state = sys.argv[i + 1]
279
- i += 2
280
- elif sys.argv[i] == "--guidelines" and i + 1 < len(sys.argv):
281
- guidelines = sys.argv[i + 1]
282
- i += 2
283
- else:
284
- task = sys.argv[i]
285
- i += 1
286
- asyncio.run(record_browser_demo(url, task, output, auth_state=auth_state, guidelines=guidelines))
@@ -1,370 +0,0 @@
1
- """
2
- CLI demo recorder. Uses `claude` CLI to plan the demo, then records it.
3
-
4
- Usage:
5
- python cli_demo.py <command> <workdir> <output_cast> [context]
6
- """
7
- import getpass
8
- import json
9
- import os
10
- import pty
11
- import re
12
- import select
13
- import socket
14
- import subprocess
15
- import sys
16
- import time
17
-
18
-
19
- def find_claude():
20
- """Find the claude CLI binary."""
21
- import shutil
22
- claude = shutil.which("claude")
23
- if claude:
24
- return claude
25
- # Common locations
26
- for path in [
27
- os.path.expanduser("~/.local/bin/claude"),
28
- "/usr/local/bin/claude",
29
- os.path.expanduser("~/.claude/bin/claude"),
30
- ]:
31
- if os.path.isfile(path):
32
- return path
33
- return "claude"
34
-
35
-
36
- def generate_demo_plan(command: str, context: str, guidelines: str = "") -> list[dict]:
37
- """Use claude CLI to plan a demo sequence."""
38
- guidelines_block = f"\n\nIMPORTANT guidelines you MUST follow:\n{guidelines}" if guidelines else ""
39
-
40
- prompt = f"""You are planning a terminal demo for a CLI tool. The tool is invoked with: {command}
41
-
42
- Context about what this tool does:
43
- {context}{guidelines_block}
44
-
45
- Generate a JSON array of demo steps. Each step is an object with:
46
- - "type": "command" (run a shell command)
47
- - "value": the command string
48
- - "delay": seconds to wait after (for dramatic effect)
49
- - "description": one-line description
50
-
51
- Make the demo:
52
- - Show the most impressive features
53
- - 5-8 steps max
54
- - Start simple, build to the wow moment
55
- - Use realistic commands
56
-
57
- Return ONLY the JSON array, no markdown, no explanation."""
58
-
59
- claude = find_claude()
60
- print(f"Using claude at: {claude}", file=sys.stderr)
61
-
62
- result = subprocess.run(
63
- [claude, "-p", prompt, "--output-format", "text"],
64
- capture_output=True,
65
- text=True,
66
- timeout=300,
67
- )
68
-
69
- if result.returncode != 0:
70
- print(f"claude stderr: {result.stderr}", file=sys.stderr)
71
- raise RuntimeError(f"claude failed: {result.stderr}")
72
-
73
- text = result.stdout.strip()
74
- print(f"Claude output: {text[:200]}...", file=sys.stderr)
75
-
76
- # Strip markdown fences if present
77
- if "```" in text:
78
- # Find JSON between fences
79
- parts = text.split("```")
80
- for part in parts:
81
- part = part.strip()
82
- if part.startswith("json"):
83
- part = part[4:].strip()
84
- if part.startswith("["):
85
- text = part
86
- break
87
-
88
- return json.loads(text)
89
-
90
-
91
- def record_demo(steps: list[dict], workdir: str, output_path: str):
92
- """Execute demo steps in a PTY and record as asciicast v2."""
93
- cols, rows = 80, 24
94
- start_time = time.time()
95
-
96
- with open(output_path, "w") as f:
97
- header = {
98
- "version": 2,
99
- "width": cols,
100
- "height": rows,
101
- "timestamp": int(start_time),
102
- }
103
- f.write(json.dumps(header) + "\n")
104
-
105
- # Build patterns to strip user identity from output
106
- _user = getpass.getuser()
107
- _host = socket.gethostname().split(".")[0]
108
- _home = os.path.expanduser("~")
109
- _title_seq = re.compile(r'\x1b\][\d;]*[^\x07\x1b]*(?:\x07|\x1b\\)')
110
- _identity = re.compile(
111
- r'|'.join(re.escape(s) for s in {_user, _host, _home} if s),
112
- re.IGNORECASE,
113
- )
114
-
115
- def _sanitize(text: str) -> str:
116
- text = _title_seq.sub('', text)
117
- text = _identity.sub('user', text)
118
- return text
119
-
120
- def write_event(event_type: str, data: str, sanitize: bool = False):
121
- if sanitize:
122
- data = _sanitize(data)
123
- elapsed = time.time() - start_time
124
- f.write(json.dumps([round(elapsed, 6), event_type, data]) + "\n")
125
- f.flush()
126
-
127
- for step in steps:
128
- if step["type"] != "command":
129
- continue
130
-
131
- cmd = step["value"]
132
- desc = step.get("description", "")
133
-
134
- # Show description as dim comment
135
- if desc:
136
- write_event("o", f"\x1b[38;5;245m# {desc}\x1b[0m\r\n")
137
- time.sleep(0.3)
138
-
139
- # Type the command character by character (no $ prompt — the renderer adds one)
140
- write_event("o", "")
141
- for char in cmd:
142
- write_event("o", char)
143
- time.sleep(0.04)
144
- write_event("o", "\r\n")
145
- time.sleep(0.2)
146
-
147
- # Execute in PTY with sanitized env (hide username/hostname)
148
- pid, fd = pty.fork()
149
- if pid == 0:
150
- os.chdir(workdir)
151
- os.environ["PS1"] = "$ "
152
- os.environ["PROMPT_COMMAND"] = ""
153
- os.environ.pop("BASH_COMMAND", None)
154
- # Suppress terminal title sequences (user@host)
155
- os.environ["TERM"] = "dumb"
156
- os.execvp("/bin/sh", ["/bin/sh", "-c", cmd])
157
- else:
158
- deadline = time.time() + 15
159
- while time.time() < deadline:
160
- r, _, _ = select.select([fd], [], [], 0.1)
161
- if r:
162
- try:
163
- data = os.read(fd, 4096)
164
- if data:
165
- write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
166
- else:
167
- break
168
- except OSError:
169
- break
170
- pid_result = os.waitpid(pid, os.WNOHANG)
171
- if pid_result[0] != 0:
172
- # Drain remaining output
173
- try:
174
- while True:
175
- r, _, _ = select.select([fd], [], [], 0.1)
176
- if r:
177
- data = os.read(fd, 4096)
178
- if data:
179
- write_event("o", data.decode("utf-8", errors="replace"), sanitize=True)
180
- else:
181
- break
182
- else:
183
- break
184
- except OSError:
185
- pass
186
- break
187
-
188
- try:
189
- os.close(fd)
190
- except OSError:
191
- pass
192
-
193
- delay = step.get("delay", 1.0)
194
- time.sleep(delay)
195
- write_event("o", "\r\n")
196
-
197
- print(f"Saved: {output_path}", file=sys.stderr)
198
-
199
-
200
- def extract_highlights(cast_path: str, context: str, guidelines: str = "") -> list[dict]:
201
- """Ask Claude to pick highlight moments from the recorded session."""
202
- # Read the asciicast and strip to just the text content
203
- lines_output = []
204
- with open(cast_path) as f:
205
- for i, line in enumerate(f):
206
- if i == 0:
207
- continue # skip header
208
- try:
209
- event = json.loads(line)
210
- if event[1] == "o":
211
- lines_output.append(event[2])
212
- except (json.JSONDecodeError, IndexError):
213
- continue
214
-
215
- raw_output = "".join(lines_output)
216
- # Clean ANSI escape codes
217
- clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_output)
218
- # Strip other common escape sequences (title sets, OSC, etc.)
219
- clean = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', clean)
220
- # Collapse carriage-return overwrites (spinners, progress bars).
221
- # \r means "go back to line start" — keep only the final version of each line.
222
- collapsed_lines = []
223
- for line in clean.split('\n'):
224
- parts = line.split('\r')
225
- final = parts[-1].strip() if parts else ""
226
- if final:
227
- # Deduplicate consecutive identical lines (spinner frames)
228
- if not collapsed_lines or final != collapsed_lines[-1]:
229
- collapsed_lines.append(final)
230
- clean = '\n'.join(collapsed_lines)
231
-
232
- guidelines_block = f"\n\nAdditional guidelines: {guidelines}" if guidelines else ""
233
-
234
- # Demo mode: more chapters, more lines, show full flows
235
- is_demo = "demo" in guidelines.lower() if guidelines else False
236
-
237
- # If terminal output is empty (e.g. tmux sessions, interactive tools),
238
- # tell Claude to generate representative highlights from context alone
239
- output_block = f"\nTerminal output:\n---\n{clean[:6000 if is_demo else 3000]}\n---" if clean.strip() else "\n(No terminal output captured — generate representative highlights from the context below.)"
240
-
241
- if is_demo:
242
- prompt = f"""You are creating chapter-based highlights for a demo walkthrough video.
243
- {output_block}
244
-
245
- Context: {context}{guidelines_block}
246
-
247
- Create 4-6 chapters that walk through the demo. For each chapter, return:
248
- - "label": chapter name (1-3 words)
249
- - "lines": array of objects with "text" (string), optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool)
250
-
251
- Each chapter: 12-20 lines. Include the prompt (isPrompt: true) and realistic output.
252
- Colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
253
-
254
- Return ONLY a JSON array. No markdown fences."""
255
- else:
256
- prompt = f"""You are creating a highlights reel for a CLI demo video.
257
- {output_block}
258
-
259
- Context: {context}{guidelines_block}
260
-
261
- Pick 3-4 highlight moments. For each, return:
262
- - "label": 1-2 word label
263
- - "lines": array of objects with "text", optionally "color" (hex), "bold", "dim", "isPrompt"
264
- - "zoomLine": (optional) index of the most impressive line
265
-
266
- Each highlight: 4-8 lines max.
267
- Colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
268
-
269
- Return ONLY a JSON array. No markdown fences."""
270
-
271
- claude = find_claude()
272
- result = subprocess.run(
273
- [claude, "-p", prompt, "--output-format", "text"],
274
- capture_output=True,
275
- text=True,
276
- timeout=300,
277
- )
278
-
279
- text = result.stdout.strip()
280
- if "```" in text:
281
- parts = text.split("```")
282
- for part in parts:
283
- part = part.strip()
284
- if part.startswith("json"):
285
- part = part[4:].strip()
286
- if part.startswith("["):
287
- text = part
288
- break
289
-
290
- try:
291
- return json.loads(text)
292
- except (json.JSONDecodeError, ValueError):
293
- print(f"Could not parse highlights from Claude, retrying with simpler prompt...", file=sys.stderr)
294
-
295
- # Retry with a minimal prompt
296
- retry_prompt = f"""Generate a JSON array of {4 if is_demo else 3} terminal highlights for a demo video.
297
- Context: {context}
298
-
299
- Each highlight: {{"label": "Name", "lines": [{{"text": "command", "isPrompt": true}}, {{"text": "output line", "color": "#50fa7b"}}]}}
300
- Use 8-15 lines per highlight. Make the output realistic for this tool.
301
- Return ONLY a JSON array."""
302
-
303
- try:
304
- retry = subprocess.run(
305
- [claude, "-p", retry_prompt, "--output-format", "text"],
306
- capture_output=True, text=True, timeout=120,
307
- )
308
- retry_text = retry.stdout.strip()
309
- if "```" in retry_text:
310
- for part in retry_text.split("```"):
311
- part = part.strip()
312
- if part.startswith("json"):
313
- part = part[4:].strip()
314
- if part.startswith("["):
315
- retry_text = part
316
- break
317
- return json.loads(retry_text)
318
- except Exception:
319
- print(f"Retry also failed, using defaults", file=sys.stderr)
320
- return [
321
- {"label": "Run", "lines": [
322
- {"text": context or "demo", "isPrompt": True},
323
- {"text": "", },
324
- {"text": " Done.", "color": "#50fa7b"},
325
- ]},
326
- ]
327
-
328
-
329
- if __name__ == "__main__":
330
- if len(sys.argv) < 4:
331
- print("Usage: python cli_demo.py <command> <workdir> <output> [context]", file=sys.stderr)
332
- print(" python cli_demo.py --highlights <cast_file> <output_json> [context]", file=sys.stderr)
333
- sys.exit(1)
334
-
335
- # Highlights mode: extract highlights from existing recording
336
- if sys.argv[1] == "--highlights":
337
- cast_file = sys.argv[2]
338
- output = sys.argv[3]
339
- context = sys.argv[4] if len(sys.argv) > 4 else ""
340
- guidelines = sys.argv[5] if len(sys.argv) > 5 else ""
341
- print(f"Extracting highlights from: {cast_file}", file=sys.stderr)
342
- highlights = extract_highlights(cast_file, context, guidelines)
343
- with open(output, "w") as f:
344
- json.dump(highlights, f, indent=2)
345
- print(f"Saved {len(highlights)} highlights to: {output}", file=sys.stderr)
346
- sys.exit(0)
347
-
348
- # Record mode: plan + record + extract highlights
349
- command = sys.argv[1]
350
- workdir = sys.argv[2]
351
- output = sys.argv[3]
352
- context = sys.argv[4] if len(sys.argv) > 4 else ""
353
- guidelines = sys.argv[5] if len(sys.argv) > 5 else ""
354
-
355
- print(f"Planning demo for: {command}", file=sys.stderr)
356
- steps = generate_demo_plan(command, context, guidelines)
357
- print(f"Generated {len(steps)} steps:", file=sys.stderr)
358
- for s in steps:
359
- print(f" $ {s['value']} — {s.get('description', '')}", file=sys.stderr)
360
-
361
- print("Recording...", file=sys.stderr)
362
- record_demo(steps, workdir, output)
363
-
364
- # Extract highlights from the recording
365
- highlights_path = output.replace(".cast", "-highlights.json")
366
- print("Extracting highlights...", file=sys.stderr)
367
- highlights = extract_highlights(output, context, guidelines)
368
- with open(highlights_path, "w") as f:
369
- json.dump(highlights, f, indent=2)
370
- print(f"Saved {len(highlights)} highlights to: {highlights_path}", file=sys.stderr)