cost-claude
Version:
Claude Code cost monitoring, analytics, and optimization toolkit
178 lines • 6.41 kB
JavaScript
import { readFile, readFileSync, readdir, readdirSync } from 'fs';
import { promisify } from 'util';
import { join } from 'path';
const readFileAsync = promisify(readFile);
const readdirAsync = promisify(readdir);
export class JSONLParser {
async parseFile(filePath) {
const content = await readFileAsync(filePath, 'utf-8');
return this.parseContent(content);
}
parseFileSync(filePath) {
const content = readFileSync(filePath, 'utf-8');
return this.parseContent(content);
}
parseContent(content) {
const lines = content.split('\n').filter((line) => line.trim());
const messages = [];
for (const line of lines) {
try {
const parsed = JSON.parse(line);
messages.push(parsed);
}
catch (error) {
console.warn(`Failed to parse line: ${line.substring(0, 100)}...`, error);
}
}
return messages;
}
parseLine(line) {
try {
return JSON.parse(line);
}
catch (error) {
return null;
}
}
parseMessageContent(message) {
if (!message.message)
return null;
if (typeof message.message === 'object') {
return message.message;
}
if (typeof message.message !== 'string') {
return null;
}
try {
return JSON.parse(message.message);
}
catch {
try {
const cleanedMessage = message.message
.replace(/'/g, '"')
.replace(/None/g, 'null')
.replace(/True/g, 'true')
.replace(/False/g, 'false')
.replace(/(\w+):/g, '"$1":');
return JSON.parse(cleanedMessage);
}
catch (error) {
try {
const contentMatch = message.message.match(/'content':\s*'([^']*)'|"content":\s*"([^"]*)"/);
const roleMatch = message.message.match(/'role':\s*['"](user|assistant)['"]/);
const usageMatch = message.message.match(/'usage':\s*({[^}]+})/);
if (roleMatch) {
const content = {
role: roleMatch[1],
content: contentMatch ? (contentMatch[1] || contentMatch[2]) : '',
};
if (usageMatch && usageMatch[1]) {
try {
const usageStr = usageMatch[1]
.replace(/'/g, '"')
.replace(/None/g, 'null');
content.usage = JSON.parse(usageStr);
}
catch {
}
}
return content;
}
}
catch {
}
}
}
console.warn('Failed to parse message content:', typeof message.message === 'string'
? message.message.substring(0, 100)
: JSON.stringify(message.message).substring(0, 100));
return null;
}
filterByType(messages, type) {
return messages.filter((msg) => msg.type === type);
}
filterBySession(messages, sessionId) {
return messages.filter((msg) => msg.sessionId === sessionId);
}
filterByDateRange(messages, startDate, endDate) {
return messages.filter((msg) => {
const msgDate = new Date(msg.timestamp);
return msgDate >= startDate && msgDate <= endDate;
});
}
getUniqueSessions(messages) {
const sessions = new Set();
messages.forEach((msg) => {
if (msg.sessionId) {
sessions.add(msg.sessionId);
}
});
return Array.from(sessions);
}
sortByTimestamp(messages, ascending = true) {
return [...messages].sort((a, b) => {
const dateA = new Date(a.timestamp).getTime();
const dateB = new Date(b.timestamp).getTime();
return ascending ? dateA - dateB : dateB - dateA;
});
}
groupBySession(messages) {
const grouped = new Map();
messages.forEach((msg) => {
if (msg.sessionId) {
if (!grouped.has(msg.sessionId)) {
grouped.set(msg.sessionId, []);
}
grouped.get(msg.sessionId).push(msg);
}
});
return grouped;
}
calculateSessionDuration(messages) {
if (messages.length === 0)
return 0;
const sorted = this.sortByTimestamp(messages);
if (sorted.length === 0)
return 0;
const firstMsg = sorted[0];
const lastMsg = sorted[sorted.length - 1];
if (!firstMsg || !lastMsg)
return 0;
const first = new Date(firstMsg.timestamp).getTime();
const last = new Date(lastMsg.timestamp).getTime();
return last - first;
}
async parseDirectory(directoryPath) {
const files = await readdirAsync(directoryPath);
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
const allMessages = [];
for (const file of jsonlFiles) {
const filePath = join(directoryPath, file);
try {
const messages = await this.parseFile(filePath);
allMessages.push(...messages);
}
catch (error) {
console.warn(`Failed to parse file ${file}:`, error);
}
}
return allMessages;
}
parseDirectorySync(directoryPath) {
const files = readdirSync(directoryPath);
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
const allMessages = [];
for (const file of jsonlFiles) {
const filePath = join(directoryPath, file);
try {
const messages = this.parseFileSync(filePath);
allMessages.push(...messages);
}
catch (error) {
console.warn(`Failed to parse file ${file}:`, error);
}
}
return allMessages;
}
}
//# sourceMappingURL=jsonl-parser.js.map