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
JavaScript
#!/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