UNPKG

cc-status

Version:

Focused Claude Code statusline with real subscription usage, context monitoring, and time projections

1,536 lines (1,527 loc) 67.3 kB
#!/usr/bin/env node import { findTodaysTranscripts, findTranscriptsForDate } from "./chunk-6L6LGNWR.js"; // src/index.ts import process2 from "process"; import { json } from "stream/consumers"; // src/segments/git.ts import { execSync } from "child_process"; import fs from "fs"; import path from "path"; var GitService = class { isGitRepo(workingDir) { try { return fs.existsSync(path.join(workingDir, ".git")); } catch { return false; } } getGitInfo(workingDir, showSha = false) { if (!this.isGitRepo(workingDir)) { return { branch: "detached", status: "clean", ahead: 0, behind: 0, sha: void 0 }; } try { const branch = this.getBranch(workingDir); const status = this.getStatus(workingDir); const { ahead, behind } = this.getAheadBehind(workingDir); const sha = showSha ? this.getSha(workingDir) || void 0 : void 0; return { branch: branch || "detached", status, ahead, behind, sha }; } catch { return null; } } getBranch(workingDir) { try { return execSync("git branch --show-current", { cwd: workingDir, encoding: "utf8", timeout: 1e3 }).trim() || null; } catch { return null; } } getStatus(workingDir) { try { const gitStatus = execSync("git status --porcelain", { cwd: workingDir, encoding: "utf8", timeout: 1e3 }).trim(); if (!gitStatus) return "clean"; if (gitStatus.includes("UU") || gitStatus.includes("AA") || gitStatus.includes("DD")) { return "conflicts"; } return "dirty"; } catch { return "clean"; } } getAheadBehind(workingDir) { try { const aheadResult = execSync("git rev-list --count @{u}..HEAD", { cwd: workingDir, encoding: "utf8", timeout: 1e3 }).trim(); const behindResult = execSync("git rev-list --count HEAD..@{u}", { cwd: workingDir, encoding: "utf8", timeout: 1e3 }).trim(); return { ahead: parseInt(aheadResult) || 0, behind: parseInt(behindResult) || 0 }; } catch { return { ahead: 0, behind: 0 }; } } getSha(workingDir) { try { const sha = execSync("git rev-parse --short=7 HEAD", { cwd: workingDir, encoding: "utf8", timeout: 1e3 }).trim(); return sha || null; } catch { return null; } } }; // src/services/transcript-parser.ts import { readFile } from "fs/promises"; var TranscriptParser = class { /** * Parse a single transcript file and extract usage entries */ async parseTranscriptFile(filePath) { try { const content = await readFile(filePath, "utf-8"); const lines = content.trim().split("\n").filter((line) => line.trim()); if (lines.length === 0) { return []; } const entries = []; for (const line of lines) { try { if (!line.trim()) continue; const entry = JSON.parse(line); if (entry.message && typeof entry.message === "object") { const message = entry.message; if (message.usage && typeof message.usage === "object") { const transcriptEntry = { timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(), message: { usage: message.usage, model: message.model } }; if (entry.model && typeof entry.model === "string") { transcriptEntry.model = entry.model; } if (entry.model_id && typeof entry.model_id === "string") { transcriptEntry.model_id = entry.model_id; } if (typeof entry.costUSD === "number") { transcriptEntry.costUSD = entry.costUSD; } entries.push(transcriptEntry); } } } catch (parseError) { continue; } } return entries; } catch (error) { console.debug(`Error reading transcript file ${filePath}:`, error); return []; } } /** * Calculate token breakdown from transcript entries */ calculateTokenBreakdown(entries) { const breakdown = entries.reduce( (acc, entry) => ({ input: acc.input + (entry.message.usage.input_tokens || 0), output: acc.output + (entry.message.usage.output_tokens || 0), cacheCreation: acc.cacheCreation + (entry.message.usage.cache_creation_input_tokens || 0), cacheRead: acc.cacheRead + (entry.message.usage.cache_read_input_tokens || 0) }), { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 } ); return { ...breakdown, total: breakdown.input + breakdown.output + breakdown.cacheCreation + breakdown.cacheRead }; } /** * Get current active session usage by session ID (like ccusage active block) */ async getDailyUsage(sessionId) { try { let targetTranscriptPath = null; if (sessionId) { targetTranscriptPath = await import("./claude-paths-XGX2L56H.js").then((m) => m.findTranscriptFile(sessionId)); } if (!targetTranscriptPath) { const transcriptFiles = await findTodaysTranscripts(); if (transcriptFiles.length === 0) { return this.getEmptyUsage(); } const transcriptStats = await Promise.all( transcriptFiles.map(async (filePath) => { try { const stats = await import("fs/promises").then((fs5) => fs5.stat(filePath)); return { filePath, mtime: stats.mtime }; } catch { return null; } }) ); const validStats = transcriptStats.filter((stat) => stat !== null); if (validStats.length === 0) { return this.getEmptyUsage(); } validStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); targetTranscriptPath = validStats[0].filePath; } const entries = await this.parseTranscriptFile(targetTranscriptPath); const currentBlock = this.getCurrentActiveBlock(entries); if (!currentBlock) { return this.getEmptyUsage(); } const tokenBreakdown = this.calculateTokenBreakdown(currentBlock.entries); const totalCost = currentBlock.entries.reduce((sum, entry) => { return sum + (entry.costUSD || 0); }, 0); return { totalTokens: tokenBreakdown.total, totalCost, tokenBreakdown, entries: currentBlock.entries, sessionCount: 1 // Current active block }; } catch (error) { console.debug("Error getting daily usage:", error); return this.getEmptyUsage(); } } /** * Get the current active 5-hour block from session entries (like ccusage) */ getCurrentActiveBlock(entries) { if (entries.length === 0) return null; const sortedEntries = entries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const now = /* @__PURE__ */ new Date(); const FIVE_HOURS_MS = 5 * 60 * 60 * 1e3; const latestEntry = sortedEntries[sortedEntries.length - 1]; const latestTime = new Date(latestEntry.timestamp); let currentBlockStart; if (now.getTime() - latestTime.getTime() < FIVE_HOURS_MS) { currentBlockStart = new Date(latestTime); for (let i = sortedEntries.length - 2; i >= 0; i--) { const entryTime = new Date(sortedEntries[i].timestamp); const timeDiff = latestTime.getTime() - entryTime.getTime(); if (timeDiff > FIVE_HOURS_MS) { break; } currentBlockStart = entryTime; } } else { return { entries: [] }; } const currentBlockEntries = sortedEntries.filter((entry) => { const entryTime = new Date(entry.timestamp); const timeDiff = latestTime.getTime() - entryTime.getTime(); return timeDiff <= FIVE_HOURS_MS && entryTime >= currentBlockStart; }); return { entries: currentBlockEntries }; } getEmptyUsage() { return { totalTokens: 0, totalCost: 0, tokenBreakdown: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0 }, entries: [], sessionCount: 0 }; } /** * Get usage data for ALL sessions today (for daily cost calculation) */ async getAllSessionsForToday() { try { const transcriptFiles = await findTodaysTranscripts(); const allEntries = []; for (const filePath of transcriptFiles) { const entries = await this.parseTranscriptFile(filePath); allEntries.push(...entries); } const tokenBreakdown = this.calculateTokenBreakdown(allEntries); const totalCost = allEntries.reduce((sum, entry) => { return sum + (entry.costUSD || 0); }, 0); return { totalTokens: tokenBreakdown.total, totalCost, tokenBreakdown, entries: allEntries, sessionCount: transcriptFiles.length }; } catch (error) { console.debug("Error getting all sessions for today:", error); return this.getEmptyUsage(); } } /** * Get usage data for a specific date */ async getUsageForDate(date) { try { const transcriptFiles = await findTranscriptsForDate(date); const allEntries = []; for (const filePath of transcriptFiles) { const entries = await this.parseTranscriptFile(filePath); allEntries.push(...entries); } const tokenBreakdown = this.calculateTokenBreakdown(allEntries); const totalCost = allEntries.reduce((sum, entry) => { return sum + (entry.costUSD || 0); }, 0); return { totalTokens: tokenBreakdown.total, totalCost, tokenBreakdown, entries: allEntries, sessionCount: transcriptFiles.length }; } catch (error) { console.debug(`Error getting usage for date ${date.toISOString()}:`, error); return { totalTokens: 0, totalCost: 0, tokenBreakdown: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0 }, entries: [], sessionCount: 0 }; } } /** * Get historical usage data for limit estimation */ async getHistoricalUsage(days = 30) { const usage = []; const today = /* @__PURE__ */ new Date(); for (let i = 0; i < days; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const dayUsage = await this.getUsageForDate(date); if (dayUsage.totalTokens > 0) { usage.push(dayUsage); } } return usage; } /** * Extract model ID from transcript entry using multiple fallback strategies */ extractModelId(entry) { if (entry.model && typeof entry.model === "string") { return entry.model; } if (entry.model_id && typeof entry.model_id === "string") { return entry.model_id; } const message = entry.message; if (message?.model) { if (typeof message.model === "string") { return message.model; } if (typeof message.model === "object" && message.model.id) { return message.model.id; } } return "claude-3-5-sonnet-20241022"; } }; // src/services/limit-detection.ts var LimitDetectionService = class { transcriptParser = new TranscriptParser(); cachedLimit = null; CACHE_TTL = 4 * 60 * 60 * 1e3; // 4 hours /** * Get the estimated daily token limit (actually per-block limit like ccusage) */ async getDailyTokenLimit() { if (this.cachedLimit && Date.now() - this.cachedLimit.lastUpdated.getTime() < this.CACHE_TTL) { return this.cachedLimit; } try { const historicalLimit = await this.detectFromHistoricalUsage(); if (historicalLimit.confidence !== "fallback") { this.cachedLimit = historicalLimit; return historicalLimit; } const fallbackLimit = this.getConservativeFallback(); this.cachedLimit = fallbackLimit; return fallbackLimit; } catch (error) { console.debug("Error detecting daily limit:", error); const fallbackLimit = this.getConservativeFallback(); this.cachedLimit = fallbackLimit; return fallbackLimit; } } /** * Analyze historical usage to estimate 5-hour block limits (like ccusage) * Uses statistical approach rather than single max values */ async detectFromHistoricalUsage() { try { const historicalBlocks = await this.getHistoricalBlockUsage(30); if (historicalBlocks.length === 0) { return this.getConservativeFallback(); } const tokenCounts = historicalBlocks.map((block) => block.totalTokens).sort((a, b) => b - a); const percentile95Index = Math.floor(tokenCounts.length * 0.05); const percentile95 = tokenCounts[percentile95Index] || 0; const percentile99Index = Math.floor(tokenCounts.length * 0.01); const percentile99 = tokenCounts[percentile99Index] || 0; const potentialLimits = this.findLimitClusters(tokenCounts); let estimatedLimit; let confidence; if (potentialLimits.length > 0 && percentile99 > 3e7) { estimatedLimit = Math.max(...potentialLimits); confidence = "high"; } else if (percentile95 > 4e7) { estimatedLimit = percentile99; confidence = "medium"; } else if (percentile95 > 2e7) { estimatedLimit = Math.max(percentile99, 456e5); confidence = "low"; } else { estimatedLimit = 456e5; confidence = "low"; } return { dailyTokenLimit: Math.round(estimatedLimit), confidence, source: "historical_analysis", lastUpdated: /* @__PURE__ */ new Date() }; } catch (error) { console.debug("Error in historical analysis:", error); return this.getConservativeFallback(); } } /** * Group transcript entries into 5-hour blocks like ccusage */ async getHistoricalBlockUsage(days) { const blocks = []; const claudePaths = await import("./claude-paths-XGX2L56H.js").then((m) => m.getClaudePaths()); const projectPaths = await import("./claude-paths-XGX2L56H.js").then((m) => m.findProjectPaths(claudePaths)); const today = /* @__PURE__ */ new Date(); for (const projectPath of projectPaths) { try { const entries = await import("fs/promises").then((fs5) => fs5.readdir(projectPath, { withFileTypes: true })); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith(".jsonl")) { const transcriptPath = await import("path").then((path3) => path3.join(projectPath, entry.name)); try { const stats = await import("fs/promises").then((fs5) => fs5.stat(transcriptPath)); const fileDate = new Date(stats.mtime); const daysDiff = (today.getTime() - fileDate.getTime()) / (1e3 * 60 * 60 * 24); if (daysDiff <= days) { const sessionUsage = await this.transcriptParser.parseTranscriptFile(transcriptPath); const sessionBlocks = this.groupInto5HourBlocks(sessionUsage); for (const block of sessionBlocks) { if (block.totalTokens > 0) { blocks.push({ totalTokens: block.totalTokens, date: block.startTime }); } } } } catch (statError) { continue; } } } } catch (error) { continue; } } return blocks; } /** * Group transcript entries into 5-hour blocks (like ccusage session blocks) */ groupInto5HourBlocks(entries) { if (entries.length === 0) return []; const sortedEntries = entries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const blocks = []; const FIVE_HOURS_MS = 5 * 60 * 60 * 1e3; let currentBlockStart = new Date(sortedEntries[0].timestamp); let currentBlockTokens = 0; for (const entry of sortedEntries) { const entryTime = new Date(entry.timestamp); if (entryTime.getTime() - currentBlockStart.getTime() > FIVE_HOURS_MS) { if (currentBlockTokens > 0) { blocks.push({ totalTokens: currentBlockTokens, startTime: new Date(currentBlockStart) }); } currentBlockStart = new Date(entryTime); currentBlockTokens = 0; } if (entry.message?.usage) { const usage = entry.message.usage; currentBlockTokens += (usage.input_tokens || 0) + (usage.output_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0); } } if (currentBlockTokens > 0) { blocks.push({ totalTokens: currentBlockTokens, startTime: new Date(currentBlockStart) }); } return blocks; } /** * Find clusters of similar token values that might indicate limits */ findLimitClusters(tokenCounts) { if (tokenCounts.length < 5) return []; const clusters = []; const CLUSTER_THRESHOLD = 0.02; const groups = []; for (const count of tokenCounts) { let addedToGroup = false; for (const group of groups) { const groupAvg = group.reduce((sum, val) => sum + val, 0) / group.length; const variation = Math.abs(count - groupAvg) / groupAvg; if (variation <= CLUSTER_THRESHOLD) { group.push(count); addedToGroup = true; break; } } if (!addedToGroup) { groups.push([count]); } } for (const group of groups) { if (group.length >= 3) { const avgValue = group.reduce((sum, val) => sum + val, 0) / group.length; if (avgValue > 3e7) { clusters.push(Math.round(avgValue)); } } } return clusters.sort((a, b) => b - a); } /** * Get all individual session usage data for analysis (legacy method) */ async getAllHistoricalSessions(days) { const sessions = []; const claudePaths = await import("./claude-paths-XGX2L56H.js").then((m) => m.getClaudePaths()); const projectPaths = await import("./claude-paths-XGX2L56H.js").then((m) => m.findProjectPaths(claudePaths)); const today = /* @__PURE__ */ new Date(); for (const projectPath of projectPaths) { try { const entries = await import("fs/promises").then((fs5) => fs5.readdir(projectPath, { withFileTypes: true })); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith(".jsonl")) { const transcriptPath = await import("path").then((path3) => path3.join(projectPath, entry.name)); try { const stats = await import("fs/promises").then((fs5) => fs5.stat(transcriptPath)); const fileDate = new Date(stats.mtime); const daysDiff = (today.getTime() - fileDate.getTime()) / (1e3 * 60 * 60 * 24); if (daysDiff <= days) { const sessionUsage = await this.transcriptParser.parseTranscriptFile(transcriptPath); const tokenBreakdown = this.transcriptParser.calculateTokenBreakdown(sessionUsage); if (tokenBreakdown.total > 0) { sessions.push({ totalTokens: tokenBreakdown.total, date: fileDate }); } } } catch (statError) { continue; } } } } catch (error) { continue; } } return sessions; } /** * Look for usage patterns that suggest hitting daily limits */ findPotentialLimits(historicalUsage) { const potentialLimits = []; for (const dayUsage of historicalUsage) { const tokens = dayUsage.totalTokens; if (this.isLikelyLimitValue(tokens)) { potentialLimits.push(tokens); } } const sortedUsage = historicalUsage.map((day) => day.totalTokens).sort((a, b) => b - a); const top10Percent = sortedUsage.slice(0, Math.ceil(sortedUsage.length * 0.1)); if (top10Percent.length >= 2) { const avgTop = top10Percent.reduce((sum, val) => sum + val, 0) / top10Percent.length; const stdDev = Math.sqrt( top10Percent.reduce((sum, val) => sum + Math.pow(val - avgTop, 2), 0) / top10Percent.length ); if (stdDev < avgTop * 0.05) { potentialLimits.push(Math.round(avgTop)); } } return potentialLimits; } /** * Check if a token count looks like it might be a limit value */ isLikelyLimitValue(tokens) { if (tokens % 1e6 === 0) return true; const millions = tokens / 1e6; if (Math.abs(millions - Math.round(millions * 10) / 10) < 0.01) return true; const knownPatterns = [ 456e5, // Observed in current cc-status 5e7, // Round 50M 4e7, // Round 40M 3e7 // Round 30M ]; return knownPatterns.some((pattern) => Math.abs(tokens - pattern) < 1e5); } /** * Get conservative fallback limit when detection fails */ getConservativeFallback() { return { dailyTokenLimit: 456e5, // Use the 45.6M observed in ccusage data confidence: "fallback", source: "fallback", lastUpdated: /* @__PURE__ */ new Date() }; } /** * Force refresh the cached limit on next call */ invalidateCache() { this.cachedLimit = null; } /** * Manually set a daily limit (for configuration override) */ setManualLimit(tokens) { const manualLimit = { dailyTokenLimit: tokens, confidence: "high", source: "configuration", lastUpdated: /* @__PURE__ */ new Date() }; this.cachedLimit = manualLimit; return manualLimit; } /** * Get debug information about limit detection */ async getDebugInfo() { const currentLimit = await this.getDailyTokenLimit(); const historicalUsage = await this.transcriptParser.getHistoricalUsage(30); const maxObservedUsage = historicalUsage.length > 0 ? Math.max(...historicalUsage.map((day) => day.totalTokens)) : 0; const potentialLimits = this.findPotentialLimits(historicalUsage); return { currentLimit, historicalUsage, maxObservedUsage, potentialLimits }; } }; // src/services/reset-time-detection.ts var ResetTimeDetectionService = class { transcriptParser = new TranscriptParser(); cachedResetTime = null; CACHE_TTL = 30 * 60 * 1e3; // 30 minutes /** * Get the estimated reset time for the current usage period */ async getResetTime() { if (this.cachedResetTime && Date.now() - this.cachedResetTime.resetTime.getTime() > -this.CACHE_TTL) { return this.cachedResetTime; } try { const resetTime = await this.detectResetTime(); this.cachedResetTime = resetTime; return resetTime; } catch (error) { console.debug("Error detecting reset time:", error); const fallbackTime = this.getFallbackResetTime(); this.cachedResetTime = fallbackTime; return fallbackTime; } } /** * Try multiple strategies to detect when usage resets */ async detectResetTime() { const blockBasedTime = await this.detectFromBlockPattern(); if (blockBasedTime.confidence !== "fallback") { return blockBasedTime; } const firstUsageTime = await this.detectFromFirstUsageToday(); if (firstUsageTime.confidence !== "fallback") { return firstUsageTime; } return this.getFallbackResetTime(); } /** * Detect reset time from transcript data (like ccusage usageLimitResetTime) */ async detectFromBlockPattern() { try { const todaysTranscripts = await import("./claude-paths-XGX2L56H.js").then((m) => m.findTodaysTranscripts()); if (todaysTranscripts.length === 0) { return this.getFallbackResetTime(); } let usageLimitResetTime = null; for (const transcriptPath of todaysTranscripts) { const entries = await this.transcriptParser.parseTranscriptFile(transcriptPath); for (const entry of entries) { if (entry.usageLimitResetTime) { usageLimitResetTime = new Date(entry.usageLimitResetTime); break; } } if (usageLimitResetTime) break; } if (usageLimitResetTime) { const now2 = /* @__PURE__ */ new Date(); const timeRemaining2 = usageLimitResetTime.getTime() - now2.getTime(); return { resetTime: usageLimitResetTime, confidence: "high", source: "usage_pattern", timeRemaining: Math.max(0, timeRemaining2) }; } const now = /* @__PURE__ */ new Date(); let resetTime = new Date(now); resetTime.setHours(2, 0, 0, 0); if (resetTime.getTime() <= now.getTime()) { resetTime.setDate(resetTime.getDate() + 1); } const timeRemaining = resetTime.getTime() - now.getTime(); return { resetTime, confidence: "low", source: "usage_pattern", timeRemaining: Math.max(0, timeRemaining) }; } catch (error) { console.debug("Error in block pattern detection:", error); return this.getFallbackResetTime(); } } /** * Analyze usage patterns to find reset time */ async detectFromUsagePatterns() { try { const historicalUsage = await this.transcriptParser.getHistoricalUsage(7); if (historicalUsage.length < 2) { return this.getFallbackResetTime(); } const startTimes = []; for (const dayUsage of historicalUsage) { if (dayUsage.entries.length > 0) { const dayEntries = dayUsage.entries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); startTimes.push(new Date(dayEntries[0].timestamp)); } } if (startTimes.length < 2) { return this.getFallbackResetTime(); } const resetTimeEstimate = this.analyzeStartTimePatterns(startTimes); if (resetTimeEstimate) { const now = /* @__PURE__ */ new Date(); const timeRemaining = resetTimeEstimate.getTime() - now.getTime(); return { resetTime: resetTimeEstimate, confidence: "medium", source: "usage_pattern", timeRemaining: Math.max(0, timeRemaining) }; } return this.getFallbackResetTime(); } catch (error) { console.debug("Error in pattern analysis:", error); return this.getFallbackResetTime(); } } /** * Analyze start time patterns to predict next reset */ analyzeStartTimePatterns(startTimes) { if (startTimes.length < 2) return null; const intervals = []; for (let i = 1; i < startTimes.length; i++) { const interval = startTimes[i].getTime() - startTimes[i - 1].getTime(); if (interval >= 20 * 60 * 60 * 1e3 && interval <= 28 * 60 * 60 * 1e3) { intervals.push(interval); } } if (intervals.length === 0) return null; const avgInterval = intervals.reduce((sum, val) => sum + val, 0) / intervals.length; const variance = intervals.reduce((sum, val) => sum + Math.pow(val - avgInterval, 2), 0) / intervals.length; const stdDev = Math.sqrt(variance); if (stdDev > 2 * 60 * 60 * 1e3) { return null; } const lastStart = startTimes[startTimes.length - 1]; const nextReset = new Date(lastStart.getTime() + avgInterval); if (nextReset.getTime() <= Date.now()) { return new Date(nextReset.getTime() + 24 * 60 * 60 * 1e3); } return nextReset; } /** * Estimate reset time from first usage today */ async detectFromFirstUsageToday() { try { const todaysTranscripts = await findTodaysTranscripts(); if (todaysTranscripts.length === 0) { return this.getFallbackResetTime(); } let earliestTimestamp = null; for (const transcriptPath of todaysTranscripts) { const entries = await this.transcriptParser.parseTranscriptFile(transcriptPath); for (const entry of entries) { const timestamp = new Date(entry.timestamp); if (!earliestTimestamp || timestamp < earliestTimestamp) { earliestTimestamp = timestamp; } } } if (!earliestTimestamp) { return this.getFallbackResetTime(); } const resetTime = new Date(earliestTimestamp.getTime() + 24 * 60 * 60 * 1e3); const now = /* @__PURE__ */ new Date(); if (resetTime.getTime() <= now.getTime()) { resetTime.setTime(resetTime.getTime() + 24 * 60 * 60 * 1e3); } const timeRemaining = resetTime.getTime() - now.getTime(); return { resetTime, confidence: "low", source: "first_usage_extrapolation", timeRemaining: Math.max(0, timeRemaining) }; } catch (error) { console.debug("Error detecting from first usage:", error); return this.getFallbackResetTime(); } } /** * Get fallback reset time (next midnight local time) */ getFallbackResetTime() { const now = /* @__PURE__ */ new Date(); const nextMidnight = new Date(now); nextMidnight.setDate(nextMidnight.getDate() + 1); nextMidnight.setHours(0, 0, 0, 0); const timeRemaining = nextMidnight.getTime() - now.getTime(); return { resetTime: nextMidnight, confidence: "fallback", source: "fallback", timeRemaining: Math.max(0, timeRemaining) }; } /** * Format time remaining in compact format like ccusage */ formatTimeRemaining(timeRemaining) { const remainingSeconds = Math.max(0, Math.floor(timeRemaining / 1e3)); const hours = Math.floor(remainingSeconds / 3600); const minutes = Math.floor(remainingSeconds % 3600 / 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, "0")}`; } else if (minutes > 0) { return `${minutes}m`; } else { return `${remainingSeconds}s`; } } /** * Format reset time in compact format like ccusage */ formatResetTime(resetTime) { let formatted = resetTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: true }); formatted = formatted.replace(/:\d{2}(:\d{2})?\s*(AM|PM)/, "$2"); formatted = formatted.replace(/^0/, ""); return formatted; } /** * Check if we're near the reset time (less than 30 minutes) */ isNearReset(timeRemaining) { return timeRemaining < 30 * 60 * 1e3; } /** * Force refresh the cached reset time on next call */ invalidateCache() { this.cachedResetTime = null; } /** * Manually set a reset time (for configuration override) */ setManualResetTime(resetTime) { const now = /* @__PURE__ */ new Date(); const timeRemaining = resetTime.getTime() - now.getTime(); const manualResetTime = { resetTime, confidence: "high", source: "configuration", timeRemaining: Math.max(0, timeRemaining) }; this.cachedResetTime = manualResetTime; return manualResetTime; } /** * Get debug information about reset time detection */ async getDebugInfo() { const currentResetTime = await this.getResetTime(); const todaysTranscripts = await findTodaysTranscripts(); let todaysFirstUsage = null; for (const transcriptPath of todaysTranscripts) { const entries = await this.transcriptParser.parseTranscriptFile(transcriptPath); for (const entry of entries) { const timestamp = new Date(entry.timestamp); if (!todaysFirstUsage || timestamp < todaysFirstUsage) { todaysFirstUsage = timestamp; } } } const historicalUsage = await this.transcriptParser.getHistoricalUsage(7); const historicalStartTimes = []; for (const dayUsage of historicalUsage) { if (dayUsage.entries.length > 0) { const dayEntries = dayUsage.entries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); historicalStartTimes.push(new Date(dayEntries[0].timestamp)); } } return { currentResetTime, todaysFirstUsage, historicalStartTimes }; } }; // src/services/pricing.ts import { homedir } from "os"; import { join } from "path"; import { readFileSync, writeFileSync, mkdirSync } from "fs"; var OFFLINE_PRICING_DATA = { "claude-3-haiku-20240307": { name: "Claude 3 Haiku", input: 0.25, output: 1.25, cache_write_5m: 0.3, cache_write_1h: 0.5, cache_read: 0.03 }, "claude-3-5-haiku-20241022": { name: "Claude 3.5 Haiku", input: 0.8, output: 4, cache_write_5m: 1, cache_write_1h: 1.6, cache_read: 0.08 }, "claude-3-5-haiku-latest": { name: "Claude 3.5 Haiku Latest", input: 1, output: 5, cache_write_5m: 1.25, cache_write_1h: 2, cache_read: 0.1 }, "claude-3-opus-latest": { name: "Claude 3 Opus Latest", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-3-opus-20240229": { name: "Claude 3 Opus", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-3-5-sonnet-latest": { name: "Claude 3.5 Sonnet Latest", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-5-sonnet-20241022": { name: "Claude 3.5 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-opus-4-20250514": { name: "Claude Opus 4", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-opus-4-1": { name: "Claude Opus 4.1", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-opus-4-1-20250805": { name: "Claude Opus 4.1", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-sonnet-4-20250514": { name: "Claude Sonnet 4", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-4-opus-20250514": { name: "Claude 4 Opus", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-4-sonnet-20250514": { name: "Claude 4 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-7-sonnet-latest": { name: "Claude 3.7 Sonnet Latest", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-7-sonnet-20250219": { name: "Claude 3.7 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 } }; var PricingService = class { static memoryCache = /* @__PURE__ */ new Map(); static CACHE_TTL = 24 * 60 * 60 * 1e3; // 24 hours static GITHUB_PRICING_URL = "https://raw.githubusercontent.com/Owloops/claude-powerline/main/pricing.json"; static getCacheFilePath() { const cacheDir = join(homedir(), ".claude", "cache"); try { mkdirSync(cacheDir, { recursive: true }); } catch { } return join(cacheDir, "pricing.json"); } static loadDiskCache() { try { const cacheFile = this.getCacheFilePath(); const content = readFileSync(cacheFile, "utf-8"); const cached = JSON.parse(content); if (cached && cached.data && cached.timestamp) { return cached; } } catch { } return null; } static saveDiskCache(data) { try { const cacheFile = this.getCacheFilePath(); const cacheData = { data, timestamp: Date.now() }; writeFileSync(cacheFile, JSON.stringify(cacheData)); } catch (error) { console.debug("Failed to save pricing cache to disk:", error); } } static async getCurrentPricing() { const now = Date.now(); const memCached = this.memoryCache.get("pricing"); if (memCached && now - memCached.timestamp < this.CACHE_TTL) { return memCached.data; } const diskCached = this.loadDiskCache(); if (diskCached && now - diskCached.timestamp < this.CACHE_TTL) { this.memoryCache.set("pricing", diskCached); return diskCached.data; } try { const response = await globalThis.fetch(this.GITHUB_PRICING_URL, { headers: { "User-Agent": "cc-status", "Cache-Control": "no-cache" } }); if (response.ok) { const data = await response.json(); const dataObj = data; const pricingData = {}; for (const [key, value] of Object.entries(dataObj)) { if (key !== "_meta") { pricingData[key] = value; } } if (this.validatePricingData(pricingData)) { this.memoryCache.set("pricing", { data: pricingData, timestamp: now }); this.saveDiskCache(pricingData); return pricingData; } } } catch (error) { console.debug("Failed to fetch pricing from GitHub, using fallback:", error); } if (diskCached) { this.memoryCache.set("pricing", diskCached); return diskCached.data; } return OFFLINE_PRICING_DATA; } static validatePricingData(data) { if (!data || typeof data !== "object") return false; for (const [, value] of Object.entries(data)) { if (!value || typeof value !== "object") return false; const pricing = value; if (typeof pricing.input !== "number" || typeof pricing.output !== "number" || typeof pricing.cache_read !== "number") { return false; } } return true; } static async getModelPricing(modelId) { const allPricing = await this.getCurrentPricing(); if (allPricing[modelId]) { return allPricing[modelId]; } return this.fuzzyMatchModel(modelId, allPricing); } static fuzzyMatchModel(modelId, allPricing) { const lowerModelId = modelId.toLowerCase(); for (const [key, pricing] of Object.entries(allPricing)) { if (key.toLowerCase() === lowerModelId) { return pricing; } } const patterns = [ { pattern: ["opus-4-1", "claude-opus-4-1"], fallback: "claude-opus-4-1-20250805" }, { pattern: ["opus-4", "claude-opus-4"], fallback: "claude-opus-4-20250514" }, { pattern: ["sonnet-4", "claude-sonnet-4"], fallback: "claude-sonnet-4-20250514" }, { pattern: ["sonnet-3.7", "3-7-sonnet"], fallback: "claude-3-7-sonnet-20250219" }, { pattern: ["3-5-sonnet", "sonnet-3.5"], fallback: "claude-3-5-sonnet-20241022" }, { pattern: ["3-5-haiku", "haiku-3.5"], fallback: "claude-3-5-haiku-20241022" }, { pattern: ["haiku", "3-haiku"], fallback: "claude-3-haiku-20240307" }, { pattern: ["opus"], fallback: "claude-opus-4-20250514" }, { pattern: ["sonnet"], fallback: "claude-3-5-sonnet-20241022" } ]; for (const { pattern, fallback } of patterns) { if (pattern.some((p) => lowerModelId.includes(p))) { if (allPricing[fallback]) { return allPricing[fallback]; } } } return allPricing["claude-3-5-sonnet-20241022"] || { name: `${modelId} (Unknown Model)`, input: 3, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3, output: 15 }; } /** * Calculate cost for a transcript entry */ static async calculateCostForEntry(entry) { const usage = entry.message?.usage; if (!usage) return 0; if (typeof entry.costUSD === "number") { return entry.costUSD; } const modelId = this.extractModelId(entry); const pricing = await this.getModelPricing(modelId); 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; const inputCost = inputTokens / 1e6 * pricing.input; const outputCost = outputTokens / 1e6 * pricing.output; const cacheReadCost = cacheReadTokens / 1e6 * pricing.cache_read; const cacheCreationCost = cacheCreationTokens / 1e6 * pricing.cache_write_5m; return inputCost + outputCost + cacheCreationCost + cacheReadCost; } static extractModelId(entry) { if (entry.model && typeof entry.model === "string") { return entry.model; } if (entry.model_id && typeof entry.model_id === "string") { return entry.model_id; } const message = entry.message; if (message?.model) { if (typeof message.model === "string") { return message.model; } if (typeof message.model === "object" && message.model.id) { return message.model.id; } } return "claude-3-5-sonnet-20241022"; } }; // src/segments/subscription.ts var SubscriptionService = class { transcriptParser = new TranscriptParser(); limitDetection = new LimitDetectionService(); resetTimeDetection = new ResetTimeDetectionService(); async getSubscriptionInfo(sessionId) { try { const dailyUsage = await this.transcriptParser.getDailyUsage(sessionId); const limitInfo = await this.limitDetection.getDailyTokenLimit(); const tokensUsed = dailyUsage.totalTokens; const tokensLimit = limitInfo.dailyTokenLimit; const percentage = tokensLimit > 0 ? tokensUsed / tokensLimit * 100 : 0; const isOverLimit = percentage > 100; const projection = await this.calculateProjection(dailyUsage, tokensLimit); return { percentage: Math.round(percentage * 10) / 10, // Round to 1 decimal tokensUsed, tokensLimit, isOverLimit, projection }; } catch (error) { console.debug("Error getting subscription info:", error); return this.getFallbackData(); } } async calculateProjection(dailyUsage, tokenLimit) { try { const resetInfo = await this.resetTimeDetection.getResetTime(); const remainingMinutes = Math.max(0, Math.floor(resetInfo.timeRemaining / (60 * 1e3))); let totalCost = dailyUsage.totalCost; for (const entry of dailyUsage.entries) { if (typeof entry.costUSD !== "number") { const calculatedCost = await PricingService.calculateCostForEntry(entry); totalCost += calculatedCost; } } return { totalTokens: dailyUsage.totalTokens, totalCost, remainingMinutes }; } catch (error) { console.debug("Error calculating projection:", error); return null; } } getFallbackData() { return { percentage: 48.6, tokensUsed: 9404300, // 9404.3k in raw tokens tokensLimit: 19342800, // 19342.8k in raw tokens isOverLimit: false, projection: null }; } formatTokens(tokens) { if (tokens >= 1e6) { return `${(tokens / 1e6).toFixed(1)}M`; } else if (tokens >= 1e3) { return `${(tokens / 1e3).toFixed(1)}k`; } return tokens.toString(); } }; // src/segments/context.ts import fs2 from "fs"; var ContextService = class { MAX_CONTEXT_TOKENS = 2e5; // Default Claude context limit async getContextInfo(transcriptPath) { if (!transcriptPath || !fs2.existsSync(transcriptPath)) { return null; } try { const contextTokens = this.calculateContextTokens(transcriptPath); if (contextTokens === null) { return null; } const percentage = Math.min(100, Math.max(0, Math.round(contextTokens / this.MAX_CONTEXT_TOKENS * 100))); const isNearLimit = percentage > 80; return { percentage, isNearLimit }; } catch (error) { return null; } } calculateContextTokens(transcriptPath) { try { const content = fs2.readFileSync(transcriptPath, "utf-8"); if (!content) { return null; } const lines = content.trim().split("\n"); if (lines.length === 0) { return null; } let mostRecentEntry = null; let mostRecentTime = 0; for (const line of lines) { if (!line.trim()) continue; try { const entry = JSON.parse(line); if (!entry.message?.usage?.input_tokens) continue; if (entry.isSidechain === true) continue; if (!entry.timestamp) continue; const entryTime = new Date(entry.timestamp).getTime(); if (entryTime > mostRecentTime) { mostRecentTime = entryTime; mostRecentEntry = entry; } } catch { continue; } } if (mostRecentEntry?.message?.usage) { const usage = mostRecentEntry.message.usage; const contextLength = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0); return contextLength; } return null; } catch (error) { return null; } } }; // src/segments/burn-rate.ts import fs3 from "fs"; var BurnRateService = class { BURN_RATE_THRESHOLDS = { HIGH: 1e3, MODERATE: 500 }; // Claude's 5-hour session duration in milliseconds SESSION_DURATION_MS = 5 * 60 * 60 * 1e3; transcriptParser = new TranscriptParser(); async getBurnRateInfo(transcriptPath) { if (!transcriptPath || !fs3.existsSync(transcriptPath)) { return null; } try { const allEntries = await this.transcriptParser.parseTranscriptFile(transcriptPath); const currentBlock = this.getCurrentActiveBlockEntries(allEntries); if (currentBlock.length < 2) { return null; } return this.calculateBurnRate(currentBlock); } catch (error) { return null; } } /** * Get current active 5-hour block entries (matches transcript parser logic) */ getCurrentActiveBlockEntries(entries) { if (entries.length === 0) return []; const validEntries = []; for (const entry of entries) { if (entry.isSidechain === true || !entry.message?.usage || !entry.timestamp) { continue; } const usage = entry.message.usage; const hasTokens = (usage.input_tokens || 0) > 0 || (usage.output_tokens || 0) > 0 || (usage.cache_creation_input_tokens || 0) > 0 || (usage.cache_read_input_tokens || 0) > 0; if (hasTokens) { validEntries.push(entry); } } if (validEntries.length === 0) return []; const sortedEntries = validEntries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const now = /* @__PURE__ */ new Date(); const FIVE_HOURS_MS = 5 * 60 * 60 * 1e3; const latestEntry = sortedEntries[sortedEntries.length - 1]; const latestTime = new Date(latestEntry.timestamp); if (now.getTime() - latestTime.getTime() > FIVE_HOURS_MS) { return []; } let currentBlockStart = new Date(latestTime); for (let i = sortedEntries.length - 2; i >= 0; i--) { const entryTime = new Date(sortedEntries[i].timestamp); const timeDiff = latestTime.getTime() - entryTime.getTime(); if (timeDiff > FIVE_HOURS_MS) { break; } currentBlockStart = entryTime; } return sortedEntries.filter((entry) => { const entryTime = new Date(entry.timestamp); const timeDiff = latestTime.getTime() - entryTime.getTime(); return timeDiff <= FIVE_HOURS_MS && entryTime >= currentBlockStart; }); } parseTranscriptEntries(transcriptPath) { const content = fs3.readFileSync(transcriptPath, "utf-8"); if (!content) { return []; } const lines = content.trim().split("\n"); const entries = []; for (const line of lines) { if (!line.trim()) continue; try { const entry = JSON.parse(line); if (entry.isSidechain === true || !entry.message?.usage || !entry.timestamp) { continue; } const usage = entry.message.usage; const hasTokens = (usage.input_tokens || 0) > 0 || (usage.output_tokens || 0) > 0 || (usage.cache_creation_input_tokens || 0) > 0 || (usage.cache_read_input_tokens || 0) > 0; if (hasTokens) { entries.push(entry); } } catch { continue; } } return entries.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); } calculateBurnRate(entries) { if (entries.length < 2) { return null; } const firstEntry = entries[0]; const lastEntry = entries[entries.length - 1]; if (!firstEntry?.timestamp || !lastEntry?.timestamp) { return null; } const firstTime = new Date(firstEntry.timestamp); const lastTime = new Date(lastEntry.timestamp); const durationMinutes = (lastTime.getTime() - firstTime.getTime()) / (1e3 * 60); if (durationMinutes <= 0) { return null; } let totalInputTokens = 0; let totalOutputTokens = 0; let totalCacheCreationTokens = 0; let totalCacheReadTokens = 0; let totalCost = 0; for (const entry of entries) { const usage = entry.message?.usage; if (usage) { totalInputTokens += usage.input_tokens || 0; totalOutputTokens += usage.output_tokens || 0; totalCacheCreationTokens += usage.cache_creation_input_tokens || 0; totalCacheReadTokens += usage.cache_read_input_tokens || 0; } totalCost += entry