|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Generate a synthetic .cast (asciinema v3) file showing a GitMem session start. |
| 4 | + * Content is anonymized. Then render it to GIF with `agg`. |
| 5 | + */ |
| 6 | + |
| 7 | +import { writeFileSync } from 'fs'; |
| 8 | +import { join, dirname } from 'path'; |
| 9 | +import { fileURLToPath } from 'url'; |
| 10 | +import { execSync } from 'child_process'; |
| 11 | + |
| 12 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 13 | + |
| 14 | +// ANSI escape helpers |
| 15 | +const ESC = '\x1b'; |
| 16 | +const RESET = `${ESC}[0m`; |
| 17 | +const BOLD = `${ESC}[1m`; |
| 18 | +const DIM = `${ESC}[2m`; |
| 19 | +const NORMAL = `${ESC}[22m`; |
| 20 | +const RED = `${ESC}[31m`; // palette 1: #DA7756 (Claude orange) |
| 21 | +const GREEN = `${ESC}[32m`; // palette 2: #00a600 |
| 22 | +const YELLOW = `${ESC}[33m`; // palette 3: #999900 |
| 23 | +const CYAN = `${ESC}[36m`; // palette 6: #00a6b3 |
| 24 | +const WHITE = `${ESC}[37m`; // palette 7: #bfbfbf |
| 25 | +const BG_BLK = `${ESC}[40m`; // black bg (for logo blocks) |
| 26 | +const BG_DEF = `${ESC}[49m`; // default bg |
| 27 | +const FG_DEF = `${ESC}[39m`; // default fg |
| 28 | +const BR_RED = `${ESC}[91m`; // bright red: #e65535 |
| 29 | +const BR_GREEN = `${ESC}[92m`; // bright green: #00d900 |
| 30 | +const BR_YELLOW= `${ESC}[93m`; // bright yellow: #e6e600 |
| 31 | +const BR_CYAN = `${ESC}[96m`; // bright cyan: #00e6e6 |
| 32 | +const BR_WHITE = `${ESC}[97m`; // bright white: #e6e6e6 |
| 33 | +const BR_MAG = `${ESC}[95m`; // bright magenta: #e600e6 |
| 34 | + |
| 35 | +// Build the cast file events: [delta_seconds, "o", "data"] |
| 36 | +const events = []; |
| 37 | +let t = 0; |
| 38 | + |
| 39 | +function out(delay, text) { |
| 40 | + events.push([delay, 'o', text]); |
| 41 | + t += delay; |
| 42 | +} |
| 43 | + |
| 44 | +function outln(delay, text) { |
| 45 | + out(delay, text + '\r\n'); |
| 46 | +} |
| 47 | + |
| 48 | +// --- Scene 1: Shell prompt + type "claude" --- |
| 49 | +out(0.5, '$ '); |
| 50 | +// Type "claude" character by character |
| 51 | +for (const ch of 'claude') { |
| 52 | + out(0.08 + Math.random() * 0.06, ch); |
| 53 | +} |
| 54 | +out(0.3, '\r\n'); |
| 55 | + |
| 56 | +// --- Scene 2: Claude Code header (actual layout from recordings) --- |
| 57 | +out(1.2, ''); |
| 58 | + |
| 59 | +// Line 1: logo + Claude Code |
| 60 | +outln(0.0, ` ${RED}▐${BG_BLK}▛███▜${BG_DEF}▌${FG_DEF} ${BOLD}Claude Code${NORMAL} ${WHITE}v2.1.32${FG_DEF}`); |
| 61 | + |
| 62 | +// Line 2: logo + model |
| 63 | +outln(0.0, `${RED}▝▜${BG_BLK}█████${BG_DEF}▛▘${FG_DEF} ${WHITE}Sonnet 4.5 · API key${FG_DEF}`); |
| 64 | + |
| 65 | +// Line 3: logo feet + path |
| 66 | +outln(0.0, `${RED} ▘▘ ▝▝${FG_DEF} ${WHITE}~/my-project${FG_DEF}`); |
| 67 | + |
| 68 | +outln(0.0, ''); |
| 69 | + |
| 70 | +// Horizontal rule |
| 71 | +outln(0.0, `${DIM}${WHITE}${'─'.repeat(76)}${FG_DEF}${NORMAL}`); |
| 72 | + |
| 73 | +// Prompt hint |
| 74 | +outln(0.0, `${DIM}❯ ${NORMAL}Try "how does <filepath> work?"${RESET}`); |
| 75 | + |
| 76 | +// Horizontal rule |
| 77 | +outln(0.0, `${DIM}${WHITE}${'─'.repeat(76)}${FG_DEF}${NORMAL}`); |
| 78 | + |
| 79 | +// Status bar |
| 80 | +outln(0.0, ` ${BR_MAG}⏵⏵ bypass permissions on${FG_DEF}`); |
| 81 | + |
| 82 | +// --- Scene 3: User types "lets start" --- |
| 83 | +out(1.5, ''); |
| 84 | + |
| 85 | +// Replace prompt area |
| 86 | +outln(0.0, ''); |
| 87 | +out(0.0, `${BR_WHITE}❯ ${RESET}`); |
| 88 | +for (const ch of 'lets start') { |
| 89 | + out(0.06 + Math.random() * 0.05, `${BR_WHITE}${ch}${RESET}`); |
| 90 | +} |
| 91 | +out(0.4, '\r\n'); |
| 92 | + |
| 93 | +// --- Scene 4: Frosting spinner --- |
| 94 | +outln(0.3, `${RED}· Frosting…${FG_DEF}`); |
| 95 | +outln(0.8, ` ${BR_YELLOW}SessionStart hook firing...${FG_DEF}`); |
| 96 | + |
| 97 | +// --- Scene 5: GitMem session start --- |
| 98 | +out(1.0, '\r\n'); |
| 99 | +outln(0.0, ` ${BR_GREEN}${BOLD}gitmem ── session started${NORMAL}${FG_DEF}`); |
| 100 | +outln(0.1, ` ${DIM}a3f8b291 · cli · my-project${NORMAL}`); |
| 101 | +outln(0.1, ''); |
| 102 | + |
| 103 | +// --- Scene 6: Threads --- |
| 104 | +out(0.8, ''); |
| 105 | +outln(0.0, ` ${BR_CYAN}${BOLD}Threads (5)${NORMAL}${FG_DEF}`); |
| 106 | +outln(0.15, ` Fix auth token refresh — stale JWT after 24h`); |
| 107 | +outln(0.1, ` Migrate user table to use UUID primary keys`); |
| 108 | +outln(0.1, ` Add rate limiting to /api/search endpoint`); |
| 109 | +outln(0.1, ` Investigate Docker build cache misses on CI`); |
| 110 | +outln(0.1, ` ${DIM}+1 more${NORMAL}`); |
| 111 | +outln(0.0, ''); |
| 112 | + |
| 113 | +// --- Scene 7: Decisions --- |
| 114 | +out(0.8, ''); |
| 115 | +outln(0.0, ` ${BR_YELLOW}${BOLD}Decisions (3)${NORMAL}${FG_DEF}`); |
| 116 | +outln(0.15, ` Use Zod for API validation, not Joi ${DIM}· Feb 18${NORMAL}`); |
| 117 | +outln(0.1, ` Keep Postgres — no migration to Mongo ${DIM}· Feb 17${NORMAL}`); |
| 118 | +outln(0.1, ` Rate limit: token bucket, not sliding ${DIM}· Feb 16${NORMAL}`); |
| 119 | +outln(0.0, ''); |
| 120 | + |
| 121 | +// --- Scene 8: Summary --- |
| 122 | +out(0.6, ''); |
| 123 | +outln(0.0, ` ${DIM}${'─'.repeat(50)}${NORMAL}`); |
| 124 | +outln(0.2, ` ${BR_GREEN}3 scars recalled · 5 threads open · ready${FG_DEF}`); |
| 125 | +outln(0.0, ''); |
| 126 | + |
| 127 | +// --- Scene 9: Agent response with scars --- |
| 128 | +out(1.2, ''); |
| 129 | +outln(0.0, ` ${BR_WHITE}Loaded institutional memory. 3 scars surfaced:${FG_DEF}`); |
| 130 | +outln(0.3, ` [${BR_RED}HIGH${FG_DEF}] JWT refresh tokens expire silently after rotation`); |
| 131 | +outln(0.2, ` [${BR_YELLOW}MED${FG_DEF}] Docker layer caching requires consistent COPY order`); |
| 132 | +outln(0.2, ` [${DIM}LOW${NORMAL}] Rate limit headers must include X-RateLimit-Reset`); |
| 133 | +outln(0.0, ''); |
| 134 | +outln(0.5, ` ${BR_GREEN}Acknowledging scars before proceeding...${FG_DEF}`); |
| 135 | + |
| 136 | +// Hold at end |
| 137 | +out(3.0, ''); |
| 138 | + |
| 139 | +// --- Write .cast file --- |
| 140 | +const header = { |
| 141 | + version: 3, |
| 142 | + term: { |
| 143 | + cols: 80, |
| 144 | + rows: 36, |
| 145 | + type: 'xterm', |
| 146 | + theme: { |
| 147 | + fg: '#a1a1a1', |
| 148 | + bg: '#30302d', |
| 149 | + palette: '#000000:#DA7756:#00a600:#999900:#0000b3:#b300b3:#00a6b3:#bfbfbf:#262624:#e65535:#00d900:#e6e600:#0000ff:#e600e6:#00e6e6:#e6e6e6' |
| 150 | + } |
| 151 | + }, |
| 152 | + timestamp: Math.floor(Date.now() / 1000), |
| 153 | + idle_time_limit: 5.0, |
| 154 | +}; |
| 155 | + |
| 156 | +const castPath = join(__dirname, 'images', 'session-start-ceremony.cast'); |
| 157 | +const gifPath = join(__dirname, 'images', 'session-start-ceremony.gif'); |
| 158 | + |
| 159 | +let castContent = JSON.stringify(header) + '\n'; |
| 160 | +for (const [delay, type, data] of events) { |
| 161 | + castContent += JSON.stringify([delay, type, data]) + '\n'; |
| 162 | +} |
| 163 | + |
| 164 | +writeFileSync(castPath, castContent); |
| 165 | +console.log(`Cast file: ${castPath} (${events.length} events, ${Math.round(t)}s total)`); |
| 166 | + |
| 167 | +// --- Render with agg --- |
| 168 | +try { |
| 169 | + execSync(`/tmp/agg ${castPath} ${gifPath} --speed 1 --font-size 14 --line-height 1.4`, { |
| 170 | + stdio: 'inherit', |
| 171 | + }); |
| 172 | + console.log(`GIF: ${gifPath}`); |
| 173 | +} catch (e) { |
| 174 | + console.error('agg render failed:', e.message); |
| 175 | + console.log('Cast file written — render manually with: agg', castPath, gifPath); |
| 176 | +} |
0 commit comments