@bacnh85/pi-sub 0.1.7 → 0.1.8

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 +20 -1
  2. package/index.ts +125 -1
  3. package/package.json +1 -1
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, and OpenCode Go (`opencode-go`) with session cost tracking. 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 (`zai`) with GLM Coding Plan quota monitoring. Displays a subscription footer status after Pi's built-in status/token usage line.
6
6
 
7
7
  ## Install
8
8
 
@@ -50,6 +50,14 @@ OpenCode Go does not expose usage windows, so the footer shows only the accumula
50
50
  OpenCode Go $0.23
51
51
  ```
52
52
 
53
+ ### Z.ai
54
+
55
+ Z.ai (GLM Coding Plan) shows 5-hour rolling and weekly quota percentages with reset times:
56
+
57
+ ```text
58
+ R:55%(18:12) W:80%(08:00 on 2 Jul)
59
+ ```
60
+
53
61
  When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
54
62
 
55
63
  ## Commands
@@ -75,6 +83,16 @@ OpenCode Go does not expose usage windows.
75
83
  Session cost: $0.23
76
84
  ```
77
85
 
86
+ For Z.ai, `/sub` shows the rolling and weekly quota windows:
87
+
88
+ ```text
89
+ Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
90
+
91
+ ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
92
+ ------------------------------------------------------------------------
93
+ * Z.ai subscription ? 55%/18:12 80%/08:00 on 2 Jul Now
94
+ ```
95
+
78
96
  ## Refresh behavior
79
97
 
80
98
  `pi-sub` refreshes usage data:
@@ -93,6 +111,7 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
93
111
 
