ccwrap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +181 -0
- package/dist/data/commentary.d.ts +13 -0
- package/dist/data/commentary.js +102 -0
- package/dist/data/parser.d.ts +2 -0
- package/dist/data/parser.js +358 -0
- package/dist/data/types.d.ts +109 -0
- package/dist/data/types.js +1 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +60 -0
- package/dist/video/Composition.d.ts +8 -0
- package/dist/video/Composition.js +50 -0
- package/dist/video/Root.d.ts +2 -0
- package/dist/video/Root.js +81 -0
- package/dist/video/components/AnimatedNumber.d.ts +10 -0
- package/dist/video/components/AnimatedNumber.js +16 -0
- package/dist/video/components/FadeIn.d.ts +8 -0
- package/dist/video/components/FadeIn.js +18 -0
- package/dist/video/components/GlowOrb.d.ts +8 -0
- package/dist/video/components/GlowOrb.js +19 -0
- package/dist/video/components/ParticleField.d.ts +5 -0
- package/dist/video/components/ParticleField.js +36 -0
- package/dist/video/index.d.ts +1 -0
- package/dist/video/index.js +3 -0
- package/dist/video/slides/ArchetypeSlide.d.ts +7 -0
- package/dist/video/slides/ArchetypeSlide.js +34 -0
- package/dist/video/slides/BusiestDaySlide.d.ts +7 -0
- package/dist/video/slides/BusiestDaySlide.js +23 -0
- package/dist/video/slides/CostSlide.d.ts +7 -0
- package/dist/video/slides/CostSlide.js +12 -0
- package/dist/video/slides/IntroSlide.d.ts +5 -0
- package/dist/video/slides/IntroSlide.js +21 -0
- package/dist/video/slides/ModelSlide.d.ts +7 -0
- package/dist/video/slides/ModelSlide.js +45 -0
- package/dist/video/slides/PeakHoursSlide.d.ts +7 -0
- package/dist/video/slides/PeakHoursSlide.js +48 -0
- package/dist/video/slides/SessionSlide.d.ts +7 -0
- package/dist/video/slides/SessionSlide.js +22 -0
- package/dist/video/slides/SummarySlide.d.ts +7 -0
- package/dist/video/slides/SummarySlide.js +52 -0
- package/dist/video/slides/TokensSlide.d.ts +7 -0
- package/dist/video/slides/TokensSlide.js +25 -0
- package/dist/video/styles.d.ts +45 -0
- package/dist/video/styles.js +84 -0
- package/package.json +58 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
// Pricing per million tokens (from Anthropic pricing page)
|
|
6
|
+
// Each model has: input, output, cache_write (25% premium), cache_read (90% discount)
|
|
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 },
|
|
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 +
|
|
22
|
+
outputTokens * pricing.output +
|
|
23
|
+
cacheCreationTokens * pricing.cacheWrite +
|
|
24
|
+
cacheReadTokens * pricing.cacheRead) / 1_000_000;
|
|
25
|
+
}
|
|
26
|
+
async function findJSONLFiles() {
|
|
27
|
+
const claudeDir = join(homedir(), ".claude", "projects");
|
|
28
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
29
|
+
? join(process.env.CLAUDE_CONFIG_DIR, "projects")
|
|
30
|
+
: null;
|
|
31
|
+
const dirs = [claudeDir, configDir].filter(Boolean);
|
|
32
|
+
const allFiles = [];
|
|
33
|
+
for (const dir of dirs) {
|
|
34
|
+
try {
|
|
35
|
+
const files = await glob("**/*.jsonl", { cwd: dir, absolute: true });
|
|
36
|
+
allFiles.push(...files);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Directory doesn't exist, skip
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return allFiles;
|
|
43
|
+
}
|
|
44
|
+
async function parseJSONLFile(filePath) {
|
|
45
|
+
const content = await readFile(filePath, "utf-8");
|
|
46
|
+
const entries = [];
|
|
47
|
+
for (const line of content.split("\n")) {
|
|
48
|
+
if (!line.trim())
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(line);
|
|
52
|
+
if (parsed.type === "assistant" && parsed.message?.usage) {
|
|
53
|
+
entries.push(parsed);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Skip invalid lines
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
function extractProject(filePath) {
|
|
63
|
+
// Path format: ~/.claude/projects/{encoded-project-path}/{sessionId}.jsonl
|
|
64
|
+
const parts = filePath.split("/projects/");
|
|
65
|
+
if (parts.length < 2)
|
|
66
|
+
return "unknown";
|
|
67
|
+
const afterProjects = parts[1];
|
|
68
|
+
const segments = afterProjects.split("/");
|
|
69
|
+
const projectEncoded = segments[0] ?? "unknown";
|
|
70
|
+
// Decode: -home-user-myproject -> /home/user/myproject
|
|
71
|
+
return projectEncoded
|
|
72
|
+
.replace(/^-/, "/")
|
|
73
|
+
.replace(/-/g, "/");
|
|
74
|
+
}
|
|
75
|
+
export async function loadAndComputeStats(range, periodLabel) {
|
|
76
|
+
const files = await findJSONLFiles();
|
|
77
|
+
if (files.length === 0) {
|
|
78
|
+
throw new Error("No Claude Code usage data found. Make sure you have used Claude Code before.");
|
|
79
|
+
}
|
|
80
|
+
// Parse all files
|
|
81
|
+
const allEntries = [];
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const entries = await parseJSONLFile(file);
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
allEntries.push({ ...entry, _file: file });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (allEntries.length === 0) {
|
|
89
|
+
throw new Error("No usage data entries found in Claude Code logs.");
|
|
90
|
+
}
|
|
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
|
|
94
|
+
if (range) {
|
|
95
|
+
const sinceMs = range.since.getTime();
|
|
96
|
+
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) {
|
|
105
|
+
throw new Error(`No usage data found in the selected time period (${before} entries exist outside this range).`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Build session map
|
|
109
|
+
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);
|
|
115
|
+
}
|
|
116
|
+
// Compute session stats
|
|
117
|
+
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))];
|
|
123
|
+
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0, totalCost = 0;
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const u = entry.message?.usage;
|
|
126
|
+
if (u) {
|
|
127
|
+
totalInput += u.input_tokens ?? 0;
|
|
128
|
+
totalOutput += u.output_tokens ?? 0;
|
|
129
|
+
totalCacheCreate += u.cache_creation_input_tokens ?? 0;
|
|
130
|
+
totalCacheRead += u.cache_read_input_tokens ?? 0;
|
|
131
|
+
}
|
|
132
|
+
if (entry.costUSD) {
|
|
133
|
+
totalCost += entry.costUSD;
|
|
134
|
+
}
|
|
135
|
+
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);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
sessions.push({
|
|
140
|
+
sessionId,
|
|
141
|
+
project: extractProject(entries[0]._file),
|
|
142
|
+
startTime,
|
|
143
|
+
endTime,
|
|
144
|
+
durationMinutes: (endTime.getTime() - startTime.getTime()) / 60_000,
|
|
145
|
+
messageCount: entries.length,
|
|
146
|
+
totalInputTokens: totalInput,
|
|
147
|
+
totalOutputTokens: totalOutput,
|
|
148
|
+
totalCacheCreationTokens: totalCacheCreate,
|
|
149
|
+
totalCacheReadTokens: totalCacheRead,
|
|
150
|
+
totalTokens: totalInput + totalOutput + totalCacheCreate + totalCacheRead,
|
|
151
|
+
models,
|
|
152
|
+
costUSD: totalCost,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
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
|
+
}
|
|
203
|
+
const totalTokensAll = [...modelMap.values()].reduce((sum, m) => sum + m.tokens, 0);
|
|
204
|
+
const modelUsage = [...modelMap.entries()]
|
|
205
|
+
.map(([model, data]) => ({
|
|
206
|
+
model,
|
|
207
|
+
tokenCount: data.tokens,
|
|
208
|
+
messageCount: data.messages,
|
|
209
|
+
costUSD: data.cost,
|
|
210
|
+
percentage: totalTokensAll > 0 ? (data.tokens / totalTokensAll) * 100 : 0,
|
|
211
|
+
}))
|
|
212
|
+
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
213
|
+
// Totals
|
|
214
|
+
const totalTokens = sessions.reduce((s, sess) => s + sess.totalTokens, 0);
|
|
215
|
+
const totalInputTokens = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0);
|
|
216
|
+
const totalOutputTokens = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0);
|
|
217
|
+
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]));
|
|
223
|
+
const totalDaysActive = uniqueDays.size;
|
|
224
|
+
// 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);
|
|
228
|
+
// Project frequency
|
|
229
|
+
const projectCounts = new Map();
|
|
230
|
+
for (const sess of sessions) {
|
|
231
|
+
projectCounts.set(sess.project, (projectCounts.get(sess.project) ?? 0) + sess.totalTokens);
|
|
232
|
+
}
|
|
233
|
+
const mostActiveProject = [...projectCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
234
|
+
// Streak
|
|
235
|
+
const sortedDays = [...uniqueDays].sort();
|
|
236
|
+
let maxStreak = 1, currentStreak = 1;
|
|
237
|
+
for (let i = 1; i < sortedDays.length; i++) {
|
|
238
|
+
const prev = new Date(sortedDays[i - 1]);
|
|
239
|
+
const curr = new Date(sortedDays[i]);
|
|
240
|
+
const diffDays = (curr.getTime() - prev.getTime()) / 86_400_000;
|
|
241
|
+
if (diffDays === 1) {
|
|
242
|
+
currentStreak++;
|
|
243
|
+
maxStreak = Math.max(maxStreak, currentStreak);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
currentStreak = 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Scores
|
|
250
|
+
const nightTokens = hourly.filter(h => h.hour >= 22 || h.hour < 5).reduce((s, h) => s + h.tokenCount, 0);
|
|
251
|
+
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;
|
|
259
|
+
const favoriteModel = modelUsage[0]?.model ?? "unknown";
|
|
260
|
+
const totalSessionHours = sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / 60;
|
|
261
|
+
const avgSessionMinutes = sessions.length > 0 ? sessions.reduce((s, sess) => s + sess.durationMinutes, 0) / sessions.length : 0;
|
|
262
|
+
// Archetype
|
|
263
|
+
const { archetype, archetypeDescription } = computeArchetype({
|
|
264
|
+
nightOwlScore,
|
|
265
|
+
weekendWarriorScore,
|
|
266
|
+
avgSessionMinutes,
|
|
267
|
+
totalSessions: sessions.length,
|
|
268
|
+
streakDays: maxStreak,
|
|
269
|
+
totalTokens,
|
|
270
|
+
favoriteModel,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
periodLabel: periodLabel ?? "Your all-time",
|
|
274
|
+
totalTokens,
|
|
275
|
+
totalInputTokens,
|
|
276
|
+
totalOutputTokens,
|
|
277
|
+
totalCost,
|
|
278
|
+
totalSessions: sessions.length,
|
|
279
|
+
totalMessages,
|
|
280
|
+
totalDaysActive,
|
|
281
|
+
firstActivity,
|
|
282
|
+
lastActivity,
|
|
283
|
+
busiestDay,
|
|
284
|
+
busiestHour,
|
|
285
|
+
longestSession,
|
|
286
|
+
mostActiveProject,
|
|
287
|
+
hourlyActivity: hourly,
|
|
288
|
+
dailyActivity,
|
|
289
|
+
modelUsage,
|
|
290
|
+
tokensPerDay: totalDaysActive > 0 ? Math.round(totalTokens / totalDaysActive) : 0,
|
|
291
|
+
avgSessionMinutes: Math.round(avgSessionMinutes),
|
|
292
|
+
nightOwlScore,
|
|
293
|
+
weekendWarriorScore,
|
|
294
|
+
streakDays: maxStreak,
|
|
295
|
+
favoriteModel,
|
|
296
|
+
archetype,
|
|
297
|
+
archetypeDescription,
|
|
298
|
+
warAndPeaceEquivalent: Math.round((totalTokens / 580_000) * 100) / 100,
|
|
299
|
+
coffeeEquivalent: Math.round(totalCost / 5),
|
|
300
|
+
marathonEquivalent: Math.round((totalSessionHours / 4.5) * 100) / 100,
|
|
301
|
+
// Environmental estimates (rough but directionally correct)
|
|
302
|
+
// ~0.5 liters of water per 1M tokens for data center cooling
|
|
303
|
+
waterLiters: Math.round(totalTokens / 1_000_000 * 0.5 * 100) / 100,
|
|
304
|
+
// ~0.3g CO2 per 1K tokens (inference energy + grid mix)
|
|
305
|
+
co2Grams: Math.round(totalTokens / 1_000 * 0.3),
|
|
306
|
+
// ~0.001 kWh per 1K tokens
|
|
307
|
+
kwhUsed: Math.round(totalTokens / 1_000 * 0.001 * 100) / 100,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function computeArchetype(params) {
|
|
311
|
+
const { nightOwlScore, weekendWarriorScore, avgSessionMinutes, totalSessions, streakDays, totalTokens, favoriteModel } = params;
|
|
312
|
+
if (nightOwlScore > 50) {
|
|
313
|
+
return {
|
|
314
|
+
archetype: "The Night Owl Demon",
|
|
315
|
+
archetypeDescription: "You code when the world sleeps. Your commit history is a horror movie for morning people.",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (weekendWarriorScore > 40) {
|
|
319
|
+
return {
|
|
320
|
+
archetype: "The Weekend Warrior",
|
|
321
|
+
archetypeDescription: "While others brunch, you prompt. Your weekends are just weekdays with better snacks.",
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (avgSessionMinutes > 120) {
|
|
325
|
+
return {
|
|
326
|
+
archetype: "The Marathon Runner",
|
|
327
|
+
archetypeDescription: "Your sessions are longer than most movies. Touch grass? You've never heard of her.",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (streakDays > 14) {
|
|
331
|
+
return {
|
|
332
|
+
archetype: "The Streak Machine",
|
|
333
|
+
archetypeDescription: "Day after day after day. Your consistency is either impressive or concerning. We're not sure which.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (totalTokens > 10_000_000) {
|
|
337
|
+
return {
|
|
338
|
+
archetype: "The Token Whale",
|
|
339
|
+
archetypeDescription: "You consume tokens like a whale consumes krill. Anthropic's servers know you by name.",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (favoriteModel.includes("opus")) {
|
|
343
|
+
return {
|
|
344
|
+
archetype: "The Connoisseur",
|
|
345
|
+
archetypeDescription: "Only the finest model for you. Opus or bust. You have expensive taste and zero regrets.",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (totalSessions > 50) {
|
|
349
|
+
return {
|
|
350
|
+
archetype: "The Serial Prompter",
|
|
351
|
+
archetypeDescription: "So many sessions, so little time. You start conversations like others start sentences.",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
archetype: "The Pragmatist",
|
|
356
|
+
archetypeDescription: "Efficient, focused, balanced. You use AI like a precision tool. How boring. (Just kidding, we respect it.)",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface RawJSONLEntry {
|
|
2
|
+
type: "assistant" | "user" | "queue-operation" | "summary";
|
|
3
|
+
timestamp: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
message?: {
|
|
8
|
+
model?: string;
|
|
9
|
+
role?: string;
|
|
10
|
+
usage?: {
|
|
11
|
+
input_tokens: number;
|
|
12
|
+
output_tokens: number;
|
|
13
|
+
cache_creation_input_tokens?: number;
|
|
14
|
+
cache_read_input_tokens?: number;
|
|
15
|
+
};
|
|
16
|
+
content?: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
thinking?: string;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
costUSD?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionStats {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
project: string;
|
|
27
|
+
startTime: Date;
|
|
28
|
+
endTime: Date;
|
|
29
|
+
durationMinutes: number;
|
|
30
|
+
messageCount: number;
|
|
31
|
+
totalInputTokens: number;
|
|
32
|
+
totalOutputTokens: number;
|
|
33
|
+
totalCacheCreationTokens: number;
|
|
34
|
+
totalCacheReadTokens: number;
|
|
35
|
+
totalTokens: number;
|
|
36
|
+
models: string[];
|
|
37
|
+
costUSD: number;
|
|
38
|
+
}
|
|
39
|
+
export interface HourlyActivity {
|
|
40
|
+
hour: number;
|
|
41
|
+
messageCount: number;
|
|
42
|
+
tokenCount: number;
|
|
43
|
+
}
|
|
44
|
+
export interface DailyActivity {
|
|
45
|
+
date: string;
|
|
46
|
+
messageCount: number;
|
|
47
|
+
tokenCount: number;
|
|
48
|
+
costUSD: number;
|
|
49
|
+
sessions: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ModelUsage {
|
|
52
|
+
model: string;
|
|
53
|
+
tokenCount: number;
|
|
54
|
+
messageCount: number;
|
|
55
|
+
costUSD: number;
|
|
56
|
+
percentage: number;
|
|
57
|
+
}
|
|
58
|
+
export interface DateRange {
|
|
59
|
+
since: Date;
|
|
60
|
+
until: Date;
|
|
61
|
+
}
|
|
62
|
+
export interface WrappedStats {
|
|
63
|
+
periodLabel: string;
|
|
64
|
+
totalTokens: number;
|
|
65
|
+
totalInputTokens: number;
|
|
66
|
+
totalOutputTokens: number;
|
|
67
|
+
totalCost: number;
|
|
68
|
+
totalSessions: number;
|
|
69
|
+
totalMessages: number;
|
|
70
|
+
totalDaysActive: number;
|
|
71
|
+
firstActivity: Date;
|
|
72
|
+
lastActivity: Date;
|
|
73
|
+
busiestDay: DailyActivity;
|
|
74
|
+
busiestHour: number;
|
|
75
|
+
longestSession: SessionStats;
|
|
76
|
+
mostActiveProject: string;
|
|
77
|
+
hourlyActivity: HourlyActivity[];
|
|
78
|
+
dailyActivity: DailyActivity[];
|
|
79
|
+
modelUsage: ModelUsage[];
|
|
80
|
+
tokensPerDay: number;
|
|
81
|
+
avgSessionMinutes: number;
|
|
82
|
+
nightOwlScore: number;
|
|
83
|
+
weekendWarriorScore: number;
|
|
84
|
+
streakDays: number;
|
|
85
|
+
favoriteModel: string;
|
|
86
|
+
archetype: string;
|
|
87
|
+
archetypeDescription: string;
|
|
88
|
+
warAndPeaceEquivalent: number;
|
|
89
|
+
coffeeEquivalent: number;
|
|
90
|
+
marathonEquivalent: number;
|
|
91
|
+
waterLiters: number;
|
|
92
|
+
co2Grams: number;
|
|
93
|
+
kwhUsed: number;
|
|
94
|
+
}
|
|
95
|
+
export interface VideoProps {
|
|
96
|
+
stats: WrappedStats;
|
|
97
|
+
commentary: Commentary;
|
|
98
|
+
mode: "sassy" | "dark";
|
|
99
|
+
}
|
|
100
|
+
export interface Commentary {
|
|
101
|
+
tokensLine: string;
|
|
102
|
+
costLine: string;
|
|
103
|
+
modelLine: string;
|
|
104
|
+
busiestDayLine: string;
|
|
105
|
+
sessionLine: string;
|
|
106
|
+
peakHoursLine: string;
|
|
107
|
+
archetypeLine: string;
|
|
108
|
+
summaryLine: string;
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { bundle } from "@remotion/bundler";
|
|
2
|
+
import { renderMedia, selectComposition } from "@remotion/renderer";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function findChromeBinary() {
|
|
7
|
+
const candidates = [
|
|
8
|
+
"/root/.cache/ms-playwright/chromium_headless_shell-1194/chrome-linux/headless_shell",
|
|
9
|
+
"/root/.cache/ms-playwright/chromium-1194/chrome-linux/chrome",
|
|
10
|
+
"/usr/bin/chromium-browser",
|
|
11
|
+
"/usr/bin/chromium",
|
|
12
|
+
"/usr/bin/google-chrome",
|
|
13
|
+
];
|
|
14
|
+
return candidates.find(p => existsSync(p));
|
|
15
|
+
}
|
|
16
|
+
export async function renderVideo(stats, commentary, mode, outputPath) {
|
|
17
|
+
const resolvedOutput = resolve(process.cwd(), outputPath);
|
|
18
|
+
// Resolve entry point - works from both src/ (dev) and dist/ (published)
|
|
19
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const tsEntry = resolve(thisDir, "video", "index.ts");
|
|
21
|
+
const jsEntry = resolve(thisDir, "video", "index.js");
|
|
22
|
+
const entryPoint = existsSync(tsEntry) ? tsEntry : jsEntry;
|
|
23
|
+
const bundleLocation = await bundle({
|
|
24
|
+
entryPoint,
|
|
25
|
+
webpackOverride: (config) => ({
|
|
26
|
+
...config,
|
|
27
|
+
module: {
|
|
28
|
+
...config.module,
|
|
29
|
+
rules: [
|
|
30
|
+
...(config.module?.rules ?? []),
|
|
31
|
+
{ test: /\.m?js$/, resolve: { fullySpecified: false } },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
const browserExecutable = findChromeBinary();
|
|
37
|
+
const inputProps = { stats, commentary, mode };
|
|
38
|
+
const composition = await selectComposition({
|
|
39
|
+
serveUrl: bundleLocation,
|
|
40
|
+
id: "CCWrapped",
|
|
41
|
+
inputProps,
|
|
42
|
+
browserExecutable,
|
|
43
|
+
});
|
|
44
|
+
await renderMedia({
|
|
45
|
+
composition,
|
|
46
|
+
serveUrl: bundleLocation,
|
|
47
|
+
codec: "h264",
|
|
48
|
+
outputLocation: resolvedOutput,
|
|
49
|
+
inputProps,
|
|
50
|
+
browserExecutable,
|
|
51
|
+
chromiumOptions: {
|
|
52
|
+
enableMultiProcessOnLinux: true,
|
|
53
|
+
},
|
|
54
|
+
crf: 15,
|
|
55
|
+
pixelFormat: "yuv420p",
|
|
56
|
+
x264Preset: "slow",
|
|
57
|
+
imageFormat: "png",
|
|
58
|
+
});
|
|
59
|
+
return resolvedOutput;
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { VideoProps } from "../data/types";
|
|
3
|
+
export declare const TOTAL_SLIDES = 9;
|
|
4
|
+
export declare const FPS = 30;
|
|
5
|
+
export declare const TOTAL_DURATION: number;
|
|
6
|
+
export declare const WIDTH = 900;
|
|
7
|
+
export declare const HEIGHT = 900;
|
|
8
|
+
export declare const CCWrappedComposition: React.FC<VideoProps>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AbsoluteFill, Sequence, useCurrentFrame, interpolate } from "remotion";
|
|
3
|
+
import { IntroSlide } from "./slides/IntroSlide";
|
|
4
|
+
import { TokensSlide } from "./slides/TokensSlide";
|
|
5
|
+
import { CostSlide } from "./slides/CostSlide";
|
|
6
|
+
import { ModelSlide } from "./slides/ModelSlide";
|
|
7
|
+
import { BusiestDaySlide } from "./slides/BusiestDaySlide";
|
|
8
|
+
import { SessionSlide } from "./slides/SessionSlide";
|
|
9
|
+
import { PeakHoursSlide } from "./slides/PeakHoursSlide";
|
|
10
|
+
import { ArchetypeSlide } from "./slides/ArchetypeSlide";
|
|
11
|
+
import { SummarySlide } from "./slides/SummarySlide";
|
|
12
|
+
// Timing (at 30fps)
|
|
13
|
+
const FADE_IN = 15; // 0.5s fade in
|
|
14
|
+
const READABLE = 180; // 6s fully visible
|
|
15
|
+
const FADE_OUT = 15; // 0.5s fade out
|
|
16
|
+
const SLIDE_CONTENT = FADE_IN + READABLE + FADE_OUT; // 5s total per slide
|
|
17
|
+
const BLACK_PAUSE = 25; // ~0.83s black between slides
|
|
18
|
+
const SLIDE_SLOT = SLIDE_CONTENT + BLACK_PAUSE; // full slot per slide
|
|
19
|
+
export const TOTAL_SLIDES = 9;
|
|
20
|
+
export const FPS = 30;
|
|
21
|
+
// Last slide has no black pause after it
|
|
22
|
+
export const TOTAL_DURATION = SLIDE_SLOT * TOTAL_SLIDES - BLACK_PAUSE;
|
|
23
|
+
// 900x900: renders sharp, displays crisp at 300x300 (3x density)
|
|
24
|
+
export const WIDTH = 900;
|
|
25
|
+
export const HEIGHT = 900;
|
|
26
|
+
const SlideWithTransition = ({ children, index }) => {
|
|
27
|
+
const frame = useCurrentFrame();
|
|
28
|
+
const fadeIn = interpolate(frame, [0, FADE_IN], [0, 1], {
|
|
29
|
+
extrapolateLeft: "clamp",
|
|
30
|
+
extrapolateRight: "clamp",
|
|
31
|
+
});
|
|
32
|
+
const fadeOut = index < TOTAL_SLIDES - 1
|
|
33
|
+
? interpolate(frame, [FADE_IN + READABLE, SLIDE_CONTENT], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
|
|
34
|
+
: 1; // last slide stays visible
|
|
35
|
+
return (_jsx(AbsoluteFill, { style: { opacity: fadeIn * fadeOut }, children: children }));
|
|
36
|
+
};
|
|
37
|
+
export const CCWrappedComposition = ({ stats, commentary, mode }) => {
|
|
38
|
+
const slides = [
|
|
39
|
+
_jsx(IntroSlide, { mode: mode, periodLabel: stats.periodLabel }),
|
|
40
|
+
_jsx(TokensSlide, { stats: stats, commentary: commentary.tokensLine, mode: mode }),
|
|
41
|
+
_jsx(CostSlide, { stats: stats, commentary: commentary.costLine, mode: mode }),
|
|
42
|
+
_jsx(ModelSlide, { stats: stats, commentary: commentary.modelLine, mode: mode }),
|
|
43
|
+
_jsx(BusiestDaySlide, { stats: stats, commentary: commentary.busiestDayLine, mode: mode }),
|
|
44
|
+
_jsx(SessionSlide, { stats: stats, commentary: commentary.sessionLine, mode: mode }),
|
|
45
|
+
_jsx(PeakHoursSlide, { stats: stats, commentary: commentary.peakHoursLine, mode: mode }),
|
|
46
|
+
_jsx(ArchetypeSlide, { stats: stats, commentary: commentary.archetypeLine, mode: mode }),
|
|
47
|
+
_jsx(SummarySlide, { stats: stats, commentary: commentary.summaryLine, mode: mode }),
|
|
48
|
+
];
|
|
49
|
+
return (_jsx(AbsoluteFill, { style: { backgroundColor: mode === "dark" ? "#050505" : "#0a0a0f" }, children: slides.map((slide, i) => (_jsx(Sequence, { from: i * SLIDE_SLOT, durationInFrames: SLIDE_CONTENT, name: `Slide ${i + 1}`, children: _jsx(SlideWithTransition, { index: i, children: slide }) }, i))) }));
|
|
50
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Composition } from "remotion";
|
|
3
|
+
import { CCWrappedComposition, TOTAL_DURATION, FPS, WIDTH, HEIGHT } from "./Composition";
|
|
4
|
+
// Default stats for preview mode
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
mode: "sassy",
|
|
7
|
+
commentary: {
|
|
8
|
+
tokensLine: "That's a LOT of tokens bestie. Your AI dependency is showing.",
|
|
9
|
+
costLine: "You could've bought something nice. Instead you bought robot words.",
|
|
10
|
+
modelLine: "Sonnet? Balanced choice. Perfectly mid in the best way.",
|
|
11
|
+
busiestDayLine: "Valentine's Day?! You spent it with Claude?! We need to talk.",
|
|
12
|
+
sessionLine: "8.5 hours straight. Your chiropractor sends their regards.",
|
|
13
|
+
peakHoursLine: "10pm peak hour. The demons come out at night and they want code reviews.",
|
|
14
|
+
archetypeLine: "Night Owl Demon. The bags under your eyes have their own zip code.",
|
|
15
|
+
summaryLine: "Outsourcing your thinking at scale. Honestly? Respect.",
|
|
16
|
+
},
|
|
17
|
+
stats: {
|
|
18
|
+
periodLabel: "Your month",
|
|
19
|
+
totalTokens: 42_690_000,
|
|
20
|
+
totalInputTokens: 35_000_000,
|
|
21
|
+
totalOutputTokens: 7_690_000,
|
|
22
|
+
totalCost: 247.83,
|
|
23
|
+
totalSessions: 156,
|
|
24
|
+
totalMessages: 3847,
|
|
25
|
+
totalDaysActive: 89,
|
|
26
|
+
firstActivity: new Date("2025-06-01"),
|
|
27
|
+
lastActivity: new Date("2026-03-15"),
|
|
28
|
+
busiestDay: { date: "2026-02-14", messageCount: 247, tokenCount: 2_400_000, costUSD: 18.50, sessions: 8 },
|
|
29
|
+
busiestHour: 22,
|
|
30
|
+
longestSession: {
|
|
31
|
+
sessionId: "demo",
|
|
32
|
+
project: "/home/user/megaproject",
|
|
33
|
+
startTime: new Date("2026-02-14T14:00:00"),
|
|
34
|
+
endTime: new Date("2026-02-14T22:30:00"),
|
|
35
|
+
durationMinutes: 510,
|
|
36
|
+
messageCount: 247,
|
|
37
|
+
totalInputTokens: 2_000_000,
|
|
38
|
+
totalOutputTokens: 400_000,
|
|
39
|
+
totalCacheCreationTokens: 500_000,
|
|
40
|
+
totalCacheReadTokens: 300_000,
|
|
41
|
+
totalTokens: 3_200_000,
|
|
42
|
+
models: ["claude-sonnet-4-6"],
|
|
43
|
+
costUSD: 18.50,
|
|
44
|
+
},
|
|
45
|
+
mostActiveProject: "/home/user/megaproject",
|
|
46
|
+
hourlyActivity: Array.from({ length: 24 }, (_, i) => ({
|
|
47
|
+
hour: i,
|
|
48
|
+
messageCount: Math.floor(Math.sin(i * 0.5) * 100 + 120 + (i >= 20 || i <= 2 ? 150 : 0)),
|
|
49
|
+
tokenCount: Math.floor(Math.sin(i * 0.5) * 250000 + 300000 + (i >= 20 || i <= 2 ? 300000 : 0)),
|
|
50
|
+
})),
|
|
51
|
+
dailyActivity: Array.from({ length: 30 }, (_, i) => ({
|
|
52
|
+
date: `2026-02-${String(i + 1).padStart(2, "0")}`,
|
|
53
|
+
messageCount: Math.floor(Math.sin(i * 0.3) * 50 + 60),
|
|
54
|
+
tokenCount: Math.floor(Math.sin(i * 0.3) * 500000 + 600000),
|
|
55
|
+
costUSD: Math.sin(i * 0.3) * 10 + 12,
|
|
56
|
+
sessions: Math.floor(Math.sin(i * 0.3) * 2 + 3),
|
|
57
|
+
})),
|
|
58
|
+
modelUsage: [
|
|
59
|
+
{ model: "claude-sonnet-4-6", tokenCount: 30_000_000, messageCount: 2500, costUSD: 150, percentage: 70 },
|
|
60
|
+
{ model: "claude-opus-4-6", tokenCount: 10_000_000, messageCount: 800, costUSD: 80, percentage: 23 },
|
|
61
|
+
{ model: "claude-haiku-4-5-20251001", tokenCount: 2_690_000, messageCount: 547, costUSD: 17, percentage: 7 },
|
|
62
|
+
],
|
|
63
|
+
tokensPerDay: 479_663,
|
|
64
|
+
avgSessionMinutes: 45,
|
|
65
|
+
nightOwlScore: 68,
|
|
66
|
+
weekendWarriorScore: 23,
|
|
67
|
+
streakDays: 12,
|
|
68
|
+
favoriteModel: "claude-sonnet-4-6",
|
|
69
|
+
archetype: "The Night Owl Demon",
|
|
70
|
+
archetypeDescription: "You code when the world sleeps. Your commit history is a horror movie for morning people.",
|
|
71
|
+
warAndPeaceEquivalent: 73.6,
|
|
72
|
+
coffeeEquivalent: 50,
|
|
73
|
+
marathonEquivalent: 28.3,
|
|
74
|
+
waterLiters: 21.3,
|
|
75
|
+
co2Grams: 12807,
|
|
76
|
+
kwhUsed: 42.7,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export const RemotionRoot = () => {
|
|
80
|
+
return (_jsx(_Fragment, { children: _jsx(Composition, { id: "CCWrapped", component: CCWrappedComposition, durationInFrames: TOTAL_DURATION, fps: FPS, width: WIDTH, height: HEIGHT, defaultProps: defaultProps }) }));
|
|
81
|
+
};
|