UNPKG

cost-claude

Version:

Claude Code cost monitoring, analytics, and optimization toolkit

281 lines (280 loc) 9.91 kB
import { writeFileSync } from 'fs'; import { logger } from '../utils/logger.js'; import { formatDuration, formatCurrency } from '../utils/format.js'; export class ExportService { async export(data, options) { try { switch (options.format) { case 'csv': await this.exportCSV(data, options.outputPath); break; case 'json': await this.exportJSON(data, options.outputPath); break; case 'html': await this.exportHTML(data, options); break; default: throw new Error(`Unsupported export format: ${options.format}`); } logger.info(`Data exported to ${options.outputPath}`); } catch (error) { logger.error('Export failed:', error); throw error; } } async exportCSV(data, outputPath) { const rows = []; if (data.sessions) { rows.push('Session ID,Start Time,End Time,Duration (min),Messages,Total Cost,Input Tokens,Output Tokens,Cache Efficiency'); data.sessions.forEach(session => { rows.push([ session.sessionId, session.startTime, session.endTime, (session.duration / 60000).toFixed(2), session.messageCount, session.totalCost.toFixed(4), session.tokens.input_tokens, session.tokens.output_tokens, session.cacheEfficiency.toFixed(2) ].join(',')); }); } else if (data.messages) { rows.push('Timestamp,Session ID,Type,Cost,Input Tokens,Output Tokens,Cache Read,Cache Creation'); data.messages.forEach(msg => { if (msg.message && typeof msg.message === 'object' && msg.message.usage) { const usage = msg.message.usage; rows.push([ msg.timestamp, msg.sessionId || '', msg.type, (msg.costUSD || 0).toFixed(4), usage.input_tokens || 0, usage.output_tokens || 0, usage.cache_read_input_tokens || 0, usage.cache_creation_input_tokens || 0 ].join(',')); } }); } writeFileSync(outputPath, rows.join('\n')); } async exportJSON(data, outputPath) { const exportObject = { exportDate: new Date().toISOString(), version: '1.0' }; if (data.messages) exportObject.messages = data.messages; if (data.sessions) exportObject.sessions = data.sessions; if (data.dailyCosts) exportObject.dailyCosts = Object.fromEntries(data.dailyCosts); if (data.insights) exportObject.insights = data.insights; if (data.predictions) exportObject.predictions = data.predictions; writeFileSync(outputPath, JSON.stringify(exportObject, null, 2)); } async exportHTML(data, options) { const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${options.title || 'Claude Cost Report'}</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; } .header { background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; } h1 { margin: 0 0 10px 0; color: #2563eb; } .section { background: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; } .metric { display: inline-block; margin: 10px 20px 10px 0; } .metric-label { font-size: 12px; color: #666; text-transform: uppercase; } .metric-value { font-size: 24px; font-weight: bold; color: #2563eb; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { text-align: left; padding: 12px; border-bottom: 1px solid #e5e7eb; } th { background: #f9fafb; font-weight: 600; } .alert { padding: 15px; border-radius: 5px; margin: 10px 0; } .alert-warning { background: #fef3c7; border-left: 4px solid #f59e0b; } .alert-success { background: #d1fae5; border-left: 4px solid #10b981; } .alert-danger { background: #fee2e2; border-left: 4px solid #ef4444; } </style> </head> <body> <div class="header"> <h1>${options.title || 'Claude Cost Report'}</h1> <p>Generated on ${new Date().toLocaleString()}</p> </div> ${this.generateHTMLSummary(data)} ${this.generateHTMLSessions(data)} ${this.generateHTMLInsights(data)} ${this.generateHTMLCharts(data)} </body> </html>`; writeFileSync(options.outputPath, html); } generateHTMLSummary(data) { if (!data.predictions) return ''; let html = '<div class="section"><h2>Summary</h2><div class="metrics">'; if (data.predictions) { html += ` <div class="metric"> <div class="metric-label">Predicted Tomorrow</div> <div class="metric-value">${formatCurrency(data.predictions.nextDay)}</div> </div> <div class="metric"> <div class="metric-label">Predicted Next Week</div> <div class="metric-value">${formatCurrency(data.predictions.nextWeek)}</div> </div>`; } html += '</div></div>'; return html; } generateHTMLSessions(data) { if (!data.sessions || data.sessions.length === 0) return ''; const topSessions = data.sessions .sort((a, b) => b.totalCost - a.totalCost) .slice(0, 10); let html = '<div class="section"><h2>Top Sessions</h2><table>'; html += '<tr><th>Session ID</th><th>Duration</th><th>Messages</th><th>Cost</th><th>Cache Efficiency</th></tr>'; topSessions.forEach(session => { html += ` <tr> <td>${session.sessionId.substring(0, 8)}...</td> <td>${formatDuration(session.duration)}</td> <td>${session.messageCount}</td> <td>${formatCurrency(session.totalCost)}</td> <td>${session.cacheEfficiency.toFixed(1)}%</td> </tr>`; }); html += '</table></div>'; return html; } generateHTMLInsights(data) { if (!data.insights || data.insights.length === 0) return ''; let html = '<div class="section"><h2>Key Insights</h2>'; data.insights.forEach(insight => { const alertClass = insight.severity === 'critical' ? 'alert-danger' : insight.severity === 'warning' ? 'alert-warning' : insight.severity === 'success' ? 'alert-success' : ''; html += ` <div class="alert ${alertClass}"> <strong>${insight.title}</strong><br> ${insight.description} ${insight.recommendation ? `<br><em>Recommendation: ${insight.recommendation}</em>` : ''} ${insight.impact ? `<br><small>Potential savings: ${formatCurrency(insight.impact)}</small>` : ''} </div>`; }); html += '</div>'; return html; } generateHTMLCharts(data) { if (!data.dailyCosts || data.dailyCosts.size === 0) return ''; const dailyData = Array.from(data.dailyCosts.entries()) .sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime()) .slice(-30); return ` <div class="section"> <h2>Daily Cost Trend</h2> <canvas id="costChart" width="400" height="200"></canvas> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> const ctx = document.getElementById('costChart').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: ${JSON.stringify(dailyData.map(d => d[0]))}, datasets: [{ label: 'Daily Cost ($)', data: ${JSON.stringify(dailyData.map(d => d[1]))}, borderColor: 'rgb(37, 99, 235)', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '$' + value.toFixed(2); } } } } } }); </script> </div>`; } } //# sourceMappingURL=export-service.js.map