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>
This commit is contained in:
212
fetch-usage.js
212
fetch-usage.js
@@ -1,106 +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();
|
||||
#!/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();
|
||||
|
||||
Reference in New Issue
Block a user