@narumitw/pi-usage 0.60.11 → 0.61.0

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
@@ -66,7 +66,7 @@ Read the [query and reset guide](./docs/operations.md) for target selection, can
66
66
 
67
67
  ## ⚙️ Settings
68
68
 
69
- Choose **Settings** in `/usage` to edit Codex Fast mode and the Codex reset countdown through Pi's settings-list interaction in TUI mode.
69
+ Choose **Settings** in `/usage` to edit Codex Fast mode, percentage display, and the reset countdown through Pi's settings-list interaction in TUI mode.
70
70
  RPC mode reports the active manual settings path instead of opening terminal UI.
71
71
 
72
72
  These preferences live in `pi-usage.json` under Pi's user agent directory, normally `~/.pi/agent/pi-usage.json`.
@@ -87,7 +87,7 @@ Run `/fast` without arguments to toggle Fast for the active supported Codex mode
87
87
  Fast is about 1.5× faster and uses more of your plan allowance.
88
88
  The `codexFastMode` preference defaults to Off.
89
89
 
90
- Fast currently applies only to official `openai-codex-responses` requests for `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` at `https://chatgpt.com`.
90
+ Fast currently applies only to official `openai-codex-responses` requests for `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, and `gpt-6-sol` at `https://chatgpt.com`.
91
91
  It sends `service_tier: "priority"` while enabled and explicit `service_tier: "default"` otherwise.
92
92
  The statusline adds `fast` only while the preference is effective, for example `codex fast 59% ↻ 2h30m` with the default reset countdown.
93
93
  Unsupported models and custom or proxy origins are left unchanged.
@@ -95,6 +95,20 @@ Unsupported models and custom or proxy origins are left unchanged.
95
95
  A toggle affects provider requests whose payload hook starts after the save; a request already sent is unchanged.
96
96
  Repair or remove an invalid file, then run `/reload` before trying the toggle again.
97
97
 
98
+ ### Codex statusline percentage
99
+
100
+ The `codexStatusPercentage` preference accepts `"remaining"` or `"used"` and defaults to `"remaining"` to preserve existing output.
101
+ Choose **Codex percentage** in the TUI Settings screen, or set the field manually and run `/reload`:
102
+
103
+ ```json
104
+ {
105
+ "codexStatusPercentage": "used"
106
+ }
107
+ ```
108
+
109
+ With `"used"`, a limit with 70% remaining appears as `30%`; its reset countdown or window label stays unchanged.
110
+ This preference changes only Codex statusline formatting, not provider data or the detailed `/usage` report.
111
+
98
112
  ### Codex statusline reset countdown
99
113
 
100
114
  The `codexStatusResetCountdown` preference defaults to `true`. It replaces the window labels with the time remaining until each returned limit resets.
package/dist/index.ts CHANGED
@@ -9,7 +9,8 @@ var CODEX_FAST_MODEL_IDS = /* @__PURE__ */ new Set([
9
9
  "gpt-5.5",
10
10
  "gpt-5.6-luna",
11
11
  "gpt-5.6-sol",
12
- "gpt-5.6-terra"
12
+ "gpt-5.6-terra",
13
+ "gpt-6-sol"
13
14
  ]);
