ccwrap 0.1.0 → 0.3.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/dist/cli.js CHANGED
@@ -50,7 +50,10 @@ function resolveDateRange(options) {
50
50
  const sinceStr = since.toLocaleDateString("en-US", fmtOpts);
51
51
  const untilYear = until.getFullYear();
52
52
  const sinceYear = since.getFullYear();
53
- const untilStr = until.toLocaleDateString("en-US", { ...fmtOpts, year: sinceYear !== untilYear ? "numeric" : undefined });
53
+ const untilStr = until.toLocaleDateString("en-US", {
54
+ ...fmtOpts,
55
+ year: sinceYear !== untilYear ? "numeric" : undefined,
56
+ });
54
57
  const periodLabel = `${sinceStr} – ${untilStr}, ${untilYear}`;
55
58
  return { range: { since, until }, periodLabel };
56
59
  }
@@ -78,42 +81,34 @@ const banner = `
78
81
  ╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
79
82
  ╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
80
83
  `;
81
- const darkBanner = `
82
- ██████╗ ██████╗██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ███████╗██████╗
83
- ██╔════╝██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
84
- ██║ ██║ ██║ █╗ ██║██████╔╝███████║██████╔╝██████╔╝█████╗ ██║ ██║
85
- ██║ ██║ ██║███╗██║██╔══██╗██╔══██║██╔═══╝ ██╔═══╝ ██╔══╝ ██║ ██║
86
- ╚██████╗╚██████╗╚███╔███╔╝██║ ██║██║ ██║██║ ██║ ███████╗██████╔╝
87
- ╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═════╝
88
- `;
89
84
  program
90
85
  .name("ccwrap")
91
- .description("Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video")
92
- .version("0.1.0")
93
- .option("-o, --output <path>", "Output video file path", "ccwrapped.mp4")
86
+ .description("ccwrap - Your AI coding stats, meme-ified into a shareable video")
87
+ .version("0.3.0")
88
+ .option("-o, --output <path>", "Output video file path", "ccwrap.mp4")
94
89
  .option("-m, --mode <mode>", "Commentary mode: sassy or dark", "sassy")
95
90
  .option("-p, --period <period>", "Time period: week, month, quarter, all (default: month)")
96
91
  .option("--since <date>", "Start date (YYYY-MM-DD), mutually exclusive with --period")
97
92
  .option("--until <date>", "End date (YYYY-MM-DD), requires --since")
93
+ .option("--skip-weekends", "Exclude weekend activity from stats")
98
94
  .option("--json", "Output stats as JSON instead of video")
99
95
  .option("--stats-only", "Print stats to console without rendering video")
