claude-monitor
Version:
Real-time terminal monitoring tool for Claude AI token usage
191 lines • 6.88 kB
JavaScript
import blessed from 'blessed';
import { calculateSessionMetrics, formatTimeUntil, formatPredictedTime, getBurnRateStatus, formatBurnRateStatus } from '../core/session-metrics.js';
import { formatLargeNumber } from '../core/calculations.js';
import { formatTime } from '../utils/time.js';
export class TerminalDisplay {
screen;
container;
mainBox;
footerBox;
lastData = null;
updateTimer = null;
constructor() {
this.screen = blessed.screen({
smartCSR: true,
title: 'Claude Monitor',
fullUnicode: true,
});
this.container = blessed.box({
parent: this.screen,
top: 0,
left: 0,
width: '100%',
height: '100%',
style: {
bg: '#1a1a2e',
},
});
blessed.box({
parent: this.container,
top: 0,
left: 0,
width: '100%',
height: 3,
content: this.createHeader(),
style: {
fg: 'cyan',
bg: '#1a1a2e',
},
});
this.mainBox = blessed.box({
parent: this.container,
top: 3,
left: 0,
width: '100%',
height: '100%-6',
style: {
fg: 'white',
bg: '#1a1a2e',
},
});
this.footerBox = blessed.box({
parent: this.container,
bottom: 0,
left: 0,
width: '100%',
height: 3,
style: {
fg: 'gray',
bg: '#1a1a2e',
},
});
this.screen.key(['q', 'C-c'], () => {
this.cleanup();
process.exit(0);
});
this.screen.render();
}
createHeader() {
const sparkles = '{cyan-fg}✦ ✧ ✦ ✧{/}';
const title = '{bold}{cyan-fg}CLAUDE TOKEN MONITOR{/}';
const line = '{cyan-fg}' + '═'.repeat(50) + '{/}';
return `${sparkles} ${title} ${sparkles}\n${line}`;
}
display(data) {
this.lastData = data;
if (data.activeSession) {
this.displayActiveSession(data.activeSession, data.tokenLimit, data.settings);
}
else {
this.displayNoSession();
}
this.updateFooter(data);
this.screen.render();
}
displayActiveSession(session, tokenLimit, settings) {
const metrics = calculateSessionMetrics(session, tokenLimit);
const content = [];
content.push('{bold}{white-fg}📊 Token Usage:{/}');
content.push(this.createProgressBar(metrics.usagePercentage, 'tokens'));
content.push('');
content.push('{bold}{white-fg}⏰ Time to Reset:{/}');
content.push(this.createProgressBar(metrics.timePercentage, 'time'));
content.push('');
const tokensFormatted = formatLargeNumber(metrics.tokensUsed);
const limitFormatted = formatLargeNumber(tokenLimit);
const remainingFormatted = formatLargeNumber(metrics.tokensRemaining);
content.push(`{white-fg}🎯 Tokens:{/} ${tokensFormatted} / ${limitFormatted} ({green-fg}${remainingFormatted} left{/})`);
if (session.burnRate) {
const burnRateFormatted = Math.round(session.burnRate.tokensPerMinute).toLocaleString();
content.push(`{white-fg}🔥 Burn Rate:{/} ${burnRateFormatted} tokens/min`);
}
content.push('');
content.push(`{white-fg}🏁 Predicted End:{/} ${formatPredictedTime(metrics.predictedEndTime, settings.timeFormat)}`);
content.push(`{white-fg}🔄 Token Reset:{/} ${formatTime(metrics.resetTime, settings.timeFormat)}`);
this.mainBox.setContent(content.join('\n'));
}
displayNoSession() {
const content = [
'',
'{center}{gray-fg}No active session{/center}',
'',
'{center}{gray-fg}Waiting for Claude activity...{/center}',
];
this.mainBox.setContent(content.join('\n'));
}
createProgressBar(percentage, type) {
const width = 40;
const filled = Math.round((percentage / 100) * width);
const empty = Math.max(0, width - filled);
let color = 'green';
let emoji = '🟢';
if (percentage >= 90) {
color = 'red';
emoji = '🔴';
}
else if (percentage >= 70) {
color = 'yellow';
emoji = '🟡';
}
const filledChar = '█';
const emptyChar = '░';
const bar = `{${color}-fg}${filledChar.repeat(filled)}{/}{red-fg}${emptyChar.repeat(empty)}{/}`;
const percentText = `${percentage}%`;
if (type === 'time' && this.lastData?.activeSession) {
const metrics = calculateSessionMetrics(this.lastData.activeSession, this.lastData.tokenLimit);
const timeLeft = formatTimeUntil(metrics.remainingMinutes);
return ` ${emoji} [${bar}] ${timeLeft}`;
}
return ` ${emoji} [${bar}] ${percentText}`;
}
updateFooter(data) {
if (!data.activeSession || !data.activeSession.burnRate) {
this.footerBox.setContent(`{gray-fg}⏱ ${formatTime(new Date(), data.settings.timeFormat)} | Ctrl+C to exit{/}`);
return;
}
const metrics = calculateSessionMetrics(data.activeSession, data.tokenLimit);
const uptime = Math.round(metrics.elapsedMinutes);
const status = getBurnRateStatus(data.activeSession.burnRate, metrics.tokensRemaining, metrics.remainingMinutes);
const statusMsg = formatBurnRateStatus(status);
const content = `{gray-fg}🐶 ${uptime.toString().padStart(2, '0')}:00:00 {/}${statusMsg} {gray-fg}| Ctrl+C to exit 📦{/}`;
this.footerBox.setContent(content);
}
displayError(error) {
const errorBox = blessed.message({
parent: this.screen,
top: 'center',
left: 'center',
width: '50%',
height: 'shrink',
label: ' Error ',
content: error.message,
border: {
type: 'line',
},
style: {
fg: 'red',
bg: 'black',
border: {
fg: 'red',
},
},
});
errorBox.display(error.message, 0, () => {
errorBox.destroy();
this.screen.render();
});
}
displayWarning(message) {
console.warn('Warning:', message);
}
displayInfo(message) {
console.info('Info:', message);
}
cleanup() {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
this.screen.destroy();
}
}
//# sourceMappingURL=terminal-display.js.map