cc-status
Version:
Focused Claude Code statusline with real subscription usage, context monitoring, and time projections
1,448 lines (1,439 loc) • 71.9 kB
JavaScript
#!/usr/bin/env node
import {
findTodaysTranscripts,
findTranscriptsForDate
} from "./chunk-JNEROV6L.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");
if (!content || !content.trim()) {
return [];
}
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 trimmedLine = line.trim();
if (!trimmedLine.startsWith("{") || !trimmedLine.endsWith("}")) {
continue;
}
const entry = JSON.parse(trimmedLine);
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;
}
if (entry.usageLimitResetTime && typeof entry.usageLimitResetTime === "string") {
transcriptEntry.usageLimitResetTime = entry.usageLimitResetTime;
}
entries.push(transcriptEntry);
}
}
} catch (parseError) {
continue;
}
}
return entries;
} catch (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-75KD26Y3.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) {
return this.getEmptyUsage();
}
}
/**
* Get the current active block from session entries (using ccusage logic with usageLimitResetTime)
*/
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();
let resetTime = null;
for (const entry of sortedEntries) {
if (entry.usageLimitResetTime) {
resetTime = new Date(entry.usageLimitResetTime);
break;
}
}
if (resetTime && now < resetTime) {
const blockStartTime = new Date(resetTime.getTime() - 5 * 60 * 60 * 1e3);
const currentBlockEntries2 = sortedEntries.filter((entry) => {
const entryTime = new Date(entry.timestamp);
return entryTime.getTime() >= blockStartTime.getTime();
});
return { entries: currentBlockEntries2 };
}
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 { entries: [] };
}
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;
}
const currentBlockEntries = sortedEntries.filter((entry) => {
const entryTime = new Date(entry.timestamp);
return entryTime.getTime() >= currentBlockStart.getTime();
});
return { entries: currentBlockEntries };
}
getEmptyUsage() {
return {
totalTokens: 0,
totalCost: 0,
tokenBreakdown: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0 },
entries: [],
sessionCount: 0
};
}
/**
* Get current 5-hour block usage across ALL sessions (for subscription tracking)
* Uses same block identification algorithm as session timer for consistency
*/
async getCurrentBlockUsageAcrossAllSessions() {
try {
const transcriptFiles = await findTodaysTranscripts();
const allEntries = [];
for (const filePath of transcriptFiles) {
const entries = await this.parseTranscriptFile(filePath);
allEntries.push(...entries);
}
if (allEntries.length === 0) {
return this.getEmptyUsage();
}
const sessionBlocks = this.identifySessionBlocks(allEntries);
const now = /* @__PURE__ */ new Date();
const sessionDurationMs = 5 * 60 * 60 * 1e3;
let activeBlock = null;
for (const block of sessionBlocks) {
const actualEndTime = block.entries.length > 0 ? block.entries[block.entries.length - 1].timestamp : block.startTime;
const isActive = now.getTime() - new Date(actualEndTime).getTime() < sessionDurationMs && now < block.endTime;
if (isActive) {
activeBlock = block;
break;
}
}
if (!activeBlock || activeBlock.entries.length === 0) {
return this.getEmptyUsage();
}
const tokenBreakdown = this.calculateTokenBreakdown(activeBlock.entries);
const totalCost = activeBlock.entries.reduce((sum, entry) => {
return sum + (entry.costUSD || 0);
}, 0);
return {
totalTokens: tokenBreakdown.total,
totalCost,
tokenBreakdown,
entries: activeBlock.entries,
sessionCount: transcriptFiles.length
};
} catch (error) {
return this.getEmptyUsage();
}
}
/**
* 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) {
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) {
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;
}
/**
* Floor timestamp to hour (used by ccusage block identification)
*/
floorToHour(timestamp) {
const floored = new Date(timestamp);
floored.setUTCMinutes(0, 0, 0);
return floored;
}
/**
* Implement ccusage's identifySessionBlocks algorithm exactly
*/
identifySessionBlocks(entries) {
if (entries.length === 0) return [];
const sessionDurationMs = 5 * 60 * 60 * 1e3;
const blocks = [];
const sortedEntries = [...entries].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
let currentBlockStart = null;
let currentBlockEntries = [];
for (const entry of sortedEntries) {
const entryTime = new Date(entry.timestamp);
if (currentBlockStart == null) {
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
const timeSinceBlockStart = entryTime.getTime() - currentBlockStart.getTime();
const lastEntry = currentBlockEntries[currentBlockEntries.length - 1];
if (lastEntry == null) {
continue;
}
const lastEntryTime = new Date(lastEntry.timestamp);
const timeSinceLastEntry = entryTime.getTime() - lastEntryTime.getTime();
if (timeSinceBlockStart > sessionDurationMs || timeSinceLastEntry > sessionDurationMs) {
const endTime = new Date(currentBlockStart.getTime() + sessionDurationMs);
blocks.push({
startTime: currentBlockStart,
endTime,
entries: currentBlockEntries
});
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
currentBlockEntries.push(entry);
}
}
}
if (currentBlockStart != null && currentBlockEntries.length > 0) {
const endTime = new Date(currentBlockStart.getTime() + sessionDurationMs);
blocks.push({
startTime: currentBlockStart,
endTime,
entries: currentBlockEntries
});
}
return blocks;
}
/**
* 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;
}
throw new Error("No historical analysis available, skipping fallback");
} catch (error) {
throw error;
}
}
/**
* 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 = percentile99;
confidence = "low";
} else {
estimatedLimit = percentile99 > 0 ? percentile99 : 3e7;
confidence = "low";
}
return {
dailyTokenLimit: Math.round(estimatedLimit),
confidence,
source: "historical_analysis",
lastUpdated: /* @__PURE__ */ new Date()
};
} catch (error) {
return this.getConservativeFallback();
}
}
/**
* Group transcript entries into 5-hour blocks like ccusage
*/
async getHistoricalBlockUsage(days) {
const blocks = [];
const claudePaths = await import("./claude-paths-75KD26Y3.js").then((m) => m.getClaudePaths());
const projectPaths = await import("./claude-paths-75KD26Y3.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-75KD26Y3.js").then((m) => m.getClaudePaths());
const projectPaths = await import("./claude-paths-75KD26Y3.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/segments/subscription.ts
var SubscriptionService = class {
transcriptParser = new TranscriptParser();
limitDetection = new LimitDetectionService();
async getSubscriptionInfo(sessionId) {
try {
const usageData = await this.getActiveBlockUsage();
const limitInfo = await this.limitDetection.getDailyTokenLimit();
const dailyTotal = usageData.dailyUsageWithCacheRead;
const activeBlockTotal = usageData.totalTokens;
const activeBlockWithCache = usageData.activeBlockWithCache;
if (process.env.CC_STATUS_DEEP_DEBUG) {
await this.logDetailedDebugInfo(usageData, limitInfo);
}
const tokensUsed = Math.round(activeBlockWithCache / 2);
const tokensLimit = limitInfo.dailyTokenLimit;
const percentage = tokensLimit > 0 ? tokensUsed / tokensLimit * 100 : 0;
const isOverLimit = percentage > 100;
return {
percentage: Math.round(percentage * 10) / 10,
// Round to 1 decimal
tokensUsed,
tokensLimit,
isOverLimit,
projection: null
// Keep it simple for now
};
} catch (error) {
return this.getFallbackData();
}
}
/**
* Get single active block usage (claude-powerline style) with daily totals
*/
async getActiveBlockUsage() {
try {
const transcriptFiles = await import("./claude-paths-75KD26Y3.js").then((m) => m.findTodaysTranscripts());
const allEntries = [];
const seenHashes = /* @__PURE__ */ new Set();
let totalRawEntries = 0;
let skippedSidechain = 0;
let skippedNoUsage = 0;
let skippedDuplicate = 0;
for (const filePath of transcriptFiles) {
const entries = await this.transcriptParser.parseTranscriptFile(filePath);
totalRawEntries += entries.length;
for (const entry of entries) {
if (this.isSidechainEntry(entry)) {
skippedSidechain++;
continue;
}
if (!entry.message?.usage) {
skippedNoUsage++;
continue;
}
const hash = this.createEntryHash(entry);
if (hash && seenHashes.has(hash)) {
skippedDuplicate++;
continue;
}
if (hash) {
seenHashes.add(hash);
}
allEntries.push({
timestamp: new Date(entry.timestamp),
usage: {
inputTokens: entry.message.usage.input_tokens || 0,
outputTokens: entry.message.usage.output_tokens || 0,
cacheCreationInputTokens: entry.message.usage.cache_creation_input_tokens || 0,
cacheReadInputTokens: entry.message.usage.cache_read_input_tokens || 0
},
costUSD: entry.costUSD || 0,
model: this.transcriptParser.extractModelId(entry)
});
}
}
if (allEntries.length === 0) {
return { totalTokens: 0, totalCost: 0 };
}
const sessionBlocks = this.identifySessionBlocks(allEntries);
const activeBlock = this.findActiveBlock(sessionBlocks);
if (!activeBlock || activeBlock.length === 0) {
return { totalTokens: 0, totalCost: 0 };
}
const totalTokens = activeBlock.reduce((sum, entry) => {
return sum + entry.usage.inputTokens + entry.usage.outputTokens + entry.usage.cacheCreationInputTokens;
}, 0);
const activeBlockWithCache = activeBlock.reduce((sum, entry) => {
return sum + entry.usage.inputTokens + entry.usage.outputTokens + entry.usage.cacheCreationInputTokens + entry.usage.cacheReadInputTokens;
}, 0);
const totalCost = activeBlock.reduce((sum, entry) => sum + entry.costUSD, 0);
const tokenBreakdown = activeBlock.reduce((acc, entry) => ({
input: acc.input + entry.usage.inputTokens,
output: acc.output + entry.usage.outputTokens,
cacheCreate: acc.cacheCreate + entry.usage.cacheCreationInputTokens,
cacheRead: acc.cacheRead + entry.usage.cacheReadInputTokens
}), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 });
const tokensWithoutCacheRead = activeBlock.reduce((sum, entry) => {
return sum + entry.usage.inputTokens + entry.usage.outputTokens + entry.usage.cacheCreationInputTokens;
}, 0);
const dailyUsage = allEntries.reduce((sum, entry) => {
return sum + entry.usage.inputTokens + entry.usage.outputTokens + entry.usage.cacheCreationInputTokens;
}, 0);
const dailyUsageWithCacheRead = allEntries.reduce((sum, entry) => {
return sum + entry.usage.inputTokens + entry.usage.outputTokens + entry.usage.cacheCreationInputTokens + entry.usage.cacheReadInputTokens;
}, 0);
return { totalTokens, totalCost, dailyUsageWithCacheRead, activeBlockWithCache };
} catch (error) {
return { totalTokens: 0, totalCost: 0, dailyUsageWithCacheRead: 0, activeBlockWithCache: 0 };
}
}
/**
* Identify session blocks using claude-powerline's algorithm
*/
identifySessionBlocks(entries) {
if (entries.length === 0) return [];
const sessionDurationMs = 5 * 60 * 60 * 1e3;
const blocks = [];
const sortedEntries = [...entries].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
let currentBlockStart = null;
let currentBlockEntries = [];
for (const entry of sortedEntries) {
const entryTime = entry.timestamp;
if (currentBlockStart == null) {
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
const timeSinceBlockStart = entryTime.getTime() - currentBlockStart.getTime();
const lastEntry = currentBlockEntries[currentBlockEntries.length - 1];
if (lastEntry == null) {
continue;
}
const lastEntryTime = lastEntry.timestamp;
const timeSinceLastEntry = entryTime.getTime() - lastEntryTime.getTime();
if (timeSinceBlockStart > sessionDurationMs || timeSinceLastEntry > sessionDurationMs) {
blocks.push(currentBlockEntries);
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
currentBlockEntries.push(entry);
}
}
}
if (currentBlockStart != null && currentBlockEntries.length > 0) {
blocks.push(currentBlockEntries);
}
return blocks;
}
/**
* Find the active block using claude-powerline's exact algorithm
*/
findActiveBlock(blocks) {
for (let i = blocks.length - 1; i >= 0; i--) {
const block = blocks[i];
if (!block || block.length === 0) continue;
const firstEntry = block[0];
if (!firstEntry) continue;
const blockStartTime = this.floorToHour(firstEntry.timestamp);
const blockInfo = this.createBlockInfo(blockStartTime, block);
if (blockInfo.isActive) {
return blockInfo.block;
}
}
return null;
}
/**
* Create block info with active status (claude-powerline's exact logic)
*/
createBlockInfo(startTime, entries) {
const now = /* @__PURE__ */ new Date();
const sessionDurationMs = 5 * 60 * 60 * 1e3;
const endTime = new Date(startTime.getTime() + sessionDurationMs);
const lastEntry = entries[entries.length - 1];
const actualEndTime = lastEntry != null ? lastEntry.timestamp : startTime;
const isActive = now.getTime() - actualEndTime.getTime() < sessionDurationMs && now < endTime;
return { block: entries, isActive };
}
/**
* Floor timestamp to hour (UTC)
*/
floorToHour(timestamp) {
const floored = new Date(timestamp);
floored.setUTCMinutes(0, 0, 0);
return floored;
}
/**
* Check if entry is a sidechain entry (claude-powerline's filtering)
*/
isSidechainEntry(entry) {
return entry.isSidechain === true;
}
/**
* Create unique hash for deduplication (claude-powerline's logic)
*/
createEntryHash(entry) {
try {
const messageId = entry.message?.id;
let requestId = entry.requestId;
if (!requestId && typeof entry.message === "object" && entry.message !== null) {
requestId = entry.message.requestId;
}
if (messageId && requestId) {
return `${messageId}:${requestId}`;
}
if (entry.timestamp && entry.message?.usage) {
const usage = entry.message.usage;
const signature = `${entry.timestamp}_${usage.input_tokens || 0}_${usage.output_tokens || 0}`;
return signature;
}
return null;
} catch {
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
};
}
};
// 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.costUSD || 0;
}
const totalTokens = totalInputTokens + totalOutputTokens + totalCacheCreationTokens + totalCacheReadTokens;
const tokensPerMinute = totalTokens / durationMinutes;
const nonCacheTokens = totalInputTokens + totalOutputTokens;
const tokensPerMinuteForIndicator = nonCacheTokens / durationMinutes;
let costPerHour = totalCost / durationMinutes * 60;
if (totalCost === 0 && totalTokens > 0) {
const estimatedCost = totalInputTokens * 3 / 1e6 + totalOutputTokens * 15 / 1e6 + totalCacheCreationTokens * 3.75 / 1e6 + // Cache creation = 1.25x input
totalCacheReadTokens * 0.3 / 1e6;
costPerHour = estimatedCost / durationMinutes * 60;
}
const projection = this.projectUsage(entries, tokensPerMinute, costPerHour);
return {
tokensPerMinute,
tokensPerMinuteForIndicator,
costPerHour,
projection
};
}
projectUsage(entries, tokensPerMinute, costPerHour) {
if (entries.length === 0) {
return null;
}
const firstEntry = entries[0];
if (!firstEntry?.timestamp) {
return null;
}
const firstTime = new Date(firstEntry.timestamp);
const sessionStart = this.floorToHour(firstTime);
const sessionEnd = new Date(sessionStart.getTime() + this.SESSION_DURATION_MS);
const now = /* @__PURE__ */ new Date();
const remainingTime = sessionEnd.getTime() - now.getTime();
const remainingMinutes = Math.max(0, remainingTime / (1e3 * 60));
let currentTotalTokens = 0;
let currentTotalCost = 0;
for (const entry of entries) {
const usage = entry.message?.usage;
if (usage) {
currentTotalTokens += (usage.input_tokens || 0) + (usage.output_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
}
currentTotalCost += entry.costUSD || 0;
}
const projectedAdditionalTokens = tokensPerMinute * remainingMinutes;
const projectedAdditionalCost = costPerHour / 60 * remainingMinutes;
return {
totalTokens: Math.round(currentTotalTokens + projectedAdditionalTokens),
totalCost: Math.round((currentTotalCost + projectedAdditionalCost) * 100) / 100,
remainingMinutes: Math.round(remainingMinutes)
};
}
floorToHour(timestamp) {
const floored = new Date(timestamp);
floored.setUTCMinutes(0, 0, 0);
return floored;
}
};
// src/segments/session-timer.ts
var SessionTimerService = class {
transcriptParser = new TranscriptParser();
sessionDurationHours = 5;
floorToHour(timestamp) {
const floored = new Date(timestamp);
floored.setUTCMinutes(0, 0, 0);
return floored;
}
async getSessionTimer() {
try {
const activeBlock = await this.findActiveBlock();
if (!activeBlock) {
return null;
}
const now = /* @__PURE__ */ new Date();
const timeRemainingMs = Math.max(0, activeBlock.endTime.getTime() - now.getTime());
const timeRemainingMinutes = Math.round(timeRemainingMs / (1e3 * 60));
const elapsedMs = now.getTime() - activeBlock.startTime.getTime();
const elapsedMinutes = Math.round(elapsedMs / (1e3 * 60));
return {
timeRemaining: this.formatTimeRemaining(timeRemainingMinutes),
resetTime: this.formatResetTime(activeBlock.endTime),
isNearReset: timeRemainingMinutes < 30,
// Less than 30 minutes
startTime: this.formatResetTime(activeBlock.startTime),
// For debugging
elapsedTime: this.formatElapsedTime(elapsedMinutes)
// For debugging
};
} catch (error) {
return null;
}
}
/**
* Find active block using ccusage's exact algorithm
*/
async findActiveBlock() {
try {
const allUsage = await this.transcriptParser.getAllSessionsForToday();
if (!allUsage.entries || allUsage.entries.length === 0) {
return null;
}
const sessionBlocks = this.identifySessionBlocks(allUsage.entries);
const now = /* @__PURE__ */ new Date();
const sessionDurationMs = this.sessionDurationHours * 60 * 60 * 1e3;
for (const block of sessionBlocks) {
const actualEndTime = block.entries.length > 0 ? block.entries[block.entries.length - 1].timestamp : block.startTime;
const isActive = now.getTime() - new Date(actualEndTime).getTime() < sessionDurationMs && now < block.endTime;
if (isActive) {
return block;
}
}
return null;
} catch (error) {
return null;
}
}
/**
* Implement ccusage's identifySessionBlocks algorithm exactly
*/
identifySessionBlocks(entries) {
if (entries.length === 0) return [];
const sessionDurationMs = this.sessionDurationHours * 60 * 60 * 1e3;
const blocks = [];
const sortedEntries = [...entries].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
let currentBlockStart = null;
let currentBlockEntries = [];
for (const entry of sortedEntries) {
const entryTime = new Date(entry.timestamp);
if (currentBlockStart == null) {
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
const timeSinceBlockStart = entryTime.getTime() - currentBlockStart.getTime();
const lastEntry = currentBlockEntries[currentBlockEntries.length - 1];
if (lastEntry == null) {
continue;
}
const lastEntryTime = new Date(lastEntry.timestamp);
const timeSinceLastEntry = entryTime.getTime() - lastEntryTime.getTime();
if (timeSinceBlockStart > sessionDurationMs || timeSinceLastEntry > sessionDurationMs) {
const endTime = new Date(currentBlockStart.getTime() + sessionDurationMs);
blocks.push({
startTime: currentBlockStart,
endTime,
entries: currentBlockEntries
});
currentBlockStart = this.floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
currentBlockEntries.push(entry);
}
}
}
if (currentBlockStart != null && currentBlockEntries.length > 0) {
const endTime = new Date(currentBlockStart.getTime() + sessionDurationMs);
blocks.push({
startTime: currentBlockStart,
endTime,
entries: currentBlockEntries
});
}
return blocks;
}
/**
* Format time remaining like ccusage: "29m" or "4h" or "0m"
*/
formatTimeRemaining(minutes) {
if (minutes <= 0) return "0m";
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours > 0) {
return `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ""}`;
} else {
return `${minutes}m`;
}
}
/**
* Format reset time like ccusage: "6PM" (not "6:00 PM")
*/
formatResetTime(resetTime) {
const timeOptions = {
hour: "numeric",
hour12: true
// Use local timezone, not Pacific
};
return resetTime.toLocaleTimeString("en-US", timeOptions);
}
/**
* Format elapsed time for debugging: "4h"
*/
formatElapsedTime(minutes) {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (hours > 0) {
return `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ""}`;
} else {
return `${minutes}m`;
}
}
};
// 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,
ca