Initial commit: standalone Claude Code statusline

Headless-friendly statusline with context window bar and optional
token usage fetcher (cron-based, no browser needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-02-14 14:23:40 +00:00
commit ddd3b6d637
4 changed files with 284 additions and 0 deletions

106
fetch-usage.js Executable file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Claude Usage Fetcher (Standalone)
*
* Fetches token usage from claude.ai API using a session key.
* Designed to run as a cron job on headless servers.
*
* Session key source (checked in order):
* 1. CLAUDE_SESSION_KEY env var
* 2. ~/.config/claude-statusline/session-key (plain text file)
*
* To get your session key:
* 1. Log into claude.ai in any browser
* 2. Open DevTools → Application → Cookies → claude.ai
* 3. Copy the value of the "sessionKey" cookie
* 4. Save it: echo "sk-ant-..." > ~/.config/claude-statusline/session-key
*
* Output: Writes JSON to $CLAUDE_USAGE_CACHE or /tmp/claude_usage.json
*
* Cron example (every 5 min):
* */5 * * * * /usr/bin/node /path/to/fetch-usage.js 2>/dev/null
*/
const fs = require('fs');
const path = require('path');
// --- Config ---
const CONFIG_DIR = process.env.CLAUDE_STATUSLINE_CONFIG
|| path.join(process.env.HOME || '/root', '.config', 'claude-statusline');
const CACHE_FILE = process.env.CLAUDE_USAGE_CACHE
|| path.join(process.env.TMPDIR || '/tmp', 'claude_usage.json');
const ORG_ID = process.env.CLAUDE_ORG_ID || '';
// --- Session Key ---
function getSessionKey() {
// env var first
if (process.env.CLAUDE_SESSION_KEY) return process.env.CLAUDE_SESSION_KEY.trim();
// config file
const keyFile = path.join(CONFIG_DIR, 'session-key');
try {
return fs.readFileSync(keyFile, 'utf8').trim();
} catch {
return null;
}
}
// --- Discover org ID ---
async function getOrgId(sessionKey) {
if (ORG_ID) return ORG_ID;
const resp = await fetch('https://claude.ai/api/organizations', {
headers: headers(sessionKey),
signal: AbortSignal.timeout(10000),
});
if (!resp.ok) throw new Error('Failed to list orgs: ' + resp.status);
const orgs = await resp.json();
if (!orgs.length) throw new Error('No organizations found');
return orgs[0].uuid;
}
function headers(sessionKey) {
return {
'Cookie': 'sessionKey=' + sessionKey,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0',
'Accept': 'application/json',
'Referer': 'https://claude.ai/',
'Origin': 'https://claude.ai',
};
}
// --- Fetch ---
async function main() {
const sessionKey = getSessionKey();
if (!sessionKey) {
console.error('No session key found. Set CLAUDE_SESSION_KEY or create ' +
path.join(CONFIG_DIR, 'session-key'));
process.exit(1);
}
try {
const orgId = await getOrgId(sessionKey);
const resp = await fetch('https://claude.ai/api/organizations/' + orgId + '/usage', {
headers: headers(sessionKey),
signal: AbortSignal.timeout(10000),
});
if (!resp.ok) {
console.error('API error: ' + resp.status);
process.exit(1);
}
const data = await resp.json();
// Ensure cache dir exists
const cacheDir = path.dirname(CACHE_FILE);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
console.log('OK — wrote ' + CACHE_FILE);
} catch (e) {
console.error('Fetch error: ' + e.message);
process.exit(1);
}
}
main();

73
install.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Install claude-statusline for the current user.
# Usage: bash install.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="${HOME}/.local/share/claude-statusline"
CONFIG_DIR="${HOME}/.config/claude-statusline"
CLAUDE_DIR="${HOME}/.claude"
echo "==> Installing claude-statusline..."
# Copy scripts
mkdir -p "$INSTALL_DIR"
cp "$SCRIPT_DIR/statusline.js" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/fetch-usage.js" "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR"/*.js
# Create config dir
mkdir -p "$CONFIG_DIR"
if [ ! -f "$CONFIG_DIR/session-key" ]; then
echo "# Paste your claude.ai sessionKey cookie value here" > "$CONFIG_DIR/session-key"
chmod 600 "$CONFIG_DIR/session-key"
echo " Created $CONFIG_DIR/session-key (edit with your session key)"
fi
# Configure Claude Code statusline
mkdir -p "$CLAUDE_DIR"
SETTINGS="$CLAUDE_DIR/settings.json"
NODE_BIN="$(which node 2>/dev/null || echo '/usr/bin/node')"
if [ -f "$SETTINGS" ]; then
# Check if statusLine already configured
if grep -q '"statusLine"' "$SETTINGS" 2>/dev/null; then
echo " statusLine already configured in $SETTINGS — skipping"
else
# Insert statusLine before last closing brace
TMP=$(mktemp)
sed '$ d' "$SETTINGS" > "$TMP"
# Add comma if needed
if grep -q '[^{]' "$TMP"; then
echo ',' >> "$TMP"
fi
cat >> "$TMP" <<SEOF
"statusLine": {
"type": "command",
"command": "${NODE_BIN} --no-warnings ${INSTALL_DIR}/statusline.js"
}
}
SEOF
mv "$TMP" "$SETTINGS"
echo " Added statusLine to $SETTINGS"
fi
else
cat > "$SETTINGS" <<SEOF
{
"statusLine": {
"type": "command",
"command": "${NODE_BIN} --no-warnings ${INSTALL_DIR}/statusline.js"
}
}
SEOF
echo " Created $SETTINGS with statusLine config"
fi
# Offer cron setup
echo ""
echo "==> Optional: set up usage fetcher cron (every 5 min):"
echo " crontab -e"
echo " */5 * * * * ${NODE_BIN} ${INSTALL_DIR}/fetch-usage.js 2>/dev/null"
echo ""
echo "==> Done! Restart Claude Code to see the statusline."
echo " Don't forget to add your session key to: $CONFIG_DIR/session-key"

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "claude-statusline",
"version": "0.1.0",
"description": "Standalone Claude Code statusline for headless Linux servers",
"main": "statusline.js",
"scripts": {
"statusline": "node statusline.js",
"fetch": "node fetch-usage.js",
"install-statusline": "bash install.sh"
},
"keywords": ["claude", "statusline", "cli"],
"license": "MIT"
}

92
statusline.js Executable file
View File

@@ -0,0 +1,92 @@
#!/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();