@sreetej510/pi-usage 0.1.1 → 0.1.4

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/src/format.ts DELETED
@@ -1,208 +0,0 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { BAR_SEGMENTS, LIMIT_VALUE_COLUMN, RESET_FOREGROUND } from "./constants.js";
3
- import { isOpenAICodexModel, reportMatchesModel } from "./models.js";
4
- import type {
5
- CodexUsageReport,
6
- NormalizedCredits,
7
- NormalizedRateLimitSnapshot,
8
- NormalizedRateLimitWindow,
9
- PiModel,
10
- ProviderUsageModel,
11
- UsageQueryError,
12
- UsageReport,
13
- } from "./types.js";
14
- import {
15
- addNormalizedUsageKey,
16
- clampPercent,
17
- compactLimitLabel,
18
- formatNumber,
19
- normalizedKeyHasToken,
20
- normalizedUsageKey,
21
- } from "./utils.js";
22
-
23
- export function formatCodexUsageReport(report: CodexUsageReport, _cacheAgeMs?: number): string {
24
- const lines = [" >_ OpenAI Codex Usage", ""];
25
-
26
- for (const snapshot of report.snapshots) {
27
- const label = snapshot.limitName ?? snapshot.limitId;
28
- if (!isPrimaryCodexSnapshot(snapshot)) {
29
- lines.push(` ${label} limit:`);
30
- }
31
- if (snapshot.primary) lines.push(formatWindowLine("5h limit:", snapshot.primary));
32
- if (snapshot.secondary) lines.push(formatWindowLine("Weekly limit:", snapshot.secondary));
33
- if (!snapshot.primary && !snapshot.secondary) {
34
- lines.push(" Limits unavailable for this account");
35
- }
36
- }
37
-
38
- if (report.bankedResetsAvailable !== undefined) {
39
- lines.push("");
40
- lines.push(` Banked resets available: ${report.bankedResetsAvailable}`);
41
- }
42
-
43
- return lines.join("\n");
44
- }
45
-
46
- export function formatCodexUsageStatusline(report: CodexUsageReport, model?: ProviderUsageModel): string {
47
- const snapshot = selectSnapshotForUsageModel(report, model);
48
- if (!snapshot) return "usage unavailable";
49
-
50
- const parts = [formatStatuslinePrefix(snapshot)];
51
- if (snapshot.primary) parts.push(`${clampPercent(snapshot.primary.usedPercent).toFixed(0)}% 5h`);
52
- if (snapshot.secondary) parts.push(`${clampPercent(snapshot.secondary.usedPercent).toFixed(0)}% wk`);
53
- if (parts.length === 1 && snapshot.credits) parts.push(formatCredits(snapshot.credits));
54
- return parts.join(" ");
55
- }
56
-
57
- export function formatUsageStatusline(report: UsageReport, model?: ProviderUsageModel): string {
58
- return report.provider === "anthropic" ? report.statusline : formatCodexUsageStatusline(report, model);
59
- }
60
-
61
- export function formatUsageReport(report: UsageReport, cacheAgeMs?: number): string {
62
- return report.provider === "anthropic" ? report.summaryLines.join("\n") : formatCodexUsageReport(report, cacheAgeMs);
63
- }
64
-
65
- export function showReport(ctx: ExtensionCommandContext, report: UsageReport, fromCache: boolean): void {
66
- const text = formatUsageReport(report, fromCache ? Date.now() - report.capturedAt : undefined);
67
- ctx.ui.notify(ctx.hasUI ? brightenInfoNotification(text) : text, "info");
68
- }
69
-
70
- export function showReports(ctx: ExtensionCommandContext, reports: UsageReport[], fromCache: boolean): void {
71
- const ordered = orderReportsForCurrentProvider(reports, ctx.model);
72
- const text = ordered
73
- .map((report) => formatUsageReport(report, fromCache ? Date.now() - report.capturedAt : undefined))
74
- .join("\n\n");
75
- ctx.ui.notify(ctx.hasUI ? brightenInfoNotification(text) : text, "info");
76
- }
77
-
78
- export function formatQueryErrors(errors: UsageQueryError[]): string {
79
- const lines = ["Unable to read usage for the current provider."];
80
- for (const error of errors) {
81
- const source =
82
- error.source === "pi-auth"
83
- ? "Pi Codex auth"
84
- : error.source === "codex-app-server"
85
- ? "Codex app-server fallback"
86
- : "Anthropic OAuth usage";
87
- lines.push(`- ${source}: ${error.message}`);
88
- }
89
- lines.push("");
90
- lines.push(
91
- "Tip: use a Pi OpenAI Codex model or Pi Anthropic model. For Codex, /login with OpenAI ChatGPT Plus/Pro. For Anthropic, /login with Anthropic so the OAuth usage endpoint can be queried.",
92
- );
93
- return lines.join("\n");
94
- }
95
-
96
- export function progressBarUsed(percentUsed: number): string {
97
- const filled = Math.round((clampPercent(percentUsed) / 100) * BAR_SEGMENTS);
98
- return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}]`;
99
- }
100
-
101
- function selectSnapshotForUsageModel(
102
- report: CodexUsageReport,
103
- model: ProviderUsageModel | undefined,
104
- ): NormalizedRateLimitSnapshot | undefined {
105
- const codexSnapshot = report.snapshots.find(isPrimaryCodexSnapshot);
106
- if (!model || !isOpenAICodexModel(model)) return codexSnapshot ?? report.snapshots[0];
107
-
108
- const modelKeys = normalizedModelUsageKeys(model);
109
- const exactMatch = report.snapshots.find((snapshot) =>
110
- normalizedSnapshotUsageKeys(snapshot).some((key) => modelKeys.has(key)),
111
- );
112
- if (exactMatch) return exactMatch;
113
-
114
- const variants = codexModelVariantKeys(modelKeys);
115
- for (const variant of variants) {
116
- const matches = report.snapshots.filter(
117
- (snapshot) =>
118
- !isPrimaryCodexSnapshot(snapshot) &&
119
- normalizedSnapshotUsageKeys(snapshot).some((key) => normalizedKeyHasToken(key, variant)),
120
- );
121
- if (matches.length === 1) return matches[0];
122
- }
123
-
124
- return codexSnapshot ?? report.snapshots[0];
125
- }
126
-
127
- function normalizedModelUsageKeys(model: ProviderUsageModel): Set<string> {
128
- const keys = new Set<string>();
129
- addNormalizedUsageKey(keys, model.id);
130
- addNormalizedUsageKey(keys, model.name);
131
-
132
- for (const key of [...keys]) {
133
- const codexIndex = key.indexOf("codex");
134
- if (codexIndex >= 0) keys.add(key.slice(codexIndex));
135
- }
136
-
137
- return keys;
138
- }
139
-
140
- function normalizedSnapshotUsageKeys(snapshot: NormalizedRateLimitSnapshot): string[] {
141
- return [normalizedUsageKey(snapshot.limitId), normalizedUsageKey(snapshot.limitName)].filter(
142
- (key): key is string => key !== undefined,
143
- );
144
- }
145
-
146
- function codexModelVariantKeys(modelKeys: Set<string>): string[] {
147
- const variants = new Set<string>();
148
- for (const key of modelKeys) {
149
- const match = key.match(/(?:^|-)codex-(.+)$/);
150
- if (match?.[1]) variants.add(match[1]);
151
- }
152
- return [...variants];
153
- }
154
-
155
- function formatStatuslinePrefix(snapshot: NormalizedRateLimitSnapshot): string {
156
- if (isPrimaryCodexSnapshot(snapshot)) return "codex";
157
- const label = snapshot.limitName ?? snapshot.limitId;
158
- return `codex ${compactLimitLabel(label)}`;
159
- }
160
-
161
- function orderReportsForCurrentProvider(
162
- reports: UsageReport[],
163
- model: Pick<PiModel, "provider"> | undefined,
164
- ): UsageReport[] {
165
- return [...reports].sort((left, right) => {
166
- const leftCurrent = reportMatchesModel(left, model) ? 0 : 1;
167
- const rightCurrent = reportMatchesModel(right, model) ? 0 : 1;
168
- return leftCurrent - rightCurrent;
169
- });
170
- }
171
-
172
- function brightenInfoNotification(text: string): string {
173
- return `${RESET_FOREGROUND}${text}`;
174
- }
175
-
176
- function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean {
177
- return normalizedUsageKey(snapshot.limitId) === "codex" || normalizedUsageKey(snapshot.limitName) === "codex";
178
- }
179
-
180
- function formatWindowLine(label: string, window: NormalizedRateLimitWindow): string {
181
- return ` ${label.padEnd(LIMIT_VALUE_COLUMN)}${formatWindow(window)}`;
182
- }
183
-
184
- function formatWindow(window: NormalizedRateLimitWindow): string {
185
- const used = clampPercent(window.usedPercent);
186
- const reset = window.resetsAt ? ` (resets ${formatReset(window.resetsAt)})` : "";
187
- return `${progressBarUsed(used)} ${used.toFixed(0)}% used${reset}`;
188
- }
189
-
190
- function formatCredits(credits: NormalizedCredits): string {
191
- if (!credits.hasCredits) return "no credits";
192
- if (credits.unlimited) return "unlimited credits";
193
- const balance = credits.balance?.trim();
194
- if (!balance) return "credits available";
195
- return `${formatNumber(Number(balance), balance)} credits`;
196
- }
197
-
198
- function formatReset(epochSeconds: number): string {
199
- const reset = new Date(epochSeconds * 1000);
200
- if (Number.isNaN(reset.getTime())) return "at an unknown time";
201
-
202
- const now = new Date();
203
- const time = `${reset.getHours().toString().padStart(2, "0")}:${reset.getMinutes().toString().padStart(2, "0")}`;
204
- if (reset.toDateString() === now.toDateString()) return time;
205
- const day = reset.getDate().toString();
206
- const month = reset.toLocaleDateString(undefined, { month: "short" });
207
- return `${time} on ${day} ${month}`;
208
- }
package/src/http.ts DELETED
@@ -1,21 +0,0 @@
1
- import { errorMessage } from "./utils.js";
2
-
3
- export async function fetchWithTimeout(
4
- url: string,
5
- init: RequestInit,
6
- timeoutMs: number,
7
- label = "usage",
8
- ): Promise<Response> {
9
- const controller = new AbortController();
10
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
11
- try {
12
- return await fetch(url, { ...init, signal: controller.signal });
13
- } catch (error) {
14
- if (controller.signal.aborted) {
15
- throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching ${label}.`);
16
- }
17
- throw error;
18
- } finally {
19
- clearTimeout(timeout);
20
- }
21
- }
package/src/index.ts DELETED
@@ -1,100 +0,0 @@
1
- /**
2
- * Pi Usage Extension
3
- *
4
- * Reports provider usage and rate-limit budgets for OpenAI Codex and Anthropic
5
- * OAuth subscriptions via /usage, with an optional statusline widget and custom
6
- * footer that right-aligns usage status.
7
- *
8
- * File layout:
9
- * index.ts extension entry point (this file) — commands + lifecycle events
10
- * constants.ts command name, URLs, timeouts, cache paths, UI constants
11
- * types.ts shared TypeScript types (public types exported)
12
- * args.ts /usage argument completions and parsing
13
- * utils.ts small helpers (asString, parseJsonObject, delay, etc.)
14
- * errors.ts UsageEndpointError, stale-context detection, rate-limit backoff
15
- * http.ts fetchWithTimeout
16
- * models.ts provider/model matching and auth candidate enumeration
17
- * shared-cache.ts on-disk shared usage cache read/write across pi sessions
18
- * codex-auth.ts resolve Pi Codex auth headers (+ WHAM + account id)
19
- * codex-reset-credits.ts fetch/consume/format Codex banked resets
20
- * codex-app-server.ts CodexAppServerClient + queryViaCodexAppServer fallback
21
- * normalize-codex.ts normalize backend + app-server Codex rate-limit payloads
22
- * normalize-anthropic.ts normalize Anthropic OAuth usage payloads
23
- * codex-query.ts queryViaPiAuth + queryCodexUsageWithFallback
24
- * anthropic-auth.ts resolve Pi Anthropic OAuth auth headers
25
- * anthropic-query.ts queryAnthropicUsage + fetchRawUsagePayloads (--raw)
26
- * query.ts queryUsage, queryAllUsage, queryUsageWithRetries
27
- * format.ts usage report/statusline formatting and display notifications
28
- * footer.ts custom footer with right-aligned usage status
29
- * statusline.ts statusline state, timers, and background refresh
30
- * command.ts /usage command handler
31
- */
32
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
33
- import { registerUsageCommand } from "./command.js";
34
- import { installUsageFooter, isFooterRegistered, setFooterRegistered } from "./footer.js";
35
- import { isUsageSupportedModel } from "./models.js";
36
- import {
37
- clearUsageStatusline,
38
- handleStaleContextError,
39
- refreshCurrentUsageStatusline,
40
- rethrowUnlessStaleContextError,
41
- setSessionActive,
42
- } from "./statusline.js";
43
-
44
- export { completeCodexStatusArguments, parseArgs } from "./args.js";
45
- export { isStaleExtensionContextError } from "./errors.js";
46
- export { formatCodexUsageReport, formatCodexUsageStatusline } from "./format.js";
47
- export { normalizeAppServerResponse, normalizeBackendPayload } from "./normalize-codex.js";
48
- export type {
49
- AnthropicUsageReport,
50
- CodexUsageReport,
51
- NormalizedCredits,
52
- NormalizedRateLimitSnapshot,
53
- NormalizedRateLimitWindow,
54
- ProviderUsageModel,
55
- } from "./types.js";
56
-
57
- export default function usageExtension(pi: ExtensionAPI) {
58
- registerUsageCommand(pi);
59
-
60
- const ensureUsageFooter = (ctx: ExtensionContext) => {
61
- if (isFooterRegistered() || !ctx.hasUI) return;
62
- try {
63
- installUsageFooter(pi, ctx);
64
- setFooterRegistered(true);
65
- } catch (error) {
66
- if (!handleStaleContextError(ctx, error)) throw error;
67
- }
68
- };
69
-
70
- pi.on("session_start", (_event, ctx) => {
71
- setSessionActive(true);
72
- ensureUsageFooter(ctx);
73
- if (isUsageSupportedModel(ctx.model)) {
74
- void refreshCurrentUsageStatusline(ctx, ctx.model).catch(rethrowUnlessStaleContextError(ctx));
75
- } else {
76
- clearUsageStatusline(ctx);
77
- }
78
- });
79
-
80
- pi.on("session_tree", (_event, ctx) => {
81
- if (isUsageSupportedModel(ctx.model)) {
82
- void refreshCurrentUsageStatusline(ctx, ctx.model).catch(rethrowUnlessStaleContextError(ctx));
83
- } else {
84
- clearUsageStatusline(ctx);
85
- }
86
- });
87
-
88
- pi.on("model_select", (event, ctx) => {
89
- if (isUsageSupportedModel(event.model)) {
90
- void refreshCurrentUsageStatusline(ctx, event.model).catch(rethrowUnlessStaleContextError(ctx));
91
- } else {
92
- clearUsageStatusline(ctx);
93
- }
94
- });
95
-
96
- pi.on("session_shutdown", (_event, ctx) => {
97
- setSessionActive(false);
98
- clearUsageStatusline(ctx);
99
- });
100
- }
package/src/models.ts DELETED
@@ -1,73 +0,0 @@
1
- import { ANTHROPIC_PROVIDER_ID, CODEX_PROVIDER_ID } from "./constants.js";
2
- import type { PiModel, UsageReport } from "./types.js";
3
-
4
- export function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boolean {
5
- return model?.provider === CODEX_PROVIDER_ID;
6
- }
7
-
8
- export function isAnthropicModel(model: Pick<PiModel, "provider"> | undefined): boolean {
9
- return model?.provider === ANTHROPIC_PROVIDER_ID;
10
- }
11
-
12
- export function isUsageSupportedModel(model: Pick<PiModel, "provider"> | undefined): boolean {
13
- return isOpenAICodexModel(model) || isAnthropicModel(model);
14
- }
15
-
16
- export function reportMatchesModel(report: UsageReport, model: Pick<PiModel, "provider"> | undefined): boolean {
17
- if (!model) return false;
18
- return (
19
- (report.provider === "codex" && isOpenAICodexModel(model)) ||
20
- (report.provider === "anthropic" && isAnthropicModel(model))
21
- );
22
- }
23
-
24
- export function providerKeyForModel(model: Pick<PiModel, "provider"> | undefined): "codex" | "anthropic" {
25
- if (isAnthropicModel(model)) return "anthropic";
26
- return "codex";
27
- }
28
-
29
- export function codexAuthCandidateModels(ctx: {
30
- model?: PiModel;
31
- modelRegistry: {
32
- getAvailable: () => PiModel[];
33
- getAll: () => PiModel[];
34
- };
35
- }): PiModel[] {
36
- return providerAuthCandidateModels(ctx, CODEX_PROVIDER_ID);
37
- }
38
-
39
- export function anthropicAuthCandidateModels(ctx: {
40
- model?: PiModel;
41
- modelRegistry: {
42
- getAvailable: () => PiModel[];
43
- getAll: () => PiModel[];
44
- };
45
- }): PiModel[] {
46
- return providerAuthCandidateModels(ctx, ANTHROPIC_PROVIDER_ID);
47
- }
48
-
49
- function providerAuthCandidateModels(
50
- ctx: {
51
- model?: PiModel;
52
- modelRegistry: {
53
- getAvailable: () => PiModel[];
54
- getAll: () => PiModel[];
55
- };
56
- },
57
- providerId: string,
58
- ): PiModel[] {
59
- const candidates: PiModel[] = [];
60
- const seen = new Set<string>();
61
- const add = (model: PiModel | undefined) => {
62
- if (!model || model.provider !== providerId) return;
63
- const key = `${model.provider}/${model.id}`;
64
- if (seen.has(key)) return;
65
- seen.add(key);
66
- candidates.push(model);
67
- };
68
-
69
- add(ctx.model);
70
- for (const model of ctx.modelRegistry.getAvailable()) add(model);
71
- for (const model of ctx.modelRegistry.getAll()) add(model);
72
- return candidates;
73
- }
@@ -1,160 +0,0 @@
1
- import { ANTHROPIC_NON_WINDOW_KEYS } from "./constants.js";
2
- import { progressBarUsed } from "./format.js";
3
- import type {
4
- AnthropicExtraUsage,
5
- AnthropicOAuthUsagePayload,
6
- AnthropicOAuthWindow,
7
- AnthropicUsageReport,
8
- } from "./types.js";
9
- import {
10
- asBoolean,
11
- asNumber,
12
- asString,
13
- assertObject,
14
- clampPercent,
15
- compactLimitLabel,
16
- formatCurrencyAmount,
17
- } from "./utils.js";
18
-
19
- export function normalizeAnthropicUsagePayload(
20
- payload: AnthropicOAuthUsagePayload,
21
- capturedAt: number,
22
- ): AnthropicUsageReport {
23
- const lines = [" >_ Anthropic Usage", ""];
24
- const statusParts: string[] = ["claude"];
25
-
26
- const enterpriseWindows: {
27
- key: string;
28
- usedPercent: number;
29
- usedDollars: number;
30
- limitDollars: number;
31
- resetsAt?: string;
32
- }[] = [];
33
- for (const [key, rawValue] of Object.entries(payload)) {
34
- if (ANTHROPIC_NON_WINDOW_KEYS.has(key)) continue;
35
- if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) continue;
36
- const value = rawValue as AnthropicOAuthWindow;
37
- const limitDollars = asNumber(value.limit_dollars);
38
- const usedDollars = asNumber(value.used_dollars);
39
- if (limitDollars === undefined || usedDollars === undefined) continue;
40
- enterpriseWindows.push({
41
- key,
42
- usedPercent: clampPercent(asNumber(value.utilization) ?? 0),
43
- usedDollars,
44
- limitDollars,
45
- resetsAt: asString(value.resets_at),
46
- });
47
- }
48
-
49
- let hasAnySection = false;
50
-
51
- // Section 1: enterprise budget windows
52
- if (enterpriseWindows.length > 0) {
53
- hasAnySection = true;
54
- enterpriseWindows.sort((left, right) => left.limitDollars - right.limitDollars);
55
- lines.push(" Enterprise budget windows:");
56
- for (const window of enterpriseWindows) {
57
- const label = compactLimitLabel(window.key).replace(/\b\w/g, (char) => char.toUpperCase());
58
- const reset = window.resetsAt ? ` (resets ${formatIsoReset(window.resetsAt)})` : "";
59
- lines.push(
60
- ` ${label}: ${progressBarUsed(window.usedPercent)} ${window.usedPercent.toFixed(0)}% used (${formatCurrencyAmount(window.usedDollars, "USD")}/${formatCurrencyAmount(window.limitDollars, "USD")})${reset}`,
61
- );
62
- }
63
- // Statusline shows the window that's actually moving: the one with the
64
- // highest utilization (not just the smallest limit).
65
- const primary = [...enterpriseWindows].sort((left, right) => right.usedPercent - left.usedPercent)[0];
66
- statusParts.push(`${primary.usedPercent.toFixed(0)}%`);
67
- statusParts.push(
68
- `${formatCurrencyAmount(primary.usedDollars, "USD", 0)}/${formatCurrencyAmount(primary.limitDollars, "USD", 0)}`,
69
- );
70
- }
71
-
72
- // Section 2: monthly extra usage (pay-per-use overflow). Shown alongside
73
- // enterprise windows — both are real spend and both belong in the statusline.
74
- const extraUsageRaw = payload.extra_usage;
75
- if (extraUsageRaw) {
76
- const extraUsage = assertObject(extraUsageRaw, "Anthropic extra usage") as AnthropicExtraUsage;
77
- const enabled = asBoolean(extraUsage.is_enabled);
78
- const usedCredits = asNumber(extraUsage.used_credits);
79
- if (enabled && usedCredits !== undefined) {
80
- if (hasAnySection) lines.push("");
81
- const hadStatusSegment = statusParts.length > 1;
82
- hasAnySection = true;
83
- const currency = asString(extraUsage.currency) ?? "USD";
84
- // used_credits / monthly_limit are minor units (cents): 30000 => 300.00
85
- const usedMajor = usedCredits / 100;
86
- const monthlyLimitMajor =
87
- asNumber(extraUsage.monthly_limit) !== undefined
88
- ? (asNumber(extraUsage.monthly_limit) as number) / 100
89
- : undefined;
90
- const usedPercent = clampPercent(asNumber(extraUsage.utilization) ?? 0);
91
- const amount = `${formatCurrencyAmount(usedMajor, currency)}${monthlyLimitMajor !== undefined ? `/${formatCurrencyAmount(monthlyLimitMajor, currency)}` : ""}`;
92
- const amountCompact = `${formatCurrencyAmount(usedMajor, currency, 0)}${monthlyLimitMajor !== undefined ? `/${formatCurrencyAmount(monthlyLimitMajor, currency, 0)}` : ""}`;
93
- const resetsAt = asString(extraUsage.reset_at) ?? asString(extraUsage.resets_at);
94
- const reset = resetsAt ? ` (resets ${formatIsoReset(resetsAt)})` : "";
95
- lines.push(" Monthly extra usage:");
96
- lines.push(` ${progressBarUsed(usedPercent)} ${usedPercent.toFixed(0)}% used ${amount}${reset}`);
97
- // Append alongside any enterprise-window segment already added — don't
98
- // overwrite it. Label with "extra" only when it's not the sole segment,
99
- // so accounts with just extra usage keep the simpler unlabeled form.
100
- statusParts.push(`${usedPercent.toFixed(0)}%`);
101
- statusParts.push(amountCompact);
102
- if (hadStatusSegment) statusParts.push("extra");
103
- }
104
- }
105
-
106
- // Section 3: subscription rolling windows
107
- const fiveHour = normalizeAnthropicRollingWindow(payload.five_hour);
108
- const sevenDay = normalizeAnthropicRollingWindow(payload.seven_day);
109
- if (fiveHour || sevenDay) {
110
- if (hasAnySection) lines.push("");
111
- hasAnySection = true;
112
- lines.push(" Subscription usage:");
113
- if (fiveHour) lines.push(` ${formatAnthropicRollingWindow("5h", fiveHour, false)}`);
114
- if (sevenDay) lines.push(` ${formatAnthropicRollingWindow("7d", sevenDay, true)}`);
115
- if (statusParts.length === 1) {
116
- if (fiveHour) statusParts.push(`${fiveHour.usedPercent.toFixed(0)}% 5h`);
117
- if (sevenDay) statusParts.push(`${sevenDay.usedPercent.toFixed(0)}% 7d`);
118
- }
119
- }
120
-
121
- if (!hasAnySection) {
122
- throw new Error("Anthropic usage endpoint returned no displayable usage data.");
123
- }
124
-
125
- return {
126
- provider: "anthropic",
127
- source: "anthropic-oauth",
128
- capturedAt,
129
- summaryLines: lines,
130
- statusline: statusParts.join(" "),
131
- };
132
- }
133
-
134
- function normalizeAnthropicRollingWindow(value: unknown): { usedPercent: number; resetsAt?: string } | undefined {
135
- if (!value) return undefined;
136
- const window = assertObject(value, "Anthropic rolling usage window") as AnthropicOAuthWindow;
137
- const usedPercent = asNumber(window.utilization);
138
- if (usedPercent === undefined) return undefined;
139
- return { usedPercent: clampPercent(usedPercent), resetsAt: asString(window.resets_at) };
140
- }
141
-
142
- function formatAnthropicRollingWindow(
143
- label: string,
144
- window: { usedPercent: number; resetsAt?: string },
145
- showDay: boolean,
146
- ): string {
147
- const reset = window.resetsAt ? ` (resets ${formatIsoReset(window.resetsAt, showDay)})` : "";
148
- return `${label}: ${progressBarUsed(window.usedPercent)} ${window.usedPercent.toFixed(0)}% used${reset}`;
149
- }
150
-
151
- function formatIsoReset(value: string, showDay = true): string {
152
- const reset = new Date(value);
153
- if (Number.isNaN(reset.getTime())) return "at an unknown time";
154
- const now = new Date();
155
- const time = `${reset.getHours().toString().padStart(2, "0")}:${reset.getMinutes().toString().padStart(2, "0")}`;
156
- if (!showDay && reset.toDateString() === now.toDateString()) return time;
157
- const day = reset.getDate().toString();
158
- const month = reset.toLocaleDateString(undefined, { month: "short" });
159
- return `${time} on ${day} ${month}`;
160
- }