@ogulcancelik/pi-minimal-footer 0.1.9 → 0.1.11

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 (3) hide show
  1. package/README.md +2 -1
  2. package/index.ts +89 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,9 +21,10 @@ Minimal footer for [pi](https://github.com/earendil-works/pi) that replaces the
21
21
  | OpenAI Codex | Primary + secondary rolling windows |
22
22
  | GitHub Copilot | Premium interactions + chat quotas |
23
23
  | Google Gemini | Pro + Flash remaining quotas |
24
- | MiniMax | 5h + weekly rolling windows (Token Plan / Coding Plan) |
24
+ | MiniMax | 5h + weekly rolling windows (Token Plan, credit-based) |
25
25
  | MiniMax CN | Same as MiniMax, China endpoint |
26
26
  | Kimi Coding | 5h + weekly rolling windows (Plan) |
27
+ | OpenCode Go | 5h + weekly + monthly usage windows |
27
28
 
28
29
  ## Install
29
30
 
package/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Custom footer with context gauge + subscription usage bars.
5
5
  * Auto-detects provider from current model and shows relevant usage.
6
6
  *
7
- * Supports: Claude Max, Codex, Copilot, Gemini
7
+ * Supports: Claude Max, Codex, Copilot, Gemini, MiniMax Token Plan, Kimi Coding
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -267,6 +267,10 @@ function getKimiToken(): string | undefined {
267
267
  return getApiKey("kimi-coding", "KIMI_API_KEY");
268
268
  }
269
269
 
270
+ function getOpencodeToken(): string | undefined {
271
+ return getApiKey("opencode-go", "OPENCODE_API_KEY");
272
+ }
273
+
270
274
  // ============ Time Formatting ============
271
275
 
272
276
  function formatResetTime(date: Date): string {
@@ -544,10 +548,12 @@ async function fetchGeminiUsage(): Promise<UsageSnapshot> {
544
548
  async function fetchMinimaxUsage(provider: "minimax" | "minimax-cn"): Promise<UsageSnapshot> {
545
549
  const token = getMinimaxToken(provider);
546
550
  const providerLabel = provider === "minimax-cn" ? "MiniMax CN" : "MiniMax";
551
+ // Docs-recommended Token Plan endpoint. The legacy /coding_plan/remains path
552
+ // still works but the response field names differ from the current UI.
547
553
  const endpoint =
548
554
  provider === "minimax-cn"
549
- ? "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains"
550
- : "https://api.minimax.io/v1/api/openplatform/coding_plan/remains";
555
+ ? "https://api.minimaxi.com/v1/token_plan/remains"
556
+ : "https://api.minimax.io/v1/token_plan/remains";
551
557
 
552
558
  if (!token) {
553
559
  return { provider: providerLabel, windows: [], error: "no-auth", fetchedAt: Date.now() };
@@ -578,9 +584,16 @@ async function fetchMinimaxUsage(provider: "minimax" | "minimax-cn"): Promise<Us
578
584
  }
579
585
 
580
586
  const remains = Array.isArray(data?.model_remains) ? data.model_remains : [];
587
+ // Token Plan returns one bucket per capability (general = text/code, video, etc.).
588
+ // Prefer the active "general" bucket since M-series chat models land there.
589
+ // status === 1 = window is active/limiting, 3 = inactive (no usage). Fall back
590
+ // to the first active bucket, then the first bucket of any kind.
581
591
  const textBucket =
582
- remains.find((entry: any) => typeof entry?.model_name === "string" && /^minimax-m/i.test(entry.model_name)) ||
583
- remains.find((entry: any) => typeof entry?.model_name === "string" && /minimax/i.test(entry.model_name)) ||
592
+ remains.find(
593
+ (entry: any) => entry?.model_name === "general" && Number(entry?.current_interval_status) === 1
594
+ ) ||
595
+ remains.find((entry: any) => entry?.model_name === "general") ||
596
+ remains.find((entry: any) => Number(entry?.current_interval_status) === 1) ||
584
597
  remains[0];
585
598
 
586
599
  if (!textBucket) {
@@ -589,12 +602,12 @@ async function fetchMinimaxUsage(provider: "minimax" | "minimax-cn"): Promise<Us
589
602
 
590
603
  const windows: RateWindow[] = [];
591
604
 
592
- const intervalTotal = Number(textBucket.current_interval_total_count) || 0;
593
- const intervalRemaining = Number(textBucket.current_interval_usage_count) || 0;
594
- if (intervalTotal > 0) {
595
- // current_*_usage_count = remaining (confirmed against MiniMax website UI)
596
- const used = intervalTotal - intervalRemaining;
597
- const usedPercent = clampPercent((used / intervalTotal) * 100);
605
+ // Source of truth: *_remaining_percent (the *_total_count / *_usage_count
606
+ // fields are zeroed in the credit-based model and cannot be used to compute
607
+ // a fraction). The UI usage bar maps 100 - remainingPercent -> used.
608
+ const intervalRemaining = Number(textBucket.current_interval_remaining_percent);
609
+ if (Number.isFinite(intervalRemaining)) {
610
+ const usedPercent = clampPercent(100 - intervalRemaining);
598
611
  const resetDate = textBucket.end_time ? new Date(Number(textBucket.end_time)) : undefined;
599
612
  const durationMs =
600
613
  textBucket.start_time && textBucket.end_time
@@ -607,13 +620,12 @@ async function fetchMinimaxUsage(provider: "minimax" | "minimax-cn"): Promise<Us
607
620
  });
608
621
  }
609
622
 
610
- const weeklyTotal = Number(textBucket.current_weekly_total_count) || 0;
611
- const weeklyRemaining = Number(textBucket.current_weekly_usage_count) || 0;
612
- if (weeklyTotal > 0) {
613
- // current_*_usage_count = remaining (confirmed against MiniMax website UI)
614
- const used = weeklyTotal - weeklyRemaining;
615
- const usedPercent = clampPercent((used / weeklyTotal) * 100);
616
- const resetDate = textBucket.weekly_end_time ? new Date(Number(textBucket.weekly_end_time)) : undefined;
623
+ const weeklyRemaining = Number(textBucket.current_weekly_remaining_percent);
624
+ if (Number.isFinite(weeklyRemaining)) {
625
+ const usedPercent = clampPercent(100 - weeklyRemaining);
626
+ const resetDate = textBucket.weekly_end_time
627
+ ? new Date(Number(textBucket.weekly_end_time))
628
+ : undefined;
617
629
  const durationMs =
618
630
  textBucket.weekly_start_time && textBucket.weekly_end_time
619
631
  ? Number(textBucket.weekly_end_time) - Number(textBucket.weekly_start_time)
@@ -625,6 +637,10 @@ async function fetchMinimaxUsage(provider: "minimax" | "minimax-cn"): Promise<Us
625
637
  });
626
638
  }
627
639
 
640
+ if (windows.length === 0) {
641
+ return { provider: providerLabel, windows: [], error: "no-usage-data", fetchedAt: Date.now() };
642
+ }
643
+
628
644
  return { provider: providerLabel, windows, fetchedAt: Date.now() };
629
645
  } catch (e) {
630
646
  return { provider: providerLabel, windows: [], error: String(e), fetchedAt: Date.now() };
@@ -694,6 +710,58 @@ async function fetchKimiUsage(): Promise<UsageSnapshot> {
694
710
  }
695
711
  }
696
712
 
713
+ async function fetchOpencodeGoUsage(): Promise<UsageSnapshot> {
714
+ const providerLabel = "OpenCode Go";
715
+ const token = getOpencodeToken();
716
+ if (!token) {
717
+ return { provider: providerLabel, windows: [], error: "no-auth", fetchedAt: Date.now() };
718
+ }
719
+
720
+ try {
721
+ const res = await fetchWithTimeout("https://opencode.ai/zen/go/v1/usage", {
722
+ method: "GET",
723
+ headers: {
724
+ Authorization: `Bearer ${token}`,
725
+ },
726
+ });
727
+
728
+ if (!res.ok) {
729
+ return { provider: providerLabel, windows: [], error: `HTTP ${res.status}`, fetchedAt: Date.now() };
730
+ }
731
+
732
+ const data = (await res.json()) as any;
733
+ const windows: RateWindow[] = [];
734
+
735
+ if (data.rollingUsage) {
736
+ const usedPercent = clampPercent(data.rollingUsage.usagePercent ?? 0);
737
+ const resetsIn = Number.isFinite(data.rollingUsage.resetInSec)
738
+ ? formatResetTime(new Date(Date.now() + data.rollingUsage.resetInSec * 1000))
739
+ : undefined;
740
+ windows.push({ label: "5h", usedPercent, resetsIn });
741
+ }
742
+
743
+ if (data.weeklyUsage) {
744
+ const usedPercent = clampPercent(data.weeklyUsage.usagePercent ?? 0);
745
+ const resetsIn = Number.isFinite(data.weeklyUsage.resetInSec)
746
+ ? formatResetTime(new Date(Date.now() + data.weeklyUsage.resetInSec * 1000))
747
+ : undefined;
748
+ windows.push({ label: "Week", usedPercent, resetsIn });
749
+ }
750
+
751
+ if (data.monthlyUsage) {
752
+ const usedPercent = clampPercent(data.monthlyUsage.usagePercent ?? 0);
753
+ const resetsIn = Number.isFinite(data.monthlyUsage.resetInSec)
754
+ ? formatResetTime(new Date(Date.now() + data.monthlyUsage.resetInSec * 1000))
755
+ : undefined;
756
+ windows.push({ label: "Month", usedPercent, resetsIn });
757
+ }
758
+
759
+ return { provider: providerLabel, windows, fetchedAt: Date.now() };
760
+ } catch (e) {
761
+ return { provider: providerLabel, windows: [], error: String(e), fetchedAt: Date.now() };
762
+ }
763
+ }
764
+
697
765
  // ============ Provider Detection ============
698
766
 
699
767
  // Map pi provider names to our internal usage provider keys
@@ -705,6 +773,7 @@ const PROVIDER_MAP: Record<string, string> = {
705
773
  minimax: "minimax", // MiniMax Token Plan / Coding Plan
706
774
  "minimax-cn": "minimax-cn", // MiniMax China plan
707
775
  "kimi-coding": "kimi-coding", // Kimi plan
776
+ "opencode-go": "opencode-go", // OpenCode Go plan
708
777
  };
709
778
 
710
779
  function detectProvider(modelProvider: string): string | null {
@@ -727,6 +796,8 @@ async function fetchUsageForProvider(provider: string): Promise<UsageSnapshot> {
727
796
  return fetchMinimaxUsage("minimax-cn");
728
797
  case "kimi-coding":
729
798
  return fetchKimiUsage();
799
+ case "opencode-go":
800
+ return fetchOpencodeGoUsage();
730
801
  default:
731
802
  return { provider: "Unknown", windows: [], error: "unknown-provider", fetchedAt: Date.now() };
732
803
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogulcancelik/pi-minimal-footer",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Minimal footer for pi with context gauge and subscription usage bars",
5
5
  "type": "module",
6
6
  "keywords": [