100
96
  .action(async (options) => {
101
97
  const mode = options.mode === "dark" ? "dark" : "sassy";
102
98
  const isDark = mode === "dark";
103
- console.log(isDark ? chalk.red(darkBanner) : chalk.hex("#7c3aed")(banner));
104
- console.log(chalk.gray(isDark
105
- ? " Your AI usage. The planet remembers.\n"
106
- : " Your AI-assisted coding, recapped.\n"));
99
+ console.log(isDark ? chalk.red(banner) : chalk.hex("#7c3aed")(banner));
100
+ console.log(chalk.gray(isDark ? " Your AI usage. The planet remembers.\n" : " Your AI-assisted coding, recapped.\n"));
107
101
  // Resolve date range
108
102
  const { range, periodLabel } = resolveDateRange(options);
109
103
  // Step 1: Load and compute stats
110
- const periodHint = range
111
- ? chalk.gray(` (${periodLabel})`)
112
- : "";
113
- const spinner = ora({ text: `Scanning Claude Code usage data...${periodHint}`, color: isDark ? "red" : "magenta" }).start();
104
+ const periodHint = range ? chalk.gray(` (${periodLabel})`) : "";
105
+ const spinner = ora({
106
+ text: `Scanning Claude Code usage data...${periodHint}`,
107
+ color: isDark ? "red" : "magenta",
108
+ }).start();
114
109
  let stats;
115
110
  try {
116
- stats = await loadAndComputeStats(range, periodLabel);
111
+ stats = await loadAndComputeStats(range, periodLabel, options.skipWeekends);
117
112
  spinner.succeed(chalk.green("Usage data loaded!"));
118
113
  }
119
114
  catch (err) {
@@ -150,16 +145,9 @@ program
150
145
  text: isDark ? "Generating dark commentary..." : "Generating sassy commentary...",
151
146
  color: isDark ? "red" : "magenta",
152
147
  }).start();
153
- let commentary;
154
- try {
155
- commentary = await generateCommentary(stats, mode);
156
- commentarySpinner.succeed(chalk.green("Commentary generated!"));
157
- }
158
- catch (err) {
159
- commentarySpinner.fail(chalk.yellow(`Commentary generation failed, using fallbacks`));
160
- // Fallbacks are handled inside generateCommentary
161
- commentary = await generateCommentary(stats, mode);
162
- }
148
+ // generateCommentary handles its own errors and returns fallbacks
149
+ const commentary = await generateCommentary(stats, mode);
150
+ commentarySpinner.succeed(chalk.green("Commentary generated!"));
163
151
  // Step 3: Render video
164
152
  const renderSpinner = ora({ text: "Rendering your Wrapped video...", color: isDark ? "red" : "magenta" }).start();
165
153
  try {
@@ -1,13 +1,3 @@
1
- import type { WrappedStats } from "./types";
2
- export interface Commentary {
3
- tokensLine: string;
4
- costLine: string;
5
- modelLine: string;
6
- busiestDayLine: string;
7
- sessionLine: string;
8
- peakHoursLine: string;
9
- archetypeLine: string;
10
- summaryLine: string;
11
- }
12
- export type Mode = "sassy" | "dark";
1
+ import type { WrappedStats, Commentary, Mode } from "./types.js";
2
+ export declare function buildPrompt(stats: WrappedStats, mode: Mode): string;
13
3
  export declare function generateCommentary(stats: WrappedStats, mode: Mode): Promise<Commentary>;
@@ -1,5 +1,5 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
- function buildPrompt(stats, mode) {
2
+ export function buildPrompt(stats, mode) {
3
3
  const modeDescription = mode === "dark"
4
4
  ? `DARK MODE: Your commentary should be darkly humorous with environmental/existential undertones.
5
5
  Think: water usage, carbon footprint, e-waste, heat death of the universe, humanity's dependence on AI.
@@ -19,7 +19,7 @@ function buildPrompt(stats, mode) {
19
19
  const periodContext = stats.periodLabel.startsWith("Your ")
20
20
  ? `This recap covers ${stats.periodLabel.toLowerCase().replace("your ", "the user's last ")} of usage.`
21
21
  : `This recap covers the period ${stats.periodLabel}.`;
22
- return `You are writing short, punchy one-liner commentary for a "Claude Code Wrapped" recap video — a shareable stats recap of someone's AI coding assistant usage. Each line appears as a caption on an animated slide.
22
+ return `You are writing short, punchy one-liner commentary for a "ccwrap" recap video — a shareable stats recap of someone's AI coding assistant usage. Each line appears as a caption on an animated slide.
23
23
 
24
24
  ${periodContext}
25
25
 
@@ -73,6 +73,7 @@ export async function generateCommentary(stats, mode) {
73
73
  console.log(" No ANTHROPIC_API_KEY found, using pre-written commentary.");
74
74
  return mode === "dark" ? darkFallbacks : sassyFallbacks;
75
75
  }
76
+ /* v8 ignore start -- requires live API key */
76
77
  try {
77
78
  const client = new Anthropic({ apiKey });
78
79
  const message = await client.messages.create({
@@ -84,8 +85,14 @@ export async function generateCommentary(stats, mode) {
84
85
  const parsed = JSON.parse(text);
85
86
  // Validate all keys exist
86
87
  const keys = [
87
- "tokensLine", "costLine", "modelLine", "busiestDayLine",
88
- "sessionLine", "peakHoursLine", "archetypeLine", "summaryLine",
88
+ "tokensLine",
89
+ "costLine",
90
+ "modelLine",
91
+ "busiestDayLine",
92
+ "sessionLine",
93
+ "peakHoursLine",
94
+ "archetypeLine",
95
+ "summaryLine",
89
96
  ];
90
97
  const fallback = mode === "dark" ? darkFallbacks : sassyFallbacks;
91
98
  for (const key of keys) {
@@ -99,4 +106,5 @@ export async function generateCommentary(stats, mode) {
99
106
  console.log(` AI commentary failed (${err.message}), using pre-written commentary.`);
100
107
  return mode === "dark" ? darkFallbacks : sassyFallbacks;
101
108
  }
109
+ /* v8 ignore stop */
102
110
  }
@@ -1,2 +1,30 @@
1
- import type { WrappedStats, DateRange } from "./types.js";
2
- export declare function loadAndComputeStats(range?: DateRange, periodLabel?: string): Promise<WrappedStats>;
1
+ import type { RawJSONLEntry, WrappedStats, DateRange } from "./types.js";
2
+ declare const PRICING: Record<string, {
3
+ input: number;
4
+ output: number;
5
+ cacheWrite: number;
6
+ cacheRead: number;
7
+ }>;
8
+ export declare function estimateCost(model: string, inputTokens: number, outputTokens: number, cacheCreationTokens: number, cacheReadTokens: number, cache?: Map<string, (typeof PRICING)[string]>): number;
9
+ export declare function extractProject(filePath: string): string;
10
+ export declare function loadAndComputeStats(range?: DateRange, periodLabel?: string, skipWeekends?: boolean): Promise<WrappedStats>;
11
+ export declare function computeStatsFromEntries(rawEntries: (RawJSONLEntry & {
12
+ _file: string;
13
+ })[], opts?: {
14
+ range?: DateRange;
15
+ periodLabel?: string;
16
+ skipWeekends?: boolean;
17
+ }): WrappedStats;
18
+ export declare function computeArchetype(params: {
19
+ nightOwlScore: number;
20
+ weekendWarriorScore: number;
21
+ avgSessionMinutes: number;
22
+ totalSessions: number;
23
+ streakDays: number;
24
+ totalTokens: number;
25
+ favoriteModel: string;
26
+ }): {
27
+ archetype: string;
28
+ archetypeDescription: string;
29
+ };
30
+ export {};
@@ -5,29 +5,35 @@ import { join } from "path";
5
5
  // Pricing per million tokens (from Anthropic pricing page)
6
6
  // Each model has: input, output, cache_write (25% premium), cache_read (90% discount)
7
7
  const PRICING = {
8
- "claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
9
- "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
10
- "claude-sonnet-4-5-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
11
- "claude-sonnet-4-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
12
- "claude-haiku-4-5-20251001": { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
13
- "claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
14
- "claude-3-5-haiku-20241022": { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
8
+ "claude-opus-4-6": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
9
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
10
+ "claude-sonnet-4-5-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
11
+ "claude-sonnet-4-20250514": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
12
+ "claude-haiku-4-5-20251001": { input: 0.8, output: 4, cacheWrite: 1.0, cacheRead: 0.08 },
13
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
14
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheWrite: 1.0, cacheRead: 0.08 },
15
15
  };
16
- function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
17
- const pricing = Object.entries(PRICING).find(([key]) => model.includes(key))?.[1]
18
- ?? (model.includes("opus") ? PRICING["claude-opus-4-6"]
19
- : model.includes("haiku") ? PRICING["claude-haiku-4-5-20251001"]
20
- : PRICING["claude-sonnet-4-6"]);
21
- return (inputTokens * pricing.input +
16
+ export function estimateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, cache) {
17
+ let pricing = cache?.get(model);
18
+ if (!pricing) {
19
+ pricing =
20
+ Object.entries(PRICING).find(([key]) => model.includes(key))?.[1] ??
21
+ (model.includes("opus")
22
+ ? PRICING["claude-opus-4-6"]
23
+ : model.includes("haiku")
24
+ ? PRICING["claude-haiku-4-5-20251001"]
25
+ : PRICING["claude-sonnet-4-6"]);
26
+ cache?.set(model, pricing);
27
+ }
28
+ return ((inputTokens * pricing.input +
22
29
  outputTokens * pricing.output +
23
30
  cacheCreationTokens * pricing.cacheWrite +
24
- cacheReadTokens * pricing.cacheRead) / 1_000_000;
31
+ cacheReadTokens * pricing.cacheRead) /
32
+ 1_000_000);
25
33
  }
26
34
  async function findJSONLFiles() {
27
35
  const claudeDir = join(homedir(), ".claude", "projects");
28
- const configDir = process.env.CLAUDE_CONFIG_DIR
29
- ? join(process.env.CLAUDE_CONFIG_DIR, "projects")
30
- : null;
36
+ const configDir = process.env.CLAUDE_CONFIG_DIR ? join(process.env.CLAUDE_CONFIG_DIR, "projects") : null;
31
37
  const dirs = [claudeDir, configDir].filter(Boolean);
32
38
  const allFiles = [];
33
39
  for (const dir of dirs) {
@@ -59,7 +65,7 @@ async function parseJSONLFile(filePath) {
59
65
  }
60
66
  return entries;
61
67
  }
62
- function extractProject(filePath) {
68
+ export function extractProject(filePath) {
63
69
  // Path format: ~/.claude/projects/{encoded-project-path}/{sessionId}.jsonl
64
70
  const parts = filePath.split("/projects/");
65
71
  if (parts.length < 2)
@@ -68,11 +74,9 @@ function extractProject(filePath) {
68
74
  const segments = afterProjects.split("/");
69
75
  const projectEncoded = segments[0] ?? "unknown";
70
76
  // Decode: -home-user-myproject -> /home/user/myproject
71
- return projectEncoded
72
- .replace(/^-/, "/")
73
- .replace(/-/g, "/");
77
+ return projectEncoded.replace(/^-/, "/").replace(/-/g, "/");
74
78
  }
75
- export async function loadAndComputeStats(range, periodLabel) {
79
+ export async function loadAndComputeStats(range, periodLabel, skipWeekends) {
76
80
  const files = await findJSONLFiles();
77
81
  if (files.length === 0) {
78
82
  throw new Error("No Claude Code usage data found. Make sure you have used Claude Code before.");
@@ -88,40 +92,94 @@ export async function loadAndComputeStats(range, periodLabel) {
88
92
  if (allEntries.length === 0) {
89
93
  throw new Error("No usage data entries found in Claude Code logs.");
90
94
  }
91
- // Sort by timestamp
92
- allEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
93
- // Filter by date range if provided
95
+ return computeStatsFromEntries(allEntries, { range, periodLabel, skipWeekends });
96
+ }
97
+ export function computeStatsFromEntries(rawEntries, opts = {}) {
98
+ const { range, periodLabel, skipWeekends } = opts;
99
+ let entries = rawEntries.map((e) => {
100
+ const _date = new Date(e.timestamp);
101
+ return { ...e, _ts: _date.getTime(), _date };
102
+ });
103
+ entries.sort((a, b) => a._ts - b._ts);
104
+ // Filter by date range
94
105
  if (range) {
95
106
  const sinceMs = range.since.getTime();
96
107
  const untilMs = range.until.getTime();
97
- const before = allEntries.length;
98
- const filtered = allEntries.filter(e => {
99
- const ts = new Date(e.timestamp).getTime();
100
- return ts >= sinceMs && ts <= untilMs;
101
- });
102
- allEntries.length = 0;
103
- allEntries.push(...filtered);
104
- if (allEntries.length === 0) {
108
+ const before = entries.length;
109
+ entries = entries.filter((e) => e._ts >= sinceMs && e._ts <= untilMs);
110
+ if (entries.length === 0) {
105
111
  throw new Error(`No usage data found in the selected time period (${before} entries exist outside this range).`);
106
112
  }
107
113
  }
108
- // Build session map
114
+ // Filter out weekends
115
+ if (skipWeekends) {
116
+ const before = entries.length;
117
+ entries = entries.filter((e) => {
118
+ const day = e._date.getDay();
119
+ return day !== 0 && day !== 6;
120
+ });
121
+ if (entries.length === 0) {
122
+ throw new Error(`No weekday usage data found (${before} weekend entries were excluded).`);
123
+ }
124
+ }
125
+ // Single pass: sessions, hourly, daily, model usage, weekend count
109
126
  const sessionMap = new Map();
110
- for (const entry of allEntries) {
111
- const sid = entry.sessionId;
112
- if (!sessionMap.has(sid))
113
- sessionMap.set(sid, []);
114
- sessionMap.get(sid).push(entry);
127
+ const hourly = Array.from({ length: 24 }, (_, i) => ({ hour: i, messageCount: 0, tokenCount: 0 }));
128
+ const dailyMap = new Map();
129
+ const dailySessionSets = new Map();
130
+ const modelMap = new Map();
131
+ let weekendCount = 0;
132
+ for (const entry of entries) {
133
+ const hour = entry._date.getHours();
134
+ const dateStr = entry.timestamp.split("T")[0] ?? entry._date.toISOString().split("T")[0];
135
+ const model = entry.message?.model ?? "unknown";
136
+ const u = entry.message?.usage;
137
+ const inOut = u ? (u.input_tokens ?? 0) + (u.output_tokens ?? 0) : 0;
138
+ // Session grouping
139
+ if (!sessionMap.has(entry.sessionId))
140
+ sessionMap.set(entry.sessionId, []);
141
+ sessionMap.get(entry.sessionId).push(entry);
142
+ // Hourly
143
+ hourly[hour].messageCount++;
144
+ hourly[hour].tokenCount += inOut;
145
+ // Daily
146
+ if (!dailyMap.has(dateStr)) {
147
+ dailyMap.set(dateStr, { date: dateStr, messageCount: 0, tokenCount: 0, costUSD: 0, sessions: 0 });
148
+ dailySessionSets.set(dateStr, new Set());
149
+ }
150
+ dailyMap.get(dateStr).messageCount++;
151
+ dailyMap.get(dateStr).tokenCount += inOut;
152
+ dailySessionSets.get(dateStr).add(entry.sessionId);
153
+ // Model usage
154
+ if (!modelMap.has(model))
155
+ modelMap.set(model, { tokens: 0, messages: 0, cost: 0 });
156
+ const md = modelMap.get(model);
157
+ md.messages++;
158
+ md.tokens += inOut;
159
+ // Weekend count
160
+ const dow = entry._date.getDay();
161
+ if (dow === 0 || dow === 6)
162
+ weekendCount++;
115
163
  }
164
+ // Finalize daily sessions
165
+ for (const [date, sessionSet] of dailySessionSets) {
166
+ dailyMap.get(date).sessions = sessionSet.size;
167
+ }
168
+ const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
116
169
  // Compute session stats
170
+ const costCache = new Map();
117
171
  const sessions = [];
118
- for (const [sessionId, entries] of sessionMap) {
119
- const timestamps = entries.map(e => new Date(e.timestamp));
120
- const startTime = new Date(Math.min(...timestamps.map(t => t.getTime())));
121
- const endTime = new Date(Math.max(...timestamps.map(t => t.getTime())));
122
- const models = [...new Set(entries.map(e => e.message?.model).filter(Boolean))];
172
+ for (const [sessionId, sessionEntries] of sessionMap) {
173
+ let minTs = Infinity, maxTs = -Infinity;
174
+ const models = new Set();
123
175
  let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0, totalCost = 0;
124
- for (const entry of entries) {
176
+ for (const entry of sessionEntries) {
177
+ if (entry._ts < minTs)
178
+ minTs = entry._ts;
179
+ if (entry._ts > maxTs)
180
+ maxTs = entry._ts;
181
+ if (entry.message?.model)
182
+ models.add(entry.message.model);
125
183
  const u = entry.message?.usage;
126
184
  if (u) {
127
185
  totalInput += u.input_tokens ?? 0;
@@ -133,73 +191,26 @@ export async function loadAndComputeStats(range, periodLabel) {
133
191
  totalCost += entry.costUSD;
134
192
  }
135
193
  else if (entry.message?.model && u) {
136
- totalCost += estimateCost(entry.message.model, u.input_tokens ?? 0, u.output_tokens ?? 0, u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0);
194
+ totalCost += estimateCost(entry.message.model, u.input_tokens ?? 0, u.output_tokens ?? 0, u.cache_creation_input_tokens ?? 0, u.cache_read_input_tokens ?? 0, costCache);
137
195
  }
138
196
  }
139
197
  sessions.push({
140
198
  sessionId,
141
- project: extractProject(entries[0]._file),
142
- startTime,
143
- endTime,
144
- durationMinutes: (endTime.getTime() - startTime.getTime()) / 60_000,
145
- messageCount: entries.length,
199
+ project: extractProject(sessionEntries[0]._file),
200
+ startTime: new Date(minTs),
201
+ endTime: new Date(maxTs),
202
+ durationMinutes: (maxTs - minTs) / 60_000,
203
+ messageCount: sessionEntries.length,
146
204
  totalInputTokens: totalInput,
147
205
  totalOutputTokens: totalOutput,
148
206
  totalCacheCreationTokens: totalCacheCreate,
149
207
  totalCacheReadTokens: totalCacheRead,
150
208
  totalTokens: totalInput + totalOutput + totalCacheCreate + totalCacheRead,
151
- models,
209
+ models: [...models],
152
210
  costUSD: totalCost,
153
211
  });
154
212
  }
155
- // Hourly activity
156
- const hourly = Array.from({ length: 24 }, (_, i) => ({
157
- hour: i,
158
- messageCount: 0,
159
- tokenCount: 0,
160
- }));
161
- for (const entry of allEntries) {
162
- const hour = new Date(entry.timestamp).getHours();
163
- hourly[hour].messageCount++;
164
- const u = entry.message?.usage;
165
- if (u) {
166
- hourly[hour].tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
167
- }
168
- }
169
- // Daily activity
170
- const dailyMap = new Map();
171
- const dailySessionSets = new Map();
172
- for (const entry of allEntries) {
173
- const date = new Date(entry.timestamp).toISOString().split("T")[0];
174
- if (!dailyMap.has(date)) {
175
- dailyMap.set(date, { date, messageCount: 0, tokenCount: 0, costUSD: 0, sessions: 0 });
176
- dailySessionSets.set(date, new Set());
177
- }
178
- const day = dailyMap.get(date);
179
- day.messageCount++;
180
- const u = entry.message?.usage;
181
- if (u) {
182
- day.tokenCount += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
183
- }
184
- dailySessionSets.get(date).add(entry.sessionId);
185
- }
186
- for (const [date, sessionSet] of dailySessionSets) {
187
- dailyMap.get(date).sessions = sessionSet.size;
188
- }
189
- const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
190
- // Model usage
191
- const modelMap = new Map();
192
- for (const entry of allEntries) {
193
- const model = entry.message?.model ?? "unknown";
194
- if (!modelMap.has(model))
195
- modelMap.set(model, { tokens: 0, messages: 0, cost: 0 });
196
- const m = modelMap.get(model);
197
- m.messages++;
198
- const u = entry.message?.usage;
199
- if (u) {
200
- m.tokens += (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
201
- }
202
- }
213
+ // Model usage percentages
203
214
  const totalTokensAll = [...modelMap.values()].reduce((sum, m) => sum + m.tokens, 0);
204
215
  const modelUsage = [...modelMap.entries()]
205
216
  .map(([model, data]) => ({
@@ -215,16 +226,16 @@ export async function loadAndComputeStats(range, periodLabel) {
215
226
  const totalInputTokens = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0);
216
227
  const totalOutputTokens = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0);
217
228
  const totalCost = sessions.reduce((s, sess) => s + sess.costUSD, 0);
218
- const totalMessages = allEntries.length;
219
- // Dates
220
- const firstActivity = new Date(allEntries[0].timestamp);
221
- const lastActivity = new Date(allEntries[allEntries.length - 1].timestamp);
222
- const uniqueDays = new Set(allEntries.map(e => new Date(e.timestamp).toISOString().split("T")[0]));
229
+ const totalMessages = entries.length;
230
+ // Dates — reuse dailyMap keys instead of reparsing
231
+ const firstActivity = entries[0]._date;
232
+ const lastActivity = entries[entries.length - 1]._date;
233
+ const uniqueDays = new Set(dailyMap.keys());
223
234
  const totalDaysActive = uniqueDays.size;
224
235
  // Peak stats
225
- const busiestDay = dailyActivity.reduce((best, d) => d.tokenCount > best.tokenCount ? d : best);
226
- const busiestHour = hourly.reduce((best, h) => h.tokenCount > best.tokenCount ? h : best).hour;
227
- const longestSession = sessions.reduce((best, s) => s.durationMinutes > best.durationMinutes ? s : best);
236
+ const busiestDay = dailyActivity.reduce((best, d) => (d.tokenCount > best.tokenCount ? d : best));
237
+ const busiestHour = hourly.reduce((best, h) => (h.tokenCount > best.tokenCount ? h : best)).hour;
238
+ const longestSession = sessions.reduce((best, s) => (s.durationMinutes > best.durationMinutes ? s : best));
228
239
  // Project frequency
229
240
  const projectCounts = new Map();
230
241
  for (const sess of sessions) {
@@ -247,15 +258,9 @@ export async function loadAndComputeStats(range, periodLabel) {
247
258
  }
248
259
  }
249
260
  // Scores
250
- const nightTokens = hourly.filter(h => h.hour >= 22 || h.hour < 5).reduce((s, h) => s + h.tokenCount, 0);
261
+ const nightTokens = hourly.filter((h) => h.hour >= 22 || h.hour < 5).reduce((s, h) => s + h.tokenCount, 0);
251
262
  const nightOwlScore = totalTokensAll > 0 ? Math.round((nightTokens / totalTokensAll) * 100) : 0;
252
- const weekendEntries = allEntries.filter(e => {
253
- const day = new Date(e.timestamp).getDay();
254
- return day === 0 || day === 6;
255
- });
256
- const weekendWarriorScore = allEntries.length > 0
257
- ? Math.round((weekendEntries.length / allEntries.length) * 100)
258
- : 0;
263
+ const weekendWarriorScore = entries.length > 0 ? Math.round((weekendCount / entries.length) * 100) : 0;
259
264
  const favoriteModel = modelUsage[0]?.model ?? "unknown";
260
265
  const totalSessionHours = sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / 60;
261
266
  const avgSessionMinutes = sessions.length > 0 ? sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / sessions.length : 0;
@@ -300,15 +305,15 @@ export async function loadAndComputeStats(range, periodLabel) {
300
305
  marathonEquivalent: Math.round((totalSessionHours / 4.5) * 100) / 100,
301
306
  // Environmental estimates (rough but directionally correct)
302
307
  // ~0.5 liters of water per 1M tokens for data center cooling
303
- waterLiters: Math.round(totalTokens / 1_000_000 * 0.5 * 100) / 100,
308
+ waterLiters: Math.round((totalTokens / 1_000_000) * 0.5 * 100) / 100,
304
309
  // ~0.3g CO2 per 1K tokens (inference energy + grid mix)
305
- co2Grams: Math.round(totalTokens / 1_000 * 0.3),
310
+ co2Grams: Math.round((totalTokens / 1_000) * 0.3),
306
311
  // ~0.001 kWh per 1K tokens
307
- kwhUsed: Math.round(totalTokens / 1_000 * 0.001 * 100) / 100,
312
+ kwhUsed: Math.round((totalTokens / 1_000) * 0.001 * 100) / 100,
308
313
  };
309
314
  }
310
- function computeArchetype(params) {
311
- const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel } = params;
315
+ export function computeArchetype(params) {
316
+ const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel, } = params;
312
317
  if (nightOwlScore > 50) {
313
318
  return {
314
319
  archetype: "The Night Owl Demon",
@@ -92,10 +92,11 @@ export interface WrappedStats {
92
92
  co2Grams: number;
93
93
  kwhUsed: number;
94
94
  }
95
+ export type Mode = "sassy" | "dark";
95
96
  export interface VideoProps {
96
97
  stats: WrappedStats;
97
98
  commentary: Commentary;
98
- mode: "sassy" | "dark";
99
+ mode: Mode;
99
100
  }
100
101
  export interface Commentary {
101
102
  tokensLine: string;
package/dist/render.js CHANGED
@@ -11,7 +11,7 @@ function findChromeBinary() {
11
11
  "/usr/bin/chromium",
12
12
  "/usr/bin/google-chrome",
13
13
  ];
14
- return candidates.find(p => existsSync(p));
14
+ return candidates.find((p) => existsSync(p));
15
15
  }
16
16
  export async function renderVideo(stats, commentary, mode, outputPath) {
17
17
  const resolvedOutput = resolve(process.cwd(), outputPath);
@@ -26,10 +26,7 @@ export async function renderVideo(stats, commentary, mode, outputPath) {
26
26
  ...config,
27
27
  module: {
28
28
  ...config.module,
29
- rules: [
30
- ...(config.module?.rules ?? []),
31
- { test: /\.m?js$/, resolve: { fullySpecified: false } },
32
- ],
29
+ rules: [...(config.module?.rules ?? []), { test: /\.m?js$/, resolve: { fullySpecified: false } }],
33
30
  },
34
31
  }),
35
32
  });
@@ -30,9 +30,12 @@ const SlideWithTransition = ({ children, index }) => {
30
30
  extrapolateRight: "clamp",
31
31
  });
32
32
  const fadeOut = index < TOTAL_SLIDES - 1
33
- ? interpolate(frame, [FADE_IN + READABLE, SLIDE_CONTENT], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
33
+ ? interpolate(frame, [FADE_IN + READABLE, SLIDE_CONTENT], [1, 0], {
34
+ extrapolateLeft: "clamp",
35
+ extrapolateRight: "clamp",
36
+ })
34
37
  : 1; // last slide stays visible
35
- return (_jsx(AbsoluteFill, { style: { opacity: fadeIn * fadeOut }, children: children }));
38
+ return _jsx(AbsoluteFill, { style: { opacity: fadeIn * fadeOut }, children: children });
36
39
  };
37
40
  export const CCWrappedComposition = ({ stats, commentary, mode }) => {
38
41
  const slides = [
@@ -25,7 +25,7 @@ const defaultProps = {
25
25
  totalDaysActive: 89,
26
26
  firstActivity: new Date("2025-06-01"),
27
27
  lastActivity: new Date("2026-03-15"),
28
- busiestDay: { date: "2026-02-14", messageCount: 247, tokenCount: 2_400_000, costUSD: 18.50, sessions: 8 },
28
+ busiestDay: { date: "2026-02-14", messageCount: 247, tokenCount: 2_400_000, costUSD: 18.5, sessions: 8 },
29
29
  busiestHour: 22,
30
30
  longestSession: {
31
31
  sessionId: "demo",
@@ -40,7 +40,7 @@ const defaultProps = {
40
40
  totalCacheReadTokens: 300_000,
41
41
  totalTokens: 3_200_000,
42
42
  models: ["claude-sonnet-4-6"],
43
- costUSD: 18.50,
43
+ costUSD: 18.5,
44
44
  },
45
45
  mostActiveProject: "/home/user/megaproject",
46
46
  hourlyActivity: Array.from({ length: 24 }, (_, i) => ({
@@ -1,15 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { useCurrentFrame, interpolate } from "remotion";
4
- import { colors } from "../styles";
4
+ import { getColors } from "../styles";
5
5
  export const ParticleField = ({ count = 40, seed = 42 }) => {
6
6
  const frame = useCurrentFrame();
7
7
  const particles = useMemo(() => {
8
8
  const rng = (s) => {
9
- let x = Math.sin(s) * 10000;
9
+ const x = Math.sin(s) * 10000;
10
10
  return x - Math.floor(x);
11
11
  };
12
- const particleColors = [colors.primary, colors.secondary, colors.accent, colors.gold, colors.pink];
12
+ const c = getColors("sassy");
13
+ const particleColors = [c.primary, c.secondary, c.accent, c.gold, c.pink];
13
14
  return Array.from({ length: count }, (_, i) => ({
14
15
  x: rng(seed + i * 7) * 100,
15
16
  y: rng(seed + i * 13) * 100,
@@ -20,7 +21,7 @@ export const ParticleField = ({ count = 40, seed = 42 }) => {
20
21
  }));
21
22
  }, [count, seed]);
22
23
  return (_jsx("div", { style: { position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }, children: particles.map((p, i) => {
23
- const y = (p.y + frame * p.speed * 0.3) % 110 - 5;
24
+ const y = ((p.y + frame * p.speed * 0.3) % 110) - 5;
24
25
  const twinkle = interpolate(Math.sin(frame * 0.1 + i), [-1, 1], [p.opacity * 0.5, p.opacity]);
25
26
  return (_jsx("div", { style: {
26
27
  position: "absolute",
@@ -15,20 +15,70 @@ function getArchetypeColor(archetype, mode) {
15
15
  if (archetype.includes("Streak"))
16
16
  return c.gold;
17
17
  if (archetype.includes("Whale"))
18
- return mode === "dark" ? c.accent : "#10b981";
18
+ return mode === "dark" ? c.accent : c.green;
19
19
  if (archetype.includes("Connoisseur"))
20
20
  return c.gold;
21
21
  if (archetype.includes("Serial"))
22
22
  return c.pink;
23
23
  return c.secondary;
24
24
  }
25
- export const ArchetypeSlide = ({ stats, commentary, mode }) => {
25
+ export const ArchetypeSlide = ({ stats, commentary, mode, }) => {
26
26
  const frame = useCurrentFrame();
27
27
  const { fps } = useVideoConfig();
28
28
  const c = getColors(mode);
29
29
  const color = getArchetypeColor(stats.archetype, mode);
30
- const revealScale = spring({ frame: Math.max(0, frame - 20), fps, from: 0, to: 1, config: { damping: 8, stiffness: 80 } });
30
+ const revealScale = spring({
31
+ frame: Math.max(0, frame - 20),
32
+ fps,
33
+ from: 0,
34
+ to: 1,
35
+ config: { damping: 8, stiffness: 80 },
36
+ });
31
37
  const glowIntensity = interpolate(Math.sin(frame * 0.08), [-1, 1], [0.5, 1]);
32
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 40, seed: 333 }), _jsx(GlowOrb, { color: color, size: 350, x: 50, y: 45, speed: 0.3 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 15, y: 20 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 85, y: 80 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color, zIndex: 1, letterSpacing: "0.3em" }, children: mode === "dark" ? "Your Classification" : "You Are..." }) }), _jsxs("div", { style: { transform: `scale(${revealScale})`, opacity: revealScale, zIndex: 1, marginTop: 24, textAlign: "center" }, children: [_jsx("div", { style: { width: 90, height: 3, background: `linear-gradient(90deg, transparent, ${color}, transparent)`, margin: "0 auto 18px", opacity: glowIntensity } }), _jsx("div", { style: { fontSize: 68, fontWeight: 900, lineHeight: 1.1, ...glowText(color), background: `linear-gradient(135deg, ${color}, ${c.text}, ${color})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", filter: `brightness(${0.8 + glowIntensity * 0.4})` }, children: stats.archetype }), _jsx("div", { style: { width: 90, height: 3, background: `linear-gradient(90deg, transparent, ${color}, transparent)`, margin: "18px auto 0", opacity: glowIntensity } })] }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { fontSize: 18, color: c.textMuted, maxWidth: 580, textAlign: "center", lineHeight: 1.5, marginTop: 28, zIndex: 1, padding: "8px 18px", borderRadius: 10, background: "rgba(0,0,0,0.35)" }, children: commentary }) }), _jsx(FadeIn, { delay: 55, style: { zIndex: 1, marginTop: 28 }, children: _jsxs("div", { style: { display: "flex", gap: 14 }, children: [_jsx(Badge, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Badge, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted }), _jsx(Badge, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) })] }));
38
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 40, seed: 333 }), _jsx(GlowOrb, { color: color, size: 350, x: 50, y: 45, speed: 0.3 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 15, y: 20 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 85, y: 80 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color, zIndex: 1, letterSpacing: "0.3em" }, children: mode === "dark" ? "Your Classification" : "You Are..." }) }), _jsxs("div", { style: {
39
+ transform: `scale(${revealScale})`,
40
+ opacity: revealScale,
41
+ zIndex: 1,
42
+ marginTop: 24,
43
+ textAlign: "center",
44
+ }, children: [_jsx("div", { style: {
45
+ width: 90,
46
+ height: 3,
47
+ background: `linear-gradient(90deg, transparent, ${color}, transparent)`,
48
+ margin: "0 auto 18px",
49
+ opacity: glowIntensity,
50
+ } }), _jsx("div", { style: {
51
+ fontSize: 68,
52
+ fontWeight: 900,
53
+ lineHeight: 1.1,
54
+ ...glowText(color),
55
+ background: `linear-gradient(135deg, ${color}, ${c.text}, ${color})`,
56
+ WebkitBackgroundClip: "text",
57
+ WebkitTextFillColor: "transparent",
58
+ filter: `brightness(${0.8 + glowIntensity * 0.4})`,
59
+ }, children: stats.archetype }), _jsx("div", { style: {
60
+ width: 90,
61
+ height: 3,
62
+ background: `linear-gradient(90deg, transparent, ${color}, transparent)`,
63
+ margin: "18px auto 0",
64
+ opacity: glowIntensity,
65
+ } })] }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: {
66
+ fontSize: 18,
67
+ color: c.textMuted,
68
+ maxWidth: 580,
69
+ textAlign: "center",
70
+ lineHeight: 1.5,
71
+ marginTop: 28,
72
+ zIndex: 1,
73
+ padding: "8px 18px",
74
+ borderRadius: 10,
75
+ background: "rgba(0,0,0,0.35)",
76
+ }, children: commentary }) }), _jsx(FadeIn, { delay: 55, style: { zIndex: 1, marginTop: 28 }, children: _jsxs("div", { style: { display: "flex", gap: 14 }, children: [_jsx(Badge, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Badge, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted }), _jsx(Badge, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) })] }));
33
77
  };