14
15
  function codexFastAvailability(model, enabled) {
15
16
  if (model?.provider !== "openai-codex") return { kind: "not-codex" };
@@ -936,11 +937,18 @@ var FIVE_HOUR_WINDOW_MINUTES = 300;
936
937
  var DAILY_WINDOW_MINUTES = 1440;
937
938
  var WEEKLY_WINDOW_MINUTES = 10080;
938
939
  var FIXED_POINT_UNITS_PER_CENT = 1e6;
940
+ var RATIO_WINDOWS = [
941
+ { key: "limit_5h", id: "five-hour", label: "Five-hour window", windowMinutes: FIVE_HOUR_WINDOW_MINUTES },
942
+ { key: "limit_week", id: "weekly", label: "Weekly window", windowMinutes: WEEKLY_WINDOW_MINUTES },
943
+ { key: "limit_month_total", id: "monthly", label: "Monthly window" }
944
+ ];
939
945
  function normalizeKimiCodingUsagePayload(payload, capturedAt) {
940
946
  const root = asObject6(payload);
941
947
  if (!root) throw new Error("Kimi Coding usage response was not an object.");
942
948
  const candidates = [];
949
+ const explicitWindows = /* @__PURE__ */ new Set();
943
950
  let omittedWindow = false;
951
+ if (root.usage !== void 0) explicitWindows.add(WEEKLY_WINDOW_MINUTES);
944
952
  const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
945
953
  if (summary) candidates.push(summary);
946
954
  else if (root.usage !== void 0) omittedWindow = true;
@@ -948,6 +956,7 @@ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
948
956
  for (const raw of root.limits) {
949
957
  const item = asObject6(raw);
950
958
  const windowMinutes = parseWindowMinutes(item?.window);
959
+ if (windowMinutes !== void 0) explicitWindows.add(windowMinutes);
951
960
  const label = sanitizedLabel(item?.name);
952
961
  const bucket = windowMinutes === void 0 ? void 0 : parseUsageRow(item?.detail, windowMinutes, label ?? defaultWindowLabel(windowMinutes));
953
962
  if (bucket) candidates.push(bucket);
@@ -966,8 +975,23 @@ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
966
975
  if (rows.length === 1) buckets.push(rows[0]);
967
976
  else omittedWindow = true;
968
977
  }
969
- buckets.sort((left, right) => (left.windowMinutes ?? 0) - (right.windowMinutes ?? 0));
970
- const metrics = parseBoosterWallet(root.boosterWallet);
978
+ const usages = asObject6(root.usages);
979
+ if (root.usages !== void 0 && !usages) omittedWindow = true;
980
+ if (usages) {
981
+ for (const window of RATIO_WINDOWS) {
982
+ const raw = usages[window.key];
983
+ if (raw === void 0 || "windowMinutes" in window && explicitWindows.has(window.windowMinutes)) continue;
984
+ const bucket = parseRatioWindow(raw, window.id, window.label);
985
+ if (bucket) {
986
+ buckets.push({
987
+ ...bucket,
988
+ ..."windowMinutes" in window ? { windowMinutes: window.windowMinutes } : {}
989
+ });
990
+ } else omittedWindow = true;
991
+ }
992
+ }
993
+ buckets.sort((left, right) => (left.windowMinutes ?? Infinity) - (right.windowMinutes ?? Infinity));
994
+ const metrics = parseBoosterWallet(root.boosterWallet === void 0 ? root.booster_wallet : root.boosterWallet);
971
995
  if (buckets.length === 0 && metrics.length === 0) {
972
996
  throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
973
997
  }
@@ -1006,6 +1030,21 @@ function parseUsageRow(value, windowMinutes, label) {
1006
1030
  ...resetsAt !== void 0 ? { resetsAt } : {}
1007
1031
  };
1008
1032
  }
1033
+ function parseRatioWindow(value, id, label) {
1034
+ const row = asObject6(value);
1035
+ const ratio = row?.used_ratio;
1036
+ if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < 0 || ratio > 1) return void 0;
1037
+ const resetsAt = asIsoEpochSeconds(row?.reset_time);
1038
+ const used = ratio * 100;
1039
+ return {
1040
+ id,
1041
+ label,
1042
+ used,
1043
+ remaining: 100 - used,
1044
+ unit: "percent",
1045
+ ...resetsAt !== void 0 ? { resetsAt } : {}
1046
+ };
1047
+ }
1009
1048
  function parseWindowMinutes(value) {
1010
1049
  const window = asObject6(value);
1011
1050
  if (!window) return void 0;
@@ -3189,10 +3228,10 @@ function formatUsageReport(report2, displayState) {
3189
3228
  }
3190
3229
  return lines.join("\n").trimEnd();
3191
3230
  }
