@llblab/pi-codex-usage 0.3.3 โ†’ 0.3.5

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/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
 
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type {
5
+ ExtensionAPI,
6
+ ExtensionContext,
7
+ } from "@earendil-works/pi-coding-agent";
5
8
 
6
9
  const CODEX_PROVIDER_ID = "openai-codex";
7
10
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -12,22 +15,22 @@ const STATUS_KEY = "codex-usage";
12
15
  const MAX_ERROR_BODY_CHARS = 600;
13
16
  const STATUS_LABEL_TEXT = "codex";
14
17
  const DUAL_BAR_CHARS = [
15
- " ",
16
- "๐œน‘",
17
- "๐œน’",
18
- "๐œน“",
19
- "๐œน ",
20
- "๐œนก",
21
- "๐œนข",
22
- "๐œนฃ",
23
- "๐œนฐ",
24
- "๐œนฑ",
25
- "๐œนฒ",
26
- "๐œนณ",
27
- "๐œบ€",
28
- "๐œบ",
29
- "๐œบ‚",
30
- "๐œบƒ",
18
+ " ",
19
+ "โ–˜",
20
+ "โ–",
21
+ "โ–€",
22
+ "โ––",
23
+ "โ–Œ",
24
+ "โ–ž",
25
+ "โ–›",
26
+ "โ–—",
27
+ "โ–š",
28
+ "โ–",
29
+ "โ–œ",
30
+ "โ–„",
31
+ "โ–™",
32
+ "โ–Ÿ",
33
+ "โ–ˆ",
31
34
  ];
32
35
 
33
36
  type UsageSource = "pi-auth" | "codex-app-server";
@@ -36,811 +39,817 @@ type PiModel = NonNullable<ExtensionContext["model"]>;
36
39
  export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
37
40
 
38
41
  type QueryUsageOptions = {
39
- timeoutMs: number;
42
+ timeoutMs: number;
40
43
  };
41
44
 
42
45
  type CachedReport = {
43
- createdAt: number;
44
- report: CodexUsageReport;
46
+ createdAt: number;
47
+ report: CodexUsageReport;
45
48
  };
46
49
 
47
50
  type QueryUsageResult =
48
- | { ok: true; report: CodexUsageReport }
49
- | { ok: false; errors: UsageQueryError[] };
51
+ | { ok: true; report: CodexUsageReport }
52
+ | { ok: false; errors: UsageQueryError[] };
50
53
 
51
54
  type UsageQueryError = {
52
- source: UsageSource;
53
- message: string;
54
- cause?: unknown;
55
+ source: UsageSource;
56
+ message: string;
57
+ cause?: unknown;
55
58
  };
56
59
 
57
60
  export type CodexUsageReport = {
58
- snapshots: NormalizedRateLimitSnapshot[];
61
+ snapshots: NormalizedRateLimitSnapshot[];
59
62
  };
60
63
 
61
64
  export type NormalizedRateLimitSnapshot = {
62
- limitId: string;
63
- primary?: NormalizedRateLimitWindow;
64
- secondary?: NormalizedRateLimitWindow;
65
+ limitId: string;
66
+ primary?: NormalizedRateLimitWindow;
67
+ secondary?: NormalizedRateLimitWindow;
65
68
  };
66
69
 
67
70
  export type NormalizedRateLimitWindow = {
68
- usedPercent: number;
71
+ usedPercent: number;
69
72
  };
70
73
 
71
74
  type RateLimitStatusPayload = {
72
- rate_limit?: unknown;
75
+ rate_limit?: unknown;
73
76
  };
74
77
 
75
78
  type BackendRateLimitDetails = {
76
- primary_window?: unknown;
77
- secondary_window?: unknown;
79
+ primary_window?: unknown;
80
+ secondary_window?: unknown;
78
81
  };
79
82
 
80
83
  type BackendWindowSnapshot = {
81
- used_percent?: unknown;
84
+ used_percent?: unknown;
82
85
  };
83
86
 
84
87
  type AppServerRateLimitResponse = {
85
- rateLimits?: unknown;
88
+ rateLimits?: unknown;
86
89
  };
87
90
 
88
91
  type AppServerRateLimitSnapshot = {
89
- limitId?: unknown;
90
- primary?: unknown;
91
- secondary?: unknown;
92
+ limitId?: unknown;
93
+ primary?: unknown;
94
+ secondary?: unknown;
92
95
  };
93
96
 
94
97
  type AppServerWindowSnapshot = {
95
- usedPercent?: unknown;
98
+ usedPercent?: unknown;
96
99
  };
97
100
 
98
101
  type RpcResponse = {
99
- id?: unknown;
100
- result?: unknown;
101
- error?: { message?: unknown; code?: unknown };
102
+ id?: unknown;
103
+ result?: unknown;
104
+ error?: { message?: unknown; code?: unknown };
102
105
  };
103
106
 
104
107
  type PendingRpc = {
105
- resolve: (value: unknown) => void;
106
- reject: (error: Error) => void;
108
+ resolve: (value: unknown) => void;
109
+ reject: (error: Error) => void;
107
110
  };
108
111
 
