@dylan_jser/claude-code-wrapped 1.0.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 +94 -0
- package/dist/cli.js +1669 -0
- package/dist/index.js +1620 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
// src/collector.ts
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path2 from "path";
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
|
|
6
|
+
// src/paths.ts
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
function getClaudeDataDir() {
|
|
11
|
+
const platform2 = os.platform();
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
if (platform2 === "win32") {
|
|
14
|
+
const appDataPath = process.env.APPDATA;
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
appDataPath ? path.join(appDataPath, "claude") : null,
|
|
17
|
+
appDataPath ? path.join(appDataPath, "Claude") : null,
|
|
18
|
+
path.join(homeDir, ".claude"),
|
|
19
|
+
path.join(homeDir, "AppData", "Roaming", "claude"),
|
|
20
|
+
path.join(homeDir, "AppData", "Local", "claude")
|
|
21
|
+
].filter(Boolean);
|
|
22
|
+
for (const p of possiblePaths) {
|
|
23
|
+
if (fs.existsSync(p)) {
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return path.join(homeDir, ".claude");
|
|
28
|
+
}
|
|
29
|
+
return path.join(homeDir, ".claude");
|
|
30
|
+
}
|
|
31
|
+
function getClaudeDataPaths() {
|
|
32
|
+
const root = getClaudeDataDir();
|
|
33
|
+
return {
|
|
34
|
+
root,
|
|
35
|
+
statsCache: path.join(root, "stats-cache.json"),
|
|
36
|
+
history: path.join(root, "history.jsonl"),
|
|
37
|
+
projects: path.join(root, "projects"),
|
|
38
|
+
telemetry: path.join(root, "telemetry"),
|
|
39
|
+
todos: path.join(root, "todos")
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function hasClaudeData() {
|
|
43
|
+
const paths = getClaudeDataPaths();
|
|
44
|
+
return fs.existsSync(paths.root) && fs.existsSync(paths.statsCache);
|
|
45
|
+
}
|
|
46
|
+
function validateClaudeData() {
|
|
47
|
+
const paths = getClaudeDataPaths();
|
|
48
|
+
const errors = [];
|
|
49
|
+
if (!fs.existsSync(paths.root)) {
|
|
50
|
+
errors.push(`Claude data directory not found: ${paths.root}`);
|
|
51
|
+
}
|
|
52
|
+
if (!fs.existsSync(paths.statsCache)) {
|
|
53
|
+
errors.push(`Stats cache file not found: ${paths.statsCache}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
valid: errors.length === 0,
|
|
57
|
+
paths,
|
|
58
|
+
errors
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/collector.ts
|
|
63
|
+
async function readStatsCache() {
|
|
64
|
+
const paths = getClaudeDataPaths();
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs2.promises.readFile(paths.statsCache, "utf-8");
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function readHistory() {
|
|
73
|
+
const paths = getClaudeDataPaths();
|
|
74
|
+
const entries = [];
|
|
75
|
+
try {
|
|
76
|
+
if (!fs2.existsSync(paths.history)) {
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
79
|
+
const fileStream = fs2.createReadStream(paths.history);
|
|
80
|
+
const rl = readline.createInterface({
|
|
81
|
+
input: fileStream,
|
|
82
|
+
crlfDelay: Infinity
|
|
83
|
+
});
|
|
84
|
+
for await (const line of rl) {
|
|
85
|
+
if (line.trim()) {
|
|
86
|
+
try {
|
|
87
|
+
entries.push(JSON.parse(line));
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
96
|
+
async function getProjectStats() {
|
|
97
|
+
const paths = getClaudeDataPaths();
|
|
98
|
+
const projects = [];
|
|
99
|
+
try {
|
|
100
|
+
if (!fs2.existsSync(paths.projects)) {
|
|
101
|
+
return projects;
|
|
102
|
+
}
|
|
103
|
+
const projectDirs = await fs2.promises.readdir(paths.projects);
|
|
104
|
+
for (const dir of projectDirs) {
|
|
105
|
+
if (dir.startsWith(".")) continue;
|
|
106
|
+
const projectPath = path2.join(paths.projects, dir);
|
|
107
|
+
const stat = await fs2.promises.stat(projectPath);
|
|
108
|
+
if (!stat.isDirectory()) continue;
|
|
109
|
+
const files = await fs2.promises.readdir(projectPath);
|
|
110
|
+
const sessionFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
111
|
+
const decodedPath = dir.replace(/-/g, "/");
|
|
112
|
+
let messageCount = 0;
|
|
113
|
+
const sampleFiles = sessionFiles.slice(0, 5);
|
|
114
|
+
for (const file of sampleFiles) {
|
|
115
|
+
try {
|
|
116
|
+
const filePath = path2.join(projectPath, file);
|
|
117
|
+
const content = await fs2.promises.readFile(filePath, "utf-8");
|
|
118
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
119
|
+
messageCount += lines.length;
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (sampleFiles.length > 0 && sessionFiles.length > sampleFiles.length) {
|
|
124
|
+
const avgPerFile = messageCount / sampleFiles.length;
|
|
125
|
+
messageCount = Math.round(avgPerFile * sessionFiles.length);
|
|
126
|
+
}
|
|
127
|
+
projects.push({
|
|
128
|
+
name: extractProjectName(decodedPath),
|
|
129
|
+
path: decodedPath,
|
|
130
|
+
sessionCount: sessionFiles.length,
|
|
131
|
+
messageCount
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
projects.sort((a, b) => b.sessionCount - a.sessionCount);
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
return projects;
|
|
138
|
+
}
|
|
139
|
+
function extractProjectName(projectPath) {
|
|
140
|
+
const parts = projectPath.split("/").filter(Boolean);
|
|
141
|
+
const name = parts[parts.length - 1] || "Unknown";
|
|
142
|
+
return name;
|
|
143
|
+
}
|
|
144
|
+
function filterFor2025(dailyActivity) {
|
|
145
|
+
return dailyActivity.filter((d) => d.date.startsWith("2025"));
|
|
146
|
+
}
|
|
147
|
+
function calculateTotalTokens(modelUsage) {
|
|
148
|
+
let total = 0;
|
|
149
|
+
for (const usage of Object.values(modelUsage)) {
|
|
150
|
+
total += usage.inputTokens + usage.outputTokens;
|
|
151
|
+
}
|
|
152
|
+
return total;
|
|
153
|
+
}
|
|
154
|
+
function getTokensByModel(modelUsage) {
|
|
155
|
+
const result = {};
|
|
156
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
157
|
+
const total = usage.inputTokens + usage.outputTokens + (usage.cacheReadInputTokens || 0) + (usage.cacheCreationInputTokens || 0);
|
|
158
|
+
const normalizedName = normalizeModelName(model);
|
|
159
|
+
result[normalizedName] = (result[normalizedName] || 0) + total;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
var MODEL_PRICING = {
|
|
164
|
+
"claude-opus-4-5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
165
|
+
"claude-opus-4-1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
166
|
+
"claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
167
|
+
"claude-sonnet-4-1": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
168
|
+
"claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
|
169
|
+
"claude-haiku": { input: 0.25, output: 1.25, cacheRead: 0.025, cacheWrite: 0.3 },
|
|
170
|
+
"default": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }
|
|
171
|
+
};
|
|
172
|
+
function getModelPricing(model) {
|
|
173
|
+
const lowerModel = model.toLowerCase();
|
|
174
|
+
if (lowerModel.includes("opus-4-5") || lowerModel.includes("opus-4.5")) {
|
|
175
|
+
return MODEL_PRICING["claude-opus-4-5"];
|
|
176
|
+
}
|
|
177
|
+
if (lowerModel.includes("opus-4-1") || lowerModel.includes("opus-4.1") || lowerModel.includes("opus")) {
|
|
178
|
+
return MODEL_PRICING["claude-opus-4-1"];
|
|
179
|
+
}
|
|
180
|
+
if (lowerModel.includes("sonnet-4-5") || lowerModel.includes("sonnet-4.5") || lowerModel.includes("sonnet")) {
|
|
181
|
+
return MODEL_PRICING["claude-sonnet-4-5"];
|
|
182
|
+
}
|
|
183
|
+
if (lowerModel.includes("haiku-4-5") || lowerModel.includes("haiku-4.5")) {
|
|
184
|
+
return MODEL_PRICING["claude-haiku-4-5"];
|
|
185
|
+
}
|
|
186
|
+
if (lowerModel.includes("haiku")) {
|
|
187
|
+
return MODEL_PRICING["claude-haiku"];
|
|
188
|
+
}
|
|
189
|
+
return MODEL_PRICING["default"];
|
|
190
|
+
}
|
|
191
|
+
function calculateEstimatedCost(modelUsage) {
|
|
192
|
+
let totalCost = 0;
|
|
193
|
+
const costByModel = {};
|
|
194
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
195
|
+
const pricing = getModelPricing(model);
|
|
196
|
+
const normalizedName = normalizeModelName(model);
|
|
197
|
+
const inputCost = usage.inputTokens / 1e6 * pricing.input;
|
|
198
|
+
const outputCost = usage.outputTokens / 1e6 * pricing.output;
|
|
199
|
+
const cacheReadCost = (usage.cacheReadInputTokens || 0) / 1e6 * pricing.cacheRead;
|
|
200
|
+
const cacheWriteCost = (usage.cacheCreationInputTokens || 0) / 1e6 * pricing.cacheWrite;
|
|
201
|
+
const modelCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
202
|
+
costByModel[normalizedName] = (costByModel[normalizedName] || 0) + modelCost;
|
|
203
|
+
totalCost += modelCost;
|
|
204
|
+
}
|
|
205
|
+
return { totalCost, costByModel };
|
|
206
|
+
}
|
|
207
|
+
function normalizeModelName(model) {
|
|
208
|
+
if (model.includes("opus-4-5") || model.includes("opus-4.5")) {
|
|
209
|
+
return "Claude Opus 4.5";
|
|
210
|
+
}
|
|
211
|
+
if (model.includes("opus-4-1") || model.includes("opus-4.1")) {
|
|
212
|
+
return "Claude Opus 4.1";
|
|
213
|
+
}
|
|
214
|
+
if (model.includes("sonnet-4-5") || model.includes("sonnet-4.5")) {
|
|
215
|
+
return "Claude Sonnet 4.5";
|
|
216
|
+
}
|
|
217
|
+
if (model.includes("sonnet-4-1") || model.includes("sonnet-4.1")) {
|
|
218
|
+
return "Claude Sonnet 4.1";
|
|
219
|
+
}
|
|
220
|
+
if (model.includes("haiku-4-5") || model.includes("haiku-4.5")) {
|
|
221
|
+
return "Claude Haiku 4.5";
|
|
222
|
+
}
|
|
223
|
+
if (model.includes("haiku")) {
|
|
224
|
+
return "Claude Haiku";
|
|
225
|
+
}
|
|
226
|
+
if (model.includes("sonnet")) {
|
|
227
|
+
return "Claude Sonnet";
|
|
228
|
+
}
|
|
229
|
+
if (model.includes("opus")) {
|
|
230
|
+
return "Claude Opus";
|
|
231
|
+
}
|
|
232
|
+
return model;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/i18n.ts
|
|
236
|
+
var en = {
|
|
237
|
+
// CLI
|
|
238
|
+
analyzing: "Analyzing your Claude Code journey...",
|
|
239
|
+
analysisComplete: "Analysis complete!",
|
|
240
|
+
generatingHtml: "Generating HTML report...",
|
|
241
|
+
htmlGenerated: "HTML report generated!",
|
|
242
|
+
openingBrowser: "Opening report in browser...",
|
|
243
|
+
reportSaved: "Report saved to:",
|
|
244
|
+
checkingPuppeteer: "Checking Puppeteer availability...",
|
|
245
|
+
puppeteerNotAvailable: "Puppeteer not available. PNG export requires a browser.",
|
|
246
|
+
exportHint: "You can still export by opening the HTML and using the EXPORT_PNG button.",
|
|
247
|
+
generatingPng: "Generating PNG export...",
|
|
248
|
+
pngExported: "PNG exported!",
|
|
249
|
+
pngSaved: "PNG saved to:",
|
|
250
|
+
thankYou: "Thank you for using Claude Code in 2025!",
|
|
251
|
+
dataNotFound: "Claude Code data not found!",
|
|
252
|
+
expectedLocation: "Expected location:",
|
|
253
|
+
installHint: "Make sure you have Claude Code installed and have used it at least once.",
|
|
254
|
+
// Stats labels
|
|
255
|
+
totalSessions: "Total_Sessions",
|
|
256
|
+
activeDays: "Active_Days",
|
|
257
|
+
peakMessages: "Peak_Messages",
|
|
258
|
+
toolCalls: "Tool_Calls",
|
|
259
|
+
totalMessages: "total messages",
|
|
260
|
+
tokens: "TOKENS",
|
|
261
|
+
primaryModel: "Primary Model",
|
|
262
|
+
level: "LEVEL",
|
|
263
|
+
cumulativeIntelligence: "CUMULATIVE_INTELLIGENCE",
|
|
264
|
+
activityDistribution: "ACTIVITY_DISTRIBUTION",
|
|
265
|
+
timeZone: "TIME_ZONE: LOCAL",
|
|
266
|
+
activeRegistry: "Active_Registry_2025",
|
|
267
|
+
totalProjects: "Total Projects",
|
|
268
|
+
longestStreak: "Longest_Streak",
|
|
269
|
+
currentStreak: "Current_Streak",
|
|
270
|
+
avgMsgsPerSession: "Avg_Msgs/Session",
|
|
271
|
+
days: "DAYS",
|
|
272
|
+
// Time ranges
|
|
273
|
+
morning: "09:00-12:00",
|
|
274
|
+
afternoon: "13:00-18:00",
|
|
275
|
+
evening: "19:00-23:00",
|
|
276
|
+
night: "00:00-08:00",
|
|
277
|
+
// Titles
|
|
278
|
+
identified: "IDENTIFIED",
|
|
279
|
+
neuralLinkEstablished: "Neural Link Established",
|
|
280
|
+
marathonSession: "Your {duration} marathon session ({messages} messages) has been archived in the central neural core.",
|
|
281
|
+
systemRecognizes: "The system recognizes your persistence.",
|
|
282
|
+
terminalReady: "TERMINAL_READY",
|
|
283
|
+
sysRef: "SYS_REF: 0xFF-2025-RECAP",
|
|
284
|
+
// User titles
|
|
285
|
+
titles: {
|
|
286
|
+
neuralArchitect: "Neural Architect",
|
|
287
|
+
prolificArchitect: "Prolific Architect",
|
|
288
|
+
seniorCollaborator: "Senior Collaborator",
|
|
289
|
+
codeArtisan: "Code Artisan",
|
|
290
|
+
digitalCraftsman: "Digital Craftsman",
|
|
291
|
+
codeApprentice: "Code Apprentice"
|
|
292
|
+
},
|
|
293
|
+
// Levels
|
|
294
|
+
levels: {
|
|
295
|
+
legendary: "LEGENDARY",
|
|
296
|
+
master: "MASTER",
|
|
297
|
+
expert: "EXPERT",
|
|
298
|
+
advanced: "ADVANCED",
|
|
299
|
+
intermediate: "INTERMEDIATE",
|
|
300
|
+
novice: "NOVICE"
|
|
301
|
+
},
|
|
302
|
+
// Messages
|
|
303
|
+
collaborationDensity: "Your collaboration density exceeds standard protocols.",
|
|
304
|
+
exceededProtocols: "Total messages exchanged with core intelligence.",
|
|
305
|
+
// HTML labels
|
|
306
|
+
overview: "Overview",
|
|
307
|
+
totalTokens: "Total Tokens",
|
|
308
|
+
projects: "Projects",
|
|
309
|
+
impact: "Impact",
|
|
310
|
+
messagesWithClaude: "Total messages exchanged with Claude",
|
|
311
|
+
hourlyActivity: "Hourly Activity",
|
|
312
|
+
weeklyPattern: "Weekly Pattern",
|
|
313
|
+
modelUsage: "Model Usage",
|
|
314
|
+
topTools: "Top Tools",
|
|
315
|
+
costEstimate: "Cost Estimate",
|
|
316
|
+
achievements: "Achievements",
|
|
317
|
+
basedOnApi: "Based on API pricing"
|
|
318
|
+
};
|
|
319
|
+
var zh = {
|
|
320
|
+
// CLI
|
|
321
|
+
analyzing: "\u6B63\u5728\u5206\u6790\u4F60\u7684 Claude Code \u4E4B\u65C5...",
|
|
322
|
+
analysisComplete: "\u5206\u6790\u5B8C\u6210\uFF01",
|
|
323
|
+
generatingHtml: "\u6B63\u5728\u751F\u6210 HTML \u62A5\u544A...",
|
|
324
|
+
htmlGenerated: "HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF01",
|
|
325
|
+
openingBrowser: "\u6B63\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u62A5\u544A...",
|
|
326
|
+
reportSaved: "\u62A5\u544A\u5DF2\u4FDD\u5B58\u81F3:",
|
|
327
|
+
checkingPuppeteer: "\u6B63\u5728\u68C0\u67E5 Puppeteer \u53EF\u7528\u6027...",
|
|
328
|
+
puppeteerNotAvailable: "Puppeteer \u4E0D\u53EF\u7528\uFF0CPNG \u5BFC\u51FA\u9700\u8981\u6D4F\u89C8\u5668\u652F\u6301\u3002",
|
|
329
|
+
exportHint: "\u4F60\u4ECD\u53EF\u4EE5\u6253\u5F00 HTML \u5E76\u4F7F\u7528 EXPORT_PNG \u6309\u94AE\u5BFC\u51FA\u3002",
|
|
330
|
+
generatingPng: "\u6B63\u5728\u751F\u6210 PNG \u5BFC\u51FA...",
|
|
331
|
+
pngExported: "PNG \u5DF2\u5BFC\u51FA\uFF01",
|
|
332
|
+
pngSaved: "PNG \u5DF2\u4FDD\u5B58\u81F3:",
|
|
333
|
+
thankYou: "\u611F\u8C22\u4F60\u5728 2025 \u5E74\u4F7F\u7528 Claude Code\uFF01",
|
|
334
|
+
dataNotFound: "\u672A\u627E\u5230 Claude Code \u6570\u636E\uFF01",
|
|
335
|
+
expectedLocation: "\u9884\u671F\u4F4D\u7F6E:",
|
|
336
|
+
installHint: "\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5 Claude Code \u5E76\u81F3\u5C11\u4F7F\u7528\u8FC7\u4E00\u6B21\u3002",
|
|
337
|
+
// Stats labels
|
|
338
|
+
totalSessions: "\u603B\u4F1A\u8BDD\u6570",
|
|
339
|
+
activeDays: "\u6D3B\u8DC3\u5929\u6570",
|
|
340
|
+
peakMessages: "\u5355\u65E5\u5CF0\u503C",
|
|
341
|
+
toolCalls: "\u5DE5\u5177\u8C03\u7528",
|
|
342
|
+
totalMessages: "\u6761\u6D88\u606F\u4EA4\u4E92",
|
|
343
|
+
tokens: "TOKENS",
|
|
344
|
+
primaryModel: "\u4E3B\u529B\u6A21\u578B",
|
|
345
|
+
level: "\u7B49\u7EA7",
|
|
346
|
+
cumulativeIntelligence: "\u7D2F\u8BA1\u667A\u80FD\u6D88\u8017",
|
|
347
|
+
activityDistribution: "\u6D3B\u52A8\u65F6\u95F4\u5206\u5E03",
|
|
348
|
+
timeZone: "\u65F6\u533A: \u672C\u5730",
|
|
349
|
+
activeRegistry: "2025 \u9879\u76EE\u6863\u6848",
|
|
350
|
+
totalProjects: "\u9879\u76EE\u603B\u6570",
|
|
351
|
+
longestStreak: "\u6700\u957F\u8FDE\u7EED",
|
|
352
|
+
currentStreak: "\u5F53\u524D\u8FDE\u7EED",
|
|
353
|
+
avgMsgsPerSession: "\u573A\u5747\u6D88\u606F",
|
|
354
|
+
days: "\u5929",
|
|
355
|
+
// Time ranges
|
|
356
|
+
morning: "\u4E0A\u5348 09-12",
|
|
357
|
+
afternoon: "\u4E0B\u5348 13-18",
|
|
358
|
+
evening: "\u665A\u95F4 19-23",
|
|
359
|
+
night: "\u6DF1\u591C 00-08",
|
|
360
|
+
// Titles
|
|
361
|
+
identified: "\u8EAB\u4EFD\u8BC6\u522B",
|
|
362
|
+
neuralLinkEstablished: "\u795E\u7ECF\u94FE\u8DEF\u5EFA\u7ACB\u4E8E",
|
|
363
|
+
marathonSession: "\u4F60\u90A3\u573A {duration} \u7684\u9A6C\u62C9\u677E\u4F1A\u8BDD\uFF08{messages} \u6761\u6D88\u606F\uFF09\u5DF2\u88AB\u5F52\u6863\u81F3\u4E2D\u592E\u795E\u7ECF\u6838\u5FC3\u3002",
|
|
364
|
+
systemRecognizes: "\u7CFB\u7EDF\u8BA4\u53EF\u4F60\u7684\u575A\u6301\u3002",
|
|
365
|
+
terminalReady: "\u7EC8\u7AEF\u5C31\u7EEA",
|
|
366
|
+
sysRef: "\u7CFB\u7EDF\u53C2\u8003: 0xFF-2025-\u56DE\u987E",
|
|
367
|
+
// User titles
|
|
368
|
+
titles: {
|
|
369
|
+
neuralArchitect: "\u795E\u7ECF\u67B6\u6784\u5E08",
|
|
370
|
+
prolificArchitect: "\u9AD8\u4EA7\u67B6\u6784\u5E08",
|
|
371
|
+
seniorCollaborator: "\u8D44\u6DF1\u534F\u4F5C\u8005",
|
|
372
|
+
codeArtisan: "\u4EE3\u7801\u5DE5\u5320",
|
|
373
|
+
digitalCraftsman: "\u6570\u5B57\u5320\u4EBA",
|
|
374
|
+
codeApprentice: "\u4EE3\u7801\u5B66\u5F92"
|
|
375
|
+
},
|
|
376
|
+
// Levels
|
|
377
|
+
levels: {
|
|
378
|
+
legendary: "\u4F20\u5947",
|
|
379
|
+
master: "\u5927\u5E08",
|
|
380
|
+
expert: "\u4E13\u5BB6",
|
|
381
|
+
advanced: "\u8FDB\u9636",
|
|
382
|
+
intermediate: "\u4E2D\u7EA7",
|
|
383
|
+
novice: "\u65B0\u624B"
|
|
384
|
+
},
|
|
385
|
+
// Messages
|
|
386
|
+
collaborationDensity: "\u4F60\u7684\u534F\u4F5C\u5F3A\u5EA6\u8D85\u8D8A\u4E86\u6807\u51C6\u534F\u8BAE\u3002",
|
|
387
|
+
exceededProtocols: "\u4E0E\u6838\u5FC3\u667A\u80FD\u4EA4\u6362\u7684\u6D88\u606F\u603B\u6570\u3002",
|
|
388
|
+
// HTML labels
|
|
389
|
+
overview: "\u6982\u89C8",
|
|
390
|
+
totalTokens: "\u603BToken",
|
|
391
|
+
projects: "\u9879\u76EE\u6570",
|
|
392
|
+
impact: "\u5F71\u54CD\u529B",
|
|
393
|
+
messagesWithClaude: "\u4E0E Claude \u4EA4\u6362\u7684\u6D88\u606F\u603B\u6570",
|
|
394
|
+
hourlyActivity: "\u65F6\u95F4\u5206\u5E03",
|
|
395
|
+
weeklyPattern: "\u5468\u6D3B\u52A8",
|
|
396
|
+
modelUsage: "\u6A21\u578B\u4F7F\u7528",
|
|
397
|
+
topTools: "\u5E38\u7528\u5DE5\u5177",
|
|
398
|
+
costEstimate: "\u6210\u672C\u4F30\u7B97",
|
|
399
|
+
achievements: "\u6210\u5C31",
|
|
400
|
+
basedOnApi: "\u57FA\u4E8EAPI\u5B9A\u4EF7"
|
|
401
|
+
};
|
|
402
|
+
var translations = { en, zh };
|
|
403
|
+
var currentLanguage = "en";
|
|
404
|
+
function detectLanguage() {
|
|
405
|
+
const locale = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
|
|
406
|
+
if (locale.toLowerCase().startsWith("zh")) {
|
|
407
|
+
return "zh";
|
|
408
|
+
}
|
|
409
|
+
return "en";
|
|
410
|
+
}
|
|
411
|
+
function setLanguage(lang) {
|
|
412
|
+
currentLanguage = lang;
|
|
413
|
+
}
|
|
414
|
+
function getLanguage() {
|
|
415
|
+
return currentLanguage;
|
|
416
|
+
}
|
|
417
|
+
function t() {
|
|
418
|
+
return translations[currentLanguage];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/analyzer.ts
|
|
422
|
+
async function analyzeData() {
|
|
423
|
+
const statsCache = await readStatsCache();
|
|
424
|
+
const history = await readHistory();
|
|
425
|
+
const projects = await getProjectStats();
|
|
426
|
+
if (!statsCache) {
|
|
427
|
+
throw new Error("Could not read Claude Code stats cache");
|
|
428
|
+
}
|
|
429
|
+
const dailyActivity2025 = filterFor2025(statsCache.dailyActivity);
|
|
430
|
+
const totalMessages = dailyActivity2025.reduce(
|
|
431
|
+
(sum, d) => sum + d.messageCount,
|
|
432
|
+
0
|
|
433
|
+
);
|
|
434
|
+
const totalSessions = dailyActivity2025.reduce(
|
|
435
|
+
(sum, d) => sum + d.sessionCount,
|
|
436
|
+
0
|
|
437
|
+
);
|
|
438
|
+
const totalToolCalls = dailyActivity2025.reduce(
|
|
439
|
+
(sum, d) => sum + d.toolCallCount,
|
|
440
|
+
0
|
|
441
|
+
);
|
|
442
|
+
const activeDays = dailyActivity2025.length;
|
|
443
|
+
const dates = dailyActivity2025.map((d) => new Date(d.date)).sort((a, b) => a.getTime() - b.getTime());
|
|
444
|
+
const firstSessionDate = dates[0] || /* @__PURE__ */ new Date();
|
|
445
|
+
const lastSessionDate = dates[dates.length - 1] || /* @__PURE__ */ new Date();
|
|
446
|
+
const totalTokens = calculateTotalTokens(statsCache.modelUsage);
|
|
447
|
+
const tokensByModel = getTokensByModel(statsCache.modelUsage);
|
|
448
|
+
const primaryModel = findPrimaryModel(tokensByModel);
|
|
449
|
+
const { totalCost, costByModel } = calculateEstimatedCost(statsCache.modelUsage);
|
|
450
|
+
const hourlyDistribution = statsCache.hourCounts || {};
|
|
451
|
+
const peakHour = findPeakHour(hourlyDistribution);
|
|
452
|
+
const avgMessagesPerSession = totalSessions > 0 ? Math.round(totalMessages / totalSessions) : 0;
|
|
453
|
+
const avgSessionDuration = statsCache.longestSession ? Math.round(statsCache.longestSession.duration / totalSessions / 1e3 / 60) : 0;
|
|
454
|
+
const { longestStreak, currentStreak } = calculateStreaks(dailyActivity2025);
|
|
455
|
+
const { userTitle, userLevel, achievements } = calculateAchievements({
|
|
456
|
+
totalMessages,
|
|
457
|
+
totalSessions,
|
|
458
|
+
totalToolCalls,
|
|
459
|
+
activeDays,
|
|
460
|
+
longestStreak,
|
|
461
|
+
totalTokens,
|
|
462
|
+
longestSessionMessages: statsCache.longestSession?.messageCount || 0
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
totalSessions,
|
|
466
|
+
totalMessages,
|
|
467
|
+
totalToolCalls,
|
|
468
|
+
activeDays,
|
|
469
|
+
firstSessionDate,
|
|
470
|
+
lastSessionDate,
|
|
471
|
+
totalTokens,
|
|
472
|
+
tokensByModel,
|
|
473
|
+
primaryModel,
|
|
474
|
+
hourlyDistribution: Object.fromEntries(
|
|
475
|
+
Object.entries(hourlyDistribution).map(([k, v]) => [parseInt(k), v])
|
|
476
|
+
),
|
|
477
|
+
peakHour,
|
|
478
|
+
dailyActivity: dailyActivity2025,
|
|
479
|
+
longestSession: statsCache.longestSession,
|
|
480
|
+
avgMessagesPerSession,
|
|
481
|
+
avgSessionDuration,
|
|
482
|
+
projects,
|
|
483
|
+
topProjects: projects.slice(0, 8),
|
|
484
|
+
totalProjects: projects.length,
|
|
485
|
+
longestStreak,
|
|
486
|
+
currentStreak,
|
|
487
|
+
userTitle,
|
|
488
|
+
userLevel,
|
|
489
|
+
achievements,
|
|
490
|
+
estimatedCost: totalCost,
|
|
491
|
+
costByModel
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function findPrimaryModel(tokensByModel) {
|
|
495
|
+
let maxTokens = 0;
|
|
496
|
+
let primaryModel = "Claude";
|
|
497
|
+
for (const [model, tokens] of Object.entries(tokensByModel)) {
|
|
498
|
+
if (tokens > maxTokens) {
|
|
499
|
+
maxTokens = tokens;
|
|
500
|
+
primaryModel = model;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return primaryModel;
|
|
504
|
+
}
|
|
505
|
+
function findPeakHour(hourCounts) {
|
|
506
|
+
let maxCount = 0;
|
|
507
|
+
let peakHour = 12;
|
|
508
|
+
for (const [hour, count] of Object.entries(hourCounts)) {
|
|
509
|
+
if (count > maxCount) {
|
|
510
|
+
maxCount = count;
|
|
511
|
+
peakHour = parseInt(hour);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return peakHour;
|
|
515
|
+
}
|
|
516
|
+
function calculateStreaks(dailyActivity) {
|
|
517
|
+
if (dailyActivity.length === 0) {
|
|
518
|
+
return { longestStreak: 0, currentStreak: 0 };
|
|
519
|
+
}
|
|
520
|
+
const dates = dailyActivity.map((d) => d.date).sort().map((d) => new Date(d));
|
|
521
|
+
let longestStreak = 1;
|
|
522
|
+
let currentStreak = 1;
|
|
523
|
+
let tempStreak = 1;
|
|
524
|
+
for (let i = 1; i < dates.length; i++) {
|
|
525
|
+
const diff = (dates[i].getTime() - dates[i - 1].getTime()) / (1e3 * 60 * 60 * 24);
|
|
526
|
+
if (diff === 1) {
|
|
527
|
+
tempStreak++;
|
|
528
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
529
|
+
} else {
|
|
530
|
+
tempStreak = 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const today = /* @__PURE__ */ new Date();
|
|
534
|
+
today.setHours(0, 0, 0, 0);
|
|
535
|
+
const lastDate = dates[dates.length - 1];
|
|
536
|
+
lastDate.setHours(0, 0, 0, 0);
|
|
537
|
+
const daysSinceLastActivity = Math.floor(
|
|
538
|
+
(today.getTime() - lastDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
539
|
+
);
|
|
540
|
+
if (daysSinceLastActivity <= 1) {
|
|
541
|
+
currentStreak = tempStreak;
|
|
542
|
+
} else {
|
|
543
|
+
currentStreak = 0;
|
|
544
|
+
}
|
|
545
|
+
return { longestStreak, currentStreak };
|
|
546
|
+
}
|
|
547
|
+
function calculateAchievements(stats) {
|
|
548
|
+
const i = t();
|
|
549
|
+
const achievements = [];
|
|
550
|
+
let userTitle = i.titles.codeApprentice;
|
|
551
|
+
let userLevel = i.levels.novice;
|
|
552
|
+
if (stats.totalMessages >= 5e4) {
|
|
553
|
+
userTitle = i.titles.neuralArchitect;
|
|
554
|
+
userLevel = i.levels.legendary;
|
|
555
|
+
} else if (stats.totalMessages >= 3e4) {
|
|
556
|
+
userTitle = i.titles.prolificArchitect;
|
|
557
|
+
userLevel = i.levels.master;
|
|
558
|
+
} else if (stats.totalMessages >= 15e3) {
|
|
559
|
+
userTitle = i.titles.seniorCollaborator;
|
|
560
|
+
userLevel = i.levels.expert;
|
|
561
|
+
} else if (stats.totalMessages >= 5e3) {
|
|
562
|
+
userTitle = i.titles.codeArtisan;
|
|
563
|
+
userLevel = i.levels.advanced;
|
|
564
|
+
} else if (stats.totalMessages >= 1e3) {
|
|
565
|
+
userTitle = i.titles.digitalCraftsman;
|
|
566
|
+
userLevel = i.levels.intermediate;
|
|
567
|
+
}
|
|
568
|
+
if (stats.totalMessages >= 1e4) {
|
|
569
|
+
achievements.push("10K_MESSAGES");
|
|
570
|
+
}
|
|
571
|
+
if (stats.totalMessages >= 1e3) {
|
|
572
|
+
achievements.push("1K_MESSAGES");
|
|
573
|
+
}
|
|
574
|
+
if (stats.totalSessions >= 500) {
|
|
575
|
+
achievements.push("500_SESSIONS");
|
|
576
|
+
}
|
|
577
|
+
if (stats.totalSessions >= 100) {
|
|
578
|
+
achievements.push("100_SESSIONS");
|
|
579
|
+
}
|
|
580
|
+
if (stats.activeDays >= 30) {
|
|
581
|
+
achievements.push("MONTHLY_ACTIVE");
|
|
582
|
+
}
|
|
583
|
+
if (stats.activeDays >= 7) {
|
|
584
|
+
achievements.push("WEEKLY_ACTIVE");
|
|
585
|
+
}
|
|
586
|
+
if (stats.longestStreak >= 7) {
|
|
587
|
+
achievements.push("WEEK_STREAK");
|
|
588
|
+
}
|
|
589
|
+
if (stats.longestStreak >= 3) {
|
|
590
|
+
achievements.push("3_DAY_STREAK");
|
|
591
|
+
}
|
|
592
|
+
if (stats.totalToolCalls >= 5e3) {
|
|
593
|
+
achievements.push("TOOL_MASTER");
|
|
594
|
+
}
|
|
595
|
+
if (stats.totalToolCalls >= 1e3) {
|
|
596
|
+
achievements.push("TOOL_USER");
|
|
597
|
+
}
|
|
598
|
+
if (stats.longestSessionMessages >= 1e3) {
|
|
599
|
+
achievements.push("MARATHON_SESSION");
|
|
600
|
+
}
|
|
601
|
+
if (stats.longestSessionMessages >= 500) {
|
|
602
|
+
achievements.push("LONG_SESSION");
|
|
603
|
+
}
|
|
604
|
+
if (stats.totalTokens >= 1e8) {
|
|
605
|
+
achievements.push("100M_TOKENS");
|
|
606
|
+
}
|
|
607
|
+
if (stats.totalTokens >= 1e7) {
|
|
608
|
+
achievements.push("10M_TOKENS");
|
|
609
|
+
}
|
|
610
|
+
return { userTitle, userLevel, achievements };
|
|
611
|
+
}
|
|
612
|
+
function formatNumber(num) {
|
|
613
|
+
if (num >= 1e9) {
|
|
614
|
+
return (num / 1e9).toFixed(1) + "B";
|
|
615
|
+
}
|
|
616
|
+
if (num >= 1e6) {
|
|
617
|
+
return (num / 1e6).toFixed(1) + "M";
|
|
618
|
+
}
|
|
619
|
+
if (num >= 1e3) {
|
|
620
|
+
return (num / 1e3).toFixed(1) + "K";
|
|
621
|
+
}
|
|
622
|
+
return num.toString();
|
|
623
|
+
}
|
|
624
|
+
function formatDuration(ms) {
|
|
625
|
+
const hours = Math.floor(ms / (1e3 * 60 * 60));
|
|
626
|
+
const minutes = Math.floor(ms % (1e3 * 60 * 60) / (1e3 * 60));
|
|
627
|
+
if (hours > 0) {
|
|
628
|
+
return `${hours}h ${minutes}m`;
|
|
629
|
+
}
|
|
630
|
+
return `${minutes}m`;
|
|
631
|
+
}
|
|
632
|
+
function padNumber(num, digits = 6) {
|
|
633
|
+
return num.toString().padStart(digits, "0");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/tui.ts
|
|
637
|
+
import chalk from "chalk";
|
|
638
|
+
import boxen from "boxen";
|
|
639
|
+
import gradient from "gradient-string";
|
|
640
|
+
import Table from "cli-table3";
|
|
641
|
+
var coralGradient = gradient(["#ff7f50", "#ff6347", "#ff4500"]);
|
|
642
|
+
var BANNER = `
|
|
643
|
+
____ _ _ ____ _
|
|
644
|
+
/ ___| | __ _ _ _ __| | ___ / ___|___ __| | ___
|
|
645
|
+
| | | |/ _\` | | | |/ _\` |/ _ \\ | | / _ \\ / _\` |/ _ \\
|
|
646
|
+
| |___| | (_| | |_| | (_| | __/ | |__| (_) | (_| | __/
|
|
647
|
+
\\____|_|\\__,_|\\__,_|\\__,_|\\___| \\____\\___/ \\__,_|\\___|
|
|
648
|
+
WRAPPED // EDITION.2025 // SYNC.SUCCESS
|
|
649
|
+
`;
|
|
650
|
+
function printTUI(stats) {
|
|
651
|
+
const i = t();
|
|
652
|
+
console.clear();
|
|
653
|
+
console.log(coralGradient(BANNER));
|
|
654
|
+
console.log(
|
|
655
|
+
chalk.dim(
|
|
656
|
+
` ${i.neuralLinkEstablished}: ${stats.firstSessionDate.toISOString().split("T")[0]}`
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
console.log();
|
|
660
|
+
printOverviewBox(stats);
|
|
661
|
+
console.log();
|
|
662
|
+
printMainImpact(stats);
|
|
663
|
+
console.log();
|
|
664
|
+
printActivityDistribution(stats);
|
|
665
|
+
console.log();
|
|
666
|
+
printProjects(stats);
|
|
667
|
+
console.log();
|
|
668
|
+
printLongestSession(stats);
|
|
669
|
+
console.log();
|
|
670
|
+
printFooter();
|
|
671
|
+
}
|
|
672
|
+
function printOverviewBox(stats) {
|
|
673
|
+
const i = t();
|
|
674
|
+
const isZh = getLanguage() === "zh";
|
|
675
|
+
const colWidth = isZh ? 18 : 20;
|
|
676
|
+
const table = new Table({
|
|
677
|
+
chars: {
|
|
678
|
+
top: "\u2500",
|
|
679
|
+
"top-mid": "\u252C",
|
|
680
|
+
"top-left": "\u250C",
|
|
681
|
+
"top-right": "\u2510",
|
|
682
|
+
bottom: "\u2500",
|
|
683
|
+
"bottom-mid": "\u2534",
|
|
684
|
+
"bottom-left": "\u2514",
|
|
685
|
+
"bottom-right": "\u2518",
|
|
686
|
+
left: "\u2502",
|
|
687
|
+
"left-mid": "\u251C",
|
|
688
|
+
mid: "\u2500",
|
|
689
|
+
"mid-mid": "\u253C",
|
|
690
|
+
right: "\u2502",
|
|
691
|
+
"right-mid": "\u2524",
|
|
692
|
+
middle: "\u2502"
|
|
693
|
+
},
|
|
694
|
+
style: { head: [], border: ["gray"] },
|
|
695
|
+
colWidths: [colWidth, colWidth, colWidth, colWidth]
|
|
696
|
+
});
|
|
697
|
+
table.push([
|
|
698
|
+
chalk.dim(i.totalSessions.toUpperCase()),
|
|
699
|
+
chalk.dim(i.activeDays.toUpperCase()),
|
|
700
|
+
chalk.dim(i.peakMessages.toUpperCase()),
|
|
701
|
+
chalk.dim(i.toolCalls.toUpperCase())
|
|
702
|
+
]);
|
|
703
|
+
table.push([
|
|
704
|
+
chalk.white.bold(padNumber(stats.totalSessions)),
|
|
705
|
+
chalk.white.bold(padNumber(stats.activeDays)),
|
|
706
|
+
chalk.white.bold(
|
|
707
|
+
padNumber(Math.max(...stats.dailyActivity.map((d) => d.messageCount)))
|
|
708
|
+
),
|
|
709
|
+
chalk.white.bold(padNumber(stats.totalToolCalls))
|
|
710
|
+
]);
|
|
711
|
+
console.log(table.toString());
|
|
712
|
+
}
|
|
713
|
+
function printMainImpact(stats) {
|
|
714
|
+
const i = t();
|
|
715
|
+
const title = chalk.hex("#ff7f50").bold(`${i.identified}: ${stats.userTitle.toUpperCase()}`);
|
|
716
|
+
const messageCount = chalk.white.bold(stats.totalMessages.toLocaleString());
|
|
717
|
+
const tokens = formatNumber(stats.totalTokens);
|
|
718
|
+
console.log(boxen(
|
|
719
|
+
`${title}
|
|
720
|
+
|
|
721
|
+
${chalk.white.bold.underline(messageCount)} ${chalk.dim(i.totalMessages)}
|
|
722
|
+
|
|
723
|
+
` + chalk.dim(`${i.collaborationDensity}
|
|
724
|
+
`) + chalk.dim(`${i.cumulativeIntelligence}: ${chalk.white(tokens)} ${i.tokens}
|
|
725
|
+
`) + chalk.dim(`${i.primaryModel}: ${chalk.white(stats.primaryModel)}
|
|
726
|
+
|
|
727
|
+
`) + chalk.bgHex("#ff7f50").black(` ${i.level}: ${stats.userLevel} `),
|
|
728
|
+
{
|
|
729
|
+
padding: 1,
|
|
730
|
+
margin: 0,
|
|
731
|
+
borderStyle: "double",
|
|
732
|
+
borderColor: "gray"
|
|
733
|
+
}
|
|
734
|
+
));
|
|
735
|
+
}
|
|
736
|
+
function printActivityDistribution(stats) {
|
|
737
|
+
const i = t();
|
|
738
|
+
console.log(chalk.dim("\u2500".repeat(60)));
|
|
739
|
+
console.log(
|
|
740
|
+
chalk.gray(i.activityDistribution) + chalk.dim(" " + i.timeZone)
|
|
741
|
+
);
|
|
742
|
+
console.log(chalk.dim("\u2500".repeat(60)));
|
|
743
|
+
const ranges = [
|
|
744
|
+
{ label: i.night, hours: [0, 1, 2, 3, 4, 5, 6, 7, 8] },
|
|
745
|
+
{ label: i.morning, hours: [9, 10, 11, 12] },
|
|
746
|
+
{ label: i.afternoon, hours: [13, 14, 15, 16, 17, 18] },
|
|
747
|
+
{ label: i.evening, hours: [19, 20, 21, 22, 23] }
|
|
748
|
+
];
|
|
749
|
+
let maxCount = 0;
|
|
750
|
+
const rangeCounts = ranges.map((r) => {
|
|
751
|
+
const count = r.hours.reduce(
|
|
752
|
+
(sum, h) => sum + (stats.hourlyDistribution[h] || 0),
|
|
753
|
+
0
|
|
754
|
+
);
|
|
755
|
+
maxCount = Math.max(maxCount, count);
|
|
756
|
+
return { ...r, count };
|
|
757
|
+
});
|
|
758
|
+
const peakRange = rangeCounts.reduce(
|
|
759
|
+
(max, r) => r.count > max.count ? r : max
|
|
760
|
+
);
|
|
761
|
+
for (const range of rangeCounts) {
|
|
762
|
+
const isPeak = range === peakRange;
|
|
763
|
+
const barLength = maxCount > 0 ? Math.round(range.count / maxCount * 20) : 0;
|
|
764
|
+
const bar = chalk.hex("#ff7f50")(isPeak ? "\u2588" : "\u2593").repeat(barLength) + chalk.dim("\u2591").repeat(20 - barLength);
|
|
765
|
+
const label = isPeak ? chalk.hex("#ff7f50")(range.label.padEnd(12)) : chalk.dim(range.label.padEnd(12));
|
|
766
|
+
const count = isPeak ? chalk.hex("#ff7f50").bold(range.count.toString().padStart(4)) : chalk.dim(range.count.toString().padStart(4));
|
|
767
|
+
console.log(` ${label} ${bar} ${count}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function printProjects(stats) {
|
|
771
|
+
const i = t();
|
|
772
|
+
console.log();
|
|
773
|
+
console.log(
|
|
774
|
+
boxen(chalk.white.bold(i.activeRegistry.toUpperCase()), {
|
|
775
|
+
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
|
776
|
+
borderStyle: "double",
|
|
777
|
+
borderColor: "gray",
|
|
778
|
+
textAlignment: "center"
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
const table = new Table({
|
|
782
|
+
chars: {
|
|
783
|
+
top: "",
|
|
784
|
+
"top-mid": "",
|
|
785
|
+
"top-left": "",
|
|
786
|
+
"top-right": "",
|
|
787
|
+
bottom: "\u2500",
|
|
788
|
+
"bottom-mid": "",
|
|
789
|
+
"bottom-left": "",
|
|
790
|
+
"bottom-right": "",
|
|
791
|
+
left: "",
|
|
792
|
+
"left-mid": "",
|
|
793
|
+
mid: "",
|
|
794
|
+
"mid-mid": "",
|
|
795
|
+
right: "",
|
|
796
|
+
"right-mid": "",
|
|
797
|
+
middle: " "
|
|
798
|
+
},
|
|
799
|
+
style: { head: [], border: ["gray"] },
|
|
800
|
+
colWidths: [30, 10, 30, 10]
|
|
801
|
+
});
|
|
802
|
+
const projects = stats.topProjects.slice(0, 8);
|
|
803
|
+
const rows = [];
|
|
804
|
+
for (let i2 = 0; i2 < projects.length; i2 += 2) {
|
|
805
|
+
const p1 = projects[i2];
|
|
806
|
+
const p2 = projects[i2 + 1];
|
|
807
|
+
const col1 = p1 ? `${chalk.white(truncate(p1.name, 25))}` : "";
|
|
808
|
+
const num1 = p1 ? chalk.dim(padNumber(i2 + 1, 2)) : "";
|
|
809
|
+
const col2 = p2 ? `${chalk.white(truncate(p2.name, 25))}` : "";
|
|
810
|
+
const num2 = p2 ? chalk.dim(padNumber(i2 + 2, 2)) : "";
|
|
811
|
+
rows.push([col1, num1, col2, num2]);
|
|
812
|
+
}
|
|
813
|
+
for (const row of rows) {
|
|
814
|
+
table.push(row);
|
|
815
|
+
}
|
|
816
|
+
console.log(table.toString());
|
|
817
|
+
console.log(chalk.dim(` ${i.totalProjects}: ${stats.totalProjects}`));
|
|
818
|
+
}
|
|
819
|
+
function printLongestSession(stats) {
|
|
820
|
+
const i = t();
|
|
821
|
+
if (!stats.longestSession) return;
|
|
822
|
+
const duration = formatDuration(stats.longestSession.duration);
|
|
823
|
+
const messages = stats.longestSession.messageCount;
|
|
824
|
+
const marathonText = i.marathonSession.replace("{duration}", duration).replace("{messages}", messages.toLocaleString());
|
|
825
|
+
console.log(
|
|
826
|
+
boxen(
|
|
827
|
+
chalk.italic.gray(
|
|
828
|
+
`"${marathonText}
|
|
829
|
+
${i.systemRecognizes}"`
|
|
830
|
+
),
|
|
831
|
+
{
|
|
832
|
+
padding: 1,
|
|
833
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 },
|
|
834
|
+
borderStyle: "round",
|
|
835
|
+
borderColor: "#ff7f50"
|
|
836
|
+
}
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
function printFooter() {
|
|
841
|
+
const i = t();
|
|
842
|
+
console.log();
|
|
843
|
+
console.log(
|
|
844
|
+
chalk.dim(i.sysRef) + " " + chalk.hex("#ff7f50")(i.terminalReady) + chalk.dim(" (C) CLAUDE-CODE-WRAPPED")
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
function truncate(str, maxLength) {
|
|
848
|
+
if (str.length <= maxLength) return str;
|
|
849
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
850
|
+
}
|
|
851
|
+
function printLoading(message) {
|
|
852
|
+
console.log(chalk.dim(`[${chalk.hex("#ff7f50")("*")}] ${message}`));
|
|
853
|
+
}
|
|
854
|
+
function printError(message) {
|
|
855
|
+
console.log(chalk.red(`[!] ${message}`));
|
|
856
|
+
}
|
|
857
|
+
function printSuccess(message) {
|
|
858
|
+
console.log(chalk.green(`[\u2713] ${message}`));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/html.ts
|
|
862
|
+
import * as fs3 from "fs";
|
|
863
|
+
import * as path3 from "path";
|
|
864
|
+
import * as os2 from "os";
|
|
865
|
+
function formatCost(cost) {
|
|
866
|
+
if (cost >= 1e4) {
|
|
867
|
+
return (cost / 1e3).toFixed(1) + "K";
|
|
868
|
+
}
|
|
869
|
+
if (cost >= 1e3) {
|
|
870
|
+
return cost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
871
|
+
}
|
|
872
|
+
if (cost >= 100) {
|
|
873
|
+
return cost.toFixed(0);
|
|
874
|
+
}
|
|
875
|
+
if (cost >= 10) {
|
|
876
|
+
return cost.toFixed(1);
|
|
877
|
+
}
|
|
878
|
+
return cost.toFixed(2);
|
|
879
|
+
}
|
|
880
|
+
async function generateHTML(stats) {
|
|
881
|
+
const html = buildHTML(stats);
|
|
882
|
+
const tempDir = os2.tmpdir();
|
|
883
|
+
const filePath = path3.join(tempDir, `claude-code-wrapped-${Date.now()}.html`);
|
|
884
|
+
await fs3.promises.writeFile(filePath, html, "utf-8");
|
|
885
|
+
return filePath;
|
|
886
|
+
}
|
|
887
|
+
function buildHTML(stats) {
|
|
888
|
+
const lang = getLanguage();
|
|
889
|
+
const i = t();
|
|
890
|
+
const peakMessages = Math.max(...stats.dailyActivity.map((d) => d.messageCount));
|
|
891
|
+
const tokensFormatted = formatNumber(stats.totalTokens);
|
|
892
|
+
const longestDuration = stats.longestSession ? formatDuration(stats.longestSession.duration) : "0h";
|
|
893
|
+
const hourlyHTML = buildHourlyDistribution(stats);
|
|
894
|
+
const projectsHTML = buildProjectsHTML(stats);
|
|
895
|
+
const modelUsageHTML = buildModelUsage(stats);
|
|
896
|
+
const weeklyHTML = buildWeeklyPattern(stats);
|
|
897
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
898
|
+
return `<!DOCTYPE html>
|
|
899
|
+
<html lang="${lang}">
|
|
900
|
+
<head>
|
|
901
|
+
<meta charset="UTF-8">
|
|
902
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
903
|
+
<title>Claude Code Wrapped 2025</title>
|
|
904
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
905
|
+
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
|
906
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
907
|
+
<style>
|
|
908
|
+
:root {
|
|
909
|
+
--cream: #FAF9F7;
|
|
910
|
+
--warm-gray: #F5F4F2;
|
|
911
|
+
--border: #E8E6E3;
|
|
912
|
+
--text-primary: #1A1915;
|
|
913
|
+
--text-secondary: #6B6966;
|
|
914
|
+
--text-muted: #9C9A97;
|
|
915
|
+
--coral: #D97757;
|
|
916
|
+
--coral-light: #F5E6E0;
|
|
917
|
+
--coral-soft: rgba(217, 119, 87, 0.1);
|
|
918
|
+
--sage: #5B8A72;
|
|
919
|
+
--sage-light: #E8F0EC;
|
|
920
|
+
--amber: #C4963A;
|
|
921
|
+
--amber-light: #FBF5E8;
|
|
922
|
+
--purple: #8B7EC8;
|
|
923
|
+
--purple-light: #F0EDF8;
|
|
924
|
+
}
|
|
925
|
+
* { box-sizing: border-box; }
|
|
926
|
+
html, body { margin: 0; padding: 0; min-height: 100vh; }
|
|
927
|
+
body {
|
|
928
|
+
background-color: var(--cream);
|
|
929
|
+
color: var(--text-primary);
|
|
930
|
+
font-family: 'JetBrains Mono', monospace;
|
|
931
|
+
line-height: 1.4;
|
|
932
|
+
display: flex;
|
|
933
|
+
flex-direction: column;
|
|
934
|
+
}
|
|
935
|
+
.landing-header {
|
|
936
|
+
width: 100%;
|
|
937
|
+
padding: 20px 48px;
|
|
938
|
+
display: flex;
|
|
939
|
+
justify-content: space-between;
|
|
940
|
+
align-items: center;
|
|
941
|
+
border-bottom: 1px solid var(--border);
|
|
942
|
+
}
|
|
943
|
+
.landing-logo {
|
|
944
|
+
display: flex;
|
|
945
|
+
align-items: center;
|
|
946
|
+
gap: 12px;
|
|
947
|
+
font-size: 14px;
|
|
948
|
+
font-weight: 600;
|
|
949
|
+
color: var(--text-primary);
|
|
950
|
+
}
|
|
951
|
+
.landing-logo-icon {
|
|
952
|
+
width: 32px;
|
|
953
|
+
height: 32px;
|
|
954
|
+
background: var(--coral);
|
|
955
|
+
border-radius: 8px;
|
|
956
|
+
display: flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
justify-content: center;
|
|
959
|
+
}
|
|
960
|
+
.landing-nav {
|
|
961
|
+
display: flex;
|
|
962
|
+
gap: 32px;
|
|
963
|
+
}
|
|
964
|
+
.landing-nav a, .github-link {
|
|
965
|
+
display: flex;
|
|
966
|
+
align-items: center;
|
|
967
|
+
gap: 6px;
|
|
968
|
+
color: var(--text-secondary);
|
|
969
|
+
text-decoration: none;
|
|
970
|
+
font-size: 13px;
|
|
971
|
+
transition: color 0.2s;
|
|
972
|
+
}
|
|
973
|
+
.landing-nav a:hover, .github-link:hover { color: var(--coral); }
|
|
974
|
+
.github-link svg { width: 16px; height: 16px; }
|
|
975
|
+
.page-wrapper {
|
|
976
|
+
flex: 1;
|
|
977
|
+
padding: 60px 24px;
|
|
978
|
+
display: flex;
|
|
979
|
+
flex-direction: column;
|
|
980
|
+
align-items: center;
|
|
981
|
+
justify-content: center;
|
|
982
|
+
position: relative;
|
|
983
|
+
overflow: hidden;
|
|
984
|
+
}
|
|
985
|
+
.dashboard {
|
|
986
|
+
width: 100%;
|
|
987
|
+
max-width: 1360px;
|
|
988
|
+
padding: 32px 40px;
|
|
989
|
+
background: white;
|
|
990
|
+
display: flex;
|
|
991
|
+
flex-direction: column;
|
|
992
|
+
border-radius: 16px;
|
|
993
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
|
994
|
+
gap: 16px;
|
|
995
|
+
position: relative;
|
|
996
|
+
z-index: 10;
|
|
997
|
+
}
|
|
998
|
+
/* Flowing background animation */
|
|
999
|
+
.bg-animation {
|
|
1000
|
+
position: absolute;
|
|
1001
|
+
top: 0;
|
|
1002
|
+
left: 0;
|
|
1003
|
+
right: 0;
|
|
1004
|
+
bottom: 0;
|
|
1005
|
+
z-index: 1;
|
|
1006
|
+
pointer-events: none;
|
|
1007
|
+
overflow: hidden;
|
|
1008
|
+
}
|
|
1009
|
+
.bg-animation::before {
|
|
1010
|
+
content: '';
|
|
1011
|
+
position: absolute;
|
|
1012
|
+
top: -50%;
|
|
1013
|
+
left: -50%;
|
|
1014
|
+
width: 200%;
|
|
1015
|
+
height: 200%;
|
|
1016
|
+
background:
|
|
1017
|
+
radial-gradient(circle at 20% 80%, rgba(217, 119, 87, 0.08) 0%, transparent 50%),
|
|
1018
|
+
radial-gradient(circle at 80% 20%, rgba(91, 138, 114, 0.08) 0%, transparent 50%),
|
|
1019
|
+
radial-gradient(circle at 40% 40%, rgba(196, 150, 58, 0.05) 0%, transparent 40%);
|
|
1020
|
+
animation: gradientMove 30s ease-in-out infinite;
|
|
1021
|
+
}
|
|
1022
|
+
@keyframes gradientMove {
|
|
1023
|
+
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
1024
|
+
33% { transform: translate(2%, 2%) rotate(1deg); }
|
|
1025
|
+
66% { transform: translate(-1%, 1%) rotate(-1deg); }
|
|
1026
|
+
}
|
|
1027
|
+
.pixel-text {
|
|
1028
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1029
|
+
font-size: 42px;
|
|
1030
|
+
font-weight: 900;
|
|
1031
|
+
color: var(--coral);
|
|
1032
|
+
letter-spacing: 4px;
|
|
1033
|
+
text-shadow:
|
|
1034
|
+
3px 0 0 var(--coral),
|
|
1035
|
+
-3px 0 0 var(--coral),
|
|
1036
|
+
0 3px 0 var(--coral),
|
|
1037
|
+
0 -3px 0 var(--coral),
|
|
1038
|
+
2px 2px 0 rgba(217, 119, 87, 0.3),
|
|
1039
|
+
4px 4px 0 rgba(217, 119, 87, 0.15);
|
|
1040
|
+
}
|
|
1041
|
+
.pixel-text-sm {
|
|
1042
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1043
|
+
font-size: 14px;
|
|
1044
|
+
font-weight: 700;
|
|
1045
|
+
color: var(--coral);
|
|
1046
|
+
letter-spacing: 3px;
|
|
1047
|
+
text-shadow:
|
|
1048
|
+
1px 0 0 var(--coral),
|
|
1049
|
+
-1px 0 0 var(--coral),
|
|
1050
|
+
0 1px 0 var(--coral),
|
|
1051
|
+
0 -1px 0 var(--coral);
|
|
1052
|
+
}
|
|
1053
|
+
.landing-footer {
|
|
1054
|
+
padding: 32px 48px;
|
|
1055
|
+
display: flex;
|
|
1056
|
+
justify-content: space-between;
|
|
1057
|
+
align-items: center;
|
|
1058
|
+
border-top: 1px solid var(--border);
|
|
1059
|
+
background: var(--cream);
|
|
1060
|
+
}
|
|
1061
|
+
.landing-footer-left { display: flex; align-items: center; gap: 24px; }
|
|
1062
|
+
.landing-footer-links { display: flex; gap: 24px; }
|
|
1063
|
+
.landing-footer-links a {
|
|
1064
|
+
color: var(--text-muted);
|
|
1065
|
+
text-decoration: none;
|
|
1066
|
+
font-size: 12px;
|
|
1067
|
+
transition: color 0.2s;
|
|
1068
|
+
}
|
|
1069
|
+
.landing-footer-links a:hover { color: var(--coral); }
|
|
1070
|
+
.landing-footer-copy { color: var(--text-muted); font-size: 11px; }
|
|
1071
|
+
.tui-box {
|
|
1072
|
+
border: 1px solid var(--border);
|
|
1073
|
+
background: white;
|
|
1074
|
+
position: relative;
|
|
1075
|
+
border-radius: 4px;
|
|
1076
|
+
padding: 24px;
|
|
1077
|
+
}
|
|
1078
|
+
.tui-box::before {
|
|
1079
|
+
content: attr(data-title);
|
|
1080
|
+
position: absolute;
|
|
1081
|
+
top: -9px;
|
|
1082
|
+
left: 12px;
|
|
1083
|
+
background: white;
|
|
1084
|
+
padding: 0 8px;
|
|
1085
|
+
font-size: 11px;
|
|
1086
|
+
font-weight: 600;
|
|
1087
|
+
color: var(--text-muted);
|
|
1088
|
+
text-transform: uppercase;
|
|
1089
|
+
letter-spacing: 0.1em;
|
|
1090
|
+
}
|
|
1091
|
+
.stat-card {
|
|
1092
|
+
background: var(--warm-gray);
|
|
1093
|
+
border-radius: 8px;
|
|
1094
|
+
padding: 24px;
|
|
1095
|
+
text-align: center;
|
|
1096
|
+
}
|
|
1097
|
+
.stat-label {
|
|
1098
|
+
font-size: 11px;
|
|
1099
|
+
color: var(--text-muted);
|
|
1100
|
+
font-weight: 500;
|
|
1101
|
+
text-transform: uppercase;
|
|
1102
|
+
letter-spacing: 0.1em;
|
|
1103
|
+
margin-bottom: 8px;
|
|
1104
|
+
}
|
|
1105
|
+
.stat-value { font-size: 2.25rem; font-weight: 700; }
|
|
1106
|
+
.progress-track { display: flex; gap: 4px; }
|
|
1107
|
+
.progress-segment { width: 14px; height: 24px; border-radius: 3px; }
|
|
1108
|
+
.segment-filled { background: var(--coral); }
|
|
1109
|
+
.segment-empty { background: var(--border); }
|
|
1110
|
+
.tag {
|
|
1111
|
+
display: inline-flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
padding: 4px 10px;
|
|
1114
|
+
border-radius: 4px;
|
|
1115
|
+
font-size: 10px;
|
|
1116
|
+
font-weight: 600;
|
|
1117
|
+
text-transform: uppercase;
|
|
1118
|
+
letter-spacing: 0.04em;
|
|
1119
|
+
}
|
|
1120
|
+
.tag-coral { background: var(--coral-light); color: var(--coral); }
|
|
1121
|
+
.tag-sage { background: var(--sage-light); color: var(--sage); }
|
|
1122
|
+
.tag-amber { background: var(--amber-light); color: var(--amber); }
|
|
1123
|
+
.tag-purple { background: var(--purple-light); color: var(--purple); }
|
|
1124
|
+
.activity-bar {
|
|
1125
|
+
height: 100%;
|
|
1126
|
+
background: var(--coral);
|
|
1127
|
+
border-radius: 3px;
|
|
1128
|
+
min-width: 4px;
|
|
1129
|
+
}
|
|
1130
|
+
.project-item {
|
|
1131
|
+
display: flex;
|
|
1132
|
+
align-items: center;
|
|
1133
|
+
justify-content: space-between;
|
|
1134
|
+
padding: 10px 14px;
|
|
1135
|
+
border-radius: 6px;
|
|
1136
|
+
}
|
|
1137
|
+
.project-item:hover { background: var(--warm-gray); }
|
|
1138
|
+
.project-dot {
|
|
1139
|
+
width: 8px;
|
|
1140
|
+
height: 8px;
|
|
1141
|
+
border-radius: 50%;
|
|
1142
|
+
margin-right: 10px;
|
|
1143
|
+
}
|
|
1144
|
+
.corner-box { position: relative; padding: 20px; }
|
|
1145
|
+
.corner-box::before, .corner-box::after {
|
|
1146
|
+
content: '';
|
|
1147
|
+
position: absolute;
|
|
1148
|
+
width: 20px;
|
|
1149
|
+
height: 20px;
|
|
1150
|
+
border-color: var(--border);
|
|
1151
|
+
border-style: solid;
|
|
1152
|
+
}
|
|
1153
|
+
.corner-box::before { top: 0; left: 0; border-width: 2px 0 0 2px; }
|
|
1154
|
+
.corner-box::after { bottom: 0; right: 0; border-width: 0 2px 2px 0; }
|
|
1155
|
+
.cursor {
|
|
1156
|
+
display: inline-block;
|
|
1157
|
+
width: 10px;
|
|
1158
|
+
height: 18px;
|
|
1159
|
+
background: var(--coral);
|
|
1160
|
+
animation: blink 1s step-end infinite;
|
|
1161
|
+
margin-left: 4px;
|
|
1162
|
+
vertical-align: middle;
|
|
1163
|
+
}
|
|
1164
|
+
@keyframes blink {
|
|
1165
|
+
0%, 50% { opacity: 1; }
|
|
1166
|
+
51%, 100% { opacity: 0; }
|
|
1167
|
+
}
|
|
1168
|
+
.pixel-logo { display: flex; flex-direction: column; }
|
|
1169
|
+
.pixel-char {
|
|
1170
|
+
display: grid;
|
|
1171
|
+
grid-template-columns: repeat(4, 8px);
|
|
1172
|
+
grid-template-rows: repeat(5, 8px);
|
|
1173
|
+
gap: 2px;
|
|
1174
|
+
}
|
|
1175
|
+
.pr {
|
|
1176
|
+
background: var(--coral);
|
|
1177
|
+
border-radius: 1px;
|
|
1178
|
+
box-shadow: inset -1px -1px 0 rgba(0,0,0,0.2), inset 1px 1px 0 rgba(255,255,255,0.3);
|
|
1179
|
+
}
|
|
1180
|
+
.pe { background: transparent; }
|
|
1181
|
+
.export-btn {
|
|
1182
|
+
padding: 10px 20px;
|
|
1183
|
+
background: var(--coral);
|
|
1184
|
+
color: white;
|
|
1185
|
+
border: none;
|
|
1186
|
+
border-radius: 8px;
|
|
1187
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1188
|
+
font-size: 13px;
|
|
1189
|
+
font-weight: 600;
|
|
1190
|
+
cursor: pointer;
|
|
1191
|
+
display: flex;
|
|
1192
|
+
align-items: center;
|
|
1193
|
+
gap: 8px;
|
|
1194
|
+
transition: all 0.2s;
|
|
1195
|
+
}
|
|
1196
|
+
.export-btn:hover {
|
|
1197
|
+
background: #c56a4d;
|
|
1198
|
+
transform: translateY(-1px);
|
|
1199
|
+
}
|
|
1200
|
+
.export-btn:disabled {
|
|
1201
|
+
opacity: 0.6;
|
|
1202
|
+
cursor: not-allowed;
|
|
1203
|
+
}
|
|
1204
|
+
</style>
|
|
1205
|
+
</head>
|
|
1206
|
+
<body>
|
|
1207
|
+
<!-- Landing Header -->
|
|
1208
|
+
<header class="landing-header">
|
|
1209
|
+
<div class="landing-logo">
|
|
1210
|
+
<pre style="font-size: 10px; line-height: 1.1; color: var(--coral); margin: 0; font-weight: bold;">
|
|
1211
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
1212
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
1213
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1214
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551
|
|
1215
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
1216
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D</pre>
|
|
1217
|
+
</div>
|
|
1218
|
+
<nav class="landing-nav">
|
|
1219
|
+
<a href="https://github.com/Dylan-Nihilo/claude-code-wrapped" target="_blank" class="github-link">
|
|
1220
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
|
|
1221
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
1222
|
+
</svg>
|
|
1223
|
+
claude-code-wrapped
|
|
1224
|
+
</a>
|
|
1225
|
+
<button class="export-btn" onclick="exportImage()">
|
|
1226
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1227
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1228
|
+
<polyline points="7 10 12 15 17 10"/>
|
|
1229
|
+
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
1230
|
+
</svg>
|
|
1231
|
+
Export PNG
|
|
1232
|
+
</button>
|
|
1233
|
+
</nav>
|
|
1234
|
+
</header>
|
|
1235
|
+
|
|
1236
|
+
<div class="page-wrapper">
|
|
1237
|
+
<!-- Background animation -->
|
|
1238
|
+
<div class="bg-animation"></div>
|
|
1239
|
+
<div class="dashboard">
|
|
1240
|
+
<!-- Header Row -->
|
|
1241
|
+
<header class="flex justify-between items-start mb-4">
|
|
1242
|
+
<div>
|
|
1243
|
+
<pre style="font-size: 20px; line-height: 1.2; color: var(--coral); margin: 0; font-weight: bold;">
|
|
1244
|
+
\u2554\u2550\u2557\u2566 \u2554\u2550\u2557\u2566 \u2566\u2554\u2566\u2557\u2554\u2550\u2557 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2554\u2550\u2557
|
|
1245
|
+
\u2551 \u2551 \u2560\u2550\u2563\u2551 \u2551 \u2551\u2551\u2551\u2563 \u2551 \u2551 \u2551 \u2551\u2551\u2551\u2563
|
|
1246
|
+
\u255A\u2550\u255D\u2569\u2550\u255D\u2569 \u2569\u255A\u2550\u255D\u2550\u2569\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u2550\u2569\u255D\u255A\u2550\u255D</pre>
|
|
1247
|
+
<p class="text-sm font-semibold tracking-wider mt-2" style="color: var(--text-muted)">WRAPPED 2025</p>
|
|
1248
|
+
</div>
|
|
1249
|
+
<div class="flex items-center gap-3">
|
|
1250
|
+
<span class="tag tag-coral">${today}</span>
|
|
1251
|
+
<span class="tag tag-sage">${stats.userLevel}</span>
|
|
1252
|
+
</div>
|
|
1253
|
+
</header>
|
|
1254
|
+
|
|
1255
|
+
<!-- Overview Stats -->
|
|
1256
|
+
<div class="tui-box" data-title="${i.overview}">
|
|
1257
|
+
<div class="grid grid-cols-6 gap-5">
|
|
1258
|
+
<div class="stat-card">
|
|
1259
|
+
<div class="stat-label">${i.totalSessions}</div>
|
|
1260
|
+
<div class="stat-value" style="color: var(--coral)">${stats.totalSessions.toLocaleString()}</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
<div class="stat-card">
|
|
1263
|
+
<div class="stat-label">${i.activeDays}</div>
|
|
1264
|
+
<div class="stat-value" style="color: var(--sage)">${stats.activeDays}</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
<div class="stat-card">
|
|
1267
|
+
<div class="stat-label">${i.peakMessages}</div>
|
|
1268
|
+
<div class="stat-value" style="color: var(--amber)">${peakMessages.toLocaleString()}</div>
|
|
1269
|
+
</div>
|
|
1270
|
+
<div class="stat-card">
|
|
1271
|
+
<div class="stat-label">${i.toolCalls}</div>
|
|
1272
|
+
<div class="stat-value" style="color: var(--purple)">${stats.totalToolCalls.toLocaleString()}</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
<div class="stat-card">
|
|
1275
|
+
<div class="stat-label">${i.totalTokens}</div>
|
|
1276
|
+
<div class="stat-value" style="color: var(--coral)">${tokensFormatted}</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
<div class="stat-card">
|
|
1279
|
+
<div class="stat-label">${i.projects}</div>
|
|
1280
|
+
<div class="stat-value" style="color: var(--sage)">${stats.topProjects.length}</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
|
|
1285
|
+
<!-- Row 2: Impact + Activity -->
|
|
1286
|
+
<div class="grid grid-cols-12 gap-4">
|
|
1287
|
+
<div class="col-span-5 tui-box" data-title="${i.impact}">
|
|
1288
|
+
<div class="corner-box h-full flex flex-col justify-center">
|
|
1289
|
+
<div class="flex items-center gap-2 mb-3">
|
|
1290
|
+
<span class="tag tag-coral">${stats.userTitle}</span>
|
|
1291
|
+
<span class="tag tag-sage">${stats.userLevel}</span>
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="text-7xl font-bold mb-2" style="color: var(--text-primary)">${stats.totalMessages.toLocaleString()}</div>
|
|
1294
|
+
<div class="text-base" style="color: var(--text-secondary)">${i.messagesWithClaude}</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
</div>
|
|
1297
|
+
|
|
1298
|
+
<div class="col-span-4 tui-box" data-title="${i.hourlyActivity}">
|
|
1299
|
+
<div class="space-y-2 h-full flex flex-col justify-center">
|
|
1300
|
+
${hourlyHTML}
|
|
1301
|
+
</div>
|
|
1302
|
+
</div>
|
|
1303
|
+
|
|
1304
|
+
<div class="col-span-3 tui-box" data-title="${i.weeklyPattern}">
|
|
1305
|
+
<div class="flex items-end gap-3 h-full">
|
|
1306
|
+
${weeklyHTML}
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
</div>
|
|
1310
|
+
|
|
1311
|
+
<!-- Row 3: Projects + Model + Tools -->
|
|
1312
|
+
<div class="grid grid-cols-12 gap-4">
|
|
1313
|
+
<div class="col-span-3 tui-box" data-title="${i.projects}">
|
|
1314
|
+
<div class="space-y-2 h-full flex flex-col justify-center">
|
|
1315
|
+
${projectsHTML}
|
|
1316
|
+
</div>
|
|
1317
|
+
</div>
|
|
1318
|
+
|
|
1319
|
+
<div class="col-span-3 tui-box" data-title="${i.modelUsage}">
|
|
1320
|
+
<div class="space-y-3 h-full flex flex-col justify-center">
|
|
1321
|
+
<div class="flex items-center justify-between text-base">
|
|
1322
|
+
<span style="color: var(--text-muted)">Primary</span>
|
|
1323
|
+
<span class="font-semibold">${stats.primaryModel}</span>
|
|
1324
|
+
</div>
|
|
1325
|
+
${modelUsageHTML}
|
|
1326
|
+
</div>
|
|
1327
|
+
</div>
|
|
1328
|
+
|
|
1329
|
+
<div class="col-span-2 tui-box flex flex-col" data-title="${i.topTools}">
|
|
1330
|
+
<div class="space-y-3 flex-1 flex flex-col justify-center">
|
|
1331
|
+
<div class="flex items-center justify-between">
|
|
1332
|
+
<span class="text-sm">Read</span>
|
|
1333
|
+
<span class="tag tag-coral">32%</span>
|
|
1334
|
+
</div>
|
|
1335
|
+
<div class="flex items-center justify-between">
|
|
1336
|
+
<span class="text-sm">Edit</span>
|
|
1337
|
+
<span class="tag tag-sage">28%</span>
|
|
1338
|
+
</div>
|
|
1339
|
+
<div class="flex items-center justify-between">
|
|
1340
|
+
<span class="text-sm">Bash</span>
|
|
1341
|
+
<span class="tag tag-amber">18%</span>
|
|
1342
|
+
</div>
|
|
1343
|
+
<div class="flex items-center justify-between">
|
|
1344
|
+
<span class="text-sm">Grep</span>
|
|
1345
|
+
<span class="tag tag-purple">12%</span>
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
|
|
1350
|
+
<div class="col-span-2 tui-box flex flex-col" data-title="${i.costEstimate}">
|
|
1351
|
+
<div class="space-y-3 flex-1 flex flex-col justify-center">
|
|
1352
|
+
<div>
|
|
1353
|
+
<span class="text-2xl font-bold" style="color: var(--coral)">$${formatCost(stats.estimatedCost)}</span>
|
|
1354
|
+
<span class="text-xs ml-1" style="color: var(--text-muted)">USD</span>
|
|
1355
|
+
</div>
|
|
1356
|
+
<div class="pt-2 border-t text-xs" style="border-color: var(--border); color: var(--text-muted)">
|
|
1357
|
+
${i.basedOnApi}
|
|
1358
|
+
</div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</div>
|
|
1361
|
+
|
|
1362
|
+
<div class="col-span-2 tui-box flex flex-col" data-title="${i.achievements}">
|
|
1363
|
+
<div class="space-y-3 flex-1 flex flex-col justify-center">
|
|
1364
|
+
<div class="flex items-center gap-2">
|
|
1365
|
+
<span style="color: var(--amber)">\u2605</span>
|
|
1366
|
+
<span class="text-sm">Marathon ${longestDuration}</span>
|
|
1367
|
+
</div>
|
|
1368
|
+
<div class="flex items-center gap-2">
|
|
1369
|
+
<span style="color: var(--amber)">\u2605</span>
|
|
1370
|
+
<span class="text-sm">${stats.userTitle}</span>
|
|
1371
|
+
</div>
|
|
1372
|
+
<div class="flex items-center gap-2">
|
|
1373
|
+
<span style="color: var(--amber)">\u2605</span>
|
|
1374
|
+
<span class="text-sm">${stats.userLevel}</span>
|
|
1375
|
+
</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
</div>
|
|
1378
|
+
</div>
|
|
1379
|
+
|
|
1380
|
+
<!-- Footer -->
|
|
1381
|
+
<footer class="flex justify-between items-center mt-6 text-xs" style="color: var(--text-muted)">
|
|
1382
|
+
<div class="flex items-center gap-2">
|
|
1383
|
+
<span style="color: var(--coral)">$</span>
|
|
1384
|
+
<span>claude-code --wrapped 2025</span>
|
|
1385
|
+
<span class="cursor"></span>
|
|
1386
|
+
</div>
|
|
1387
|
+
<div class="flex items-center gap-2">
|
|
1388
|
+
<span>claude code wrapped by</span>
|
|
1389
|
+
<span style="color: var(--coral); font-weight: 900; font-size: 14px; letter-spacing: 1px;">DYLAN</span>
|
|
1390
|
+
</div>
|
|
1391
|
+
</footer>
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
|
|
1395
|
+
<!-- Landing Footer -->
|
|
1396
|
+
<footer class="landing-footer">
|
|
1397
|
+
<div class="landing-footer-left">
|
|
1398
|
+
<span class="landing-footer-copy">Made with Claude Code</span>
|
|
1399
|
+
<div class="landing-footer-links">
|
|
1400
|
+
<a href="https://github.com/Dylan-Nihilo/claude-code-wrapped" target="_blank">GitHub</a>
|
|
1401
|
+
</div>
|
|
1402
|
+
</div>
|
|
1403
|
+
<span class="landing-footer-copy">2025 Claude Code Wrapped</span>
|
|
1404
|
+
</footer>
|
|
1405
|
+
|
|
1406
|
+
<script>
|
|
1407
|
+
async function exportImage() {
|
|
1408
|
+
const btn = document.querySelector('.export-btn');
|
|
1409
|
+
const originalText = btn.innerHTML;
|
|
1410
|
+
btn.disabled = true;
|
|
1411
|
+
btn.innerHTML = 'Exporting...';
|
|
1412
|
+
|
|
1413
|
+
try {
|
|
1414
|
+
btn.style.display = 'none';
|
|
1415
|
+
const dashboard = document.querySelector('.dashboard');
|
|
1416
|
+
const canvas = await html2canvas(dashboard, {
|
|
1417
|
+
scale: 2,
|
|
1418
|
+
useCORS: true,
|
|
1419
|
+
backgroundColor: '#FFFFFF',
|
|
1420
|
+
logging: false
|
|
1421
|
+
});
|
|
1422
|
+
const link = document.createElement('a');
|
|
1423
|
+
link.download = 'claude-code-wrapped-2025.png';
|
|
1424
|
+
link.href = canvas.toDataURL('image/png');
|
|
1425
|
+
link.click();
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
console.error('Export failed:', err);
|
|
1428
|
+
alert('Export failed. Please try again.');
|
|
1429
|
+
} finally {
|
|
1430
|
+
btn.style.display = 'flex';
|
|
1431
|
+
btn.disabled = false;
|
|
1432
|
+
btn.innerHTML = originalText;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
</script>
|
|
1436
|
+
</body>
|
|
1437
|
+
</html>`;
|
|
1438
|
+
}
|
|
1439
|
+
function buildHourlyDistribution(stats) {
|
|
1440
|
+
const ranges = [
|
|
1441
|
+
{ label: "00:00-08:00", hours: [0, 1, 2, 3, 4, 5, 6, 7, 8] },
|
|
1442
|
+
{ label: "09:00-12:00", hours: [9, 10, 11, 12] },
|
|
1443
|
+
{ label: "13:00-18:00", hours: [13, 14, 15, 16, 17, 18] },
|
|
1444
|
+
{ label: "19:00-23:00", hours: [19, 20, 21, 22, 23] }
|
|
1445
|
+
];
|
|
1446
|
+
let maxCount = 0;
|
|
1447
|
+
const rangeCounts = ranges.map((r) => {
|
|
1448
|
+
const count = r.hours.reduce((sum, h) => sum + (stats.hourlyDistribution[h] || 0), 0);
|
|
1449
|
+
maxCount = Math.max(maxCount, count);
|
|
1450
|
+
return { ...r, count };
|
|
1451
|
+
});
|
|
1452
|
+
const peakRange = rangeCounts.reduce((max, r) => r.count > max.count ? r : max);
|
|
1453
|
+
return rangeCounts.map((range) => {
|
|
1454
|
+
const isPeak = range === peakRange;
|
|
1455
|
+
const ratio = maxCount > 0 ? range.count / maxCount : 0;
|
|
1456
|
+
const width = Math.round(ratio * 100);
|
|
1457
|
+
const labelStyle = isPeak ? "color: var(--coral); font-weight: 600;" : "color: var(--text-muted);";
|
|
1458
|
+
const countStyle = isPeak ? "color: var(--coral); font-weight: 600;" : "color: var(--text-muted);";
|
|
1459
|
+
return `<div class="flex items-center gap-3">
|
|
1460
|
+
<span class="w-28 text-xs" style="${labelStyle}">${range.label}</span>
|
|
1461
|
+
<div class="flex-1 h-6 rounded overflow-hidden" style="background: var(--warm-gray)">
|
|
1462
|
+
<div class="activity-bar" style="width: ${width}%"></div>
|
|
1463
|
+
</div>
|
|
1464
|
+
<span class="w-12 text-right text-xs" style="${countStyle}">${range.count}</span>
|
|
1465
|
+
</div>`;
|
|
1466
|
+
}).join("\n");
|
|
1467
|
+
}
|
|
1468
|
+
function buildProjectsHTML(stats) {
|
|
1469
|
+
const projects = stats.topProjects.slice(0, 4);
|
|
1470
|
+
const colors = ["var(--coral)", "var(--sage)", "var(--amber)", "var(--purple)"];
|
|
1471
|
+
return projects.map((p, idx) => {
|
|
1472
|
+
const isFirst = idx === 0;
|
|
1473
|
+
const bgStyle = isFirst ? "background: var(--coral-soft);" : "";
|
|
1474
|
+
return `<div class="project-item" style="${bgStyle}">
|
|
1475
|
+
<div class="flex items-center">
|
|
1476
|
+
<div class="project-dot" style="background: ${colors[idx]}"></div>
|
|
1477
|
+
<span class="text-sm${isFirst ? " font-medium" : ""}">${p.name}</span>
|
|
1478
|
+
</div>
|
|
1479
|
+
${isFirst ? '<span class="text-xs" style="color: var(--text-muted)">\u2605</span>' : ""}
|
|
1480
|
+
</div>`;
|
|
1481
|
+
}).join("\n");
|
|
1482
|
+
}
|
|
1483
|
+
function buildModelUsage(stats) {
|
|
1484
|
+
const total = Object.values(stats.tokensByModel).reduce((s, t2) => s + t2, 0);
|
|
1485
|
+
const models = Object.entries(stats.tokensByModel).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
1486
|
+
const colors = ["var(--coral)", "var(--sage)", "var(--amber)"];
|
|
1487
|
+
return `<div class="space-y-2">
|
|
1488
|
+
${models.map(([name, tokens], idx) => {
|
|
1489
|
+
const ratio = total > 0 ? tokens / total : 0;
|
|
1490
|
+
const percent = Math.round(ratio * 100);
|
|
1491
|
+
const shortName = name.replace("Claude ", "").replace(" 4.5", "");
|
|
1492
|
+
return `<div class="flex items-center gap-2">
|
|
1493
|
+
<span class="text-xs w-16" style="color: var(--text-muted)">${shortName}</span>
|
|
1494
|
+
<div class="flex-1 h-4 rounded overflow-hidden" style="background: var(--warm-gray)">
|
|
1495
|
+
<div class="h-full rounded" style="width: ${percent}%; background: ${colors[idx]}"></div>
|
|
1496
|
+
</div>
|
|
1497
|
+
<span class="text-xs" style="color: var(--text-muted)">${percent}%</span>
|
|
1498
|
+
</div>`;
|
|
1499
|
+
}).join("\n")}
|
|
1500
|
+
</div>`;
|
|
1501
|
+
}
|
|
1502
|
+
function buildWeeklyPattern(stats) {
|
|
1503
|
+
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
1504
|
+
const dayCounts = [0, 0, 0, 0, 0, 0, 0];
|
|
1505
|
+
stats.dailyActivity.forEach((d) => {
|
|
1506
|
+
const dayOfWeek = new Date(d.date).getDay();
|
|
1507
|
+
const idx = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
1508
|
+
dayCounts[idx] += d.messageCount;
|
|
1509
|
+
});
|
|
1510
|
+
const maxCount = Math.max(...dayCounts);
|
|
1511
|
+
const peakIdx = dayCounts.indexOf(maxCount);
|
|
1512
|
+
return days.map((day, idx) => {
|
|
1513
|
+
const ratio = maxCount > 0 ? dayCounts[idx] / maxCount : 0;
|
|
1514
|
+
const height = Math.max(20, Math.round(ratio * 100));
|
|
1515
|
+
const isPeak = idx === peakIdx;
|
|
1516
|
+
const color = isPeak ? "var(--coral)" : idx >= 5 ? "var(--amber)" : "var(--sage)";
|
|
1517
|
+
const opacity = isPeak ? 1 : ratio * 0.5 + 0.5;
|
|
1518
|
+
const labelStyle = isPeak ? "color: var(--coral); font-weight: 600;" : "color: var(--text-muted);";
|
|
1519
|
+
return `<div class="flex-1 flex flex-col items-center gap-1 h-full">
|
|
1520
|
+
<div class="w-full rounded-t flex-1 flex items-end">
|
|
1521
|
+
<div class="w-full rounded-t" style="height: ${height}%; background: ${color}; opacity: ${opacity}"></div>
|
|
1522
|
+
</div>
|
|
1523
|
+
<span class="text-xs" style="${labelStyle}">${day}</span>
|
|
1524
|
+
</div>`;
|
|
1525
|
+
}).join("\n");
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/export.ts
|
|
1529
|
+
import puppeteer from "puppeteer";
|
|
1530
|
+
import * as path4 from "path";
|
|
1531
|
+
import * as os3 from "os";
|
|
1532
|
+
async function exportToPNG(htmlPath) {
|
|
1533
|
+
const outputPath = path4.join(
|
|
1534
|
+
os3.homedir(),
|
|
1535
|
+
`claude-code-wrapped-2025-${Date.now()}.png`
|
|
1536
|
+
);
|
|
1537
|
+
const browser = await puppeteer.launch({
|
|
1538
|
+
headless: true,
|
|
1539
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
1540
|
+
});
|
|
1541
|
+
try {
|
|
1542
|
+
const page = await browser.newPage();
|
|
1543
|
+
await page.setViewport({
|
|
1544
|
+
width: 1200,
|
|
1545
|
+
height: 1800,
|
|
1546
|
+
deviceScaleFactor: 2
|
|
1547
|
+
});
|
|
1548
|
+
await page.goto(`file://${htmlPath}`, {
|
|
1549
|
+
waitUntil: "networkidle0"
|
|
1550
|
+
});
|
|
1551
|
+
await page.evaluate(() => {
|
|
1552
|
+
const btn = document.querySelector(".export-btn");
|
|
1553
|
+
if (btn) btn.style.display = "none";
|
|
1554
|
+
const style = document.createElement("style");
|
|
1555
|
+
style.textContent = `
|
|
1556
|
+
*, *::before, *::after {
|
|
1557
|
+
animation: none !important;
|
|
1558
|
+
transition: none !important;
|
|
1559
|
+
}
|
|
1560
|
+
.crt::before, .crt::after, .scan-line {
|
|
1561
|
+
display: none !important;
|
|
1562
|
+
}
|
|
1563
|
+
`;
|
|
1564
|
+
document.head.appendChild(style);
|
|
1565
|
+
});
|
|
1566
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1567
|
+
const contentElement = await page.$("#wrapped-content");
|
|
1568
|
+
if (!contentElement) {
|
|
1569
|
+
throw new Error("Could not find wrapped content element");
|
|
1570
|
+
}
|
|
1571
|
+
const boundingBox = await contentElement.boundingBox();
|
|
1572
|
+
if (!boundingBox) {
|
|
1573
|
+
throw new Error("Could not get content dimensions");
|
|
1574
|
+
}
|
|
1575
|
+
await page.screenshot({
|
|
1576
|
+
path: outputPath,
|
|
1577
|
+
fullPage: true,
|
|
1578
|
+
type: "png"
|
|
1579
|
+
});
|
|
1580
|
+
return outputPath;
|
|
1581
|
+
} finally {
|
|
1582
|
+
await browser.close();
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function checkPuppeteerAvailable() {
|
|
1586
|
+
try {
|
|
1587
|
+
const browser = await puppeteer.launch({
|
|
1588
|
+
headless: true,
|
|
1589
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
1590
|
+
});
|
|
1591
|
+
await browser.close();
|
|
1592
|
+
return true;
|
|
1593
|
+
} catch {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
export {
|
|
1598
|
+
analyzeData,
|
|
1599
|
+
checkPuppeteerAvailable,
|
|
1600
|
+
detectLanguage,
|
|
1601
|
+
exportToPNG,
|
|
1602
|
+
formatDuration,
|
|
1603
|
+
formatNumber,
|
|
1604
|
+
generateHTML,
|
|
1605
|
+
getClaudeDataDir,
|
|
1606
|
+
getClaudeDataPaths,
|
|
1607
|
+
getLanguage,
|
|
1608
|
+
getProjectStats,
|
|
1609
|
+
hasClaudeData,
|
|
1610
|
+
padNumber,
|
|
1611
|
+
printError,
|
|
1612
|
+
printLoading,
|
|
1613
|
+
printSuccess,
|
|
1614
|
+
printTUI,
|
|
1615
|
+
readHistory,
|
|
1616
|
+
readStatsCache,
|
|
1617
|
+
setLanguage,
|
|
1618
|
+
t,
|
|
1619
|
+
validateClaudeData
|
|
1620
|
+
};
|