@alvax-ai/adapter-claude-local 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/format-event.d.ts +2 -0
- package/dist/cli/format-event.d.ts.map +1 -0
- package/dist/cli/format-event.js +136 -0
- package/dist/cli/format-event.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/quota-probe.d.ts +3 -0
- package/dist/cli/quota-probe.d.ts.map +1 -0
- package/dist/cli/quota-probe.js +106 -0
- package/dist/cli/quota-probe.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/server/claude-config.d.ts +5 -0
- package/dist/server/claude-config.d.ts.map +1 -0
- package/dist/server/claude-config.js +106 -0
- package/dist/server/claude-config.js.map +1 -0
- package/dist/server/execute.d.ts +18 -0
- package/dist/server/execute.d.ts.map +1 -0
- package/dist/server/execute.js +700 -0
- package/dist/server/execute.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +64 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/models.d.ts +11 -0
- package/dist/server/models.d.ts.map +1 -0
- package/dist/server/models.js +32 -0
- package/dist/server/models.js.map +1 -0
- package/dist/server/parse.d.ts +34 -0
- package/dist/server/parse.d.ts.map +1 -0
- package/dist/server/parse.js +319 -0
- package/dist/server/parse.js.map +1 -0
- package/dist/server/prompt-cache.d.ts +17 -0
- package/dist/server/prompt-cache.d.ts.map +1 -0
- package/dist/server/prompt-cache.js +125 -0
- package/dist/server/prompt-cache.js.map +1 -0
- package/dist/server/quota.d.ts +21 -0
- package/dist/server/quota.d.ts.map +1 -0
- package/dist/server/quota.js +484 -0
- package/dist/server/quota.js.map +1 -0
- package/dist/server/skills.d.ts +8 -0
- package/dist/server/skills.d.ts.map +1 -0
- package/dist/server/skills.js +97 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/test.d.ts +3 -0
- package/dist/server/test.d.ts.map +1 -0
- package/dist/server/test.js +258 -0
- package/dist/server/test.js.map +1 -0
- package/dist/ui/build-config.d.ts +3 -0
- package/dist/ui/build-config.d.ts.map +1 -0
- package/dist/ui/build-config.js +111 -0
- package/dist/ui/build-config.js.map +1 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/parse-stdout.d.ts +3 -0
- package/dist/ui/parse-stdout.d.ts.map +1 -0
- package/dist/ui/parse-stdout.js +149 -0
- package/dist/ui/parse-stdout.js.map +1 -0
- package/package.json +60 -0
- package/skills/alvax/SKILL.md +448 -0
- package/skills/alvax/references/api-reference.md +284 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { ensureAlvaxSkillSymlink } from "@alvax-ai/adapter-utils/server-utils";
|
|
7
|
+
const DEFAULT_ALVAX_INSTANCE_ID = "default";
|
|
8
|
+
function nonEmpty(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
function resolveManagedClaudePromptCacheRoot(env, companyId) {
|
|
12
|
+
const alvaxHome = nonEmpty(env.ALVAX_HOME) ?? path.resolve(os.homedir(), ".alvax");
|
|
13
|
+
const instanceId = nonEmpty(env.ALVAX_INSTANCE_ID) ?? DEFAULT_ALVAX_INSTANCE_ID;
|
|
14
|
+
return path.resolve(alvaxHome, "instances", instanceId, "companies", companyId, "claude-prompt-cache");
|
|
15
|
+
}
|
|
16
|
+
async function hashPathContents(candidate, hash, relativePath, seenDirectories) {
|
|
17
|
+
const stat = await fs.lstat(candidate);
|
|
18
|
+
if (stat.isSymbolicLink()) {
|
|
19
|
+
hash.update(`symlink:${relativePath}\n`);
|
|
20
|
+
const resolved = await fs.realpath(candidate).catch(() => null);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
hash.update("missing\n");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await hashPathContents(resolved, hash, relativePath, seenDirectories);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
const realDir = await fs.realpath(candidate).catch(() => candidate);
|
|
30
|
+
hash.update(`dir:${relativePath}\n`);
|
|
31
|
+
if (seenDirectories.has(realDir)) {
|
|
32
|
+
hash.update("loop\n");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
seenDirectories.add(realDir);
|
|
36
|
+
const entries = await fs.readdir(candidate, { withFileTypes: true });
|
|
37
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const childRelativePath = relativePath.length > 0 ? `${relativePath}/${entry.name}` : entry.name;
|
|
40
|
+
await hashPathContents(path.join(candidate, entry.name), hash, childRelativePath, seenDirectories);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (stat.isFile()) {
|
|
45
|
+
hash.update(`file:${relativePath}\n`);
|
|
46
|
+
hash.update(await fs.readFile(candidate));
|
|
47
|
+
hash.update("\n");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
hash.update(`other:${relativePath}:${stat.mode}\n`);
|
|
51
|
+
}
|
|
52
|
+
async function buildClaudePromptBundleKey(input) {
|
|
53
|
+
const hash = createHash("sha256");
|
|
54
|
+
hash.update("alvax-claude-prompt-bundle:v1\n");
|
|
55
|
+
if (input.instructionsContents) {
|
|
56
|
+
hash.update("instructions\n");
|
|
57
|
+
hash.update(input.instructionsContents);
|
|
58
|
+
hash.update("\n");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
hash.update("instructions:none\n");
|
|
62
|
+
}
|
|
63
|
+
const sortedSkills = [...input.skills].sort((left, right) => left.runtimeName.localeCompare(right.runtimeName));
|
|
64
|
+
for (const entry of sortedSkills) {
|
|
65
|
+
hash.update(`skill:${entry.key}:${entry.runtimeName}\n`);
|
|
66
|
+
await hashPathContents(entry.source, hash, entry.runtimeName, new Set());
|
|
67
|
+
}
|
|
68
|
+
return hash.digest("hex");
|
|
69
|
+
}
|
|
70
|
+
async function ensureReadableFile(targetPath, contents) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(targetPath, fsConstants.R_OK);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Fall through and materialize the file.
|
|
77
|
+
}
|
|
78
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
79
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
await fs.writeFile(tempPath, contents, "utf8");
|
|
82
|
+
await fs.rename(tempPath, targetPath);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const targetReadable = await fs.access(targetPath, fsConstants.R_OK).then(() => true).catch(() => false);
|
|
86
|
+
if (!targetReadable) {
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function prepareClaudePromptBundle(input) {
|
|
95
|
+
const { companyId, skills, instructionsContents, onLog } = input;
|
|
96
|
+
const bundleKey = await buildClaudePromptBundleKey({
|
|
97
|
+
skills,
|
|
98
|
+
instructionsContents,
|
|
99
|
+
});
|
|
100
|
+
const rootDir = path.join(resolveManagedClaudePromptCacheRoot(process.env, companyId), bundleKey);
|
|
101
|
+
const skillsHome = path.join(rootDir, ".claude", "skills");
|
|
102
|
+
await fs.mkdir(skillsHome, { recursive: true });
|
|
103
|
+
for (const entry of skills) {
|
|
104
|
+
const target = path.join(skillsHome, entry.runtimeName);
|
|
105
|
+
try {
|
|
106
|
+
await ensureAlvaxSkillSymlink(entry.source, target);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
await onLog("stderr", `[alvax] Failed to materialize Claude skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const instructionsFilePath = instructionsContents
|
|
113
|
+
? path.join(rootDir, "agent-instructions.md")
|
|
114
|
+
: null;
|
|
115
|
+
if (instructionsFilePath && instructionsContents) {
|
|
116
|
+
await ensureReadableFile(instructionsFilePath, instructionsContents);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
bundleKey,
|
|
120
|
+
rootDir,
|
|
121
|
+
addDir: rootDir,
|
|
122
|
+
instructionsFilePath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=prompt-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-cache.js","sourceRoot":"","sources":["../../src/server/prompt-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProviderQuotaResult, QuotaWindow } from "@alvax-ai/adapter-utils";
|
|
2
|
+
export declare function claudeConfigDir(): string;
|
|
3
|
+
interface ClaudeAuthStatus {
|
|
4
|
+
loggedIn: boolean;
|
|
5
|
+
authMethod: string | null;
|
|
6
|
+
subscriptionType: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function readClaudeAuthStatus(): Promise<ClaudeAuthStatus | null>;
|
|
9
|
+
export declare function readClaudeToken(): Promise<string | null>;
|
|
10
|
+
/** Convert a utilization value to a 0-100 integer percent. Returns null for null/undefined input.
|
|
11
|
+
* Handles both 0-1 fractions (legacy) and 0-100 percentages (current API). */
|
|
12
|
+
export declare function toPercent(utilization: number | null | undefined): number | null;
|
|
13
|
+
/** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */
|
|
14
|
+
export declare function fetchWithTimeout(url: string, init: RequestInit, ms?: number): Promise<Response>;
|
|
15
|
+
export declare function fetchClaudeQuota(token: string): Promise<QuotaWindow[]>;
|
|
16
|
+
export declare function parseClaudeCliUsageText(text: string): QuotaWindow[];
|
|
17
|
+
export declare function captureClaudeCliUsageText(timeoutMs?: number): Promise<string>;
|
|
18
|
+
export declare function fetchClaudeCliQuota(): Promise<QuotaWindow[]>;
|
|
19
|
+
export declare function getQuotaWindows(): Promise<ProviderQuotaResult>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=quota.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../../src/server/quota.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAOhF,wBAAgB,eAAe,IAAI,MAAM,CAIxC;AA4FD,UAAU,gBAAgB;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAgB7E;AASD,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO9D;AA+CD;+EAC+E;AAC/E,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAG/E;AAED,0GAA0G;AAC1G,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,SAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQnG;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA+D5E;AAgHD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAsCnE;AAeD,wBAAsB,yBAAyB,CAAC,SAAS,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BnF;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAGlE;AAOD,wBAAsB,eAAe,IAAI,OAAO,CAAC,mBAAmB,CAAC,CA4DpE"}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const CLAUDE_USAGE_SOURCE_OAUTH = "anthropic-oauth";
|
|
8
|
+
const CLAUDE_USAGE_SOURCE_CLI = "claude-cli";
|
|
9
|
+
export function claudeConfigDir() {
|
|
10
|
+
const fromEnv = process.env.CLAUDE_CONFIG_DIR;
|
|
11
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0)
|
|
12
|
+
return fromEnv.trim();
|
|
13
|
+
return path.join(os.homedir(), ".claude");
|
|
14
|
+
}
|
|
15
|
+
function hasNonEmptyProcessEnv(key) {
|
|
16
|
+
const value = process.env[key];
|
|
17
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
18
|
+
}
|
|
19
|
+
function createClaudeQuotaEnv() {
|
|
20
|
+
const env = {};
|
|
21
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
continue;
|
|
24
|
+
if (key.startsWith("ANTHROPIC_"))
|
|
25
|
+
continue;
|
|
26
|
+
env[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
function stripBackspaces(text) {
|
|
31
|
+
let out = "";
|
|
32
|
+
for (const char of text) {
|
|
33
|
+
if (char === "\b") {
|
|
34
|
+
out = out.slice(0, -1);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
out += char;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function stripAnsi(text) {
|
|
43
|
+
return text
|
|
44
|
+
.replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g, "")
|
|
45
|
+
.replace(/\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
46
|
+
}
|
|
47
|
+
function cleanTerminalText(text) {
|
|
48
|
+
return stripAnsi(stripBackspaces(text))
|
|
49
|
+
.replace(/\u0000/g, "")
|
|
50
|
+
.replace(/\r/g, "\n");
|
|
51
|
+
}
|
|
52
|
+
function normalizeForLabelSearch(text) {
|
|
53
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
54
|
+
}
|
|
55
|
+
function trimToLatestUsagePanel(text) {
|
|
56
|
+
const lower = text.toLowerCase();
|
|
57
|
+
const settingsIndex = lower.lastIndexOf("settings:");
|
|
58
|
+
if (settingsIndex < 0)
|
|
59
|
+
return null;
|
|
60
|
+
let tail = text.slice(settingsIndex);
|
|
61
|
+
const tailLower = tail.toLowerCase();
|
|
62
|
+
if (!tailLower.includes("usage"))
|
|
63
|
+
return null;
|
|
64
|
+
if (!tailLower.includes("current session") && !tailLower.includes("loading usage"))
|
|
65
|
+
return null;
|
|
66
|
+
const stopMarkers = [
|
|
67
|
+
"status dialog dismissed",
|
|
68
|
+
"checking for updates",
|
|
69
|
+
"press ctrl-c again to exit",
|
|
70
|
+
];
|
|
71
|
+
let stopIndex = -1;
|
|
72
|
+
for (const marker of stopMarkers) {
|
|
73
|
+
const markerIndex = tailLower.indexOf(marker);
|
|
74
|
+
if (markerIndex >= 0 && (stopIndex === -1 || markerIndex < stopIndex)) {
|
|
75
|
+
stopIndex = markerIndex;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (stopIndex >= 0) {
|
|
79
|
+
tail = tail.slice(0, stopIndex);
|
|
80
|
+
}
|
|
81
|
+
return tail;
|
|
82
|
+
}
|
|
83
|
+
async function readClaudeTokenFromFile(credPath) {
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = await fs.readFile(credPath, "utf8");
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
99
|
+
return null;
|
|
100
|
+
const obj = parsed;
|
|
101
|
+
const oauth = obj["claudeAiOauth"];
|
|
102
|
+
if (typeof oauth !== "object" || oauth === null)
|
|
103
|
+
return null;
|
|
104
|
+
const token = oauth["accessToken"];
|
|
105
|
+
return typeof token === "string" && token.length > 0 ? token : null;
|
|
106
|
+
}
|
|
107
|
+
export async function readClaudeAuthStatus() {
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execFileAsync("claude", ["auth", "status"], {
|
|
110
|
+
env: process.env,
|
|
111
|
+
timeout: 5_000,
|
|
112
|
+
maxBuffer: 1024 * 1024,
|
|
113
|
+
});
|
|
114
|
+
const parsed = JSON.parse(stdout);
|
|
115
|
+
return {
|
|
116
|
+
loggedIn: parsed.loggedIn === true,
|
|
117
|
+
authMethod: typeof parsed.authMethod === "string" ? parsed.authMethod : null,
|
|
118
|
+
subscriptionType: typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function describeClaudeSubscriptionAuth(status) {
|
|
126
|
+
if (!status?.loggedIn || status.authMethod !== "claude.ai")
|
|
127
|
+
return null;
|
|
128
|
+
return status.subscriptionType
|
|
129
|
+
? `Claude is logged in via claude.ai (${status.subscriptionType})`
|
|
130
|
+
: "Claude is logged in via claude.ai";
|
|
131
|
+
}
|
|
132
|
+
export async function readClaudeToken() {
|
|
133
|
+
const configDir = claudeConfigDir();
|
|
134
|
+
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
135
|
+
const token = await readClaudeTokenFromFile(path.join(configDir, filename));
|
|
136
|
+
if (token)
|
|
137
|
+
return token;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function formatCurrencyAmount(value, currency) {
|
|
142
|
+
const code = typeof currency === "string" && currency.trim().length > 0 ? currency.trim().toUpperCase() : "USD";
|
|
143
|
+
return new Intl.NumberFormat("en-US", {
|
|
144
|
+
style: "currency",
|
|
145
|
+
currency: code,
|
|
146
|
+
maximumFractionDigits: 2,
|
|
147
|
+
}).format(value);
|
|
148
|
+
}
|
|
149
|
+
function formatExtraUsageLabel(extraUsage) {
|
|
150
|
+
const monthlyLimit = extraUsage.monthly_limit;
|
|
151
|
+
const usedCredits = extraUsage.used_credits;
|
|
152
|
+
if (typeof monthlyLimit !== "number" ||
|
|
153
|
+
!Number.isFinite(monthlyLimit) ||
|
|
154
|
+
typeof usedCredits !== "number" ||
|
|
155
|
+
!Number.isFinite(usedCredits)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// API returns values in cents — convert to dollars for display
|
|
159
|
+
return `${formatCurrencyAmount(usedCredits / 100, extraUsage.currency)} / ${formatCurrencyAmount(monthlyLimit / 100, extraUsage.currency)}`;
|
|
160
|
+
}
|
|
161
|
+
/** Convert a utilization value to a 0-100 integer percent. Returns null for null/undefined input.
|
|
162
|
+
* Handles both 0-1 fractions (legacy) and 0-100 percentages (current API). */
|
|
163
|
+
export function toPercent(utilization) {
|
|
164
|
+
if (utilization == null)
|
|
165
|
+
return null;
|
|
166
|
+
return Math.min(100, Math.round(utilization < 1 ? utilization * 100 : utilization));
|
|
167
|
+
}
|
|
168
|
+
/** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */
|
|
169
|
+
export async function fetchWithTimeout(url, init, ms = 8000) {
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
172
|
+
try {
|
|
173
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function fetchClaudeQuota(token) {
|
|
180
|
+
const resp = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", {
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: `Bearer ${token}`,
|
|
183
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (!resp.ok)
|
|
187
|
+
throw new Error(`anthropic usage api returned ${resp.status}`);
|
|
188
|
+
const body = (await resp.json());
|
|
189
|
+
const windows = [];
|
|
190
|
+
if (body.five_hour != null) {
|
|
191
|
+
windows.push({
|
|
192
|
+
label: "Current session",
|
|
193
|
+
usedPercent: toPercent(body.five_hour.utilization),
|
|
194
|
+
resetsAt: body.five_hour.resets_at ?? null,
|
|
195
|
+
valueLabel: null,
|
|
196
|
+
detail: null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (body.seven_day != null) {
|
|
200
|
+
windows.push({
|
|
201
|
+
label: "Current week (all models)",
|
|
202
|
+
usedPercent: toPercent(body.seven_day.utilization),
|
|
203
|
+
resetsAt: body.seven_day.resets_at ?? null,
|
|
204
|
+
valueLabel: null,
|
|
205
|
+
detail: null,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (body.seven_day_sonnet != null) {
|
|
209
|
+
windows.push({
|
|
210
|
+
label: "Current week (Sonnet only)",
|
|
211
|
+
usedPercent: toPercent(body.seven_day_sonnet.utilization),
|
|
212
|
+
resetsAt: body.seven_day_sonnet.resets_at ?? null,
|
|
213
|
+
valueLabel: null,
|
|
214
|
+
detail: null,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (body.seven_day_opus != null) {
|
|
218
|
+
windows.push({
|
|
219
|
+
label: "Current week (Opus only)",
|
|
220
|
+
usedPercent: toPercent(body.seven_day_opus.utilization),
|
|
221
|
+
resetsAt: body.seven_day_opus.resets_at ?? null,
|
|
222
|
+
valueLabel: null,
|
|
223
|
+
detail: null,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (body.extra_usage != null) {
|
|
227
|
+
windows.push({
|
|
228
|
+
label: "Extra usage",
|
|
229
|
+
usedPercent: body.extra_usage.is_enabled === false ? null : toPercent(body.extra_usage.utilization),
|
|
230
|
+
resetsAt: null,
|
|
231
|
+
valueLabel: body.extra_usage.is_enabled === false
|
|
232
|
+
? "Not enabled"
|
|
233
|
+
: formatExtraUsageLabel(body.extra_usage),
|
|
234
|
+
detail: body.extra_usage.is_enabled === false
|
|
235
|
+
? "Extra usage not enabled"
|
|
236
|
+
: "Monthly extra usage pool",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return windows;
|
|
240
|
+
}
|
|
241
|
+
function usageOutputLooksRelevant(text) {
|
|
242
|
+
const normalized = normalizeForLabelSearch(text);
|
|
243
|
+
return normalized.includes("currentsession")
|
|
244
|
+
|| normalized.includes("currentweek")
|
|
245
|
+
|| normalized.includes("loadingusage")
|
|
246
|
+
|| normalized.includes("failedtoloadusagedata")
|
|
247
|
+
|| normalized.includes("tokenexpired")
|
|
248
|
+
|| normalized.includes("authenticationerror")
|
|
249
|
+
|| normalized.includes("ratelimited");
|
|
250
|
+
}
|
|
251
|
+
function usageOutputLooksComplete(text) {
|
|
252
|
+
const normalized = normalizeForLabelSearch(text);
|
|
253
|
+
if (normalized.includes("failedtoloadusagedata")
|
|
254
|
+
|| normalized.includes("tokenexpired")
|
|
255
|
+
|| normalized.includes("authenticationerror")
|
|
256
|
+
|| normalized.includes("ratelimited")) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return normalized.includes("currentsession")
|
|
260
|
+
&& (normalized.includes("currentweek") || normalized.includes("extrausage"))
|
|
261
|
+
&& /[0-9]{1,3}(?:\.[0-9]+)?%/i.test(text);
|
|
262
|
+
}
|
|
263
|
+
function extractUsageError(text) {
|
|
264
|
+
const lower = text.toLowerCase();
|
|
265
|
+
const compact = lower.replace(/\s+/g, "");
|
|
266
|
+
if (lower.includes("token_expired") || lower.includes("token has expired")) {
|
|
267
|
+
return "Claude CLI token expired. Run `claude login` to refresh.";
|
|
268
|
+
}
|
|
269
|
+
if (lower.includes("authentication_error")) {
|
|
270
|
+
return "Claude CLI authentication error. Run `claude login`.";
|
|
271
|
+
}
|
|
272
|
+
if (lower.includes("rate_limit_error") || lower.includes("rate limited") || compact.includes("ratelimited")) {
|
|
273
|
+
return "Claude CLI usage endpoint is rate limited right now. Please try again later.";
|
|
274
|
+
}
|
|
275
|
+
if (lower.includes("failed to load usage data") || compact.includes("failedtoloadusagedata")) {
|
|
276
|
+
return "Claude CLI could not load usage data. Open the CLI and retry `/usage`.";
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function percentFromLine(line) {
|
|
281
|
+
const match = line.match(/([0-9]{1,3}(?:\.[0-9]+)?)\s*%/i);
|
|
282
|
+
if (!match)
|
|
283
|
+
return null;
|
|
284
|
+
const rawValue = Number(match[1]);
|
|
285
|
+
if (!Number.isFinite(rawValue))
|
|
286
|
+
return null;
|
|
287
|
+
const clamped = Math.min(100, Math.max(0, rawValue));
|
|
288
|
+
const lower = line.toLowerCase();
|
|
289
|
+
if (lower.includes("remaining") || lower.includes("left") || lower.includes("available")) {
|
|
290
|
+
return Math.max(0, Math.min(100, Math.round(100 - clamped)));
|
|
291
|
+
}
|
|
292
|
+
return Math.round(clamped);
|
|
293
|
+
}
|
|
294
|
+
function isQuotaLabel(line) {
|
|
295
|
+
const normalized = normalizeForLabelSearch(line);
|
|
296
|
+
return normalized === "currentsession"
|
|
297
|
+
|| normalized === "currentweekallmodels"
|
|
298
|
+
|| normalized === "currentweeksonnetonly"
|
|
299
|
+
|| normalized === "currentweeksonnet"
|
|
300
|
+
|| normalized === "currentweekopusonly"
|
|
301
|
+
|| normalized === "currentweekopus"
|
|
302
|
+
|| normalized === "extrausage";
|
|
303
|
+
}
|
|
304
|
+
function canonicalQuotaLabel(line) {
|
|
305
|
+
switch (normalizeForLabelSearch(line)) {
|
|
306
|
+
case "currentsession":
|
|
307
|
+
return "Current session";
|
|
308
|
+
case "currentweekallmodels":
|
|
309
|
+
return "Current week (all models)";
|
|
310
|
+
case "currentweeksonnetonly":
|
|
311
|
+
case "currentweeksonnet":
|
|
312
|
+
return "Current week (Sonnet only)";
|
|
313
|
+
case "currentweekopusonly":
|
|
314
|
+
case "currentweekopus":
|
|
315
|
+
return "Current week (Opus only)";
|
|
316
|
+
case "extrausage":
|
|
317
|
+
return "Extra usage";
|
|
318
|
+
default:
|
|
319
|
+
return line;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function formatClaudeCliDetail(label, lines) {
|
|
323
|
+
const normalizedLabel = normalizeForLabelSearch(label);
|
|
324
|
+
if (normalizedLabel === "extrausage") {
|
|
325
|
+
const compact = lines.join(" ").replace(/\s+/g, "").toLowerCase();
|
|
326
|
+
if (compact.includes("extrausagenotenabled")) {
|
|
327
|
+
return "Extra usage not enabled • /extra-usage to enable";
|
|
328
|
+
}
|
|
329
|
+
const firstLine = lines.find((line) => line.trim().length > 0) ?? null;
|
|
330
|
+
return firstLine;
|
|
331
|
+
}
|
|
332
|
+
const resetLine = lines.find((line) => /^resets/i.test(line) || normalizeForLabelSearch(line).startsWith("resets"));
|
|
333
|
+
if (!resetLine)
|
|
334
|
+
return null;
|
|
335
|
+
return resetLine
|
|
336
|
+
.replace(/^Resets/i, "Resets ")
|
|
337
|
+
.replace(/([A-Z][a-z]{2})(\d)/g, "$1 $2")
|
|
338
|
+
.replace(/(\d)at(\d)/g, "$1 at $2")
|
|
339
|
+
.replace(/(am|pm)\(/gi, "$1 (")
|
|
340
|
+
.replace(/([A-Za-z])\(/g, "$1 (")
|
|
341
|
+
.replace(/\s+/g, " ")
|
|
342
|
+
.trim();
|
|
343
|
+
}
|
|
344
|
+
export function parseClaudeCliUsageText(text) {
|
|
345
|
+
const cleaned = trimToLatestUsagePanel(cleanTerminalText(text)) ?? cleanTerminalText(text);
|
|
346
|
+
const usageError = extractUsageError(cleaned);
|
|
347
|
+
if (usageError)
|
|
348
|
+
throw new Error(usageError);
|
|
349
|
+
const lines = cleaned
|
|
350
|
+
.split("\n")
|
|
351
|
+
.map((line) => line.trim())
|
|
352
|
+
.filter((line) => line.length > 0);
|
|
353
|
+
const sections = [];
|
|
354
|
+
let current = null;
|
|
355
|
+
for (const line of lines) {
|
|
356
|
+
if (isQuotaLabel(line)) {
|
|
357
|
+
if (current)
|
|
358
|
+
sections.push(current);
|
|
359
|
+
current = { label: canonicalQuotaLabel(line), lines: [] };
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (current)
|
|
363
|
+
current.lines.push(line);
|
|
364
|
+
}
|
|
365
|
+
if (current)
|
|
366
|
+
sections.push(current);
|
|
367
|
+
const windows = sections.map((section) => {
|
|
368
|
+
const usedPercent = section.lines.map(percentFromLine).find((value) => value != null) ?? null;
|
|
369
|
+
return {
|
|
370
|
+
label: section.label,
|
|
371
|
+
usedPercent,
|
|
372
|
+
resetsAt: null,
|
|
373
|
+
valueLabel: null,
|
|
374
|
+
detail: formatClaudeCliDetail(section.label, section.lines),
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
if (!windows.some((window) => normalizeForLabelSearch(window.label) === "currentsession")) {
|
|
378
|
+
throw new Error("Could not parse Claude CLI usage output.");
|
|
379
|
+
}
|
|
380
|
+
return windows;
|
|
381
|
+
}
|
|
382
|
+
function quoteForShell(value) {
|
|
383
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
384
|
+
}
|
|
385
|
+
function buildClaudeCliShellProbeCommand() {
|
|
386
|
+
const feed = "(sleep 2; printf '/usage\\r'; sleep 6; printf '\\033'; sleep 1; printf '\\003')";
|
|
387
|
+
const claudeCommand = "claude --tools \"\"";
|
|
388
|
+
if (process.platform === "darwin") {
|
|
389
|
+
return `${feed} | script -q /dev/null ${claudeCommand}`;
|
|
390
|
+
}
|
|
391
|
+
return `${feed} | script -q -e -f -c ${quoteForShell(claudeCommand)} /dev/null`;
|
|
392
|
+
}
|
|
393
|
+
export async function captureClaudeCliUsageText(timeoutMs = 12_000) {
|
|
394
|
+
const command = buildClaudeCliShellProbeCommand();
|
|
395
|
+
try {
|
|
396
|
+
const { stdout, stderr } = await execFileAsync("sh", ["-c", command], {
|
|
397
|
+
env: createClaudeQuotaEnv(),
|
|
398
|
+
timeout: timeoutMs,
|
|
399
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
400
|
+
});
|
|
401
|
+
const output = `${stdout}${stderr}`;
|
|
402
|
+
const cleaned = cleanTerminalText(output);
|
|
403
|
+
if (usageOutputLooksComplete(cleaned))
|
|
404
|
+
return output;
|
|
405
|
+
throw new Error("Claude CLI usage probe ended before rendering usage.");
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
const stdout = typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string"
|
|
409
|
+
? error.stdout
|
|
410
|
+
: "";
|
|
411
|
+
const stderr = typeof error === "object" && error !== null && "stderr" in error && typeof error.stderr === "string"
|
|
412
|
+
? error.stderr
|
|
413
|
+
: "";
|
|
414
|
+
const output = `${stdout}${stderr}`;
|
|
415
|
+
const cleaned = cleanTerminalText(output);
|
|
416
|
+
if (usageOutputLooksComplete(cleaned))
|
|
417
|
+
return output;
|
|
418
|
+
if (usageOutputLooksRelevant(cleaned)) {
|
|
419
|
+
throw new Error("Claude CLI usage probe ended before rendering usage.");
|
|
420
|
+
}
|
|
421
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
export async function fetchClaudeCliQuota() {
|
|
425
|
+
const rawText = await captureClaudeCliUsageText();
|
|
426
|
+
return parseClaudeCliUsageText(rawText);
|
|
427
|
+
}
|
|
428
|
+
function formatProviderError(source, error) {
|
|
429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
return `${source}: ${message}`;
|
|
431
|
+
}
|
|
432
|
+
export async function getQuotaWindows() {
|
|
433
|
+
if (process.env.CLAUDE_CODE_USE_BEDROCK === "1" ||
|
|
434
|
+
process.env.CLAUDE_CODE_USE_BEDROCK === "true" ||
|
|
435
|
+
hasNonEmptyProcessEnv("ANTHROPIC_BEDROCK_BASE_URL")) {
|
|
436
|
+
return { provider: "anthropic", source: "bedrock", ok: true, windows: [] };
|
|
437
|
+
}
|
|
438
|
+
const authStatus = await readClaudeAuthStatus();
|
|
439
|
+
const authDescription = describeClaudeSubscriptionAuth(authStatus);
|
|
440
|
+
const token = await readClaudeToken();
|
|
441
|
+
const errors = [];
|
|
442
|
+
if (token) {
|
|
443
|
+
try {
|
|
444
|
+
const windows = await fetchClaudeQuota(token);
|
|
445
|
+
return { provider: "anthropic", source: CLAUDE_USAGE_SOURCE_OAUTH, ok: true, windows };
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
errors.push(formatProviderError("Anthropic OAuth usage", error));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const windows = await fetchClaudeCliQuota();
|
|
453
|
+
return { provider: "anthropic", source: CLAUDE_USAGE_SOURCE_CLI, ok: true, windows };
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
errors.push(formatProviderError("Claude CLI /usage", error));
|
|
457
|
+
}
|
|
458
|
+
if (hasNonEmptyProcessEnv("ANTHROPIC_API_KEY") && !authDescription) {
|
|
459
|
+
return {
|
|
460
|
+
provider: "anthropic",
|
|
461
|
+
ok: false,
|
|
462
|
+
error: errors[0]
|
|
463
|
+
?? "ANTHROPIC_API_KEY is set and no local Claude subscription session is available for quota polling",
|
|
464
|
+
windows: [],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (authDescription) {
|
|
468
|
+
return {
|
|
469
|
+
provider: "anthropic",
|
|
470
|
+
ok: false,
|
|
471
|
+
error: errors.length > 0
|
|
472
|
+
? `${authDescription}, but quota polling failed (${errors.join("; ")})`
|
|
473
|
+
: `${authDescription}, but Alvax could not load subscription quota data`,
|
|
474
|
+
windows: [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
provider: "anthropic",
|
|
479
|
+
ok: false,
|
|
480
|
+
error: errors[0] ?? "no local claude auth token",
|
|
481
|
+
windows: [],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
//# sourceMappingURL=quota.js.map
|