109
112
  export default function codexUsage(pi: ExtensionAPI) {
110
- let cache: CachedReport | undefined;
111
- let failedRefreshes = 0;
112
- let statuslineBlinkTimer: TimeoutHandle | undefined;
113
- let statuslineClearTimer: TimeoutHandle | undefined;
114
- let statuslineRefreshTimer: TimeoutHandle | undefined;
115
- let statuslineRequestId = 0;
116
-
117
- const clearStatuslineTimers = () => {
118
- if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
119
- if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
120
- if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
121
- statuslineBlinkTimer = undefined;
122
- statuslineClearTimer = undefined;
123
- statuslineRefreshTimer = undefined;
124
- };
125
-
126
- const clearUsageStatusline = (ctx: ExtensionContext) => {
127
- statuslineRequestId += 1;
128
- clearStatuslineTimers();
129
- ctx.ui.setStatus(STATUS_KEY, undefined);
130
- };
131
-
132
- const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
133
- if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
134
- statuslineClearTimer = setTimeout(() => {
135
- ctx.ui.setStatus(STATUS_KEY, undefined);
136
- statuslineClearTimer = undefined;
137
- }, REFRESH_INTERVAL_MS) as TimeoutHandle;
138
- statuslineClearTimer.unref?.();
139
- };
140
-
141
- const scheduleStatuslineRefresh = (ctx: ExtensionContext) => {
142
- if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
143
- statuslineRefreshTimer = setTimeout(() => {
144
- void refreshCurrentCodexUsageStatusline(ctx, true);
145
- }, REFRESH_INTERVAL_MS) as TimeoutHandle;
146
- statuslineRefreshTimer.unref?.();
147
- };
148
-
149
- const setUsageStatusline = (
150
- ctx: ExtensionContext,
151
- report: CodexUsageReport,
152
- options: {
153
- autoRefresh: boolean;
154
- blink: boolean;
155
- model: CodexUsageModel | undefined;
156
- },
157
- ) => {
158
- if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
159
- if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
160
- statuslineBlinkTimer = undefined;
161
- statuslineClearTimer = undefined;
162
- const text = formatCodexUsageStatusline(report, ctx, options.model);
163
- if (options.blink) {
164
- ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
165
- statuslineBlinkTimer = setTimeout(() => {
166
- ctx.ui.setStatus(STATUS_KEY, text);
167
- statuslineBlinkTimer = undefined;
168
- }, REDRAW_BLINK_MS) as TimeoutHandle;
169
- statuslineBlinkTimer.unref?.();
170
- } else {
171
- ctx.ui.setStatus(STATUS_KEY, text);
172
- }
173
- if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
174
- else scheduleTemporaryStatuslineClear(ctx);
175
- };
176
-
177
- const refreshCurrentCodexUsageStatusline = async (
178
- ctx: ExtensionContext,
179
- force: boolean,
180
- model = ctx.model,
181
- ) => {
182
- if (!isOpenAICodexModel(model)) {
183
- clearUsageStatusline(ctx);
184
- return;
185
- }
186
-
187
- if (!cache) ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
188
- const requestId = statuslineRequestId + 1;
189
- statuslineRequestId = requestId;
190
- const cached =
191
- cache && Date.now() - cache.createdAt < REFRESH_INTERVAL_MS
192
- ? cache
193
- : undefined;
194
- if (cached && !force) {
195
- setUsageStatusline(ctx, cached.report, {
196
- autoRefresh: true,
197
- blink: false,
198
- model,
199
- });
200
- return;
201
- }
202
-
203
- const result = await queryUsage(ctx, { timeoutMs: DEFAULT_TIMEOUT_MS });
204
- if (requestId !== statuslineRequestId) return;
205
- if (!isOpenAICodexModel(ctx.model)) {
206
- clearUsageStatusline(ctx);
207
- return;
208
- }
209
-
210
- if (!result.ok) {
211
- failedRefreshes += 1;
212
- if (!cache || failedRefreshes >= 5) {
213
- ctx.ui.setStatus(STATUS_KEY, formatStatuslineProblem(ctx, result.errors));
214
- }
215
- scheduleStatuslineRefresh(ctx);
216
- return;
217
- }
218
-
219
- failedRefreshes = 0;
220
- cache = { createdAt: Date.now(), report: result.report };
221
- setUsageStatusline(ctx, result.report, {
222
- autoRefresh: true,
223
- blink: cache !== undefined,
224
- model,
225
- });
226
- };
227
-
228
- pi.on("session_start", (_event, ctx) => {
229
- if (isOpenAICodexModel(ctx.model))
230
- void refreshCurrentCodexUsageStatusline(ctx, false);
231
- else clearUsageStatusline(ctx);
232
- });
233
-
234
- pi.on("session_tree", (_event, ctx) => {
235
- if (isOpenAICodexModel(ctx.model))
236
- void refreshCurrentCodexUsageStatusline(ctx, false);
237
- else clearUsageStatusline(ctx);
238
- });
239
-
240
- pi.on("model_select", (event, ctx) => {
241
- if (isOpenAICodexModel(event.model)) {
242
- void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
243
- } else {
244
- clearUsageStatusline(ctx);
245
- }
246
- });
247
-
248
- pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
113
+ let cache: CachedReport | undefined;
114
+ let failedRefreshes = 0;
115
+ let statuslineBlinkTimer: TimeoutHandle | undefined;
116
+ let statuslineClearTimer: TimeoutHandle | undefined;
117
+ let statuslineRefreshTimer: TimeoutHandle | undefined;
118
+ let statuslineRequestId = 0;
119
+
120
+ const clearStatuslineTimers = () => {
121
+ if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
122
+ if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
123
+ if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
124
+ statuslineBlinkTimer = undefined;
125
+ statuslineClearTimer = undefined;
126
+ statuslineRefreshTimer = undefined;
127
+ };
128
+
129
+ const clearUsageStatusline = (ctx: ExtensionContext) => {
130
+ statuslineRequestId += 1;
131
+ clearStatuslineTimers();
132
+ ctx.ui.setStatus(STATUS_KEY, undefined);
133
+ };
134
+
135
+ const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
136
+ if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
137
+ statuslineClearTimer = setTimeout(() => {
138
+ ctx.ui.setStatus(STATUS_KEY, undefined);
139
+ statuslineClearTimer = undefined;
140
+ }, REFRESH_INTERVAL_MS) as TimeoutHandle;
141
+ statuslineClearTimer.unref?.();
142
+ };
143
+
144
+ const scheduleStatuslineRefresh = (ctx: ExtensionContext) => {
145
+ if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
146
+ statuslineRefreshTimer = setTimeout(() => {
147
+ void refreshCurrentCodexUsageStatusline(ctx, true);
148
+ }, REFRESH_INTERVAL_MS) as TimeoutHandle;
149
+ statuslineRefreshTimer.unref?.();
150
+ };
151
+
152
+ const setUsageStatusline = (
153
+ ctx: ExtensionContext,
154
+ report: CodexUsageReport,
155
+ options: {
156
+ autoRefresh: boolean;
157
+ blink: boolean;
158
+ model: CodexUsageModel | undefined;
159
+ },
160
+ ) => {
161
+ if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
162
+ if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
163
+ statuslineBlinkTimer = undefined;
164
+ statuslineClearTimer = undefined;
165
+ const text = formatCodexUsageStatusline(report, ctx, options.model);
166
+ if (options.blink) {
167
+ ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
168
+ statuslineBlinkTimer = setTimeout(() => {
169
+ ctx.ui.setStatus(STATUS_KEY, text);
170
+ statuslineBlinkTimer = undefined;
171
+ }, REDRAW_BLINK_MS) as TimeoutHandle;
172
+ statuslineBlinkTimer.unref?.();
173
+ } else {
174
+ ctx.ui.setStatus(STATUS_KEY, text);
175
+ }
176
+ if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
177
+ else scheduleTemporaryStatuslineClear(ctx);
178
+ };
179
+
180
+ const refreshCurrentCodexUsageStatusline = async (
181
+ ctx: ExtensionContext,
182
+ force: boolean,
183
+ model = ctx.model,
184
+ ) => {
185
+ if (!isOpenAICodexModel(model)) {
186
+ clearUsageStatusline(ctx);
187
+ return;
188
+ }
189
+
190
+ if (!cache) ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
191
+ const requestId = statuslineRequestId + 1;
192
+ statuslineRequestId = requestId;
193
+ const cached =
194
+ cache && Date.now() - cache.createdAt < REFRESH_INTERVAL_MS
195
+ ? cache
196
+ : undefined;
197
+ if (cached && !force) {
198
+ setUsageStatusline(ctx, cached.report, {
199
+ autoRefresh: true,
200
+ blink: false,
201
+ model,
202
+ });
203
+ return;
204
+ }
205
+
206
+ const result = await queryUsage(ctx, { timeoutMs: DEFAULT_TIMEOUT_MS });
207
+ if (requestId !== statuslineRequestId) return;
208
+ if (!isOpenAICodexModel(ctx.model)) {
209
+ clearUsageStatusline(ctx);
210
+ return;
211
+ }
212
+
213
+ if (!result.ok) {
214
+ failedRefreshes += 1;
215
+ if (!cache || failedRefreshes >= 5) {
216
+ ctx.ui.setStatus(
217
+ STATUS_KEY,
218
+ formatStatuslineProblem(ctx, result.errors),
219
+ );
220
+ }
221
+ scheduleStatuslineRefresh(ctx);
222
+ return;
223
+ }
224
+
225
+ const previousReport = cache?.report;
226
+ const blink = previousReport
227
+ ? formatReportBar(previousReport) !== formatReportBar(result.report)
228
+ : false;
229
+ failedRefreshes = 0;
230
+ cache = { createdAt: Date.now(), report: result.report };
231
+ setUsageStatusline(ctx, result.report, {
232
+ autoRefresh: true,
233
+ blink,
234
+ model,
235
+ });
236
+ };
237
+
238
+ pi.on("session_start", (_event, ctx) => {
239
+ if (isOpenAICodexModel(ctx.model))
240
+ void refreshCurrentCodexUsageStatusline(ctx, false);
241
+ else clearUsageStatusline(ctx);
242
+ });
243
+
244
+ pi.on("session_tree", (_event, ctx) => {
245
+ if (isOpenAICodexModel(ctx.model))
246
+ void refreshCurrentCodexUsageStatusline(ctx, false);
247
+ else clearUsageStatusline(ctx);
248
+ });
249
+
250
+ pi.on("model_select", (event, ctx) => {
251
+ if (isOpenAICodexModel(event.model)) {
252
+ void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
253
+ } else {
254
+ clearUsageStatusline(ctx);
255
+ }
256
+ });
257
+
258
+ pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
249
259
  }
