agentreel 0.1.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/README.md +65 -0
- package/bin/agentreel.mjs +461 -0
- package/package.json +36 -0
- package/public/browser-demo.mp4 +0 -0
- package/public/music.mp3 +0 -0
- package/public/screenshot.png +0 -0
- package/scripts/browser_demo.py +215 -0
- package/scripts/cli_demo.py +272 -0
- package/src/CastVideo.tsx +1000 -0
- package/src/Root.tsx +26 -0
- package/src/index.ts +4 -0
- package/src/types.ts +82 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
|
|
16
|
+
|
|
17
|
+
def find_claude():
|
|
18
|
+
"""Find the claude CLI binary."""
|
|
19
|
+
import shutil
|
|
20
|
+
claude = shutil.which("claude")
|
|
21
|
+
if claude:
|
|
22
|
+
return claude
|
|
23
|
+
for path in [
|
|
24
|
+
os.path.expanduser("~/.local/bin/claude"),
|
|
25
|
+
"/usr/local/bin/claude",
|
|
26
|
+
]:
|
|
27
|
+
if os.path.isfile(path):
|
|
28
|
+
return path
|
|
29
|
+
return "claude"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_playwright_script(url, task):
|
|
33
|
+
"""Use claude CLI to generate a Playwright demo script."""
|
|
34
|
+
prompt = (
|
|
35
|
+
f"Generate a Playwright Python async function that demos a web app at {url}. "
|
|
36
|
+
f"Task: {task}. "
|
|
37
|
+
f"The function signature is: async def demo(page). "
|
|
38
|
+
f"Navigate to the URL, wait for load, interact with key features — "
|
|
39
|
+
f"click buttons, fill forms, scroll. Take about 20 seconds total. "
|
|
40
|
+
f"Add page.wait_for_timeout(1500) between actions so the viewer can see each step. "
|
|
41
|
+
f"Return ONLY the Python function code, no imports, no markdown fences."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
claude = find_claude()
|
|
45
|
+
print(f"Using claude at: {claude}", file=sys.stderr)
|
|
46
|
+
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[claude, "-p", prompt, "--output-format", "text"],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
timeout=120,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
text = result.stdout.strip()
|
|
55
|
+
if "```" in text:
|
|
56
|
+
parts = text.split("```")
|
|
57
|
+
for part in parts:
|
|
58
|
+
part = part.strip()
|
|
59
|
+
if part.startswith("python"):
|
|
60
|
+
part = part[6:].strip()
|
|
61
|
+
if "async def demo" in part:
|
|
62
|
+
text = part
|
|
63
|
+
break
|
|
64
|
+
return text
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_highlights(video_path, task):
|
|
68
|
+
"""Ask Claude to suggest highlight timestamps for a browser recording."""
|
|
69
|
+
claude = find_claude()
|
|
70
|
+
|
|
71
|
+
prompt = (
|
|
72
|
+
f"I recorded a browser demo video of a web app. The task was: {task}. "
|
|
73
|
+
f"The video is about 20 seconds long. "
|
|
74
|
+
f"Suggest 3-4 highlight moments as a JSON array. Each highlight has: "
|
|
75
|
+
f'"label" (1-2 words), "overlay" (short caption with **bold** for accent), '
|
|
76
|
+
f'"videoStartSec" (start time in seconds), "videoEndSec" (end time). '
|
|
77
|
+
f"Each clip should be 3-5 seconds. Cover: page load, key interaction, result. "
|
|
78
|
+
f"Return ONLY the JSON array."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
[claude, "-p", prompt, "--output-format", "text"],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
timeout=120,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
text = result.stdout.strip()
|
|
89
|
+
if "```" in text:
|
|
90
|
+
parts = text.split("```")
|
|
91
|
+
for part in parts:
|
|
92
|
+
part = part.strip()
|
|
93
|
+
if part.startswith("json"):
|
|
94
|
+
part = part[4:].strip()
|
|
95
|
+
if part.startswith("["):
|
|
96
|
+
text = part
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
highlights = json.loads(text)
|
|
100
|
+
|
|
101
|
+
# Add videoSrc to each highlight (the Go/JS CLI will set the actual path)
|
|
102
|
+
for h in highlights:
|
|
103
|
+
h["videoSrc"] = "browser-demo.mp4"
|
|
104
|
+
|
|
105
|
+
return highlights
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def record_browser_demo(url, task, output_path):
|
|
109
|
+
"""Generate and run a Playwright demo with video recording."""
|
|
110
|
+
from playwright.async_api import async_playwright
|
|
111
|
+
|
|
112
|
+
print(f"Generating demo script for {url}...", file=sys.stderr)
|
|
113
|
+
script_code = generate_playwright_script(url, task)
|
|
114
|
+
print(f"Script ready ({len(script_code)} chars)", file=sys.stderr)
|
|
115
|
+
|
|
116
|
+
video_dir = tempfile.mkdtemp()
|
|
117
|
+
|
|
118
|
+
async with async_playwright() as p:
|
|
119
|
+
browser = await p.chromium.launch(headless=True)
|
|
120
|
+
context = await browser.new_context(
|
|
121
|
+
viewport={"width": 1280, "height": 800},
|
|
122
|
+
record_video_dir=video_dir,
|
|
123
|
+
record_video_size={"width": 1280, "height": 800},
|
|
124
|
+
)
|
|
125
|
+
page = await context.new_page()
|
|
126
|
+
|
|
127
|
+
# Navigate first
|
|
128
|
+
print(f"Loading {url}...", file=sys.stderr)
|
|
129
|
+
try:
|
|
130
|
+
await page.goto(url, wait_until="networkidle", timeout=15000)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Navigation warning: {e}", file=sys.stderr)
|
|
133
|
+
await page.goto(url, timeout=15000)
|
|
134
|
+
|
|
135
|
+
await page.wait_for_timeout(1000)
|
|
136
|
+
|
|
137
|
+
# Execute the generated demo
|
|
138
|
+
try:
|
|
139
|
+
local_ns = {}
|
|
140
|
+
full_code = f"import asyncio\n{script_code}"
|
|
141
|
+
compiled = compile(full_code, "<demo>", "exec")
|
|
142
|
+
exec(compiled, local_ns) # noqa: S102
|
|
143
|
+
|
|
144
|
+
if "demo" in local_ns:
|
|
145
|
+
print("Running demo script...", file=sys.stderr)
|
|
146
|
+
await local_ns["demo"](page)
|
|
147
|
+
else:
|
|
148
|
+
print("No demo() function found, waiting on page...", file=sys.stderr)
|
|
149
|
+
await page.wait_for_timeout(10000)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"Demo script error: {e}", file=sys.stderr)
|
|
152
|
+
# Scroll around as fallback
|
|
153
|
+
await page.wait_for_timeout(2000)
|
|
154
|
+
await page.evaluate("window.scrollTo({ top: 500, behavior: 'smooth' })")
|
|
155
|
+
await page.wait_for_timeout(2000)
|
|
156
|
+
await page.evaluate("window.scrollTo({ top: 0, behavior: 'smooth' })")
|
|
157
|
+
await page.wait_for_timeout(2000)
|
|
158
|
+
|
|
159
|
+
# Get the video path before closing
|
|
160
|
+
video = page.video
|
|
161
|
+
await page.close()
|
|
162
|
+
await context.close()
|
|
163
|
+
await browser.close()
|
|
164
|
+
|
|
165
|
+
# Find the recorded video and convert to mp4
|
|
166
|
+
for f in os.listdir(video_dir):
|
|
167
|
+
if f.endswith(".webm"):
|
|
168
|
+
webm_path = os.path.join(video_dir, f)
|
|
169
|
+
|
|
170
|
+
# Convert webm to mp4 with ffmpeg
|
|
171
|
+
print(f"Converting to mp4...", file=sys.stderr)
|
|
172
|
+
try:
|
|
173
|
+
subprocess.run(
|
|
174
|
+
["ffmpeg", "-y", "-i", webm_path, "-c:v", "libx264",
|
|
175
|
+
"-preset", "fast", "-crf", "23", output_path],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
timeout=60,
|
|
178
|
+
)
|
|
179
|
+
print(f"Saved: {output_path}", file=sys.stderr)
|
|
180
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
181
|
+
# ffmpeg not available, use webm directly
|
|
182
|
+
import shutil
|
|
183
|
+
mp4_path = output_path.replace(".mp4", ".webm")
|
|
184
|
+
shutil.copy2(webm_path, mp4_path)
|
|
185
|
+
print(f"Saved (webm): {mp4_path}", file=sys.stderr)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
print("Error: no video file recorded", file=sys.stderr)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
if len(sys.argv) < 3:
|
|
194
|
+
print(
|
|
195
|
+
"Usage:\n"
|
|
196
|
+
" python browser_demo.py <url> <output_video> [task]\n"
|
|
197
|
+
" python browser_demo.py --highlights <video_path> <output_json> [task]",
|
|
198
|
+
file=sys.stderr,
|
|
199
|
+
)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
if sys.argv[1] == "--highlights":
|
|
203
|
+
video_path = sys.argv[2]
|
|
204
|
+
output = sys.argv[3]
|
|
205
|
+
task = sys.argv[4] if len(sys.argv) > 4 else "Demo the web app"
|
|
206
|
+
print(f"Extracting highlights...", file=sys.stderr)
|
|
207
|
+
highlights = extract_highlights(video_path, task)
|
|
208
|
+
with open(output, "w") as f:
|
|
209
|
+
json.dump(highlights, f, indent=2)
|
|
210
|
+
print(f"Saved {len(highlights)} highlights to: {output}", file=sys.stderr)
|
|
211
|
+
else:
|
|
212
|
+
url = sys.argv[1]
|
|
213
|
+
output = sys.argv[2]
|
|
214
|
+
task = sys.argv[3] if len(sys.argv) > 3 else "Explore the main features"
|
|
215
|
+
asyncio.run(record_browser_demo(url, task, output))
|
|
@@ -0,0 +1,272 @@
|
|
|
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 json
|
|
8
|
+
import os
|
|
9
|
+
import pty
|
|
10
|
+
import select
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_claude():
|
|
17
|
+
"""Find the claude CLI binary."""
|
|
18
|
+
import shutil
|
|
19
|
+
claude = shutil.which("claude")
|
|
20
|
+
if claude:
|
|
21
|
+
return claude
|
|
22
|
+
# Common locations
|
|
23
|
+
for path in [
|
|
24
|
+
os.path.expanduser("~/.local/bin/claude"),
|
|
25
|
+
"/usr/local/bin/claude",
|
|
26
|
+
os.path.expanduser("~/.claude/bin/claude"),
|
|
27
|
+
]:
|
|
28
|
+
if os.path.isfile(path):
|
|
29
|
+
return path
|
|
30
|
+
return "claude"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_demo_plan(command: str, context: str) -> list[dict]:
|
|
34
|
+
"""Use claude CLI to plan a demo sequence."""
|
|
35
|
+
prompt = f"""You are planning a terminal demo for a CLI tool. The tool is invoked with: {command}
|
|
36
|
+
|
|
37
|
+
Context about what this tool does:
|
|
38
|
+
{context}
|
|
39
|
+
|
|
40
|
+
Generate a JSON array of demo steps. Each step is an object with:
|
|
41
|
+
- "type": "command" (run a shell command)
|
|
42
|
+
- "value": the command string
|
|
43
|
+
- "delay": seconds to wait after (for dramatic effect)
|
|
44
|
+
- "description": one-line description
|
|
45
|
+
|
|
46
|
+
Make the demo:
|
|
47
|
+
- Show the most impressive features
|
|
48
|
+
- 5-8 steps max
|
|
49
|
+
- Start simple, build to the wow moment
|
|
50
|
+
- Use realistic commands
|
|
51
|
+
|
|
52
|
+
Return ONLY the JSON array, no markdown, no explanation."""
|
|
53
|
+
|
|
54
|
+
claude = find_claude()
|
|
55
|
+
print(f"Using claude at: {claude}", file=sys.stderr)
|
|
56
|
+
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[claude, "-p", prompt, "--output-format", "text"],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
timeout=120,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
print(f"claude stderr: {result.stderr}", file=sys.stderr)
|
|
66
|
+
raise RuntimeError(f"claude failed: {result.stderr}")
|
|
67
|
+
|
|
68
|
+
text = result.stdout.strip()
|
|
69
|
+
print(f"Claude output: {text[:200]}...", file=sys.stderr)
|
|
70
|
+
|
|
71
|
+
# Strip markdown fences if present
|
|
72
|
+
if "```" in text:
|
|
73
|
+
# Find JSON between fences
|
|
74
|
+
parts = text.split("```")
|
|
75
|
+
for part in parts:
|
|
76
|
+
part = part.strip()
|
|
77
|
+
if part.startswith("json"):
|
|
78
|
+
part = part[4:].strip()
|
|
79
|
+
if part.startswith("["):
|
|
80
|
+
text = part
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return json.loads(text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def record_demo(steps: list[dict], workdir: str, output_path: str):
|
|
87
|
+
"""Execute demo steps in a PTY and record as asciicast v2."""
|
|
88
|
+
cols, rows = 80, 24
|
|
89
|
+
start_time = time.time()
|
|
90
|
+
|
|
91
|
+
with open(output_path, "w") as f:
|
|
92
|
+
header = {
|
|
93
|
+
"version": 2,
|
|
94
|
+
"width": cols,
|
|
95
|
+
"height": rows,
|
|
96
|
+
"timestamp": int(start_time),
|
|
97
|
+
}
|
|
98
|
+
f.write(json.dumps(header) + "\n")
|
|
99
|
+
|
|
100
|
+
def write_event(event_type: str, data: str):
|
|
101
|
+
elapsed = time.time() - start_time
|
|
102
|
+
f.write(json.dumps([round(elapsed, 6), event_type, data]) + "\n")
|
|
103
|
+
f.flush()
|
|
104
|
+
|
|
105
|
+
for step in steps:
|
|
106
|
+
if step["type"] != "command":
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
cmd = step["value"]
|
|
110
|
+
desc = step.get("description", "")
|
|
111
|
+
|
|
112
|
+
# Show description as dim comment
|
|
113
|
+
if desc:
|
|
114
|
+
write_event("o", f"\x1b[38;5;245m# {desc}\x1b[0m\r\n")
|
|
115
|
+
time.sleep(0.3)
|
|
116
|
+
|
|
117
|
+
# Type the command character by character
|
|
118
|
+
write_event("o", "\x1b[38;5;76m$\x1b[0m ")
|
|
119
|
+
for char in cmd:
|
|
120
|
+
write_event("o", char)
|
|
121
|
+
time.sleep(0.04)
|
|
122
|
+
write_event("o", "\r\n")
|
|
123
|
+
time.sleep(0.2)
|
|
124
|
+
|
|
125
|
+
# Execute in PTY
|
|
126
|
+
pid, fd = pty.fork()
|
|
127
|
+
if pid == 0:
|
|
128
|
+
os.chdir(workdir)
|
|
129
|
+
os.execvp("/bin/sh", ["/bin/sh", "-c", cmd])
|
|
130
|
+
else:
|
|
131
|
+
deadline = time.time() + 15
|
|
132
|
+
while time.time() < deadline:
|
|
133
|
+
r, _, _ = select.select([fd], [], [], 0.1)
|
|
134
|
+
if r:
|
|
135
|
+
try:
|
|
136
|
+
data = os.read(fd, 4096)
|
|
137
|
+
if data:
|
|
138
|
+
write_event("o", data.decode("utf-8", errors="replace"))
|
|
139
|
+
else:
|
|
140
|
+
break
|
|
141
|
+
except OSError:
|
|
142
|
+
break
|
|
143
|
+
pid_result = os.waitpid(pid, os.WNOHANG)
|
|
144
|
+
if pid_result[0] != 0:
|
|
145
|
+
# Drain remaining output
|
|
146
|
+
try:
|
|
147
|
+
while True:
|
|
148
|
+
r, _, _ = select.select([fd], [], [], 0.1)
|
|
149
|
+
if r:
|
|
150
|
+
data = os.read(fd, 4096)
|
|
151
|
+
if data:
|
|
152
|
+
write_event("o", data.decode("utf-8", errors="replace"))
|
|
153
|
+
else:
|
|
154
|
+
break
|
|
155
|
+
else:
|
|
156
|
+
break
|
|
157
|
+
except OSError:
|
|
158
|
+
pass
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
os.close(fd)
|
|
163
|
+
except OSError:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
delay = step.get("delay", 1.0)
|
|
167
|
+
time.sleep(delay)
|
|
168
|
+
write_event("o", "\r\n")
|
|
169
|
+
|
|
170
|
+
print(f"Saved: {output_path}", file=sys.stderr)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def extract_highlights(cast_path: str, context: str) -> list[dict]:
|
|
174
|
+
"""Ask Claude to pick 3-4 highlight moments from the recorded session."""
|
|
175
|
+
# Read the asciicast and strip to just the text content
|
|
176
|
+
lines_output = []
|
|
177
|
+
with open(cast_path) as f:
|
|
178
|
+
for i, line in enumerate(f):
|
|
179
|
+
if i == 0:
|
|
180
|
+
continue # skip header
|
|
181
|
+
try:
|
|
182
|
+
event = json.loads(line)
|
|
183
|
+
if event[1] == "o":
|
|
184
|
+
lines_output.append(event[2])
|
|
185
|
+
except (json.JSONDecodeError, IndexError):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
raw_output = "".join(lines_output)
|
|
189
|
+
# Clean ANSI for Claude to read, but keep the raw for display
|
|
190
|
+
import re
|
|
191
|
+
clean = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_output)
|
|
192
|
+
|
|
193
|
+
prompt = f"""You are creating a highlights reel for a CLI tool demo video. Here is the full terminal output:
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
{clean[:3000]}
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
Context: {context}
|
|
200
|
+
|
|
201
|
+
Pick 3-4 highlight moments that would look impressive in a short video. For each highlight, return:
|
|
202
|
+
- "label": short label (1-2 words) like "Initialize", "Configure", "Run", "Results"
|
|
203
|
+
- "lines": array of objects, each with "text" (string), and optionally "color" (hex), "bold" (bool), "dim" (bool), "isPrompt" (bool if it's a shell command)
|
|
204
|
+
- "zoomLine": (optional) index of the most impressive line to zoom into
|
|
205
|
+
|
|
206
|
+
Each highlight should have 4-8 lines max. Keep the text concise.
|
|
207
|
+
Use these colors: green="#50fa7b", yellow="#f1fa8c", purple="#bd93f9", red="#ff5555", dim="#6272a4", white="#f8f8f2"
|
|
208
|
+
|
|
209
|
+
Return ONLY a JSON array of highlights. No markdown fences."""
|
|
210
|
+
|
|
211
|
+
claude = find_claude()
|
|
212
|
+
result = subprocess.run(
|
|
213
|
+
[claude, "-p", prompt, "--output-format", "text"],
|
|
214
|
+
capture_output=True,
|
|
215
|
+
text=True,
|
|
216
|
+
timeout=120,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
text = result.stdout.strip()
|
|
220
|
+
if "```" in text:
|
|
221
|
+
parts = text.split("```")
|
|
222
|
+
for part in parts:
|
|
223
|
+
part = part.strip()
|
|
224
|
+
if part.startswith("json"):
|
|
225
|
+
part = part[4:].strip()
|
|
226
|
+
if part.startswith("["):
|
|
227
|
+
text = part
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
return json.loads(text)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
if len(sys.argv) < 4:
|
|
235
|
+
print("Usage: python cli_demo.py <command> <workdir> <output> [context]", file=sys.stderr)
|
|
236
|
+
print(" python cli_demo.py --highlights <cast_file> <output_json> [context]", file=sys.stderr)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
# Highlights mode: extract highlights from existing recording
|
|
240
|
+
if sys.argv[1] == "--highlights":
|
|
241
|
+
cast_file = sys.argv[2]
|
|
242
|
+
output = sys.argv[3]
|
|
243
|
+
context = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
244
|
+
print(f"Extracting highlights from: {cast_file}", file=sys.stderr)
|
|
245
|
+
highlights = extract_highlights(cast_file, context)
|
|
246
|
+
with open(output, "w") as f:
|
|
247
|
+
json.dump(highlights, f, indent=2)
|
|
248
|
+
print(f"Saved {len(highlights)} highlights to: {output}", file=sys.stderr)
|
|
249
|
+
sys.exit(0)
|
|
250
|
+
|
|
251
|
+
# Record mode: plan + record + extract highlights
|
|
252
|
+
command = sys.argv[1]
|
|
253
|
+
workdir = sys.argv[2]
|
|
254
|
+
output = sys.argv[3]
|
|
255
|
+
context = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
256
|
+
|
|
257
|
+
print(f"Planning demo for: {command}", file=sys.stderr)
|
|
258
|
+
steps = generate_demo_plan(command, context)
|
|
259
|
+
print(f"Generated {len(steps)} steps:", file=sys.stderr)
|
|
260
|
+
for s in steps:
|
|
261
|
+
print(f" $ {s['value']} — {s.get('description', '')}", file=sys.stderr)
|
|
262
|
+
|
|
263
|
+
print("Recording...", file=sys.stderr)
|
|
264
|
+
record_demo(steps, workdir, output)
|
|
265
|
+
|
|
266
|
+
# Extract highlights from the recording
|
|
267
|
+
highlights_path = output.replace(".cast", "-highlights.json")
|
|
268
|
+
print("Extracting highlights...", file=sys.stderr)
|
|
269
|
+
highlights = extract_highlights(output, context)
|
|
270
|
+
with open(highlights_path, "w") as f:
|
|
271
|
+
json.dump(highlights, f, indent=2)
|
|
272
|
+
print(f"Saved {len(highlights)} highlights to: {highlights_path}", file=sys.stderr)
|