94
112
  - **OpenAI Codex**: Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`. The entry must include `access` and `accountId` fields.
95
113
  - **OpenCode Go**: Pi auth must contain an `opencode-go` API key entry (via `/login` or env var). The entry must have a `key` field or an `accountId` field.
114
+ - **Z.ai**: Pi auth must contain a `zai` entry in `auth.json` with a `key` field (the same API key used for Z.ai model access via `@czottmann/pi-zai-api`). The Z.ai provider must be registered (e.g., `pi install npm:@czottmann/pi-zai-api`).
96
115
  - `pi-sub` redacts auth/token-related errors and never prints credentials.
97
116
 
98
117
  ## Design notes
package/index.ts CHANGED
@@ -12,6 +12,8 @@ const REFRESH_TTL_MS = 30_000;
12
12
  const REFRESH_DEBOUNCE_MS = 2_000;
13
13
  const CODEX_PROVIDER = "openai-codex";
14
14
  const OPC_PROVIDER = "opencode-go";
15
+ const ZAI_PROVIDER = "zai";
16
+ const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
15
17
 
16
18
  type ModelLike = { provider?: string; id?: string } | undefined;
17
19
 
@@ -98,6 +100,10 @@ function isOpenCodeGoModel(model: ModelLike): boolean {
98
100
  return (model?.provider?.toLowerCase() ?? "") === OPC_PROVIDER;
99
101
  }
100
102
 
103
+ function isZaiModel(model: ModelLike): boolean {
104
+ return (model?.provider?.toLowerCase() ?? "") === ZAI_PROVIDER;
105
+ }
106
+
101
107
  function piAuthPath(): string {
102
108
  const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
103
109
  return path.join(configDir, "auth.json");
@@ -246,6 +252,13 @@ async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string
246
252
  return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined };
247
253
  }
248
254
 
255
+ async function readZaiAuth(): Promise<string> {
256
+ const auth = await readJsonFile<PiAuthFile>(piAuthPath());
257
+ const entry = auth[ZAI_PROVIDER];
258
+ if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
259
+ return entry.key;
260
+ }
261
+
249
262
  async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
250
263
  const timeoutSignal = AbortSignal.timeout(7_000);
251
264
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
@@ -267,6 +280,7 @@ function redactedError(error: unknown, provider = "Codex"): string {
267
280
  if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
268
281
  if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
269
282
  if (/missing opencode-go/i.test(message)) return "opencode-go auth not found";
283
+ if (/missing zai/i.test(message)) return "zai auth not found";
270
284
  if (/timed out|timeout|aborted/i.test(message)) return `${provider} usage refresh timed out`;
271
285
  if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return `${provider} auth unavailable`;
272
286
  return `${provider} usage unavailable`;
@@ -316,6 +330,109 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
316
330
  }
317
331
  }
318
332
 
333
+ // ---------------------------------------------------------------------------
334
+ // Z.ai adapter
335
+ // ---------------------------------------------------------------------------
336
+
337
+ interface ZaiLimitEntry {
338
+ type: string;
339
+ percentage: number;
340
+ nextResetTime?: number;
341
+ }
342
+
343
+ interface ZaiUsageApiResponse {
344
+ data?: {
345
+ limits?: ZaiLimitEntry[];
346
+ };
347
+ }
348
+
349
+ interface ZaiUsageApiError {
350
+ code: number;
351
+ msg: string;
352
+ success: boolean;
353
+ }
354
+
355
+ function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
356
+ if (typeof limit.percentage !== "number") return undefined;
357
+ const percent = Math.round(limit.percentage);
358
+ const remaining = Math.max(0, 100 - percent);
359
+ // Z.ai returns nextResetTime in epoch milliseconds; format helpers expect seconds.
360
+ const resetAtSec = limit.nextResetTime ? limit.nextResetTime / 1000 : undefined;
361
+ const resetLabel = formatReset(resetAtSec);
362
+ const remainingLabel = formatRemainingTime(resetAtSec);
363
+ return {
364
+ percent,
365
+ remaining,
366
+ remainingLabel,
367
+ resetLabel,
368
+ resetsAt: limit.nextResetTime,
369
+ label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
370
+ };
371
+ }
372
+
373
+ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
374
+ try {
375
+ const apiKey = await readZaiAuth();
376
+ const timeoutSignal = AbortSignal.timeout(7_000);
377
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
378
+
379
+ const response = await fetch(ZAI_USAGE_URL, {
380
+ headers: {
381
+ Accept: "application/json",
382
+ Authorization: `Bearer ${apiKey}`,
383
+ "User-Agent": "pi-sub/0.1.0",
384
+ },
385
+ signal: combinedSignal,
386
+ });
387
+
388
+ const body = await response.json();
389
+
390
+ // Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
391
+ const apiError = body as ZaiUsageApiError;
392
+ if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
393
+ throw new Error(`Z.ai API error: ${apiError.msg}`);
394
+ }
395
+
396
+ const parsed = body as ZaiUsageApiResponse;
397
+ const tokenLimits = (parsed.data?.limits ?? [])
398
+ .filter((l) => l.type === "TOKENS_LIMIT")
399
+ .sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
400
+
401
+ if (tokenLimits.length === 0) {
402
+ throw new Error("No TOKENS_LIMIT entries in Z.ai usage response");
403
+ }
404
+
405
+ // The limit with the nearest reset is the 5-hour rolling window;
406
+ // the next one (if present) is the weekly window.
407
+ const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
408
+ const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
409
+
410
+ const account: SubscriptionAccountSnapshot = {
411
+ isActive: true,
412
+ accountLabel: "Z.ai subscription",
413
+ fiveHour,
414
+ weekly,
415
+ lastActivity: "Now",
416
+ };
417
+
418
+ return {
419
+ providerId: ZAI_PROVIDER,
420
+ providerDisplayName: "Z.ai",
421
+ accounts: [account],
422
+ activeAccount: account,
423
+ fetchedAt: Date.now(),
424
+ };
425
+ } catch (error) {
426
+ return {
427
+ providerId: ZAI_PROVIDER,
428
+ providerDisplayName: "Z.ai",
429
+ accounts: [],
430
+ fetchedAt: Date.now(),
431
+ error: redactedError(error, "Z.ai"),
432
+ };
433
+ }
434
+ }
435
+
319
436
  const codexAdapter: SubscriptionProviderAdapter = {
320
437
  id: CODEX_PROVIDER,
321
438
  displayName: "Codex",
@@ -330,7 +447,14 @@ const openCodeGoAdapter: SubscriptionProviderAdapter = {
330
447
  fetchUsage: fetchOpenCodeGoUsage,
331
448
  };
332
449
 
333
- const adapters = [codexAdapter, openCodeGoAdapter];
450
+ const zaiAdapter: SubscriptionProviderAdapter = {
451
+ id: ZAI_PROVIDER,
452
+ displayName: "Z.ai",
453
+ isModelSupported: isZaiModel,
454
+ fetchUsage: fetchZaiUsage,
455
+ };
456
+
457
+ const adapters = [codexAdapter, openCodeGoAdapter, zaiAdapter];
334
458
 
335
459
  function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
336
460
  return adapters.find((adapter) => adapter.isModelSupported(model));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",