250
260
 
251
261
  function isOpenAICodexModel(
252
- model: Pick<PiModel, "provider"> | undefined,
262
+ model: Pick<PiModel, "provider"> | undefined,
253
263
  ): boolean {
254
- return model?.provider === CODEX_PROVIDER_ID;
264
+ return model?.provider === CODEX_PROVIDER_ID;
255
265
  }
256
266
 
257
267
  async function queryUsage(
258
- ctx: ExtensionContext,
259
- options: Pick<QueryUsageOptions, "timeoutMs">,
268
+ ctx: ExtensionContext,
269
+ options: Pick<QueryUsageOptions, "timeoutMs">,
260
270
  ): Promise<QueryUsageResult> {
261
- const errors: UsageQueryError[] = [];
271
+ const errors: UsageQueryError[] = [];
262
272
 
263
- try {
264
- const report = await queryViaPiAuth(ctx, options.timeoutMs);
265
- return { ok: true, report };
266
- } catch (cause) {
267
- errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
268
- }
273
+ try {
274
+ const report = await queryViaPiAuth(ctx, options.timeoutMs);
275
+ return { ok: true, report };
276
+ } catch (cause) {
277
+ errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
278
+ }
269
279
 
270
- try {
271
- const report = await queryViaCodexAppServer(options.timeoutMs);
272
- return { ok: true, report };
273
- } catch (cause) {
274
- errors.push({
275
- source: "codex-app-server",
276
- message: errorMessage(cause),
277
- cause,
278
- });
279
- }
280
+ try {
281
+ const report = await queryViaCodexAppServer(options.timeoutMs);
282
+ return { ok: true, report };
283
+ } catch (cause) {
284
+ errors.push({
285
+ source: "codex-app-server",
286
+ message: errorMessage(cause),
287
+ cause,
288
+ });
289
+ }
280
290
 
281
- return { ok: false, errors };
291
+ return { ok: false, errors };
282
292
  }
283
293
 
284
294
  async function queryViaPiAuth(
285
- ctx: ExtensionContext,
286
- timeoutMs: number,
295
+ ctx: ExtensionContext,
296
+ timeoutMs: number,
287
297
  ): Promise<CodexUsageReport> {
288
- const auth = await resolvePiCodexAuth(ctx);
289
- if (!auth) {
290
- throw new Error(
291
- "No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
292
- );
293
- }
294
-
295
- const response = await fetchWithTimeout(
296
- CODEX_USAGE_URL,
297
- { headers: auth.headers },
298
- timeoutMs,
299
- );
300
- const text = await response.text();
301
- if (!response.ok) {
302
- throw new Error(
303
- `Codex usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
304
- );
305
- }
306
-
307
- const payload = parseJsonObject(text, "Codex usage endpoint response");
308
- return normalizeBackendPayload(
309
- payload as RateLimitStatusPayload,
310
- Date.now(),
311
- "pi-auth",
312
- );
298
+ const auth = await resolvePiCodexAuth(ctx);
299
+ if (!auth) {
300
+ throw new Error(
301
+ "No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
302
+ );
303
+ }
304
+
305
+ const response = await fetchWithTimeout(
306
+ CODEX_USAGE_URL,
307
+ { headers: auth.headers },
308
+ timeoutMs,
309
+ );
310
+ const text = await response.text();
311
+ if (!response.ok) {
312
+ throw new Error(
313
+ `Codex usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
314
+ );
315
+ }
316
+
317
+ const payload = parseJsonObject(text, "Codex usage endpoint response");
318
+ return normalizeBackendPayload(
319
+ payload as RateLimitStatusPayload,
320
+ Date.now(),
321
+ "pi-auth",
322
+ );
313
323
  }
314
324
 
