claude-monitor
Version:
Real-time terminal monitoring tool for Claude AI token usage
195 lines • 6.71 kB
JavaScript
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