claude-code-templates 1.28.3 → 1.28.5
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/bin/create-claude-config.js +1 -0
- package/package.json +1 -1
- package/src/analytics/core/YearInReview2025.js +963 -0
- package/src/analytics-web/2025.html +2106 -0
- package/src/analytics.js +39 -3
- package/src/index.js +11 -2
- package/src/plugin-dashboard.js +11 -3
- package/src/validation/README.md +1 -1
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Year in Review 2025 Analyzer
|
|
7
|
+
* Generates comprehensive yearly statistics for Claude Code usage in 2025
|
|
8
|
+
*/
|
|
9
|
+
class YearInReview2025 {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.year = 2025;
|
|
12
|
+
this.yearStart = new Date('2025-01-01T00:00:00.000Z');
|
|
13
|
+
this.yearEnd = new Date('2025-12-31T23:59:59.999Z');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate complete 2025 year in review statistics
|
|
18
|
+
* @param {Array} allConversations - All conversations from analytics
|
|
19
|
+
* @param {string} claudeDir - Claude directory path for subagent analysis
|
|
20
|
+
* @returns {Object} Complete year in review data
|
|
21
|
+
*/
|
|
22
|
+
async generateYearInReview(allConversations, claudeDir) {
|
|
23
|
+
// Filter conversations from 2025
|
|
24
|
+
const conversations2025 = this.filterConversations2025(allConversations);
|
|
25
|
+
|
|
26
|
+
console.log(`📅 Analyzing ${conversations2025.length} conversations from 2025...`);
|
|
27
|
+
|
|
28
|
+
// Detect installed components
|
|
29
|
+
const installedComponents = await this.detectInstalledComponents();
|
|
30
|
+
|
|
31
|
+
// Analyze commands, skills, MCPs, and subagents
|
|
32
|
+
const [commandsData, skillsData, mcpsData, subagentsData] = await Promise.all([
|
|
33
|
+
this.analyzeCommands(),
|
|
34
|
+
this.analyzeSkills(),
|
|
35
|
+
this.analyzeMCPs(),
|
|
36
|
+
claudeDir ? this.analyzeSubagents(claudeDir) : { subagents: [], total: 0 }
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Calculate all statistics
|
|
40
|
+
const stats = {
|
|
41
|
+
// Basic stats
|
|
42
|
+
totalConversations: conversations2025.length,
|
|
43
|
+
|
|
44
|
+
// Model usage
|
|
45
|
+
models: this.analyzeModelUsage(conversations2025),
|
|
46
|
+
|
|
47
|
+
// Tool usage, Agents, and MCPs
|
|
48
|
+
toolsCount: this.countTools(conversations2025),
|
|
49
|
+
agentsCount: this.countAgents(conversations2025),
|
|
50
|
+
mcpCount: this.countMCPs(conversations2025),
|
|
51
|
+
|
|
52
|
+
// Token usage
|
|
53
|
+
tokens: this.calculateTokenUsage(conversations2025),
|
|
54
|
+
|
|
55
|
+
// Streak analysis
|
|
56
|
+
streak: this.calculateStreak(conversations2025),
|
|
57
|
+
|
|
58
|
+
// Activity heatmap data (GitHub-style contribution graph)
|
|
59
|
+
activityHeatmap: this.generateActivityHeatmap(conversations2025),
|
|
60
|
+
|
|
61
|
+
// Installed components for Gource visualization
|
|
62
|
+
componentInstalls: installedComponents,
|
|
63
|
+
|
|
64
|
+
// Top projects
|
|
65
|
+
topProjects: this.analyzeTopProjects(conversations2025),
|
|
66
|
+
|
|
67
|
+
// Tool usage details
|
|
68
|
+
toolUsage: this.analyzeToolUsage(conversations2025),
|
|
69
|
+
|
|
70
|
+
// Time of day analysis
|
|
71
|
+
timeOfDay: this.analyzeTimeOfDay(conversations2025),
|
|
72
|
+
|
|
73
|
+
// Additional insights
|
|
74
|
+
insights: this.generateInsights(conversations2025),
|
|
75
|
+
|
|
76
|
+
// Commands, Skills, MCPs, Subagents (new data)
|
|
77
|
+
commands: commandsData,
|
|
78
|
+
skills: skillsData,
|
|
79
|
+
mcps: mcpsData,
|
|
80
|
+
subagents: subagentsData
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return stats;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Filter conversations from 2025
|
|
88
|
+
* @param {Array} conversations - All conversations
|
|
89
|
+
* @returns {Array} Conversations from 2025
|
|
90
|
+
*/
|
|
91
|
+
filterConversations2025(conversations) {
|
|
92
|
+
return conversations.filter(conv => {
|
|
93
|
+
if (!conv.lastModified) return false;
|
|
94
|
+
|
|
95
|
+
const date = new Date(conv.lastModified);
|
|
96
|
+
return date >= this.yearStart && date <= this.yearEnd;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Count total tool calls across all conversations
|
|
102
|
+
* @param {Array} conversations - 2025 conversations
|
|
103
|
+
* @returns {Object} Tools count information
|
|
104
|
+
*/
|
|
105
|
+
countTools(conversations) {
|
|
106
|
+
let totalTools = 0;
|
|
107
|
+
|
|
108
|
+
conversations.forEach(conv => {
|
|
109
|
+
if (conv.toolUsage && conv.toolUsage.totalToolCalls) {
|
|
110
|
+
totalTools += conv.toolUsage.totalToolCalls;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
total: totalTools,
|
|
116
|
+
formatted: totalTools >= 1000 ? `${(totalTools / 1000).toFixed(1)}K` : totalTools.toString()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Count unique MCPs used across conversations
|
|
122
|
+
* @param {Array} conversations - 2025 conversations
|
|
123
|
+
* @returns {Object} MCP count information
|
|
124
|
+
*/
|
|
125
|
+
countMCPs(conversations) {
|
|
126
|
+
const mcpSet = new Set();
|
|
127
|
+
|
|
128
|
+
conversations.forEach(conv => {
|
|
129
|
+
// Try to extract MCP usage from conversation metadata
|
|
130
|
+
// This is a simplified implementation - adjust based on actual data structure
|
|
131
|
+
if (conv.mcpServers && Array.isArray(conv.mcpServers)) {
|
|
132
|
+
conv.mcpServers.forEach(mcp => mcpSet.add(mcp));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const count = mcpSet.size;
|
|
137
|
+
return {
|
|
138
|
+
total: count,
|
|
139
|
+
formatted: count >= 1000 ? `${(count / 1000).toFixed(1)}K` : count.toString()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Analyze model usage patterns
|
|
145
|
+
* @param {Array} conversations - 2025 conversations
|
|
146
|
+
* @returns {Array} Top models used
|
|
147
|
+
*/
|
|
148
|
+
analyzeModelUsage(conversations) {
|
|
149
|
+
const modelCounts = new Map();
|
|
150
|
+
|
|
151
|
+
conversations.forEach(conv => {
|
|
152
|
+
if (conv.modelInfo && conv.modelInfo.primaryModel) {
|
|
153
|
+
const model = conv.modelInfo.primaryModel;
|
|
154
|
+
modelCounts.set(model, (modelCounts.get(model) || 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Convert to array and sort by usage
|
|
159
|
+
const models = Array.from(modelCounts.entries())
|
|
160
|
+
.map(([name, count]) => ({
|
|
161
|
+
name: this.formatModelName(name),
|
|
162
|
+
count,
|
|
163
|
+
percentage: (count / conversations.length * 100).toFixed(1)
|
|
164
|
+
}))
|
|
165
|
+
.sort((a, b) => b.count - a.count);
|
|
166
|
+
|
|
167
|
+
return models.slice(0, 3); // Top 3 models
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Format model name for display
|
|
172
|
+
* @param {string} modelName - Raw model name
|
|
173
|
+
* @returns {string} Formatted model name
|
|
174
|
+
*/
|
|
175
|
+
formatModelName(modelName) {
|
|
176
|
+
if (!modelName) return 'Auto';
|
|
177
|
+
|
|
178
|
+
// Map common model names
|
|
179
|
+
const nameMap = {
|
|
180
|
+
'claude-3-5-sonnet': 'Claude 3.5 Sonnet',
|
|
181
|
+
'claude-3-sonnet': 'Claude 3 Sonnet',
|
|
182
|
+
'claude-3-opus': 'Claude 3 Opus',
|
|
183
|
+
'claude-3-haiku': 'Claude 3 Haiku',
|
|
184
|
+
'claude-sonnet-4-5-20250929': 'Claude 4.5 Sonnet',
|
|
185
|
+
'claude-opus-4-5-20251101': 'Claude Opus 4.5'
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return nameMap[modelName] || modelName;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Count unique agents used
|
|
193
|
+
* @param {Array} conversations - 2025 conversations
|
|
194
|
+
* @returns {number} Number of unique agents
|
|
195
|
+
*/
|
|
196
|
+
countAgents(conversations) {
|
|
197
|
+
const agents = new Set();
|
|
198
|
+
|
|
199
|
+
conversations.forEach(conv => {
|
|
200
|
+
// Count unique agent sessions/conversations
|
|
201
|
+
if (conv.id) {
|
|
202
|
+
agents.add(conv.id);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Return count in thousands for display
|
|
207
|
+
const count = agents.size;
|
|
208
|
+
return {
|
|
209
|
+
total: count,
|
|
210
|
+
formatted: count >= 1000 ? `${(count / 1000).toFixed(1)}K` : count.toString()
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Calculate total token usage
|
|
217
|
+
* @param {Array} conversations - 2025 conversations
|
|
218
|
+
* @returns {Object} Token usage information
|
|
219
|
+
*/
|
|
220
|
+
calculateTokenUsage(conversations) {
|
|
221
|
+
let totalTokens = 0;
|
|
222
|
+
let inputTokens = 0;
|
|
223
|
+
let outputTokens = 0;
|
|
224
|
+
let cacheTokens = 0;
|
|
225
|
+
|
|
226
|
+
conversations.forEach(conv => {
|
|
227
|
+
if (conv.tokenUsage) {
|
|
228
|
+
totalTokens += conv.tokenUsage.total || 0;
|
|
229
|
+
inputTokens += conv.tokenUsage.inputTokens || 0;
|
|
230
|
+
outputTokens += conv.tokenUsage.outputTokens || 0;
|
|
231
|
+
cacheTokens += conv.tokenUsage.cacheReadTokens || 0;
|
|
232
|
+
} else {
|
|
233
|
+
// Fallback to estimated tokens
|
|
234
|
+
totalTokens += conv.tokens || 0;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Format in billions
|
|
239
|
+
const billions = totalTokens / 1000000000;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
total: totalTokens,
|
|
243
|
+
billions: billions.toFixed(2),
|
|
244
|
+
formatted: billions >= 1 ? `${billions.toFixed(2)}B` : `${(totalTokens / 1000000).toFixed(0)}M`,
|
|
245
|
+
breakdown: {
|
|
246
|
+
input: inputTokens,
|
|
247
|
+
output: outputTokens,
|
|
248
|
+
cache: cacheTokens
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Calculate longest and current streak
|
|
255
|
+
* @param {Array} conversations - 2025 conversations
|
|
256
|
+
* @returns {Object} Streak information
|
|
257
|
+
*/
|
|
258
|
+
calculateStreak(conversations) {
|
|
259
|
+
if (conversations.length === 0) {
|
|
260
|
+
return { longest: 0, current: 0, formatted: '0d' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Create set of active days
|
|
264
|
+
const activeDays = new Set();
|
|
265
|
+
conversations.forEach(conv => {
|
|
266
|
+
if (conv.lastModified) {
|
|
267
|
+
const date = new Date(conv.lastModified);
|
|
268
|
+
const dayKey = date.toISOString().split('T')[0];
|
|
269
|
+
activeDays.add(dayKey);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Convert to sorted array
|
|
274
|
+
const sortedDays = Array.from(activeDays).sort();
|
|
275
|
+
|
|
276
|
+
// Calculate longest streak
|
|
277
|
+
let longestStreak = 1;
|
|
278
|
+
let currentStreakCount = 1;
|
|
279
|
+
|
|
280
|
+
for (let i = 1; i < sortedDays.length; i++) {
|
|
281
|
+
const prevDate = new Date(sortedDays[i - 1]);
|
|
282
|
+
const currDate = new Date(sortedDays[i]);
|
|
283
|
+
|
|
284
|
+
const diffDays = Math.round((currDate - prevDate) / (1000 * 60 * 60 * 24));
|
|
285
|
+
|
|
286
|
+
if (diffDays === 1) {
|
|
287
|
+
currentStreakCount++;
|
|
288
|
+
longestStreak = Math.max(longestStreak, currentStreakCount);
|
|
289
|
+
} else {
|
|
290
|
+
currentStreakCount = 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Calculate current streak (from most recent day)
|
|
295
|
+
const today = new Date();
|
|
296
|
+
today.setHours(0, 0, 0, 0);
|
|
297
|
+
let currentStreak = 0;
|
|
298
|
+
|
|
299
|
+
const lastDay = new Date(sortedDays[sortedDays.length - 1]);
|
|
300
|
+
const daysSinceLastActivity = Math.round((today - lastDay) / (1000 * 60 * 60 * 24));
|
|
301
|
+
|
|
302
|
+
if (daysSinceLastActivity <= 1) {
|
|
303
|
+
// Activity today or yesterday, calculate backward streak
|
|
304
|
+
let checkDate = new Date(lastDay);
|
|
305
|
+
currentStreak = 1;
|
|
306
|
+
|
|
307
|
+
for (let i = sortedDays.length - 2; i >= 0; i--) {
|
|
308
|
+
const prevDate = new Date(sortedDays[i]);
|
|
309
|
+
const diff = Math.round((checkDate - prevDate) / (1000 * 60 * 60 * 24));
|
|
310
|
+
|
|
311
|
+
if (diff === 1) {
|
|
312
|
+
currentStreak++;
|
|
313
|
+
checkDate = prevDate;
|
|
314
|
+
} else {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
longest: longestStreak,
|
|
322
|
+
current: currentStreak,
|
|
323
|
+
formatted: `${longestStreak}d`,
|
|
324
|
+
activeDays: activeDays.size
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Calculate number of active days
|
|
330
|
+
* @param {Array} conversations - Conversations
|
|
331
|
+
* @returns {number} Number of active days
|
|
332
|
+
*/
|
|
333
|
+
calculateActiveDays(conversations) {
|
|
334
|
+
const activeDays = new Set();
|
|
335
|
+
conversations.forEach(conv => {
|
|
336
|
+
if (conv.lastModified) {
|
|
337
|
+
const date = new Date(conv.lastModified);
|
|
338
|
+
const dayKey = date.toISOString().split('T')[0];
|
|
339
|
+
activeDays.add(dayKey);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return activeDays.size;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Generate activity heatmap data (GitHub-style)
|
|
347
|
+
* @param {Array} conversations - 2025 conversations
|
|
348
|
+
* @returns {Array} Heatmap data by week
|
|
349
|
+
*/
|
|
350
|
+
generateActivityHeatmap(conversations) {
|
|
351
|
+
// Create map of day -> activity count, tools, and models
|
|
352
|
+
const dailyActivity = new Map();
|
|
353
|
+
|
|
354
|
+
conversations.forEach(conv => {
|
|
355
|
+
if (conv.lastModified) {
|
|
356
|
+
const date = new Date(conv.lastModified);
|
|
357
|
+
const dayKey = date.toISOString().split('T')[0];
|
|
358
|
+
|
|
359
|
+
const current = dailyActivity.get(dayKey) || {
|
|
360
|
+
count: 0,
|
|
361
|
+
tools: [],
|
|
362
|
+
models: [],
|
|
363
|
+
modelCounts: {},
|
|
364
|
+
toolCounts: {}
|
|
365
|
+
};
|
|
366
|
+
current.count += 1;
|
|
367
|
+
|
|
368
|
+
// Count tool usage with actual numbers from toolStats
|
|
369
|
+
if (conv.toolUsage && conv.toolUsage.toolStats) {
|
|
370
|
+
Object.entries(conv.toolUsage.toolStats).forEach(([tool, count]) => {
|
|
371
|
+
if (!current.tools.includes(tool)) {
|
|
372
|
+
current.tools.push(tool);
|
|
373
|
+
}
|
|
374
|
+
// Add the actual count from this conversation
|
|
375
|
+
current.toolCounts[tool] = (current.toolCounts[tool] || 0) + count;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Count model usage using totalToolCalls as proxy for activity
|
|
380
|
+
if (conv.modelInfo && conv.modelInfo.primaryModel) {
|
|
381
|
+
const model = conv.modelInfo.primaryModel;
|
|
382
|
+
if (!current.models.includes(model)) {
|
|
383
|
+
current.models.push(model);
|
|
384
|
+
}
|
|
385
|
+
// Use totalToolCalls as proxy for model activity (each tool call ≈ one model response)
|
|
386
|
+
const activityCount = (conv.toolUsage && conv.toolUsage.totalToolCalls) || 1;
|
|
387
|
+
current.modelCounts[model] = (current.modelCounts[model] || 0) + activityCount;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
dailyActivity.set(dayKey, current);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Generate full year grid (52-53 weeks)
|
|
395
|
+
const weeks = [];
|
|
396
|
+
const startDate = new Date(this.yearStart);
|
|
397
|
+
|
|
398
|
+
// Start from first Sunday of the year or week before
|
|
399
|
+
const firstDay = startDate.getDay();
|
|
400
|
+
if (firstDay !== 0) {
|
|
401
|
+
startDate.setDate(startDate.getDate() - firstDay);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let currentWeek = [];
|
|
405
|
+
let currentDate = new Date(startDate);
|
|
406
|
+
|
|
407
|
+
while (currentDate <= this.yearEnd || currentWeek.length > 0) {
|
|
408
|
+
const dayKey = currentDate.toISOString().split('T')[0];
|
|
409
|
+
const dayData = dailyActivity.get(dayKey) || {
|
|
410
|
+
count: 0,
|
|
411
|
+
tools: [],
|
|
412
|
+
models: [],
|
|
413
|
+
toolCounts: {},
|
|
414
|
+
modelCounts: {}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Determine intensity level (0-4 like GitHub)
|
|
418
|
+
let level = 0;
|
|
419
|
+
if (dayData.count > 0) level = 1;
|
|
420
|
+
if (dayData.count >= 3) level = 2;
|
|
421
|
+
if (dayData.count >= 6) level = 3;
|
|
422
|
+
if (dayData.count >= 10) level = 4;
|
|
423
|
+
|
|
424
|
+
currentWeek.push({
|
|
425
|
+
date: dayKey,
|
|
426
|
+
count: dayData.count,
|
|
427
|
+
tools: dayData.tools,
|
|
428
|
+
models: dayData.models,
|
|
429
|
+
toolCounts: dayData.toolCounts,
|
|
430
|
+
modelCounts: dayData.modelCounts,
|
|
431
|
+
level,
|
|
432
|
+
day: currentDate.getDay()
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// If Sunday (end of week) or last day, push week
|
|
436
|
+
if (currentDate.getDay() === 6 || currentDate > this.yearEnd) {
|
|
437
|
+
weeks.push([...currentWeek]);
|
|
438
|
+
currentWeek = [];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
442
|
+
|
|
443
|
+
// Safety check
|
|
444
|
+
if (weeks.length > 60) break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return weeks;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Analyze top projects worked on
|
|
453
|
+
* @param {Array} conversations - 2025 conversations
|
|
454
|
+
* @returns {Array} Top projects
|
|
455
|
+
*/
|
|
456
|
+
analyzeTopProjects(conversations) {
|
|
457
|
+
const projectActivity = new Map();
|
|
458
|
+
|
|
459
|
+
conversations.forEach(conv => {
|
|
460
|
+
const project = conv.project || 'Unknown';
|
|
461
|
+
const current = projectActivity.get(project) || { count: 0, tokens: 0 };
|
|
462
|
+
|
|
463
|
+
current.count += 1;
|
|
464
|
+
current.tokens += conv.tokens || 0;
|
|
465
|
+
|
|
466
|
+
projectActivity.set(project, current);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return Array.from(projectActivity.entries())
|
|
470
|
+
.map(([name, data]) => ({
|
|
471
|
+
name,
|
|
472
|
+
conversations: data.count,
|
|
473
|
+
tokens: data.tokens
|
|
474
|
+
}))
|
|
475
|
+
.sort((a, b) => b.conversations - a.conversations)
|
|
476
|
+
.slice(0, 5);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Analyze tool usage patterns
|
|
481
|
+
* @param {Array} conversations - 2025 conversations
|
|
482
|
+
* @returns {Object} Tool usage statistics
|
|
483
|
+
*/
|
|
484
|
+
analyzeToolUsage(conversations) {
|
|
485
|
+
let totalToolCalls = 0;
|
|
486
|
+
const toolTypes = new Map();
|
|
487
|
+
|
|
488
|
+
conversations.forEach(conv => {
|
|
489
|
+
if (conv.toolUsage) {
|
|
490
|
+
totalToolCalls += conv.toolUsage.totalToolCalls || 0;
|
|
491
|
+
|
|
492
|
+
if (conv.toolUsage.toolStats) {
|
|
493
|
+
Object.entries(conv.toolUsage.toolStats).forEach(([tool, count]) => {
|
|
494
|
+
toolTypes.set(tool, (toolTypes.get(tool) || 0) + count);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const topTools = Array.from(toolTypes.entries())
|
|
501
|
+
.map(([name, count]) => ({ name, count }))
|
|
502
|
+
.sort((a, b) => b.count - a.count)
|
|
503
|
+
.slice(0, 5);
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
total: totalToolCalls,
|
|
507
|
+
topTools
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Analyze time of day usage patterns
|
|
513
|
+
* @param {Array} conversations - 2025 conversations
|
|
514
|
+
* @returns {Object} Time of day statistics
|
|
515
|
+
*/
|
|
516
|
+
analyzeTimeOfDay(conversations) {
|
|
517
|
+
const hourCounts = new Array(24).fill(0);
|
|
518
|
+
|
|
519
|
+
conversations.forEach(conv => {
|
|
520
|
+
if (conv.lastModified) {
|
|
521
|
+
const date = new Date(conv.lastModified);
|
|
522
|
+
const hour = date.getHours();
|
|
523
|
+
hourCounts[hour]++;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Find peak hour
|
|
528
|
+
let peakHour = 0;
|
|
529
|
+
let peakCount = 0;
|
|
530
|
+
|
|
531
|
+
hourCounts.forEach((count, hour) => {
|
|
532
|
+
if (count > peakCount) {
|
|
533
|
+
peakCount = count;
|
|
534
|
+
peakHour = hour;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
peakHour,
|
|
540
|
+
peakHourFormatted: `${peakHour}:00 - ${peakHour + 1}:00`,
|
|
541
|
+
hourlyDistribution: hourCounts
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Detect installed components from .claude directory
|
|
547
|
+
* @returns {Promise<Array>} List of component installations
|
|
548
|
+
*/
|
|
549
|
+
async detectInstalledComponents() {
|
|
550
|
+
const components = [];
|
|
551
|
+
const os = require('os');
|
|
552
|
+
const fs = require('fs-extra');
|
|
553
|
+
const path = require('path');
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
557
|
+
const localDir = path.join(claudeDir, 'local');
|
|
558
|
+
|
|
559
|
+
// Check if local directory exists
|
|
560
|
+
if (await fs.pathExists(localDir)) {
|
|
561
|
+
const settings = await fs.readJSON(path.join(claudeDir, 'settings.json')).catch(() => ({}));
|
|
562
|
+
|
|
563
|
+
// Extract MCPs from settings
|
|
564
|
+
if (settings.mcpServers) {
|
|
565
|
+
Object.keys(settings.mcpServers).forEach(mcpName => {
|
|
566
|
+
components.push({
|
|
567
|
+
type: 'mcp',
|
|
568
|
+
name: mcpName,
|
|
569
|
+
date: new Date('2025-01-15') // Default date - could be improved
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Extract enabled plugins
|
|
575
|
+
if (settings.enabledPlugins) {
|
|
576
|
+
Object.entries(settings.enabledPlugins).forEach(([pluginName, enabled]) => {
|
|
577
|
+
if (enabled) {
|
|
578
|
+
components.push({
|
|
579
|
+
type: 'skill',
|
|
580
|
+
name: pluginName.split('@')[0],
|
|
581
|
+
date: new Date('2025-02-01')
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.warn('Could not detect installed components:', error.message);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return components;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Generate insights and fun facts
|
|
596
|
+
* @param {Array} conversations - 2025 conversations
|
|
597
|
+
* @returns {Array} Insights
|
|
598
|
+
*/
|
|
599
|
+
generateInsights(conversations) {
|
|
600
|
+
const insights = [];
|
|
601
|
+
|
|
602
|
+
// Total messages
|
|
603
|
+
const totalMessages = conversations.reduce((sum, conv) => sum + (conv.messageCount || 0), 0);
|
|
604
|
+
insights.push(`Sent ${totalMessages.toLocaleString()} messages`);
|
|
605
|
+
|
|
606
|
+
// Average session length
|
|
607
|
+
const avgMessages = Math.round(totalMessages / conversations.length);
|
|
608
|
+
insights.push(`Average ${avgMessages} messages per conversation`);
|
|
609
|
+
|
|
610
|
+
// Most productive day
|
|
611
|
+
const dailyActivity = new Map();
|
|
612
|
+
conversations.forEach(conv => {
|
|
613
|
+
if (conv.lastModified) {
|
|
614
|
+
const dayKey = new Date(conv.lastModified).toISOString().split('T')[0];
|
|
615
|
+
dailyActivity.set(dayKey, (dailyActivity.get(dayKey) || 0) + 1);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const mostProductiveDay = Array.from(dailyActivity.entries())
|
|
620
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
621
|
+
|
|
622
|
+
if (mostProductiveDay) {
|
|
623
|
+
const date = new Date(mostProductiveDay[0]);
|
|
624
|
+
insights.push(`Most productive: ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return insights;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Analyze command usage from history
|
|
632
|
+
* @returns {Promise<Object>} Command usage data
|
|
633
|
+
*/
|
|
634
|
+
async analyzeCommands() {
|
|
635
|
+
const fs = require('fs-extra');
|
|
636
|
+
const os = require('os');
|
|
637
|
+
const path = require('path');
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl');
|
|
641
|
+
if (!await fs.pathExists(historyPath)) {
|
|
642
|
+
return { commands: [], total: 0, events: [] };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const content = await fs.readFile(historyPath, 'utf8');
|
|
646
|
+
const lines = content.trim().split('\n').filter(l => l.trim());
|
|
647
|
+
|
|
648
|
+
const commandCounts = new Map();
|
|
649
|
+
const commandEvents = []; // Track each command execution with timestamp
|
|
650
|
+
|
|
651
|
+
lines.forEach(line => {
|
|
652
|
+
try {
|
|
653
|
+
const entry = JSON.parse(line);
|
|
654
|
+
if (entry.display && entry.display.startsWith('/')) {
|
|
655
|
+
const cmd = entry.display.trim();
|
|
656
|
+
// Extract base command (before space or arguments)
|
|
657
|
+
const baseCmd = cmd.split(' ')[0];
|
|
658
|
+
commandCounts.set(baseCmd, (commandCounts.get(baseCmd) || 0) + 1);
|
|
659
|
+
|
|
660
|
+
// Extract timestamp and add to events
|
|
661
|
+
if (entry.timestamp) {
|
|
662
|
+
const timestamp = new Date(entry.timestamp);
|
|
663
|
+
// Only include 2025 events
|
|
664
|
+
if (timestamp.getFullYear() === 2025) {
|
|
665
|
+
commandEvents.push({
|
|
666
|
+
name: baseCmd,
|
|
667
|
+
timestamp: timestamp,
|
|
668
|
+
display: entry.display
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {
|
|
674
|
+
// Skip invalid lines
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const commands = Array.from(commandCounts.entries())
|
|
679
|
+
.map(([name, count]) => ({ name, count }))
|
|
680
|
+
.sort((a, b) => b.count - a.count);
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
commands: commands.slice(0, 20), // Top 20 commands
|
|
684
|
+
total: commands.reduce((sum, cmd) => sum + cmd.count, 0),
|
|
685
|
+
events: commandEvents.sort((a, b) => a.timestamp - b.timestamp) // Sort by date
|
|
686
|
+
};
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.warn('Could not analyze commands:', error.message);
|
|
689
|
+
return { commands: [], total: 0, events: [] };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Analyze installed skills
|
|
695
|
+
* @returns {Promise<Object>} Skills data
|
|
696
|
+
*/
|
|
697
|
+
async analyzeSkills() {
|
|
698
|
+
const fs = require('fs-extra');
|
|
699
|
+
const os = require('os');
|
|
700
|
+
const path = require('path');
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const skillsPath = path.join(os.homedir(), '.claude', 'skills');
|
|
704
|
+
if (!await fs.pathExists(skillsPath)) {
|
|
705
|
+
return { skills: [], total: 0, events: [] };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const items = await fs.readdir(skillsPath);
|
|
709
|
+
const skills = [];
|
|
710
|
+
const skillEvents = [];
|
|
711
|
+
|
|
712
|
+
for (const item of items) {
|
|
713
|
+
const itemPath = path.join(skillsPath, item);
|
|
714
|
+
const stats = await fs.stat(itemPath);
|
|
715
|
+
if (stats.isDirectory()) {
|
|
716
|
+
const installedAt = stats.birthtime;
|
|
717
|
+
skills.push({
|
|
718
|
+
name: item,
|
|
719
|
+
installedAt: installedAt
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Only include 2025 skills
|
|
723
|
+
if (installedAt.getFullYear() === 2025) {
|
|
724
|
+
skillEvents.push({
|
|
725
|
+
name: item,
|
|
726
|
+
timestamp: installedAt
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
skills: skills.sort((a, b) => b.installedAt - a.installedAt),
|
|
734
|
+
total: skills.length,
|
|
735
|
+
events: skillEvents.sort((a, b) => a.timestamp - b.timestamp)
|
|
736
|
+
};
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.warn('Could not analyze skills:', error.message);
|
|
739
|
+
return { skills: [], total: 0, events: [] };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Analyze installed MCPs
|
|
745
|
+
* @returns {Promise<Object>} MCPs data
|
|
746
|
+
*/
|
|
747
|
+
async analyzeMCPs() {
|
|
748
|
+
const fs = require('fs-extra');
|
|
749
|
+
const os = require('os');
|
|
750
|
+
const path = require('path');
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
754
|
+
if (!await fs.pathExists(settingsPath)) {
|
|
755
|
+
return { mcps: [], total: 0, events: [] };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
759
|
+
const settings = JSON.parse(content);
|
|
760
|
+
const stats = await fs.stat(settingsPath);
|
|
761
|
+
const modifiedAt = stats.mtime; // Use file modification time as fallback
|
|
762
|
+
|
|
763
|
+
const mcps = [];
|
|
764
|
+
const mcpEvents = [];
|
|
765
|
+
|
|
766
|
+
if (settings.enabledPlugins) {
|
|
767
|
+
Object.entries(settings.enabledPlugins).forEach(([name, enabled], index) => {
|
|
768
|
+
if (enabled) {
|
|
769
|
+
mcps.push({ name, enabled });
|
|
770
|
+
|
|
771
|
+
// Only include 2025 MCPs
|
|
772
|
+
// Use modification time with slight offset for each MCP
|
|
773
|
+
if (modifiedAt.getFullYear() === 2025) {
|
|
774
|
+
const timestamp = new Date(modifiedAt.getTime() + (index * 1000)); // 1 second apart
|
|
775
|
+
mcpEvents.push({
|
|
776
|
+
name: name,
|
|
777
|
+
timestamp: timestamp
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
mcps: mcps,
|
|
786
|
+
total: mcps.length,
|
|
787
|
+
events: mcpEvents.sort((a, b) => a.timestamp - b.timestamp)
|
|
788
|
+
};
|
|
789
|
+
} catch (error) {
|
|
790
|
+
console.warn('Could not analyze MCPs:', error.message);
|
|
791
|
+
return { mcps: [], total: 0, events: [] };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Analyze subagent usage
|
|
797
|
+
* @param {string} claudeDir - Claude directory path
|
|
798
|
+
* @returns {Promise<Object>} Subagents data
|
|
799
|
+
*/
|
|
800
|
+
async analyzeSubagents(claudeDir) {
|
|
801
|
+
const fs = require('fs-extra');
|
|
802
|
+
const path = require('path');
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const projectsDir = path.join(claudeDir, 'projects');
|
|
806
|
+
if (!await fs.pathExists(projectsDir)) {
|
|
807
|
+
return { subagents: [], total: 0, events: [] };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const agentData = new Map(); // Map of agent ID -> { id, timestamp }
|
|
811
|
+
const projects = await fs.readdir(projectsDir);
|
|
812
|
+
|
|
813
|
+
// Search for agent files in all project directories
|
|
814
|
+
for (const project of projects) {
|
|
815
|
+
const projectPath = path.join(projectsDir, project);
|
|
816
|
+
const stats = await fs.stat(projectPath);
|
|
817
|
+
|
|
818
|
+
if (stats.isDirectory()) {
|
|
819
|
+
const files = await fs.readdir(projectPath);
|
|
820
|
+
const agentFiles = files.filter(f => f.startsWith('agent-') && f.endsWith('.jsonl'));
|
|
821
|
+
|
|
822
|
+
for (const file of agentFiles) {
|
|
823
|
+
const match = file.match(/agent-(.+)\.jsonl$/);
|
|
824
|
+
if (match) {
|
|
825
|
+
const agentId = match[1];
|
|
826
|
+
|
|
827
|
+
// Read only first 2 lines for performance (don't read entire file)
|
|
828
|
+
try {
|
|
829
|
+
const filePath = path.join(projectPath, file);
|
|
830
|
+
const readline = require('readline');
|
|
831
|
+
const fileStream = require('fs').createReadStream(filePath);
|
|
832
|
+
const rl = readline.createInterface({
|
|
833
|
+
input: fileStream,
|
|
834
|
+
crlfDelay: Infinity
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
let timestamp = null;
|
|
838
|
+
let agentType = 'Agent';
|
|
839
|
+
let lineCount = 0;
|
|
840
|
+
const lines = [];
|
|
841
|
+
|
|
842
|
+
// Read only first 2 lines
|
|
843
|
+
for await (const line of rl) {
|
|
844
|
+
if (line.trim()) {
|
|
845
|
+
lines.push(line.trim());
|
|
846
|
+
lineCount++;
|
|
847
|
+
if (lineCount >= 2) break; // Stop after 2 lines
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
rl.close();
|
|
851
|
+
fileStream.close();
|
|
852
|
+
|
|
853
|
+
// Get timestamp from first line
|
|
854
|
+
if (lines[0]) {
|
|
855
|
+
const firstEntry = JSON.parse(lines[0]);
|
|
856
|
+
if (firstEntry.timestamp) {
|
|
857
|
+
timestamp = new Date(firstEntry.timestamp);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Try to extract agent type from second line (assistant response)
|
|
862
|
+
if (lines[1]) {
|
|
863
|
+
const secondEntry = JSON.parse(lines[1]);
|
|
864
|
+
if (secondEntry.message && secondEntry.message.content) {
|
|
865
|
+
const content = Array.isArray(secondEntry.message.content)
|
|
866
|
+
? secondEntry.message.content[0]?.text
|
|
867
|
+
: secondEntry.message.content;
|
|
868
|
+
|
|
869
|
+
// Look for agent type indicators in the response (check first 500 chars only)
|
|
870
|
+
if (content) {
|
|
871
|
+
const lowerContent = content.substring(0, 500).toLowerCase();
|
|
872
|
+
|
|
873
|
+
// Plan agents
|
|
874
|
+
if (lowerContent.includes('planning mode') ||
|
|
875
|
+
lowerContent.includes('read-only')) {
|
|
876
|
+
agentType = 'Plan';
|
|
877
|
+
}
|
|
878
|
+
// Explore agents
|
|
879
|
+
else if (lowerContent.includes('explore') ||
|
|
880
|
+
lowerContent.includes('search')) {
|
|
881
|
+
agentType = 'Explore';
|
|
882
|
+
}
|
|
883
|
+
// Default to Plan for most agents
|
|
884
|
+
else {
|
|
885
|
+
agentType = 'Plan';
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Only store the earliest timestamp for each agent
|
|
892
|
+
if (!agentData.has(agentId) || (timestamp && timestamp < agentData.get(agentId).timestamp)) {
|
|
893
|
+
agentData.set(agentId, { id: agentId, timestamp, type: agentType });
|
|
894
|
+
}
|
|
895
|
+
} catch (e) {
|
|
896
|
+
// If we can't read timestamp, just add the agent without timestamp
|
|
897
|
+
if (!agentData.has(agentId)) {
|
|
898
|
+
agentData.set(agentId, { id: agentId, timestamp: null, type: 'Agent' });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const subagents = Array.from(agentData.values());
|
|
907
|
+
|
|
908
|
+
// Group subagents by type for cleaner visualization
|
|
909
|
+
const groupedByType = {};
|
|
910
|
+
subagents.forEach(agent => {
|
|
911
|
+
const type = agent.type || 'Unknown';
|
|
912
|
+
if (!groupedByType[type]) {
|
|
913
|
+
groupedByType[type] = { count: 0, timestamps: [] };
|
|
914
|
+
}
|
|
915
|
+
groupedByType[type].count++;
|
|
916
|
+
if (agent.timestamp && agent.timestamp.getFullYear() === 2025) {
|
|
917
|
+
groupedByType[type].timestamps.push(agent.timestamp);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Create events grouped by type (one event per type per day)
|
|
922
|
+
const subagentEvents = [];
|
|
923
|
+
Object.entries(groupedByType).forEach(([type, data]) => {
|
|
924
|
+
// Group timestamps by day to avoid too many events
|
|
925
|
+
const dayMap = new Map();
|
|
926
|
+
data.timestamps.forEach(ts => {
|
|
927
|
+
const dayKey = ts.toISOString().split('T')[0];
|
|
928
|
+
if (!dayMap.has(dayKey)) {
|
|
929
|
+
dayMap.set(dayKey, { count: 0, timestamp: ts });
|
|
930
|
+
}
|
|
931
|
+
dayMap.get(dayKey).count++;
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Create one event per day per type
|
|
935
|
+
dayMap.forEach((dayData, dayKey) => {
|
|
936
|
+
subagentEvents.push({
|
|
937
|
+
name: type, // Just "Plan" or "Explore", not "Plan-a68a"
|
|
938
|
+
timestamp: dayData.timestamp,
|
|
939
|
+
type: type,
|
|
940
|
+
count: dayData.count // How many times used that day
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
subagentEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
subagents: Object.entries(groupedByType).map(([type, data]) => ({
|
|
949
|
+
type,
|
|
950
|
+
count: data.count
|
|
951
|
+
})),
|
|
952
|
+
total: subagents.length,
|
|
953
|
+
events: subagentEvents,
|
|
954
|
+
grouped: groupedByType // Include grouped data for display
|
|
955
|
+
};
|
|
956
|
+
} catch (error) {
|
|
957
|
+
console.warn('Could not analyze subagents:', error.message);
|
|
958
|
+
return { subagents: [], total: 0, events: [] };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
module.exports = YearInReview2025;
|