claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
948 lines (835 loc) • 32.6 kB
JavaScript
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
/**
* ConversationAnalyzer - Handles conversation data loading, parsing, and analysis
* Extracted from monolithic analytics.js for better maintainability
*/
class ConversationAnalyzer {
constructor(claudeDir, dataCache = null) {
this.claudeDir = claudeDir;
this.dataCache = dataCache;
this.data = {
conversations: [],
activeProjects: [],
summary: {},
orphanProcesses: [],
realtimeStats: {}
};
}
/**
* Main data loading orchestrator method
* @param {Object} stateCalculator - StateCalculator instance
* @param {Object} processDetector - ProcessDetector instance
* @returns {Promise<Object>} Complete analyzed data
*/
async loadInitialData(stateCalculator, processDetector) {
console.log(chalk.yellow('📊 Analyzing Claude Code data...'));
try {
// Load conversation files
const conversations = await this.loadConversations(stateCalculator);
this.data.conversations = conversations;
// Load active projects
const projects = await this.loadActiveProjects();
this.data.activeProjects = projects;
// Detect active Claude processes and enrich data
const enrichmentResult = await processDetector.enrichWithRunningProcesses(
this.data.conversations,
this.claudeDir,
stateCalculator
);
this.data.conversations = enrichmentResult.conversations;
this.data.orphanProcesses = enrichmentResult.orphanProcesses;
// Calculate summary statistics with caching
this.data.summary = await this.calculateSummary(conversations, projects);
// Update realtime stats
this.updateRealtimeStats();
console.log(chalk.green('✅ Data analysis complete'));
console.log(chalk.gray(`Found ${conversations.length} conversations across ${projects.length} projects`));
return this.data;
} catch (error) {
console.error(chalk.red('Error loading Claude data:'), error.message);
throw error;
}
}
/**
* Load and parse all conversation files recursively
* @param {Object} stateCalculator - StateCalculator instance for status determination
* @returns {Promise<Array>} Array of conversation objects
*/
async loadConversations(stateCalculator) {
const conversations = [];
try {
// Search for .jsonl files recursively in all subdirectories
const findJsonlFiles = async (dir) => {
const files = [];
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
// Recursively search subdirectories
const subFiles = await findJsonlFiles(itemPath);
files.push(...subFiles);
} else if (item.endsWith('.jsonl')) {
files.push(itemPath);
}
}
return files;
};
const jsonlFiles = await findJsonlFiles(this.claudeDir);
// Loading conversation files quietly for better UX
for (const filePath of jsonlFiles) {
const stats = await this.getFileStats(filePath);
const filename = path.basename(filePath);
try {
// Extract project name from path
const projectFromPath = await this.extractProjectFromPath(filePath);
// Use cached parsed conversation if available
const parsedMessages = await this.getParsedConversation(filePath);
// Calculate real token usage and extract model info with caching
const tokenUsage = await this.getCachedTokenUsage(filePath, parsedMessages);
const modelInfo = await this.getCachedModelInfo(filePath, parsedMessages);
// Calculate tool usage data with caching
const toolUsage = await this.getCachedToolUsage(filePath, parsedMessages);
const projectFromConversation = await this.extractProjectFromConversation(filePath);
const finalProject = projectFromConversation || projectFromPath;
const conversation = {
id: filename.replace('.jsonl', ''),
filename: filename,
filePath: filePath,
messageCount: parsedMessages.length,
fileSize: stats.size,
lastModified: stats.mtime,
created: stats.birthtime,
tokens: tokenUsage.total > 0 ? tokenUsage.total : this.estimateTokens(await this.getFileContent(filePath)),
tokenUsage: tokenUsage,
modelInfo: modelInfo,
toolUsage: toolUsage,
project: finalProject,
status: stateCalculator.determineConversationStatus(parsedMessages, stats.mtime),
conversationState: stateCalculator.determineConversationState(parsedMessages, stats.mtime),
statusSquares: await this.getCachedStatusSquares(filePath, parsedMessages),
// parsedMessages removed to prevent memory leak - available via cache when needed
};
conversations.push(conversation);
} catch (error) {
console.warn(chalk.yellow(`Warning: Could not parse ${filename}:`, error.message));
}
}
return conversations.sort((a, b) => b.lastModified - a.lastModified);
} catch (error) {
console.error(chalk.red('Error loading conversations:'), error.message);
return [];
}
}
/**
* Load active Claude projects from directory structure
* @returns {Promise<Array>} Array of project objects
*/
async loadActiveProjects() {
const projects = [];
try {
const files = await fs.readdir(this.claudeDir);
for (const file of files) {
const filePath = path.join(this.claudeDir, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory() && !file.startsWith('.')) {
const projectPath = filePath;
const todoFiles = await this.findTodoFiles(projectPath);
const project = {
name: file,
path: projectPath,
lastActivity: stats.mtime,
todoFiles: todoFiles.length,
status: this.determineProjectStatus(stats.mtime),
};
projects.push(project);
}
}
return projects.sort((a, b) => b.lastActivity - a.lastActivity);
} catch (error) {
console.error(chalk.red('Error loading projects:'), error.message);
return [];
}
}
/**
* Get file content with caching support
* @param {string} filepath - Path to file
* @returns {Promise<string>} File content
*/
async getFileContent(filepath) {
if (this.dataCache) {
return await this.dataCache.getFileContent(filepath);
}
return await fs.readFile(filepath, 'utf8');
}
/**
* Get file stats with caching support
* @param {string} filepath - Path to file
* @returns {Promise<Object>} File stats
*/
async getFileStats(filepath) {
if (this.dataCache) {
return await this.dataCache.getFileStats(filepath);
}
return await fs.stat(filepath);
}
/**
* Get parsed conversation with caching support
* @param {string} filepath - Path to conversation file
* @returns {Promise<Array>} Parsed conversation messages
*/
async getParsedConversation(filepath) {
if (this.dataCache) {
return await this.dataCache.getParsedConversation(filepath);
}
// Fallback to direct parsing with tool correlation
const content = await fs.readFile(filepath, 'utf8');
const lines = content.trim().split('\n').filter(line => line.trim());
return this.parseAndCorrelateToolMessages(lines);
}
/**
* Parse JSONL lines and correlate tool_use with tool_result
* @param {Array} lines - JSONL lines
* @returns {Array} Parsed and correlated messages
*/
parseAndCorrelateToolMessages(lines) {
const entries = [];
const toolUseMap = new Map();
// First pass: parse all entries and map tool_use entries
for (const line of lines) {
try {
const item = JSON.parse(line);
if (item.message && (item.type === 'assistant' || item.type === 'user')) {
entries.push(item);
// Track tool_use entries by their ID
if (item.type === 'assistant' && item.message.content) {
const toolUseBlock = Array.isArray(item.message.content)
? item.message.content.find(c => c.type === 'tool_use')
: (item.message.content.type === 'tool_use' ? item.message.content : null);
if (toolUseBlock && toolUseBlock.id) {
toolUseMap.set(toolUseBlock.id, item);
}
}
}
} catch (error) {
// Skip invalid JSONL lines
}
}
// Second pass: correlate tool_result with tool_use and filter out standalone tool_result entries
const processedMessages = [];
for (const item of entries) {
if (item.type === 'user' && item.message.content) {
// Check if this is a tool_result entry
const toolResultBlock = Array.isArray(item.message.content)
? item.message.content.find(c => c.type === 'tool_result')
: (item.message.content.type === 'tool_result' ? item.message.content : null);
if (toolResultBlock && toolResultBlock.tool_use_id) {
// This is a tool_result - attach it to the corresponding tool_use
// console.log(`🔍 ConversationAnalyzer: Found tool_result for ${toolResultBlock.tool_use_id}, content: "${toolResultBlock.content}"`);
const toolUseEntry = toolUseMap.get(toolResultBlock.tool_use_id);
// console.log(`🔍 ConversationAnalyzer: toolUseEntry found: ${!!toolUseEntry}`);
if (toolUseEntry) {
// Attach tool result to the tool use entry
if (!toolUseEntry.toolResults) {
toolUseEntry.toolResults = [];
}
toolUseEntry.toolResults.push(toolResultBlock);
// console.log(`✅ ConversationAnalyzer: Attached tool result to ${toolResultBlock.tool_use_id}, content length: ${toolResultBlock.content?.length || 0}`);
// Don't add this tool_result as a separate message
continue;
} else {
// console.log(`❌ ConversationAnalyzer: No tool_use found for ${toolResultBlock.tool_use_id}`);
}
}
}
// Convert to our standard format
if (item.toolResults) {
// console.log(`ConversationAnalyzer: Processing item with ${item.toolResults.length} tool results`);
}
const parsed = {
id: item.message.id || item.uuid || null,
role: item.message.role || (item.type === 'assistant' ? 'assistant' : 'user'),
timestamp: new Date(item.timestamp),
content: item.message.content,
model: item.message.model || null,
usage: item.message.usage || null,
toolResults: item.toolResults || null, // Include attached tool results
isCompactSummary: item.isCompactSummary || false, // Preserve compact summary flag
uuid: item.uuid || null, // Include UUID for message identification
type: item.type || null // Include type field
};
processedMessages.push(parsed);
}
return processedMessages;
}
/**
* Get cached token usage calculation
* @param {string} filepath - File path
* @param {Array} parsedMessages - Parsed messages array
* @returns {Promise<Object>} Token usage statistics
*/
async getCachedTokenUsage(filepath, parsedMessages) {
if (this.dataCache) {
return await this.dataCache.getCachedTokenUsage(filepath, () => {
return this.calculateRealTokenUsage(parsedMessages);
});
}
return this.calculateRealTokenUsage(parsedMessages);
}
/**
* Get cached model info extraction
* @param {string} filepath - File path
* @param {Array} parsedMessages - Parsed messages array
* @returns {Promise<Object>} Model info data
*/
async getCachedModelInfo(filepath, parsedMessages) {
if (this.dataCache) {
return await this.dataCache.getCachedModelInfo(filepath, () => {
return this.extractModelInfo(parsedMessages);
});
}
return this.extractModelInfo(parsedMessages);
}
/**
* Get cached status squares generation
* @param {string} filepath - File path
* @param {Array} parsedMessages - Parsed messages array
* @returns {Promise<Array>} Status squares data
*/
async getCachedStatusSquares(filepath, parsedMessages) {
if (this.dataCache) {
return await this.dataCache.getCachedStatusSquares(filepath, () => {
return this.generateStatusSquares(parsedMessages);
});
}
return this.generateStatusSquares(parsedMessages);
}
/**
* Get cached tool usage analysis
* @param {string} filepath - File path
* @param {Array} parsedMessages - Parsed messages array
* @returns {Promise<Object>} Tool usage data
*/
async getCachedToolUsage(filepath, parsedMessages) {
if (this.dataCache) {
return await this.dataCache.getCachedToolUsage(filepath, () => {
return this.extractToolUsage(parsedMessages);
});
}
return this.extractToolUsage(parsedMessages);
}
/**
* Calculate real token usage from message usage data
* @param {Array} parsedMessages - Array of parsed message objects
* @returns {Object} Token usage statistics
*/
calculateRealTokenUsage(parsedMessages) {
let totalInputTokens = 0;
let totalOutputTokens = 0;
let totalCacheCreationTokens = 0;
let totalCacheReadTokens = 0;
let messagesWithUsage = 0;
parsedMessages.forEach(message => {
if (message.usage) {
totalInputTokens += message.usage.input_tokens || 0;
totalOutputTokens += message.usage.output_tokens || 0;
totalCacheCreationTokens += message.usage.cache_creation_input_tokens || 0;
totalCacheReadTokens += message.usage.cache_read_input_tokens || 0;
messagesWithUsage++;
}
});
return {
total: totalInputTokens + totalOutputTokens,
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
cacheCreationTokens: totalCacheCreationTokens,
cacheReadTokens: totalCacheReadTokens,
messagesWithUsage: messagesWithUsage,
totalMessages: parsedMessages.length,
};
}
/**
* Extract model and service tier information from messages
* @param {Array} parsedMessages - Array of parsed message objects
* @returns {Object} Model information
*/
extractModelInfo(parsedMessages) {
const models = new Set();
const serviceTiers = new Set();
let lastModel = null;
let lastServiceTier = null;
parsedMessages.forEach(message => {
if (message.model) {
models.add(message.model);
lastModel = message.model;
}
if (message.usage && message.usage.service_tier) {
serviceTiers.add(message.usage.service_tier);
lastServiceTier = message.usage.service_tier;
}
});
return {
models: Array.from(models),
primaryModel: lastModel || models.values().next().value || 'Unknown',
serviceTiers: Array.from(serviceTiers),
currentServiceTier: lastServiceTier || serviceTiers.values().next().value || 'Unknown',
hasMultipleModels: models.size > 1,
};
}
/**
* Extract project name from Claude directory file path using settings.json
* @param {string} filePath - Full path to conversation file
* @returns {Promise<string|null>} Project name or null
*/
async extractProjectFromPath(filePath) {
// Extract project name from file path like:
// /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
const pathParts = filePath.split('/');
const projectIndex = pathParts.findIndex(part => part === 'projects');
if (projectIndex !== -1 && projectIndex + 1 < pathParts.length) {
const projectDir = pathParts[projectIndex + 1];
// Try to read the settings.json file for this project
try {
const projectPath = path.join(path.dirname(filePath)); // Directory containing the conversation file
const settingsPath = path.join(projectPath, 'settings.json');
if (await fs.pathExists(settingsPath)) {
const settingsContent = await fs.readFile(settingsPath, 'utf8');
const settings = JSON.parse(settingsContent);
if (settings.projectName) {
return settings.projectName;
}
// If no projectName in settings, try to extract from projectPath
if (settings.projectPath) {
return path.basename(settings.projectPath);
}
}
} catch (error) {
// If we can't read settings.json, fall back to parsing the directory name
console.warn(chalk.yellow(`Warning: Could not read settings.json for project ${projectDir}:`, error.message));
}
// Fallback: we'll extract project name from conversation content instead
// For now, return null to trigger reading from conversation file
return null;
}
return null;
}
/**
* Attempt to extract project information from conversation content
* @param {string} filePath - Path to the conversation file
* @returns {Promise<string>} Project name or 'Unknown'
*/
async extractProjectFromConversation(filePath) {
try {
// Read the conversation file and look for cwd field
const content = await this.getFileContent(filePath);
const lines = content.trim().split('\n').filter(line => line.trim());
for (const line of lines.slice(0, 10)) { // Check first 10 lines
try {
const item = JSON.parse(line);
// Look for cwd field in the message
if (item.cwd) {
const projectName = path.basename(item.cwd);
return projectName;
}
// Also check if it's in nested objects
if (item.message && item.message.cwd) {
return path.basename(item.message.cwd);
}
} catch (parseError) {
// Skip invalid JSON lines
continue;
}
}
} catch (error) {
console.warn(chalk.yellow(`Warning: Could not extract project from conversation ${filePath}:`, error.message));
}
return 'Unknown';
}
/**
* Extract tool usage statistics from parsed messages
* @param {Array} parsedMessages - Array of parsed message objects
* @returns {Object} Tool usage statistics
*/
extractToolUsage(parsedMessages) {
const toolStats = {};
const toolTimeline = [];
let totalToolCalls = 0;
parsedMessages.forEach(message => {
if (message.role === 'assistant' && message.content) {
const content = message.content;
const timestamp = message.timestamp;
// Handle string content with tool indicators
if (typeof content === 'string') {
const toolMatches = content.match(/\[Tool:\s*([^\]]+)\]/g);
if (toolMatches) {
toolMatches.forEach(match => {
const toolName = match.replace(/\[Tool:\s*([^\]]+)\]/, '$1').trim();
toolStats[toolName] = (toolStats[toolName] || 0) + 1;
totalToolCalls++;
toolTimeline.push({
tool: toolName,
timestamp: timestamp,
type: 'usage'
});
});
}
}
// Handle array content with tool_use blocks
if (Array.isArray(content)) {
content.forEach(block => {
if (block.type === 'tool_use') {
const toolName = block.name || 'Unknown Tool';
toolStats[toolName] = (toolStats[toolName] || 0) + 1;
totalToolCalls++;
toolTimeline.push({
tool: toolName,
timestamp: timestamp,
type: 'usage',
parameters: block.input || {}
});
}
});
}
}
});
return {
toolStats,
toolTimeline: toolTimeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)),
totalToolCalls,
uniqueTools: Object.keys(toolStats).length
};
}
/**
* Generate status indicators for conversation messages
* @param {Array} messages - Array of message objects
* @returns {Array} Array of status square objects
*/
generateStatusSquares(messages) {
if (!messages || messages.length === 0) {
return [];
}
// Sort messages by timestamp and take last 10 for status squares
const sortedMessages = messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
const recentMessages = sortedMessages.slice(-10);
return recentMessages.map((message, index) => {
const messageNum = sortedMessages.length - recentMessages.length + index + 1;
// Determine status based on message content and role
if (message.role === 'user') {
return {
type: 'pending',
tooltip: `Message #${messageNum}: User input`,
};
} else if (message.role === 'assistant') {
// Check if the message contains tool usage or errors
const content = message.content || '';
if (typeof content === 'string') {
if (content.includes('[Tool:') || content.includes('tool_use')) {
return {
type: 'tool',
tooltip: `Message #${messageNum}: Tool execution`,
};
} else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
return {
type: 'error',
tooltip: `Message #${messageNum}: Error in response`,
};
} else {
return {
type: 'success',
tooltip: `Message #${messageNum}: Successful response`,
};
}
} else if (Array.isArray(content)) {
// Check for tool_use blocks in array content
const hasToolUse = content.some(block => block.type === 'tool_use');
const hasError = content.some(block =>
block.type === 'text' && (block.text?.includes('error') || block.text?.includes('Error'))
);
if (hasError) {
return {
type: 'error',
tooltip: `Message #${messageNum}: Error in response`,
};
} else if (hasToolUse) {
return {
type: 'tool',
tooltip: `Message #${messageNum}: Tool execution`,
};
} else {
return {
type: 'success',
tooltip: `Message #${messageNum}: Successful response`,
};
}
}
}
return {
type: 'pending',
tooltip: `Message #${messageNum}: Unknown status`,
};
});
}
/**
* Calculate summary statistics from conversations and projects data with caching
* @param {Array} conversations - Array of conversation objects
* @param {Array} projects - Array of project objects
* @returns {Promise<Object>} Summary statistics
*/
async calculateSummary(conversations, projects) {
if (this.dataCache) {
const dependencies = conversations.map(conv => conv.filePath);
return await this.dataCache.getCachedComputation(
'summary',
() => this.computeSummary(conversations, projects),
dependencies
);
}
return this.computeSummary(conversations, projects);
}
/**
* Compute summary statistics (internal method)
* @param {Array} conversations - Array of conversation objects
* @param {Array} projects - Array of project objects
* @returns {Promise<Object>} Summary statistics
*/
async computeSummary(conversations, projects) {
const totalTokens = conversations.reduce((sum, conv) => sum + conv.tokens, 0);
const totalConversations = conversations.length;
const activeConversations = conversations.filter(c => c.status === 'active').length;
const activeProjects = projects.filter(p => p.status === 'active').length;
const avgTokensPerConversation = totalConversations > 0 ? Math.round(totalTokens / totalConversations) : 0;
const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
// Calculate real Claude sessions (5-hour periods)
const claudeSessionsResult = await this.calculateClaudeSessions(conversations);
const claudeSessions = claudeSessionsResult?.total || 0;
return {
totalConversations,
totalTokens,
activeConversations,
activeProjects,
avgTokensPerConversation,
totalFileSize: this.formatBytes(totalFileSize),
dataSize: this.formatBytes(totalFileSize), // Alias for original dashboard compatibility
lastActivity: conversations.length > 0 ? conversations[0].lastModified : null,
claudeSessions,
claudeSessionsDetail: claudeSessions > 0 ? `${claudeSessions} session${claudeSessions > 1 ? 's' : ''}` : 'no sessions',
claudeSessionsFullData: claudeSessionsResult, // Keep full session data for detailed analysis
};
}
/**
* Calculate Claude usage sessions based on 5-hour periods with caching
* @param {Array} conversations - Array of conversation objects
* @returns {Promise<Object>} Session statistics
*/
async calculateClaudeSessions(conversations) {
if (this.dataCache) {
const dependencies = conversations.map(conv => conv.filePath);
return await this.dataCache.getCachedComputation(
'sessions',
() => this.computeClaudeSessions(conversations),
dependencies
);
}
return this.computeClaudeSessions(conversations);
}
/**
* Compute Claude usage sessions (internal method)
* @param {Array} conversations - Array of conversation objects
* @returns {Promise<Object>} Session statistics
*/
async computeClaudeSessions(conversations) {
// Collect all message timestamps across all conversations
const allMessages = [];
for (const conv of conversations) {
// Use cached file content for better performance
try {
const content = await this.getFileContent(conv.filePath);
const lines = content.trim().split('\n').filter(line => line.trim());
lines.forEach(line => {
try {
const item = JSON.parse(line);
if (item.timestamp && item.message && item.message.role === 'user') {
// Only count user messages as session starters
allMessages.push({
timestamp: new Date(item.timestamp),
conversationId: conv.id,
});
}
} catch {}
});
} catch {}
};
if (allMessages.length === 0) return {
total: 0,
currentMonth: 0,
thisWeek: 0
};
// Sort messages by timestamp
allMessages.sort((a, b) => a.timestamp - b.timestamp);
// Calculate sessions (5-hour periods)
const sessions = [];
let currentSession = null;
allMessages.forEach(message => {
if (!currentSession) {
// Start first session
currentSession = {
start: message.timestamp,
end: new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000), // +5 hours
messageCount: 1,
conversations: new Set([message.conversationId]),
};
} else if (message.timestamp <= currentSession.end) {
// Message is within current session
currentSession.messageCount++;
currentSession.conversations.add(message.conversationId);
// Update session end if this message extends beyond current session
const potentialEnd = new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000);
if (potentialEnd > currentSession.end) {
currentSession.end = potentialEnd;
}
} else {
// Message is outside current session, start new session
sessions.push(currentSession);
currentSession = {
start: message.timestamp,
end: new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000),
messageCount: 1,
conversations: new Set([message.conversationId]),
};
}
});
// Add the last session
if (currentSession) {
sessions.push(currentSession);
}
// Calculate statistics
const now = new Date();
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const thisWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const currentMonthSessions = sessions.filter(s => s.start >= currentMonth).length;
const thisWeekSessions = sessions.filter(s => s.start >= thisWeek).length;
return {
total: sessions.length,
currentMonth: currentMonthSessions,
thisWeek: thisWeekSessions,
sessions: sessions.map(s => ({
start: s.start,
end: s.end,
messageCount: s.messageCount,
conversationCount: s.conversations.size,
duration: Math.round((s.end - s.start) / (1000 * 60 * 60) * 10) / 10, // hours with 1 decimal
})),
};
}
/**
* Simple token estimation fallback
* @param {string} text - Text to estimate tokens for
* @returns {number} Estimated token count
*/
estimateTokens(text) {
// Simple token estimation (roughly 4 characters per token)
return Math.ceil(text.length / 4);
}
/**
* Find TODO files in project directories
* @param {string} projectPath - Path to project directory
* @returns {Promise<Array>} Array of TODO file names
*/
async findTodoFiles(projectPath) {
try {
const files = await fs.readdir(projectPath);
return files.filter(file => file.includes('todo') || file.includes('TODO'));
} catch {
return [];
}
}
/**
* Determine project activity status based on last modification time
* @param {Date} lastActivity - Last activity timestamp
* @returns {string} Status: 'active', 'recent', or 'inactive'
*/
determineProjectStatus(lastActivity) {
const now = new Date();
const timeDiff = now - lastActivity;
const hoursAgo = timeDiff / (1000 * 60 * 60);
if (hoursAgo < 1) return 'active';
if (hoursAgo < 24) return 'recent';
return 'inactive';
}
/**
* Update real-time statistics cache
*/
updateRealtimeStats() {
this.data.realtimeStats = {
totalConversations: this.data.conversations.length,
totalTokens: this.data.conversations.reduce((sum, conv) => sum + conv.tokens, 0),
activeProjects: this.data.activeProjects.filter(p => p.status === 'active').length,
lastActivity: this.data.summary.lastActivity,
};
}
/**
* Format byte sizes for display
* @param {number} bytes - Number of bytes
* @returns {string} Formatted byte string
*/
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Get current conversation data
* @returns {Array} Current conversations
*/
getConversations() {
return this.data.conversations;
}
/**
* Get current project data
* @returns {Array} Current projects
*/
getActiveProjects() {
return this.data.activeProjects;
}
/**
* Get current summary data
* @returns {Object} Current summary
*/
getSummary() {
return this.data.summary;
}
/**
* Get current orphan processes
* @returns {Array} Current orphan processes
*/
getOrphanProcesses() {
return this.data.orphanProcesses;
}
/**
* Get current realtime stats
* @returns {Object} Current realtime stats
*/
getRealtimeStats() {
return this.data.realtimeStats;
}
/**
* Update conversations data (used for external updates)
* @param {Array} conversations - Updated conversations array
*/
setConversations(conversations) {
this.data.conversations = conversations;
this.updateRealtimeStats();
}
/**
* Update orphan processes data
* @param {Array} orphanProcesses - Updated orphan processes array
*/
setOrphanProcesses(orphanProcesses) {
this.data.orphanProcesses = orphanProcesses;
}
}
module.exports = ConversationAnalyzer;