| name | demo-recording |
| description | Record or regenerate terminal demos for the ntucool project — the CLI demo (VHS, scripted in demo/cli.tape) and the MCP / agent demo (asciinema recording an interactive Claude Code session, rendered via agg), with personal data masked through the project's redact pipeline. Use this whenever the user asks to update, record, regenerate, tweak, restyle, or speed up a demo GIF/video for this project, or mentions VHS .tape files, asciinema .cast files, agg flags, redact.py, or PII masking for terminal recordings — even if they don't explicitly name the tools. |
Demo recording
The demo/ directory has two independent recipes. Both produce a GIF and optionally a .cast for sharing on asciinema.org.
| Demo | Tool | Driver | Output |
|---|
| CLI | VHS | Scripted tape | demo/cli.gif |
| MCP / agent | asciinema + agg | Interactive — user drives Claude Code | demo/mcp.cast + demo/mcp.gif |
Pick VHS when the content is fully scriptable and you want a deterministic artifact that re-runs cleanly after every change. Pick asciinema when authenticity matters — real timing, real agent reasoning, real tool calls — which scripting would flatten.
CLI demo (VHS)
demo/
├── cli.tape VHS script: command sequence + styling
├── record.sh Entry point — sets PATH, runs vhs
├── bin/cool Masking wrapper around the real `cool` binary
├── redact.py Masks personal data on stdout
└── cli.gif Output
Regenerate: ./demo/record.sh
The script puts demo/bin first on PATH so cool in the tape resolves to the masking wrapper. The wrapper finds the real cool by scanning PATH minus its own directory, then pipes stdout through redact.py.
Tweaking commands shown. Edit the Type "cool ..." blocks in cli.tape. Each follows the pattern Type / Sleep / Enter / Sleep — the first sleep lets viewers register the command, the second lets them read the output.
Tweaking styling. The Set block at the top of cli.tape controls FontSize, Theme, WindowBar, BorderRadius, Margin, etc. The current theme is Catppuccin Mocha; vhs themes lists alternatives.
MCP demo (asciinema + agg)
demo/
├── record-mcp.sh Entry point — prints prompts, records, redacts, renders
├── redact_cast.py Applies redact.py to each output event of a .cast
├── mcp.raw.cast Pre-redaction recording (review then delete)
├── mcp.cast Redacted recording (shareable)
└── mcp.gif Rendered GIF
Record: ./demo/record-mcp.sh
The script:
- Prints three suggested prompts and pauses for confirmation.
- Launches
asciinema rec --command "claude --dangerously-skip-permissions" so the recording IS the Claude Code session — when the user /exits, recording stops.
- Pipes the raw cast through
redact_cast.py.
- Greps the redacted cast for residual PII and warns if anything leaked across event chunks.
- Runs
agg to render the GIF.
The suggested prompts intentionally avoid asking about grades — grade numbers in raw MCP JSON output ("current_score": 88.6) need different regex than the CLI's current: 88.6 (A-) shape. If a new prompt would expose grades, extend redact.py first with a JSON-shaped rule.
Why --dangerously-skip-permissions. Smooth recording. The demo only exercises read-only ntucool MCP tools. If a prompt could trigger a write tool (e.g., assignments_submit), remove the flag and approve interactively.
Rendering knobs (agg flags currently in record-mcp.sh):
| Flag | Value | Effect |
|---|
--font-size | 22 | Larger = sharper at README scale |
--text-font-family | DejaVu Sans Mono,Noto Sans Mono CJK TC | DejaVu for ASCII, Noto for CJK fallback (agg's default chain has no CJK and will drop chars like 哪, 內) |
--speed | 2 | Whole recording plays 2× faster |
--idle-time-limit | 1 | Caps any single idle gap to 1 s |
--speed and --idle-time-limit are the two speedup knobs — --speed scales everything, --idle-time-limit cuts dead air. Start with --speed 2 --idle-time-limit 1; go gentler (1.5 / 0.75) if tool-call animations look choppy.
Re-render without re-recording — once a .cast exists, render variants are cheap:
agg --speed 2 --idle-time-limit 1 --font-size 22 \
--text-font-family "DejaVu Sans Mono,Noto Sans Mono CJK TC" \
demo/mcp.cast demo/mcp.gif
PII masking philosophy
Both demos share redact.py. The rule (also persisted in user memory):
Mask, don't fake. Sensitive values become █ bars of the same display width — not plausible fake names or grades. A fake reads as real and is misleading; a █ bar unambiguously says "redacted".
What's masked:
| Field | Rule | Example |
|---|
| Chinese name | Keep first character, mask the rest | 洪愷希 → 洪████ |
| Romanized name | Keep first letter, mask the rest | HONG, CASEY → H██████████ |
| Student ID | Keep b104010 prefix, mask trailing digits | b10401006 → b104010██ |
| Grade values | Mask current: X (Y) / final: X (Y) shape | 88.6 (A-) → ████ (██) |
| Submission scores | Mask earned numerator, keep the assignment total | 93/100 → ██/100 |
| "No grade" markers | Kept as-is — — (—) is not sensitive | unchanged |
To re-target a different account, edit only the IDENTITY dict at the top of redact.py. Grade and submission rules are shape-matched and work for any course / assignment automatically.
CLI demo: rules apply to stdout, line-oriented — clean. MCP demo (redact_cast.py): each .cast output event is redacted independently, so if a sensitive substring is split across two events (rare, but possible for streamed agent text), it can leak. The verification grep in record-mcp.sh catches that — fix by hand by editing the .cast directly.
Dependencies
All installed without sudo on this machine:
| Tool | Purpose | Installed via |
|---|
vhs, ttyd, ffmpeg | CLI demo recording + rendering | Static binaries → ~/.local/bin |
asciinema | TUI session recording | uv tool install asciinema |
agg | .cast → GIF | cargo install --git https://github.com/asciinema/agg --tag v1.8.1 |
Noto Sans Mono CJK TC | CJK font for agg | apt install fonts-noto-cjk |
A couple of gotchas to remember when reproducing on a fresh machine:
- The prebuilt
agg binary from GitHub releases needs GLIBC 2.38; older systems have to build from source as above. mise use -g ubi:asciinema/agg will hit this on Ubuntu 22.04.
- The
agg crate on crates.io is a different project (a 2D graphics engine); cargo install agg installs the wrong thing — always use the git URL.
Both entry scripts command -v-check each dependency at the top, so a missing tool fails fast with a clear message.
Common tasks
Add a command to the CLI demo. Insert another Type "..." / Sleep / Enter / Sleep block in cli.tape. Re-run ./demo/record.sh.
Change which prompts the MCP demo suggests. Edit the heredoc near the top of record-mcp.sh.
Mask a new field. Add a substring entry to IDENTITY in redact.py, or for structural fields (e.g. a JSON key with a sensitive value), add a re.sub(...) call near the bottom of redact(). Both demos pick it up automatically — redact_cast.py calls redact() per event.
Just regenerate the GIFs after pulling new code. ./demo/record.sh for the CLI demo (always deterministic). For the MCP demo, if you don't want to re-record, just re-run agg on the existing demo/mcp.cast (see the "Re-render without re-recording" snippet above).