UNPKG

@owloops/claude-powerline

Version:

Beautiful vim-style powerline statusline for Claude Code with real-time usage tracking, git integration, and custom themes

208 lines (180 loc) 5.52 kB
import { debug } from "../utils/logger"; import { PricingService } from "./pricing"; import { findTranscriptFile, findAgentTranscripts, parseJsonlFile, type ParsedEntry, type ClaudeHookData, } from "../utils/claude"; import { dirname } from "node:path"; export interface SessionUsageEntry { timestamp: string; message: { usage: { input_tokens: number; output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; }; }; costUSD?: number; } export interface SessionUsage { totalCost: number; entries: SessionUsageEntry[]; } export interface TokenBreakdown { input: number; output: number; cacheCreation: number; cacheRead: number; } export interface SessionInfo { cost: number | null; calculatedCost: number | null; officialCost: number | null; tokens: number | null; tokenBreakdown: TokenBreakdown | null; } export interface UsageInfo { session: SessionInfo; } function convertToSessionEntry(entry: ParsedEntry): SessionUsageEntry { return { timestamp: entry.timestamp.toISOString(), message: { usage: { input_tokens: entry.message?.usage?.input_tokens || 0, output_tokens: entry.message?.usage?.output_tokens || 0, cache_creation_input_tokens: entry.message?.usage?.cache_creation_input_tokens, cache_read_input_tokens: entry.message?.usage?.cache_read_input_tokens, }, }, costUSD: entry.costUSD, }; } export class SessionProvider { async getSessionUsage(sessionId: string): Promise<SessionUsage | null> { try { const transcriptPath = await findTranscriptFile(sessionId); if (!transcriptPath) { debug(`No transcript found for session: ${sessionId}`); return null; } debug(`Found transcript at: ${transcriptPath}`); const parsedEntries = await parseJsonlFile(transcriptPath); const projectPath = dirname(transcriptPath); const agentTranscripts = await findAgentTranscripts( sessionId, projectPath, ); debug(`Found ${agentTranscripts.length} agent transcripts for session`); for (const agentPath of agentTranscripts) { const agentEntries = await parseJsonlFile(agentPath); parsedEntries.push(...agentEntries); } if (parsedEntries.length === 0) { return { totalCost: 0, entries: [] }; } const entries: SessionUsageEntry[] = []; let totalCost = 0; for (const entry of parsedEntries) { if (entry.message?.usage) { const sessionEntry = convertToSessionEntry(entry); if (sessionEntry.costUSD !== undefined) { totalCost += sessionEntry.costUSD; } else { const cost = await PricingService.calculateCostForEntry(entry.raw); sessionEntry.costUSD = cost; totalCost += cost; } entries.push(sessionEntry); } } debug( `Parsed ${entries.length} usage entries, total cost: $${totalCost.toFixed(4)}`, ); return { totalCost, entries }; } catch (error) { debug(`Error reading session usage for ${sessionId}:`, error); return null; } } calculateTokenBreakdown(entries: SessionUsageEntry[]): TokenBreakdown { return entries.reduce( (breakdown, entry) => ({ input: breakdown.input + (entry.message.usage.input_tokens || 0), output: breakdown.output + (entry.message.usage.output_tokens || 0), cacheCreation: breakdown.cacheCreation + (entry.message.usage.cache_creation_input_tokens || 0), cacheRead: breakdown.cacheRead + (entry.message.usage.cache_read_input_tokens || 0), }), { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 }, ); } async getSessionInfo( sessionId: string, hookData?: ClaudeHookData, ): Promise<SessionInfo> { const sessionUsage = await this.getSessionUsage(sessionId); if (!sessionUsage || sessionUsage.entries.length === 0) { return { cost: null, calculatedCost: null, officialCost: null, tokens: null, tokenBreakdown: null, }; } const tokenBreakdown = this.calculateTokenBreakdown(sessionUsage.entries); const totalTokens = tokenBreakdown.input + tokenBreakdown.output + tokenBreakdown.cacheCreation + tokenBreakdown.cacheRead; const calculatedCost = sessionUsage.totalCost; const hookDataCost = hookData?.cost?.total_cost_usd ?? null; const cost = calculatedCost ?? hookDataCost; return { cost, calculatedCost, officialCost: hookDataCost, tokens: totalTokens, tokenBreakdown, }; } } export class UsageProvider { private sessionProvider = new SessionProvider(); async getUsageInfo( sessionId: string, hookData?: ClaudeHookData, ): Promise<UsageInfo> { try { debug(`Starting usage info retrieval for session: ${sessionId}`); const sessionInfo = await this.sessionProvider.getSessionInfo( sessionId, hookData, ); return { session: sessionInfo, }; } catch (error) { debug(`Error getting usage info for session ${sessionId}:`, error); return { session: { cost: null, calculatedCost: null, officialCost: null, tokens: null, tokenBreakdown: null, }, }; } } }