claude-code-statusline
Version:
A cute pet-themed status line plugin for Claude Code that displays your token usage through adorable emoji pets!
230 lines (191 loc) ⢠7.84 kB
JavaScript
import { statSync } from 'fs';
import { getPetState } from './animations.js';
import { formatAccountType, createStatusLine, formatTokens } from './utils.js';
// Import ccusage modules directly
let loadSessionBlockData, getTotalTokens;
try {
const dataLoaderModule = await import('ccusage/data-loader');
const calculateCostModule = await import('ccusage/calculate-cost');
loadSessionBlockData = dataLoaderModule.loadSessionBlockData;
getTotalTokens = calculateCostModule.getTotalTokens;
} catch (error) {
// Fallback if ccusage modules are not available
console.error('Warning: ccusage modules not available, falling back to command line');
}
// Handle command line arguments
function handleCliArgs() {
const args = process.argv.slice(2);
if (args.includes('--version') || args.includes('-v')) {
console.log('1.1.4');
process.exit(0);
}
if (args.includes('--help') || args.includes('-h')) {
console.log(`
š¾ Claude Code Statusline v1.1.4
A cute pet-themed status line plugin for Claude Code that displays
your token usage through adorable emoji pets! Now with international support.
Usage:
claude-code-statusline Display status line
claude-code-statusline --version Show version
claude-code-statusline --help Show this help
Configuration:
Add this to your .claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "claude-code-statusline",
"padding": 0
}
}
For more information, visit:
https://github.com/xpzouying/claude-code-statusline
`);
process.exit(0);
}
}
// Parse ccusage data using direct import
async function getCCUsageData(sessionId) {
try {
// Use direct ccusage import if available
if (loadSessionBlockData && getTotalTokens) {
const blocks = await loadSessionBlockData({ offline: true });
// Find the active block
const activeBlock = blocks.find(b => b.isActive);
if (!activeBlock) {
return null;
}
// Calculate current tokens using getTotalTokens
const currentTokens = getTotalTokens(activeBlock.tokenCounts);
// Calculate remaining minutes from time data
const now = new Date();
const remainingMinutes = Math.round((new Date(activeBlock.endTime).getTime() - now.getTime()) / (1000 * 60));
// Calculate tokens per minute for burn rate indicator
const elapsedMinutes = Math.round((now.getTime() - new Date(activeBlock.startTime).getTime()) / (1000 * 60));
const tokensPerMinute = elapsedMinutes > 0 ? Math.round(currentTokens / elapsedMinutes) : 0;
// Find the maximum token count from all non-gap blocks to use as reference
let maxTokensReference = 28795328; // fallback from historical data
const historicalTokens = blocks
.filter(b => !b.isGap && b.tokenCounts)
.map(b => getTotalTokens(b.tokenCounts))
.filter(tokens => tokens > 0);
if (historicalTokens.length > 0) {
maxTokensReference = Math.max(...historicalTokens);
}
// Calculate time window countdown percentage (100% = start, 0% = reset time)
const windowDurationMs = new Date(activeBlock.endTime).getTime() - new Date(activeBlock.startTime).getTime();
const elapsedMs = now.getTime() - new Date(activeBlock.startTime).getTime();
const windowPercent = Math.max(0, Math.round((1 - elapsedMs / windowDurationMs) * 100));
return {
windowPercent,
currentTokens,
remainingMinutes: Math.max(0, remainingMinutes),
tokensPerMinute,
totalCost: activeBlock.costUSD || 0,
blockStartTime: activeBlock.startTime
};
}
// Fallback to command line method if direct import failed
return await getCCUsageDataFallback(sessionId);
} catch (error) {
// Fallback if direct import fails - return null to use Claude Code data
console.error('ccusage direct import failed:', error.message);
return null;
}
}
// Fallback command line method (keeping original implementation)
async function getCCUsageDataFallback(sessionId) {
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
const { stdout } = await execAsync(`bunx ccusage blocks --json 2>/dev/null`);
const data = JSON.parse(stdout);
const activeBlock = data.blocks?.find(b => b.isActive);
if (!activeBlock) {
return null;
}
const remainingMinutes = activeBlock.projection?.remainingMinutes || 300;
const currentTokens = activeBlock.totalTokens || 0;
const tokensPerMinute = activeBlock.burnRate?.tokensPerMinuteForIndicator || 0;
let maxTokensReference = 28795328;
if (data.blocks) {
const maxFromHistory = Math.max(
...data.blocks.filter(b => !b.isGap && b.totalTokens).map(b => b.totalTokens)
);
if (maxFromHistory > 0) {
maxTokensReference = maxFromHistory;
}
}
// Calculate time window countdown percentage for fallback method
const windowDurationMs = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
const elapsedMs = windowDurationMs - (remainingMinutes * 60 * 1000);
const windowPercent = Math.max(0, Math.round((remainingMinutes * 60 * 1000) / windowDurationMs * 100));
return {
windowPercent,
currentTokens,
remainingMinutes,
tokensPerMinute,
totalCost: activeBlock.costUSD || 0,
blockStartTime: activeBlock.startTime
};
} catch (error) {
return null;
}
}
// Main function
async function main() {
// Handle CLI arguments first
handleCliArgs();
let input = '';
// Read stdin
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
input += chunk;
}
// Parse Claude Code JSON input
let claudeData = {};
try {
claudeData = JSON.parse(input);
} catch (e) {
// If no valid JSON input, use empty object
}
// Get session ID
const sessionId = claudeData.session_id || 'unknown';
const modelName = claudeData.model?.display_name || 'Claude';
// Check for test mode (skip ccusage if session_id starts with 'test-')
const isTestMode = sessionId.startsWith('test-');
// Get ccusage data (skip in test mode)
const usageData = isTestMode ? null : await getCCUsageData(sessionId);
// Default values if ccusage fails
let windowPercent = 100;
let currentTokens = 0;
let remainingMinutes = 300;
let totalCost = 0;
if (usageData) {
windowPercent = usageData.windowPercent;
currentTokens = usageData.currentTokens || 0;
remainingMinutes = usageData.remainingMinutes;
totalCost = usageData.totalCost;
} else if (claudeData.cost) {
// Fallback to Claude Code provided cost data
totalCost = claudeData.cost.total_cost_usd || 0;
// Rough estimate of window percentage based on cost and time
// Assuming average $2 per 5h block, estimate time elapsed
const costBasedElapsedPercent = Math.min(100, (totalCost / 2) * 100);
windowPercent = Math.max(0, 100 - costBasedElapsedPercent);
remainingMinutes = Math.round(300 * (windowPercent / 100));
}
// Get dynamic pet state based on time window percentage (inverted for pet state)
const petState = getPetState(100 - windowPercent);
// Format account type
const accountType = formatAccountType(claudeData.version);
// Create and output the status line using core utilities
const statusLine = createStatusLine(petState, windowPercent, remainingMinutes, totalCost, accountType, currentTokens);
console.log(statusLine);
}
// Run the script
main().catch(error => {
// On error, output a simple fallback status
console.log('š± Claude Code Pet Status | Initializing...');
});