315
325
  async function resolvePiCodexAuth(
316
- ctx: ExtensionContext,
326
+ ctx: ExtensionContext,
317
327
  ): Promise<{ headers: Record<string, string> } | undefined> {
318
- const models = codexAuthCandidateModels(ctx);
319
- const errors: string[] = [];
320
-
321
- for (const model of models) {
322
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
323
- if (!auth.ok) {
324
- errors.push(auth.error);
325
- continue;
326
- }
327
-
328
- const headers = { ...(auth.headers ?? {}) };
329
- if (!hasHeader(headers, "Authorization") && auth.apiKey) {
330
- headers.Authorization = `Bearer ${auth.apiKey}`;
331
- }
332
- if (!hasHeader(headers, "User-Agent")) {
333
- headers["User-Agent"] = "pi-codex-usage";
334
- }
335
- if (hasHeader(headers, "Authorization")) {
336
- return { headers };
337
- }
338
- }
339
-
340
- if (errors.length > 0) {
341
- throw new Error(errors.join("; "));
342
- }
343
- return undefined;
328
+ const models = codexAuthCandidateModels(ctx);
329
+ const errors: string[] = [];
330
+
331
+ for (const model of models) {
332
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
333
+ if (!auth.ok) {
334
+ errors.push(auth.error);
335
+ continue;
336
+ }
337
+
338
+ const headers = { ...(auth.headers ?? {}) };
339
+ if (!hasHeader(headers, "Authorization") && auth.apiKey) {
340
+ headers.Authorization = `Bearer ${auth.apiKey}`;
341
+ }
342
+ if (!hasHeader(headers, "User-Agent")) {
343
+ headers["User-Agent"] = "pi-codex-usage";
344
+ }
345
+ if (hasHeader(headers, "Authorization")) {
346
+ return { headers };
347
+ }
348
+ }
349
+
350
+ if (errors.length > 0) {
351
+ throw new Error(errors.join("; "));
352
+ }
353
+ return undefined;
344
354
  }
345
355
 
346
356
  function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
347
- const candidates: PiModel[] = [];
348
- const seen = new Set<string>();
349
- const add = (model: PiModel | undefined) => {
350
- if (!model || model.provider !== CODEX_PROVIDER_ID) return;
351
- const key = `${model.provider}/${model.id}`;
352
- if (seen.has(key)) return;
353
- seen.add(key);
354
- candidates.push(model);
355
- };
356
-
357
- add(ctx.model);
358
- for (const model of ctx.modelRegistry.getAvailable()) add(model);
359
- for (const model of ctx.modelRegistry.getAll()) add(model);
360
- return candidates;
357
+ const candidates: PiModel[] = [];
358
+ const seen = new Set<string>();
359
+ const add = (model: PiModel | undefined) => {
360
+ if (!model || model.provider !== CODEX_PROVIDER_ID) return;
361
+ const key = `${model.provider}/${model.id}`;
362
+ if (seen.has(key)) return;
363
+ seen.add(key);
364
+ candidates.push(model);
365
+ };
366
+
367
+ add(ctx.model);
368
+ for (const model of ctx.modelRegistry.getAvailable()) add(model);
369
+ for (const model of ctx.modelRegistry.getAll()) add(model);
370
+ return candidates;
361
371
  }
362
372
 
363
373
  async function fetchWithTimeout(
364
- url: string,
365
- init: RequestInit,
366
- timeoutMs: number,
374
+ url: string,
375
+ init: RequestInit,
376
+ timeoutMs: number,
367
377
  ): Promise<Response> {
368
- const controller = new AbortController();
369
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
370
- try {
371
- return await fetch(url, { ...init, signal: controller.signal });
372
- } catch (error) {
373
- if (controller.signal.aborted) {
374
- throw new Error(
375
- `Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
376
- );
377
- }
378
- throw error;
379
- } finally {
380
- clearTimeout(timeout);
381
- }
378
+ const controller = new AbortController();
379
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
380
+ try {
381
+ return await fetch(url, { ...init, signal: controller.signal });
382
+ } catch (error) {
383
+ if (controller.signal.aborted) {
384
+ throw new Error(
385
+ `Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
386
+ );
387
+ }
388
+ throw error;
389
+ } finally {
390
+ clearTimeout(timeout);
391
+ }
382
392
  }
383
393
 
384
394
  async function queryViaCodexAppServer(
385
- timeoutMs: number,
395
+ timeoutMs: number,
386
396
  ): Promise<CodexUsageReport> {
387
- const client = new CodexAppServerClient(timeoutMs);
388
- try {
389
- await client.start();
390
- await client.request("initialize", {
391
- clientInfo: {
392
- name: "pi_codex_usage",
393
- title: "Pi Codex Usage",
394
- version: "0.1.0",
395
- },
396
- capabilities: {
397
- experimentalApi: false,
398
- requestAttestation: false,
399
- optOutNotificationMethods: [],
400
- },
401
- });
402
- client.notify("initialized");
403
- const result = await client.request("account/rateLimits/read", undefined);
404
- return normalizeAppServerResponse(
405
- assertObject(
406
- result,
407
- "account/rateLimits/read result",
408
- ) as AppServerRateLimitResponse,
409
- Date.now(),
410
- );
411
- } finally {
412
- client.dispose();
413
- }
397
+ const client = new CodexAppServerClient(timeoutMs);
398
+ try {
399
+ await client.start();
400
+ await client.request("initialize", {
401
+ clientInfo: {
402
+ name: "pi_codex_usage",
403
+ title: "Pi Codex Usage",
404
+ version: "0.1.0",
405
+ },
406
+ capabilities: {
407
+ experimentalApi: false,
408
+ requestAttestation: false,
409
+ optOutNotificationMethods: [],
410
+ },
411
+ });
412
+ client.notify("initialized");
413
+ const result = await client.request("account/rateLimits/read", undefined);
414
+ return normalizeAppServerResponse(
415
+ assertObject(
416
+ result,
417
+ "account/rateLimits/read result",
418
+ ) as AppServerRateLimitResponse,
419
+ Date.now(),
420
+ );
421
+ } finally {
422
+ client.dispose();
423
+ }
414
424
  }
415
425
 
