@bacnh85/pi-sub 0.1.0 → 0.1.2

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 +13 -19
  2. package/index.ts +198 -116
  3. package/package.json +1 -4
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
- V1 supports OpenAI Codex models via `@loongphy/codex-auth` and displays a compact footer status only while the active Pi model provider is `openai-codex`.
5
+ V1 supports OpenAI Codex models by reading Pi's auth state from `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`) and displays a separate subscription line below the editor only while the active Pi model provider is `openai-codex`.
6
6
 
7
7
  ## Install
8
8
 
@@ -34,13 +34,13 @@ The footer status includes:
34
34
  - weekly usage percentage and reset time/date;
35
35
  - active Codex model id.
36
36
 
37
- Example compact footer text:
37
+ Example subscription line:
38
38
 
39
39
  ```text
40
- Sub Plus user@example.com 5H 85%→18:12 W 80%→2 Jul 08:00 · gpt-5-codex
40
+ Sub · Plus · user@example.com · 5H 85% (18:12) · W 80% (08:00 on 2 Jul) · gpt-5-codex
41
41
  ```
42
42
 
43
- When the current model provider is not supported, `pi-sub` clears its footer status and does not refresh subscription data.
43
+ When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
44
44
 
45
45
  ## Commands
46
46
 
@@ -50,12 +50,11 @@ When the current model provider is not supported, `pi-sub` clears its footer sta
50
50
  | `/sub status` | Same as `/sub`. |
51
51
  | `/sub refresh` | Force a usage refresh, then show details. |
52
52
 
53
- When Codex account data is available, `/sub` shows a table similar to `codex-auth list`:
53
+ When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
54
54
 
55
55
  ```text
56
56
  ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
57
57
  * hangdanchi@gmail.com Plus 85% (18:12) 80% (08:00 on 2 Jul) Now
58
- mail@bacnh.com Plus 75% (14:21) 20% (14:23 on 29 Jun) Now
59
58
  ```
60
59
 
61
60
  ## Refresh behavior
@@ -68,23 +67,18 @@ ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
68
67
  - periodically while a supported provider remains active;
69
68
  - when `/sub refresh` is run.
70
69
 
71
- Refreshes are cached briefly to avoid excessive `codex-auth` calls.
70
+ `pi-sub` reads the `openai-codex` OAuth entry from Pi's auth file and refreshes live usage directly against ChatGPT's usage endpoint. It does not execute the `codex-auth` CLI and does not assume a separate Codex CLI installation exists.
72
71
 
73
- ## Requirements and troubleshooting
74
-
75
- - `@loongphy/codex-auth` is installed as a runtime dependency of this package.
76
- - Codex account data must already be configured for `codex-auth list` to return usage.
77
- - Node.js 22+ is required by `@loongphy/codex-auth`.
78
- - `pi-sub` redacts auth/token-related errors in the footer and never prints Codex tokens.
72
+ Refreshes are cached briefly to avoid excessive usage endpoint calls.
79
73
 
80
- If usage is unavailable, run:
74
+ ## Requirements and troubleshooting
81
75
 
