@diegopetrucci/pi-extensions 0.1.73 → 0.1.76

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.
Files changed (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +71 -21
  54. package/extensions/fast/index.ts +513 -313
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -14,271 +14,276 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
14
14
  export type UsageAvailability = "unsupported" | "unavailable";
15
15
 
16
16
  interface WhamUsageWindow {
17
- limit_window_seconds?: number;
18
- reset_at?: number;
19
- used_percent?: number;
17
+ limit_window_seconds?: number;
18
+ reset_at?: number;
19
+ used_percent?: number;
20
20
  }
21
21
 
22
22
  interface WhamUsageResponse {
23
- rate_limit?: {
24
- primary_window?: WhamUsageWindow;
25
- secondary_window?: WhamUsageWindow;
26
- };
23
+ rate_limit?: {
24
+ primary_window?: WhamUsageWindow;
25
+ secondary_window?: WhamUsageWindow;
26
+ };
27
27
  }
28
28
 
29
29
  export interface UsageWindow {
30
- usedPercent?: number;
31
- windowSeconds?: number;
32
- resetAt?: number;
30
+ usedPercent?: number;
31
+ windowSeconds?: number;
32
+ resetAt?: number;
33
33
  }
34
34
 
35
35
  export interface UsageSnapshot {
36
- primary?: UsageWindow;
37
- secondary?: UsageWindow;
38
- fetchedAt: number;
36
+ primary?: UsageWindow;
37
+ secondary?: UsageWindow;
38
+ fetchedAt: number;
39
39
  }
40
40
 
41
41
  export interface UsageSummaryWindowsConfig {
42
- primary: {
43
- enabled: boolean;
44
- label: string;
45
- };
46
- secondary: {
47
- enabled: boolean;
48
- label: string;
49
- };
42
+ primary: {
43
+ enabled: boolean;
44
+ label: string;
45
+ };
46
+ secondary: {
47
+ enabled: boolean;
48
+ label: string;
49
+ };
50
50
  }
51
51
 
52
52
  function normalizeUsedPercent(value?: number): number | undefined {
53
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
54
- return Math.min(100, Math.max(0, value));
53
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
54
+ return Math.min(100, Math.max(0, value));
55
55
  }
56
56
 
57
57
  function normalizeWindowSeconds(value?: number): number | undefined {
58
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined;
59
- return value;
58
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined;
59
+ return value;
60
60
  }
61
61
 
62
62
  function normalizeResetAt(value?: number): number | undefined {
63
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
64
- return value * 1000;
63
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
64
+ return value * 1000;
65
65
  }
66
66
 
67
67
  function parseUsageWindow(window?: WhamUsageWindow): UsageWindow | undefined {
68
- if (!window) return undefined;
69
- const usedPercent = normalizeUsedPercent(window.used_percent);
70
- const windowSeconds = normalizeWindowSeconds(window.limit_window_seconds);
71
- const resetAt = normalizeResetAt(window.reset_at);
72
- if (usedPercent === undefined && windowSeconds === undefined && resetAt === undefined) return undefined;
73
- return { usedPercent, windowSeconds, resetAt };
68
+ if (!window) return undefined;
69
+ const usedPercent = normalizeUsedPercent(window.used_percent);
70
+ const windowSeconds = normalizeWindowSeconds(window.limit_window_seconds);
71
+ const resetAt = normalizeResetAt(window.reset_at);
72
+ if (usedPercent === undefined && windowSeconds === undefined && resetAt === undefined)
73
+ return undefined;
74
+ return { usedPercent, windowSeconds, resetAt };
74
75
  }
75
76
 
76
77
  function parseUsageSnapshot(data: WhamUsageResponse): Omit<UsageSnapshot, "fetchedAt"> {
77
- return {
78
- primary: parseUsageWindow(data.rate_limit?.primary_window),
79
- secondary: parseUsageWindow(data.rate_limit?.secondary_window),
80
- };
78
+ return {
79
+ primary: parseUsageWindow(data.rate_limit?.primary_window),
80
+ secondary: parseUsageWindow(data.rate_limit?.secondary_window),
81
+ };
81
82
  }
82
83
 
83
84
  function isRecord(value: unknown): value is Record<string, unknown> {
84
- return typeof value === "object" && value !== null;
85
+ return typeof value === "object" && value !== null;
85
86
  }
86
87
 
87
88
  function readStoredProviderCredential(providerId: string): unknown {
88
- try {
89
- const data = JSON.parse(readFileSync(join(getAgentDir(), "auth.json"), "utf-8")) as unknown;
90
- if (!isRecord(data)) return undefined;
91
- return data[providerId];
92
- } catch {
93
- return undefined;
94
- }
89
+ try {
90
+ const data = JSON.parse(readFileSync(join(getAgentDir(), "auth.json"), "utf-8")) as unknown;
91
+ if (!isRecord(data)) return undefined;
92
+ return data[providerId];
93
+ } catch {
94
+ return undefined;
95
+ }
95
96
  }
96
97
 
97
98
  async function getAccessToken(authSource: unknown): Promise<string | undefined> {
98
- if (isRecord(authSource) && typeof authSource.getProviderAuth === "function") {
99
- const result = await (authSource as {
100
- getProviderAuth(providerId: string): Promise<unknown>;
101
- }).getProviderAuth(PROVIDER_ID);
102
- if (isRecord(result) && isRecord(result.auth) && typeof result.auth.apiKey === "string") {
103
- return result.auth.apiKey;
104
- }
105
- }
106
-
107
- if (isRecord(authSource) && typeof authSource.getApiKey === "function") {
108
- const token = await (authSource as {
109
- getApiKey(providerId: string, options: { includeFallback: boolean }): Promise<unknown>;
110
- }).getApiKey(PROVIDER_ID, { includeFallback: false });
111
- return typeof token === "string" ? token : undefined;
112
- }
113
-
114
- const credential = readStoredProviderCredential(PROVIDER_ID);
115
- if (!isRecord(credential) || credential.type !== "oauth") return undefined;
116
- return typeof credential.access === "string" ? credential.access : undefined;
99
+ if (isRecord(authSource) && typeof authSource.getProviderAuth === "function") {
100
+ const result = await (
101
+ authSource as {
102
+ getProviderAuth(providerId: string): Promise<unknown>;
103
+ }
104
+ ).getProviderAuth(PROVIDER_ID);
105
+ if (isRecord(result) && isRecord(result.auth) && typeof result.auth.apiKey === "string") {
106
+ return result.auth.apiKey;
107
+ }
108
+ }
109
+
110
+ if (isRecord(authSource) && typeof authSource.getApiKey === "function") {
111
+ const token = await (
112
+ authSource as {
113
+ getApiKey(providerId: string, options: { includeFallback: boolean }): Promise<unknown>;
114
+ }
115
+ ).getApiKey(PROVIDER_ID, { includeFallback: false });
116
+ return typeof token === "string" ? token : undefined;
117
+ }
118
+
119
+ const credential = readStoredProviderCredential(PROVIDER_ID);
120
+ if (!isRecord(credential) || credential.type !== "oauth") return undefined;
121
+ return typeof credential.access === "string" ? credential.access : undefined;
117
122
  }
118
123
 
119
124
  function getOAuthAccountId(authSource: unknown): string | undefined {
120
- let credential: unknown;
121
-
122
- if (
123
- isRecord(authSource) &&
124
- typeof authSource.getApiKey === "function" &&
125
- typeof authSource.get === "function"
126
- ) {
127
- if (typeof authSource.reload === "function") authSource.reload();
128
- credential = (authSource as { get(providerId: string): unknown }).get(PROVIDER_ID);
129
- } else {
130
- credential = readStoredProviderCredential(PROVIDER_ID);
131
- }
132
-
133
- if (!isRecord(credential) || credential.type !== "oauth") return undefined;
134
- const accountId = credential.accountId;
135
- return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
125
+ let credential: unknown;
126
+
127
+ if (
128
+ isRecord(authSource) &&
129
+ typeof authSource.getApiKey === "function" &&
130
+ typeof authSource.get === "function"
131
+ ) {
132
+ if (typeof authSource.reload === "function") authSource.reload();
133
+ credential = (authSource as { get(providerId: string): unknown }).get(PROVIDER_ID);
134
+ } else {
135
+ credential = readStoredProviderCredential(PROVIDER_ID);
136
+ }
137
+
138
+ if (!isRecord(credential) || credential.type !== "oauth") return undefined;
139
+ const accountId = credential.accountId;
140
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
136
141
  }
137
142
 
138
143
  function getAccountIdFromAccessToken(accessToken: string): string | undefined {
139
- try {
140
- // Keep this parser aligned with Pi's openai-codex OAuth implementation:
141
- // the account identity is the exact JWT claim used when credentials are
142
- // created, not a separately loaded account option.
143
- const parts = accessToken.split(".");
144
- if (parts.length !== 3) return undefined;
145
- const payload = JSON.parse(atob(parts[1] ?? "")) as unknown;
146
- if (!isRecord(payload)) return undefined;
147
- const auth = payload[JWT_CLAIM_PATH];
148
- if (!isRecord(auth)) return undefined;
149
- const accountId = auth.chatgpt_account_id;
150
- return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
151
- } catch {
152
- return undefined;
153
- }
144
+ try {
145
+ // Keep this parser aligned with Pi's openai-codex OAuth implementation:
146
+ // the account identity is the exact JWT claim used when credentials are
147
+ // created, not a separately loaded account option.
148
+ const parts = accessToken.split(".");
149
+ if (parts.length !== 3) return undefined;
150
+ const payload = JSON.parse(atob(parts[1] ?? "")) as unknown;
151
+ if (!isRecord(payload)) return undefined;
152
+ const auth = payload[JWT_CLAIM_PATH];
153
+ if (!isRecord(auth)) return undefined;
154
+ const accountId = auth.chatgpt_account_id;
155
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
156
+ } catch {
157
+ return undefined;
158
+ }
154
159
  }
155
160
 
156
161
  export function getOpenAICodexAccountId(authSource: unknown): string | undefined {
157
- try {
158
- return getOAuthAccountId(authSource);
159
- } catch {
160
- return undefined;
161
- }
162
+ try {
163
+ return getOAuthAccountId(authSource);
164
+ } catch {
165
+ return undefined;
166
+ }
162
167
  }
163
168
 
164
169
  function formatUsagePercent(value?: number): string | undefined {
165
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
166
- return `${Math.round(value)}%`;
170
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
171
+ return `${Math.round(value)}%`;
167
172
  }
168
173
 
169
174
  // Primary/secondary describe positions, not fixed periods. Match each reported
170
175
  // duration the same way Codex does, allowing small backend rounding differences.
171
176
  const KNOWN_WINDOW_LABELS: ReadonlyArray<{ seconds: number; label: string }> = [
172
- { seconds: 5 * 60 * 60, label: "5h" },
173
- { seconds: 24 * 60 * 60, label: "1d" },
174
- { seconds: 7 * 24 * 60 * 60, label: "7d" },
175
- { seconds: 30 * 24 * 60 * 60, label: "30d" },
176
- { seconds: 365 * 24 * 60 * 60, label: "365d" },
177
+ { seconds: 5 * 60 * 60, label: "5h" },
178
+ { seconds: 24 * 60 * 60, label: "1d" },
179
+ { seconds: 7 * 24 * 60 * 60, label: "7d" },
180
+ { seconds: 30 * 24 * 60 * 60, label: "30d" },
181
+ { seconds: 365 * 24 * 60 * 60, label: "365d" },
177
182
  ];
178
183
 
179
184
  function isApproximateWindow(actualSeconds: number, expectedSeconds: number): boolean {
180
- return Math.abs(actualSeconds - expectedSeconds) <= expectedSeconds * 0.05;
185
+ return Math.abs(actualSeconds - expectedSeconds) <= expectedSeconds * 0.05;
181
186
  }
182
187
 
183
188
  function formatUsageWindowLabel(
184
- window: UsageWindow | undefined,
185
- configuredLabel: string,
186
- fallbackLabel: string,
189
+ window: UsageWindow | undefined,
190
+ configuredLabel: string,
191
+ fallbackLabel: string,
187
192
  ): string {
188
- if (configuredLabel.toLowerCase() !== "auto") return configuredLabel;
189
- const windowSeconds = window?.windowSeconds;
190
- if (windowSeconds !== undefined) {
191
- const knownWindow = KNOWN_WINDOW_LABELS.find(({ seconds }) =>
192
- isApproximateWindow(windowSeconds, seconds)
193
- );
194
- if (knownWindow) return knownWindow.label;
195
- }
196
- return fallbackLabel;
193
+ if (configuredLabel.toLowerCase() !== "auto") return configuredLabel;
194
+ const windowSeconds = window?.windowSeconds;
195
+ if (windowSeconds !== undefined) {
196
+ const knownWindow = KNOWN_WINDOW_LABELS.find(({ seconds }) =>
197
+ isApproximateWindow(windowSeconds, seconds),
198
+ );
199
+ if (knownWindow) return knownWindow.label;
200
+ }
201
+ return fallbackLabel;
197
202
  }
198
203
 
199
204
  export function isOpenAICodexProvider(provider?: string): boolean {
200
- return provider === PROVIDER_ID;
205
+ return provider === PROVIDER_ID;
201
206
  }
202
207
 
203
208
  export function isOpenAIProvider(provider?: string): boolean {
204
- return provider === "openai";
209
+ return provider === "openai";
205
210
  }
206
211
 
207
212
  export function formatUsageStatus(status?: UsageAvailability): string | undefined {
208
- if (status === "unsupported") return "usage unsupported";
209
- if (status === "unavailable") return "usage unavailable";
210
- return undefined;
213
+ if (status === "unsupported") return "usage unsupported";
214
+ if (status === "unavailable") return "usage unavailable";
215
+ return undefined;
211
216
  }
212
217
 
213
218
  export function formatUsageSummary(
214
- snapshot: UsageSnapshot | undefined,
215
- windows: UsageSummaryWindowsConfig,
219
+ snapshot: UsageSnapshot | undefined,
220
+ windows: UsageSummaryWindowsConfig,
216
221
  ): string | undefined {
217
- if (!snapshot) return undefined;
218
-
219
- const primary = formatUsagePercent(snapshot.primary?.usedPercent);
220
- const secondary = formatUsagePercent(snapshot.secondary?.usedPercent);
221
- const parts: string[] = [];
222
-
223
- if (windows.primary.enabled && primary) {
224
- const label = formatUsageWindowLabel(snapshot.primary, windows.primary.label, "usage");
225
- parts.push(`${label} ${primary}`);
226
- }
227
- if (windows.secondary.enabled && secondary) {
228
- const label = formatUsageWindowLabel(
229
- snapshot.secondary,
230
- windows.secondary.label,
231
- "secondary usage",
232
- );
233
- parts.push(`${label} ${secondary}`);
234
- }
235
-
236
- return parts.length > 0 ? parts.join(" · ") : undefined;
222
+ if (!snapshot) return undefined;
223
+
224
+ const primary = formatUsagePercent(snapshot.primary?.usedPercent);
225
+ const secondary = formatUsagePercent(snapshot.secondary?.usedPercent);
226
+ const parts: string[] = [];
227
+
228
+ if (windows.primary.enabled && primary) {
229
+ const label = formatUsageWindowLabel(snapshot.primary, windows.primary.label, "usage");
230
+ parts.push(`${label} ${primary}`);
231
+ }
232
+ if (windows.secondary.enabled && secondary) {
233
+ const label = formatUsageWindowLabel(
234
+ snapshot.secondary,
235
+ windows.secondary.label,
236
+ "secondary usage",
237
+ );
238
+ parts.push(`${label} ${secondary}`);
239
+ }
240
+
241
+ return parts.length > 0 ? parts.join(" · ") : undefined;
237
242
  }
238
243
 
239
244
  export async function fetchOpenAICodexUsage(
240
- authSource: unknown,
241
- options?: { timeoutMs?: number; accountId?: string },
245
+ authSource: unknown,
246
+ options?: { timeoutMs?: number; accountId?: string },
242
247
  ): Promise<UsageSnapshot | undefined> {
243
- const accessToken = await getAccessToken(authSource);
244
- if (!accessToken) return undefined;
245
-
246
- // Pi derives the Codex credential's accountId from this access-token claim.
247
- // Resolve it before constructing the request so a stale account option can
248
- // never become the header, and reject credentials that cannot be bound to an
249
- // account at all.
250
- const tokenAccountId = getAccountIdFromAccessToken(accessToken);
251
- if (!tokenAccountId) return undefined;
252
-
253
- // A lifecycle caller may provide the account identity observed at the
254
- // refresh boundary. It is only an expected identity: the token claim remains
255
- // authoritative, and any mismatch fails closed before fetch is called.
256
- if (options && "accountId" in options && options.accountId !== tokenAccountId) {
257
- return undefined;
258
- }
259
-
260
- const controller = new AbortController();
261
- const timeoutMs = options?.timeoutMs ?? 10_000;
262
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
263
-
264
- try {
265
- const headers: Record<string, string> = {
266
- Authorization: `Bearer ${accessToken}`,
267
- Accept: "application/json",
268
- };
269
- headers["ChatGPT-Account-Id"] = tokenAccountId;
270
-
271
- const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
272
- headers,
273
- signal: controller.signal,
274
- });
275
- if (!response.ok) {
276
- throw new Error(`Usage request failed: ${response.status}`);
277
- }
278
-
279
- const data = (await response.json()) as WhamUsageResponse;
280
- return { ...parseUsageSnapshot(data), fetchedAt: Date.now() };
281
- } finally {
282
- clearTimeout(timeout);
283
- }
248
+ const accessToken = await getAccessToken(authSource);
249
+ if (!accessToken) return undefined;
250
+
251
+ // Pi derives the Codex credential's accountId from this access-token claim.
252
+ // Resolve it before constructing the request so a stale account option can
253
+ // never become the header, and reject credentials that cannot be bound to an
254
+ // account at all.
255
+ const tokenAccountId = getAccountIdFromAccessToken(accessToken);
256
+ if (!tokenAccountId) return undefined;
257
+
258
+ // A lifecycle caller may provide the account identity observed at the
259
+ // refresh boundary. It is only an expected identity: the token claim remains
260
+ // authoritative, and any mismatch fails closed before fetch is called.
261
+ if (options && "accountId" in options && options.accountId !== tokenAccountId) {
262
+ return undefined;
263
+ }
264
+
265
+ const controller = new AbortController();
266
+ const timeoutMs = options?.timeoutMs ?? 10_000;
267
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
268
+
269
+ try {
270
+ const headers: Record<string, string> = {
271
+ Authorization: `Bearer ${accessToken}`,
272
+ Accept: "application/json",
273
+ };
274
+ headers["ChatGPT-Account-Id"] = tokenAccountId;
275
+
276
+ const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
277
+ headers,
278
+ signal: controller.signal,
279
+ });
280
+ if (!response.ok) {
281
+ throw new Error(`Usage request failed: ${response.status}`);
282
+ }
283
+
284
+ const data = (await response.json()) as WhamUsageResponse;
285
+ return { ...parseUsageSnapshot(data), fetchedAt: Date.now() };
286
+ } finally {
287
+ clearTimeout(timeout);
288
+ }
284
289
  }