Files
claude-statusline/statusline.js
Axel Meyer d152dde002 Fix CRLF line endings, harden install.sh
- Add .gitattributes enforcing LF line endings
- Renormalize all files from CRLF to LF
- Replace fragile sed-based JSON manipulation with node -e
- Add Node.js 18+ version check (required for built-in fetch)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:28:34 +00:00

93 lines
2.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Claude Code Status Line (Standalone)
*
* Designed for headless Linux servers — no browser, no tray app needed.
*
* Context window: Always shown, read from stdin (JSON piped by Claude Code).
* Token usage: Optional. Reads from a cache file that can be populated by
* the included fetcher (cron) or any external source.
*
* Output: Context ▓▓▓▓░░░░░░ 40% | Token ░░░░░░░░░░ 10% 267M
*/
const fs = require('fs');
const path = require('path');
// --- Config ---
const CACHE_FILE = process.env.CLAUDE_USAGE_CACHE
|| path.join(process.env.TMPDIR || process.env.TEMP || '/tmp', 'claude_usage.json');
const CACHE_MAX_AGE_S = parseInt(process.env.CLAUDE_USAGE_MAX_AGE || '900', 10); // 15 min default
// --- Bar renderer ---
function bar(pct, width) {
width = width || 10;
const filled = Math.floor(Math.min(100, Math.max(0, pct)) / (100 / width));
return '\u2593'.repeat(filled) + '\u2591'.repeat(width - filled);
}
// --- Context Window ---
function getContextPart(data) {
try {
const pct = Math.round(parseFloat(data?.context_window?.used_percentage) || 0);
return 'Context ' + bar(pct) + ' ' + pct + '%';
} catch {
return 'Context ' + '\u2591'.repeat(10);
}
}
// --- Usage Cache ---
function readCache() {
try {
if (!fs.existsSync(CACHE_FILE)) return null;
const stat = fs.statSync(CACHE_FILE);
const ageS = (Date.now() - stat.mtimeMs) / 1000;
if (ageS > CACHE_MAX_AGE_S) return null;
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
} catch {
return null;
}
}
function formatMinutes(isoStr) {
if (!isoStr) return '';
try {
const remaining = Math.max(0, Math.round((new Date(isoStr).getTime() - Date.now()) / 60000));
return remaining + 'M';
} catch {
return '';
}
}
function getUsagePart(data) {
if (!data) return '';
const parts = [];
if (data.five_hour && data.five_hour.utilization > 0) {
const pct = Math.round(data.five_hour.utilization);
const remaining = formatMinutes(data.five_hour.resets_at);
parts.push('Token ' + bar(pct) + ' ' + pct + '%' + (remaining ? ' ' + remaining : ''));
}
if (data.seven_day && data.seven_day.utilization > 20) {
const pct = Math.round(data.seven_day.utilization);
parts.push('7d ' + pct + '%');
}
return parts.join(' | ');
}
// --- Main ---
function main() {
let stdinData = {};
try {
stdinData = JSON.parse(fs.readFileSync(0, 'utf8'));
} catch { /* no stdin or invalid JSON */ }
const ctx = getContextPart(stdinData);
const usage = getUsagePart(readCache());
console.log(usage ? ctx + ' | ' + usage : ctx);
}
main();