feat(status): add claude.ai usage fallback

This commit is contained in:
Peter Steinberger
2026-01-09 15:30:28 +00:00
parent 922ca2ee1c
commit 014a4d51a6
4 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,299 @@
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
type Args = {
agentId: string;
reveal: boolean;
sessionKey?: string;
};
const mask = (value: string) => {
const compact = value.trim();
if (!compact) return "missing";
const edge = compact.length >= 12 ? 6 : 4;
return `${compact.slice(0, edge)}${compact.slice(-edge)}`;
};
const parseArgs = (): Args => {
const args = process.argv.slice(2);
let agentId = "main";
let reveal = false;
let sessionKey: string | undefined;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--agent" && args[i + 1]) {
agentId = String(args[++i]).trim() || "main";
continue;
}
if (arg === "--reveal") {
reveal = true;
continue;
}
if (arg === "--session-key" && args[i + 1]) {
sessionKey = String(args[++i]).trim() || undefined;
continue;
}
}
return { agentId, reveal, sessionKey };
};
const loadAuthProfiles = (agentId: string) => {
const stateRoot =
process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(os.homedir(), ".clawdbot");
const authPath = path.join(stateRoot, "agents", agentId, "agent", "auth-profiles.json");
if (!fs.existsSync(authPath)) throw new Error(`Missing: ${authPath}`);
const store = JSON.parse(fs.readFileSync(authPath, "utf8")) as {
profiles?: Record<string, { provider?: string; type?: string; token?: string; key?: string }>;
};
return { authPath, store };
};
const pickAnthropicToken = (store: {
profiles?: Record<string, { provider?: string; type?: string; token?: string; key?: string }>;
}): { profileId: string; token: string } | null => {
const profiles = store.profiles ?? {};
for (const [id, cred] of Object.entries(profiles)) {
if (cred?.provider !== "anthropic") continue;
const token = cred.type === "token" ? cred.token?.trim() : undefined;
if (token) return { profileId: id, token };
}
return null;
};
const fetchAnthropicOAuthUsage = async (token: string) => {
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"anthropic-version": "2023-06-01",
"anthropic-beta": "oauth-2025-04-20",
"User-Agent": "clawdbot-debug",
},
});
const text = await res.text();
return { status: res.status, contentType: res.headers.get("content-type"), text };
};
const chromeServiceNameForPath = (cookiePath: string): string => {
if (cookiePath.includes("/Arc/")) return "Arc Safe Storage";
if (cookiePath.includes("/BraveSoftware/")) return "Brave Safe Storage";
if (cookiePath.includes("/Microsoft Edge/")) return "Microsoft Edge Safe Storage";
if (cookiePath.includes("/Chromium/")) return "Chromium Safe Storage";
return "Chrome Safe Storage";
};
const readKeychainPassword = (service: string): string | null => {
try {
const out = execFileSync(
"security",
["find-generic-password", "-w", "-s", service],
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 },
);
const pw = out.trim();
return pw ? pw : null;
} catch {
return null;
}
};
const decryptChromeCookieValue = (encrypted: Buffer, service: string): string | null => {
if (encrypted.length < 4) return null;
const prefix = encrypted.subarray(0, 3).toString("utf8");
if (prefix !== "v10" && prefix !== "v11") return null;
const password = readKeychainPassword(service);
if (!password) return null;
const key = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
const iv = Buffer.alloc(16, 0x20);
const data = encrypted.subarray(3);
try {
const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
decipher.setAutoPadding(true);
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
const text = decrypted.toString("utf8").trim();
return text ? text : null;
} catch {
return null;
}
};
const queryChromeCookieDb = (cookieDb: string): string | null => {
try {
const out = execFileSync(
"sqlite3",
[
"-readonly",
cookieDb,
`
SELECT
COALESCE(NULLIF(value,''), hex(encrypted_value))
FROM cookies
WHERE (host_key LIKE '%claude.ai%' OR host_key = '.claude.ai')
AND name = 'sessionKey'
LIMIT 1;
`,
],
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 },
).trim();
if (!out) return null;
if (out.startsWith("sk-ant-")) return out;
const hex = out.replace(/[^0-9A-Fa-f]/g, "");
if (!hex) return null;
const buf = Buffer.from(hex, "hex");
const service = chromeServiceNameForPath(cookieDb);
const decrypted = decryptChromeCookieValue(buf, service);
return decrypted && decrypted.startsWith("sk-ant-") ? decrypted : null;
} catch {
return null;
}
};
const queryFirefoxCookieDb = (cookieDb: string): string | null => {
try {
const out = execFileSync(
"sqlite3",
[
"-readonly",
cookieDb,
`
SELECT value
FROM moz_cookies
WHERE (host LIKE '%claude.ai%' OR host = '.claude.ai')
AND name = 'sessionKey'
LIMIT 1;
`,
],
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 },
).trim();
return out && out.startsWith("sk-ant-") ? out : null;
} catch {
return null;
}
};
const findClaudeSessionKey = (): { sessionKey: string; source: string } | null => {
if (process.platform !== "darwin") return null;
const firefoxRoot = path.join(
os.homedir(),
"Library",
"Application Support",
"Firefox",
"Profiles",
);
if (fs.existsSync(firefoxRoot)) {
for (const entry of fs.readdirSync(firefoxRoot)) {
const db = path.join(firefoxRoot, entry, "cookies.sqlite");
if (!fs.existsSync(db)) continue;
const value = queryFirefoxCookieDb(db);
if (value) return { sessionKey: value, source: `firefox:${db}` };
}
}
const chromeCandidates = [
path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome"),
path.join(os.homedir(), "Library", "Application Support", "Chromium"),
path.join(os.homedir(), "Library", "Application Support", "Arc"),
path.join(os.homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
path.join(os.homedir(), "Library", "Application Support", "Microsoft Edge"),
];
for (const root of chromeCandidates) {
if (!fs.existsSync(root)) continue;
const profiles = fs
.readdirSync(root)
.filter((name) => name === "Default" || name.startsWith("Profile "));
for (const profile of profiles) {
const db = path.join(root, profile, "Cookies");
if (!fs.existsSync(db)) continue;
const value = queryChromeCookieDb(db);
if (value) return { sessionKey: value, source: `chromium:${db}` };
}
}
return null;
};
const fetchClaudeWebUsage = async (sessionKey: string) => {
const headers = {
Cookie: `sessionKey=${sessionKey}`,
Accept: "application/json",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
};
const orgRes = await fetch("https://claude.ai/api/organizations", { headers });
const orgText = await orgRes.text();
if (!orgRes.ok) {
return { ok: false as const, step: "organizations", status: orgRes.status, body: orgText };
}
const orgs = JSON.parse(orgText) as Array<{ uuid?: string }>;
const orgId = orgs?.[0]?.uuid;
if (!orgId) {
return { ok: false as const, step: "organizations", status: 200, body: orgText };
}
const usageRes = await fetch(`https://claude.ai/api/organizations/${orgId}/usage`, { headers });
const usageText = await usageRes.text();
return usageRes.ok
? { ok: true as const, orgId, body: usageText }
: { ok: false as const, step: "usage", status: usageRes.status, body: usageText };
};
const main = async () => {
const opts = parseArgs();
const { authPath, store } = loadAuthProfiles(opts.agentId);
console.log(`Auth file: ${authPath}`);
const anthropic = pickAnthropicToken(store);
if (!anthropic) {
console.log("Anthropic: no token profiles found in auth-profiles.json");
} else {
console.log(
`Anthropic: ${anthropic.profileId} token=${opts.reveal ? anthropic.token : mask(anthropic.token)}`,
);
const oauth = await fetchAnthropicOAuthUsage(anthropic.token);
console.log(
`OAuth usage: HTTP ${oauth.status} (${oauth.contentType ?? "no content-type"})`,
);
console.log(oauth.text.slice(0, 400).replace(/\s+/g, " ").trim());
console.log("");
}
const sessionKey =
opts.sessionKey?.trim() ||
process.env.CLAUDE_AI_SESSION_KEY?.trim() ||
process.env.CLAUDE_WEB_SESSION_KEY?.trim() ||
findClaudeSessionKey()?.sessionKey;
const source =
opts.sessionKey
? "--session-key"
: process.env.CLAUDE_AI_SESSION_KEY || process.env.CLAUDE_WEB_SESSION_KEY
? "env"
: findClaudeSessionKey()?.source ?? "auto";
if (!sessionKey) {
console.log("Claude web: no sessionKey found (try --session-key or export CLAUDE_AI_SESSION_KEY)");
return;
}
console.log(
`Claude web: sessionKey=${opts.reveal ? sessionKey : mask(sessionKey)} (source: ${source})`,
);
const web = await fetchClaudeWebUsage(sessionKey);
if (!web.ok) {
console.log(`Claude web: ${web.step} HTTP ${web.status}`);
console.log(String(web.body).slice(0, 400).replace(/\s+/g, " ").trim());
return;
}
console.log(`Claude web: org=${web.orgId} OK`);
console.log(web.body.slice(0, 400).replace(/\s+/g, " ").trim());
};
await main();