@narumitw/pi-codex-usage 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -67,9 +67,12 @@ When the selected Pi model provider is `openai-codex`, `pi-codex-usage` refreshe
67
67
 
68
68
  ```text
69
69
  codex 59% 5h 61% wk
70
+ codex spark 100% 5h 100% wk
70
71
  ```
71
72
 
72
- The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`. Switching away from an OpenAI Codex model clears the item.
73
+ The statusline value uses the cached usage snapshot and refreshes every five minutes while the current model remains `openai-codex`.
74
+ When the selected model has its own returned usage bucket, such as `gpt-5.3-codex-spark`, the statusline switches to that bucket instead of the default `codex` bucket.
75
+ Switching away from an OpenAI Codex model clears the item.
73
76
 
74
77
  Use `/codex-status --no-statusline` for a one-off notification without updating the statusline, or `/codex-status --clear-statusline` to clear the item manually.
75
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-codex-usage",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Pi extension that shows Codex ChatGPT subscription usage without requiring Codex CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@ const RESET_FOREGROUND = "\x1b[39m";
20
20
 
21
21
  type UsageSource = "pi-auth" | "codex-app-server";
22
22
  type PiModel = NonNullable<ExtensionContext["model"]>;
23
+ export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
23
24
 
24
25
  type QueryUsageOptions = {
25
26
  clearStatusline: boolean;
@@ -176,17 +177,21 @@ export default function codexUsage(pi: ExtensionAPI) {
176
177
  const setUsageStatusline = (
177
178
  ctx: ExtensionContext,
178
179
  report: CodexUsageReport,
179
- options: { autoRefresh: boolean },
180
+ options: { autoRefresh: boolean; model: CodexUsageModel | undefined },
180
181
  ) => {
181
182
  if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
182
183
  statuslineClearTimer = undefined;
183
- ctx.ui.setStatus(STATUS_KEY, formatCodexUsageStatusline(report));
184
+ ctx.ui.setStatus(STATUS_KEY, formatCodexUsageStatusline(report, options.model));
184
185
  if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
185
186
  else scheduleTemporaryStatuslineClear(ctx);
186
187
  };
187
188
 
188
- const refreshCurrentCodexUsageStatusline = async (ctx: ExtensionContext, force: boolean) => {
189
- if (!isOpenAICodexModel(ctx.model)) {
189
+ const refreshCurrentCodexUsageStatusline = async (
190
+ ctx: ExtensionContext,
191
+ force: boolean,
192
+ model = ctx.model,
193
+ ) => {
194
+ if (!isOpenAICodexModel(model)) {
190
195
  clearUsageStatusline(ctx);
191
196
  return;
192
197
  }
@@ -195,7 +200,7 @@ export default function codexUsage(pi: ExtensionAPI) {
195
200
  statuslineRequestId = requestId;
196
201
  const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
197
202
  if (cached && !force) {
198
- setUsageStatusline(ctx, cached.report, { autoRefresh: true });
203
+ setUsageStatusline(ctx, cached.report, { autoRefresh: true, model });
199
204
  return;
200
205
  }
201
206
 
@@ -214,7 +219,7 @@ export default function codexUsage(pi: ExtensionAPI) {
214
219
  }
215
220
 
216
221
  cache = { createdAt: Date.now(), report: result.report };
217
- setUsageStatusline(ctx, result.report, { autoRefresh: true });
222
+ setUsageStatusline(ctx, result.report, { autoRefresh: true, model });
218
223
  };
219
224
 
220
225
  pi.registerCommand(COMMAND_NAME, {
@@ -235,7 +240,10 @@ export default function codexUsage(pi: ExtensionAPI) {
235
240
  const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
236
241
  if (cached && !options.value.refresh) {
237
242
  if (options.value.statusline) {
238
- setUsageStatusline(ctx, cached.report, { autoRefresh: isOpenAICodexModel(ctx.model) });
243
+ setUsageStatusline(ctx, cached.report, {
244
+ autoRefresh: isOpenAICodexModel(ctx.model),
245
+ model: ctx.model,
246
+ });
239
247
  }
240
248
  showReport(ctx, cached.report, true);
241
249
  return;
@@ -252,7 +260,10 @@ export default function codexUsage(pi: ExtensionAPI) {
252
260
 
253
261
  cache = { createdAt: Date.now(), report: result.report };
254
262
  if (options.value.statusline) {
255
- setUsageStatusline(ctx, result.report, { autoRefresh: isOpenAICodexModel(ctx.model) });
263
+ setUsageStatusline(ctx, result.report, {
264
+ autoRefresh: isOpenAICodexModel(ctx.model),
265
+ model: ctx.model,
266
+ });
256
267
  keepStatusline = true;
257
268
  }
258
269
  showReport(ctx, result.report, false);
@@ -273,8 +284,11 @@ export default function codexUsage(pi: ExtensionAPI) {
273
284
  });
274
285
 
275
286
  pi.on("model_select", (event, ctx) => {
276
- if (isOpenAICodexModel(event.model)) void refreshCurrentCodexUsageStatusline(ctx, false);
277
- else clearUsageStatusline(ctx);
287
+ if (isOpenAICodexModel(event.model)) {
288
+ void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
289
+ } else {
290
+ clearUsageStatusline(ctx);
291
+ }
278
292
  });
279
293
 
280
294
  pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
@@ -324,7 +338,7 @@ function parseArgs(
324
338
  return { ok: true, value: { clearStatusline, refresh, statusline, timeoutMs } };
325
339
  }
326
340
 
327
- function isOpenAICodexModel(model: ExtensionContext["model"]): boolean {
341
+ function isOpenAICodexModel(model: Pick<PiModel, "provider"> | undefined): boolean {
328
342
  return model?.provider === CODEX_PROVIDER_ID;
329
343
  }
330
344
 
@@ -806,19 +820,109 @@ export function formatCodexUsageReport(report: CodexUsageReport, _cacheAgeMs?: n
806
820
  return lines.join("\n");
807
821
  }
808
822
 
809
- export function formatCodexUsageStatusline(report: CodexUsageReport): string {
810
- const snapshot =
811
- report.snapshots.find((item) => item.limitId.toLowerCase() === "codex") ??
812
- report.snapshots[0];
823
+ export function formatCodexUsageStatusline(
824
+ report: CodexUsageReport,
825
+ model?: CodexUsageModel,
826
+ ): string {
827
+ const snapshot = selectSnapshotForModel(report, model);
813
828
  if (!snapshot) return "codex usage unavailable";
814
829
 
815
- const parts = ["codex"];
830
+ const parts = [formatStatuslinePrefix(snapshot)];
816
831
  if (snapshot.primary) parts.push(`${formatRemainingPercent(snapshot.primary)} 5h`);
817
832
  if (snapshot.secondary) parts.push(`${formatRemainingPercent(snapshot.secondary)} wk`);
818
833
  if (parts.length === 1 && snapshot.credits) parts.push(formatCredits(snapshot.credits));
819
834
  return parts.join(" ");
820
835
  }
821
836
 
837
+ function selectSnapshotForModel(
838
+ report: CodexUsageReport,
839
+ model: CodexUsageModel | undefined,
840
+ ): NormalizedRateLimitSnapshot | undefined {
841
+ const codexSnapshot = report.snapshots.find(isPrimaryCodexSnapshot);
842
+ if (!model || !isOpenAICodexModel(model)) return codexSnapshot ?? report.snapshots[0];
843
+
844
+ const modelKeys = normalizedModelUsageKeys(model);
845
+ const exactMatch = report.snapshots.find((snapshot) =>
846
+ normalizedSnapshotUsageKeys(snapshot).some((key) => modelKeys.has(key)),
847
+ );
848
+ if (exactMatch) return exactMatch;
849
+
850
+ const variants = codexModelVariantKeys(modelKeys);
851
+ for (const variant of variants) {
852
+ const matches = report.snapshots.filter(
853
+ (snapshot) =>
854
+ !isPrimaryCodexSnapshot(snapshot) &&
855
+ normalizedSnapshotUsageKeys(snapshot).some((key) => normalizedKeyHasToken(key, variant)),
856
+ );
857
+ if (matches.length === 1) return matches[0];
858
+ }
859
+
860
+ return codexSnapshot ?? report.snapshots[0];
861
+ }
862
+
863
+ function normalizedModelUsageKeys(model: CodexUsageModel): Set<string> {
864
+ const keys = new Set<string>();
865
+ addNormalizedUsageKey(keys, model.id);
866
+ addNormalizedUsageKey(keys, model.name);
867
+
868
+ for (const key of [...keys]) {
869
+ const codexIndex = key.indexOf("codex");
870
+ if (codexIndex >= 0) keys.add(key.slice(codexIndex));
871
+ }
872
+
873
+ return keys;
874
+ }
875
+
876
+ function normalizedSnapshotUsageKeys(snapshot: NormalizedRateLimitSnapshot): string[] {
877
+ return [normalizedUsageKey(snapshot.limitId), normalizedUsageKey(snapshot.limitName)].filter(
878
+ (key): key is string => key !== undefined,
879
+ );
880
+ }
881
+
882
+ function addNormalizedUsageKey(keys: Set<string>, value: string | undefined): void {
883
+ const key = normalizedUsageKey(value);
884
+ if (key) keys.add(key);
885
+ }
886
+
887
+ function normalizedUsageKey(value: string | undefined): string | undefined {
888
+ const key = value
889
+ ?.toLowerCase()
890
+ .replace(/[^a-z0-9]+/g, "-")
891
+ .replace(/^-+|-+$/g, "");
892
+ return key || undefined;
893
+ }
894
+
895
+ function codexModelVariantKeys(modelKeys: Set<string>): string[] {
896
+ const variants = new Set<string>();
897
+ for (const key of modelKeys) {
898
+ const match = key.match(/(?:^|-)codex-(.+)$/);
899
+ if (match?.[1]) variants.add(match[1]);
900
+ }
901
+ return [...variants];
902
+ }
903
+
904
+ function normalizedKeyHasToken(key: string, token: string): boolean {
905
+ return (
906
+ key === token ||
907
+ key.startsWith(`${token}-`) ||
908
+ key.endsWith(`-${token}`) ||
909
+ key.includes(`-${token}-`)
910
+ );
911
+ }
912
+
913
+ function formatStatuslinePrefix(snapshot: NormalizedRateLimitSnapshot): string {
914
+ if (isPrimaryCodexSnapshot(snapshot)) return "codex";
915
+ const label = snapshot.limitName ?? snapshot.limitId;
916
+ return `codex ${compactLimitLabel(label)}`;
917
+ }
918
+
919
+ function compactLimitLabel(label: string): string {
920
+ const normalized = label.replace(/[_-]+/g, " ").trim();
921
+ const codexVariant = normalized.match(/\bcodex\s+(.+)$/i)?.[1]?.trim();
922
+ const compact = codexVariant || normalized;
923
+ return compact.toLowerCase().replace(/\s+/g, " ");
924
+ }
925
+
822
926
  function formatRemainingPercent(window: NormalizedRateLimitWindow): string {
823
927
  return `${(100 - clampPercent(window.usedPercent)).toFixed(0)}%`;
824
928
  }
@@ -840,7 +944,10 @@ function brightenInfoNotification(text: string): string {
840
944
  }
841
945
 
842
946
  function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean {
843
- return snapshot.limitId.toLowerCase() === "codex";
947
+ return (
948
+ normalizedUsageKey(snapshot.limitId) === "codex" ||
949
+ normalizedUsageKey(snapshot.limitName) === "codex"
950
+ );
844
951
  }
845
952
 
846
953
  function formatWindowLine(label: string, window: NormalizedRateLimitWindow): string {