@bacnh85/pi-sub 0.1.38 → 0.1.40

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.40 (2026-09-14)
4
+
5
+ ### Tests
6
+
7
+ - Added unit test coverage for the three untested parsers: the OmniRoute
8
+ om-usage free-text report (`parseOmniUsageText` — four windows, section
9
+ switching, out-of-range percentages, disabled/no-cache text), the Command
10
+ Code `/alpha/billing/credits` window mapper (used/cap → remaining%, epoch-ms
11
+ resetAt, over-cap clamp, bad-window bail), and the `.env.local` parser
12
+ (extracted as exported `parseEnvText`; `export ` prefix, quoted values,
13
+ comments, CRLF, `#`-in-value). No behavior changes.
14
+ One edge case differs from the inline loop it replaces: duplicate keys in a
15
+ single .env file now resolve last-wins (dotenv convention) instead of
16
+ first-wins.
17
+
18
+ ## 0.1.39 (2026-09-12)
19
+
20
+ ### Fixed
21
+
22
+ - **Guarded footer render against an uninitialized theme proxy** (pi-budget
23
+ parity): `renderSubscriptionLine` dereferenced `ctx.ui.theme.fg` unguarded —
24
+ if the theme isn't ready yet the throw escapes as a rejected promise and can
25
+ exit pi (the same unhandledRejection class 0.1.37/0.1.38 fixed elsewhere).
26
+ The footer is now best-effort: skipped when the theme isn't available.
27
+ - **Finite-cost guard on `message_end` accumulation** (pi-budget parity): a
28
+ string or NaN `cost.total` previously hit `+=` directly — a string cost
29
+ concatenated onto the accumulator and garbled every subsequent footer.
30
+ Costs are now coerced with `Number()` and only finite positive values
31
+ accumulate.
32
+ - README intro: Router (pi-router) listed among supported providers.
33
+
3
34
  ## 0.1.38 (2026-09-07)
4
35
 
5
36
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Pi extension that shows subscription usage for the currently selected supported model provider.
4
4
 
5
- Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint, OpenCode Go (`opencode-go`) with session cost tracking, and Z.ai GLM Coding Plan — both the international (`zai`) and China (`zai-coding-cn`, `open.bigmodel.cn`) endpoints — with quota monitoring. Also tracks Command Code (`commandcode`) 5-hour/weekly windows and monthly credit balance. Displays a subscription footer status after Pi's built-in status/token usage line.
5
+ Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint, OpenCode Go (`opencode-go`) with session cost tracking, and Z.ai GLM Coding Plan — both the international (`zai`) and China (`zai-coding-cn`, `open.bigmodel.cn`) endpoints — with quota monitoring. Also tracks Router (pi-router, `router` provider) with response-speed tracking and optional OmniRoute quota windows, and Command Code (`commandcode`) 5-hour/weekly windows and monthly credit balance. Displays a subscription footer status after Pi's built-in status/token usage line.
6
6
 
7
7
  ## Install
8
8
 
@@ -4,6 +4,22 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
+ /** Parse .env-style text into KEY→VALUE entries: `export ` prefix allowed,
8
+ * single/double quotes stripped, comment/blank/non-assignment lines ignored.
9
+ * No inline-comment stripping (a `#` in the value stays part of the value).
10
+ * Exported for tests. */
11
+ export function parseEnvText(text: string): Record<string, string> {
12
+ const out: Record<string, string> = {};
13
+ for (const line of text.split(/\r?\n/)) {
14
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
15
+ if (!m) continue;
16
+ let v = m[2].trim();
17
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
18
+ out[m[1]] = v;
19
+ }
20
+ return out;
21
+ }
22
+
7
23
  /** Pi config dirs + .env.local/.env discovery (pi-munin convention, stdlib parse). */
8
24
  function loadEnvFiles(): void {
9
25
  const dirs = process.env.PI_CODING_AGENT_DIR
@@ -14,12 +30,8 @@ function loadEnvFiles(): void {
14
30
  for (const file of candidates) {
15
31
  try {
16
32
  const text = fs.readFileSync(file, "utf8");
17
- for (const line of text.split(/\r?\n/)) {
18
- const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
19
- if (!m) continue;
20
- let v = m[2].trim();
21
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
22
- if (process.env[m[1]] === undefined) process.env[m[1]] = v;
33
+ for (const [key, value] of Object.entries(parseEnvText(text))) {
34
+ if (process.env[key] === undefined) process.env[key] = value;
23
35
  }
24
36
  } catch { /* optional file */ }
25
37
  }
@@ -437,7 +449,8 @@ export function parseOmniUsageText(text: string): {
437
449
  return out;
438
450
  }
439
451
 
440
- // ponytail: runnable self-check (pi-sub has no test runner pack gate only)
452
+ // ponytail: runnable self-check (pack gate; extensions/test covers the same
453
+ // parser paths plus the adapters the self-check doesn't)
441
454
  if (process.env.PI_SUB_SELF_CHECK === "1") {
442
455
  const sample = [
443
456
  "Personal quota", "Daily", "80% left", "⏱ reset in 15h 0m", "",
@@ -850,7 +863,7 @@ interface CommandCodeCreditsApiResponse {
850
863
 
851
864
  /** Map a Command Code USD window (used/cap in dollars, resetAt in ms) into
852
865
  * the shared UsageWindow shape (remaining%, reset labels). */
853
- function commandCodeWindowToUsageWindow(window: CommandCodeWindowApi | undefined): UsageWindow | undefined {
866
+ export function commandCodeWindowToUsageWindow(window: CommandCodeWindowApi | undefined): UsageWindow | undefined {
854
867
  if (!window || typeof window.used !== "number" || typeof window.cap !== "number" || window.cap <= 0) return undefined;
855
868
  const usedPct = Math.round((window.used / window.cap) * 100);
856
869
  const percent = Math.min(100, usedPct);
@@ -1103,6 +1116,9 @@ export function renderSubscriptionLine(state: State): void {
1103
1116
  const ctx = state.ctx;
1104
1117
  if (!ctx) return;
1105
1118
  const theme = ctx.ui.theme;
1119
+ // pi-budget parity: the theme proxy may not be initialized yet — dereferencing
1120
+ // theme.fg throws (unhandledRejection → pi exits). Best-effort footer: skip.
1121
+ if (!theme?.fg) return;
1106
1122
  if (!state.adapter) {
1107
1123
  // Unsupported provider (e.g. Ollama): still show the last response speed.
1108
1124
  ctx.ui.setStatus(STATUS_KEY, state.lastTokPerSec !== undefined ? theme.fg("dim", `${state.lastTokPerSec} tok/s`) : undefined);
@@ -1340,7 +1356,11 @@ export default function (pi: ExtensionAPI) {
1340
1356
 
1341
1357
  pi.on("message_end", async (event, _ctx) => {
1342
1358
  if (event.message.role === "assistant") {
1343
- state.cumulativeCost += (event.message.usage as any)?.cost?.total ?? 0;
1359
+ // pi-budget parity: coerce + finite guard so a string/NaN cost.total can
1360
+ // never poison the accumulator (string concat garbles every subsequent
1361
+ // footer).
1362
+ const cost = Number((event.message.usage as any)?.cost?.total);
1363
+ if (Number.isFinite(cost) && cost > 0) state.cumulativeCost += cost;
1344
1364
  if (state.responseStartTime) {
1345
1365
  // usage.output already includes reasoning tokens (Pi SDK contract) —
1346
1366
  // this is total tok/s in both thinking and normal mode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",