UNPKG

claude-monitor

Version:

Real-time terminal monitoring tool for Claude AI token usage

195 lines 6.71 kB
import { EventEmitter } from 'events'; import { CostMode } from '../types/index.js'; import { UsageDataReader } from '../data/reader.js'; import { SessionAnalyzer } from '../data/analyzer.js'; import { getTokenLimit, DEFAULT_TOKEN_LIMIT } from '../core/plans.js'; import { calculateBurnRate } from '../core/calculations.js'; export class MonitoringOrchestrator extends EventEmitter { updateInterval; dataReader; sessionAnalyzer; settings = null; monitoring = false; updateTimer = null; lastValidData = null; currentSessionId = null; sessionCount = 0; dataCache = null; cacheTTL = 5000; constructor(options = {}) { super(); this.updateInterval = (options.updateInterval || 10) * 1000; this.cacheTTL = (options.cacheTTL || 5) * 1000; this.dataReader = new UsageDataReader(options.dataPath); this.sessionAnalyzer = new SessionAnalyzer(); this.dataReader.on('error', (error) => this.emit('error', error)); this.dataReader.on('warning', (message) => this.emit('warning', message)); this.dataReader.on('info', (message) => this.emit('info', message)); } setSettings(settings) { this.settings = settings; if (settings.updateInterval) { this.updateInterval = settings.updateInterval * 1000; } } start() { if (this.monitoring) { this.emit('warning', 'Monitoring already running'); return; } this.emit('info', `Starting monitoring with ${this.updateInterval / 1000}s interval`); this.monitoring = true; this.fetchAndProcessData().then(() => { this.scheduleNextUpdate(); }); } stop() { if (!this.monitoring) { return; } this.emit('info', 'Stopping monitoring'); this.monitoring = false; if (this.updateTimer) { clearTimeout(this.updateTimer); this.updateTimer = null; } } async forceRefresh() { this.dataCache = null; return this.fetchAndProcessData(); } getLastData() { return this.lastValidData; } async waitForInitialData(timeout = 10000) { if (this.lastValidData) { return true; } return new Promise((resolve) => { let resolved = false; const timer = setTimeout(() => { if (!resolved) { resolved = true; resolve(false); } }, timeout); const handler = () => { if (!resolved) { resolved = true; clearTimeout(timer); this.off('data-update', handler); resolve(true); } }; this.once('data-update', handler); }); } scheduleNextUpdate() { if (!this.monitoring) { return; } this.updateTimer = setTimeout(() => { this.fetchAndProcessData().then(() => { this.scheduleNextUpdate(); }); }, this.updateInterval); } async fetchAndProcessData() { try { const startTime = Date.now(); if (this.dataCache && Date.now() - this.dataCache.timestamp < this.cacheTTL) { this.emit('info', 'Using cached data'); return this.processBlocks(this.dataCache.blocks); } const result = await this.dataReader.loadUsageEntries({ hoursBack: this.settings?.hoursBack || 5, costMode: this.settings?.costMode || CostMode.AUTO, }); if (result.entries.length === 0) { this.emit('warning', 'No usage entries found'); return null; } const blocks = this.sessionAnalyzer.transformToBlocks(result.entries); this.dataCache = { blocks, timestamp: Date.now(), }; const monitoringData = this.processBlocks(blocks); const elapsed = Date.now() - startTime; this.emit('info', `Data processing completed in ${elapsed}ms`); return monitoringData; } catch (error) { this.emit('error', error); return null; } } async processBlocks(blocks) { if (!this.settings) { this.emit('error', new Error('Settings not initialized')); return null; } const activeSession = blocks.find((block) => block.isActive) || null; for (const block of blocks) { if (!block.isGap) { block.burnRate = calculateBurnRate(block); } } this.checkSessionChanges(activeSession); const tokenLimit = await this.calculateTokenLimit(blocks); const monitoringData = { blocks, activeSession, tokenLimit, settings: this.settings, sessionId: this.currentSessionId, sessionCount: this.sessionCount, lastUpdate: new Date(), }; this.lastValidData = monitoringData; this.emit('data-update', monitoringData); return monitoringData; } checkSessionChanges(activeSession) { const newSessionId = activeSession?.id || null; if (newSessionId !== this.currentSessionId) { if (this.currentSessionId) { this.emit('session-change', 'ended', this.currentSessionId); } if (newSessionId && activeSession) { this.sessionCount++; this.emit('session-change', 'started', newSessionId, activeSession); } this.currentSessionId = newSessionId; } } async calculateTokenLimit(blocks) { if (!this.settings) { return DEFAULT_TOKEN_LIMIT; } const plan = this.settings.planType; try { if (plan === 'custom') { return await getTokenLimit(plan, blocks); } return await getTokenLimit(plan); } catch (error) { this.emit('error', new Error(`Error calculating token limit: ${error}`)); return DEFAULT_TOKEN_LIMIT; } } emit(event, ...args) { return super.emit(event, ...args); } on(event, listener) { return super.on(event, listener); } once(event, listener) { return super.once(event, listener); } off(event, listener) { return super.off(event, listener); } } //# sourceMappingURL=orchestrator.js.map