UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

984 lines (838 loc) • 30.7 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); /** * Year in Review 2025 Analyzer * Generates comprehensive yearly statistics for Claude Code usage in 2025 */ class YearInReview2025 { constructor() { this.year = 2025; this.yearStart = new Date('2025-01-01T00:00:00.000Z'); this.yearEnd = new Date('2025-12-31T23:59:59.999Z'); } /** * Generate complete 2025 year in review statistics * @param {Array} allConversations - All conversations from analytics * @param {string} claudeDir - Claude directory path for subagent analysis * @returns {Object} Complete year in review data */ async generateYearInReview(allConversations, claudeDir) { // Filter conversations from 2025 const conversations2025 = this.filterConversations2025(allConversations); console.log(`šŸ“… Analyzing ${conversations2025.length} conversations from 2025...`); // Detect installed components const installedComponents = await this.detectInstalledComponents(); // Analyze commands, skills, MCPs, and subagents const [commandsData, skillsData, mcpsData, subagentsData] = await Promise.all([ this.analyzeCommands(), this.analyzeSkills(), this.analyzeMCPs(), claudeDir ? this.analyzeSubagents(claudeDir) : { subagents: [], total: 0 } ]); // Calculate all statistics const stats = { // Basic stats totalConversations: conversations2025.length, // Model usage models: this.analyzeModelUsage(conversations2025), // Tool usage, Agents, and MCPs toolsCount: this.countTools(conversations2025), agentsCount: this.countAgents(conversations2025), mcpCount: this.countMCPs(conversations2025), // Token usage tokens: this.calculateTokenUsage(conversations2025), // Streak analysis streak: this.calculateStreak(conversations2025), // Activity heatmap data (GitHub-style contribution graph) activityHeatmap: this.generateActivityHeatmap(conversations2025), // Installed components for Gource visualization componentInstalls: installedComponents, // Top projects topProjects: this.analyzeTopProjects(conversations2025), // Tool usage details toolUsage: this.analyzeToolUsage(conversations2025), // Time of day analysis timeOfDay: this.analyzeTimeOfDay(conversations2025), // Additional insights insights: this.generateInsights(conversations2025), // Commands, Skills, MCPs, Subagents (new data) commands: commandsData, skills: skillsData, mcps: mcpsData, subagents: subagentsData }; return stats; } /** * Filter conversations from 2025 * @param {Array} conversations - All conversations * @returns {Array} Conversations from 2025 */ filterConversations2025(conversations) { return conversations.filter(conv => { if (!conv.lastModified) return false; const date = new Date(conv.lastModified); return date >= this.yearStart && date <= this.yearEnd; }); } /** * Count total tool calls across all conversations * @param {Array} conversations - 2025 conversations * @returns {Object} Tools count information */ countTools(conversations) { let totalTools = 0; conversations.forEach(conv => { if (conv.toolUsage && conv.toolUsage.totalToolCalls) { totalTools += conv.toolUsage.totalToolCalls; } }); return { total: totalTools, formatted: totalTools >= 1000 ? `${(totalTools / 1000).toFixed(1)}K` : totalTools.toString() }; } /** * Count unique MCPs used across conversations * @param {Array} conversations - 2025 conversations * @returns {Object} MCP count information */ countMCPs(conversations) { const mcpSet = new Set(); conversations.forEach(conv => { // Try to extract MCP usage from conversation metadata // This is a simplified implementation - adjust based on actual data structure if (conv.mcpServers && Array.isArray(conv.mcpServers)) { conv.mcpServers.forEach(mcp => mcpSet.add(mcp)); } }); const count = mcpSet.size; return { total: count, formatted: count >= 1000 ? `${(count / 1000).toFixed(1)}K` : count.toString() }; } /** * Analyze model usage patterns * @param {Array} conversations - 2025 conversations * @returns {Array} Top models used */ analyzeModelUsage(conversations) { const modelCounts = new Map(); conversations.forEach(conv => { if (conv.modelInfo && conv.modelInfo.primaryModel) { const model = conv.modelInfo.primaryModel; modelCounts.set(model, (modelCounts.get(model) || 0) + 1); } }); // Convert to array and sort by usage const models = Array.from(modelCounts.entries()) .map(([name, count]) => ({ name: this.formatModelName(name), count, percentage: (count / conversations.length * 100).toFixed(1) })) .sort((a, b) => b.count - a.count); return models.slice(0, 3); // Top 3 models } /** * Format model name for display * @param {string} modelName - Raw model name * @returns {string} Formatted model name */ formatModelName(modelName) { if (!modelName) return 'Auto'; // Map common model names const nameMap = { 'claude-3-5-sonnet': 'Claude 3.5 Sonnet', 'claude-3-sonnet': 'Claude 3 Sonnet', 'claude-3-opus': 'Claude 3 Opus', 'claude-3-haiku': 'Claude 3 Haiku', 'claude-sonnet-4-5-20250929': 'Claude 4.5 Sonnet', 'claude-opus-4-5-20251101': 'Claude Opus 4.5' }; return nameMap[modelName] || modelName; } /** * Count unique agents used * @param {Array} conversations - 2025 conversations * @returns {number} Number of unique agents */ countAgents(conversations) { const agents = new Set(); conversations.forEach(conv => { // Count unique agent sessions/conversations if (conv.id) { agents.add(conv.id); } }); // Return count in thousands for display const count = agents.size; return { total: count, formatted: count >= 1000 ? `${(count / 1000).toFixed(1)}K` : count.toString() }; } /** * Calculate total token usage * @param {Array} conversations - 2025 conversations * @returns {Object} Token usage information */ calculateTokenUsage(conversations) { let totalTokens = 0; let inputTokens = 0; let outputTokens = 0; let cacheTokens = 0; conversations.forEach(conv => { if (conv.tokenUsage) { totalTokens += conv.tokenUsage.total || 0; inputTokens += conv.tokenUsage.inputTokens || 0; outputTokens += conv.tokenUsage.outputTokens || 0; cacheTokens += conv.tokenUsage.cacheReadTokens || 0; } else { // Fallback to estimated tokens totalTokens += conv.tokens || 0; } }); // Format in billions const billions = totalTokens / 1000000000; return { total: totalTokens, billions: billions.toFixed(2), formatted: billions >= 1 ? `${billions.toFixed(2)}B` : `${(totalTokens / 1000000).toFixed(0)}M`, breakdown: { input: inputTokens, output: outputTokens, cache: cacheTokens } }; } /** * Calculate longest and current streak * @param {Array} conversations - 2025 conversations * @returns {Object} Streak information */ calculateStreak(conversations) { if (conversations.length === 0) { return { longest: 0, current: 0, formatted: '0d' }; } // Create set of active days const activeDays = new Set(); conversations.forEach(conv => { if (conv.lastModified) { const date = new Date(conv.lastModified); const dayKey = date.toISOString().split('T')[0]; activeDays.add(dayKey); } }); // Convert to sorted array const sortedDays = Array.from(activeDays).sort(); // Calculate longest streak let longestStreak = 1; let currentStreakCount = 1; for (let i = 1; i < sortedDays.length; i++) { const prevDate = new Date(sortedDays[i - 1]); const currDate = new Date(sortedDays[i]); const diffDays = Math.round((currDate - prevDate) / (1000 * 60 * 60 * 24)); if (diffDays === 1) { currentStreakCount++; longestStreak = Math.max(longestStreak, currentStreakCount); } else { currentStreakCount = 1; } } // Calculate current streak (from most recent day) const today = new Date(); today.setHours(0, 0, 0, 0); let currentStreak = 0; const lastDay = new Date(sortedDays[sortedDays.length - 1]); const daysSinceLastActivity = Math.round((today - lastDay) / (1000 * 60 * 60 * 24)); if (daysSinceLastActivity <= 1) { // Activity today or yesterday, calculate backward streak let checkDate = new Date(lastDay); currentStreak = 1; for (let i = sortedDays.length - 2; i >= 0; i--) { const prevDate = new Date(sortedDays[i]); const diff = Math.round((checkDate - prevDate) / (1000 * 60 * 60 * 24)); if (diff === 1) { currentStreak++; checkDate = prevDate; } else { break; } } } return { longest: longestStreak, current: currentStreak, formatted: `${longestStreak}d`, activeDays: activeDays.size }; } /** * Calculate number of active days * @param {Array} conversations - Conversations * @returns {number} Number of active days */ calculateActiveDays(conversations) { const activeDays = new Set(); conversations.forEach(conv => { if (conv.lastModified) { const date = new Date(conv.lastModified); const dayKey = date.toISOString().split('T')[0]; activeDays.add(dayKey); } }); return activeDays.size; } /** * Generate activity heatmap data (GitHub-style) * @param {Array} conversations - 2025 conversations * @returns {Array} Heatmap data by week */ generateActivityHeatmap(conversations) { console.log(`\nšŸ”„šŸ”„šŸ”„ GENERATING HEATMAP for ${conversations.length} conversations\n`); // Create map of day -> activity count, tools, and models const dailyActivity = new Map(); conversations.forEach((conv, idx) => { if (conv.lastModified) { const date = new Date(conv.lastModified); const dayKey = date.toISOString().split('T')[0]; if (idx < 3) { console.log(` šŸ“ Conv ${idx}: id=${conv.id}, tokens=${conv.tokens}, tokenUsage.total=${conv.tokenUsage?.total}`); } const current = dailyActivity.get(dayKey) || { count: 0, tools: [], models: [], modelCounts: {}, toolCounts: {}, tokens: 0 }; current.count += 1; // Add tokens from this conversation if (conv.tokenUsage && conv.tokenUsage.total) { current.tokens += conv.tokenUsage.total; if (idx < 3) console.log(` šŸ’Ž Added ${conv.tokenUsage.total} tokens from conv ${conv.id} to ${dayKey}`); } else if (conv.tokens) { current.tokens += conv.tokens; if (idx < 3) console.log(` šŸ’Ž Added ${conv.tokens} tokens (fallback) from conv ${conv.id} to ${dayKey}`); } else { if (idx < 3) console.log(` āš ļø Conv ${conv.id} has NO token data`); } // Count tool usage with actual numbers from toolStats if (conv.toolUsage && conv.toolUsage.toolStats) { Object.entries(conv.toolUsage.toolStats).forEach(([tool, count]) => { if (!current.tools.includes(tool)) { current.tools.push(tool); } // Add the actual count from this conversation current.toolCounts[tool] = (current.toolCounts[tool] || 0) + count; }); } // Count model usage using totalToolCalls as proxy for activity if (conv.modelInfo && conv.modelInfo.primaryModel) { const model = conv.modelInfo.primaryModel; if (!current.models.includes(model)) { current.models.push(model); } // Use totalToolCalls as proxy for model activity (each tool call ā‰ˆ one model response) const activityCount = (conv.toolUsage && conv.toolUsage.totalToolCalls) || 1; current.modelCounts[model] = (current.modelCounts[model] || 0) + activityCount; } dailyActivity.set(dayKey, current); } }); // Generate full year grid (52-53 weeks) const weeks = []; const startDate = new Date(this.yearStart); // Start from first Sunday of the year or week before const firstDay = startDate.getDay(); if (firstDay !== 0) { startDate.setDate(startDate.getDate() - firstDay); } let currentWeek = []; let currentDate = new Date(startDate); while (currentDate <= this.yearEnd || currentWeek.length > 0) { const dayKey = currentDate.toISOString().split('T')[0]; const dayData = dailyActivity.get(dayKey) || { count: 0, tools: [], models: [], toolCounts: {}, modelCounts: {}, tokens: 0 }; // Determine intensity level (0-4 like GitHub) let level = 0; if (dayData.count > 0) level = 1; if (dayData.count >= 3) level = 2; if (dayData.count >= 6) level = 3; if (dayData.count >= 10) level = 4; currentWeek.push({ date: dayKey, count: dayData.count, tools: dayData.tools, models: dayData.models, toolCounts: dayData.toolCounts, modelCounts: dayData.modelCounts, tokens: dayData.tokens, level, day: currentDate.getDay() }); // If Sunday (end of week) or last day, push week if (currentDate.getDay() === 6 || currentDate > this.yearEnd) { weeks.push([...currentWeek]); currentWeek = []; } currentDate.setDate(currentDate.getDate() + 1); // Safety check if (weeks.length > 60) break; } return weeks; } /** * Analyze top projects worked on * @param {Array} conversations - 2025 conversations * @returns {Array} Top projects */ analyzeTopProjects(conversations) { const projectActivity = new Map(); conversations.forEach(conv => { const project = conv.project || 'Unknown'; const current = projectActivity.get(project) || { count: 0, tokens: 0 }; current.count += 1; current.tokens += conv.tokens || 0; projectActivity.set(project, current); }); return Array.from(projectActivity.entries()) .map(([name, data]) => ({ name, conversations: data.count, tokens: data.tokens })) .sort((a, b) => b.conversations - a.conversations) .slice(0, 5); } /** * Analyze tool usage patterns * @param {Array} conversations - 2025 conversations * @returns {Object} Tool usage statistics */ analyzeToolUsage(conversations) { let totalToolCalls = 0; const toolTypes = new Map(); conversations.forEach(conv => { if (conv.toolUsage) { totalToolCalls += conv.toolUsage.totalToolCalls || 0; if (conv.toolUsage.toolStats) { Object.entries(conv.toolUsage.toolStats).forEach(([tool, count]) => { toolTypes.set(tool, (toolTypes.get(tool) || 0) + count); }); } } }); const topTools = Array.from(toolTypes.entries()) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); return { total: totalToolCalls, topTools }; } /** * Analyze time of day usage patterns * @param {Array} conversations - 2025 conversations * @returns {Object} Time of day statistics */ analyzeTimeOfDay(conversations) { const hourCounts = new Array(24).fill(0); conversations.forEach(conv => { if (conv.lastModified) { const date = new Date(conv.lastModified); const hour = date.getHours(); hourCounts[hour]++; } }); // Find peak hour let peakHour = 0; let peakCount = 0; hourCounts.forEach((count, hour) => { if (count > peakCount) { peakCount = count; peakHour = hour; } }); return { peakHour, peakHourFormatted: `${peakHour}:00 - ${peakHour + 1}:00`, hourlyDistribution: hourCounts }; } /** * Detect installed components from .claude directory * @returns {Promise<Array>} List of component installations */ async detectInstalledComponents() { const components = []; const os = require('os'); const fs = require('fs-extra'); const path = require('path'); try { const claudeDir = path.join(os.homedir(), '.claude'); const localDir = path.join(claudeDir, 'local'); // Check if local directory exists if (await fs.pathExists(localDir)) { const settings = await fs.readJSON(path.join(claudeDir, 'settings.json')).catch(() => ({})); // Extract MCPs from settings if (settings.mcpServers) { Object.keys(settings.mcpServers).forEach(mcpName => { components.push({ type: 'mcp', name: mcpName, date: new Date('2025-01-15') // Default date - could be improved }); }); } // Extract enabled plugins if (settings.enabledPlugins) { Object.entries(settings.enabledPlugins).forEach(([pluginName, enabled]) => { if (enabled) { components.push({ type: 'skill', name: pluginName.split('@')[0], date: new Date('2025-02-01') }); } }); } } } catch (error) { console.warn('Could not detect installed components:', error.message); } return components; } /** * Generate insights and fun facts * @param {Array} conversations - 2025 conversations * @returns {Array} Insights */ generateInsights(conversations) { const insights = []; // Total messages const totalMessages = conversations.reduce((sum, conv) => sum + (conv.messageCount || 0), 0); insights.push(`Sent ${totalMessages.toLocaleString()} messages`); // Average session length const avgMessages = Math.round(totalMessages / conversations.length); insights.push(`Average ${avgMessages} messages per conversation`); // Most productive day const dailyActivity = new Map(); conversations.forEach(conv => { if (conv.lastModified) { const dayKey = new Date(conv.lastModified).toISOString().split('T')[0]; dailyActivity.set(dayKey, (dailyActivity.get(dayKey) || 0) + 1); } }); const mostProductiveDay = Array.from(dailyActivity.entries()) .sort((a, b) => b[1] - a[1])[0]; if (mostProductiveDay) { const date = new Date(mostProductiveDay[0]); insights.push(`Most productive: ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`); } return insights; } /** * Analyze command usage from history * @returns {Promise<Object>} Command usage data */ async analyzeCommands() { const fs = require('fs-extra'); const os = require('os'); const path = require('path'); try { const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl'); if (!await fs.pathExists(historyPath)) { return { commands: [], total: 0, events: [] }; } const content = await fs.readFile(historyPath, 'utf8'); const lines = content.trim().split('\n').filter(l => l.trim()); const commandCounts = new Map(); const commandEvents = []; // Track each command execution with timestamp lines.forEach(line => { try { const entry = JSON.parse(line); if (entry.display && entry.display.startsWith('/')) { const cmd = entry.display.trim(); // Extract base command (before space or arguments) const baseCmd = cmd.split(' ')[0]; commandCounts.set(baseCmd, (commandCounts.get(baseCmd) || 0) + 1); // Extract timestamp and add to events if (entry.timestamp) { const timestamp = new Date(entry.timestamp); // Only include 2025 events if (timestamp.getFullYear() === 2025) { commandEvents.push({ name: baseCmd, timestamp: timestamp, display: entry.display }); } } } } catch (e) { // Skip invalid lines } }); const commands = Array.from(commandCounts.entries()) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count); return { commands: commands.slice(0, 20), // Top 20 commands total: commands.reduce((sum, cmd) => sum + cmd.count, 0), events: commandEvents.sort((a, b) => a.timestamp - b.timestamp) // Sort by date }; } catch (error) { console.warn('Could not analyze commands:', error.message); return { commands: [], total: 0, events: [] }; } } /** * Analyze installed skills * @returns {Promise<Object>} Skills data */ async analyzeSkills() { const fs = require('fs-extra'); const os = require('os'); const path = require('path'); try { const skillsPath = path.join(os.homedir(), '.claude', 'skills'); if (!await fs.pathExists(skillsPath)) { return { skills: [], total: 0, events: [] }; } const items = await fs.readdir(skillsPath); const skills = []; const skillEvents = []; for (const item of items) { const itemPath = path.join(skillsPath, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { const installedAt = stats.birthtime; skills.push({ name: item, installedAt: installedAt }); // Only include 2025 skills if (installedAt.getFullYear() === 2025) { skillEvents.push({ name: item, timestamp: installedAt }); } } } return { skills: skills.sort((a, b) => b.installedAt - a.installedAt), total: skills.length, events: skillEvents.sort((a, b) => a.timestamp - b.timestamp) }; } catch (error) { console.warn('Could not analyze skills:', error.message); return { skills: [], total: 0, events: [] }; } } /** * Analyze installed MCPs * @returns {Promise<Object>} MCPs data */ async analyzeMCPs() { const fs = require('fs-extra'); const os = require('os'); const path = require('path'); try { const settingsPath = path.join(os.homedir(), '.claude', 'settings.json'); if (!await fs.pathExists(settingsPath)) { return { mcps: [], total: 0, events: [] }; } const content = await fs.readFile(settingsPath, 'utf8'); const settings = JSON.parse(content); const stats = await fs.stat(settingsPath); const modifiedAt = stats.mtime; // Use file modification time as fallback const mcps = []; const mcpEvents = []; if (settings.enabledPlugins) { Object.entries(settings.enabledPlugins).forEach(([name, enabled], index) => { if (enabled) { mcps.push({ name, enabled }); // Only include 2025 MCPs // Use modification time with slight offset for each MCP if (modifiedAt.getFullYear() === 2025) { const timestamp = new Date(modifiedAt.getTime() + (index * 1000)); // 1 second apart mcpEvents.push({ name: name, timestamp: timestamp }); } } }); } return { mcps: mcps, total: mcps.length, events: mcpEvents.sort((a, b) => a.timestamp - b.timestamp) }; } catch (error) { console.warn('Could not analyze MCPs:', error.message); return { mcps: [], total: 0, events: [] }; } } /** * Analyze subagent usage * @param {string} claudeDir - Claude directory path * @returns {Promise<Object>} Subagents data */ async analyzeSubagents(claudeDir) { const fs = require('fs-extra'); const path = require('path'); try { const projectsDir = path.join(claudeDir, 'projects'); if (!await fs.pathExists(projectsDir)) { return { subagents: [], total: 0, events: [] }; } const agentData = new Map(); // Map of agent ID -> { id, timestamp } const projects = await fs.readdir(projectsDir); // Search for agent files in all project directories for (const project of projects) { const projectPath = path.join(projectsDir, project); const stats = await fs.stat(projectPath); if (stats.isDirectory()) { const files = await fs.readdir(projectPath); const agentFiles = files.filter(f => f.startsWith('agent-') && f.endsWith('.jsonl')); for (const file of agentFiles) { const match = file.match(/agent-(.+)\.jsonl$/); if (match) { const agentId = match[1]; // Read only first 2 lines for performance (don't read entire file) try { const filePath = path.join(projectPath, file); const readline = require('readline'); const fileStream = require('fs').createReadStream(filePath); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); let timestamp = null; let agentType = 'Agent'; let lineCount = 0; const lines = []; // Read only first 2 lines for await (const line of rl) { if (line.trim()) { lines.push(line.trim()); lineCount++; if (lineCount >= 2) break; // Stop after 2 lines } } rl.close(); fileStream.close(); // Get timestamp from first line if (lines[0]) { const firstEntry = JSON.parse(lines[0]); if (firstEntry.timestamp) { timestamp = new Date(firstEntry.timestamp); } } // Try to extract agent type from second line (assistant response) if (lines[1]) { const secondEntry = JSON.parse(lines[1]); if (secondEntry.message && secondEntry.message.content) { const content = Array.isArray(secondEntry.message.content) ? secondEntry.message.content[0]?.text : secondEntry.message.content; // Look for agent type indicators in the response (check first 500 chars only) if (content) { const lowerContent = content.substring(0, 500).toLowerCase(); // Plan agents if (lowerContent.includes('planning mode') || lowerContent.includes('read-only')) { agentType = 'Plan'; } // Explore agents else if (lowerContent.includes('explore') || lowerContent.includes('search')) { agentType = 'Explore'; } // Default to Plan for most agents else { agentType = 'Plan'; } } } } // Only store the earliest timestamp for each agent if (!agentData.has(agentId) || (timestamp && timestamp < agentData.get(agentId).timestamp)) { agentData.set(agentId, { id: agentId, timestamp, type: agentType }); } } catch (e) { // If we can't read timestamp, just add the agent without timestamp if (!agentData.has(agentId)) { agentData.set(agentId, { id: agentId, timestamp: null, type: 'Agent' }); } } } } } } const subagents = Array.from(agentData.values()); // Group subagents by type for cleaner visualization const groupedByType = {}; subagents.forEach(agent => { const type = agent.type || 'Unknown'; if (!groupedByType[type]) { groupedByType[type] = { count: 0, timestamps: [] }; } groupedByType[type].count++; if (agent.timestamp && agent.timestamp.getFullYear() === 2025) { groupedByType[type].timestamps.push(agent.timestamp); } }); // Create events grouped by type (one event per type per day) const subagentEvents = []; Object.entries(groupedByType).forEach(([type, data]) => { // Group timestamps by day to avoid too many events const dayMap = new Map(); data.timestamps.forEach(ts => { const dayKey = ts.toISOString().split('T')[0]; if (!dayMap.has(dayKey)) { dayMap.set(dayKey, { count: 0, timestamp: ts }); } dayMap.get(dayKey).count++; }); // Create one event per day per type dayMap.forEach((dayData, dayKey) => { subagentEvents.push({ name: type, // Just "Plan" or "Explore", not "Plan-a68a" timestamp: dayData.timestamp, type: type, count: dayData.count // How many times used that day }); }); }); subagentEvents.sort((a, b) => a.timestamp - b.timestamp); return { subagents: Object.entries(groupedByType).map(([type, data]) => ({ type, count: data.count })), total: subagents.length, events: subagentEvents, grouped: groupedByType // Include grouped data for display }; } catch (error) { console.warn('Could not analyze subagents:', error.message); return { subagents: [], total: 0, events: [] }; } } } module.exports = YearInReview2025;