claude-code-templates 1.28.2 → 1.28.4

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.
@@ -0,0 +1,928 @@
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
+ const subagentEvents = subagents
908
+ .filter(agent => agent.timestamp && agent.timestamp.getFullYear() === 2025)
909
+ .map(agent => ({
910
+ name: `${agent.type}-${agent.id.substring(0, 4)}`, // e.g., "Plan-a68a" or "Explore-a1e4"
911
+ timestamp: agent.timestamp,
912
+ type: agent.type
913
+ }))
914
+ .sort((a, b) => a.timestamp - b.timestamp);
915
+
916
+ return {
917
+ subagents: subagents.map(a => ({ id: a.id, type: a.type })),
918
+ total: subagents.length,
919
+ events: subagentEvents
920
+ };
921
+ } catch (error) {
922
+ console.warn('Could not analyze subagents:', error.message);
923
+ return { subagents: [], total: 0, events: [] };
924
+ }
925
+ }
926
+ }
927
+
928
+ module.exports = YearInReview2025;