UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

368 lines 13.9 kB
/** * Claude AI Token Usage Monitor * Real-time monitoring of Claude token consumption with burn rate analysis * Based on: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor */ import { spawn } from "child_process"; import { EventEmitter } from "events"; export class ClaudeTokenMonitor extends EventEmitter { config; tokenLimit; isRunning = false; monitorProcess; lastStats; constructor(config = {}) { super(); this.config = { plan: 'pro', timezone: 'UTC', updateInterval: 3, ...config }; this.tokenLimit = this.getTokenLimit(this.config.plan); } getTokenLimit(plan) { const limits = { 'pro': 7000, 'max5': 35000, 'max20': 140000, 'custom_max': 7000 // Will be auto-detected }; return limits[plan] || 7000; } async runCCUsage() { // Try newer Claude Code CLI first const claudeData = await this.tryClaudeCodeMetrics(); if (claudeData) { return claudeData; } // Fallback to legacy ccusage if available return new Promise((resolve) => { const process = spawn('ccusage', ['blocks', '--json'], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data.toString(); }); process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('close', (code) => { if (code === 0) { try { const data = JSON.parse(stdout); resolve(data); } catch (error) { // Silently fail - no JSON logging resolve(null); } } else { // Silently fail - no JSON logging resolve(null); } }); process.on('error', (error) => { // Silently fail - no JSON logging resolve(null); }); }); } async tryClaudeCodeMetrics() { try { // Check for Claude Code logs/metrics const fs = await import('fs/promises'); const path = await import('path'); const os = await import('os'); // Try to find Claude Code configuration directory const homeDir = os.homedir(); const configDirs = [ path.join(homeDir, '.config', 'claude'), path.join(homeDir, '.claude'), path.join(homeDir, 'Library', 'Application Support', 'ClaudeCode'), ]; for (const configDir of configDirs) { try { const stats = await fs.stat(configDir); if (stats.isDirectory()) { const sessionData = await this.parseClaudeCodeSessions(configDir); if (sessionData) { return sessionData; } } } catch (error) { // Directory doesn't exist, continue } } return null; } catch (error) { return null; } } async parseClaudeCodeSessions(configDir) { try { const fs = await import('fs/promises'); const path = await import('path'); // Look for session files or logs const sessionsDir = path.join(configDir, 'sessions'); try { const sessionFiles = await fs.readdir(sessionsDir); const blocks = []; const now = new Date(); for (const file of sessionFiles) { if (file.endsWith('.json')) { try { const filePath = path.join(sessionsDir, file); const content = await fs.readFile(filePath, 'utf-8'); const sessionData = JSON.parse(content); // Parse session data to extract token usage const block = this.parseSessionToBlock(sessionData, now); if (block) { blocks.push(block); } } catch (error) { // Skip invalid session files } } } if (blocks.length > 0) { return { blocks }; } } catch (error) { // Sessions directory doesn't exist or can't be read } return null; } catch (error) { return null; } } parseSessionToBlock(sessionData, currentTime) { try { // Extract token usage from Claude Code session data let totalTokens = 0; let startTime = currentTime.toISOString(); let endTime; let isActive = false; // Look for token usage in various possible formats if (sessionData.messages && Array.isArray(sessionData.messages)) { for (const message of sessionData.messages) { if (message.usage) { totalTokens += (message.usage.input_tokens || 0); totalTokens += (message.usage.output_tokens || 0); totalTokens += (message.usage.cache_creation_tokens || 0); totalTokens += (message.usage.cache_read_tokens || 0); } // Track timing if (message.timestamp || message.created_at) { const msgTime = message.timestamp || message.created_at; if (!startTime || msgTime < startTime) { startTime = msgTime; } if (!endTime || msgTime > endTime) { endTime = msgTime; } } } } // Check if session is active (recent activity within last 5 hours) const fiveHoursAgo = new Date(currentTime.getTime() - 5 * 60 * 60 * 1000); const sessionStart = new Date(startTime); isActive = sessionStart > fiveHoursAgo; if (totalTokens > 0) { return { startTime, actualEndTime: endTime, totalTokens, isActive, isGap: false }; } return null; } catch (error) { return null; } } calculateHourlyBurnRate(blocks, currentTime) { if (!blocks.length) return 0; const oneHourAgo = new Date(currentTime.getTime() - 60 * 60 * 1000); let totalTokens = 0; for (const block of blocks) { if (block.isGap) continue; const startTime = new Date(block.startTime); let sessionEnd; if (block.isActive) { sessionEnd = currentTime; } else if (block.actualEndTime) { sessionEnd = new Date(block.actualEndTime); } else { sessionEnd = currentTime; } // Skip sessions that ended before the last hour if (sessionEnd < oneHourAgo) continue; // Calculate overlap with the last hour const sessionStartInHour = new Date(Math.max(startTime.getTime(), oneHourAgo.getTime())); const sessionEndInHour = new Date(Math.min(sessionEnd.getTime(), currentTime.getTime())); if (sessionEndInHour <= sessionStartInHour) continue; // Calculate portion of tokens used in the last hour const totalSessionDuration = (sessionEnd.getTime() - startTime.getTime()) / (1000 * 60); const hourDuration = (sessionEndInHour.getTime() - sessionStartInHour.getTime()) / (1000 * 60); if (totalSessionDuration > 0) { const tokensInHour = block.totalTokens * (hourDuration / totalSessionDuration); totalTokens += tokensInHour; } } return totalTokens / 60; // tokens per minute } getNextResetTime(currentTime) { const resetHours = this.config.resetHour ? [this.config.resetHour] : [4, 9, 14, 18, 23]; const current = new Date(currentTime); // Find next reset hour for (const hour of resetHours) { const resetTime = new Date(current); resetTime.setHours(hour, 0, 0, 0); if (resetTime > current) { return resetTime; } } // If no reset today, use first one tomorrow const tomorrow = new Date(current); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(resetHours[0], 0, 0, 0); return tomorrow; } async calculateStats(data) { const activeBlock = data.blocks.find(block => block.isActive); if (!activeBlock) return null; const currentTime = new Date(); const tokensUsed = activeBlock.totalTokens; // Auto-detect custom_max if needed if (this.config.plan === 'custom_max') { const maxTokens = Math.max(...data.blocks .filter(block => !block.isGap && !block.isActive) .map(block => block.totalTokens)); if (maxTokens > this.tokenLimit) { this.tokenLimit = maxTokens; } } const tokensLeft = Math.max(0, this.tokenLimit - tokensUsed); const usagePercentage = (tokensUsed / this.tokenLimit) * 100; const burnRate = this.calculateHourlyBurnRate(data.blocks, currentTime); const startTime = new Date(activeBlock.startTime); const sessionDuration = (currentTime.getTime() - startTime.getTime()) / (1000 * 60); const resetTime = this.getNextResetTime(currentTime); const timeToReset = (resetTime.getTime() - currentTime.getTime()) / (1000 * 60); let predictedEndTime; if (burnRate > 0 && tokensLeft > 0) { const minutesToDepletion = tokensLeft / burnRate; predictedEndTime = new Date(currentTime.getTime() + minutesToDepletion * 60 * 1000); } else { predictedEndTime = resetTime; } return { tokensUsed, tokensLeft, usagePercentage, burnRate, sessionDuration, timeToReset, predictedEndTime, resetTime }; } async start() { if (this.isRunning) return; this.isRunning = true; this.emit('started'); const monitor = async () => { if (!this.isRunning) return; try { const data = await this.runCCUsage(); if (data) { const stats = await this.calculateStats(data); if (stats) { this.lastStats = stats; this.emit('stats', stats); } } } catch (error) { // Silently handle errors - emit error event but don't log JSON this.emit('error', error); } if (this.isRunning) { this.monitorProcess = setTimeout(monitor, this.config.updateInterval * 1000); } }; await monitor(); } stop() { this.isRunning = false; if (this.monitorProcess) { clearTimeout(this.monitorProcess); this.monitorProcess = undefined; } this.emit('stopped'); } getLastStats() { return this.lastStats; } isMonitoring() { return this.isRunning; } updateConfig(config) { this.config = { ...this.config, ...config }; this.tokenLimit = this.getTokenLimit(this.config.plan); } } export function formatTime(minutes) { if (minutes < 60) { return `${Math.round(minutes)}m`; } const hours = Math.floor(minutes / 60); const mins = Math.round(minutes % 60); return mins === 0 ? `${hours}h` : `${hours}h ${mins}m`; } export function createProgressBar(percentage, width = 50) { const filled = Math.round(width * percentage / 100); const empty = width - filled; const greenBar = '█'.repeat(filled); const redBar = '░'.repeat(empty); // ANSI color codes const green = '\x1b[92m'; const red = '\x1b[91m'; const reset = '\x1b[0m'; return `🟢 [${green}${greenBar}${red}${redBar}${reset}] ${percentage.toFixed(1)}%`; } export function createTimeProgressBar(elapsed, total, width = 50) { const percentage = Math.min(100, (elapsed / total) * 100); const filled = Math.round(width * percentage / 100); const empty = width - filled; const blueBar = '█'.repeat(filled); const redBar = '░'.repeat(empty); // ANSI color codes const blue = '\x1b[94m'; const red = '\x1b[91m'; const reset = '\x1b[0m'; const remainingTime = formatTime(Math.max(0, total - elapsed)); return `⏰ [${blue}${blueBar}${red}${redBar}${reset}] ${remainingTime}`; } //# sourceMappingURL=claude-token-monitor.js.map