82
- ```bash
83
- npx @loongphy/codex-auth list
84
- ```
76
+ - Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`.
77
+ - The entry must include `access` and `accountId` fields.
78
+ - `pi-sub` redacts auth/token-related errors and never prints Codex tokens.
85
79
 
86
- outside Pi to verify the underlying Codex account state.
80
+ If usage is unavailable, verify that Pi can use the `openai-codex` provider and that the auth file exists.
87
81
 
88
82
  ## Design notes
89
83
 
90
- The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. V1 intentionally supports only OpenAI Codex.
84
+ The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. V1 intentionally supports only OpenAI Codex and vendors only the small amount of behavior needed for this extension instead of invoking `codex-auth` directly.
package/index.ts CHANGED
@@ -1,17 +1,41 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Box, Text } from "@earendil-works/pi-tui";
3
- import { spawn } from "node:child_process";
4
- import { createRequire } from "node:module";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
5
6
 
6
- const require = createRequire(import.meta.url);
7
7
  const STATUS_KEY = "pi-sub";
8
8
  const MESSAGE_TYPE = "pi-sub-status";
9
+ const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
9
10
  const REFRESH_INTERVAL_MS = 60_000;
10
11
  const REFRESH_TTL_MS = 30_000;
11
12
  const REFRESH_DEBOUNCE_MS = 2_000;
13
+ const PI_SUB_PROVIDER = "openai-codex";
12
14
 
13
15
  type ModelLike = { provider?: string; id?: string } | undefined;
14
16
 
17
+ type UsageApiWindow = {
18
+ used_percent?: number;
19
+ limit_window_seconds?: number;
20
+ reset_at?: number;
21
+ };
22
+
23
+ type UsageApiSnapshot = {
24
+ primary?: UsageApiWindow;
25
+ secondary?: UsageApiWindow;
26
+ plan_type?: string;
27
+ };
28
+
29
+ type PiAuthFile = Record<string, PiAuthEntry | undefined>;
30
+
31
+ type PiAuthEntry = {
32
+ type?: string;
33
+ access?: string;
34
+ refresh?: string;
35
+ expires?: number;
36
+ accountId?: string;
37
+ };
38
+
15
39
  interface UsageWindow {
16
40
  used?: number;
17
41
  limit?: number;
@@ -59,117 +83,177 @@ interface State {
59
83
 
60
84
  function isCodexModel(model: ModelLike): boolean {
61
85
  const provider = model?.provider?.toLowerCase() ?? "";
62
- return provider === "openai-codex" || provider.includes("openai-codex");
86
+ return provider === PI_SUB_PROVIDER || provider.includes(PI_SUB_PROVIDER);
87
+ }
88
+
89
+ function piAuthPath(): string {
90
+ const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
91
+ return path.join(configDir, "auth.json");
92
+ }
93
+
94
+ async function readJsonFile<T>(file: string): Promise<T> {
95
+ return JSON.parse(await fs.readFile(file, "utf8")) as T;
63
96
  }
64
97
 
65
- function parsePercent(value: string): number | undefined {
66
- const match = value.match(/(\d+(?:\.\d+)?)\s*%/);
67
- return match ? Number(match[1]) : undefined;
98
+ function decodeJwtPayload(token: string | undefined): Record<string, any> | undefined {
99
+ if (!token) return undefined;
100
+ const parts = token.split(".");
101
+ if (parts.length < 2) return undefined;
102
+ try {
103
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, any>;
104
+ } catch {
105
+ return undefined;
106
+ }
68
107
  }
69
108
 
70
- function parseUsageWindow(value: string | undefined): UsageWindow | undefined {
71
- if (!value) return undefined;
72
- const trimmed = value.trim();
73
- if (!trimmed || trimmed === "-" || trimmed === "—") return undefined;
74
- const percent = parsePercent(trimmed);
75
- const resetMatch = trimmed.match(/\(([^)]+)\)/);
109
+ function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
110
+ const claims = decodeJwtPayload(entry.access);
111
+ const profile = claims?.["https://api.openai.com/profile"];
112
+ const auth = claims?.["https://api.openai.com/auth"];
113
+ const email = typeof profile?.email === "string" ? profile.email : undefined;
114
+ const plan = typeof auth?.chatgpt_plan_type === "string" ? planLabel(auth.chatgpt_plan_type) : undefined;
115
+ const accountId = typeof entry.accountId === "string" ? entry.accountId : typeof auth?.chatgpt_account_id === "string" ? auth.chatgpt_account_id : undefined;
76
116
  return {
77
- percent,
78
- resetLabel: resetMatch?.[1]?.trim(),
79
- label: trimmed,
117
+ id: accountId,
118
+ isActive: true,
119
+ accountLabel: email ?? accountId ?? "openai-codex account",
120
+ plan,
121
+ lastActivity: "Now",
80
122
  };
81
123
  }
82
124
 
83
- function stripAnsi(value: string): string {
84
- return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
125
+ function planLabel(plan: string | undefined): string | undefined {
126
+ if (!plan) return undefined;
127
+ const normalized = plan.toLowerCase().replace(/[_-]+/g, " ");
128
+ const labels: Record<string, string> = {
129
+ free: "Free",
130
+ plus: "Plus",
131
+ prolite: "Pro Lite",
132
+ "pro lite": "Pro Lite",
133
+ pro: "Pro",
134
+ team: "Business",
135
+ business: "Business",
136
+ enterprise: "Enterprise",
137
+ edu: "Edu",
138
+ unknown: "Unknown",
139
+ };
140
+ return labels[normalized] ?? plan;
85
141
  }
86
142
 
87
- function parseCodexAuthList(output: string): SubscriptionUsageSnapshot {
88
- const accounts: SubscriptionAccountSnapshot[] = [];
89
- const lines = stripAnsi(output).split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean);
90
- const dataLines = lines.filter((line) => !/^ACCOUNT\s+PLAN\s+5H USAGE/i.test(line) && !/^-{5,}$/.test(line.trim()));
91
-
92
- for (const line of dataLines) {
93
- const match = line.match(/^(\*)?\s*(?:(\d+)\s+)?(\S+@\S+)\s+(\S+)\s+(\d+(?:\.\d+)?%\s*\([^)]*\)|[-—?])\s+(\d+(?:\.\d+)?%\s*\([^)]*\)|[-—?])\s*(.*)$/);
94
- if (!match) continue;
95
- accounts.push({
96
- isActive: Boolean(match[1]),
97
- id: match[2],
98
- accountLabel: match[3],
99
- plan: match[4],
100
- fiveHour: parseUsageWindow(match[5]),
101
- weekly: parseUsageWindow(match[6]),
102
- lastActivity: match[7]?.trim() || undefined,
103
- });
104
- }
143
+ function formatReset(timestampSeconds: number | undefined): string | undefined {
144
+ if (!timestampSeconds) return undefined;
145
+ const date = new Date(timestampSeconds * 1000);
146
+ const now = new Date();
147
+ const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
148
+ if (date.toDateString() === now.toDateString()) return time;
149
+ const day = date.toLocaleDateString(undefined, { day: "numeric" });
150
+ const month = date.toLocaleDateString(undefined, { month: "short" });
151
+ return `${time} on ${day} ${month}`;
152
+ }
153
+
154
+ function usageWindowFromApi(window: UsageApiWindow | undefined): UsageWindow | undefined {
155
+ if (!window || typeof window.used_percent !== "number") return undefined;
156
+ const percent = Math.round(window.used_percent);
157
+ const resetLabel = formatReset(window.reset_at);
158
+ return {
159
+ percent,
160
+ resetLabel,
161
+ resetsAt: window.reset_at,
162
+ label: resetLabel ? `${percent}% (${resetLabel})` : `${percent}%`,
163
+ };
164
+ }
105
165
 
166
+ function mergeUsageIntoAccount(account: SubscriptionAccountSnapshot, usage: UsageApiSnapshot | undefined): SubscriptionAccountSnapshot {
167
+ if (!usage) return account;
106
168
  return {
107
- providerId: "openai-codex",
108
- providerDisplayName: "Codex",
109
- accounts,
110
- activeAccount: accounts.find((account) => account.isActive) ?? accounts[0],
111
- fetchedAt: Date.now(),
169
+ ...account,
170
+ plan: planLabel(usage.plan_type) ?? account.plan,
171
+ fiveHour: usageWindowFromApi(usage.primary) ?? account.fiveHour,
172
+ weekly: usageWindowFromApi(usage.secondary) ?? account.weekly,
112
173
  };
113
174
  }
114
175
 
176
+ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
177
+ if (!body || typeof body !== "object") return undefined;
178
+ const root = body as any;
179
+ const rateLimit = root.rate_limit;
180
+ if (!rateLimit || typeof rateLimit !== "object") return undefined;
181
+ const parseWindow = (window: any): UsageApiWindow | undefined => {
182
+ if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
183
+ return {
184
+ used_percent: window.used_percent,
185
+ limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
186
+ reset_at: typeof window.reset_at === "number" ? window.reset_at : undefined,
187
+ };
188
+ };
189
+ return {
190
+ primary: parseWindow(rateLimit.primary_window),
191
+ secondary: parseWindow(rateLimit.secondary_window),
192
+ plan_type: typeof root.plan_type === "string" ? root.plan_type : undefined,
193
+ };
194
+ }
195
+
196
+ async function readPiCodexAuth(): Promise<PiAuthEntry> {
197
+ const auth = await readJsonFile<PiAuthFile>(piAuthPath());
198
+ const entry = auth[PI_SUB_PROVIDER];
199
+ if (!entry?.access || !entry.accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
200
+ return entry;
201
+ }
202
+
203
+ async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
204
+ const timeoutSignal = AbortSignal.timeout(7_000);
205
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
206
+ const response = await fetch(USAGE_ENDPOINT, {
207
+ headers: {
208
+ Accept: "application/json",
209
+ Authorization: `Bearer ${entry.access}`,
210
+ "ChatGPT-Account-Id": entry.accountId!,
211
+ "User-Agent": "pi-sub/0.1.0",
212
+ },
213
+ signal: combinedSignal,
214
+ });
215
+ if (!response.ok) throw new Error(`usage request failed with HTTP ${response.status}`);
216
+ return parseUsageResponse(await response.json());
217
+ }
218
+
115
219
  function redactedError(error: unknown): string {
116
220
  const message = error instanceof Error ? error.message : String(error || "Unknown error");
117
- if (/missing platform package|cannot find module|ENOENT/i.test(message)) return "codex-auth is not installed correctly";
118
- if (/timed out|timeout/i.test(message)) return "codex-auth timed out";
119
- if (/not logged in|auth|token|unauthorized|forbidden/i.test(message)) return "Codex auth unavailable";
221
+ if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
222
+ if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
223
+ if (/timed out|timeout|aborted/i.test(message)) return "Codex usage refresh timed out";
224
+ if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return "Codex auth unavailable";
120
225
  return "Codex usage unavailable";
121
226
  }
122
227
 
123
- function resolveCodexAuthBin(): string {
124
- return require.resolve("@loongphy/codex-auth/bin/codex-auth.js");
125
- }
126
-
127
- function runCodexAuthList(signal?: AbortSignal): Promise<string> {
128
- return new Promise((resolve, reject) => {
129
- const child = spawn(process.execPath, [resolveCodexAuthBin(), "list"], {
130
- stdio: ["ignore", "pipe", "pipe"],
131
- env: { ...process.env, NO_COLOR: "1" },
132
- });
133
- let stdout = "";
134
- let stderr = "";
135
- const timeout = setTimeout(() => {
136
- child.kill("SIGTERM");
137
- reject(new Error("codex-auth list timed out"));
138
- }, 20_000);
139
- const onAbort = () => {
140
- child.kill("SIGTERM");
141
- reject(signal?.reason ?? new Error("Operation aborted"));
228
+ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
229
+ try {
230
+ const entry = await readPiCodexAuth();
231
+ let activeAccount = accountFromPiAuth(entry);
232
+ const usage = await fetchUsageFromPiAuth(entry, signal);
233
+ activeAccount = mergeUsageIntoAccount(activeAccount, usage);
234
+ return {
235
+ providerId: PI_SUB_PROVIDER,
236
+ providerDisplayName: "Codex",
237
+ accounts: [activeAccount],
238
+ activeAccount,
239
+ fetchedAt: Date.now(),
142
240
  };
143
- signal?.addEventListener("abort", onAbort, { once: true });
144
- child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
145
- child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
146
- child.on("error", reject);
147
- child.on("close", (code) => {
148
- clearTimeout(timeout);
149
- signal?.removeEventListener("abort", onAbort);
150
- if (code === 0) resolve(stdout);
151
- else reject(new Error(stderr.trim() || `codex-auth list exited with code ${code}`));
152
- });
153
- });
241
+ } catch (error) {
242
+ return {
243
+ providerId: PI_SUB_PROVIDER,
244
+ providerDisplayName: "Codex",
245
+ accounts: [],
246
+ fetchedAt: Date.now(),
247
+ error: redactedError(error),
248
+ };
249
+ }
154
250
  }
155
251
 
156
252
  const codexAdapter: SubscriptionProviderAdapter = {
157
- id: "openai-codex",
253
+ id: PI_SUB_PROVIDER,
158
254
  displayName: "Codex",
159
255
  isModelSupported: isCodexModel,
160
- async fetchUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
161
- try {
162
- return parseCodexAuthList(await runCodexAuthList(signal));
163
- } catch (error) {
164
- return {
165
- providerId: "openai-codex",
166
- providerDisplayName: "Codex",
167
- accounts: [],
168
- fetchedAt: Date.now(),
169
- error: redactedError(error),
170
- };
171
- }
172
- },
256
+ fetchUsage: fetchCodexUsage,
173
257
  };
174
258
 
175
259
  const adapters = [codexAdapter];
@@ -178,48 +262,46 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
178
262
  return adapters.find((adapter) => adapter.isModelSupported(model));
179
263
  }
180
264
 
181
- function formatWindow(window: UsageWindow | undefined, compact = true): string {
265
+ function formatWindow(window: UsageWindow | undefined, _compact = true): string {
182
266
  if (!window) return "?";
183
- if (window.percent !== undefined && window.resetLabel) return compact ? `${window.percent}%→${compactResetLabel(window.resetLabel)}` : `${window.percent}% (${window.resetLabel})`;
267
+ if (window.percent !== undefined && window.resetLabel) return `${window.percent}% (${window.resetLabel})`;
184
268
  if (window.label) return window.label;
185
269
  if (window.percent !== undefined) return `${window.percent}%`;
186
270
  if (window.used !== undefined && window.limit !== undefined) return `${window.used}/${window.limit}`;
187
271
  return "?";
188
272
  }
189
273
 
190
- function compactResetLabel(label: string): string {
191
- return label.replace(/^0?(\d+:\d+) on (\d+ \w+)$/i, "$2 $1");
192
- }
193
-
194
274
  function maxPercent(account: SubscriptionAccountSnapshot | undefined): number {
195
275
  return Math.max(account?.fiveHour?.percent ?? 0, account?.weekly?.percent ?? 0);
196
276
  }
197
277
 
198
- function renderStatus(ctx: ExtensionContext, state: State): void {
278
+ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
199
279
  if (!state.adapter) {
200
- ctx.ui.setStatus(STATUS_KEY, undefined);
280
+ ctx.ui.setWidget(STATUS_KEY, undefined);
201
281
  return;
202
282
  }
203
283
  const theme = ctx.ui.theme;
204
284
  const snapshot = state.snapshot;
205
285
  const modelId = state.model?.id ?? "unknown-model";
286
+ let line: string;
287
+ let color: "dim" | "warning" | "error" = "dim";
206
288
  if (!snapshot) {
207
- ctx.ui.setStatus(STATUS_KEY, theme.fg("dim", `Sub ${state.adapter.displayName} loading · ${modelId}`));
208
- return;
209
- }
210
- if (snapshot.error) {
211
- ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", `Sub ${snapshot.error} · ${modelId}`));
212
- return;
289
+ line = `Sub ${state.adapter.displayName} loading · ${modelId}`;
290
+ } else if (snapshot.error) {
291
+ line = `Sub ${snapshot.error} · ${modelId}`;
292
+ color = "warning";
293
+ } else {
294
+ const account = snapshot.activeAccount;
295
+ const pieces = ["Sub"];
296
+ if (account?.plan) pieces.push(account.plan);
297
+ pieces.push(account?.accountLabel ?? "unknown account");
298
+ pieces.push(`5H ${formatWindow(account?.fiveHour)}`);
299
+ pieces.push(`W ${formatWindow(account?.weekly)}`);
300
+ pieces.push(modelId);
301
+ line = pieces.join(" · ");
302
+ color = maxPercent(account) >= 90 ? "error" : maxPercent(account) >= 80 ? "warning" : "dim";
213
303
  }
214
- const account = snapshot.activeAccount;
215
- const pieces = ["Sub"];
216
- if (account?.plan) pieces.push(account.plan);
217
- pieces.push(account?.accountLabel ?? "unknown account");
218
- pieces.push(`5H ${formatWindow(account?.fiveHour)}`);
219
- pieces.push(`W ${formatWindow(account?.weekly)}`);
220
- pieces.push("·", modelId);
221
- const color = maxPercent(account) >= 90 ? "error" : maxPercent(account) >= 80 ? "warning" : "dim";
222
- ctx.ui.setStatus(STATUS_KEY, theme.fg(color, pieces.join(" ")));
304
+ ctx.ui.setWidget(STATUS_KEY, [theme.fg(color, line)], { placement: "belowEditor" });
223
305
  }
224
306
 
225
307
  function startTimer(ctx: ExtensionContext, state: State): void {
@@ -244,23 +326,23 @@ function updateActiveAdapter(ctx: ExtensionContext, state: State, model: ModelLi
244
326
  state.lastRefreshAt = 0;
245
327
  stopTimer(state);
246
328
  }
247
- renderStatus(ctx, state);
329
+ renderSubscriptionLine(ctx, state);
248
330
  if (state.adapter) startTimer(ctx, state);
249
331
  }
250
332
 
251
333
  async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean): Promise<SubscriptionUsageSnapshot | undefined> {
252
334
  const adapter = state.adapter;
253
335
  if (!adapter) {
254
- renderStatus(ctx, state);
336
+ renderSubscriptionLine(ctx, state);
255
337
  return undefined;
256
338
  }
257
339
  if (!force && state.snapshot && Date.now() - state.lastRefreshAt < REFRESH_TTL_MS) return state.snapshot;
258
340
  if (state.inFlight) return state.inFlight;
259
- renderStatus(ctx, state);
341
+ renderSubscriptionLine(ctx, state);
260
342
  state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
261
343
  state.snapshot = snapshot;
262
344
  state.lastRefreshAt = Date.now();
263
- renderStatus(ctx, state);
345
+ renderSubscriptionLine(ctx, state);
264
346
  return snapshot;
265
347
  }).finally(() => {
266
348
  state.inFlight = undefined;
@@ -331,7 +413,7 @@ export default function (pi: ExtensionAPI) {
331
413
 
332
414
  pi.on("session_shutdown", async (_event, ctx) => {
333
415
  stopTimer(state);
334
- ctx.ui.setStatus(STATUS_KEY, undefined);
416
+ ctx.ui.setWidget(STATUS_KEY, undefined);
335
417
  });
336
418
 
337
419
  pi.registerCommand("sub", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,9 +33,6 @@
33
33
  "./index.ts"
34
34
  ]
35
35
  },
36
- "dependencies": {
37
- "@loongphy/codex-auth": "^0.2.10"
38
- },
39
36
  "peerDependencies": {
40
37
  "@earendil-works/pi-coding-agent": "*",
41
38
  "@earendil-works/pi-tui": "*"