sidekick-shared 0.28.2 → 0.28.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/CHANGELOG.md +19 -0
- package/README.md +29 -13
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.js +17 -4
- package/dist/esm/browser.js +2 -0
- package/dist/esm/codexQuota.js +18 -5
- package/dist/esm/index.js +3 -3
- package/dist/esm/multiProviderQuotaService.js +1 -1
- package/dist/esm/quota.js +356 -24
- package/dist/esm/quotaHistory.js +33 -8
- package/dist/esm/quotaPoller.js +5 -2
- package/dist/esm/quotaResolver.js +31 -6
- package/dist/esm/quotaSnapshots.js +65 -10
- package/dist/esm/quotaThresholds.js +7 -2
- package/dist/esm/schemas/index.js +2 -2
- package/dist/esm/schemas/quota.js +30 -0
- package/dist/esm/schemas/quotaHistory.js +10 -2
- package/dist/esm/schemas/stateFile.js +9 -0
- package/dist/esm/stateFile.js +18 -3
- package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/esm/statusline/formatter.js +57 -18
- package/dist/esm/statusline/index.js +2 -2
- package/dist/esm/zaiQuotaApi.js +4 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +18 -5
- package/dist/multiProviderQuotaService.js +1 -1
- package/dist/quota.d.ts +131 -1
- package/dist/quota.js +366 -24
- package/dist/quotaHistory.d.ts +10 -0
- package/dist/quotaHistory.js +34 -8
- package/dist/quotaPoller.d.ts +11 -3
- package/dist/quotaPoller.js +5 -2
- package/dist/quotaResolver.js +30 -5
- package/dist/quotaSnapshots.js +64 -9
- package/dist/quotaThresholds.js +7 -2
- package/dist/schemas/index.d.ts +3 -3
- package/dist/schemas/index.js +5 -2
- package/dist/schemas/quota.d.ts +258 -0
- package/dist/schemas/quota.js +31 -1
- package/dist/schemas/quotaHistory.d.ts +2 -0
- package/dist/schemas/quotaHistory.js +10 -2
- package/dist/schemas/stateFile.d.ts +34 -0
- package/dist/schemas/stateFile.js +10 -1
- package/dist/stateFile.d.ts +18 -0
- package/dist/stateFile.js +17 -2
- package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
- package/dist/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/statusline/formatter.d.ts +5 -0
- package/dist/statusline/formatter.js +56 -17
- package/dist/statusline/index.d.ts +4 -4
- package/dist/statusline/index.js +5 -1
- package/dist/zaiQuotaApi.js +3 -1
- package/package.json +1 -1
|
@@ -293,7 +293,7 @@ class MultiProviderQuotaService {
|
|
|
293
293
|
await this.refreshPeakHours();
|
|
294
294
|
const creds = await this.readClaudeCredentials();
|
|
295
295
|
if (!creds) {
|
|
296
|
-
this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude
|
|
296
|
+
this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude login to view quota', 'auth'));
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
const state = await this.fetchClaudeQuota(creds.accessToken);
|
package/dist/quota.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { QuotaProviderId } from './providerIds';
|
|
|
2
2
|
export { QUOTA_PROVIDER_IDS } from './providerIds';
|
|
3
3
|
export type { QuotaProviderId } from './providerIds';
|
|
4
4
|
/**
|
|
5
|
-
* Stateless quota fetcher for Claude
|
|
5
|
+
* Stateless quota fetcher for Claude subscription usage (Pro, Max, Ultra).
|
|
6
6
|
*
|
|
7
7
|
* Calls the Anthropic OAuth usage endpoint and returns utilization data.
|
|
8
8
|
* Polling / eventing is the caller's responsibility.
|
|
@@ -12,6 +12,67 @@ export interface QuotaWindow {
|
|
|
12
12
|
utilization: number;
|
|
13
13
|
/** ISO timestamp when the window resets */
|
|
14
14
|
resetsAt: string;
|
|
15
|
+
/**
|
|
16
|
+
* True when the provider reported that this window does not exist for the
|
|
17
|
+
* account — Claude Ultra has no five-hour session limit (`five_hour: null`),
|
|
18
|
+
* and a Codex plan can report a primary window without a secondary one.
|
|
19
|
+
* `utilization`/`resetsAt` are then `0`/`''` placeholders, so readers that
|
|
20
|
+
* predate the flag keep their old behaviour. Absent or false: the window
|
|
21
|
+
* applies (it may still be idle, with no reset time yet).
|
|
22
|
+
*/
|
|
23
|
+
absent?: boolean;
|
|
24
|
+
/** Window length in milliseconds when the provider reports it (e.g. Codex `window_minutes`). */
|
|
25
|
+
windowMs?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One usage meter as reported by the Claude usage endpoint's `limits[]` array.
|
|
29
|
+
* The server decides which meters exist and in what order; classify on
|
|
30
|
+
* `kind`, never on `label`.
|
|
31
|
+
*/
|
|
32
|
+
export interface QuotaLimitRow {
|
|
33
|
+
/** Server meter kind, e.g. `session`, `weekly_all`, `weekly_scoped`. */
|
|
34
|
+
kind: string;
|
|
35
|
+
/** Server row group, e.g. `session` or `weekly`. */
|
|
36
|
+
group: string;
|
|
37
|
+
/** Display label derived from kind, group, and scope, e.g. `7-Day (Fable)`. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Share of the window used, 0–100. */
|
|
40
|
+
utilization: number;
|
|
41
|
+
/** ISO timestamp when the window resets; empty when unknown. */
|
|
42
|
+
resetsAt: string;
|
|
43
|
+
/** Window length in milliseconds, derived from the group when known. */
|
|
44
|
+
windowMs?: number;
|
|
45
|
+
/** Projected utilization at reset (percentage, capped at 200). */
|
|
46
|
+
projected?: number;
|
|
47
|
+
/** Model the meter is scoped to (server display name), e.g. `Fable`. */
|
|
48
|
+
scopeModel?: string;
|
|
49
|
+
/** Surface the meter is scoped to (server display name). */
|
|
50
|
+
scopeSurface?: string;
|
|
51
|
+
/** Server severity for colouring: `normal`, `warning`, `critical`. */
|
|
52
|
+
severity?: string;
|
|
53
|
+
/** The server's headline pick — the row a single-value indicator shows. */
|
|
54
|
+
isActive?: boolean;
|
|
55
|
+
/** For scoped weekly meters: how much of the all-models weekly limit this meter may use. */
|
|
56
|
+
allowancePercentOfWeekly?: number;
|
|
57
|
+
/** For scoped weekly meters: how much of the all-models weekly limit it has used. */
|
|
58
|
+
usedPercentOfWeekly?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Claude extra-usage (usage credits) spend for the billing period. */
|
|
61
|
+
export interface QuotaExtraUsage {
|
|
62
|
+
/** Whether extra usage can currently cover requests. */
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/** Monthly cap in minor units of `currency` (cents for USD); null when uncapped. */
|
|
65
|
+
monthlyLimit: number | null;
|
|
66
|
+
/** Spend so far this period, in minor units of `currency`. */
|
|
67
|
+
usedCredits: number | null;
|
|
68
|
+
/** Share of the monthly cap used, 0–100. */
|
|
69
|
+
utilization: number | null;
|
|
70
|
+
/** ISO 4217 currency code, e.g. `USD`. */
|
|
71
|
+
currency?: string;
|
|
72
|
+
/** Number of minor-unit decimals (2 for USD). */
|
|
73
|
+
decimalPlaces?: number;
|
|
74
|
+
/** Server reason extra usage is off, e.g. `out_of_credits`. */
|
|
75
|
+
disabledReason?: string;
|
|
15
76
|
}
|
|
16
77
|
export interface CodexResetCredit {
|
|
17
78
|
title?: string;
|
|
@@ -80,8 +141,23 @@ export interface QuotaState {
|
|
|
80
141
|
resetCredits?: CodexResetCreditsSnapshot;
|
|
81
142
|
/** Provider-specific plan type */
|
|
82
143
|
planType?: string;
|
|
144
|
+
/** Display name of the subscription plan, e.g. `Pro`, `Max`, `Ultra`. */
|
|
145
|
+
planLabel?: string;
|
|
83
146
|
/** Provider-specific rate-limit reached reason */
|
|
84
147
|
rateLimitReachedType?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Every usage meter the provider reported, in server order (Claude's
|
|
150
|
+
* `limits[]`). The `session` and `weekly_all` rows duplicate `fiveHour` and
|
|
151
|
+
* `sevenDay`; use `extraQuotaRows()` for the rest (e.g. a Fable weekly cap).
|
|
152
|
+
*/
|
|
153
|
+
limits?: QuotaLimitRow[];
|
|
154
|
+
/** Claude extra-usage (usage credits) spend, when the plan reports it. */
|
|
155
|
+
extraUsage?: QuotaExtraUsage;
|
|
156
|
+
}
|
|
157
|
+
/** Plan facts read from the Claude OAuth credentials. */
|
|
158
|
+
export interface ClaudePlanInfo {
|
|
159
|
+
subscriptionType?: string | null;
|
|
160
|
+
rateLimitTier?: string | null;
|
|
85
161
|
}
|
|
86
162
|
export declare const FIVE_HOUR_WINDOW_MS: number;
|
|
87
163
|
export declare const SEVEN_DAY_WINDOW_MS: number;
|
|
@@ -93,6 +169,39 @@ export declare const QUOTA_FRESH_MAX_AGE_MS: number;
|
|
|
93
169
|
export declare const QUOTA_AGING_MAX_AGE_MS: number;
|
|
94
170
|
/** Classify a cached sample's age. Non-finite or negative ages count as stale. */
|
|
95
171
|
export declare function classifyQuotaFreshness(ageMs: number | undefined): QuotaFreshness;
|
|
172
|
+
/** Whether the provider reports this window for the account at all. */
|
|
173
|
+
export declare function quotaWindowApplies(window: QuotaWindow | null | undefined): boolean;
|
|
174
|
+
/** Whether the window applies and carries a reading worth showing (a reset time or any usage). */
|
|
175
|
+
export declare function quotaWindowHasData(window: QuotaWindow | null | undefined): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* True when every window of a sample that has a known reset time has already
|
|
178
|
+
* reset by `nowMs`. Such a sample says nothing about current usage — showing
|
|
179
|
+
* it as live would print old percentages with "resets now". Samples without
|
|
180
|
+
* any reset time are never considered expired.
|
|
181
|
+
*/
|
|
182
|
+
export declare function quotaSampleExpired(quota: QuotaState, nowMs?: number): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Human label for a window length: `5-Hour`, `Daily`, `Weekly`, `30-Day`, …
|
|
185
|
+
* Returns `fallback` when the length is unknown or not a whole number of hours.
|
|
186
|
+
*/
|
|
187
|
+
export declare function quotaWindowLabel(windowMs: number | undefined, fallback: string): string;
|
|
188
|
+
/** Compact label for a window length: `5h`, `1d`, `7d`. Returns `fallback` when unknown. */
|
|
189
|
+
export declare function quotaWindowShortLabel(windowMs: number | undefined, fallback: string): string;
|
|
190
|
+
/**
|
|
191
|
+
* The reported meters that `fiveHour` and `sevenDay` do not already show —
|
|
192
|
+
* per-model or per-surface weekly caps and any future meter kind.
|
|
193
|
+
*/
|
|
194
|
+
export declare function extraQuotaRows(quota: Pick<QuotaState, 'limits'>): QuotaLimitRow[];
|
|
195
|
+
/**
|
|
196
|
+
* Display name for a Claude subscription. The 20× tier was renamed Ultra and
|
|
197
|
+
* the 5× tier Max on 2026-10-30; credentials keep the old tier ids.
|
|
198
|
+
*/
|
|
199
|
+
export declare function claudePlanLabel(plan: ClaudePlanInfo | null | undefined): string | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* One-line extra-usage summary, e.g. `$12.40 of $200.00 used` or
|
|
202
|
+
* `$0.00 of $200.00 used · off (out of credits)`.
|
|
203
|
+
*/
|
|
204
|
+
export declare function formatExtraUsage(extra: QuotaExtraUsage): string;
|
|
96
205
|
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
97
206
|
export declare function formatQuotaAge(ageMs: number | undefined): string;
|
|
98
207
|
export interface QuotaProjectionInput {
|
|
@@ -112,6 +221,25 @@ export declare function withQuotaProjections<T extends QuotaState>(state: T, opt
|
|
|
112
221
|
sevenDayWindowMs?: number;
|
|
113
222
|
capturedAt?: string;
|
|
114
223
|
}): T;
|
|
224
|
+
/**
|
|
225
|
+
* Normalize a Claude OAuth usage-endpoint body into a `QuotaState`.
|
|
226
|
+
*
|
|
227
|
+
* - `five_hour`/`seven_day` that are null or missing become `absent` windows
|
|
228
|
+
* (Ultra plans have no five-hour session limit), unless `limits[]` carries
|
|
229
|
+
* the matching `session`/`weekly_all` meter.
|
|
230
|
+
* - `limits[]` is kept verbatim as `limits` (with derived labels, window
|
|
231
|
+
* lengths, and projections); older servers' `seven_day_opus` /
|
|
232
|
+
* `seven_day_sonnet` / `seven_day_overage_included` fill it instead.
|
|
233
|
+
* - `extra_usage` becomes `extraUsage`.
|
|
234
|
+
*
|
|
235
|
+
* Bodies in the original two-window shape produce exactly the state they did
|
|
236
|
+
* before these fields existed.
|
|
237
|
+
*/
|
|
238
|
+
export declare function quotaStateFromClaudeUsagePayload(payload: unknown, options?: {
|
|
239
|
+
capturedAt?: string;
|
|
240
|
+
}): QuotaState;
|
|
241
|
+
/** Stamp `planType` / `planLabel` from Claude credential plan facts (no-op without them). */
|
|
242
|
+
export declare function withClaudePlan<T extends QuotaState>(state: T, plan: ClaudePlanInfo | null | undefined): T;
|
|
115
243
|
/**
|
|
116
244
|
* Fetch current quota utilization from the Anthropic OAuth usage API.
|
|
117
245
|
*
|
|
@@ -120,8 +248,10 @@ export declare function withQuotaProjections<T extends QuotaState>(state: T, opt
|
|
|
120
248
|
*
|
|
121
249
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
122
250
|
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
251
|
+
* @param options.plan - Plan facts from the same credentials; stamps `planType` and `planLabel`
|
|
123
252
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
124
253
|
*/
|
|
125
254
|
export declare function fetchQuota(accessToken: string, options?: {
|
|
126
255
|
fetchImpl?: typeof fetch;
|
|
256
|
+
plan?: ClaudePlanInfo | null;
|
|
127
257
|
}): Promise<QuotaState>;
|
package/dist/quota.js
CHANGED
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QUOTA_AGING_MAX_AGE_MS = exports.QUOTA_FRESH_MAX_AGE_MS = exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
|
|
4
4
|
exports.classifyQuotaFreshness = classifyQuotaFreshness;
|
|
5
|
+
exports.quotaWindowApplies = quotaWindowApplies;
|
|
6
|
+
exports.quotaWindowHasData = quotaWindowHasData;
|
|
7
|
+
exports.quotaSampleExpired = quotaSampleExpired;
|
|
8
|
+
exports.quotaWindowLabel = quotaWindowLabel;
|
|
9
|
+
exports.quotaWindowShortLabel = quotaWindowShortLabel;
|
|
10
|
+
exports.extraQuotaRows = extraQuotaRows;
|
|
11
|
+
exports.claudePlanLabel = claudePlanLabel;
|
|
12
|
+
exports.formatExtraUsage = formatExtraUsage;
|
|
5
13
|
exports.formatQuotaAge = formatQuotaAge;
|
|
6
14
|
exports.projectQuotaWindow = projectQuotaWindow;
|
|
7
15
|
exports.withQuotaProjections = withQuotaProjections;
|
|
16
|
+
exports.quotaStateFromClaudeUsagePayload = quotaStateFromClaudeUsagePayload;
|
|
17
|
+
exports.withClaudePlan = withClaudePlan;
|
|
8
18
|
exports.fetchQuota = fetchQuota;
|
|
9
19
|
var providerIds_1 = require("./providerIds");
|
|
10
20
|
Object.defineProperty(exports, "QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.QUOTA_PROVIDER_IDS; } });
|
|
@@ -27,6 +37,126 @@ function classifyQuotaFreshness(ageMs) {
|
|
|
27
37
|
return 'aging';
|
|
28
38
|
return 'stale';
|
|
29
39
|
}
|
|
40
|
+
const HOUR_MS = 3_600_000;
|
|
41
|
+
const DAY_MS = 86_400_000;
|
|
42
|
+
/** Whether the provider reports this window for the account at all. */
|
|
43
|
+
function quotaWindowApplies(window) {
|
|
44
|
+
return Boolean(window) && window?.absent !== true;
|
|
45
|
+
}
|
|
46
|
+
/** Whether the window applies and carries a reading worth showing (a reset time or any usage). */
|
|
47
|
+
function quotaWindowHasData(window) {
|
|
48
|
+
if (!window || window.absent === true)
|
|
49
|
+
return false;
|
|
50
|
+
return Boolean(window.resetsAt) || window.utilization > 0;
|
|
51
|
+
}
|
|
52
|
+
function usableWindowMs(windowMs) {
|
|
53
|
+
return typeof windowMs === 'number' && Number.isFinite(windowMs) && windowMs > 0
|
|
54
|
+
? windowMs
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True when every window of a sample that has a known reset time has already
|
|
59
|
+
* reset by `nowMs`. Such a sample says nothing about current usage — showing
|
|
60
|
+
* it as live would print old percentages with "resets now". Samples without
|
|
61
|
+
* any reset time are never considered expired.
|
|
62
|
+
*/
|
|
63
|
+
function quotaSampleExpired(quota, nowMs = Date.now()) {
|
|
64
|
+
const resets = [quota.fiveHour, quota.sevenDay]
|
|
65
|
+
.filter((window) => !window.absent && window.resetsAt)
|
|
66
|
+
.map((window) => Date.parse(window.resetsAt))
|
|
67
|
+
.filter((ms) => Number.isFinite(ms));
|
|
68
|
+
return resets.length > 0 && resets.every((ms) => ms <= nowMs);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Human label for a window length: `5-Hour`, `Daily`, `Weekly`, `30-Day`, …
|
|
72
|
+
* Returns `fallback` when the length is unknown or not a whole number of hours.
|
|
73
|
+
*/
|
|
74
|
+
function quotaWindowLabel(windowMs, fallback) {
|
|
75
|
+
const ms = usableWindowMs(windowMs);
|
|
76
|
+
if (ms === undefined)
|
|
77
|
+
return fallback;
|
|
78
|
+
if (ms === DAY_MS)
|
|
79
|
+
return 'Daily';
|
|
80
|
+
if (ms === 7 * DAY_MS)
|
|
81
|
+
return 'Weekly';
|
|
82
|
+
if (ms % DAY_MS === 0)
|
|
83
|
+
return `${ms / DAY_MS}-Day`;
|
|
84
|
+
if (ms % HOUR_MS === 0)
|
|
85
|
+
return `${ms / HOUR_MS}-Hour`;
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
/** Compact label for a window length: `5h`, `1d`, `7d`. Returns `fallback` when unknown. */
|
|
89
|
+
function quotaWindowShortLabel(windowMs, fallback) {
|
|
90
|
+
const ms = usableWindowMs(windowMs);
|
|
91
|
+
if (ms === undefined)
|
|
92
|
+
return fallback;
|
|
93
|
+
if (ms % DAY_MS === 0)
|
|
94
|
+
return `${ms / DAY_MS}d`;
|
|
95
|
+
if (ms % HOUR_MS === 0)
|
|
96
|
+
return `${ms / HOUR_MS}h`;
|
|
97
|
+
if (ms % 60_000 === 0)
|
|
98
|
+
return `${ms / 60_000}m`;
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The reported meters that `fiveHour` and `sevenDay` do not already show —
|
|
103
|
+
* per-model or per-surface weekly caps and any future meter kind.
|
|
104
|
+
*/
|
|
105
|
+
function extraQuotaRows(quota) {
|
|
106
|
+
return (quota.limits ?? []).filter((row) => row.kind !== 'session' && row.kind !== 'weekly_all');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Display name for a Claude subscription. The 20× tier was renamed Ultra and
|
|
110
|
+
* the 5× tier Max on 2026-10-30; credentials keep the old tier ids.
|
|
111
|
+
*/
|
|
112
|
+
function claudePlanLabel(plan) {
|
|
113
|
+
const tier = plan?.rateLimitTier?.trim().toLowerCase() ?? '';
|
|
114
|
+
if (tier.includes('ultra') || tier.includes('max_20x'))
|
|
115
|
+
return 'Ultra';
|
|
116
|
+
if (tier.includes('max_5x'))
|
|
117
|
+
return 'Max';
|
|
118
|
+
const subscription = plan?.subscriptionType?.trim().toLowerCase();
|
|
119
|
+
if (!subscription)
|
|
120
|
+
return undefined;
|
|
121
|
+
return subscription.charAt(0).toUpperCase() + subscription.slice(1);
|
|
122
|
+
}
|
|
123
|
+
function formatMinorUnits(amount, currency, decimals) {
|
|
124
|
+
const value = amount / 10 ** decimals;
|
|
125
|
+
if (currency) {
|
|
126
|
+
try {
|
|
127
|
+
return new Intl.NumberFormat('en-US', {
|
|
128
|
+
style: 'currency',
|
|
129
|
+
currency,
|
|
130
|
+
minimumFractionDigits: decimals,
|
|
131
|
+
maximumFractionDigits: decimals,
|
|
132
|
+
}).format(value);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return `${value.toFixed(decimals)} ${currency}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return value.toFixed(decimals);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One-line extra-usage summary, e.g. `$12.40 of $200.00 used` or
|
|
142
|
+
* `$0.00 of $200.00 used · off (out of credits)`.
|
|
143
|
+
*/
|
|
144
|
+
function formatExtraUsage(extra) {
|
|
145
|
+
const decimals = typeof extra.decimalPlaces === 'number' &&
|
|
146
|
+
Number.isInteger(extra.decimalPlaces) &&
|
|
147
|
+
extra.decimalPlaces >= 0 &&
|
|
148
|
+
extra.decimalPlaces <= 6
|
|
149
|
+
? extra.decimalPlaces
|
|
150
|
+
: 2;
|
|
151
|
+
const used = formatMinorUnits(extra.usedCredits ?? 0, extra.currency, decimals);
|
|
152
|
+
const amount = extra.monthlyLimit !== null
|
|
153
|
+
? `${used} of ${formatMinorUnits(extra.monthlyLimit, extra.currency, decimals)} used`
|
|
154
|
+
: `${used} used`;
|
|
155
|
+
if (extra.enabled)
|
|
156
|
+
return amount;
|
|
157
|
+
const reason = extra.disabledReason ? ` (${extra.disabledReason.replace(/_/g, ' ')})` : '';
|
|
158
|
+
return `${amount} · off${reason}`;
|
|
159
|
+
}
|
|
30
160
|
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
31
161
|
function formatQuotaAge(ageMs) {
|
|
32
162
|
if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
|
|
@@ -65,21 +195,21 @@ function withQuotaProjections(state, options = {}) {
|
|
|
65
195
|
return state;
|
|
66
196
|
const next = { ...state };
|
|
67
197
|
const capturedAt = options.capturedAt ?? state.capturedAt;
|
|
68
|
-
if (next.projectedFiveHour == null) {
|
|
198
|
+
if (next.projectedFiveHour == null && !next.fiveHour.absent) {
|
|
69
199
|
const projected = projectQuotaWindow({
|
|
70
200
|
utilization: next.fiveHour.utilization,
|
|
71
201
|
resetsAt: next.fiveHour.resetsAt,
|
|
72
|
-
windowMs: options.fiveHourWindowMs ?? exports.FIVE_HOUR_WINDOW_MS,
|
|
202
|
+
windowMs: options.fiveHourWindowMs ?? usableWindowMs(next.fiveHour.windowMs) ?? exports.FIVE_HOUR_WINDOW_MS,
|
|
73
203
|
capturedAt,
|
|
74
204
|
});
|
|
75
205
|
if (projected !== undefined)
|
|
76
206
|
next.projectedFiveHour = projected;
|
|
77
207
|
}
|
|
78
|
-
if (next.projectedSevenDay == null) {
|
|
208
|
+
if (next.projectedSevenDay == null && !next.sevenDay.absent) {
|
|
79
209
|
const projected = projectQuotaWindow({
|
|
80
210
|
utilization: next.sevenDay.utilization,
|
|
81
211
|
resetsAt: next.sevenDay.resetsAt,
|
|
82
|
-
windowMs: options.sevenDayWindowMs ?? exports.SEVEN_DAY_WINDOW_MS,
|
|
212
|
+
windowMs: options.sevenDayWindowMs ?? usableWindowMs(next.sevenDay.windowMs) ?? exports.SEVEN_DAY_WINDOW_MS,
|
|
83
213
|
capturedAt,
|
|
84
214
|
});
|
|
85
215
|
if (projected !== undefined)
|
|
@@ -87,6 +217,235 @@ function withQuotaProjections(state, options = {}) {
|
|
|
87
217
|
}
|
|
88
218
|
return next;
|
|
89
219
|
}
|
|
220
|
+
// ── Claude usage endpoint payload ──
|
|
221
|
+
/** Hard cap on parsed meter rows, matching Claude Code's own bound. */
|
|
222
|
+
const MAX_LIMIT_ROWS = 64;
|
|
223
|
+
const MAX_LABEL_LENGTH = 128;
|
|
224
|
+
function isRecord(value) {
|
|
225
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
226
|
+
}
|
|
227
|
+
function finiteNumber(value) {
|
|
228
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
229
|
+
}
|
|
230
|
+
function nullableFiniteNumber(value) {
|
|
231
|
+
return finiteNumber(value) ?? null;
|
|
232
|
+
}
|
|
233
|
+
function shortString(value) {
|
|
234
|
+
if (typeof value !== 'string')
|
|
235
|
+
return undefined;
|
|
236
|
+
const trimmed = value.trim();
|
|
237
|
+
return trimmed && trimmed.length <= MAX_LABEL_LENGTH ? trimmed : undefined;
|
|
238
|
+
}
|
|
239
|
+
function displayName(value) {
|
|
240
|
+
return isRecord(value) ? shortString(value.display_name) : undefined;
|
|
241
|
+
}
|
|
242
|
+
function usageWindow(value) {
|
|
243
|
+
if (!isRecord(value))
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
utilization: finiteNumber(value.utilization) ?? 0,
|
|
247
|
+
resetsAt: typeof value.resets_at === 'string' ? value.resets_at : '',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function absentWindow() {
|
|
251
|
+
return { utilization: 0, resetsAt: '', absent: true };
|
|
252
|
+
}
|
|
253
|
+
function windowMsForMeter(kind, group) {
|
|
254
|
+
if (kind === 'session' || group === 'session')
|
|
255
|
+
return exports.FIVE_HOUR_WINDOW_MS;
|
|
256
|
+
if (group === 'weekly' || kind.startsWith('weekly'))
|
|
257
|
+
return exports.SEVEN_DAY_WINDOW_MS;
|
|
258
|
+
if (group === 'daily')
|
|
259
|
+
return DAY_MS;
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function titleCase(value) {
|
|
263
|
+
return value
|
|
264
|
+
.split(/[_\s-]+/)
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
267
|
+
.join(' ');
|
|
268
|
+
}
|
|
269
|
+
function meterLabel(kind, group, scope) {
|
|
270
|
+
let base;
|
|
271
|
+
if (kind === 'session' || group === 'session')
|
|
272
|
+
base = '5-Hour';
|
|
273
|
+
else if (group === 'weekly' || kind.startsWith('weekly'))
|
|
274
|
+
base = '7-Day';
|
|
275
|
+
else if (group === 'daily')
|
|
276
|
+
base = 'Daily';
|
|
277
|
+
else
|
|
278
|
+
base = titleCase(group || kind) || kind;
|
|
279
|
+
return scope ? `${base} (${scope})` : base;
|
|
280
|
+
}
|
|
281
|
+
function weeklySharesByIndex(value) {
|
|
282
|
+
const shares = new Map();
|
|
283
|
+
if (!Array.isArray(value))
|
|
284
|
+
return shares;
|
|
285
|
+
for (const entry of value.slice(0, MAX_LIMIT_ROWS)) {
|
|
286
|
+
if (!isRecord(entry))
|
|
287
|
+
continue;
|
|
288
|
+
const index = finiteNumber(entry.limit_index);
|
|
289
|
+
if (index === undefined || !Number.isInteger(index) || index < 0)
|
|
290
|
+
continue;
|
|
291
|
+
shares.set(index, {
|
|
292
|
+
allowance: finiteNumber(entry.allowance_percent_of_weekly),
|
|
293
|
+
used: finiteNumber(entry.used_percent_of_weekly),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return shares;
|
|
297
|
+
}
|
|
298
|
+
function buildMeterRow(input, capturedAt) {
|
|
299
|
+
const windowMs = windowMsForMeter(input.kind, input.group);
|
|
300
|
+
const projected = windowMs !== undefined
|
|
301
|
+
? projectQuotaWindow({
|
|
302
|
+
utilization: input.utilization,
|
|
303
|
+
resetsAt: input.resetsAt,
|
|
304
|
+
windowMs,
|
|
305
|
+
capturedAt,
|
|
306
|
+
})
|
|
307
|
+
: undefined;
|
|
308
|
+
return {
|
|
309
|
+
kind: input.kind,
|
|
310
|
+
group: input.group,
|
|
311
|
+
label: meterLabel(input.kind, input.group, input.scopeModel ?? input.scopeSurface),
|
|
312
|
+
utilization: input.utilization,
|
|
313
|
+
resetsAt: input.resetsAt,
|
|
314
|
+
...(windowMs !== undefined ? { windowMs } : {}),
|
|
315
|
+
...(projected !== undefined ? { projected } : {}),
|
|
316
|
+
...(input.scopeModel ? { scopeModel: input.scopeModel } : {}),
|
|
317
|
+
...(input.scopeSurface ? { scopeSurface: input.scopeSurface } : {}),
|
|
318
|
+
...(input.severity ? { severity: input.severity } : {}),
|
|
319
|
+
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
320
|
+
...(input.share?.allowance !== undefined
|
|
321
|
+
? { allowancePercentOfWeekly: input.share.allowance }
|
|
322
|
+
: {}),
|
|
323
|
+
...(input.share?.used !== undefined ? { usedPercentOfWeekly: input.share.used } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function parseLimitRows(value, sharesValue, capturedAt) {
|
|
327
|
+
if (!Array.isArray(value))
|
|
328
|
+
return undefined;
|
|
329
|
+
const shares = weeklySharesByIndex(sharesValue);
|
|
330
|
+
const rows = [];
|
|
331
|
+
value.slice(0, MAX_LIMIT_ROWS).forEach((raw, index) => {
|
|
332
|
+
if (!isRecord(raw))
|
|
333
|
+
return;
|
|
334
|
+
const kind = shortString(raw.kind);
|
|
335
|
+
const percent = finiteNumber(raw.percent);
|
|
336
|
+
if (!kind || percent === undefined)
|
|
337
|
+
return;
|
|
338
|
+
const scope = isRecord(raw.scope) ? raw.scope : undefined;
|
|
339
|
+
rows.push(buildMeterRow({
|
|
340
|
+
kind,
|
|
341
|
+
group: shortString(raw.group) ?? '',
|
|
342
|
+
utilization: Math.max(0, percent),
|
|
343
|
+
resetsAt: typeof raw.resets_at === 'string' ? raw.resets_at : '',
|
|
344
|
+
scopeModel: displayName(scope?.model),
|
|
345
|
+
scopeSurface: displayName(scope?.surface),
|
|
346
|
+
severity: shortString(raw.severity),
|
|
347
|
+
isActive: typeof raw.is_active === 'boolean' ? raw.is_active : undefined,
|
|
348
|
+
share: shares.get(index),
|
|
349
|
+
}, capturedAt));
|
|
350
|
+
});
|
|
351
|
+
return rows;
|
|
352
|
+
}
|
|
353
|
+
/** Pre-`limits[]` per-model weekly fields, read only when the server sends no `limits[]`. */
|
|
354
|
+
const LEGACY_SCOPED_WEEKLY_FIELDS = [
|
|
355
|
+
['seven_day_opus', 'Opus'],
|
|
356
|
+
['seven_day_sonnet', 'Sonnet'],
|
|
357
|
+
['seven_day_overage_included', 'Fable'],
|
|
358
|
+
];
|
|
359
|
+
function legacyScopedRows(data, capturedAt) {
|
|
360
|
+
const rows = [];
|
|
361
|
+
for (const [field, model] of LEGACY_SCOPED_WEEKLY_FIELDS) {
|
|
362
|
+
const window = usageWindow(data[field]);
|
|
363
|
+
if (!window)
|
|
364
|
+
continue;
|
|
365
|
+
rows.push(buildMeterRow({
|
|
366
|
+
kind: 'weekly_scoped',
|
|
367
|
+
group: 'weekly',
|
|
368
|
+
utilization: window.utilization,
|
|
369
|
+
resetsAt: window.resetsAt,
|
|
370
|
+
scopeModel: model,
|
|
371
|
+
}, capturedAt));
|
|
372
|
+
}
|
|
373
|
+
return rows;
|
|
374
|
+
}
|
|
375
|
+
function parseExtraUsage(value) {
|
|
376
|
+
if (!isRecord(value) || typeof value.is_enabled !== 'boolean')
|
|
377
|
+
return undefined;
|
|
378
|
+
const decimalPlaces = finiteNumber(value.decimal_places);
|
|
379
|
+
return {
|
|
380
|
+
enabled: value.is_enabled,
|
|
381
|
+
monthlyLimit: nullableFiniteNumber(value.monthly_limit),
|
|
382
|
+
usedCredits: nullableFiniteNumber(value.used_credits),
|
|
383
|
+
utilization: nullableFiniteNumber(value.utilization),
|
|
384
|
+
...(shortString(value.currency) ? { currency: shortString(value.currency) } : {}),
|
|
385
|
+
...(decimalPlaces !== undefined ? { decimalPlaces } : {}),
|
|
386
|
+
...(shortString(value.disabled_reason)
|
|
387
|
+
? { disabledReason: shortString(value.disabled_reason) }
|
|
388
|
+
: {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function windowFromMeter(row) {
|
|
392
|
+
return row ? { utilization: row.utilization, resetsAt: row.resetsAt } : null;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Normalize a Claude OAuth usage-endpoint body into a `QuotaState`.
|
|
396
|
+
*
|
|
397
|
+
* - `five_hour`/`seven_day` that are null or missing become `absent` windows
|
|
398
|
+
* (Ultra plans have no five-hour session limit), unless `limits[]` carries
|
|
399
|
+
* the matching `session`/`weekly_all` meter.
|
|
400
|
+
* - `limits[]` is kept verbatim as `limits` (with derived labels, window
|
|
401
|
+
* lengths, and projections); older servers' `seven_day_opus` /
|
|
402
|
+
* `seven_day_sonnet` / `seven_day_overage_included` fill it instead.
|
|
403
|
+
* - `extra_usage` becomes `extraUsage`.
|
|
404
|
+
*
|
|
405
|
+
* Bodies in the original two-window shape produce exactly the state they did
|
|
406
|
+
* before these fields existed.
|
|
407
|
+
*/
|
|
408
|
+
function quotaStateFromClaudeUsagePayload(payload, options = {}) {
|
|
409
|
+
const data = isRecord(payload) ? payload : {};
|
|
410
|
+
const capturedAt = options.capturedAt;
|
|
411
|
+
const parsedLimits = parseLimitRows(data.limits, data.weekly_scoped_shares, capturedAt);
|
|
412
|
+
const legacyRows = parsedLimits === undefined ? legacyScopedRows(data, capturedAt) : [];
|
|
413
|
+
const limits = parsedLimits ?? (legacyRows.length > 0 ? legacyRows : undefined);
|
|
414
|
+
const fiveHour = usageWindow(data.five_hour) ??
|
|
415
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'session')) ??
|
|
416
|
+
absentWindow();
|
|
417
|
+
const sevenDay = usageWindow(data.seven_day) ??
|
|
418
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'weekly_all')) ??
|
|
419
|
+
absentWindow();
|
|
420
|
+
const extraUsage = parseExtraUsage(data.extra_usage);
|
|
421
|
+
const projectedFiveHour = fiveHour.absent
|
|
422
|
+
? undefined
|
|
423
|
+
: projectQuotaWindow({ ...fiveHour, windowMs: exports.FIVE_HOUR_WINDOW_MS, capturedAt });
|
|
424
|
+
const projectedSevenDay = sevenDay.absent
|
|
425
|
+
? undefined
|
|
426
|
+
: projectQuotaWindow({ ...sevenDay, windowMs: exports.SEVEN_DAY_WINDOW_MS, capturedAt });
|
|
427
|
+
return {
|
|
428
|
+
fiveHour,
|
|
429
|
+
sevenDay,
|
|
430
|
+
available: true,
|
|
431
|
+
projectedFiveHour,
|
|
432
|
+
projectedSevenDay,
|
|
433
|
+
...(limits ? { limits } : {}),
|
|
434
|
+
...(extraUsage ? { extraUsage } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Stamp `planType` / `planLabel` from Claude credential plan facts (no-op without them). */
|
|
438
|
+
function withClaudePlan(state, plan) {
|
|
439
|
+
const planLabel = claudePlanLabel(plan);
|
|
440
|
+
const planType = plan?.subscriptionType?.trim() || undefined;
|
|
441
|
+
if (!planLabel && !planType)
|
|
442
|
+
return state;
|
|
443
|
+
return {
|
|
444
|
+
...state,
|
|
445
|
+
...(planType ? { planType } : {}),
|
|
446
|
+
...(planLabel ? { planLabel } : {}),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
90
449
|
/**
|
|
91
450
|
* Helper to build an unavailable QuotaState.
|
|
92
451
|
*/
|
|
@@ -119,6 +478,7 @@ function parseRetryAfterMs(retryAfter) {
|
|
|
119
478
|
*
|
|
120
479
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
121
480
|
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
481
|
+
* @param options.plan - Plan facts from the same credentials; stamps `planType` and `planLabel`
|
|
122
482
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
123
483
|
*/
|
|
124
484
|
async function fetchQuota(accessToken, options = {}) {
|
|
@@ -161,26 +521,8 @@ async function fetchQuota(accessToken, options = {}) {
|
|
|
161
521
|
httpStatus: res.status,
|
|
162
522
|
});
|
|
163
523
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const sevenUtil = data.seven_day?.utilization ?? 0;
|
|
167
|
-
const fiveResetsAt = data.five_hour?.resets_at ?? '';
|
|
168
|
-
const sevenResetsAt = data.seven_day?.resets_at ?? '';
|
|
169
|
-
return {
|
|
170
|
-
fiveHour: { utilization: fiveUtil, resetsAt: fiveResetsAt },
|
|
171
|
-
sevenDay: { utilization: sevenUtil, resetsAt: sevenResetsAt },
|
|
172
|
-
available: true,
|
|
173
|
-
projectedFiveHour: projectQuotaWindow({
|
|
174
|
-
utilization: fiveUtil,
|
|
175
|
-
resetsAt: fiveResetsAt,
|
|
176
|
-
windowMs: exports.FIVE_HOUR_WINDOW_MS,
|
|
177
|
-
}),
|
|
178
|
-
projectedSevenDay: projectQuotaWindow({
|
|
179
|
-
utilization: sevenUtil,
|
|
180
|
-
resetsAt: sevenResetsAt,
|
|
181
|
-
windowMs: exports.SEVEN_DAY_WINDOW_MS,
|
|
182
|
-
}),
|
|
183
|
-
};
|
|
524
|
+
const state = quotaStateFromClaudeUsagePayload(await res.json());
|
|
525
|
+
return withClaudePlan(state, options.plan);
|
|
184
526
|
}
|
|
185
527
|
catch {
|
|
186
528
|
return unavailableState('Network error', { failureKind: 'network' });
|
package/dist/quotaHistory.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* dashboard, the `sidekick quota history` CLI, external consumers) can render heatmaps and
|
|
7
7
|
* trend visualisations over a 13-week window.
|
|
8
8
|
*/
|
|
9
|
+
import type { QuotaState } from './quota';
|
|
9
10
|
import type { RuntimeQuotaProviderId } from './providerIds';
|
|
10
11
|
export type QuotaHistoryRuntimeProvider = RuntimeQuotaProviderId;
|
|
11
12
|
export interface QuotaHistorySample {
|
|
@@ -13,13 +14,16 @@ export interface QuotaHistorySample {
|
|
|
13
14
|
runtimeProvider: QuotaHistoryRuntimeProvider;
|
|
14
15
|
providerId: string;
|
|
15
16
|
workspaceId: string;
|
|
17
|
+
/** `absent` marks a window the plan does not have (e.g. Ultra's five-hour limit). */
|
|
16
18
|
fiveHour: {
|
|
17
19
|
utilization: number;
|
|
18
20
|
resetsAt: string;
|
|
21
|
+
absent?: boolean;
|
|
19
22
|
};
|
|
20
23
|
sevenDay: {
|
|
21
24
|
utilization: number;
|
|
22
25
|
resetsAt: string;
|
|
26
|
+
absent?: boolean;
|
|
23
27
|
};
|
|
24
28
|
available: boolean;
|
|
25
29
|
error?: string;
|
|
@@ -54,6 +58,12 @@ export interface QuotaHistoryDailyBucket {
|
|
|
54
58
|
* Shared between the VS Code extension (workspace folder fsPath) and the CLI (process.cwd()).
|
|
55
59
|
*/
|
|
56
60
|
export declare function getWorkspaceIdFromPath(workspacePath: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Project a quota window onto the history-sample shape, keeping the `absent`
|
|
63
|
+
* marker so summaries and the derived snapshot do not mistake "no such
|
|
64
|
+
* window" for "0% used".
|
|
65
|
+
*/
|
|
66
|
+
export declare function quotaHistoryWindow(window: QuotaState['fiveHour']): QuotaHistorySample['fiveHour'];
|
|
57
67
|
export declare function appendQuotaHistorySample(sample: QuotaHistorySample, options?: QuotaHistoryAppendOptions): Promise<void>;
|
|
58
68
|
export declare function readQuotaHistoryRange(options: QuotaHistoryRangeOptions): Promise<QuotaHistorySample[]>;
|
|
59
69
|
export declare function readQuotaHistoryDailyBuckets(options: QuotaHistoryRangeOptions): Promise<QuotaHistoryDailyBucket[]>;
|