416
426
  class CodexAppServerClient {
417
- private child?: ChildProcessWithoutNullStreams;
418
- private nextId = 1;
419
- private stderr = "";
420
- private readonly pending = new Map<number, PendingRpc>();
421
- private startPromise?: Promise<void>;
422
- private exitError?: Error;
423
- private readonly timeoutMs: number;
424
-
425
- constructor(timeoutMs: number) {
426
- this.timeoutMs = timeoutMs;
427
- }
428
-
429
- start(): Promise<void> {
430
- if (this.startPromise) return this.startPromise;
431
-
432
- this.startPromise = new Promise((resolve, reject) => {
433
- const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
434
- stdio: ["pipe", "pipe", "pipe"],
435
- });
436
- this.child = child;
437
-
438
- const startupTimeout = setTimeout(() => {
439
- reject(
440
- new Error(
441
- `Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
442
- ),
443
- );
444
- }, this.timeoutMs);
445
-
446
- child.once("spawn", () => {
447
- clearTimeout(startupTimeout);
448
- resolve();
449
- });
450
-
451
- child.once("error", (error) => {
452
- clearTimeout(startupTimeout);
453
- reject(new Error(`Failed to start codex app-server: ${error.message}`));
454
- this.rejectAll(error);
455
- });
456
-
457
- child.once("exit", (code, signal) => {
458
- const suffix = this.stderr
459
- ? ` stderr: ${redactErrorBody(this.stderr)}`
460
- : "";
461
- this.exitError = new Error(
462
- `codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
463
- );
464
- this.rejectAll(this.exitError);
465
- });
466
-
467
- child.stderr.setEncoding("utf8");
468
- child.stderr.on("data", (chunk: string) => {
469
- this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
470
- });
471
-
472
- const lines = createInterface({ input: child.stdout });
473
- lines.on("line", (line) => this.handleLine(line));
474
- });
475
-
476
- return this.startPromise;
477
- }
478
-
479
- request(method: string, params: unknown): Promise<unknown> {
480
- const child = this.child;
481
- if (!child?.stdin.writable) {
482
- throw new Error("codex app-server is not running.");
483
- }
484
- if (this.exitError) throw this.exitError;
485
-
486
- const id = this.nextId++;
487
- const payload =
488
- params === undefined ? { method, id } : { method, id, params };
489
- const response = new Promise<unknown>((resolve, reject) => {
490
- const timeout = setTimeout(() => {
491
- this.pending.delete(id);
492
- reject(
493
- new Error(
494
- `Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`,
495
- ),
496
- );
497
- }, this.timeoutMs);
498
-
499
- this.pending.set(id, {
500
- resolve: (value) => {
501
- clearTimeout(timeout);
502
- resolve(value);
503
- },
504
- reject: (error) => {
505
- clearTimeout(timeout);
506
- reject(error);
507
- },
508
- });
509
- });
510
-
511
- child.stdin.write(`${JSON.stringify(payload)}\n`);
512
- return response;
513
- }
514
-
515
- notify(method: string): void {
516
- const child = this.child;
517
- if (!child?.stdin.writable) return;
518
- child.stdin.write(`${JSON.stringify({ method })}\n`);
519
- }
520
-
521
- dispose(): void {
522
- for (const [id, pending] of this.pending) {
523
- pending.reject(new Error(`codex app-server request ${id} cancelled.`));
524
- }
525
- this.pending.clear();
526
-
527
- const child = this.child;
528
- if (!child) return;
529
- child.stdin.end();
530
- if (!child.killed) child.kill();
531
- this.child = undefined;
532
- }
533
-
534
- private handleLine(line: string): void {
535
- let parsed: RpcResponse;
536
- try {
537
- parsed = JSON.parse(line) as RpcResponse;
538
- } catch {
539
- return;
540
- }
541
-
542
- if (typeof parsed.id !== "number") return;
543
- const pending = this.pending.get(parsed.id);
544
- if (!pending) return;
545
- this.pending.delete(parsed.id);
546
-
547
- if (parsed.error) {
548
- const message =
549
- typeof parsed.error.message === "string"
550
- ? parsed.error.message
551
- : "unknown error";
552
- pending.reject(new Error(`codex app-server request failed: ${message}`));
553
- return;
554
- }
555
-
556
- pending.resolve(parsed.result);
557
- }
558
-
559
- private rejectAll(error: Error): void {
560
- for (const pending of this.pending.values()) pending.reject(error);
561
- this.pending.clear();
562
- }
427
+ private child?: ChildProcessWithoutNullStreams;
428
+ private nextId = 1;
429
+ private stderr = "";
430
+ private readonly pending = new Map<number, PendingRpc>();
431
+ private startPromise?: Promise<void>;
432
+ private exitError?: Error;
433
+ private readonly timeoutMs: number;
434
+
435
+ constructor(timeoutMs: number) {
436
+ this.timeoutMs = timeoutMs;
437
+ }
438
+
439
+ start(): Promise<void> {
440
+ if (this.startPromise) return this.startPromise;
441
+
442
+ this.startPromise = new Promise((resolve, reject) => {
443
+ const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
444
+ stdio: ["pipe", "pipe", "pipe"],
445
+ });
446
+ this.child = child;
447
+
448
+ const startupTimeout = setTimeout(() => {
449
+ reject(
450
+ new Error(
451
+ `Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
452
+ ),
453
+ );
454
+ }, this.timeoutMs);
455
+
456
+ child.once("spawn", () => {
457
+ clearTimeout(startupTimeout);
458
+ resolve();
459
+ });
460
+
461
+ child.once("error", (error) => {
462
+ clearTimeout(startupTimeout);
463
+ reject(new Error(`Failed to start codex app-server: ${error.message}`));
464
+ this.rejectAll(error);
465
+ });
466
+
467
+ child.once("exit", (code, signal) => {
468
+ const suffix = this.stderr
469
+ ? ` stderr: ${redactErrorBody(this.stderr)}`
470
+ : "";
471
+ this.exitError = new Error(
472
+ `codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
473
+ );
474
+ this.rejectAll(this.exitError);
475
+ });
476
+
477
+ child.stderr.setEncoding("utf8");
478
+ child.stderr.on("data", (chunk: string) => {
479
+ this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
480
+ });
481
+
482
+ const lines = createInterface({ input: child.stdout });
483
+ lines.on("line", (line) => this.handleLine(line));
484
+ });
485
+
486
+ return this.startPromise;
487
+ }
488
+
489
+ request(method: string, params: unknown): Promise<unknown> {
490
+ const child = this.child;
491
+ if (!child?.stdin.writable) {
492
+ throw new Error("codex app-server is not running.");
493
+ }
494
+ if (this.exitError) throw this.exitError;
495
+
496
+ const id = this.nextId++;
497
+ const payload =
498
+ params === undefined ? { method, id } : { method, id, params };
499
+ const response = new Promise<unknown>((resolve, reject) => {
500
+ const timeout = setTimeout(() => {
501
+ this.pending.delete(id);
502
+ reject(
503
+ new Error(
504
+ `Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`,
505
+ ),
506
+ );
507
+ }, this.timeoutMs);
508
+
509
+ this.pending.set(id, {
510
+ resolve: (value) => {
511
+ clearTimeout(timeout);
512
+ resolve(value);
513
+ },
514
+ reject: (error) => {
515
+ clearTimeout(timeout);
516
+ reject(error);
517
+ },
518
+ });
519
+ });
520
+
521
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
522
+ return response;
523
+ }
524
+
525
+ notify(method: string): void {
526
+ const child = this.child;
527
+ if (!child?.stdin.writable) return;
528
+ child.stdin.write(`${JSON.stringify({ method })}\n`);
529
+ }
530
+
531
+ dispose(): void {
532
+ for (const [id, pending] of this.pending) {
533
+ pending.reject(new Error(`codex app-server request ${id} cancelled.`));
534
+ }
535
+ this.pending.clear();
536
+
537
+ const child = this.child;
538
+ if (!child) return;
539
+ child.stdin.end();
540
+ if (!child.killed) child.kill();
541
+ this.child = undefined;
542
+ }
543
+
544
+ private handleLine(line: string): void {
545
+ let parsed: RpcResponse;
546
+ try {
547
+ parsed = JSON.parse(line) as RpcResponse;
548
+ } catch {
549
+ return;
550
+ }
551
+
552
+ if (typeof parsed.id !== "number") return;
553
+ const pending = this.pending.get(parsed.id);
554
+ if (!pending) return;
555
+ this.pending.delete(parsed.id);
556
+
557
+ if (parsed.error) {
558
+ const message =
559
+ typeof parsed.error.message === "string"
560
+ ? parsed.error.message
561
+ : "unknown error";
562
+ pending.reject(new Error(`codex app-server request failed: ${message}`));
563
+ return;
564
+ }
565
+
566
+ pending.resolve(parsed.result);
567
+ }
568
+
569
+ private rejectAll(error: Error): void {
570
+ for (const pending of this.pending.values()) pending.reject(error);
571
+ this.pending.clear();
572
+ }
563
573
  }
