@hk_net/pi-usage-bars 0.3.0 → 0.4.1

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.
@@ -1,4 +1,4 @@
1
- /** Usage bars for subscription-backed providers supported by current Pi releases. */
1
+ /** Quota, balance, and spend indicators for providers supported by current Pi releases. */
2
2
 
3
3
  import {
4
4
  DynamicBorder,
@@ -22,9 +22,16 @@ import {
22
22
  fetchAllUsages,
23
23
  fetchClaudeUsageWithFallback,
24
24
  fetchCodexUsage,
25
+ fetchDeepSeekBalance,
26
+ fetchKimiUsage,
27
+ fetchMiniMaxUsage,
28
+ fetchMoonshotBalance,
29
+ fetchOpenRouterUsage,
25
30
  fetchZaiUsage,
26
31
  providerToPiProviderId,
27
32
  resolveUsageEndpoints,
33
+ type AccountBalance,
34
+ type AccountSpend,
28
35
  type ProviderKey,
29
36
  type UsageByProvider,
30
37
  type UsageData,
@@ -33,15 +40,63 @@ import {
33
40
 
34
41
  const POLL_INTERVAL_MS = 2 * 60 * 1000;
35
42
  const STATUS_KEY = "usage-bars";
36
- const PROVIDERS: readonly ProviderKey[] = ["codex", "claude", "zai", "zai-cn"];
43
+ const PROVIDERS: readonly ProviderKey[] = [
44
+ "codex",
45
+ "claude",
46
+ "zai",
47
+ "zai-cn",
48
+ "kimi",
49
+ "minimax",
50
+ "minimax-cn",
51
+ "openrouter",
52
+ "deepseek",
53
+ "moonshot",
54
+ "moonshot-cn",
55
+ ];
37
56
 
38
57
  const PROVIDER_LABELS: Record<ProviderKey, string> = {
39
58
  codex: "Codex",
40
59
  claude: "Claude",
41
60
  zai: "ZAI Coding Plan (Global)",
42
61
  "zai-cn": "ZAI Coding Plan (China)",
62
+ kimi: "Kimi For Coding",
63
+ minimax: "MiniMax Coding Plan (Global)",
64
+ "minimax-cn": "MiniMax Coding Plan (China)",
65
+ openrouter: "OpenRouter",
66
+ deepseek: "DeepSeek",
67
+ moonshot: "Moonshot/Kimi API (Global)",
68
+ "moonshot-cn": "Moonshot/Kimi API (China)",
43
69
  };
44
70
 
71
+ function formatFinancialAmount(amount: number, unit: string): string {
72
+ if (/^[A-Z]{3}$/.test(unit)) {
73
+ return new Intl.NumberFormat("en-US", {
74
+ style: "currency",
75
+ currency: unit,
76
+ minimumFractionDigits: 2,
77
+ maximumFractionDigits: 5,
78
+ }).format(amount);
79
+ }
80
+ const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(amount);
81
+ return `${formatted} ${unit}`;
82
+ }
83
+
84
+ function formatAccountBalance(balance: AccountBalance): string {
85
+ return `${balance.label} · ${formatFinancialAmount(balance.amount, balance.unit)}`;
86
+ }
87
+
88
+ function formatAccountSpend(spend: AccountSpend): string {
89
+ const values = [
90
+ spend.daily === undefined ? undefined : `today ${formatFinancialAmount(spend.daily, spend.unit)}`,
91
+ spend.weekly === undefined ? undefined : `week ${formatFinancialAmount(spend.weekly, spend.unit)}`,
92
+ spend.monthly === undefined ? undefined : `month ${formatFinancialAmount(spend.monthly, spend.unit)}`,
93
+ ].filter((value): value is string => Boolean(value));
94
+ if (values.length === 0 && spend.lifetime !== undefined) {
95
+ values.push(`lifetime ${formatFinancialAmount(spend.lifetime, spend.unit)}`);
96
+ }
97
+ return `Spent · ${values.join(" · ")}`;
98
+ }
99
+
45
100
  interface SubscriptionItem {
46
101
  name: string;
47
102
  provider: ProviderKey;
@@ -135,12 +190,12 @@ class UsageSelectorComponent extends Container implements Focusable {
135
190
 
136
191
  private updateHint(): void {
137
192
  if (this.hint === "loading") {
138
- this.hintText.setText(this.theme.fg("dim", "Fetching usage from configured subscription providers…"));
193
+ this.hintText.setText(this.theme.fg("dim", "Fetching quota, balance, and spend from configured providers…"));
139
194
  } else if (this.hint === "error") {
140
195
  this.hintText.setText(this.theme.fg("error", "Failed to fetch usage data"));
141
196
  } else {
142
197
  this.hintText.setText(
143
- this.theme.fg("muted", "Only showing configured subscription providers. ") +
198
+ this.theme.fg("muted", "Only showing configured usage providers. ") +
144
199
  this.theme.fg("dim", "✓ = active provider"),
145
200
  );
146
201
  }
@@ -197,19 +252,51 @@ class UsageSelectorComponent extends Container implements Focusable {
197
252
  const weeklyReset = item.data.weeklyResetsIn
198
253
  ? theme.fg("dim", ` resets in ${item.data.weeklyResetsIn}`)
199
254
  : "";
200
-
201
- this.listContainer.addChild(new Text(
202
- indent + theme.fg("muted", "Session ") + this.renderBar(session) + " " +
203
- theme.fg(colorForPercent(session), `${session}%`.padStart(4)) + sessionReset,
204
- 0,
205
- 0,
206
- ));
207
- this.listContainer.addChild(new Text(
208
- indent + theme.fg("muted", "Weekly ") + this.renderBar(weekly) + " " +
209
- theme.fg(colorForPercent(weekly), `${weekly}%`.padStart(4)) + weeklyReset,
210
- 0,
211
- 0,
212
- ));
255
+ const sessionLabel = (item.data.sessionLabel ?? "Session").slice(0, 9).padEnd(10);
256
+ const weeklyLabel = (item.data.weeklyLabel ?? "Weekly").slice(0, 9).padEnd(10);
257
+
258
+ if (!item.data.quotaHidden) {
259
+ if (!item.data.sessionHidden) {
260
+ this.listContainer.addChild(new Text(
261
+ indent + theme.fg("muted", sessionLabel) + this.renderBar(session) + " " +
262
+ theme.fg(colorForPercent(session), `${session}%`.padStart(4)) + sessionReset,
263
+ 0,
264
+ 0,
265
+ ));
266
+ }
267
+ if (!item.data.weeklyHidden) {
268
+ this.listContainer.addChild(new Text(
269
+ indent + theme.fg("muted", weeklyLabel) + this.renderBar(weekly) + " " +
270
+ theme.fg(colorForPercent(weekly), `${weekly}%`.padStart(4)) + weeklyReset,
271
+ 0,
272
+ 0,
273
+ ));
274
+ }
275
+ }
276
+ if (item.data.accountBalance) {
277
+ this.listContainer.addChild(new Text(
278
+ indent + theme.fg("muted", formatAccountBalance(item.data.accountBalance)),
279
+ 0,
280
+ 0,
281
+ ));
282
+ }
283
+ for (const balance of item.data.accountBalanceDetails ?? []) {
284
+ this.listContainer.addChild(new Text(
285
+ indent + theme.fg("dim", formatAccountBalance(balance)),
286
+ 0,
287
+ 0,
288
+ ));
289
+ }
290
+ if (item.data.accountSpend) {
291
+ this.listContainer.addChild(new Text(
292
+ indent + theme.fg("muted", formatAccountSpend(item.data.accountSpend)),
293
+ 0,
294
+ 0,
295
+ ));
296
+ }
297
+ if (item.data.notice) {
298
+ this.listContainer.addChild(new Text(indent + theme.fg("muted", item.data.notice), 0, 0));
299
+ }
213
300
 
214
301
  if (typeof item.data.extraSpend === "number" && typeof item.data.extraLimit === "number") {
215
302
  this.listContainer.addChild(new Text(
@@ -300,6 +387,13 @@ export default function (pi: ExtensionAPI): void {
300
387
  claude: null,
301
388
  zai: null,
302
389
  "zai-cn": null,
390
+ kimi: null,
391
+ minimax: null,
392
+ "minimax-cn": null,
393
+ openrouter: null,
394
+ deepseek: null,
395
+ moonshot: null,
396
+ "moonshot-cn": null,
303
397
  activeProvider: null,
304
398
  available: {},
305
399
  };
@@ -343,15 +437,50 @@ export default function (pi: ExtensionAPI): void {
343
437
  ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", `${label} usage unavailable (${data.error})`));
344
438
  return;
345
439
  }
440
+ if (data.quotaHidden) {
441
+ const financial = [
442
+ data.accountBalance ? formatAccountBalance(data.accountBalance) : undefined,
443
+ data.accountSpend?.monthly === undefined
444
+ ? undefined
445
+ : `Month · ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`,
446
+ ].filter((value): value is string => Boolean(value));
447
+ const summary = financial.length > 0 ? financial.join(" · ") : data.notice;
448
+ ctx.ui.setStatus(
449
+ STATUS_KEY,
450
+ summary ? theme.fg("dim", `${label} `) + theme.fg("muted", summary) : undefined,
451
+ );
452
+ return;
453
+ }
346
454
 
347
455
  const session = clampPercent(data.session);
348
456
  const weekly = clampPercent(data.weekly);
457
+ const sessionPrefix = data.sessionLabel === "5-hour"
458
+ ? "5h "
459
+ : data.sessionLabel === "Interval"
460
+ ? "I "
461
+ : data.sessionLabel === "Key limit"
462
+ ? "L "
463
+ : "S ";
464
+ const quotaLanes: string[] = [];
465
+ if (!data.sessionHidden) {
466
+ quotaLanes.push(
467
+ theme.fg("muted", sessionPrefix) + renderBar(theme, session) + " " + renderPercent(theme, session) +
468
+ (data.sessionResetsIn ? theme.fg("dim", ` ⟳ ${data.sessionResetsIn}`) : ""),
469
+ );
470
+ }
471
+ if (!data.weeklyHidden) {
472
+ quotaLanes.push(
473
+ theme.fg("muted", "W ") + renderBar(theme, weekly) + " " + renderPercent(theme, weekly) +
474
+ (data.weeklyResetsIn ? theme.fg("dim", ` ⟳ ${data.weeklyResetsIn}`) : ""),
475
+ );
476
+ }
349
477
  const status =
350
478
  theme.fg("dim", `${label} `) +
351
- theme.fg("muted", "S ") + renderBar(theme, session) + " " + renderPercent(theme, session) +
352
- (data.sessionResetsIn ? theme.fg("dim", ` ${data.sessionResetsIn}`) : "") +
353
- theme.fg("muted", " W ") + renderBar(theme, weekly) + " " + renderPercent(theme, weekly) +
354
- (data.weeklyResetsIn ? theme.fg("dim", ` ⟳ ${data.weeklyResetsIn}`) : "") +
479
+ quotaLanes.join(" ") +
480
+ (data.accountBalance ? theme.fg("muted", ` · ${formatAccountBalance(data.accountBalance)}`) : "") +
481
+ (data.accountSpend?.monthly === undefined
482
+ ? ""
483
+ : theme.fg("muted", ` · Month ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`)) +
355
484
  (data.stale ? theme.fg("warning", " stale") : "") +
356
485
  (data.warning && !data.stale ? theme.fg("warning", " ⚠") : "");
357
486
  ctx.ui.setStatus(STATUS_KEY, status);
@@ -404,6 +533,25 @@ export default function (pi: ExtensionAPI): void {
404
533
  if (provider === "claude") state.claude = await fetchClaudeUsageWithFallback(credential.token, { signal });
405
534
  if (provider === "zai") state.zai = await fetchZaiUsage(credential.token, "zai", { endpoints, signal });
406
535
  if (provider === "zai-cn") state["zai-cn"] = await fetchZaiUsage(credential.token, "zai-cn", { endpoints, signal });
536
+ if (provider === "kimi") state.kimi = await fetchKimiUsage(credential.token, { endpoints, signal });
537
+ if (provider === "minimax") {
538
+ state.minimax = await fetchMiniMaxUsage(credential.token, "minimax", { endpoints, signal });
539
+ }
540
+ if (provider === "minimax-cn") {
541
+ state["minimax-cn"] = await fetchMiniMaxUsage(credential.token, "minimax-cn", { endpoints, signal });
542
+ }
543
+ if (provider === "openrouter") {
544
+ state.openrouter = await fetchOpenRouterUsage(credential.token, { endpoints, signal });
545
+ }
546
+ if (provider === "deepseek") {
547
+ state.deepseek = await fetchDeepSeekBalance(credential.token, { endpoints, signal });
548
+ }
549
+ if (provider === "moonshot") {
550
+ state.moonshot = await fetchMoonshotBalance(credential.token, "moonshot", { endpoints, signal });
551
+ }
552
+ if (provider === "moonshot-cn") {
553
+ state["moonshot-cn"] = await fetchMoonshotBalance(credential.token, "moonshot-cn", { endpoints, signal });
554
+ }
407
555
  }
408
556
 
409
557
  async function runPoll(): Promise<void> {
@@ -493,7 +641,7 @@ export default function (pi: ExtensionAPI): void {
493
641
  });
494
642
 
495
643
  pi.registerCommand("usage", {
496
- description: "Show subscription usage for configured providers",
644
+ description: "Show quota, balance, and spend for configured providers",
497
645
  handler: async (_args, ctx) => {
498
646
  currentContext = ctx;
499
647
  updateProviderFrom(ctx.model);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hk_net/pi-usage-bars",
3
- "version": "0.3.0",
4
- "description": "Subscription usage bars for current Pi providers (Codex, Claude, and ZAI Coding Plan)",
3
+ "version": "0.4.1",
4
+ "description": "Quota, balance, and spend indicators for current Pi providers",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
@@ -9,7 +9,12 @@
9
9
  "usage",
10
10
  "codex",
11
11
  "anthropic",
12
- "zai"
12
+ "zai",
13
+ "kimi",
14
+ "minimax",
15
+ "openrouter",
16
+ "deepseek",
17
+ "moonshot"
13
18
  ],
14
19
  "license": "MIT",
15
20
  "repository": {
@@ -27,6 +32,7 @@
27
32
  "type": "module",
28
33
  "files": [
29
34
  "extensions",
35
+ "docs",
30
36
  "README.md",
31
37
  "CHANGELOG.md",
32
38
  "LICENSE"
@@ -34,8 +40,7 @@
34
40
  "scripts": {
35
41
  "test": "bun test tests",
36
42
  "typecheck": "tsc --noEmit",
37
- "smoke": "pi -e ./extensions/usage-bars/index.ts --list-models",
38
- "check": "npm run typecheck && npm test && npm run smoke",
43
+ "check": "npm run typecheck && npm test",
39
44
  "prepublishOnly": "npm run check"
40
45
  },
41
46
  "engines": {
@@ -46,8 +51,8 @@
46
51
  "@earendil-works/pi-tui": ">=0.81.1"
47
52
  },
48
53
  "devDependencies": {
49
- "@earendil-works/pi-coding-agent": "0.81.1",
50
- "@earendil-works/pi-tui": "0.81.1",
54
+ "@earendil-works/pi-coding-agent": "0.83.0",
55
+ "@earendil-works/pi-tui": "0.83.0",
51
56
  "@types/bun": "1.3.14",
52
57
  "@types/node": "24.13.3",
53
58
  "typescript": "5.9.3"