ftown 0.25.2 → 0.25.3
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/command-rpc.d.ts +3 -0
- package/dist/command-rpc.js +41 -0
- package/dist/command-rpc.js.map +1 -1
- package/dist/ftown-cli.js +5 -2
- package/dist/ftown-cli.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/quota/agy.d.ts +16 -0
- package/dist/quota/agy.js +287 -0
- package/dist/quota/agy.js.map +1 -0
- package/dist/quota/annotate.d.ts +5 -0
- package/dist/quota/annotate.js +56 -0
- package/dist/quota/annotate.js.map +1 -0
- package/dist/quota/claude.d.ts +27 -0
- package/dist/quota/claude.js +265 -0
- package/dist/quota/claude.js.map +1 -0
- package/dist/quota/codex.d.ts +25 -0
- package/dist/quota/codex.js +378 -0
- package/dist/quota/codex.js.map +1 -0
- package/dist/quota/format.d.ts +6 -0
- package/dist/quota/format.js +113 -0
- package/dist/quota/format.js.map +1 -0
- package/dist/quota/grok.d.ts +97 -0
- package/dist/quota/grok.js +563 -0
- package/dist/quota/grok.js.map +1 -0
- package/dist/quota/models-agy.d.ts +10 -0
- package/dist/quota/models-agy.js +144 -0
- package/dist/quota/models-agy.js.map +1 -0
- package/dist/quota/models-claude.d.ts +10 -0
- package/dist/quota/models-claude.js +227 -0
- package/dist/quota/models-claude.js.map +1 -0
- package/dist/quota/models-codex.d.ts +18 -0
- package/dist/quota/models-codex.js +305 -0
- package/dist/quota/models-codex.js.map +1 -0
- package/dist/quota/models-grok.d.ts +19 -0
- package/dist/quota/models-grok.js +272 -0
- package/dist/quota/models-grok.js.map +1 -0
- package/dist/quota/models-muse.d.ts +13 -0
- package/dist/quota/models-muse.js +200 -0
- package/dist/quota/models-muse.js.map +1 -0
- package/dist/quota/models-opencode.d.ts +51 -0
- package/dist/quota/models-opencode.js +400 -0
- package/dist/quota/models-opencode.js.map +1 -0
- package/dist/quota/muse.d.ts +38 -0
- package/dist/quota/muse.js +225 -0
- package/dist/quota/muse.js.map +1 -0
- package/dist/quota/opencode.d.ts +7 -0
- package/dist/quota/opencode.js +47 -0
- package/dist/quota/opencode.js.map +1 -0
- package/dist/quota/routing-config.d.ts +18 -0
- package/dist/quota/routing-config.js +438 -0
- package/dist/quota/routing-config.js.map +1 -0
- package/dist/quota/routing-service.d.ts +24 -0
- package/dist/quota/routing-service.js +118 -0
- package/dist/quota/routing-service.js.map +1 -0
- package/dist/quota/routing.d.ts +10 -0
- package/dist/quota/routing.js +180 -0
- package/dist/quota/routing.js.map +1 -0
- package/dist/quota/types.d.ts +304 -0
- package/dist/quota/types.js +45 -0
- package/dist/quota/types.js.map +1 -0
- package/dist/quota-cli.d.ts +80 -0
- package/dist/quota-cli.js +462 -0
- package/dist/quota-cli.js.map +1 -0
- package/dist/security/bridge/permissions.js +4 -2
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +3 -2
- package/skills/ftown/SKILL.md +2 -1
- package/skills/ftown/references/orchestrator.md +5 -0
- package/skills/ftown/references/quota.md +177 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, mkdtemp, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Google Antigravity (`agy` CLI) subscription quota provider.
|
|
8
|
+
*
|
|
9
|
+
* `agy -p /usage --output-format json` is handled locally by the CLI: it makes
|
|
10
|
+
* no model turn and spends no quota. The JSON envelope carries the limit
|
|
11
|
+
* buckets under `command.data.groups[].buckets[]`. The command runs in an empty
|
|
12
|
+
* scratch directory, so no project files are involved.
|
|
13
|
+
*/
|
|
14
|
+
const LOCAL_BIN_SEGMENTS = ['.local', 'bin', 'agy'];
|
|
15
|
+
const BINARY_NAME = 'agy';
|
|
16
|
+
const USAGE_ARGS = ['-p', '/usage', '--output-format', 'json'];
|
|
17
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
18
|
+
const FIVE_HOUR_MINUTES = 300;
|
|
19
|
+
const WEEKLY_MINUTES = 10080;
|
|
20
|
+
const NOT_FOUND_MESSAGE = 'agy CLI not found';
|
|
21
|
+
const NO_BUCKETS_MESSAGE = 'agy reported no usage buckets';
|
|
22
|
+
const UNEXPECTED_OUTPUT_MESSAGE = 'unexpected output from `agy -p /usage`';
|
|
23
|
+
const TIMED_OUT_MESSAGE = '`agy -p /usage` timed out';
|
|
24
|
+
const FAILED_TO_START_MESSAGE = '`agy -p /usage` failed to start';
|
|
25
|
+
const AUTH_MESSAGE = 'agy login problem: run `agy` and sign in';
|
|
26
|
+
const TURN_MESSAGE = 'agy ran a model turn for /usage (unexpected)';
|
|
27
|
+
const AUTH_PATTERN = /\b(log ?in|sign ?in|unauthori[sz]ed|unauthenticated|authenticat\w*)\b/i;
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function nonEmptyString(value) {
|
|
32
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
33
|
+
}
|
|
34
|
+
function observedNow(options) {
|
|
35
|
+
try {
|
|
36
|
+
return options.now().toISOString();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return new Date().toISOString();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function failureQuota(status, message, observedAt) {
|
|
43
|
+
return {
|
|
44
|
+
provider: 'agy',
|
|
45
|
+
status,
|
|
46
|
+
plan: null,
|
|
47
|
+
windows: [],
|
|
48
|
+
source: 'live',
|
|
49
|
+
observedAt,
|
|
50
|
+
message,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** True only for a regular file that the current user can execute. Never throws. */
|
|
54
|
+
async function isExecutableFile(path) {
|
|
55
|
+
try {
|
|
56
|
+
await access(path, constants.X_OK);
|
|
57
|
+
return (await stat(path)).isFile();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the agy binary to an absolute path: first `<homeDir>/.local/bin/agy`
|
|
65
|
+
* (skipped when homeDir is empty or not absolute), then `agy` in each PATH
|
|
66
|
+
* entry. Empty and relative PATH entries are skipped. A candidate must be an
|
|
67
|
+
* executable regular file. Returns null when none matches. Never throws.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveAgyBinary(homeDir, pathValue) {
|
|
70
|
+
const candidates = [];
|
|
71
|
+
if (homeDir.length > 0 && isAbsolute(homeDir)) {
|
|
72
|
+
candidates.push(join(homeDir, ...LOCAL_BIN_SEGMENTS));
|
|
73
|
+
}
|
|
74
|
+
for (const dir of (pathValue ?? '').split(delimiter)) {
|
|
75
|
+
if (dir.length === 0 || !isAbsolute(dir))
|
|
76
|
+
continue;
|
|
77
|
+
candidates.push(join(dir, BINARY_NAME));
|
|
78
|
+
}
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
if (await isExecutableFile(candidate))
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/** Percent used from remaining_fraction: (1 - remaining) * 100, one decimal, clamped to 0..100. */
|
|
86
|
+
function usedPercentOf(remaining) {
|
|
87
|
+
if (typeof remaining !== 'number' || !Number.isFinite(remaining))
|
|
88
|
+
return null;
|
|
89
|
+
const used = Math.round((1 - remaining) * 1000) / 10;
|
|
90
|
+
return Math.min(100, Math.max(0, used));
|
|
91
|
+
}
|
|
92
|
+
function isoOf(value) {
|
|
93
|
+
const text = nonEmptyString(value);
|
|
94
|
+
if (text === null)
|
|
95
|
+
return null;
|
|
96
|
+
const ms = Date.parse(text);
|
|
97
|
+
return Number.isNaN(ms) ? null : new Date(ms).toISOString();
|
|
98
|
+
}
|
|
99
|
+
function rankOf(usedPercent) {
|
|
100
|
+
return usedPercent === null ? -1 : usedPercent;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Pure: maps the parsed JSON envelope of `agy -p /usage --output-format json`
|
|
104
|
+
* onto a ProviderQuota. Windows keep the most used bucket per kind; scopedLimits
|
|
105
|
+
* list every usable bucket. Never throws.
|
|
106
|
+
*/
|
|
107
|
+
export function parseAgyUsage(body, observedAt) {
|
|
108
|
+
const command = isRecord(body) ? body.command : undefined;
|
|
109
|
+
const data = isRecord(command) ? command.data : undefined;
|
|
110
|
+
const groups = isRecord(data) ? data.groups : undefined;
|
|
111
|
+
if (!isRecord(body) || body.status !== 'SUCCESS' || !Array.isArray(groups)) {
|
|
112
|
+
return failureQuota('error', UNEXPECTED_OUTPUT_MESSAGE, observedAt);
|
|
113
|
+
}
|
|
114
|
+
const entries = [];
|
|
115
|
+
groups.forEach((group, groupIndex) => {
|
|
116
|
+
if (!isRecord(group) || !Array.isArray(group.buckets))
|
|
117
|
+
return;
|
|
118
|
+
const groupName = nonEmptyString(group.name);
|
|
119
|
+
for (const bucket of group.buckets) {
|
|
120
|
+
if (!isRecord(bucket))
|
|
121
|
+
continue;
|
|
122
|
+
const shape = bucket.window === '5h'
|
|
123
|
+
? { kind: 'five_hour', windowMinutes: FIVE_HOUR_MINUTES }
|
|
124
|
+
: bucket.window === 'weekly'
|
|
125
|
+
? { kind: 'weekly', windowMinutes: WEEKLY_MINUTES }
|
|
126
|
+
: null;
|
|
127
|
+
if (shape === null)
|
|
128
|
+
continue;
|
|
129
|
+
const label = groupName ?? nonEmptyString(bucket.name) ?? nonEmptyString(bucket.id) ?? 'unknown';
|
|
130
|
+
entries.push({
|
|
131
|
+
kind: shape.kind,
|
|
132
|
+
windowMinutes: shape.windowMinutes,
|
|
133
|
+
groupIndex,
|
|
134
|
+
label,
|
|
135
|
+
usedPercent: usedPercentOf(bucket.remaining_fraction),
|
|
136
|
+
resetsAt: isoOf(bucket.reset_time),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const windows = [];
|
|
141
|
+
for (const kind of ['five_hour', 'weekly']) {
|
|
142
|
+
const ofKind = entries.filter((entry) => entry.kind === kind);
|
|
143
|
+
if (ofKind.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
let best = ofKind[0];
|
|
146
|
+
for (const entry of ofKind.slice(1)) {
|
|
147
|
+
if (rankOf(entry.usedPercent) > rankOf(best.usedPercent))
|
|
148
|
+
best = entry;
|
|
149
|
+
}
|
|
150
|
+
const groupCount = new Set(ofKind.map((entry) => entry.groupIndex)).size;
|
|
151
|
+
windows.push({
|
|
152
|
+
kind,
|
|
153
|
+
usedPercent: best.usedPercent,
|
|
154
|
+
resetsAt: best.resetsAt,
|
|
155
|
+
windowMinutes: best.windowMinutes,
|
|
156
|
+
note: groupCount >= 2 ? `tightest: ${best.label}` : null,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const scopedLimits = entries.map((entry) => ({
|
|
160
|
+
scope: entry.label,
|
|
161
|
+
kind: entry.kind,
|
|
162
|
+
usedPercent: entry.usedPercent,
|
|
163
|
+
resetsAt: entry.resetsAt,
|
|
164
|
+
}));
|
|
165
|
+
const hasNumber = windows.some((window) => window.usedPercent !== null);
|
|
166
|
+
let message = hasNumber ? null : NO_BUCKETS_MESSAGE;
|
|
167
|
+
if (typeof body.num_turns === 'number' && body.num_turns > 0)
|
|
168
|
+
message = TURN_MESSAGE;
|
|
169
|
+
const quota = {
|
|
170
|
+
provider: 'agy',
|
|
171
|
+
status: hasNumber ? 'ok' : 'no_data',
|
|
172
|
+
plan: null,
|
|
173
|
+
windows,
|
|
174
|
+
source: 'live',
|
|
175
|
+
observedAt,
|
|
176
|
+
message,
|
|
177
|
+
};
|
|
178
|
+
if (scopedLimits.length > 0)
|
|
179
|
+
quota.scopedLimits = scopedLimits;
|
|
180
|
+
return quota;
|
|
181
|
+
}
|
|
182
|
+
/** Pure: extracts the JSON object from stdout: the whole text, else the last line starting with `{`. */
|
|
183
|
+
function extractUsageJson(stdout) {
|
|
184
|
+
const trimmed = stdout.trim();
|
|
185
|
+
try {
|
|
186
|
+
const whole = JSON.parse(trimmed);
|
|
187
|
+
if (isRecord(whole))
|
|
188
|
+
return whole;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// fall through to line-by-line extraction
|
|
192
|
+
}
|
|
193
|
+
const lines = trimmed.split(/\r?\n/).map((line) => line.trim());
|
|
194
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
195
|
+
if (!lines[i].startsWith('{'))
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(lines[i]);
|
|
199
|
+
if (isRecord(parsed))
|
|
200
|
+
return parsed;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// not a JSON line; keep scanning upward
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
/** One `agy -p /usage` run in `cwd` (no shell). Resolves with a ProviderQuota; never rejects. */
|
|
209
|
+
function execUsage(binary, cwd, timeoutMs, observedAt) {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
try {
|
|
212
|
+
execFile(binary, [...USAGE_ARGS], {
|
|
213
|
+
cwd,
|
|
214
|
+
timeout: timeoutMs,
|
|
215
|
+
killSignal: 'SIGKILL',
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
218
|
+
windowsHide: true,
|
|
219
|
+
}, (error, stdout, stderr) => {
|
|
220
|
+
try {
|
|
221
|
+
if (error) {
|
|
222
|
+
const code = error.code;
|
|
223
|
+
const killed = error.killed === true;
|
|
224
|
+
if (killed) {
|
|
225
|
+
resolve(failureQuota('error', TIMED_OUT_MESSAGE, observedAt));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (code === 'ENOENT') {
|
|
229
|
+
resolve(failureQuota('unavailable', NOT_FOUND_MESSAGE, observedAt));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (AUTH_PATTERN.test(String(stderr))) {
|
|
233
|
+
resolve(failureQuota('auth', AUTH_MESSAGE, observedAt));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const shown = typeof code === 'number' ? String(code) : 'unknown';
|
|
237
|
+
resolve(failureQuota('error', `\`agy -p /usage\` failed (exit ${shown})`, observedAt));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const parsed = extractUsageJson(String(stdout));
|
|
241
|
+
if (parsed === undefined) {
|
|
242
|
+
resolve(failureQuota('error', UNEXPECTED_OUTPUT_MESSAGE, observedAt));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
resolve(parseAgyUsage(parsed, observedAt));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
resolve(failureQuota('error', UNEXPECTED_OUTPUT_MESSAGE, observedAt));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
resolve(failureQuota('error', FAILED_TO_START_MESSAGE, observedAt));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** Runs /usage inside a fresh scratch directory and removes it afterwards. Never rejects. */
|
|
258
|
+
async function runAgyUsage(binary, timeoutMs, observedAt) {
|
|
259
|
+
let scratch = null;
|
|
260
|
+
try {
|
|
261
|
+
scratch = await mkdtemp(join(tmpdir(), 'ftown-agy-usage-'));
|
|
262
|
+
return await execUsage(binary, scratch, timeoutMs, observedAt);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return failureQuota('error', FAILED_TO_START_MESSAGE, observedAt);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if (scratch !== null) {
|
|
269
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Reads agy's own `/usage` report non-interactively. Never throws or rejects. */
|
|
274
|
+
export const fetchAgyQuota = async (options) => {
|
|
275
|
+
const observedAt = observedNow(options);
|
|
276
|
+
let binary = null;
|
|
277
|
+
try {
|
|
278
|
+
binary = await resolveAgyBinary(options.homeDir, process.env.PATH);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
binary = null;
|
|
282
|
+
}
|
|
283
|
+
if (binary === null)
|
|
284
|
+
return failureQuota('unavailable', NOT_FOUND_MESSAGE, observedAt);
|
|
285
|
+
return runAgyUsage(binary, options.timeoutMs, observedAt);
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=agy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agy.js","sourceRoot":"","sources":["../../src/quota/agy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AnnotatedPick, AnnotatedTier, ModelRoutingConfig, ProviderModels, ProviderQuota, ProviderRouting, RoutingPick } from './types.js';
|
|
2
|
+
/** Annotate one pick with the provider's live availability, scoped limits and model check. */
|
|
3
|
+
export declare function annotatePick(pick: RoutingPick, quota: readonly ProviderQuota[], routing: readonly ProviderRouting[], models: readonly ProviderModels[]): AnnotatedPick;
|
|
4
|
+
/** Annotate every tier of the user's routing config, keeping tier and pick order. */
|
|
5
|
+
export declare function annotateTiers(config: ModelRoutingConfig, quota: readonly ProviderQuota[], routing: readonly ProviderRouting[], models: readonly ProviderModels[]): AnnotatedTier[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** First entry for a provider, so duplicate provider rows resolve to the first one. */
|
|
2
|
+
function firstFor(list, provider) {
|
|
3
|
+
return list.find((entry) => entry.provider === provider);
|
|
4
|
+
}
|
|
5
|
+
/** Annotate one pick with the provider's live availability, scoped limits and model check. */
|
|
6
|
+
export function annotatePick(pick, quota, routing, models) {
|
|
7
|
+
const route = firstFor(routing, pick.provider);
|
|
8
|
+
const quotaRow = firstFor(quota, pick.provider);
|
|
9
|
+
const modelList = firstFor(models, pick.provider);
|
|
10
|
+
const availability = route ? route.availability : 'unknown';
|
|
11
|
+
const leftPercent = route ? route.leftPercent : null;
|
|
12
|
+
const resetsAt = route ? route.resetsAt : null;
|
|
13
|
+
const scopedLimits = (quotaRow?.scopedLimits ?? []).map((limit) => ({ ...limit }));
|
|
14
|
+
let modelName = null;
|
|
15
|
+
let problem = null;
|
|
16
|
+
const checked = modelList !== undefined && modelList.status === 'ok';
|
|
17
|
+
if (modelList && modelList.status === 'ok') {
|
|
18
|
+
const model = modelList.models.find((candidate) => candidate.id === pick.model);
|
|
19
|
+
if (!model) {
|
|
20
|
+
problem = `${pick.provider} no longer offers model "${pick.model}"`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
modelName = model.name ?? null;
|
|
24
|
+
if (pick.effort !== null) {
|
|
25
|
+
if (model.efforts.length === 0) {
|
|
26
|
+
problem = `model "${pick.model}" takes no effort setting`;
|
|
27
|
+
}
|
|
28
|
+
else if (!model.efforts.includes(pick.effort)) {
|
|
29
|
+
problem = `model "${pick.model}" does not support effort "${pick.effort}"`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
provider: pick.provider,
|
|
36
|
+
model: pick.model,
|
|
37
|
+
effort: pick.effort,
|
|
38
|
+
modelName,
|
|
39
|
+
availability,
|
|
40
|
+
leftPercent,
|
|
41
|
+
resetsAt,
|
|
42
|
+
scopedLimits,
|
|
43
|
+
problem,
|
|
44
|
+
checked,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Annotate every tier of the user's routing config, keeping tier and pick order. */
|
|
48
|
+
export function annotateTiers(config, quota, routing, models) {
|
|
49
|
+
return config.tiers.map((tier) => ({
|
|
50
|
+
name: tier.name,
|
|
51
|
+
description: tier.description,
|
|
52
|
+
picks: tier.picks.map((pick) => annotatePick(pick, quota, routing, models)),
|
|
53
|
+
guidance: typeof tier.guidance === 'string' ? tier.guidance : '',
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=annotate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotate.js","sourceRoot":"","sources":["../../src/quota/annotate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProviderQuota, QuotaFetcher } from './types.js';
|
|
2
|
+
export interface ClaudeCredentials {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
expiresAt: number | null;
|
|
5
|
+
plan: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pure mapping from the body of GET /api/oauth/usage to a ProviderQuota.
|
|
9
|
+
* Does no I/O; `plan` and `observedAt` are passed through as given.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseClaudeUsageResponse(body: unknown, plan: string | null, observedAt: string): ProviderQuota;
|
|
12
|
+
/**
|
|
13
|
+
* Pure parser for the Claude Code credentials JSON (keychain or file).
|
|
14
|
+
* A token that is not printable ASCII without spaces is treated as absent,
|
|
15
|
+
* so it can never reach an HTTP header or an error message.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseClaudeCredentials(raw: string): ClaudeCredentials | null;
|
|
18
|
+
/**
|
|
19
|
+
* One-line, bounded, secret-free reason for a caught error. Every occurrence
|
|
20
|
+
* of `secret` is replaced before truncation, so no part of it can survive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function safeReason(error: unknown, secret: string | null): string;
|
|
23
|
+
/**
|
|
24
|
+
* Live fetch of the Claude subscription usage windows. Never throws, never
|
|
25
|
+
* prints, never refreshes the OAuth token and never writes to the keychain.
|
|
26
|
+
*/
|
|
27
|
+
export declare const fetchClaudeQuota: QuotaFetcher;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
5
|
+
const OAUTH_BETA = 'oauth-2025-04-20';
|
|
6
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
7
|
+
const FALLBACK_CLAUDE_VERSION = '2.1.323';
|
|
8
|
+
const VERSION_TIMEOUT_MS = 2000;
|
|
9
|
+
const WEEKLY_MINUTES = 10080;
|
|
10
|
+
const FIVE_HOUR_MINUTES = 300;
|
|
11
|
+
const EXPIRED_MESSAGE = 'Claude login expired: run `claude` once to refresh it';
|
|
12
|
+
const NOT_SIGNED_IN_MESSAGE = 'not signed in: run `claude` and log in';
|
|
13
|
+
const UNEXPECTED_BODY_MESSAGE = 'unexpected response from Anthropic usage endpoint';
|
|
14
|
+
const MAX_REASON_LENGTH = 120;
|
|
15
|
+
const TOKEN_PATTERN = /^[\x21-\x7e]+$/;
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function normaliseResetsAt(raw) {
|
|
20
|
+
if (typeof raw !== 'string')
|
|
21
|
+
return null;
|
|
22
|
+
// Anthropic sends microseconds ("...00.043805+00:00"); keep milliseconds only.
|
|
23
|
+
const trimmed = raw.replace(/(\.\d{3})\d+(?=[Z+-]|$)/, '$1');
|
|
24
|
+
const date = new Date(trimmed);
|
|
25
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
26
|
+
}
|
|
27
|
+
function toWindow(kind, raw, windowMinutes) {
|
|
28
|
+
const utilization = raw.utilization;
|
|
29
|
+
return {
|
|
30
|
+
kind,
|
|
31
|
+
usedPercent: typeof utilization === 'number' && Number.isFinite(utilization) ? utilization : null,
|
|
32
|
+
resetsAt: normaliseResetsAt(raw.resets_at),
|
|
33
|
+
windowMinutes,
|
|
34
|
+
note: null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Per-model limits from the body's `limits` array. Only entries with a model
|
|
39
|
+
* scope are kept (unscoped entries duplicate the main windows). Malformed
|
|
40
|
+
* input yields an empty list and never throws.
|
|
41
|
+
*/
|
|
42
|
+
function parseScopedLimits(raw) {
|
|
43
|
+
if (!Array.isArray(raw))
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const entry of raw) {
|
|
47
|
+
if (!isRecord(entry) || !isRecord(entry.scope) || !isRecord(entry.scope.model))
|
|
48
|
+
continue;
|
|
49
|
+
const model = entry.scope.model;
|
|
50
|
+
const name = [model.display_name, model.id].find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
|
|
51
|
+
if (name === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
const kind = typeof entry.kind === 'string' && (entry.kind.startsWith('five_hour') || entry.kind.startsWith('session'))
|
|
54
|
+
? 'five_hour'
|
|
55
|
+
: 'weekly';
|
|
56
|
+
const percent = entry.percent;
|
|
57
|
+
out.push({
|
|
58
|
+
scope: name,
|
|
59
|
+
kind,
|
|
60
|
+
usedPercent: typeof percent === 'number' && Number.isFinite(percent) ? percent : null,
|
|
61
|
+
resetsAt: normaliseResetsAt(entry.resets_at),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Pure mapping from the body of GET /api/oauth/usage to a ProviderQuota.
|
|
68
|
+
* Does no I/O; `plan` and `observedAt` are passed through as given.
|
|
69
|
+
*/
|
|
70
|
+
export function parseClaudeUsageResponse(body, plan, observedAt) {
|
|
71
|
+
if (!isRecord(body)) {
|
|
72
|
+
return {
|
|
73
|
+
provider: 'claude',
|
|
74
|
+
status: 'error',
|
|
75
|
+
plan,
|
|
76
|
+
windows: [],
|
|
77
|
+
source: 'live',
|
|
78
|
+
observedAt,
|
|
79
|
+
message: UNEXPECTED_BODY_MESSAGE,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const windows = [];
|
|
83
|
+
const fiveHourRaw = body.five_hour;
|
|
84
|
+
const fiveHourActive = isRecord(fiveHourRaw);
|
|
85
|
+
if (fiveHourActive) {
|
|
86
|
+
windows.push(toWindow('five_hour', fiveHourRaw, FIVE_HOUR_MINUTES));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
windows.push({
|
|
90
|
+
kind: 'five_hour',
|
|
91
|
+
usedPercent: null,
|
|
92
|
+
resetsAt: null,
|
|
93
|
+
windowMinutes: FIVE_HOUR_MINUTES,
|
|
94
|
+
note: 'not reported',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const weeklyRaw = body.seven_day;
|
|
98
|
+
const weeklyPresent = isRecord(weeklyRaw);
|
|
99
|
+
if (weeklyPresent) {
|
|
100
|
+
windows.push(toWindow('weekly', weeklyRaw, WEEKLY_MINUTES));
|
|
101
|
+
}
|
|
102
|
+
const hasNumeric = windows.some((w) => w.usedPercent !== null);
|
|
103
|
+
const idleFiveHour = !fiveHourActive;
|
|
104
|
+
const hasData = hasNumeric || (idleFiveHour && weeklyPresent);
|
|
105
|
+
const status = hasData ? 'ok' : 'no_data';
|
|
106
|
+
const scopedLimits = parseScopedLimits(body.limits);
|
|
107
|
+
return {
|
|
108
|
+
provider: 'claude',
|
|
109
|
+
status,
|
|
110
|
+
plan,
|
|
111
|
+
windows,
|
|
112
|
+
source: 'live',
|
|
113
|
+
observedAt,
|
|
114
|
+
message: status === 'no_data' ? 'no usage window reported by Anthropic' : null,
|
|
115
|
+
...(scopedLimits.length > 0 ? { scopedLimits } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pure parser for the Claude Code credentials JSON (keychain or file).
|
|
120
|
+
* A token that is not printable ASCII without spaces is treated as absent,
|
|
121
|
+
* so it can never reach an HTTP header or an error message.
|
|
122
|
+
*/
|
|
123
|
+
export function parseClaudeCredentials(raw) {
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (!isRecord(parsed) || !isRecord(parsed.claudeAiOauth))
|
|
132
|
+
return null;
|
|
133
|
+
const oauth = parsed.claudeAiOauth;
|
|
134
|
+
if (typeof oauth.accessToken !== 'string' || !TOKEN_PATTERN.test(oauth.accessToken))
|
|
135
|
+
return null;
|
|
136
|
+
const expiresAt = typeof oauth.expiresAt === 'number' && Number.isFinite(oauth.expiresAt) ? oauth.expiresAt : null;
|
|
137
|
+
const plan = typeof oauth.subscriptionType === 'string' ? oauth.subscriptionType : null;
|
|
138
|
+
return { accessToken: oauth.accessToken, expiresAt, plan };
|
|
139
|
+
}
|
|
140
|
+
function readKeychain(timeoutMs) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
try {
|
|
143
|
+
execFile('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'], { timeout: timeoutMs, encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
144
|
+
resolve(error ? null : String(stdout));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
resolve(null);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function readCredentialsFile(homeDir) {
|
|
153
|
+
try {
|
|
154
|
+
return await readFile(join(homeDir, '.claude', '.credentials.json'), 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function loadCredentials(homeDir, timeoutMs) {
|
|
161
|
+
if (process.platform === 'darwin') {
|
|
162
|
+
const fromKeychain = await readKeychain(timeoutMs);
|
|
163
|
+
const parsed = fromKeychain === null ? null : parseClaudeCredentials(fromKeychain);
|
|
164
|
+
if (parsed)
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
167
|
+
const fromFile = await readCredentialsFile(homeDir);
|
|
168
|
+
return fromFile === null ? null : parseClaudeCredentials(fromFile);
|
|
169
|
+
}
|
|
170
|
+
function claudeVersion(timeoutMs) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
try {
|
|
173
|
+
execFile('claude', ['--version'], { timeout: timeoutMs, encoding: 'utf8' }, (error, stdout) => {
|
|
174
|
+
const match = error ? null : /(\d+\.\d+\.\d+)/.exec(String(stdout));
|
|
175
|
+
resolve(match ? match[1] : FALLBACK_CLAUDE_VERSION);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
resolve(FALLBACK_CLAUDE_VERSION);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function failure(options, status, plan, message) {
|
|
184
|
+
return {
|
|
185
|
+
provider: 'claude',
|
|
186
|
+
status,
|
|
187
|
+
plan,
|
|
188
|
+
windows: [],
|
|
189
|
+
source: 'live',
|
|
190
|
+
observedAt: options.now().toISOString(),
|
|
191
|
+
message,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* One-line, bounded, secret-free reason for a caught error. Every occurrence
|
|
196
|
+
* of `secret` is replaced before truncation, so no part of it can survive.
|
|
197
|
+
*/
|
|
198
|
+
export function safeReason(error, secret) {
|
|
199
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
200
|
+
return 'timed out';
|
|
201
|
+
let text = error instanceof Error ? error.message : String(error);
|
|
202
|
+
if (secret !== null && secret.length > 0) {
|
|
203
|
+
text = text.split(secret).join('<redacted>');
|
|
204
|
+
}
|
|
205
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
206
|
+
if (oneLine.length === 0)
|
|
207
|
+
return 'unknown error';
|
|
208
|
+
return oneLine.length > MAX_REASON_LENGTH ? `${oneLine.slice(0, MAX_REASON_LENGTH)}...` : oneLine;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Live fetch of the Claude subscription usage windows. Never throws, never
|
|
212
|
+
* prints, never refreshes the OAuth token and never writes to the keychain.
|
|
213
|
+
*/
|
|
214
|
+
export const fetchClaudeQuota = async (options) => {
|
|
215
|
+
const started = Date.now();
|
|
216
|
+
const remaining = () => Math.max(1, options.timeoutMs - (Date.now() - started));
|
|
217
|
+
let plan = null;
|
|
218
|
+
let secret = null;
|
|
219
|
+
try {
|
|
220
|
+
const credentials = await loadCredentials(options.homeDir, remaining());
|
|
221
|
+
if (!credentials) {
|
|
222
|
+
return failure(options, 'auth', null, NOT_SIGNED_IN_MESSAGE);
|
|
223
|
+
}
|
|
224
|
+
secret = credentials.accessToken;
|
|
225
|
+
plan = credentials.plan;
|
|
226
|
+
if (credentials.expiresAt !== null && credentials.expiresAt <= options.now().getTime()) {
|
|
227
|
+
return failure(options, 'auth', plan, EXPIRED_MESSAGE);
|
|
228
|
+
}
|
|
229
|
+
const version = await claudeVersion(Math.min(VERSION_TIMEOUT_MS, remaining()));
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
const timer = setTimeout(() => controller.abort(), remaining());
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(USAGE_URL, {
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
236
|
+
'anthropic-beta': OAUTH_BETA,
|
|
237
|
+
'User-Agent': `claude-code/${version}`,
|
|
238
|
+
},
|
|
239
|
+
signal: controller.signal,
|
|
240
|
+
});
|
|
241
|
+
if (response.status === 401 || response.status === 403) {
|
|
242
|
+
return failure(options, 'auth', plan, EXPIRED_MESSAGE);
|
|
243
|
+
}
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
return failure(options, 'error', plan, `Anthropic usage endpoint returned HTTP ${response.status}`);
|
|
246
|
+
}
|
|
247
|
+
const text = await response.text();
|
|
248
|
+
let body;
|
|
249
|
+
try {
|
|
250
|
+
body = JSON.parse(text);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
body = undefined;
|
|
254
|
+
}
|
|
255
|
+
return parseClaudeUsageResponse(body, plan, options.now().toISOString());
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return failure(options, 'error', plan, `could not reach Anthropic: ${safeReason(error, secret)}`);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=claude.js.map
|