564
574
 
565
575
  export function normalizeBackendPayload(
566
- payload: RateLimitStatusPayload,
567
- _capturedAt: number,
568
- _source: UsageSource,
576
+ payload: RateLimitStatusPayload,
577
+ _capturedAt: number,
578
+ _source: UsageSource,
569
579
  ): CodexUsageReport {
570
- const snapshot = normalizeBackendSnapshot("codex", payload.rate_limit);
571
- if (!snapshot) {
572
- throw new Error(
573
- "Codex usage endpoint returned no displayable rate-limit windows.",
574
- );
575
- }
576
- return { snapshots: [snapshot] };
580
+ const snapshot = normalizeBackendSnapshot("codex", payload.rate_limit);
581
+ if (!snapshot) {
582
+ throw new Error(
583
+ "Codex usage endpoint returned no displayable rate-limit windows.",
584
+ );
585
+ }
586
+ return { snapshots: [snapshot] };
577
587
  }
578
588
 
579
589
  function normalizeBackendSnapshot(
580
- limitId: string,
581
- rateLimit: unknown,
590
+ limitId: string,
591
+ rateLimit: unknown,
582
592
  ): NormalizedRateLimitSnapshot | undefined {
583
- if (rateLimit === null || rateLimit === undefined) return undefined;
584
- const details = assertObject(
585
- rateLimit,
586
- "rate limit",
587
- ) as BackendRateLimitDetails;
588
- const primary = normalizeBackendWindow(details.primary_window);
589
- const secondary = normalizeBackendWindow(details.secondary_window);
590
- if (!primary && !secondary) return undefined;
591
- return { limitId, primary, secondary };
593
+ if (rateLimit === null || rateLimit === undefined) return undefined;
594
+ const details = assertObject(
595
+ rateLimit,
596
+ "rate limit",
597
+ ) as BackendRateLimitDetails;
598
+ const primary = normalizeBackendWindow(details.primary_window);
599
+ const secondary = normalizeBackendWindow(details.secondary_window);
600
+ if (!primary && !secondary) return undefined;
601
+ return { limitId, primary, secondary };
592
602
  }
593
603
 
594
604
  function normalizeBackendWindow(
595
- value: unknown,
605
+ value: unknown,
596
606
  ): NormalizedRateLimitWindow | undefined {
597
- if (value === null || value === undefined) return undefined;
598
- const window = assertObject(
599
- value,
600
- "rate-limit window",
601
- ) as BackendWindowSnapshot;
602
- const usedPercent = asNumber(window.used_percent);
603
- if (usedPercent === undefined) return undefined;
604
- return { usedPercent };
607
+ if (value === null || value === undefined) return undefined;
608
+ const window = assertObject(
609
+ value,
610
+ "rate-limit window",
611
+ ) as BackendWindowSnapshot;
612
+ const usedPercent = asNumber(window.used_percent);
613
+ if (usedPercent === undefined) return undefined;
614
+ return { usedPercent };
605
615
  }
606
616
 
607
617
  export function normalizeAppServerResponse(
608
- response: AppServerRateLimitResponse,
609
- _capturedAt: number,
618
+ response: AppServerRateLimitResponse,
619
+ _capturedAt: number,
610
620
  ): CodexUsageReport {
611
- const snapshots: NormalizedRateLimitSnapshot[] = [];
612
- const addSnapshot = (raw: unknown, fallbackId: string) => {
613
- const snapshot = normalizeAppServerSnapshot(raw, fallbackId);
614
- if (!snapshot) return;
615
- const existingIndex = snapshots.findIndex(
616
- (item) => item.limitId === snapshot.limitId,
617
- );
618
- if (existingIndex >= 0)
619
- snapshots[existingIndex] = mergeSnapshot(
620
- snapshots[existingIndex],
621
- snapshot,
622
- );
623
- else snapshots.push(snapshot);
624
- };
625
-
626
- addSnapshot(response.rateLimits, "codex");
627
- if (snapshots.length === 0) {
628
- throw new Error(
629
- "codex app-server returned no displayable rate-limit windows.",
630
- );
631
- }
632
-
633
- return { snapshots };
621
+ const snapshots: NormalizedRateLimitSnapshot[] = [];
622
+ const addSnapshot = (raw: unknown, fallbackId: string) => {
623
+ const snapshot = normalizeAppServerSnapshot(raw, fallbackId);
624
+ if (!snapshot) return;
625
+ const existingIndex = snapshots.findIndex(
626
+ (item) => item.limitId === snapshot.limitId,
627
+ );
628
+ if (existingIndex >= 0)
629
+ snapshots[existingIndex] = mergeSnapshot(
630
+ snapshots[existingIndex],
631
+ snapshot,
632
+ );
633
+ else snapshots.push(snapshot);
634
+ };
635
+
636
+ addSnapshot(response.rateLimits, "codex");
637
+ if (snapshots.length === 0) {
638
+ throw new Error(
639
+ "codex app-server returned no displayable rate-limit windows.",
640
+ );
641
+ }
642
+
643
+ return { snapshots };
634
644
  }
635
645
 
636
646
  function normalizeAppServerSnapshot(
637
- raw: unknown,
638
- fallbackId: string,
647
+ raw: unknown,
648
+ fallbackId: string,
639
649
  ): NormalizedRateLimitSnapshot | undefined {
640
- if (raw === null || raw === undefined) return undefined;
641
- const snapshot = assertObject(
642
- raw,
643
- "app-server rate-limit snapshot",
644
- ) as AppServerRateLimitSnapshot;
645
- const limitId = asString(snapshot.limitId) ?? fallbackId;
646
- const primary = normalizeAppServerWindow(snapshot.primary);
647
- const secondary = normalizeAppServerWindow(snapshot.secondary);
648
- if (!primary && !secondary) return undefined;
649
- return { limitId, primary, secondary };
650
+ if (raw === null || raw === undefined) return undefined;
651
+ const snapshot = assertObject(
652
+ raw,
653
+ "app-server rate-limit snapshot",
654
+ ) as AppServerRateLimitSnapshot;
655
+ const limitId = asString(snapshot.limitId) ?? fallbackId;
656
+ const primary = normalizeAppServerWindow(snapshot.primary);
657
+ const secondary = normalizeAppServerWindow(snapshot.secondary);
658
+ if (!primary && !secondary) return undefined;
659
+ return { limitId, primary, secondary };
650
660
  }