34
- const Badge = ({ label: l, value, color, textColor }) => (_jsxs("div", { style: { background: `${color}20`, border: `1px solid ${color}50`, borderRadius: 12, padding: "10px 20px", textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 22, fontWeight: 700, color }, children: value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2, textTransform: "uppercase" }, children: l })] }));
78
+ const Badge = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: {
79
+ background: `${color}20`,
80
+ border: `1px solid ${color}50`,
81
+ borderRadius: 12,
82
+ padding: "10px 20px",
83
+ textAlign: "center",
84
+ }, children: [_jsx("div", { style: { fontSize: 22, fontWeight: 700, color }, children: value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2, textTransform: "uppercase" }, children: l })] }));
@@ -9,15 +9,24 @@ function formatDate(dateStr) {
9
9
  const d = new Date(dateStr + "T00:00:00");
10
10
  return d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" });
11
11
  }
12
- export const BusiestDaySlide = ({ stats, commentary, mode }) => {
12
+ export const BusiestDaySlide = ({ stats, commentary, mode, }) => {
13
13
  const frame = useCurrentFrame();
14
14
  const c = getColors(mode);
15
15
  const day = stats.busiestDay;
16
16
  const recentDays = stats.dailyActivity.slice(-14);
17
- const maxTokens = Math.max(...recentDays.map(d => d.tokenCount), 1);
17
+ const maxTokens = Math.max(...recentDays.map((d) => d.tokenCount), 1);
18
18
  return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(45, c), color: c.text }, children: [_jsx(ParticleField, { count: 30, seed: 55 }), _jsx(GlowOrb, { color: c.accent, size: 250, x: 50, y: 30 }), _jsx(GlowOrb, { color: c.pink, size: 180, x: 25, y: 70 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.accent, zIndex: 1 }, children: mode === "dark" ? "Peak Destruction Day" : "Your Biggest Day" }) }), _jsx(FadeIn, { delay: 12, children: _jsx("div", { style: { fontSize: 44, fontWeight: 900, ...glowText(c.accent), color: c.text, zIndex: 1, marginTop: 16 }, children: formatDate(day.date) }) }), _jsx(FadeIn, { delay: 25, children: _jsxs("div", { style: { display: "flex", gap: 48, marginTop: 24, zIndex: 1 }, children: [_jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.tokenCount, style: { fontSize: 40, fontWeight: 800, color: c.accent }, startFrame: 25, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "TOKENS" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.messageCount, style: { fontSize: 40, fontWeight: 800, color: c.pink }, startFrame: 28, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "MESSAGES" })] }), _jsxs("div", { style: { textAlign: "center" }, children: [_jsx(AnimatedNumber, { value: day.sessions, style: { fontSize: 40, fontWeight: 800, color: c.secondary }, startFrame: 31, duration: 25 }), _jsx("div", { style: { fontSize: 13, color: c.textMuted, marginTop: 4 }, children: "SESSIONS" })] })] }) }), _jsx(FadeIn, { delay: 45, style: { width: "88%", marginTop: 28, zIndex: 1 }, children: _jsx("div", { style: { display: "flex", alignItems: "flex-end", gap: 4, height: 100 }, children: recentDays.map((d, i) => {
19
- const barHeight = interpolate(frame, [48 + i, 68 + i], [0, (d.tokenCount / maxTokens) * 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
19
+ const barHeight = interpolate(frame, [48 + i, 68 + i], [0, (d.tokenCount / maxTokens) * 100], {
20
+ extrapolateLeft: "clamp",
21
+ extrapolateRight: "clamp",
22
+ });
20
23
  const isBusiest = d.date === day.date;
21
- return (_jsx("div", { style: { flex: 1, height: barHeight, borderRadius: 3, background: isBusiest ? `linear-gradient(180deg, ${c.accent}, ${c.pink})` : `${c.primary}90`, boxShadow: isBusiest ? `0 0 8px ${c.accent}80` : undefined } }, d.date));
24
+ return (_jsx("div", { style: {
25
+ flex: 1,
26
+ height: barHeight,
27
+ borderRadius: 3,
28
+ background: isBusiest ? `linear-gradient(180deg, ${c.accent}, ${c.pink})` : `${c.primary}90`,
29
+ boxShadow: isBusiest ? `0 0 8px ${c.accent}80` : undefined,
30
+ } }, d.date));
22
31
  }) }) }), _jsx(FadeIn, { delay: 60, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
23
32
  };
@@ -4,9 +4,24 @@ import { FadeIn } from "../components/FadeIn";
4
4
  import { AnimatedNumber } from "../components/AnimatedNumber";
5
5
  import { ParticleField } from "../components/ParticleField";
6
6
  import { GlowOrb } from "../components/GlowOrb";
7
- export const CostSlide = ({ stats, commentary, mode }) => {
7
+ export const CostSlide = ({ stats, commentary, mode, }) => {
8
8
  const c = getColors(mode);
9
- const costColor = mode === "dark" ? c.accent : "#10b981";
10
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(320, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 42 }), _jsx(GlowOrb, { color: costColor, size: 280, x: 50, y: 35 }), _jsx(GlowOrb, { color: c.primary, size: 160, x: 80, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: costColor, zIndex: 1 }, children: mode === "dark" ? "The Price of Progress" : "Estimated Cost" }) }), _jsx(AnimatedNumber, { value: stats.totalCost, decimals: 2, prefix: "$", style: { ...bigNumber, ...glowText(costColor), color: costColor, display: "block", textAlign: "center", zIndex: 1, marginTop: 20 }, startFrame: 10, duration: 35 }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { display: "flex", gap: 50, marginTop: 36, zIndex: 1 }, children: mode === "dark" ? (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\uD83D\uDCA7", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, label: "Water" }), _jsx(Metric, { icon: "\uD83C\uDFED", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, label: "CO\u2082" }), _jsx(Metric, { icon: "\u26A1", value: `${stats.kwhUsed.toLocaleString(undefined, { maximumFractionDigits: 1 })}`, label: "kWh" })] })) : (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\u2615", value: stats.coffeeEquivalent.toLocaleString(), label: "Coffees" }), _jsx(Metric, { icon: "\uD83C\uDFC3", value: stats.marathonEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "Marathons" }), _jsx(Metric, { icon: "\uD83D\uDCD6", value: stats.warAndPeaceEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "War & Peaces" })] })) }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
9
+ const costColor = mode === "dark" ? c.accent : c.green;
10
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(320, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 42 }), _jsx(GlowOrb, { color: costColor, size: 280, x: 50, y: 35 }), _jsx(GlowOrb, { color: c.primary, size: 160, x: 80, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: costColor, zIndex: 1 }, children: mode === "dark" ? "The Price of Progress" : "Estimated Cost" }) }), _jsx(AnimatedNumber, { value: stats.totalCost, decimals: 2, prefix: "$", style: {
11
+ ...bigNumber,
12
+ ...glowText(costColor),
13
+ color: costColor,
14
+ display: "block",
15
+ textAlign: "center",
16
+ zIndex: 1,
17
+ marginTop: 20,
18
+ }, startFrame: 10, duration: 35 }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { display: "flex", gap: 50, marginTop: 36, zIndex: 1 }, children: mode === "dark" ? (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\uD83D\uDCA7", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, label: "Water" }), _jsx(Metric, { icon: "\uD83C\uDFED", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, label: "CO\u2082" }), _jsx(Metric, { icon: "\u26A1", value: `${stats.kwhUsed.toLocaleString(undefined, { maximumFractionDigits: 1 })}`, label: "kWh" })] })) : (_jsxs(_Fragment, { children: [_jsx(Metric, { icon: "\u2615", value: stats.coffeeEquivalent.toLocaleString(), label: "Coffees" }), _jsx(Metric, { icon: "\uD83C\uDFC3", value: stats.marathonEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "Marathons" }), _jsx(Metric, { icon: "\uD83D\uDCD6", value: stats.warAndPeaceEquivalent.toLocaleString(undefined, { maximumFractionDigits: 1 }), label: "War & Peaces" })] })) }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
11
19
  };
12
- const Metric = ({ icon, value, label: l }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 36 }, children: icon }), _jsx("div", { style: { fontSize: 30, fontWeight: 800, color: "#f1f5f9", marginTop: 6 }, children: value }), _jsx("div", { style: { fontSize: 13, color: "inherit", opacity: 0.7, marginTop: 3, textTransform: "uppercase", letterSpacing: "0.1em" }, children: l })] }));
20
+ const Metric = ({ icon, value, label: l }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 36 }, children: icon }), _jsx("div", { style: { fontSize: 30, fontWeight: 800, color: "#f1f5f9", marginTop: 6 }, children: value }), _jsx("div", { style: {
21
+ fontSize: 13,
22
+ color: "inherit",
23
+ opacity: 0.7,
24
+ marginTop: 3,
25
+ textTransform: "uppercase",
26
+ letterSpacing: "0.1em",
27
+ }, children: l })] }));
@@ -13,7 +13,48 @@ export const IntroSlide = ({ mode, periodLabel }) => {
13
13
  const taglineScale = spring({ frame: Math.max(0, frame - 35), fps, from: 0.5, to: 1, config: { damping: 8 } });
14
14
  const taglineOpacity = interpolate(frame, [35, 50], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
15
15
  const rotation = interpolate(frame, [0, 150], [0, 360], { extrapolateRight: "extend" });
16
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: { position: "absolute", width: 180, height: 180, borderRadius: "50%", border: `2px solid ${c.primary}30`, transform: `scale(${logoScale}) rotate(${rotation}deg)` } }), _jsx("div", { style: { position: "absolute", width: 150, height: 150, borderRadius: "50%", border: `1px solid ${c.secondary}20`, transform: `scale(${logoScale}) rotate(-${rotation * 0.7}deg)` } }), _jsx("div", { style: { transform: `scale(${logoScale})`, fontSize: 72, fontWeight: 900, background: `linear-gradient(135deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", zIndex: 1 }, children: "</>" }), _jsxs("div", { style: { opacity: titleOpacity, fontSize: 48, fontWeight: 800, marginTop: 24, letterSpacing: "-0.02em", zIndex: 1 }, children: [_jsx("span", { style: { color: c.text }, children: "Claude Code " }), _jsx("span", { style: { background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }, children: "Wrapped" })] }), _jsx(FadeIn, { delay: 35, children: _jsx("div", { style: { opacity: taglineOpacity, transform: `scale(${taglineScale})`, fontSize: 22, fontWeight: 500, color: c.textMuted, marginTop: 20, zIndex: 1 }, children: periodLabel.startsWith("Your ")
16
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 40 }), _jsx(GlowOrb, { color: c.primary, size: 250, x: 15, y: 25 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 80, y: 70 }), _jsx(GlowOrb, { color: c.accent, size: 150, x: 50, y: 15, speed: 0.3 }), _jsx("div", { style: {
17
+ position: "absolute",
18
+ width: 180,
19
+ height: 180,
20
+ borderRadius: "50%",
21
+ border: `2px solid ${c.primary}30`,
22
+ transform: `scale(${logoScale}) rotate(${rotation}deg)`,
23
+ } }), _jsx("div", { style: {
24
+ position: "absolute",
25
+ width: 150,
26
+ height: 150,
27
+ borderRadius: "50%",
28
+ border: `1px solid ${c.secondary}20`,
29
+ transform: `scale(${logoScale}) rotate(-${rotation * 0.7}deg)`,
30
+ } }), _jsx("div", { style: {
31
+ transform: `scale(${logoScale})`,
32
+ fontSize: 72,
33
+ fontWeight: 900,
34
+ background: `linear-gradient(135deg, ${c.primary}, ${c.secondary})`,
35
+ WebkitBackgroundClip: "text",
36
+ WebkitTextFillColor: "transparent",
37
+ zIndex: 1,
38
+ }, children: "</>" }), _jsx("div", { style: {
39
+ opacity: titleOpacity,
40
+ fontSize: 48,
41
+ fontWeight: 800,
42
+ marginTop: 24,
43
+ letterSpacing: "-0.02em",
44
+ zIndex: 1,
45
+ }, children: _jsx("span", { style: {
46
+ background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`,
47
+ WebkitBackgroundClip: "text",
48
+ WebkitTextFillColor: "transparent",
49
+ }, children: "ccwrap" }) }), _jsx(FadeIn, { delay: 35, children: _jsx("div", { style: {
50
+ opacity: taglineOpacity,
51
+ transform: `scale(${taglineScale})`,
52
+ fontSize: 22,
53
+ fontWeight: 500,
54
+ color: c.textMuted,
55
+ marginTop: 20,
56
+ zIndex: 1,
57
+ }, children: periodLabel.startsWith("Your ")
17
58
  ? mode === "dark"
18
59
  ? `${periodLabel} in AI. The planet remembers.`
19
60
  : `${periodLabel} in AI-assisted coding`
@@ -31,15 +31,34 @@ function getModelColor(model, mode) {
31
31
  return c.secondary;
32
32
  return c.accent;
33
33
  }
34
- export const ModelSlide = ({ stats, commentary, mode }) => {
34
+ export const ModelSlide = ({ stats, commentary, mode, }) => {
35
35
  const frame = useCurrentFrame();
36
36
  const { fps } = useVideoConfig();
37
37
  const c = getColors(mode);
38
38
  const favoriteColor = getModelColor(stats.favoriteModel, mode);
39
39
  const revealScale = spring({ frame: Math.max(0, frame - 15), fps, from: 0.3, to: 1, config: { damping: 10 } });
40
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(260, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 77 }), _jsx(GlowOrb, { color: favoriteColor, size: 300, x: 50, y: 40 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: favoriteColor, zIndex: 1 }, children: mode === "dark" ? "Your Weapon of Choice" : "Your #1 Model" }) }), _jsx(FadeIn, { delay: 15, children: _jsx("div", { style: { fontSize: 90, fontWeight: 900, ...glowText(favoriteColor), background: `linear-gradient(135deg, ${favoriteColor}, ${c.text})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", transform: `scale(${revealScale})`, zIndex: 1, marginTop: 12 }, children: getModelDisplayName(stats.favoriteModel) }) }), _jsx(FadeIn, { delay: 30, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) }), _jsx(FadeIn, { delay: 40, style: { width: "85%", marginTop: 28, zIndex: 1 }, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: stats.modelUsage.slice(0, 4).map((m, i) => {
41
- const barWidth = interpolate(frame, [45 + i * 5, 65 + i * 5], [0, m.percentage], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
40
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(260, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 77 }), _jsx(GlowOrb, { color: favoriteColor, size: 300, x: 50, y: 40 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: favoriteColor, zIndex: 1 }, children: mode === "dark" ? "Your Weapon of Choice" : "Your #1 Model" }) }), _jsx(FadeIn, { delay: 15, children: _jsx("div", { style: {
41
+ fontSize: 90,
42
+ fontWeight: 900,
43
+ ...glowText(favoriteColor),
44
+ background: `linear-gradient(135deg, ${favoriteColor}, ${c.text})`,
45
+ WebkitBackgroundClip: "text",
46
+ WebkitTextFillColor: "transparent",
47
+ transform: `scale(${revealScale})`,
48
+ zIndex: 1,
49
+ marginTop: 12,
50
+ }, children: getModelDisplayName(stats.favoriteModel) }) }), _jsx(FadeIn, { delay: 30, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) }), _jsx(FadeIn, { delay: 40, style: { width: "85%", marginTop: 28, zIndex: 1 }, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: stats.modelUsage.slice(0, 4).map((m, i) => {
51
+ const barWidth = interpolate(frame, [45 + i * 5, 65 + i * 5], [0, m.percentage], {
52
+ extrapolateLeft: "clamp",
53
+ extrapolateRight: "clamp",
54
+ });
42
55
  const modelColor = getModelColor(m.model, mode);
43
- return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("div", { style: { width: 100, fontSize: 14, color: c.textMuted, textAlign: "right", fontWeight: 500 }, children: getModelDisplayName(m.model) }), _jsx("div", { style: { flex: 1, height: 24, borderRadius: 12, background: `${c.text}20`, overflow: "hidden" }, children: _jsx("div", { style: { width: `${barWidth}%`, height: "100%", borderRadius: 12, background: `linear-gradient(90deg, ${modelColor}, ${modelColor}cc)`, boxShadow: `0 0 8px ${modelColor}60` } }) }), _jsxs("div", { style: { width: 44, fontSize: 14, color: c.textMuted, fontWeight: 600 }, children: [m.percentage.toFixed(0), "%"] })] }, m.model));
56
+ return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [_jsx("div", { style: { width: 100, fontSize: 14, color: c.textMuted, textAlign: "right", fontWeight: 500 }, children: getModelDisplayName(m.model) }), _jsx("div", { style: { flex: 1, height: 24, borderRadius: 12, background: `${c.text}20`, overflow: "hidden" }, children: _jsx("div", { style: {
57
+ width: `${barWidth}%`,
58
+ height: "100%",
59
+ borderRadius: 12,
60
+ background: `linear-gradient(90deg, ${modelColor}, ${modelColor}cc)`,
61
+ boxShadow: `0 0 8px ${modelColor}60`,
62
+ } }) }), _jsxs("div", { style: { width: 44, fontSize: 14, color: c.textMuted, fontWeight: 600 }, children: [m.percentage.toFixed(0), "%"] })] }, m.model));
44
63
  }) }) })] }));
45
64
  };
@@ -35,14 +35,24 @@ function getTimeTitle(hour, mode) {
35
35
  return "Night Coder";
36
36
  return "Midnight Demon";
37
37
  }
38
- export const PeakHoursSlide = ({ stats, commentary, mode }) => {
38
+ export const PeakHoursSlide = ({ stats, commentary, mode, }) => {
39
39
  const frame = useCurrentFrame();
40
40
  const c = getColors(mode);
41
- const maxActivity = Math.max(...stats.hourlyActivity.map(h => h.tokenCount), 1);
41
+ const maxActivity = Math.max(...stats.hourlyActivity.map((h) => h.tokenCount), 1);
42
42
  return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(280, c), color: c.text }, children: [_jsx(ParticleField, { count: 20, seed: 200 }), _jsx(GlowOrb, { color: c.secondary, size: 250, x: 50, y: 30 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 20, y: 70 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.secondary, zIndex: 1 }, children: mode === "dark" ? "Peak Power Draw" : "Peak Coding Hours" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: { fontSize: 56, fontWeight: 900, ...glowText(c.secondary), color: c.text, zIndex: 1, marginTop: 12 }, children: getTimeTitle(stats.busiestHour, mode) }), _jsxs("div", { style: { fontSize: 18, color: c.textMuted, textAlign: "center", marginTop: 8, zIndex: 1 }, children: ["Your peak: ", _jsx("span", { style: { color: c.secondary, fontWeight: 700 }, children: formatHour(stats.busiestHour) })] })] }), _jsx(FadeIn, { delay: 25, style: { marginTop: 32, zIndex: 1 }, children: _jsxs("div", { style: { width: 700, height: 180 }, children: [_jsx("div", { style: { display: "flex", alignItems: "flex-end", gap: 3, height: 150 }, children: stats.hourlyActivity.map((h, i) => {
43
43
  const barHeight = interpolate(frame, [28 + i * 0.5, 48 + i * 0.5], [0, (h.tokenCount / maxActivity) * 150], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
44
44
  const isPeak = h.hour === stats.busiestHour;
45
45
  const isNight = h.hour >= 22 || h.hour < 5;
46
- return (_jsx("div", { style: { flex: 1 }, children: _jsx("div", { style: { width: "100%", height: barHeight, borderRadius: 3, background: isPeak ? `linear-gradient(180deg, ${c.secondary}, ${c.primary})` : isNight ? `${c.primary}cc` : `${c.text}60`, boxShadow: isPeak ? `0 0 8px ${c.secondary}80` : undefined } }) }, i));
47
- }) }), _jsx("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 6 }, children: [0, 6, 12, 18, 23].map(h => _jsx("div", { style: { fontSize: 12, color: c.textMuted }, children: formatHour(h) }, h)) })] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
46
+ return (_jsx("div", { style: { flex: 1 }, children: _jsx("div", { style: {
47
+ width: "100%",
48
+ height: barHeight,
49
+ borderRadius: 3,
50
+ background: isPeak
51
+ ? `linear-gradient(180deg, ${c.secondary}, ${c.primary})`
52
+ : isNight
53
+ ? `${c.primary}cc`
54
+ : `${c.text}60`,
55
+ boxShadow: isPeak ? `0 0 8px ${c.secondary}80` : undefined,
56
+ } }) }, i));
57
+ }) }), _jsx("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 6 }, children: [0, 6, 12, 18, 23].map((h) => (_jsx("div", { style: { fontSize: 12, color: c.textMuted }, children: formatHour(h) }, h))) })] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
48
58
  };
@@ -11,12 +11,32 @@ function formatDuration(minutes) {
11
11
  const mins = Math.round(minutes % 60);
12
12
  return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
13
13
  }
14
- export const SessionSlide = ({ stats, commentary, mode }) => {
14
+ export const SessionSlide = ({ stats, commentary, mode, }) => {
15
15
  const frame = useCurrentFrame();
16
16
  const { fps } = useVideoConfig();
17
17
  const c = getColors(mode);
18
18
  const pulse = spring({ frame: frame % 40, fps, from: 1, to: 1.04, config: { damping: 5 } });
19
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(160, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 123 }), _jsx(GlowOrb, { color: c.pink, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 75, y: 20 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.pink, zIndex: 1 }, children: mode === "dark" ? "Endurance Test" : "Marathon Session" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: { fontSize: 80, fontWeight: 900, ...glowText(c.pink), color: c.text, zIndex: 1, marginTop: 16, transform: `scale(${pulse})` }, children: formatDuration(stats.longestSession.durationMinutes) }), _jsx("div", { style: { fontSize: 18, color: c.textMuted, zIndex: 1, marginTop: 8 }, children: "longest session" })] }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { display: "flex", gap: 36, marginTop: 32, zIndex: 1 }, children: [_jsx(Pill, { label: "Sessions", value: stats.totalSessions, color: c.primary, textColor: c.textMuted }), _jsx(Pill, { label: "Avg", value: `${stats.avgSessionMinutes}m`, color: c.secondary, textColor: c.textMuted }), _jsx(Pill, { label: "Messages", value: stats.totalMessages, color: mode === "dark" ? c.textMuted : "#10b981", textColor: c.textMuted }), _jsx(Pill, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { display: "flex", gap: 14, marginTop: 24, zIndex: 1 }, children: [_jsx(Chip, { label: "Active Days", value: stats.totalDaysActive, color: c.accent, textColor: c.textMuted }), _jsx(Chip, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Chip, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 60, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
19
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(160, c), color: c.text }, children: [_jsx(ParticleField, { count: 25, seed: 123 }), _jsx(GlowOrb, { color: c.pink, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.primary, size: 180, x: 75, y: 20 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: c.pink, zIndex: 1 }, children: mode === "dark" ? "Endurance Test" : "Marathon Session" }) }), _jsxs(FadeIn, { delay: 12, children: [_jsx("div", { style: {
20
+ fontSize: 80,
21
+ fontWeight: 900,
22
+ ...glowText(c.pink),
23
+ color: c.text,
24
+ zIndex: 1,
25
+ marginTop: 16,
26
+ transform: `scale(${pulse})`,
27
+ }, children: formatDuration(stats.longestSession.durationMinutes) }), _jsx("div", { style: { fontSize: 18, color: c.textMuted, zIndex: 1, marginTop: 8 }, children: "longest session" })] }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { display: "flex", gap: 36, marginTop: 32, zIndex: 1 }, children: [_jsx(Pill, { label: "Sessions", value: stats.totalSessions, color: c.primary, textColor: c.textMuted }), _jsx(Pill, { label: "Avg", value: `${stats.avgSessionMinutes}m`, color: c.secondary, textColor: c.textMuted }), _jsx(Pill, { label: "Messages", value: stats.totalMessages, color: mode === "dark" ? c.textMuted : c.green, textColor: c.textMuted }), _jsx(Pill, { label: "Streak", value: `${stats.streakDays}d`, color: c.gold, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { display: "flex", gap: 14, marginTop: 24, zIndex: 1 }, children: [_jsx(Chip, { label: "Active Days", value: stats.totalDaysActive, color: c.accent, textColor: c.textMuted }), _jsx(Chip, { label: "Night Owl", value: `${stats.nightOwlScore}%`, color: c.primary, textColor: c.textMuted }), _jsx(Chip, { label: "Weekend", value: `${stats.weekendWarriorScore}%`, color: c.secondary, textColor: c.textMuted })] }) }), _jsx(FadeIn, { delay: 60, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
20
28
  };
21
- const Pill = ({ label: l, value, color, textColor }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 34, fontWeight: 800, color, ...glowText(color) }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: { fontSize: 12, color: textColor, marginTop: 3, textTransform: "uppercase", letterSpacing: "0.1em" }, children: l })] }));
22
- const Chip = ({ label: l, value, color, textColor }) => (_jsxs("div", { style: { background: `${color}20`, border: `1px solid ${color}50`, borderRadius: 10, padding: "8px 18px", textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 24, fontWeight: 700, color }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2 }, children: l })] }));
29
+ const Pill = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsx("div", { style: { fontSize: 34, fontWeight: 800, color, ...glowText(color) }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: {
30
+ fontSize: 12,
31
+ color: textColor,
32
+ marginTop: 3,
33
+ textTransform: "uppercase",
34
+ letterSpacing: "0.1em",
35
+ }, children: l })] }));
36
+ const Chip = ({ label: l, value, color, textColor, }) => (_jsxs("div", { style: {
37
+ background: `${color}20`,
38
+ border: `1px solid ${color}50`,
39
+ borderRadius: 10,
40
+ padding: "8px 18px",
41
+ textAlign: "center",
42
+ }, children: [_jsx("div", { style: { fontSize: 24, fontWeight: 700, color }, children: typeof value === "number" ? value.toLocaleString() : value }), _jsx("div", { style: { fontSize: 11, color: textColor, marginTop: 2 }, children: l })] }));
@@ -34,7 +34,7 @@ function getSummaryTitle(periodLabel, mode) {
34
34
  return mode === "dark" ? match[1] : match[0];
35
35
  return periodLabel; // custom date range string
36
36
  }
37
- export const SummarySlide = ({ stats, commentary, mode }) => {
37
+ export const SummarySlide = ({ stats, commentary, mode, }) => {
38
38
  const frame = useCurrentFrame();
39
39
  const { fps } = useVideoConfig();
40
40
  const c = getColors(mode);
@@ -42,11 +42,43 @@ export const SummarySlide = ({ stats, commentary, mode }) => {
42
42
  const cardOpacity = interpolate(frame, [5, 20], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
43
43
  const rotation = interpolate(Math.sin(frame * 0.02), [-1, 1], [-0.8, 0.8]);
44
44
  return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(135, c), color: c.text }, children: [_jsx(ParticleField, { count: 50, seed: 444 }), _jsx(GlowOrb, { color: c.primary, size: 240, x: 25, y: 20 }), _jsx(GlowOrb, { color: c.secondary, size: 200, x: 75, y: 75 }), _jsx(GlowOrb, { color: c.accent, size: 170, x: 50, y: 50, speed: 0.2 }), _jsxs("div", { style: {
45
- opacity: cardOpacity, transform: `scale(${cardScale}) rotate(${rotation}deg)`,
45
+ opacity: cardOpacity,
46
+ transform: `scale(${cardScale}) rotate(${rotation}deg)`,
46
47
  background: `linear-gradient(145deg, ${c.bgGradient1}ee, ${c.bgGradient3}ee)`,
47
- border: `1px solid ${c.primary}50`, borderRadius: 24, padding: "36px 44px",
48
- maxWidth: 620, width: "92%",
49
- boxShadow: `0 0 36px ${c.primary}20, 0 12px 36px rgba(0,0,0,0.5)`, zIndex: 1,
50
- }, children: [_jsx("div", { style: { textAlign: "center", marginBottom: 20 }, children: _jsx("div", { style: { fontSize: 28, fontWeight: 900, ...glowText(c.primary), background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }, children: getSummaryTitle(stats.periodLabel, mode) }) }), _jsx(FadeIn, { delay: 15, children: _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 36px" }, children: [_jsx(Row, { label: "Tokens", value: formatNumber(stats.totalTokens), color: c.gold, textColor: c.textMuted }), _jsx(Row, { label: "Cost", value: `$${stats.totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, color: mode === "dark" ? c.accent : "#10b981", textColor: c.textMuted }), _jsx(Row, { label: "Sessions", value: stats.totalSessions.toLocaleString(), color: c.primary, textColor: c.textMuted }), _jsx(Row, { label: "Messages", value: formatNumber(stats.totalMessages), color: c.pink, textColor: c.textMuted }), _jsx(Row, { label: "Active Days", value: stats.totalDaysActive.toLocaleString(), color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "Fav Model", value: getModelShortName(stats.favoriteModel), color: c.accent, textColor: c.textMuted }), mode === "dark" && _jsxs(_Fragment, { children: [_jsx(Row, { label: "Water", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "CO\u2082", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, color: c.textMuted, textColor: c.textMuted })] })] }) }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: { marginTop: 20, textAlign: "center", padding: "12px 20px", background: `linear-gradient(135deg, ${c.primary}20, ${c.accent}20)`, borderRadius: 12, border: `1px solid ${c.primary}40` }, children: [_jsx("div", { style: { fontSize: 12, color: c.textMuted, textTransform: "uppercase", letterSpacing: "0.15em" }, children: "Your Archetype" }), _jsx("div", { style: { fontSize: 26, fontWeight: 800, background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", marginTop: 3 }, children: stats.archetype })] }) }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: { textAlign: "center", marginTop: 16, fontSize: 15, fontStyle: "italic", color: c.textMuted }, children: commentary }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { textAlign: "center", marginTop: 14, fontSize: 12, color: c.textDim }, children: ["ccwrapped", mode === "dark" ? " — know your impact" : " — put meme software on the map"] }) })] })] }));
48
+ border: `1px solid ${c.primary}50`,
49
+ borderRadius: 24,
50
+ padding: "36px 44px",
51
+ maxWidth: 620,
52
+ width: "92%",
53
+ boxShadow: `0 0 36px ${c.primary}20, 0 12px 36px rgba(0,0,0,0.5)`,
54
+ zIndex: 1,
55
+ }, children: [_jsx("div", { style: { textAlign: "center", marginBottom: 20 }, children: _jsx("div", { style: {
56
+ fontSize: 28,
57
+ fontWeight: 900,
58
+ ...glowText(c.primary),
59
+ background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`,
60
+ WebkitBackgroundClip: "text",
61
+ WebkitTextFillColor: "transparent",
62
+ }, children: getSummaryTitle(stats.periodLabel, mode) }) }), _jsx(FadeIn, { delay: 15, children: _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 36px" }, children: [_jsx(Row, { label: "Tokens", value: formatNumber(stats.totalTokens), color: c.gold, textColor: c.textMuted }), _jsx(Row, { label: "Cost", value: `$${stats.totalCost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, color: mode === "dark" ? c.accent : c.green, textColor: c.textMuted }), _jsx(Row, { label: "Sessions", value: stats.totalSessions.toLocaleString(), color: c.primary, textColor: c.textMuted }), _jsx(Row, { label: "Messages", value: formatNumber(stats.totalMessages), color: c.pink, textColor: c.textMuted }), _jsx(Row, { label: "Active Days", value: stats.totalDaysActive.toLocaleString(), color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "Fav Model", value: getModelShortName(stats.favoriteModel), color: c.accent, textColor: c.textMuted }), mode === "dark" && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Water", value: `${stats.waterLiters.toLocaleString(undefined, { maximumFractionDigits: 1 })}L`, color: c.secondary, textColor: c.textMuted }), _jsx(Row, { label: "CO\u2082", value: `${(stats.co2Grams / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}kg`, color: c.textMuted, textColor: c.textMuted })] }))] }) }), _jsx(FadeIn, { delay: 30, children: _jsxs("div", { style: {
63
+ marginTop: 20,
64
+ textAlign: "center",
65
+ padding: "12px 20px",
66
+ background: `linear-gradient(135deg, ${c.primary}20, ${c.accent}20)`,
67
+ borderRadius: 12,
68
+ border: `1px solid ${c.primary}40`,
69
+ }, children: [_jsx("div", { style: { fontSize: 12, color: c.textMuted, textTransform: "uppercase", letterSpacing: "0.15em" }, children: "Your Archetype" }), _jsx("div", { style: {
70
+ fontSize: 26,
71
+ fontWeight: 800,
72
+ background: `linear-gradient(90deg, ${c.primary}, ${c.accent})`,
73
+ WebkitBackgroundClip: "text",
74
+ WebkitTextFillColor: "transparent",
75
+ marginTop: 3,
76
+ }, children: stats.archetype })] }) }), _jsx(FadeIn, { delay: 40, children: _jsx("div", { style: {
77
+ textAlign: "center",
78
+ marginTop: 16,
79
+ fontSize: 15,
80
+ fontStyle: "italic",
81
+ color: c.textMuted,
82
+ }, children: commentary }) }), _jsx(FadeIn, { delay: 50, children: _jsxs("div", { style: { textAlign: "center", marginTop: 14, fontSize: 12, color: c.textDim }, children: ["ccwrap", mode === "dark" ? " — know your impact" : " — put meme software on the map"] }) })] })] }));
51
83
  };
