UNPKG

claude-monitor

Version:

Real-time terminal monitoring tool for Claude AI token usage

200 lines 7.66 kB
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