651
661
 
652
662
  function normalizeAppServerWindow(
653
- value: unknown,
663
+ value: unknown,
654
664
  ): NormalizedRateLimitWindow | undefined {
655
- if (value === null || value === undefined) return undefined;
656
- const window = assertObject(
657
- value,
658
- "app-server rate-limit window",
659
- ) as AppServerWindowSnapshot;
660
- const usedPercent = asNumber(window.usedPercent);
661
- if (usedPercent === undefined) return undefined;
662
- return { usedPercent };
665
+ if (value === null || value === undefined) return undefined;
666
+ const window = assertObject(
667
+ value,
668
+ "app-server rate-limit window",
669
+ ) as AppServerWindowSnapshot;
670
+ const usedPercent = asNumber(window.usedPercent);
671
+ if (usedPercent === undefined) return undefined;
672
+ return { usedPercent };
663
673
  }
664
674
 
665
675
  function mergeSnapshot(
666
- left: NormalizedRateLimitSnapshot,
667
- right: NormalizedRateLimitSnapshot,
676
+ left: NormalizedRateLimitSnapshot,
677
+ right: NormalizedRateLimitSnapshot,
668
678
  ): NormalizedRateLimitSnapshot {
669
- return {
670
- limitId: right.limitId || left.limitId,
671
- primary: right.primary ?? left.primary,
672
- secondary: right.secondary ?? left.secondary,
673
- };
679
+ return {
680
+ limitId: right.limitId || left.limitId,
681
+ primary: right.primary ?? left.primary,
682
+ secondary: right.secondary ?? left.secondary,
683
+ };
674
684
  }
675
685
 
676
686
  export function formatCodexUsageStatusline(
677
- report: CodexUsageReport,
678
- ctx: ExtensionContext,
679
- _model?: CodexUsageModel,
687
+ report: CodexUsageReport,
688
+ ctx: ExtensionContext,
689
+ _model?: CodexUsageModel,
680
690
  ): string {
681
- const snapshot = selectPrimaryCodexSnapshot(report);
682
- if (!snapshot) return formatStatuslineText(ctx, "n/a");
691
+ return formatStatuslineText(ctx, formatReportBar(report) ?? "n/a");
692
+ }
683
693
 
684
- if (!snapshot.primary && !snapshot.secondary) return formatStatuslineText(ctx, "n/a");
685
- return formatStatuslineText(
686
- ctx,
687
- `[${formatDualLimitBar(snapshot.primary, snapshot.secondary)}]`,
688
- );
694
+ function formatReportBar(report: CodexUsageReport): string | undefined {
695
+ const snapshot = selectPrimaryCodexSnapshot(report);
696
+ if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
697
+ return formatDualLimitBar(snapshot.primary, snapshot.secondary);
689
698
  }
690
699
 
691
700
  function formatStatuslineText(ctx: ExtensionContext, value: string): string {
692
- const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
693
- return `${label} ${ctx.ui.theme.fg("muted", value)}`;
701
+ const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
702
+ return `${label} ${ctx.ui.theme.fg("dim", value)}`;
694
703
  }
695
704
 
696
705
  function formatEmptyStatuslineBar(ctx: ExtensionContext): string {
697
- return formatStatuslineText(ctx, "[\u00a0\u00a0\u00a0\u00a0\u00a0]");
706
+ return formatStatuslineText(ctx, "\u00a0\u00a0\u00a0\u00a0\u00a0");
698
707
  }
699
708
 
700
709
  function formatStatuslineProblem(
701
- ctx: ExtensionContext,
702
- errors: UsageQueryError[],
710
+ ctx: ExtensionContext,
711
+ errors: UsageQueryError[],
703
712
  ): string {
704
- const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
705
- const value = isUnavailable(errors)
706
- ? ctx.ui.theme.fg("muted", "n/a")
707
- : ctx.ui.theme.fg("error", "error");
708
- return `${label} ${value}`;
713
+ const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
714
+ const value = isUnavailable(errors)
715
+ ? ctx.ui.theme.fg("muted", "n/a")
716
+ : ctx.ui.theme.fg("error", "error");
717
+ return `${label} ${value}`;
709
718
  }
710
719
 
711
720
  function isUnavailable(errors: UsageQueryError[]): boolean {
712
- return errors.some((error) => {
713
- const message = error.message.toLowerCase();
714
- return (
715
- message.includes("no pi openai codex subscription auth") ||
716
- message.includes("no displayable rate-limit windows") ||
717
- message.includes("returned no displayable rate-limit windows") ||
718
- message.includes("returned 401") ||
719
- message.includes("returned 403") ||
720
- message.includes("unauthorized") ||
721
- message.includes("forbidden") ||
722
- message.includes("subscription") ||
723
- message.includes("no active plan") ||
724
- message.includes("plan unavailable") ||
725
- message.includes("quota unavailable") ||
726
- message.includes("rate limits unavailable")
727
- );
728
- });
721
+ return errors.some((error) => {
722
+ const message = error.message.toLowerCase();
723
+ return (
724
+ message.includes("no pi openai codex subscription auth") ||
725
+ message.includes("no displayable rate-limit windows") ||
726
+ message.includes("returned no displayable rate-limit windows") ||
727
+ message.includes("returned 401") ||
728
+ message.includes("returned 403") ||
729
+ message.includes("unauthorized") ||
730
+ message.includes("forbidden") ||
731
+ message.includes("subscription") ||
732
+ message.includes("no active plan") ||
733
+ message.includes("plan unavailable") ||
734
+ message.includes("quota unavailable") ||
735
+ message.includes("rate limits unavailable")
736
+ );
737
+ });
729
738
  }
730
739
 
731
740
  function selectPrimaryCodexSnapshot(
732
- report: CodexUsageReport,
741
+ report: CodexUsageReport,
733
742
  ): NormalizedRateLimitSnapshot | undefined {
734
- return report.snapshots.find(isPrimaryCodexSnapshot) ?? report.snapshots[0];
743
+ return report.snapshots.find(isPrimaryCodexSnapshot) ?? report.snapshots[0];
735
744
  }
736
745
 
