@bacnh85/pi-sub 0.1.4 → 0.1.6

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 (2) hide show
  1. package/index.ts +39 -31
  2. package/package.json +2 -1
package/index.ts CHANGED
@@ -80,8 +80,10 @@ interface SubscriptionProviderAdapter {
80
80
  interface State {
81
81
  model?: ModelLike;
82
82
  adapter?: SubscriptionProviderAdapter;
83
+ adapterId?: string;
83
84
  snapshot?: SubscriptionUsageSnapshot;
84
85
  lastRefreshAt: number;
86
+ refreshGeneration: number;
85
87
  inFlight?: Promise<SubscriptionUsageSnapshot>;
86
88
  refreshTimer?: NodeJS.Timeout;
87
89
  debounceTimer?: NodeJS.Timeout;
@@ -132,6 +134,14 @@ function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
132
134
  };
133
135
  }
134
136
 
137
+ function getCodexAccountId(entry: PiAuthEntry | undefined): string | undefined {
138
+ if (!entry) return undefined;
139
+ if (typeof entry.accountId === "string" && entry.accountId.length > 0) return entry.accountId;
140
+ const claims = decodeJwtPayload(entry.access);
141
+ const auth = claims?.["https://api.openai.com/auth"];
142
+ return typeof auth?.chatgpt_account_id === "string" ? auth.chatgpt_account_id : undefined;
143
+ }
144
+
135
145
  function planLabel(plan: string | undefined): string | undefined {
136
146
  if (!plan) return undefined;
137
147
  const normalized = plan.toLowerCase().replace(/[_-]+/g, " ");
@@ -239,11 +249,12 @@ function parseOpcodeUsageResponse(body: unknown): UsageApiSnapshot | undefined {
239
249
  };
240
250
  }
241
251
 
242
- async function readPiCodexAuth(): Promise<PiAuthEntry> {
252
+ async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
243
253
  const auth = await readJsonFile<PiAuthFile>(piAuthPath());
244
254
  const entry = auth[CODEX_PROVIDER];
245
- if (!entry?.access || !entry.accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
246
- return entry;
255
+ const accountId = getCodexAccountId(entry!);
256
+ if (!entry?.access || !accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
257
+ return { ...entry, accountId };
247
258
  }
248
259
 
249
260
  async function readOpenCodeGoAuth(): Promise<{ key: string }> {
@@ -260,7 +271,7 @@ async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): P
260
271
  headers: {
261
272
  Accept: "application/json",
262
273
  Authorization: `Bearer ${entry.access}`,
263
- "ChatGPT-Account-Id": entry.accountId!,
274
+ "ChatGPT-Account-Id": getCodexAccountId(entry) ?? entry.accountId,
264
275
  "User-Agent": "pi-sub/0.1.0",
265
276
  },
266
277
  signal: combinedSignal,
@@ -305,33 +316,13 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
305
316
 
306
317
  async function fetchOpenCodeGoUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
307
318
  try {
308
- const auth = await readOpenCodeGoAuth();
309
- const timeoutSignal = AbortSignal.timeout(7_000);
310
- const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
311
- const response = await fetch("https://opencode.ai/zen/go/v1/usage", {
312
- headers: {
313
- Accept: "application/json",
314
- Authorization: `Bearer ${auth.key}`,
315
- "User-Agent": "pi-sub/0.1.4",
316
- },
317
- signal: combinedSignal,
318
- });
319
- if (!response.ok) throw new Error(`usage request failed with HTTP ${response.status}`);
320
- const usage = parseOpcodeUsageResponse(await response.json());
321
- const account: SubscriptionAccountSnapshot = {
322
- isActive: true,
323
- accountLabel: "OpenCode Go",
324
- plan: planLabel(usage?.plan_type),
325
- fiveHour: usageWindowFromApi(usage?.primary),
326
- weekly: usageWindowFromApi(usage?.secondary),
327
- lastActivity: "Now",
328
- };
319
+ await readOpenCodeGoAuth();
329
320
  return {
330
321
  providerId: OPC_PROVIDER,
331
322
  providerDisplayName: "OpenCode Go",
332
- accounts: [account],
333
- activeAccount: account,
323
+ accounts: [],
334
324
  fetchedAt: Date.now(),
325
+ error: "OpenCode Go usage tracking not yet implemented",
335
326
  };
336
327
  } catch (error) {
337
328
  return {
@@ -427,9 +418,12 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
427
418
  const account = snapshot.activeAccount;
428
419
  const segments = windowSegments(account);
429
420
  const cost = snapshot.cost;
421
+ const hasWindows = segments.length > 0;
430
422
  if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
431
423
  if (segments.length === 0) {
432
424
  line = `Sub ${state.adapter.displayName}`;
425
+ } else if (!hasWindows) {
426
+ line = `${state.adapter.displayName} ${segments.join(" ")}`;
433
427
  } else {
434
428
  line = segments.join(" ");
435
429
  }
@@ -454,11 +448,21 @@ function stopTimer(state: State): void {
454
448
  }
455
449
 
456
450
  function updateActiveAdapter(ctx: ExtensionContext, state: State, model: ModelLike): void {
451
+ const nextAdapter = supportedAdapter(model);
452
+ const adapterChanged = state.adapterId !== nextAdapter?.id;
453
+
457
454
  state.model = model;
458
- state.adapter = supportedAdapter(model);
459
- if (!state.adapter) {
455
+ state.adapter = nextAdapter;
456
+ state.adapterId = nextAdapter?.id;
457
+
458
+ if (adapterChanged) {
460
459
  state.snapshot = undefined;
461
460
  state.lastRefreshAt = 0;
461
+ state.inFlight = undefined;
462
+ state.refreshGeneration++;
463
+ }
464
+
465
+ if (!state.adapter) {
462
466
  stopTimer(state);
463
467
  }
464
468
  renderSubscriptionLine(ctx, state);
@@ -473,15 +477,19 @@ async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean)
473
477
  }
474
478
  if (!force && state.snapshot && Date.now() - state.lastRefreshAt < REFRESH_TTL_MS) return state.snapshot;
475
479
  if (state.inFlight) return state.inFlight;
480
+ const generation = state.refreshGeneration;
476
481
  renderSubscriptionLine(ctx, state);
477
482
  state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
483
+ if (state.refreshGeneration !== generation) return snapshot;
478
484
  snapshot.cost = aggregateSessionCost(ctx);
479
485
  state.snapshot = snapshot;
480
486
  state.lastRefreshAt = Date.now();
481
487
  renderSubscriptionLine(ctx, state);
482
488
  return snapshot;
483
489
  }).finally(() => {
484
- state.inFlight = undefined;
490
+ if (state.refreshGeneration === generation) {
491
+ state.inFlight = undefined;
492
+ }
485
493
  });
486
494
  return state.inFlight;
487
495
  }
@@ -541,7 +549,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
541
549
  }
542
550
 
543
551
  export default function (pi: ExtensionAPI) {
544
- const state: State = { lastRefreshAt: 0 };
552
+ const state: State = { lastRefreshAt: 0, refreshGeneration: 0 };
545
553
 
546
554
  pi.registerMessageRenderer(MESSAGE_TYPE, (message, _options, theme) => {
547
555
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "./index.ts"
34
34
  ]
35
35
  },
36
+ "engines": { "node": ">=20.3.0" },
36
37
  "peerDependencies": {
37
38
  "@earendil-works/pi-coding-agent": "*",
38
39
  "@earendil-works/pi-tui": "*"