UNPKG

cost-claude

Version:

Claude Code cost monitoring, analytics, and optimization toolkit

353 lines 15.5 kB
import { EventEmitter } from 'events'; import { ProjectParser } from '../core/project-parser.js'; import { JSONLParser } from '../core/jsonl-parser.js'; import { CostCalculator } from '../core/cost-calculator.js'; import { logger } from '../utils/logger.js'; export class SessionDetector extends EventEmitter { jsonlParser; costCalculator; sessions = new Map(); config; constructor(config = {}) { super(); this.jsonlParser = new JSONLParser(); this.costCalculator = new CostCalculator(); this.costCalculator.ensureRatesLoaded().catch(err => { logger.error('Failed to load pricing rates:', err); }); this.config = { inactivityTimeout: config.inactivityTimeout ?? 300000, summaryMessageTimeout: config.summaryMessageTimeout ?? 5000, taskCompletionTimeout: config.taskCompletionTimeout ?? 3000, delayedTaskCompletionTimeout: config.delayedTaskCompletionTimeout ?? 30000, minTaskCost: config.minTaskCost ?? 0.01, minTaskMessages: config.minTaskMessages ?? 1, enableProgressNotifications: config.enableProgressNotifications ?? true, progressCheckInterval: config.progressCheckInterval ?? 10000, minProgressCost: config.minProgressCost ?? 0.02, minProgressDuration: config.minProgressDuration ?? 15000, }; } processMessage(message, filePath) { const sessionId = message.sessionId || 'unknown'; if (!this.sessions.has(sessionId)) { const projectName = ProjectParser.getProjectFromMessage(message, filePath); this.sessions.set(sessionId, { messages: [], totalCost: 0, startTime: new Date(message.timestamp), lastActivity: new Date(message.timestamp), projectName, hasSummary: false, filePath, currentTaskCost: 0, currentTaskAssistantCount: 0, taskInProgress: false }); logger.debug(`New session detected: ${sessionId} for project: ${projectName}`); } const session = this.sessions.get(sessionId); session.messages.push(message); session.lastActivity = new Date(message.timestamp); session.lastMessageUuid = message.uuid; if (session.inactivityTimer) { clearTimeout(session.inactivityTimer); } if (session.summaryTimer) { clearTimeout(session.summaryTimer); } if (session.taskTimer) { clearTimeout(session.taskTimer); } if (session.delayedTaskTimer) { clearTimeout(session.delayedTaskTimer); } if (session.progressTimer) { clearTimeout(session.progressTimer); } if (message.type === 'user') { if (session.currentTaskAssistantCount > 0) { logger.debug(`Task interrupted by new user message in session ${sessionId}`); } session.currentTaskStartTime = new Date(message.timestamp); session.currentTaskCost = 0; session.currentTaskAssistantCount = 0; session.lastAssistantMessageTime = undefined; session.taskInProgress = false; session.lastProgressNotificationTime = undefined; } else if (message.type === 'assistant') { let messageCost = 0; if (message.costUSD !== null && message.costUSD !== undefined) { messageCost = message.costUSD; } else { const content = this.jsonlParser.parseMessageContent(message); if (content?.usage) { messageCost = this.costCalculator.calculate(content.usage); } } session.totalCost += messageCost; if (!session.currentTaskStartTime) { session.currentTaskStartTime = new Date(message.timestamp); } session.currentTaskCost += messageCost; session.currentTaskAssistantCount++; session.lastAssistantMessageTime = new Date(message.timestamp); session.taskInProgress = true; session.taskTimer = setTimeout(() => { this.completeTask(sessionId, 'immediate'); }, this.config.taskCompletionTimeout); session.delayedTaskTimer = setTimeout(() => { this.completeTask(sessionId, 'delayed'); }, this.config.delayedTaskCompletionTimeout); if (this.config.enableProgressNotifications && !session.progressTimer) { this.startProgressMonitoring(sessionId); } logger.debug(`Assistant message in session ${sessionId}, immediate timer: ${this.config.taskCompletionTimeout}ms, delayed timer: ${this.config.delayedTaskCompletionTimeout}ms`); } else if (message.type === 'summary') { session.hasSummary = true; session.summaryText = message.summary || 'Task completed'; logger.debug(`Summary detected for session ${sessionId}: ${session.summaryText}`); session.summaryTimer = setTimeout(() => { this.completeSession(sessionId, 'summary'); }, this.config.summaryMessageTimeout); return; } session.inactivityTimer = setTimeout(() => { this.completeSession(sessionId, 'inactivity'); }, this.config.inactivityTimeout); } startProgressMonitoring(sessionId) { const session = this.sessions.get(sessionId); if (!session) return; if (session.progressTimer) { clearTimeout(session.progressTimer); } session.progressTimer = setInterval(() => { this.checkTaskProgress(sessionId); }, this.config.progressCheckInterval); } checkTaskProgress(sessionId) { const session = this.sessions.get(sessionId); if (!session || !session.taskInProgress) { if (session?.progressTimer) { clearInterval(session.progressTimer); session.progressTimer = undefined; } return; } const now = Date.now(); const taskDuration = session.lastAssistantMessageTime ? now - (session.currentTaskStartTime?.getTime() || 0) : 0; if (taskDuration >= this.config.minProgressDuration && session.currentTaskCost >= this.config.minProgressCost) { const timeSinceLastNotification = session.lastProgressNotificationTime ? now - session.lastProgressNotificationTime.getTime() : Infinity; if (timeSinceLastNotification >= this.config.progressCheckInterval) { const progressData = { sessionId, projectName: session.projectName, currentCost: session.currentTaskCost, currentDuration: taskDuration, assistantMessageCount: session.currentTaskAssistantCount, isActive: true, estimatedCompletion: this.estimateCompletion(session) }; logger.debug(`Task progress in session ${sessionId}`, { cost: progressData.currentCost, duration: progressData.currentDuration, messages: progressData.assistantMessageCount }); this.emit('task-progress', progressData); session.lastProgressNotificationTime = new Date(now); } } } estimateCompletion(session) { if (!session.lastAssistantMessageTime) return undefined; const timeSinceLastMessage = Date.now() - session.lastAssistantMessageTime.getTime(); const averageMessageInterval = session.currentTaskAssistantCount > 1 ? (session.lastAssistantMessageTime.getTime() - session.currentTaskStartTime.getTime()) / session.currentTaskAssistantCount : 5000; if (timeSinceLastMessage > averageMessageInterval * 2) { return Math.min(5000, this.config.taskCompletionTimeout - timeSinceLastMessage); } return averageMessageInterval * 2; } completeTask(sessionId, completionType = 'immediate') { const session = this.sessions.get(sessionId); if (!session || session.currentTaskAssistantCount === 0) return; if (session.taskTimer) { clearTimeout(session.taskTimer); session.taskTimer = undefined; } if (session.delayedTaskTimer) { clearTimeout(session.delayedTaskTimer); session.delayedTaskTimer = undefined; } if (session.progressTimer) { clearInterval(session.progressTimer); session.progressTimer = undefined; } if (session.currentTaskCost < this.config.minTaskCost || session.currentTaskAssistantCount < this.config.minTaskMessages) { logger.debug(`Task in session ${sessionId} below thresholds (cost: ${session.currentTaskCost}, messages: ${session.currentTaskAssistantCount})`); session.currentTaskCost = 0; session.currentTaskAssistantCount = 0; session.currentTaskStartTime = undefined; session.lastAssistantMessageTime = undefined; session.taskInProgress = false; session.lastProgressNotificationTime = undefined; return; } const taskDuration = session.lastAssistantMessageTime ? session.lastAssistantMessageTime.getTime() - (session.currentTaskStartTime?.getTime() || 0) : 0; const taskCompletionData = { sessionId, projectName: session.projectName, taskCost: session.currentTaskCost, taskDuration, assistantMessageCount: session.currentTaskAssistantCount, lastMessageUuid: session.lastMessageUuid || '', timestamp: session.lastAssistantMessageTime || new Date(), completionType }; logger.debug(`Task completed in session ${sessionId} (${completionType})`, { cost: taskCompletionData.taskCost, messages: taskCompletionData.assistantMessageCount, duration: taskCompletionData.taskDuration, completionType }); this.emit('task-completed', taskCompletionData); session.currentTaskCost = 0; session.currentTaskAssistantCount = 0; session.currentTaskStartTime = undefined; session.lastAssistantMessageTime = undefined; session.taskInProgress = false; session.lastProgressNotificationTime = undefined; } completeSession(sessionId, reason) { const session = this.sessions.get(sessionId); if (!session) return; if (session.inactivityTimer) { clearTimeout(session.inactivityTimer); } if (session.summaryTimer) { clearTimeout(session.summaryTimer); } if (session.taskTimer) { clearTimeout(session.taskTimer); } if (session.delayedTaskTimer) { clearTimeout(session.delayedTaskTimer); } if (session.progressTimer) { clearInterval(session.progressTimer); } const duration = session.lastActivity.getTime() - session.startTime.getTime(); const completionData = { sessionId, projectName: session.projectName, summary: session.summaryText || this.generateSummary(session.messages), totalCost: session.totalCost, messageCount: session.messages.length, duration, startTime: session.startTime, endTime: session.lastActivity, lastMessageUuid: session.lastMessageUuid || '' }; logger.debug(`Session completed (${reason}): ${sessionId}`, { project: completionData.projectName, cost: completionData.totalCost, messages: completionData.messageCount, duration: completionData.duration }); this.emit('session-completed', completionData); this.sessions.delete(sessionId); } generateSummary(messages) { const userMessages = messages.filter(m => m.type === 'user').length; const assistantMessages = messages.filter(m => m.type === 'assistant').length; if (userMessages === 0) { return 'No user interaction'; } else if (assistantMessages === 0) { return 'No assistant responses'; } else { return `${userMessages} questions, ${assistantMessages} responses`; } } completeAllSessions() { const sessionIds = Array.from(this.sessions.keys()); sessionIds.forEach(sessionId => { this.completeSession(sessionId, 'manual'); }); } getActiveSessions() { return Array.from(this.sessions.keys()); } getSessionInfo(sessionId) { const session = this.sessions.get(sessionId); if (!session) return null; return { sessionId, projectName: session.projectName, totalCost: session.totalCost, messageCount: session.messages.length, hasSummary: session.hasSummary, lastActivity: session.lastActivity, duration: session.lastActivity.getTime() - session.startTime.getTime() }; } isSessionIdle(sessionId) { const session = this.sessions.get(sessionId); if (!session) return false; const idleTime = Date.now() - session.lastActivity.getTime(); return idleTime > this.config.inactivityTimeout; } updateConfig(config) { if (config.inactivityTimeout !== undefined) { this.config.inactivityTimeout = config.inactivityTimeout; } if (config.summaryMessageTimeout !== undefined) { this.config.summaryMessageTimeout = config.summaryMessageTimeout; } if (config.taskCompletionTimeout !== undefined) { this.config.taskCompletionTimeout = config.taskCompletionTimeout; } if (config.delayedTaskCompletionTimeout !== undefined) { this.config.delayedTaskCompletionTimeout = config.delayedTaskCompletionTimeout; } if (config.minTaskCost !== undefined) { this.config.minTaskCost = config.minTaskCost; } if (config.minTaskMessages !== undefined) { this.config.minTaskMessages = config.minTaskMessages; } if (config.enableProgressNotifications !== undefined) { this.config.enableProgressNotifications = config.enableProgressNotifications; } if (config.progressCheckInterval !== undefined) { this.config.progressCheckInterval = config.progressCheckInterval; } if (config.minProgressCost !== undefined) { this.config.minProgressCost = config.minProgressCost; } if (config.minProgressDuration !== undefined) { this.config.minProgressDuration = config.minProgressDuration; } } } //# sourceMappingURL=session-detector.js.map