Files
claude-statusline/fetch-usage.js
Axel Meyer f308a8105e Fix block comment syntax error from cron example
The */5 in the cron example inside the JSDoc block comment was parsed
as end-of-comment, causing a SyntaxError. Replaced with a reference
to install.sh.

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

116 lines
3.4 KiB
JavaScript
Executable File

#!/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: every 5 min (see install.sh or README for crontab line)
*/
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 || '';
// --- Cache writer ---
function writeCache(data) {
const cacheDir = path.dirname(CACHE_FILE);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
}
// --- 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) {
if (resp.status === 401 || resp.status === 403) {
writeCache({ _error: 'auth_expired', _status: resp.status });
}
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) {
writeCache({ _error: resp.status === 401 || resp.status === 403
? 'auth_expired' : 'api_error', _status: resp.status });
console.error('API error: ' + resp.status);
process.exit(1);
}
const data = await resp.json();
writeCache(data);
console.log('OK \u2014 wrote ' + CACHE_FILE);
} catch (e) {
writeCache({ _error: 'fetch_failed', _message: e.message });
console.error('Fetch error: ' + e.message);
process.exit(1);
}
}
main();