737
746
  function normalizedUsageKey(value: string | undefined): string | undefined {
738
- const key = value
739
- ?.toLowerCase()
740
- .replace(/[^a-z0-9]+/g, "-")
741
- .replace(/^-+|-+$/g, "");
742
- return key || undefined;
747
+ const key = value
748
+ ?.toLowerCase()
749
+ .replace(/[^a-z0-9]+/g, "-")
750
+ .replace(/^-+|-+$/g, "");
751
+ return key || undefined;
743
752
  }
744
753
 
745
754
  function formatDualLimitBar(
746
- primary: NormalizedRateLimitWindow | undefined,
747
- secondary: NormalizedRateLimitWindow | undefined,
755
+ primary: NormalizedRateLimitWindow | undefined,
756
+ secondary: NormalizedRateLimitWindow | undefined,
748
757
  ): string {
749
- const primaryParts = filledTenths(primary);
750
- const secondaryParts = filledTenths(secondary);
751
- let value = "";
752
- for (let index = 0; index < 5; index++) {
753
- const leftPart = index * 2 + 1;
754
- const rightPart = leftPart + 1;
755
- let mask = 0;
756
- if (primaryParts >= leftPart) mask |= 1;
757
- if (primaryParts >= rightPart) mask |= 2;
758
- if (secondaryParts >= leftPart) mask |= 4;
759
- if (secondaryParts >= rightPart) mask |= 8;
760
- value += DUAL_BAR_CHARS[mask];
761
- }
762
- return value;
758
+ const primaryParts = filledTenths(primary);
759
+ const secondaryParts = filledTenths(secondary);
760
+ let value = "";
761
+ for (let index = 0; index < 5; index++) {
762
+ const leftPart = index * 2 + 1;
763
+ const rightPart = leftPart + 1;
764
+ let mask = 0;
765
+ if (primaryParts >= leftPart) mask |= 1;
766
+ if (primaryParts >= rightPart) mask |= 2;
767
+ if (secondaryParts >= leftPart) mask |= 4;
768
+ if (secondaryParts >= rightPart) mask |= 8;
769
+ value += DUAL_BAR_CHARS[mask];
770
+ }
771
+ return value;
763
772
  }
764
773
 
765
774
  function filledTenths(window: NormalizedRateLimitWindow | undefined): number {
766
- if (!window) return 0;
767
- return Math.round(remainingPercent(window) / 10);
775
+ if (!window) return 0;
776
+ return Math.round(remainingPercent(window) / 10);
768
777
  }
769
778
 
770
779
  function remainingPercent(window: NormalizedRateLimitWindow): number {
771
- return 100 - clampPercent(window.usedPercent);
780
+ return 100 - clampPercent(window.usedPercent);
772
781
  }
773
782
 
774
783
  function isPrimaryCodexSnapshot(
775
- snapshot: NormalizedRateLimitSnapshot,
784
+ snapshot: NormalizedRateLimitSnapshot,
776
785
  ): boolean {
777
- return normalizedUsageKey(snapshot.limitId) === "codex";
786
+ return normalizedUsageKey(snapshot.limitId) === "codex";
778
787
  }
779
788
 
780
789
  function clampPercent(value: number): number {
781
- if (!Number.isFinite(value)) return 0;
782
- return Math.min(100, Math.max(0, value));
790
+ if (!Number.isFinite(value)) return 0;
791
+ return Math.min(100, Math.max(0, value));
783
792
  }
784
793
 
785
794
  function parseJsonObject(
786
- text: string,
787
- description: string,
795
+ text: string,
796
+ description: string,
788
797
  ): Record<string, unknown> {
789
- let parsed: unknown;
790
- try {
791
- parsed = JSON.parse(text) as unknown;
792
- } catch (error) {
793
- throw new Error(
794
- `${description} was not valid JSON: ${errorMessage(error)}`,
795
- );
796
- }
797
- return assertObject(parsed, description);
798
+ let parsed: unknown;
799
+ try {
800
+ parsed = JSON.parse(text) as unknown;
801
+ } catch (error) {
802
+ throw new Error(
803
+ `${description} was not valid JSON: ${errorMessage(error)}`,
804
+ );
805
+ }
806
+ return assertObject(parsed, description);
798
807
  }
799
808
 
800
809
  function assertObject(
801
- value: unknown,
802
- description: string,
810
+ value: unknown,
811
+ description: string,
803
812
  ): Record<string, unknown> {
804
- if (!value || typeof value !== "object" || Array.isArray(value)) {
805
- throw new Error(`${description} was not an object.`);
806
- }
807
- return value as Record<string, unknown>;
813
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
814
+ throw new Error(`${description} was not an object.`);
815
+ }
816
+ return value as Record<string, unknown>;
808
817
  }
809
818
 
810
819
  function asString(value: unknown): string | undefined {
811
- return typeof value === "string" ? value : undefined;
820
+ return typeof value === "string" ? value : undefined;
812
821
  }
813
822
 
814
823
  function asNumber(value: unknown): number | undefined {
815
- if (typeof value === "number" && Number.isFinite(value)) return value;
816
- if (typeof value === "string" && value.trim()) {
817
- const parsed = Number(value);
818
- return Number.isFinite(parsed) ? parsed : undefined;
819
- }
820
- return undefined;
824
+ if (typeof value === "number" && Number.isFinite(value)) return value;
825
+ if (typeof value === "string" && value.trim()) {
826
+ const parsed = Number(value);
827
+ return Number.isFinite(parsed) ? parsed : undefined;
828
+ }
829
+ return undefined;
821
830
  }
822
831
 
823
832
  function hasHeader(headers: Record<string, string>, name: string): boolean {
824
- return Object.keys(headers).some(
825
- (key) => key.toLowerCase() === name.toLowerCase(),
826
- );
833
+ return Object.keys(headers).some(
834
+ (key) => key.toLowerCase() === name.toLowerCase(),
835
+ );
827
836
  }
828
837
 
829
838
  function redactErrorBody(body: string): string {
830
- return truncateEnd(
831
- body
832
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
833
- .replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
834
- .trim(),
835
- MAX_ERROR_BODY_CHARS,
836
- );
839
+ return truncateEnd(
840
+ body
841
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
842
+ .replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
843
+ .trim(),
844
+ MAX_ERROR_BODY_CHARS,
845
+ );
837
846
  }
838
847
 
839
848
  function truncateEnd(value: string, maxChars: number): string {
840
- if (value.length <= maxChars) return value;
841
- return `${value.slice(0, maxChars - 1)}โ€ฆ`;
849
+ if (value.length <= maxChars) return value;
850
+ return `${value.slice(0, maxChars - 1)}โ€ฆ`;
842
851
  }
843
852
 
844
853
  function errorMessage(error: unknown): string {
845
- return error instanceof Error ? error.message : String(error);
854
+ return error instanceof Error ? error.message : String(error);
846
855
  }