52
- const Row = ({ label, value, color, textColor }) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("div", { style: { fontSize: 14, color: textColor }, children: label }), _jsx("div", { style: { fontSize: 20, fontWeight: 700, color }, children: value })] }));
84
+ const Row = ({ label, value, color, textColor, }) => (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("div", { style: { fontSize: 14, color: textColor }, children: label }), _jsx("div", { style: { fontSize: 20, fontWeight: 700, color }, children: value })] }));
@@ -7,19 +7,34 @@ import { ParticleField } from "../components/ParticleField";
7
7
  import { GlowOrb } from "../components/GlowOrb";
8
8
  function formatTokens(n) {
9
9
  if (n >= 1_000_000_000)
10
- return { value: Math.round(n / 1_000_000_000 * 10) / 10, suffix: "B" };
10
+ return { value: Math.round((n / 1_000_000_000) * 10) / 10, suffix: "B" };
11
11
  if (n >= 1_000_000)
12
- return { value: Math.round(n / 1_000_000 * 10) / 10, suffix: "M" };
12
+ return { value: Math.round((n / 1_000_000) * 10) / 10, suffix: "M" };
13
13
  if (n >= 1_000)
14
- return { value: Math.round(n / 1_000 * 10) / 10, suffix: "K" };
14
+ return { value: Math.round((n / 1_000) * 10) / 10, suffix: "K" };
15
15
  return { value: n, suffix: "" };
16
16
  }
