UNPKG

claude-code-templates

Version:

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

355 lines (305 loc) 12.3 kB
/** * AgentAnalyzer - Analyzes Claude Code specialized agent usage patterns * Extracts agent invocation data, usage frequency, and workflow patterns */ const chalk = require('chalk'); const fs = require('fs-extra'); const path = require('path'); class AgentAnalyzer { constructor() { // Known Claude Code specialized agents this.AGENT_TYPES = { 'general-purpose': { name: 'General Purpose', description: 'Multi-step tasks and research', color: '#3fb950', icon: '🔧' }, 'claude-code-best-practices': { name: 'Claude Code Best Practices', description: 'Workflow optimization and setup guidance', color: '#f97316', icon: '⚡' }, 'docusaurus-expert': { name: 'Docusaurus Expert', description: 'Documentation site management', color: '#0969da', icon: '📚' } }; } /** * Analyze agent usage across all conversations * @param {Array} conversations - Array of conversation objects with parsed messages * @param {Object} dateRange - Optional date range filter * @returns {Object} Agent usage analysis */ async analyzeAgentUsage(conversations, dateRange = null) { const agentStats = {}; const agentTimeline = []; const agentWorkflows = {}; let totalAgentInvocations = 0; for (const conversation of conversations) { // Parse messages from JSONL file if not already parsed let messages = conversation.parsedMessages; if (!messages && conversation.filePath) { messages = await this.parseJsonlFile(conversation.filePath); } if (!messages) continue; messages.forEach(message => { // Skip if outside date range if (dateRange && !this.isWithinDateRange(message.timestamp, dateRange)) { return; } // Look for Task tool usage with subagent_type // Handle both direct message structure and nested message structure const messageContent = message.message ? message.message.content : message.content; const messageRole = message.message ? message.message.role : message.role; if (messageRole === 'assistant' && messageContent && Array.isArray(messageContent)) { messageContent.forEach(content => { if (content.type === 'tool_use' && content.name === 'Task' && content.input && content.input.subagent_type) { const agentType = content.input.subagent_type; const timestamp = new Date(message.timestamp); const prompt = content.input.prompt || content.input.description || 'No description'; // Initialize agent stats if (!agentStats[agentType]) { agentStats[agentType] = { type: agentType, name: this.AGENT_TYPES[agentType]?.name || agentType, description: this.AGENT_TYPES[agentType]?.description || 'Custom agent', color: this.AGENT_TYPES[agentType]?.color || '#8b5cf6', icon: this.AGENT_TYPES[agentType]?.icon || '🤖', totalInvocations: 0, uniqueConversations: new Set(), firstUsed: timestamp, lastUsed: timestamp, prompts: [], hourlyDistribution: new Array(24).fill(0), dailyUsage: {} }; } const stats = agentStats[agentType]; // Update stats stats.totalInvocations++; stats.uniqueConversations.add(conversation.id); stats.lastUsed = new Date(Math.max(stats.lastUsed, timestamp)); stats.firstUsed = new Date(Math.min(stats.firstUsed, timestamp)); // Store prompt for analysis stats.prompts.push({ text: prompt, timestamp: timestamp, conversationId: conversation.id }); // Track hourly distribution const hour = timestamp.getHours(); stats.hourlyDistribution[hour]++; // Track daily usage const dateKey = timestamp.toISOString().split('T')[0]; stats.dailyUsage[dateKey] = (stats.dailyUsage[dateKey] || 0) + 1; // Add to timeline agentTimeline.push({ timestamp: timestamp, agentType: agentType, agentName: stats.name, prompt: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : ''), conversationId: conversation.id, color: stats.color, icon: stats.icon }); totalAgentInvocations++; } }); } }); } // Convert Sets to counts and finalize stats Object.keys(agentStats).forEach(agentType => { const stats = agentStats[agentType]; stats.uniqueConversations = stats.uniqueConversations.size; stats.averageUsagePerConversation = stats.uniqueConversations > 0 ? (stats.totalInvocations / stats.uniqueConversations).toFixed(1) : 0; }); // Sort timeline by timestamp agentTimeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); // Calculate agent workflows (sequences of agent usage) const workflowPatterns = this.analyzeAgentWorkflows(agentTimeline); return { totalAgentInvocations, totalAgentTypes: Object.keys(agentStats).length, agentStats: Object.values(agentStats).sort((a, b) => b.totalInvocations - a.totalInvocations), agentTimeline, workflowPatterns, popularHours: this.calculatePopularHours(agentStats), usageByDay: this.calculateDailyUsage(agentStats), efficiency: this.calculateAgentEfficiency(agentStats) }; } /** * Analyze agent workflow patterns * @param {Array} timeline - Chronological agent invocations * @returns {Object} Workflow patterns */ analyzeAgentWorkflows(timeline) { const workflows = {}; const SESSION_GAP_MINUTES = 30; // Minutes between workflow sessions let currentWorkflow = []; let lastTimestamp = null; timeline.forEach(event => { const currentTime = new Date(event.timestamp); // Start new workflow if gap is too large or first event if (!lastTimestamp || (currentTime - lastTimestamp) > (SESSION_GAP_MINUTES * 60 * 1000)) { // Save previous workflow if it had multiple agents if (currentWorkflow.length > 1) { const workflowKey = currentWorkflow.map(e => e.agentType).join(' → '); workflows[workflowKey] = (workflows[workflowKey] || 0) + 1; } currentWorkflow = [event]; } else { currentWorkflow.push(event); } lastTimestamp = currentTime; }); // Don't forget the last workflow if (currentWorkflow.length > 1) { const workflowKey = currentWorkflow.map(e => e.agentType).join(' → '); workflows[workflowKey] = (workflows[workflowKey] || 0) + 1; } // Convert to sorted array return Object.entries(workflows) .map(([pattern, count]) => ({ pattern, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); // Top 10 workflows } /** * Calculate popular usage hours across all agents * @param {Object} agentStats - Agent statistics * @returns {Array} Hour usage data */ calculatePopularHours(agentStats) { const hourlyTotals = new Array(24).fill(0); Object.values(agentStats).forEach(stats => { stats.hourlyDistribution.forEach((count, hour) => { hourlyTotals[hour] += count; }); }); return hourlyTotals.map((count, hour) => ({ hour, count, label: `${hour.toString().padStart(2, '0')}:00` })); } /** * Calculate daily usage across all agents * @param {Object} agentStats - Agent statistics * @returns {Array} Daily usage data */ calculateDailyUsage(agentStats) { const dailyTotals = {}; Object.values(agentStats).forEach(stats => { Object.entries(stats.dailyUsage).forEach(([date, count]) => { dailyTotals[date] = (dailyTotals[date] || 0) + count; }); }); return Object.entries(dailyTotals) .map(([date, count]) => ({ date, count, timestamp: new Date(date) })) .sort((a, b) => a.timestamp - b.timestamp); } /** * Calculate agent efficiency metrics * @param {Object} agentStats - Agent statistics * @returns {Object} Efficiency metrics */ calculateAgentEfficiency(agentStats) { const agents = Object.values(agentStats); if (agents.length === 0) return {}; const totalInvocations = agents.reduce((sum, agent) => sum + agent.totalInvocations, 0); const totalConversations = agents.reduce((sum, agent) => sum + agent.uniqueConversations, 0); return { averageInvocationsPerAgent: (totalInvocations / agents.length).toFixed(1), averageConversationsPerAgent: (totalConversations / agents.length).toFixed(1), mostUsedAgent: agents[0], agentDiversity: agents.length, adoptionRate: (agents.filter(a => a.totalInvocations > 1).length / agents.length * 100).toFixed(1) }; } /** * Parse JSONL file to extract messages * @param {string} filePath - Path to the JSONL file * @returns {Array} Array of parsed messages */ async parseJsonlFile(filePath) { try { if (!await fs.pathExists(filePath)) { return null; } const content = await fs.readFile(filePath, 'utf8'); const lines = content.trim().split('\n').filter(line => line.trim()); return lines.map((line, index) => { try { // Skip empty or whitespace-only lines if (!line.trim()) { return null; } // Basic validation - must start with { and end with } const trimmed = line.trim(); if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) { return null; } return JSON.parse(trimmed); } catch (error) { // Only log the error occasionally to avoid spam if (index % 10 === 0) { console.warn(`Error parsing JSONL line ${index + 1} in ${filePath}:`, error.message.substring(0, 100)); } return null; } }).filter(Boolean); } catch (error) { console.error(`Error reading JSONL file ${filePath}:`, error); return null; } } /** * Check if timestamp is within date range * @param {string|Date} timestamp - Message timestamp * @param {Object} dateRange - Date range with startDate and endDate * @returns {boolean} Whether timestamp is in range */ isWithinDateRange(timestamp, dateRange) { if (!dateRange || (!dateRange.startDate && !dateRange.endDate)) return true; const messageDate = new Date(timestamp); const startDate = dateRange.startDate ? new Date(dateRange.startDate) : new Date(0); const endDate = dateRange.endDate ? new Date(dateRange.endDate) : new Date(); return messageDate >= startDate && messageDate <= endDate; } /** * Generate agent usage summary for display * @param {Object} analysisResult - Result from analyzeAgentUsage * @returns {Object} Summary data */ generateSummary(analysisResult) { const { totalAgentInvocations, totalAgentTypes, agentStats, efficiency } = analysisResult; return { totalInvocations: totalAgentInvocations, totalAgentTypes, topAgent: agentStats[0] || null, averageUsage: efficiency.averageInvocationsPerAgent, adoptionRate: efficiency.adoptionRate, summary: totalAgentInvocations > 0 ? `${totalAgentInvocations} agent invocations across ${totalAgentTypes} different agents` : 'No agent usage detected' }; } } module.exports = AgentAnalyzer;