
Asciinema Converter
- 116 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Use asciinema-converter for development tasks
About
asciinema-converter: A skill for development. This provides functionality for development workflows.
- asciinema-converter
Asciinema Converter by the numbers
- 116 all-time installs (skills.sh)
- Ranked #2,887 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill asciinema-converterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Use asciinema-converter for development tasks
Files
asciinema-converter
Convert asciinema .cast recordings to clean .txt files for Claude Code analysis. Achieves 950:1 compression (3.8GB -> 4MB) by stripping ANSI codes and JSON structure.
Platform: macOS, Linux (requires asciinema CLI v2.4+)
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
When to Use This Skill
Use this skill when:
- Converting .cast recordings to searchable .txt format
- Preparing recordings for Claude Code Read/Grep tools
- Batch converting multiple recordings
- Reducing storage size of session archives
- Extracting clean text from ANSI-coded terminal output
---
Why Convert?
| Format | Size (22h session) | Claude Code Compatible | Searchable |
|---|---|---|---|
| .cast | 3.8GB | No (NDJSON + ANSI) | Via jq |
| .txt | ~4MB | Yes (clean text) | Grep/Read |
Key benefit: Claude Code's Read and Grep tools work directly on .txt output.
---
Requirements
| Component | Required | Installation | Notes |
|---|---|---|---|
| asciinema | Yes | brew install asciinema | v2.4+ for convert cmd |
---
Workflow Overview
IMPORTANT: All phases are MANDATORY. Do NOT skip any phase. AskUserQuestion MUST be used at each decision point.
Single File Mode (Phases 0-6)
| Phase | Purpose | Key Action |
|---|---|---|
| 0 | Preflight check | Verify asciinema CLI v2.4+ |
| 1 | File discovery & selection | AskUserQuestion: file to convert |
| 2 | Output options | AskUserQuestion: conversion opts |
| 3 | Output location | AskUserQuestion: save destination |
| 4 | Execute conversion | asciinema convert -f txt |
| 5 | Timestamp index | Optional [HH:MM:SS] index |
| 6 | Next steps | AskUserQuestion: what's next |
Full implementation details: Workflow Phases
Batch Mode (Phases 7-10)
Activated via --batch flag. Converts all .cast files in a directory with organized output.
| Phase | Purpose | Key Action |
|---|---|---|
| 7 | Source selection | AskUserQuestion (skip if --source) |
| 8 | Output organization | AskUserQuestion (skip if --output-dir) |
| 9 | Execute batch | Convert all with progress reporting |
| 10 | Batch next steps | AskUserQuestion: what's next |
Full implementation details: Batch Workflow
---
iTerm2 Filename Format
iTerm2 auto-logged files follow this format:
{creationTimeString}.{profileName}.{termid}.{iterm2.pid}.{autoLogId}.castExample: 20260118_232025.Claude Code.w0t1p1.70C05103-2F29-4B42-8067-BE475DB6126A.68721.4013739999.cast
| Component | Description | Example |
|---|---|---|
| creationTimeString | YYYYMMDD_HHMMSS | 20260118_232025 |
| profileName | iTerm2 profile (may have dots) | Claude Code |
| termid | Window/tab/pane identifier | w0t1p1 |
| iterm2.pid | iTerm2 process UUID | 70C05103-2F29-4B42-8067-BE475DB6126A |
| autoLogId | Session auto-log identifier | 68721.4013739999 |
---
CLI Quick Reference
# Basic conversion
asciinema convert -f txt recording.cast recording.txt
# Check asciinema version
asciinema --version
# Verify convert command exists
asciinema convert --help---
Reference Documentation
Internal References
- Workflow Phases - Single file mode phases 0-6 with full scripts
- Batch Workflow - Batch mode phases 7-10 with full scripts
- Task Templates - TodoWrite templates for single and batch modes
- Post-Change Checklist - Verification after modifications
- Anti-Patterns - Common mistakes to avoid
- Batch Processing - Patterns for bulk conversion
- Integration Guide - Chaining with analyze/summarize
External References
- asciinema convert command
- asciinema-cast-format skill
---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| convert command not found | asciinema too old | Upgrade: brew upgrade asciinema (need v2.4+) |
| asciinema not installed | Missing CLI | brew install asciinema |
| Empty output file | Corrupted .cast input | Verify .cast file has valid NDJSON structure |
| Conversion failed | Invalid cast format | Check header line is valid JSON with jq |
| numfmt not found | macOS missing coreutils | Use raw byte count or brew install coreutils |
| stat syntax error | Linux vs macOS difference | Script handles both; check stat version |
| Batch skipping all files | All .txt already exist | Use --skip-existing=false to reconvert |
| Permission denied on output | Directory not writable | Check output directory permissions |
Post-Execution Reflection
After this skill completes, check before closing:
1. Did the command succeed? — If not, fix the instruction or error table that caused the failure. 2. Did parameters or output change? — If the underlying tool's interface drifted, update Usage examples and Parameters table to match. 3. Was a workaround needed? — If you had to improvise (different flags, extra steps), update this SKILL.md so the next invocation doesn't need the same workaround.
Only update if the issue is real and reproducible — not speculative.
Anti-Patterns in asciinema Conversion
Common mistakes and their remediation when converting .cast files to .txt.
Wrong Format Flag
Problem
Using -f raw instead of -f txt produces NDJSON output, not clean text.
# WRONG: Raw format preserves ANSI codes
asciinema convert -f raw input.cast output.txt # Still has ANSI!
# CORRECT: Text format strips ANSI
asciinema convert -f txt input.cast output.txtImpact
- ANSI escape sequences bloat file size
- Claude Code Read/Grep tools see garbage characters
- Compression ratio significantly worse (~10:1 vs ~950:1)
Detection
# Check for ANSI escape codes in output
grep -P '\x1b\[' output.txt && echo "ERROR: ANSI codes present"---
Memory Issues with Large Files
Problem
Files >1GB can cause memory exhaustion during conversion.
Symptoms
asciinema converthangs or crashes- System becomes unresponsive
- Error: "MemoryError" or "Killed"
Impact
- ~5% of iTerm2 auto-logged sessions exceed 1GB
- Typical 8-hour coding session: 200MB-500MB
- 24-hour sessions or high output: 1-4GB
Remediation
# Check file size before conversion
file_size=$(stat -f%z "$cast_file" 2>/dev/null || stat -c%s "$cast_file")
if [[ $file_size -gt 1073741824 ]]; then # 1GB
echo "WARNING: File >1GB, may cause memory issues"
# Consider splitting or streaming approach
fiWorkaround for Large Files
# Stream-process large files (experimental)
tail -n +2 large.cast | jq -r '.[2]' | \
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' > output.txt---
Path Handling Issues
Problem
Filenames with spaces, special characters, or Unicode cause failures.
Common Failures
# WRONG: Unquoted paths fail with spaces
asciinema convert $FILE output.txt # Fails if path has spaces
# WRONG: Glob patterns expand unexpectedly
for f in *.cast; do # Fails if no .cast files existCorrect Handling
# ALWAYS quote paths
asciinema convert "$cast_file" "$txt_file"
# Guard against empty globs
shopt -s nullglob
for cast_file in "$SOURCE_DIR"/*.cast; do
[[ -f "$cast_file" ]] || continue
# ...
doneiTerm2 Filename Gotcha
iTerm2 profile names can contain dots and spaces:
Claude Code→ spacesmy.profile.name→ dots look like extensions
# This filename has profile "Claude Code" with spaces
20260118_232025.Claude Code.w0t1p1.UUID.pid.id.cast---
Missing Preflight Check
Problem
Running conversion without verifying asciinema is installed or supports convert.
Symptoms
command not found: asciinemaError: Unknown command 'convert'(old asciinema version)
Remediation
Always run preflight before conversion:
# Check asciinema exists and convert command works
if ! command -v asciinema &>/dev/null; then
echo "ERROR: asciinema not installed"
exit 1
fi
if ! asciinema convert --help &>/dev/null 2>&1; then
echo "ERROR: asciinema convert not available (need v2.4+)"
exit 1
fi---
Re-Converting Unchanged Files
Problem
Batch conversion without skip logic wastes CPU on already-converted files.
Impact
- 2400 files × 5 seconds = 3.3 hours wasted
- Disk I/O thrashing
- Repeated compression calculations
Remediation
Always use skip-existing logic:
txt_file="$OUTPUT_DIR/${basename}.txt"
# Skip if already converted
if [[ -f "$txt_file" ]]; then
echo "SKIP: $basename (already exists)"
((skipped++))
continue
fiAdvanced: Size-Based Invalidation
# Re-convert if source is newer or larger
cast_mtime=$(stat -f%m "$cast_file" 2>/dev/null)
txt_mtime=$(stat -f%m "$txt_file" 2>/dev/null)
if [[ -f "$txt_file" && "$txt_mtime" -gt "$cast_mtime" ]]; then
echo "SKIP: $basename (up to date)"
continue
fi---
Mixing Batch and Single Mode
Problem
Using both positional file argument and --batch flag causes undefined behavior.
Example
# UNDEFINED: What should this do?
/asciinema-tools:convert session.cast --batchRemediation
Mutual exclusivity check:
if [[ -n "$FILE" && "$BATCH_MODE" == "true" ]]; then
echo "ERROR: Cannot use both file argument and --batch"
echo "Use: /asciinema-tools:convert FILE # Single file"
echo "Or: /asciinema-tools:convert --batch # Directory"
exit 1
fi---
Ignoring Conversion Failures
Problem
Silent failures in batch mode leave partially converted directories.
Symptoms
- Missing .txt files for some .cast files
- No error log
- Inconsistent output directory
Remediation
Track and report failures:
failed=0
failed_files=()
for cast_file in "$SOURCE_DIR"/*.cast; do
if ! asciinema convert -f txt "$cast_file" "$txt_file" 2>/dev/null; then
echo "FAIL: $basename"
((failed++))
failed_files+=("$basename")
fi
done
# Report failures at end
if [[ $failed -gt 0 ]]; then
echo ""
echo "=== FAILED FILES ==="
printf '%s\n' "${failed_files[@]}"
fi---
Checklist
Before running conversions:
- [ ] Preflight check passed (asciinema installed, convert available)
- [ ] Using
-f txtformat flag - [ ] All paths are quoted
- [ ] Skip-existing logic enabled for batch
- [ ] Large file warning for >1GB files
- [ ] Failure tracking enabled
---
Related
- Batch Processing - Patterns for bulk conversion
- Integration Guide - Chaining with analyze
Batch Processing Patterns
Patterns and best practices for bulk conversion of .cast files.
Directory Organization
Recommended Structure
~/Downloads/cast-txt/ # Default output directory
├── 20260118_232025.Claude Code.w0t1p1.*.txt
├── 20260118_235012.Claude Code.w0t2p1.*.txt
└── ...Alternative: Date-Based Hierarchy
For archives with thousands of files:
~/.local/share/asciinema-txt/
├── 2026/
│ ├── 01/
│ │ ├── 18/
│ │ │ ├── session-001.txt
│ │ │ └── session-002.txt
│ │ └── 19/
│ └── 02/
└── 2025/Source Directories
| Directory | Purpose | Notes |
|---|---|---|
~/asciinemalogs | iTerm2 auto-logged (future) | Default when configured |
~/Downloads | Manual downloads | Fallback if asciinemalogs empty |
${PWD} | Current project | For project-specific recordings |
---
Skip/Resume Logic
Basic Skip (Default)
Skip files that already have corresponding .txt:
txt_file="$OUTPUT_DIR/${basename}.txt"
if [[ -f "$txt_file" ]]; then
echo "SKIP: $basename (already exists)"
((skipped++))
continue
fiTimestamp-Based Invalidation
Re-convert if source is newer:
cast_mtime=$(stat -f%m "$cast_file" 2>/dev/null)
txt_mtime=$(stat -f%m "$txt_file" 2>/dev/null || echo 0)
if [[ -f "$txt_file" && "$txt_mtime" -ge "$cast_mtime" ]]; then
echo "SKIP: $basename (up to date)"
continue
fiForce Re-Convert
Disable skip logic with --skip-existing=false:
if [[ "$SKIP_EXISTING" != "false" && -f "$txt_file" ]]; then
echo "SKIP: $basename"
continue
fi---
Progress Reporting
Per-File Progress
echo "[$current/$total] Converting: $basename"Aggregate Summary
echo ""
echo "=== Batch Complete ==="
echo "Converted: $converted"
echo "Skipped: $skipped"
echo "Failed: $failed"
echo "Total: $total"Compression Ratio Reporting
# Per-file ratio
ratio=$((input_size / output_size))
echo "OK: $basename (${ratio}:1)"
# Aggregate ratio
if [[ $total_output_size -gt 0 ]]; then
overall_ratio=$((total_input_size / total_output_size))
echo "Overall compression: ${overall_ratio}:1"
fi---
Handling 1000+ Files
Memory-Efficient Iteration
Avoid loading all filenames into memory:
# WRONG: Loads all names into array
files=($(find . -name "*.cast"))
# CORRECT: Stream processing
find "$SOURCE_DIR" -maxdepth 1 -name "*.cast" -type f | while read -r cast_file; do
# Process one at a time
doneSize-Tiered Processing
Process largest files first to identify memory issues early:
# Sort by size descending, process largest first
find "$SOURCE_DIR" -maxdepth 1 -name "*.cast" -type f -print0 | \
xargs -0 ls -S | while read -r cast_file; do
# Largest files first
doneParallel Processing (Advanced)
Use GNU parallel for multi-core conversion:
# Requires: brew install parallel
find "$SOURCE_DIR" -name "*.cast" | \
parallel -j4 'asciinema convert -f txt {} {.}.txt'Caution: Monitor memory usage with parallel conversion of large files.
---
iTerm2 Auto-Log Filename Parsing
Filename Format
{creationTimeString}.{profileName}.{termid}.{iterm2.pid}.{autoLogId}.castExample
20260118_232025.Claude Code.w0t1p1.70C05103-2F29-4B42-8067-BE475DB6126A.68721.4013739999.castComponent Extraction
Parse from right to left (most reliable):
filename="20260118_232025.Claude Code.w0t1p1.70C05103-2F29-4B42-8067-BE475DB6126A.68721.4013739999.cast"
# Remove .cast extension
base="${filename%.cast}"
# Extract autoLogId (last component)
autoLogId="${base##*.}"
base="${base%.*}"
# Extract pid
pid="${base##*.}"
base="${base%.*}"
# Extract UUID (contains hyphens)
uuid="${base##*.}"
base="${base%.*}"
# Extract termid (w#t#p# format)
termid="${base##*.}"
base="${base%.*}"
# Remaining is: creationTimeString.profileName
# Profile name can have dots, so extract timestamp first
timestamp="${base%%.*}"
profileName="${base#*.}"Metadata Extraction
# Parse creation timestamp
timestamp="20260118_232025"
date="${timestamp:0:8}" # 20260118
time="${timestamp:9:6}" # 232025
year="${date:0:4}" # 2026
month="${date:4:2}" # 01
day="${date:6:2}" # 18---
Error Handling
Allow-on-Error Semantics
Continue batch even when individual files fail:
for cast_file in "$SOURCE_DIR"/*.cast; do
if ! asciinema convert -f txt "$cast_file" "$txt_file" 2>/dev/null; then
echo "FAIL: $basename"
((failed++))
failed_files+=("$cast_file")
continue # Don't abort batch
fi
doneError Log
Write failures to log file:
ERROR_LOG="$OUTPUT_DIR/.conversion-errors.log"
if ! asciinema convert -f txt "$cast_file" "$txt_file" 2>>"$ERROR_LOG"; then
echo "FAIL: $basename (see $ERROR_LOG)"
fiPost-Batch Summary
if [[ $failed -gt 0 ]]; then
echo ""
echo "=== FAILED FILES ($failed) ==="
printf '%s\n' "${failed_files[@]}"
echo ""
echo "Re-run failed files:"
echo "for f in ${failed_files[*]}; do asciinema convert -f txt \"\$f\" \"${OUTPUT_DIR}/\$(basename \"\$f\" .cast).txt\"; done"
fi---
Performance Benchmarks
Typical Conversion Speeds
| File Size | Duration | Compression | Notes |
|---|---|---|---|
| 10MB | ~1 second | ~100:1 | Short session |
| 100MB | ~5 seconds | ~500:1 | Typical 2-hour session |
| 500MB | ~20 seconds | ~800:1 | Full day session |
| 1GB | ~45 seconds | ~900:1 | Extended session |
| 4GB | ~3 minutes | ~950:1 | Maximum observed |
Batch Estimates
| Files | Avg Size | Est. Time | Notes |
|---|---|---|---|
| 100 | 50MB | ~2 minutes | Daily batch |
| 500 | 100MB | ~15 minutes | Weekly cleanup |
| 2400 | 150MB | ~1 hour | Full archive (skip mode) |
---
Related
- Anti-Patterns - Common mistakes to avoid
- Integration Guide - Chaining with analyze
Batch Mode Workflow (Phases 7-10)
Batch mode converts all .cast files in a directory with organized output. Activated via --batch flag.
Use case: Convert 1000+ iTerm2 auto-logged recordings efficiently.
Phase 7: Batch Source Selection
Purpose: Select source directory for batch conversion.
Trigger: --batch flag without --source argument.
Question: "Select source directory for batch conversion:"
Header: "Source"
Options:
- Label: "~/asciinemalogs (iTerm2 default)" (Recommended)
Description: "Auto-logged iTerm2 recordings"
- Label: "~/Downloads"
Description: "Recent downloads containing .cast files"
- Label: "Current directory"
Description: "Convert .cast files in current working directory"
- Label: "Custom path"
Description: "Specify a custom source directory"Skip condition: If --source argument provided, skip this phase.
---
Phase 8: Batch Output Organization
Purpose: Configure output directory structure.
Trigger: --batch flag without --output-dir argument.
Question: "Where should converted files be saved?"
Header: "Output"
Options:
- Label: "~/Downloads/cast-txt/ (Recommended)"
Description: "Organized output directory, easy to find"
- Label: "Same as source"
Description: "Save .txt files next to .cast files"
- Label: "Custom directory"
Description: "Specify a custom output location"Skip condition: If --output-dir argument provided, skip this phase.
---
Phase 9: Execute Batch Conversion
Purpose: Convert all files with progress reporting.
/usr/bin/env bash << 'BATCH_EOF'
SOURCE_DIR="${1:?Source directory required}"
OUTPUT_DIR="${2:?Output directory required}"
SKIP_EXISTING="${3:-true}"
mkdir -p "$OUTPUT_DIR"
echo "=== Batch Conversion ==="
echo "Source: $SOURCE_DIR"
echo "Output: $OUTPUT_DIR"
echo "Skip existing: $SKIP_EXISTING"
echo ""
total=0
converted=0
skipped=0
failed=0
total_input_size=0
total_output_size=0
# Count files first
total=$(find "$SOURCE_DIR" -maxdepth 1 -name "*.cast" -type f 2>/dev/null | wc -l | tr -d ' ')
echo "Found $total .cast files"
echo ""
for cast_file in "$SOURCE_DIR"/*.cast; do
[[ -f "$cast_file" ]] || continue
basename=$(basename "$cast_file" .cast)
txt_file="$OUTPUT_DIR/${basename}.txt"
# Skip if already converted (and skip mode enabled)
if [[ "$SKIP_EXISTING" == "true" && -f "$txt_file" ]]; then
echo "SKIP: $basename (already exists)"
((skipped++))
continue
fi
# Get input size
input_size=$(stat -f%z "$cast_file" 2>/dev/null || stat -c%s "$cast_file" 2>/dev/null)
# Convert
if asciinema convert -f txt "$cast_file" "$txt_file" 2>/dev/null; then
output_size=$(stat -f%z "$txt_file" 2>/dev/null || stat -c%s "$txt_file" 2>/dev/null)
if [[ $output_size -gt 0 ]]; then
ratio=$((input_size / output_size))
else
ratio=0
fi
echo "OK: $basename (${ratio}:1 compression)"
((converted++))
total_input_size=$((total_input_size + input_size))
total_output_size=$((total_output_size + output_size))
else
echo "FAIL: $basename"
((failed++))
fi
done
echo ""
echo "=== Batch Complete ==="
echo "Converted: $converted"
echo "Skipped: $skipped"
echo "Failed: $failed"
if [[ $total_output_size -gt 0 ]]; then
overall_ratio=$((total_input_size / total_output_size))
echo "Overall compression: ${overall_ratio}:1"
fi
echo "Output directory: $OUTPUT_DIR"
BATCH_EOF---
Phase 10: Batch Next Steps
Purpose: Guide user after batch conversion.
Question: "Batch conversion complete. What's next?"
Header: "Next"
Options:
- Label: "Batch analyze with /asciinema-tools:analyze --batch"
Description: "Run keyword extraction on all converted files"
- Label: "Open output directory"
Description: "View converted files in Finder"
- Label: "Done"
Description: "Exit - no further action needed"Evolution Log
Convention: Reverse chronological order (newest on top, oldest at bottom). Prepend new entries.
---
2026-02-26: Initial Evolution Log
Status: Skill is in use and maintained. Track improvements here.
Purpose
This evolution log tracks updates to the asciinema-converter skill. Each entry should note:
- What changed (content, structure, tooling)
- Why it changed (bug fix, feature request, best practice)
- Files affected
How to Use
1. When updating SKILL.md or references, add an entry here with the date 2. Keep entries reverse-chronological (newest first) 3. Link to ADRs or GitHub issues when relevant 4. Reference specific line changes when helpful
---
Integration Guide
How to chain asciinema-converter with other asciinema-tools skills.
Skill Chain Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ .cast │────▶│ convert │────▶│ .txt │
│ (NDJSON) │ │ │ │ (clean text)│
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ analyze │ │ summarize │ │ Read │
│ (keywords) │ │ (AI deep) │ │ (manual) │
└─────────────┘ └─────────────┘ └─────────────┘Single File Chains
Convert → Analyze
Extract keywords and patterns from a single converted file:
# Step 1: Convert
/asciinema-tools:convert ~/Downloads/session.cast
# Step 2: Analyze (auto-chained with --analyze flag)
/asciinema-tools:convert ~/Downloads/session.cast --analyzeOr run separately:
/asciinema-tools:analyze ~/Downloads/session.txtConvert → Summarize
Deep AI analysis of session content:
# Convert first
/asciinema-tools:convert ~/Downloads/session.cast -o ~/tmp/session.txt
# Then summarize
/asciinema-tools:summarize ~/tmp/session.txt---
Batch Chains
Batch Convert → Batch Analyze
Convert all files, then analyze all outputs:
# Step 1: Batch convert
/asciinema-tools:convert --batch --source ~/Downloads --output-dir ~/cast-txt/
# Step 2: Batch analyze (if skill supports it)
/asciinema-tools:analyze --batch --source ~/cast-txt/Post-Session Workflow
Complete workflow for session wrap-up:
/asciinema-tools:post-sessionThis internally chains:
1. finalize - Stop active recordings 2. convert - Convert to .txt 3. summarize - Generate AI summary
---
File Path Conventions
Naming Pattern
Output files preserve base name:
| Input | Output |
|---|---|
session.cast | session.txt |
20260118_232025.*.cast | 20260118_232025.*.txt |
path/to/recording.cast | path/to/recording.txt |
Directory Structure
When using --output-dir:
| Source | Output |
|---|---|
~/Downloads/session.cast | ~/cast-txt/session.txt |
~/asciinemalogs/rec.cast | ~/Downloads/cast-txt/rec.txt |
Index Files
When --index flag used:
| Base | Index |
|---|---|
session.txt | session.index.txt |
session.cast | session.index.txt |
---
Manifest Files (Advanced)
For automated pipelines, batch convert generates a manifest:
{
"source_dir": "~/Downloads",
"output_dir": "~/cast-txt",
"converted": [
{
"source": "session1.cast",
"output": "session1.txt",
"size_input": 104857600,
"size_output": 131072,
"compression_ratio": 800
}
],
"skipped": ["session2.cast"],
"failed": [],
"timestamp": "2026-01-19T12:00:00Z"
}Location: $OUTPUT_DIR/.convert-manifest.json
Using Manifest for Downstream Skills
# Read manifest and process converted files
jq -r '.converted[].output' ~/cast-txt/.convert-manifest.json | \
while read -r txt_file; do
/asciinema-tools:analyze "$txt_file"
done---
Workflow Commands
Full Workflow
Record, convert, and analyze in one command:
/asciinema-tools:full-workflowChains:
1. record → Start recording 2. (user works) 3. convert → Convert to .txt 4. analyze → Extract insights
Bootstrap
Pre-session setup:
/asciinema-tools:bootstrapOutputs a script to start recording before entering Claude Code.
Post-Session
End-of-session cleanup:
/asciinema-tools:post-sessionChains:
1. finalize → Stop orphaned recordings 2. convert → Convert recent .cast files 3. summarize → AI analysis
---
Error Handling in Chains
Fail-Fast (Single File)
If convert fails, subsequent skills don't run:
# This aborts if conversion fails
/asciinema-tools:convert session.cast --analyzeContinue-on-Error (Batch)
Batch mode continues even if individual files fail:
# Converts all possible files, reports failures at end
/asciinema-tools:convert --batchChain Recovery
If a downstream skill fails:
# Re-run just the failed step
/asciinema-tools:analyze ~/cast-txt/session.txt
# Don't re-convert (already done)---
Performance Considerations
Chaining Overhead
| Chain | Overhead | Notes |
|---|---|---|
| convert only | Baseline | ~5s per 100MB |
| convert → analyze | +2-3s | Keyword extraction |
| convert → summarize | +30-60s | AI API calls |
| batch convert | Baseline × N | Parallelizable |
| batch convert → analyze | +2s × N | Sequential by default |
Parallel Batch Analysis
For large batches, run analysis in parallel:
# Convert first (sequential for disk I/O)
/asciinema-tools:convert --batch --output-dir ~/cast-txt/
# Then analyze in parallel (CPU-bound)
find ~/cast-txt -name "*.txt" | parallel -j4 '/asciinema-tools:analyze {}'---
Related
- Anti-Patterns - Common mistakes to avoid
- Batch Processing - Bulk conversion patterns
- asciinema-analyzer skill - Keyword extraction and semantic analysis
/asciinema-tools:summarizecommand - AI-powered iterative deep-dive analysis
Post-Change Checklist
After modifying this skill:
Single File Mode
1. [ ] Preflight check detects asciinema version correctly 2. [ ] Discovery uses heredoc wrapper for bash compatibility 3. [ ] Compression calculation handles macOS stat syntax 4. [ ] All AskUserQuestion phases are present 5. [ ] TodoWrite template matches actual workflow
Batch Mode
1. [ ] --batch flag triggers batch workflow (phases 7-10) 2. [ ] --source skips Phase 7 (source selection) 3. [ ] --output-dir skips Phase 8 (output organization) 4. [ ] --skip-existing prevents re-conversion of existing files 5. [ ] Aggregate compression ratio calculated correctly 6. [ ] iTerm2 filename format documented
TodoWrite Task Templates
Single File Mode
1. [Preflight] Check asciinema CLI and convert command
2. [Preflight] Offer installation if missing
3. [Discovery] Find .cast files with metadata
4. [Selection] AskUserQuestion: file to convert
5. [Options] AskUserQuestion: conversion options (multi-select)
6. [Location] AskUserQuestion: output location
7. [Convert] Run asciinema convert -f txt
8. [Report] Display compression ratio and output path
9. [Index] Create timestamp index if requested
10. [Next] AskUserQuestion: next stepsBatch Mode (--batch flag)
1. [Preflight] Check asciinema CLI and convert command
2. [Preflight] Offer installation if missing
3. [Source] AskUserQuestion: source directory (skip if --source)
4. [Output] AskUserQuestion: output directory (skip if --output-dir)
5. [Batch] Execute batch conversion with progress
6. [Report] Display aggregate compression stats
7. [Next] AskUserQuestion: batch next stepsWorkflow Phases (Single File Mode)
All phases are MANDATORY. Do NOT skip any phase. AskUserQuestion MUST be used at each decision point.
Phase 0: Preflight Check
Purpose: Verify asciinema is installed and supports convert command.
/usr/bin/env bash << 'PREFLIGHT_EOF'
if command -v asciinema &>/dev/null; then
VERSION=$(asciinema --version | head -1)
echo "asciinema: $VERSION"
# Check if convert command exists (v2.4+)
if asciinema convert --help &>/dev/null 2>&1; then
echo "convert: available"
else
echo "convert: MISSING (update asciinema to v2.4+)"
fi
else
echo "asciinema: MISSING"
fi
PREFLIGHT_EOFIf asciinema is NOT installed or convert is missing, use AskUserQuestion:
Question: "asciinema CLI issue detected. How would you like to proceed?"
Header: "Setup"
Options:
- Label: "Install/upgrade asciinema (Recommended)"
Description: "Run: brew install asciinema (or upgrade if outdated)"
- Label: "Show manual instructions"
Description: "Display installation commands for all platforms"
- Label: "Cancel"
Description: "Exit without converting"---
Phase 1: File Discovery & Selection (MANDATORY)
Purpose: Discover .cast files and let user select which to convert.
Step 1.1: Discover .cast Files
/usr/bin/env bash << 'DISCOVER_EOF'
# Search for .cast files with metadata
for file in $(fd -e cast . --max-depth 5 2>/dev/null | head -10); do
SIZE=$(ls -lh "$file" 2>/dev/null | awk '{print $5}')
LINES=$(wc -l < "$file" 2>/dev/null | tr -d ' ')
DURATION=$(head -1 "$file" 2>/dev/null | jq -r '.duration // "unknown"' 2>/dev/null)
BASENAME=$(basename "$file")
echo "FILE:$file|SIZE:$SIZE|LINES:$LINES|DURATION:$DURATION|NAME:$BASENAME"
done
DISCOVER_EOFStep 1.2: Present File Selection (MANDATORY AskUserQuestion)
Use discovery results to populate options:
Question: "Which recording would you like to convert?"
Header: "Recording"
Options:
- Label: "{filename} ({size})"
Description: "{line_count} events, {duration}s duration"
- Label: "{filename2} ({size2})"
Description: "{line_count2} events, {duration2}s duration"
- Label: "Browse for file"
Description: "Search in a different directory"
- Label: "Enter path"
Description: "Provide a custom path to a .cast file"---
Phase 2: Output Options (MANDATORY)
Purpose: Let user configure conversion behavior.
Question: "Select conversion options:"
Header: "Options"
multiSelect: true
Options:
- Label: "Plain text output (Recommended)"
Description: "Convert to .txt with all ANSI codes stripped"
- Label: "Create timestamp index"
Description: "Generate [HH:MM:SS] indexed version for navigation"
- Label: "Split by idle time"
Description: "Create separate chunks at 30s+ pauses"
- Label: "Preserve terminal dimensions"
Description: "Add header with original terminal size"---
Phase 3: Output Location (MANDATORY)
Purpose: Let user choose where to save the output.
Question: "Where should the output be saved?"
Header: "Output"
Options:
- Label: "Same directory as source (Recommended)"
Description: "Save {filename}.txt next to {filename}.cast"
- Label: "Workspace tmp/"
Description: "Save to ${PWD}/tmp/"
- Label: "Custom path"
Description: "Specify a custom output location"---
Phase 4: Execute Conversion
Purpose: Run the conversion and report results.
Step 4.1: Run asciinema convert
/usr/bin/env bash << 'CONVERT_EOF'
INPUT_FILE="${1:?Input file required}"
OUTPUT_FILE="${2:?Output file required}"
echo "Converting: $INPUT_FILE"
echo "Output: $OUTPUT_FILE"
echo ""
# Run conversion
asciinema convert -f txt "$INPUT_FILE" "$OUTPUT_FILE"
if [[ $? -eq 0 && -f "$OUTPUT_FILE" ]]; then
echo "Conversion successful"
else
echo "ERROR: Conversion failed"
exit 1
fi
CONVERT_EOFStep 4.2: Report Compression
/usr/bin/env bash << 'REPORT_EOF'
INPUT_FILE="${1:?}"
OUTPUT_FILE="${2:?}"
# Get file sizes (macOS compatible)
INPUT_SIZE=$(stat -f%z "$INPUT_FILE" 2>/dev/null || stat -c%s "$INPUT_FILE" 2>/dev/null)
OUTPUT_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null)
# Calculate ratio
if [[ $OUTPUT_SIZE -gt 0 ]]; then
RATIO=$((INPUT_SIZE / OUTPUT_SIZE))
else
RATIO=0
fi
# Human-readable sizes
INPUT_HR=$(numfmt --to=iec "$INPUT_SIZE" 2>/dev/null || echo "$INPUT_SIZE bytes")
OUTPUT_HR=$(numfmt --to=iec "$OUTPUT_SIZE" 2>/dev/null || echo "$OUTPUT_SIZE bytes")
echo ""
echo "=== Conversion Complete ==="
echo "Input: $INPUT_HR"
echo "Output: $OUTPUT_HR"
echo "Compression: ${RATIO}:1"
echo "Output path: $OUTPUT_FILE"
REPORT_EOF---
Phase 5: Create Timestamp Index (if selected)
Purpose: Generate indexed version for navigation.
/usr/bin/env bash << 'INDEX_EOF'
INPUT_CAST="${1:?}"
OUTPUT_INDEX="${2:?}"
echo "Creating timestamp index..."
# Process .cast file to indexed format
(
echo "# Recording Index"
echo "# Format: [HH:MM:SS] content"
echo "#"
cumtime=0
tail -n +2 "$INPUT_CAST" | while IFS= read -r line; do
# Extract timestamp and content
ts=$(echo "$line" | jq -r '.[0]' 2>/dev/null)
type=$(echo "$line" | jq -r '.[1]' 2>/dev/null)
content=$(echo "$line" | jq -r '.[2]' 2>/dev/null)
if [[ "$type" == "o" && -n "$content" ]]; then
# Format timestamp as HH:MM:SS
hours=$((${ts%.*} / 3600))
mins=$(((${ts%.*} % 3600) / 60))
secs=$((${ts%.*} % 60))
timestamp=$(printf "%02d:%02d:%02d" "$hours" "$mins" "$secs")
# Clean and output (strip ANSI, limit length)
clean=$(echo "$content" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | tr -d '\r' | head -c 200)
[[ -n "$clean" ]] && echo "[$timestamp] $clean"
fi
done
) > "$OUTPUT_INDEX"
echo "Index created: $OUTPUT_INDEX"
wc -l "$OUTPUT_INDEX"
INDEX_EOF---
Phase 6: Next Steps (MANDATORY)
Purpose: Guide user to next action.
Question: "Conversion complete. What's next?"
Header: "Next"
Options:
- Label: "Analyze with /asciinema-tools:analyze"
Description: "Run keyword extraction on the converted file"
- Label: "Open in editor"
Description: "View the converted text file"
- Label: "Done"
Description: "Exit - no further action needed"#!/usr/bin/env bats
# Integration tests for asciinema-converter skill
# Run with: bats plugins/asciinema-tools/skills/asciinema-converter/tests/converter-integration.bats
FIXTURES_DIR="$BATS_TEST_DIRNAME/fixtures"
TMP_DIR="$BATS_TEST_DIRNAME/tmp"
setup() {
mkdir -p "$TMP_DIR"
mkdir -p "$FIXTURES_DIR"
# Create minimal test fixture if it doesn't exist
if [[ ! -f "$FIXTURES_DIR/simple.cast" ]]; then
cat > "$FIXTURES_DIR/simple.cast" << 'CAST_EOF'
{"version": 2, "width": 80, "height": 24, "timestamp": 1705600000, "duration": 5.0}
[0.0, "o", "Hello"]
[1.0, "o", " World"]
[2.0, "o", "\r\n"]
[3.0, "o", "$ exit"]
[4.0, "o", "\r\n"]
CAST_EOF
fi
# Create filename with spaces fixture
if [[ ! -f "$FIXTURES_DIR/with spaces.cast" ]]; then
cp "$FIXTURES_DIR/simple.cast" "$FIXTURES_DIR/with spaces.cast"
fi
}
teardown() {
rm -rf "$TMP_DIR"
}
# ============================================================================
# Preflight Tests
# ============================================================================
@test "asciinema CLI is installed" {
command -v asciinema
}
@test "asciinema convert command exists" {
run asciinema convert --help
[ "$status" -eq 0 ]
}
@test "asciinema version is 2.4+" {
version=$(asciinema --version | head -1 | grep -oE '[0-9]+\.[0-9]+')
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
# Need at least 2.4 for convert command
if [[ "$major" -lt 2 ]]; then
skip "asciinema major version too old: $version"
fi
if [[ "$major" -eq 2 && "$minor" -lt 4 ]]; then
skip "asciinema minor version too old: $version"
fi
}
# ============================================================================
# Single File Conversion Tests
# ============================================================================
@test "single file conversion works" {
run asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
[ "$status" -eq 0 ]
[ -f "$TMP_DIR/simple.txt" ]
}
@test "converted file contains expected content" {
asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
# Should contain "Hello World"
run grep -q "Hello" "$TMP_DIR/simple.txt"
[ "$status" -eq 0 ]
}
@test "converted file has no ANSI escape codes" {
asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
# Should NOT contain ANSI escape codes
run grep -P '\x1b\[' "$TMP_DIR/simple.txt"
[ "$status" -ne 0 ]
}
@test "conversion achieves compression" {
asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
input_size=$(stat -f%z "$FIXTURES_DIR/simple.cast" 2>/dev/null || stat -c%s "$FIXTURES_DIR/simple.cast")
output_size=$(stat -f%z "$TMP_DIR/simple.txt" 2>/dev/null || stat -c%s "$TMP_DIR/simple.txt")
# Output should be smaller than input
[ "$output_size" -lt "$input_size" ]
}
@test "handles filenames with spaces" {
run asciinema convert -f txt "$FIXTURES_DIR/with spaces.cast" "$TMP_DIR/with spaces.txt"
[ "$status" -eq 0 ]
[ -f "$TMP_DIR/with spaces.txt" ]
}
# ============================================================================
# Batch Conversion Tests
# ============================================================================
@test "batch creates output directory" {
# Setup: ensure output doesn't exist
rm -rf "$TMP_DIR/batch-output"
# Create batch output dir
mkdir -p "$TMP_DIR/batch-output"
[ -d "$TMP_DIR/batch-output" ]
}
@test "batch converts multiple files" {
mkdir -p "$TMP_DIR/batch-source"
mkdir -p "$TMP_DIR/batch-output"
# Create multiple test files
cp "$FIXTURES_DIR/simple.cast" "$TMP_DIR/batch-source/file1.cast"
cp "$FIXTURES_DIR/simple.cast" "$TMP_DIR/batch-source/file2.cast"
cp "$FIXTURES_DIR/simple.cast" "$TMP_DIR/batch-source/file3.cast"
# Convert each file
for cast_file in "$TMP_DIR/batch-source"/*.cast; do
basename=$(basename "$cast_file" .cast)
asciinema convert -f txt "$cast_file" "$TMP_DIR/batch-output/${basename}.txt"
done
# Verify all files were converted
[ -f "$TMP_DIR/batch-output/file1.txt" ]
[ -f "$TMP_DIR/batch-output/file2.txt" ]
[ -f "$TMP_DIR/batch-output/file3.txt" ]
}
@test "skip existing files logic works" {
mkdir -p "$TMP_DIR/skip-test"
# Pre-create output file
echo "existing content" > "$TMP_DIR/skip-test/existing.txt"
original_content=$(cat "$TMP_DIR/skip-test/existing.txt")
# Copy source
cp "$FIXTURES_DIR/simple.cast" "$TMP_DIR/skip-test/existing.cast"
# Skip logic simulation (don't convert if exists)
txt_file="$TMP_DIR/skip-test/existing.txt"
if [[ -f "$txt_file" ]]; then
skipped=true
else
asciinema convert -f txt "$TMP_DIR/skip-test/existing.cast" "$txt_file"
fi
# File should not have been modified
current_content=$(cat "$TMP_DIR/skip-test/existing.txt")
[ "$current_content" = "$original_content" ]
}
# ============================================================================
# Compression Ratio Tests
# ============================================================================
@test "compression ratio calculation works" {
asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
input_size=$(stat -f%z "$FIXTURES_DIR/simple.cast" 2>/dev/null || stat -c%s "$FIXTURES_DIR/simple.cast")
output_size=$(stat -f%z "$TMP_DIR/simple.txt" 2>/dev/null || stat -c%s "$TMP_DIR/simple.txt")
if [[ $output_size -gt 0 ]]; then
ratio=$((input_size / output_size))
else
ratio=0
fi
# Ratio should be positive
[ "$ratio" -ge 1 ]
}
# ============================================================================
# Error Handling Tests
# ============================================================================
@test "fails gracefully on missing input file" {
run asciinema convert -f txt "$TMP_DIR/nonexistent.cast" "$TMP_DIR/output.txt"
[ "$status" -ne 0 ]
}
@test "fails gracefully on invalid cast file" {
echo "not valid json" > "$TMP_DIR/invalid.cast"
run asciinema convert -f txt "$TMP_DIR/invalid.cast" "$TMP_DIR/output.txt"
[ "$status" -ne 0 ]
}
@test "handles empty directory gracefully" {
mkdir -p "$TMP_DIR/empty-source"
mkdir -p "$TMP_DIR/empty-output"
# Count should be zero
count=$(find "$TMP_DIR/empty-source" -maxdepth 1 -name "*.cast" -type f | wc -l | tr -d ' ')
[ "$count" -eq 0 ]
}
# ============================================================================
# Path Handling Tests
# ============================================================================
@test "handles absolute paths correctly" {
run asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/absolute.txt"
[ "$status" -eq 0 ]
[ -f "$TMP_DIR/absolute.txt" ]
}
@test "preserves basename in output" {
asciinema convert -f txt "$FIXTURES_DIR/simple.cast" "$TMP_DIR/simple.txt"
# Output filename should match input basename
[ -f "$TMP_DIR/simple.txt" ]
}
/**
* Unit tests for asciinema-converter skill utilities
*
* Run with: bun test plugins/asciinema-tools/skills/asciinema-converter/tests/
*/
import { describe, expect, it, beforeAll, afterAll } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync, statSync } from "fs";
import { join } from "path";
const FIXTURES_DIR = join(import.meta.dir, "fixtures");
const TMP_DIR = join(import.meta.dir, "tmp");
// Test fixture: minimal valid .cast file
const MINIMAL_CAST = `{"version": 2, "width": 80, "height": 24, "timestamp": 1705600000, "duration": 5.0}
[0.0, "o", "Hello"]
[1.0, "o", " World"]
[2.0, "o", "\\r\\n"]
[3.0, "o", "$ exit"]
[4.0, "o", "\\r\\n"]
`;
// iTerm2 filename format test cases
const ITERM2_FILENAMES = [
{
filename:
"20260118_232025.Claude Code.w0t1p1.70C05103-2F29-4B42-8067-BE475DB6126A.68721.4013739999.cast",
expected: {
timestamp: "20260118_232025",
profile: "Claude Code",
termid: "w0t1p1",
uuid: "70C05103-2F29-4B42-8067-BE475DB6126A",
pid: "68721",
autoLogId: "4013739999",
},
},
{
filename:
"20260119_120000.Default.w1t0p0.AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE.12345.9999999999.cast",
expected: {
timestamp: "20260119_120000",
profile: "Default",
termid: "w1t0p0",
uuid: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
pid: "12345",
autoLogId: "9999999999",
},
},
{
filename:
"20260101_000000.my.profile.name.w0t0p0.12345678-ABCD-1234-5678-ABCDEF012345.999.1.cast",
expected: {
timestamp: "20260101_000000",
profile: "my.profile.name",
termid: "w0t0p0",
uuid: "12345678-ABCD-1234-5678-ABCDEF012345",
pid: "999",
autoLogId: "1",
},
},
];
/**
* Parse iTerm2 auto-log filename (right-to-left parsing)
*/
function parseITerm2Filename(filename: string): Record<string, string> | null {
// Remove .cast extension
const base = filename.replace(/\.cast$/, "");
const parts = base.split(".");
// Need at least 6 parts: timestamp, profile (1+), termid, uuid, pid, autoLogId
if (parts.length < 6) return null;
// Parse from right
const autoLogId = parts.pop()!;
const pid = parts.pop()!;
// UUID has hyphens, find it
const uuidIndex = parts.findIndex((p) =>
/^[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}$/i.test(p)
);
if (uuidIndex === -1) return null;
const uuid = parts[uuidIndex];
const termid = parts[uuidIndex - 1];
// Everything before termid is timestamp.profile
const beforeTermid = parts.slice(0, uuidIndex - 1);
const timestamp = beforeTermid[0];
const profile = beforeTermid.slice(1).join(".");
return { timestamp, profile, termid, uuid, pid, autoLogId };
}
/**
* Calculate compression ratio
*/
function calculateCompressionRatio(
inputSize: number,
outputSize: number
): number {
if (outputSize <= 0) return 0;
return Math.floor(inputSize / outputSize);
}
/**
* Check if .txt file exists for given .cast file
*/
function txtExists(castPath: string, outputDir: string): boolean {
const basename = castPath.replace(/\.cast$/, "");
const txtPath = join(outputDir, `${basename}.txt`);
return existsSync(txtPath);
}
describe("iTerm2 Filename Parsing", () => {
for (const testCase of ITERM2_FILENAMES) {
it(`should parse ${testCase.filename}`, () => {
const result = parseITerm2Filename(testCase.filename);
expect(result).not.toBeNull();
expect(result!.timestamp).toBe(testCase.expected.timestamp);
expect(result!.profile).toBe(testCase.expected.profile);
expect(result!.termid).toBe(testCase.expected.termid);
expect(result!.uuid).toBe(testCase.expected.uuid);
expect(result!.pid).toBe(testCase.expected.pid);
expect(result!.autoLogId).toBe(testCase.expected.autoLogId);
});
}
it("should return null for invalid filename", () => {
expect(parseITerm2Filename("invalid.cast")).toBeNull();
expect(parseITerm2Filename("simple.cast")).toBeNull();
expect(parseITerm2Filename("no-uuid.here.cast")).toBeNull();
});
});
describe("Compression Ratio Calculation", () => {
it("should calculate correct ratio", () => {
expect(calculateCompressionRatio(1000, 10)).toBe(100);
expect(calculateCompressionRatio(950000, 1000)).toBe(950);
expect(calculateCompressionRatio(100, 100)).toBe(1);
});
it("should handle edge cases", () => {
expect(calculateCompressionRatio(1000, 0)).toBe(0);
expect(calculateCompressionRatio(0, 100)).toBe(0);
expect(calculateCompressionRatio(0, 0)).toBe(0);
});
it("should floor the result", () => {
expect(calculateCompressionRatio(100, 3)).toBe(33);
expect(calculateCompressionRatio(1000, 7)).toBe(142);
});
});
describe("Skip Existing Logic", () => {
beforeAll(() => {
// Create tmp directory
if (!existsSync(TMP_DIR)) {
mkdirSync(TMP_DIR, { recursive: true });
}
// Create a test .txt file
writeFileSync(join(TMP_DIR, "existing.txt"), "test content");
});
afterAll(() => {
// Cleanup
if (existsSync(TMP_DIR)) {
rmSync(TMP_DIR, { recursive: true });
}
});
it("should detect existing .txt file", () => {
expect(txtExists("existing.cast", TMP_DIR)).toBe(true);
});
it("should return false for missing .txt file", () => {
expect(txtExists("missing.cast", TMP_DIR)).toBe(false);
});
});
describe("Fixture Validation", () => {
it("should have valid fixtures directory", () => {
expect(existsSync(FIXTURES_DIR)).toBe(true);
});
it("minimal.cast fixture should be valid NDJSON", () => {
const lines = MINIMAL_CAST.trim().split("\n");
// First line is header
const header = JSON.parse(lines[0]);
expect(header.version).toBe(2);
expect(header.width).toBe(80);
expect(header.height).toBe(24);
// Remaining lines are events
for (let i = 1; i < lines.length; i++) {
const event = JSON.parse(lines[i]);
expect(Array.isArray(event)).toBe(true);
expect(event.length).toBe(3);
expect(typeof event[0]).toBe("number"); // timestamp
expect(typeof event[1]).toBe("string"); // event type
expect(typeof event[2]).toBe("string"); // data
}
});
});
describe("Path Handling", () => {
it("should handle paths with spaces", () => {
const pathWithSpaces = "/path/to/my file.cast";
const basename = pathWithSpaces.split("/").pop()!.replace(/\.cast$/, "");
expect(basename).toBe("my file");
});
it("should handle paths with special characters", () => {
const specialPath = "/path/to/file-with_special.chars!.cast";
const basename = specialPath.split("/").pop()!.replace(/\.cast$/, "");
expect(basename).toBe("file-with_special.chars!");
});
it("should extract basename correctly", () => {
const paths = [
{ input: "/a/b/c.cast", expected: "c" },
{ input: "simple.cast", expected: "simple" },
{ input: "/path/to/file.cast", expected: "file" },
];
for (const { input, expected } of paths) {
const basename = input.split("/").pop()!.replace(/\.cast$/, "");
expect(basename).toBe(expected);
}
});
});
describe("Size Calculations", () => {
beforeAll(() => {
if (!existsSync(TMP_DIR)) {
mkdirSync(TMP_DIR, { recursive: true });
}
// Create test files with known sizes
writeFileSync(join(TMP_DIR, "small.txt"), "x".repeat(100));
writeFileSync(join(TMP_DIR, "medium.txt"), "x".repeat(10000));
});
afterAll(() => {
if (existsSync(TMP_DIR)) {
rmSync(TMP_DIR, { recursive: true });
}
});
it("should get correct file sizes", () => {
const smallSize = statSync(join(TMP_DIR, "small.txt")).size;
const mediumSize = statSync(join(TMP_DIR, "medium.txt")).size;
expect(smallSize).toBe(100);
expect(mediumSize).toBe(10000);
});
it("should calculate ratio from file sizes", () => {
const inputSize = 100000;
const outputSize = 100;
const ratio = calculateCompressionRatio(inputSize, outputSize);
expect(ratio).toBe(1000);
});
});
{"version": 2, "width": 80, "height": 24, "timestamp": 1705600000, "duration": 5.0}
[0.0, "o", "Hello"]
[1.0, "o", " World"]
[2.0, "o", "\r\n"]
[3.0, "o", "$ exit"]
[4.0, "o", "\r\n"]
{"version": 2, "width": 80, "height": 24, "timestamp": 1705600000, "duration": 5.0}
[0.0, "o", "Hello"]
[1.0, "o", " World"]
[2.0, "o", "\r\n"]
[3.0, "o", "$ exit"]
[4.0, "o", "\r\n"]