3192
- function formatUsageStatusline(report2, model, now = Date.now(), showCodexResetCountdown = true) {
3231
+ function formatUsageStatusline(report2, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
3193
3232
  if (report2.providerId === "baseten") return formatBasetenStatusline(report2);
3194
3233
  if (report2.providerId === "openai-codex") {
3195
- return formatCodexStatusline(report2, model, now, showCodexResetCountdown);
3234
+ return formatCodexStatusline(report2, model, now, showCodexResetCountdown, codexStatusPercentage);
3196
3235
  }
3197
3236
  if (report2.providerId === "deepseek") return formatDeepSeekStatusline(report2);
3198
3237
  if (report2.providerId === "fireworks") return formatFireworksStatusline(report2);
@@ -3387,6 +3426,12 @@ function formatOpenCodeZenStatusline(report2) {
3387
3426
  function formatKimiCodingReport(lines, report2) {
3388
3427
  for (const bucket of report2.buckets) {
3389
3428
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
3429
+ if (bucket.unit === "percent") {
3430
+ const remaining = bucket.remaining === void 0 ? void 0 : Math.round(clampPercent4(bucket.remaining));
3431
+ const value = bucket.used === void 0 || remaining === void 0 ? "unavailable" : `${100 - remaining}% used \xB7 ${remaining}% left`;
3432
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
3433
+ continue;
3434
+ }
3390
3435
  if (bucket.used === void 0 || bucket.limit === void 0) {
3391
3436
  lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
3392
3437
  continue;
@@ -3415,15 +3460,18 @@ function formatKimiCodingReport(lines, report2) {
3415
3460
  function formatKimiCodingStatusline(report2) {
3416
3461
  const fiveHour = report2.buckets.find((bucket) => bucket.id === "five-hour");
3417
3462
  const weekly = report2.buckets.find((bucket) => bucket.id === "weekly");
3418
- const subWindow = fiveHour ?? report2.buckets.find((bucket) => bucket.id !== "weekly");
3419
- const selected = [subWindow, weekly].filter(
3463
+ const monthly = report2.buckets.find((bucket) => bucket.id === "monthly");
3464
+ const subWindow = fiveHour ?? report2.buckets.find((bucket) => bucket.id !== "weekly" && bucket.id !== "monthly");
3465
+ const selected = [subWindow, weekly, monthly].filter(
3420
3466
  (bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
3421
3467
  );
3422
3468
  const parts = ["kimi"];
3423
3469
  for (const bucket of selected) {
3424
- if (!bucket.limit || bucket.remaining === void 0) continue;
3470
+ if (bucket.remaining === void 0 || bucket.unit !== "percent" && !bucket.limit) continue;
3425
3471
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
3426
- parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
3472
+ const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
3473
+ const remaining = bucket.unit === "percent" ? Math.round(clampPercent4(bucket.remaining)) : percentRemaining(bucket);
3474
+ parts.push(`${remaining}% ${window}`);
3427
3475
  }
3428
3476
  return parts.length > 1 ? parts.join(" ") : void 0;
3429
3477
  }
@@ -3597,7 +3645,7 @@ function formatGenericReport(lines, report2) {
3597
3645
  lines.push(`${`${metric2.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric2.value, metric2.unit)}`);
3598
3646
  }
3599
3647
  }
3600
- function formatCodexStatusline(report2, model, now = Date.now(), showResetCountdown = true) {
3648
+ function formatCodexStatusline(report2, model, now = Date.now(), showResetCountdown = true, percentage = "remaining") {
3601
3649
  const group = selectCodexGroup(report2, model);
3602
3650
  if (!group) return formatCodexCreditsStatus(report2);
3603
3651
  const buckets = report2.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
@@ -3605,7 +3653,8 @@ function formatCodexStatusline(report2, model, now = Date.now(), showResetCountd
3605
3653
  const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
3606
3654
  for (const bucket of buckets) {
3607
3655
  if (bucket.remaining === void 0) continue;
3608
- const percent = `${clampPercent4(bucket.remaining).toFixed(0)}%`;
3656
+ const displayedPercentage = percentage === "used" ? bucket.used ?? 100 - bucket.remaining : bucket.remaining;
3657
+ const percent = `${clampPercent4(displayedPercentage).toFixed(0)}%`;
3609
3658
  const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
3610
3659
  const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
3611
3660
  if (!showResetCountdown) {
@@ -3739,6 +3788,7 @@ var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
3739
3788
  var DEFAULT_USAGE_SETTINGS = Object.freeze({
3740
3789
  codexFastMode: false,
3741
3790
  codexStatusResetCountdown: true,
3791
+ codexStatusPercentage: "remaining",
3742
3792
  selectedTargets: Object.freeze({})
3743
3793
  });
3744
3794
  function usageSettingsPath() {
@@ -3752,6 +3802,9 @@ function normalizeUsageSettings(value) {
3752
3802
  if (Object.hasOwn(value, "codexStatusResetCountdown") && typeof value.codexStatusResetCountdown !== "boolean") {
3753
3803
  return void 0;
3754
3804
  }
3805
+ if (Object.hasOwn(value, "codexStatusPercentage") && value.codexStatusPercentage !== "remaining" && value.codexStatusPercentage !== "used") {
3806
+ return void 0;
3807
+ }
3755
3808
  if (Object.hasOwn(value, "fireworksAccountId") && !isFireworksAccountId(value.fireworksAccountId)) {
3756
3809
  return void 0;
3757
3810
  }
@@ -3764,6 +3817,7 @@ function normalizeUsageSettings(value) {
3764
3817
  return {
3765
3818
  codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
3766
3819
  codexStatusResetCountdown: typeof value.codexStatusResetCountdown === "boolean" ? value.codexStatusResetCountdown : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
3820
+ codexStatusPercentage: value.codexStatusPercentage === "used" ? "used" : DEFAULT_USAGE_SETTINGS.codexStatusPercentage,
3767
3821
  selectedTargets: effectiveTargets
3768
3822
  };
3769
3823
  }
@@ -4166,6 +4220,12 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
4166
4220
  import { Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
4167
4221
  var OFF = "Off";
4168
4222
  var ON = "On";
4223
+ var REMAINING = "Remaining";
4224
+ var USED = "Used";
4225
+ function settingValueLabel(id, value) {
4226
+ if (id === "codexStatusPercentage") return value === "used" ? USED : REMAINING;
4227
+ return value === true ? ON : OFF;
4228
+ }
4169
4229
  async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent, onApplied) {
4170
4230
  if (ctx.mode !== "tui") {
4171
4231
  if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
@@ -4194,6 +4254,13 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4194
4254
  description: "Show time remaining until each Codex usage limit resets.",
4195
4255
  currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
4196
4256
  values: [OFF, ON]
4257
+ },
4258
+ {
4259
+ id: "codexStatusPercentage",
4260
+ label: "Codex percentage",
4261
+ description: "Show remaining or used Codex quota in the statusline.",
4262
+ currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
4263
+ values: [REMAINING, USED]
4197
4264
  }
4198
4265
  ];
4199
4266
  const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
@@ -4208,7 +4275,7 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4208
4275
  saveQueue = saveQueue.then(async () => {
4209
4276
  const previous = settingsRuntime.get().settings[id];
4210
4277
  if (settingsRuntime.get().kind === "invalid") {
4211
- settingsList.updateValue(id, previous ? ON : OFF);
4278
+ settingsList.updateValue(id, settingValueLabel(id, previous));
4212
4279
  if (!signal.aborted && isCurrent()) {
4213
4280
  ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
4214
4281
  tui.requestRender();
@@ -4216,10 +4283,12 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4216
4283
  return;
4217
4284
  }
4218
4285
  try {
4219
- await settingsRuntime.update({ [id]: requested }, signal);
4286
+ const patch = {};
4287
+ patch[id] = requested;
4288
+ await settingsRuntime.update(patch, signal);
4220
4289
  } catch (error) {
4221
4290
  if (signal.aborted || !isCurrent()) return;
4222
- settingsList.updateValue(id, previous ? ON : OFF);
4291
+ settingsList.updateValue(id, settingValueLabel(id, previous));
4223
4292
  ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
4224
4293
  tui.requestRender();
4225
4294
  return;
@@ -4239,7 +4308,11 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4239
4308
  getSettingsListTheme(),
4240
4309
  (id, value) => {
4241
4310
  if (closing || signal.aborted || !isCurrent()) return;
4242
- queueUpdate(id, value !== OFF, value);
4311
+ if (id === "codexStatusPercentage") {
4312
+ queueUpdate(id, value === USED ? "used" : "remaining", value);
4313
+ } else if (id === "codexFastMode" || id === "codexStatusResetCountdown") {
4314
+ queueUpdate(id, value === ON, value);
4315
+ }
4243
4316
  },
4244
4317
  cancel
4245
4318
  );
@@ -4368,9 +4441,16 @@ function usageExtension(pi, dependencies = {}) {
4368
4441
  }
4369
4442
  return;
4370
4443
  }
4371
- const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settingsRuntime.get().settings.codexStatusResetCountdown;
4444
+ const settings = settingsRuntime.get().settings;
4445
+ const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
4372
4446
  const now = Date.now();
4373
- const rawValue = formatUsageStatusline(outcome.state.report, model, now, showCodexResetCountdown);
4447
+ const rawValue = formatUsageStatusline(
4448
+ outcome.state.report,
4449
+ model,
4450
+ now,
4451
+ showCodexResetCountdown,
4452
+ settings.codexStatusPercentage
4453
+ );
4374
4454
  const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : void 0;
4375
4455
  if (!safeSetStatus(ctx, value)) return;
4376
4456
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
@@ -5063,7 +5143,7 @@ function usageExtension(pi, dependencies = {}) {
5063
5143
  controller.signal,
5064
5144
  () => statusGeneration === menuGeneration && !controller.signal.aborted,
5065
5145
  (id) => {
5066
- if (id === "codexStatusResetCountdown" && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
5146
+ if ((id === "codexStatusResetCountdown" || id === "codexStatusPercentage") && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
5067
5147
  publishStableCurrent(ctx, stableCurrent);
5068
5148
  }
5069
5149
  }