claude-monitor
Version:
Real-time terminal monitoring tool for Claude AI token usage
200 lines • 7.66 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as readline from 'readline';
import { EventEmitter } from 'events';
import { CostMode } from '../types/index.js';
import { normalizeModelName } from '../types/index.js';
import { PricingCalculator } from '../core/pricing.js';
import { isWithinHours } from '../utils/time.js';
export class UsageDataReader extends EventEmitter {
dataPath;
pricingCalculator;
processedHashes = new Set();
constructor(dataPath, pricingCalculator) {
super();
this.dataPath = dataPath || path.join(os.homedir(), '.claude', 'projects');
this.pricingCalculator = pricingCalculator || new PricingCalculator();
}
async loadUsageEntries(options = {}) {
const { dataPath = this.dataPath, hoursBack, costMode = 'auto', includeRaw = false, } = options;
const jsonlFiles = await this.findJsonlFiles(dataPath);
if (jsonlFiles.length === 0) {
this.emit('warning', `No JSONL files found in ${dataPath}`);
return { entries: [], rawEntries: includeRaw ? [] : undefined };
}
const entries = [];
const rawEntries = includeRaw ? [] : [];
for (const filePath of jsonlFiles) {
const result = await this.processFile(filePath, {
hoursBack,
costMode,
includeRaw,
});
entries.push(...result.entries);
if (includeRaw && result.rawEntries) {
rawEntries.push(...result.rawEntries);
}
}
entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
this.emit('info', `Processed ${entries.length} entries from ${jsonlFiles.length} files`);
return {
entries,
rawEntries: includeRaw ? rawEntries : undefined,
};
}
async findJsonlFiles(dataPath) {
const files = [];
const findFiles = async (dir) => {
try {
const items = await fs.promises.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
await findFiles(fullPath);
}
else if (item.isFile() && item.name.endsWith('.jsonl')) {
files.push(fullPath);
}
}
}
catch (error) {
this.emit('error', `Error reading directory ${dir}: ${error}`);
}
};
if (fs.existsSync(dataPath)) {
await findFiles(dataPath);
}
else {
this.emit('warning', `Data path does not exist: ${dataPath}`);
}
return files;
}
async processFile(filePath, options) {
const entries = [];
const rawEntries = options.includeRaw ? [] : [];
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let entriesRead = 0;
let entriesFiltered = 0;
let entriesMapped = 0;
rl.on('line', (line) => {
line = line.trim();
if (!line)
return;
try {
const data = JSON.parse(line);
entriesRead++;
if (!this.shouldProcessEntry(data, options.hoursBack)) {
entriesFiltered++;
return;
}
const entry = this.mapToUsageEntry(data, options.costMode);
if (entry) {
entriesMapped++;
entries.push(entry);
this.updateProcessedHashes(data);
}
if (options.includeRaw) {
rawEntries.push(data);
}
}
catch (error) {
this.emit('debug', `Failed to parse JSON line in ${filePath}: ${error}`);
}
});
rl.on('close', () => {
this.emit('debug', `File ${path.basename(filePath)}: ${entriesRead} read, ` +
`${entriesFiltered} filtered out, ${entriesMapped} successfully mapped`);
resolve({ entries, rawEntries: options.includeRaw ? rawEntries : undefined });
});
rl.on('error', (error) => {
this.emit('error', `Error reading file ${filePath}: ${error}`);
reject(error);
});
});
}
shouldProcessEntry(data, hoursBack) {
if (hoursBack && data.timestamp) {
if (!isWithinHours(data.timestamp, hoursBack)) {
return false;
}
}
const hash = this.createUniqueHash(data);
return !hash || !this.processedHashes.has(hash);
}
createUniqueHash(data) {
const messageId = data.message?.id || data.message_id;
const requestId = data.requestId || data.request_id;
if (messageId && requestId) {
return `${messageId}:${requestId}`;
}
return null;
}
updateProcessedHashes(data) {
const hash = this.createUniqueHash(data);
if (hash) {
this.processedHashes.add(hash);
}
}
mapToUsageEntry(data, costMode) {
try {
if (data.type !== 'assistant') {
return null;
}
const timestamp = new Date(data.timestamp);
if (isNaN(timestamp.getTime())) {
return null;
}
const usage = data.message?.usage || data.usage;
if (!usage) {
return null;
}
const inputTokens = usage.input_tokens || 0;
const outputTokens = usage.output_tokens || 0;
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
const cacheReadTokens = usage.cache_read_input_tokens || 0;
if (inputTokens === 0 && outputTokens === 0 && cacheCreationTokens === 0 && cacheReadTokens === 0) {
return null;
}
const model = normalizeModelName(data.message?.model || data.model || 'unknown');
let costUsd = 0;
if (costMode === CostMode.CACHED && data.cost?.total_cost !== undefined) {
costUsd = data.cost.total_cost;
}
else {
costUsd = this.pricingCalculator.calculateCost({
model,
inputTokens,
outputTokens,
cacheCreationTokens,
cacheReadTokens,
});
}
return {
timestamp,
inputTokens,
outputTokens,
cacheCreationTokens,
cacheReadTokens,
costUsd,
model,
messageId: data.message?.id || data.message_id || '',
requestId: data.requestId || data.request_id || 'unknown',
};
}
catch (error) {
this.emit('debug', `Failed to map entry: ${error}`);
return null;
}
}
clearCache() {
this.processedHashes.clear();
}
}
export const defaultReader = new UsageDataReader();
//# sourceMappingURL=reader.js.map