- 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>
93 lines
2.6 KiB
JavaScript
Executable File
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();
|