17
- export const TokensSlide = ({ stats, commentary, mode }) => {
17
+ export const TokensSlide = ({ stats, commentary, mode, }) => {
18
18
  const frame = useCurrentFrame();
19
19
  const c = getColors(mode);
20
20
  const formatted = formatTokens(stats.totalTokens);
21
21
  const shake = frame > 30 && frame < 45 ? Math.sin(frame * 2) * 2 : 0;
22
22
  const accentColor = mode === "dark" ? c.accent : c.gold;
23
- return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 99 }), _jsx(GlowOrb, { color: accentColor, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 20, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: accentColor, zIndex: 1 }, children: "Total Tokens Consumed" }) }), _jsx("div", { style: { transform: `translateX(${shake}px)`, zIndex: 1, marginTop: 20 }, children: _jsx(AnimatedNumber, { value: formatted.value, decimals: formatted.suffix ? 1 : 0, suffix: formatted.suffix, style: { ...bigNumber, ...glowText(accentColor), background: `linear-gradient(135deg, ${accentColor}, ${c.text})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", display: "block", textAlign: "center" }, startFrame: 10, duration: 40 }) }), _jsx(FadeIn, { delay: 45, children: _jsxs("div", { style: { display: "flex", gap: 50, marginTop: 32, zIndex: 1 }, children: [_jsx(StatBox, { label: "Input", value: formatTokens(stats.totalInputTokens), color: c.secondary }), _jsx(StatBox, { label: "Output", value: formatTokens(stats.totalOutputTokens), color: mode === "dark" ? c.textMuted : "#10b981" }), mode === "dark" && _jsx(StatBox, { label: "Water", value: { value: stats.waterLiters, suffix: "L" }, color: c.secondary }), mode === "dark" && _jsx(StatBox, { label: "Energy", value: { value: stats.kwhUsed, suffix: " kWh" }, color: c.gold })] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
23
+ return (_jsxs("div", { style: { ...baseSlide, ...gradientBg(200, c), color: c.text }, children: [_jsx(ParticleField, { count: 35, seed: 99 }), _jsx(GlowOrb, { color: accentColor, size: 300, x: 50, y: 40 }), _jsx(GlowOrb, { color: c.accent, size: 180, x: 20, y: 75 }), _jsx(FadeIn, { delay: 5, children: _jsx("div", { style: { ...label, color: accentColor, zIndex: 1 }, children: "Total Tokens Consumed" }) }), _jsx("div", { style: { transform: `translateX(${shake}px)`, zIndex: 1, marginTop: 20 }, children: _jsx(AnimatedNumber, { value: formatted.value, decimals: formatted.suffix ? 1 : 0, suffix: formatted.suffix, style: {
24
+ ...bigNumber,
25
+ ...glowText(accentColor),
26
+ background: `linear-gradient(135deg, ${accentColor}, ${c.text})`,
27
+ WebkitBackgroundClip: "text",
28
+ WebkitTextFillColor: "transparent",
29
+ display: "block",
30
+ textAlign: "center",
31
+ }, startFrame: 10, duration: 40 }) }), _jsx(FadeIn, { delay: 45, children: _jsxs("div", { style: { display: "flex", gap: 50, marginTop: 32, zIndex: 1 }, children: [_jsx(StatBox, { label: "Input", value: formatTokens(stats.totalInputTokens), color: c.secondary }), _jsx(StatBox, { label: "Output", value: formatTokens(stats.totalOutputTokens), color: mode === "dark" ? c.textMuted : c.green }), mode === "dark" && (_jsx(StatBox, { label: "Water", value: { value: stats.waterLiters, suffix: "L" }, color: c.secondary })), mode === "dark" && (_jsx(StatBox, { label: "Energy", value: { value: stats.kwhUsed, suffix: " kWh" }, color: c.gold }))] }) }), _jsx(FadeIn, { delay: 55, children: _jsx("div", { style: { ...memeCaption, color: c.textMuted, zIndex: 1 }, children: commentary }) })] }));
24
32
  };
25
- const StatBox = ({ label: l, value, color }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsxs("div", { style: { fontSize: 36, fontWeight: 800, color, ...glowText(color) }, children: [value.value.toLocaleString(undefined, { maximumFractionDigits: value.suffix ? 1 : 0 }), value.suffix] }), _jsx("div", { style: { fontSize: 13, color: "inherit", opacity: 0.7, marginTop: 4, textTransform: "uppercase", letterSpacing: "0.1em" }, children: l })] }));
33
+ const StatBox = ({ label: l, value, color, }) => (_jsxs("div", { style: { textAlign: "center" }, children: [_jsxs("div", { style: { fontSize: 36, fontWeight: 800, color, ...glowText(color) }, children: [value.value.toLocaleString(undefined, { maximumFractionDigits: value.suffix ? 1 : 0 }), value.suffix] }), _jsx("div", { style: {
34
+ fontSize: 13,
35
+ color: "inherit",
36
+ opacity: 0.7,
37
+ marginTop: 4,
38
+ textTransform: "uppercase",
39
+ letterSpacing: "0.1em",
40
+ }, children: l })] }));
@@ -16,21 +16,6 @@ declare const sassyColors: {
16
16
  };
17
17
  export type ColorScheme = typeof sassyColors;
18
18
  export declare function getColors(mode: "sassy" | "dark"): ColorScheme;
19
- export declare const colors: {
20
- bg: string;
21
- bgGradient1: string;
22
- bgGradient2: string;
23
- bgGradient3: string;
24
- primary: string;
25
- secondary: string;
26
- accent: string;
27
- gold: string;
28
- green: string;
29
- pink: string;
30
- text: string;
31
- textMuted: string;
32
- textDim: string;
33
- };
34
19
  export declare const fonts: {
35
20
  heading: string;
36
21
  body: string;
@@ -32,7 +32,6 @@ const darkColors = {
32
32
  export function getColors(mode) {
33
33
  return mode === "dark" ? darkColors : sassyColors;
34
34
  }
35
- export const colors = sassyColors;
36
35
  export const fonts = {
37
36
  heading: "Inter, system-ui, sans-serif",
38
37
  body: "Inter, system-ui, sans-serif",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccwrap",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code Wrapped - Your AI coding stats, meme-ified into a shareable video",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,12 @@
23
23
  "start": "tsx src/cli.ts",
24
24
  "preview": "remotion preview src/video/index.ts",
25
25
  "render": "tsx src/cli.ts",
26
+ "test": "vitest run",
27
+ "test:coverage": "vitest run --coverage",
28
+ "lint": "eslint src/ tests/",
29
+ "lint:fix": "eslint src/ tests/ --fix",
30
+ "format": "prettier --write 'src/**/*.{ts,tsx}' 'tests/**/*.ts'",
31
+ "format:check": "prettier --check 'src/**/*.{ts,tsx}' 'tests/**/*.ts'",
26
32
  "prepublishOnly": "npm run build"
27
33
  },
28
34
  "bin": {
@@ -50,9 +56,16 @@
50
56
  "remotion": "^4.0.0"
51
57
  },
52
58
  "devDependencies": {
59
+ "@eslint/js": "^10.0.1",
53
60
  "@types/node": "^22.0.0",
54
61
  "@types/react": "^18.3.0",
62
+ "@vitest/coverage-v8": "^4.1.0",
63
+ "eslint": "^10.0.3",
64
+ "eslint-config-prettier": "^10.1.8",
65
+ "prettier": "^3.8.1",
55
66
  "tsx": "^4.0.0",
56
- "typescript": "^5.6.0"
67
+ "typescript": "^5.6.0",
68
+ "typescript-eslint": "^8.57.1",
69
+ "vitest": "^4.1.0"
57
70
  }
58
71
  }