claude-monitor
Version:
Real-time terminal monitoring tool for Claude AI token usage
173 lines • 6.04 kB
JavaScript
import { normalizeModelName, getTotalTokens } from '../types/index.js';
export class SessionAnalyzer {
sessionDurationMs;
constructor(sessionDurationHours = 5) {
this.sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
}
transformToBlocks(entries) {
if (entries.length === 0) {
return [];
}
const blocks = [];
let currentBlock = null;
for (const entry of entries) {
if (!currentBlock || this.shouldCreateNewBlock(currentBlock, entry)) {
if (currentBlock) {
this.finalizeBlock(currentBlock);
blocks.push(currentBlock);
const gap = this.checkForGap(currentBlock, entry);
if (gap) {
blocks.push(gap);
}
}
currentBlock = this.createNewBlock(entry);
}
this.addEntryToBlock(currentBlock, entry);
}
if (currentBlock) {
this.finalizeBlock(currentBlock);
blocks.push(currentBlock);
}
this.markActiveBlocks(blocks);
return blocks;
}
detectLimits(rawEntries) {
const limits = [];
for (const entry of rawEntries) {
const limitInfo = this.detectSingleLimit(entry);
if (limitInfo) {
limits.push(limitInfo);
}
}
return limits;
}
shouldCreateNewBlock(block, entry) {
if (entry.timestamp >= block.endTime) {
return true;
}
if (block.entries.length > 0) {
const lastEntry = block.entries[block.entries.length - 1];
const timeDiff = entry.timestamp.getTime() - lastEntry.timestamp.getTime();
return timeDiff >= this.sessionDurationMs;
}
return false;
}
roundToHour(timestamp) {
const rounded = new Date(timestamp);
rounded.setMinutes(0, 0, 0);
return rounded;
}
createNewBlock(entry) {
const startTime = this.roundToHour(entry.timestamp);
const endTime = new Date(startTime.getTime() + this.sessionDurationMs);
const blockId = startTime.toISOString();
return {
id: blockId,
startTime,
endTime,
entries: [],
tokenCounts: {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
},
isActive: false,
isGap: false,
burnRate: null,
actualEndTime: null,
perModelStats: {},
models: [],
sentMessagesCount: 0,
costUsd: 0,
limitMessages: [],
projectionData: null,
burnRateSnapshot: null,
};
}
addEntryToBlock(block, entry) {
block.entries.push(entry);
const model = normalizeModelName(entry.model || 'unknown');
if (!block.perModelStats[model]) {
block.perModelStats[model] = {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
cost: 0,
messageCount: 0,
};
}
const modelStats = block.perModelStats[model];
modelStats.inputTokens += entry.inputTokens;
modelStats.outputTokens += entry.outputTokens;
modelStats.cacheCreationTokens += entry.cacheCreationTokens;
modelStats.cacheReadTokens += entry.cacheReadTokens;
modelStats.totalTokens = getTotalTokens(modelStats);
modelStats.cost += entry.costUsd;
modelStats.messageCount += 1;
block.tokenCounts.inputTokens += entry.inputTokens;
block.tokenCounts.outputTokens += entry.outputTokens;
block.tokenCounts.cacheCreationTokens += entry.cacheCreationTokens;
block.tokenCounts.cacheReadTokens += entry.cacheReadTokens;
block.costUsd += entry.costUsd;
if (!block.models.includes(model)) {
block.models.push(model);
}
block.sentMessagesCount += 1;
}
finalizeBlock(block) {
if (block.entries.length > 0) {
const lastEntry = block.entries[block.entries.length - 1];
block.actualEndTime = lastEntry.timestamp;
}
block.sentMessagesCount = block.entries.length;
}
checkForGap(lastBlock, nextEntry) {
if (!lastBlock.actualEndTime) {
return null;
}
const gapDuration = nextEntry.timestamp.getTime() - lastBlock.actualEndTime.getTime();
if (gapDuration >= this.sessionDurationMs) {
const gapId = `gap-${lastBlock.actualEndTime.toISOString()}`;
return {
id: gapId,
startTime: lastBlock.actualEndTime,
endTime: nextEntry.timestamp,
actualEndTime: null,
isGap: true,
isActive: false,
entries: [],
tokenCounts: {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
},
burnRate: null,
perModelStats: {},
models: [],
sentMessagesCount: 0,
costUsd: 0,
limitMessages: [],
projectionData: null,
burnRateSnapshot: null,
};
}
return null;
}
markActiveBlocks(blocks) {
const currentTime = new Date();
for (const block of blocks) {
if (!block.isGap && block.endTime > currentTime) {
block.isActive = true;
}
}
}
detectSingleLimit(_rawData) {
return null;
}
}
export const defaultAnalyzer = new SessionAnalyzer();
//# sourceMappingURL=analyzer.js.map