@victor-software-house/pi-multicodex 2.0.9 → 2.0.10

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.
@@ -80,6 +80,7 @@ export class AccountManager {
80
80
  existing.expiresAt = creds.expires;
81
81
  existing.importSource = options?.importSource;
82
82
  existing.importFingerprint = options?.importFingerprint;
83
+ existing.needsReauth = undefined;
83
84
  if (accountId) {
84
85
  existing.accountId = accountId;
85
86
  }
@@ -231,10 +232,22 @@ export class AccountManager {
231
232
  return this.usageCache.get(email);
232
233
  }
233
234
 
235
+ getAccountsNeedingReauth(): Account[] {
236
+ return this.data.accounts.filter((a) => a.needsReauth);
237
+ }
238
+
239
+ private markNeedsReauth(account: Account): void {
240
+ account.needsReauth = true;
241
+ this.save();
242
+ this.notifyStateChanged();
243
+ }
244
+
234
245
  async refreshUsageForAccount(
235
246
  account: Account,
236
247
  options?: { force?: boolean; signal?: AbortSignal },
237
248
  ): Promise<CodexUsageSnapshot | undefined> {
249
+ if (account.needsReauth) return this.usageCache.get(account.email);
250
+
238
251
  const cached = this.usageCache.get(account.email);
239
252
  const now = Date.now();
240
253
  if (
@@ -340,6 +353,15 @@ export class AccountManager {
340
353
  }
341
354
 
342
355
  async ensureValidToken(account: Account): Promise<string> {
356
+ if (account.needsReauth) {
357
+ const hint = account.importSource
358
+ ? "/login openai-codex"
359
+ : `/multicodex use ${account.email}`;
360
+ throw new Error(
361
+ `${account.email}: re-authentication required — run ${hint}`,
362
+ );
363
+ }
364
+
343
365
  if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
344
366
  return account.accessToken;
345
367
  }
@@ -369,6 +391,9 @@ export class AccountManager {
369
391
  this.save();
370
392
  this.notifyStateChanged();
371
393
  return account.accessToken;
394
+ } catch (error) {
395
+ this.markNeedsReauth(account);
396
+ throw error;
372
397
  } finally {
373
398
  this.refreshPromises.delete(account.email);
374
399
  }
@@ -410,11 +435,17 @@ export class AccountManager {
410
435
 
411
436
  // Both our copy and auth.json are expired — let AuthStorage refresh with
412
437
  // its file lock so only one caller (us or pi) fires the API call.
413
- const authStorage = AuthStorage.create();
414
- const apiKey = await authStorage.getApiKey("openai-codex");
438
+ let apiKey: string | undefined;
439
+ try {
440
+ const authStorage = AuthStorage.create();
441
+ apiKey = await authStorage.getApiKey("openai-codex");
442
+ } catch {
443
+ // AuthStorage refresh failed; mark for re-auth below.
444
+ }
415
445
  if (!apiKey) {
446
+ this.markNeedsReauth(account);
416
447
  throw new Error(
417
- "OpenAI Codex: token refresh failed — please re-authenticate with /login",
448
+ `${account.email}: token refresh failed — run /login openai-codex to re-authenticate`,
418
449
  );
419
450
  }
420
451
 
package/commands.ts CHANGED
@@ -80,11 +80,16 @@ function parseResetTarget(value: string): ResetTarget | undefined {
80
80
  return undefined;
81
81
  }
82
82
 
83
- function getAccountLabel(email: string, quotaExhaustedUntil?: number): string {
84
- if (!quotaExhaustedUntil || quotaExhaustedUntil <= Date.now()) {
85
- return email;
86
- }
87
- return `${email} (Quota)`;
83
+ function getAccountLabel(
84
+ email: string,
85
+ options?: { quotaExhaustedUntil?: number; needsReauth?: boolean },
86
+ ): string {
87
+ const tags: string[] = [];
88
+ if (options?.needsReauth) tags.push("Reauth");
89
+ if (options?.quotaExhaustedUntil && options.quotaExhaustedUntil > Date.now())
90
+ tags.push("Quota");
91
+ if (tags.length === 0) return email;
92
+ return `${email} (${tags.join(", ")})`;
88
93
  }
89
94
 
90
95
  function formatAccountStatusLine(
@@ -100,9 +105,11 @@ function formatAccountStatusLine(
100
105
  account.quotaExhaustedUntil && account.quotaExhaustedUntil > Date.now();
101
106
  const untouched = isUsageUntouched(usage) ? "untouched" : null;
102
107
  const imported = account.importSource ? "imported" : null;
108
+ const reauth = account.needsReauth ? "needs reauth" : null;
103
109
  const tags = [
104
110
  active?.email === account.email ? "active" : null,
105
111
  manual?.email === account.email ? "manual" : null,
112
+ reauth,
106
113
  quotaHit ? "quota" : null,
107
114
  untouched,
108
115
  imported,
@@ -234,7 +241,10 @@ async function openAccountSelectionPanel(
234
241
  const accounts = accountManager.getAccounts();
235
242
  const items = accounts.map((account) => ({
236
243
  value: account.email,
237
- label: getAccountLabel(account.email, account.quotaExhaustedUntil),
244
+ label: getAccountLabel(account.email, {
245
+ quotaExhaustedUntil: account.quotaExhaustedUntil,
246
+ needsReauth: account.needsReauth,
247
+ }),
238
248
  }));
239
249
 
240
250
  return ctx.ui.custom<AccountPanelResult>((_tui, theme, _kb, done) => {
package/extension.ts CHANGED
@@ -28,7 +28,7 @@ export default function multicodexExtension(pi: ExtensionAPI) {
28
28
 
29
29
  pi.on("session_start", (_event: unknown, ctx: ExtensionContext) => {
30
30
  lastContext = ctx;
31
- handleSessionStart(accountManager);
31
+ handleSessionStart(accountManager, (msg) => ctx.ui.notify(msg, "warning"));
32
32
  statusController.startAutoRefresh();
33
33
  void (async () => {
34
34
  await statusController.loadPreferences(ctx);
@@ -41,7 +41,9 @@ export default function multicodexExtension(pi: ExtensionAPI) {
41
41
  (event: { reason?: string }, ctx: ExtensionContext) => {
42
42
  lastContext = ctx;
43
43
  if (event.reason === "new") {
44
- handleNewSessionSwitch(accountManager);
44
+ handleNewSessionSwitch(accountManager, (msg) =>
45
+ ctx.ui.notify(msg, "warning"),
46
+ );
45
47
  }
46
48
  void statusController.refreshFor(ctx);
47
49
  },
package/hooks.ts CHANGED
@@ -1,10 +1,27 @@
1
1
  import type { AccountManager } from "./account-manager";
2
2
 
3
+ type WarningHandler = (message: string) => void;
4
+
3
5
  async function refreshAndActivateBestAccount(
4
6
  accountManager: AccountManager,
7
+ warningHandler?: WarningHandler,
5
8
  ): Promise<void> {
6
9
  await accountManager.syncImportedOpenAICodexAuth();
7
10
  await accountManager.refreshUsageForAllAccounts({ force: true });
11
+
12
+ const needsReauth = accountManager.getAccountsNeedingReauth();
13
+ if (needsReauth.length > 0) {
14
+ const hints = needsReauth.map((a) => {
15
+ const cmd = a.importSource
16
+ ? "/login openai-codex"
17
+ : `/multicodex use ${a.email}`;
18
+ return `${a.email} (${cmd})`;
19
+ });
20
+ warningHandler?.(
21
+ `Multicodex: ${needsReauth.length} account(s) need re-authentication: ${hints.join(", ")}`,
22
+ );
23
+ }
24
+
8
25
  const manual = accountManager.getAvailableManualAccount();
9
26
  if (manual) return;
10
27
  if (accountManager.hasManualAccount()) {
@@ -13,11 +30,17 @@ async function refreshAndActivateBestAccount(
13
30
  await accountManager.activateBestAccount();
14
31
  }
15
32
 
16
- export function handleSessionStart(accountManager: AccountManager): void {
33
+ export function handleSessionStart(
34
+ accountManager: AccountManager,
35
+ warningHandler?: WarningHandler,
36
+ ): void {
17
37
  if (accountManager.getAccounts().length === 0) return;
18
- void refreshAndActivateBestAccount(accountManager);
38
+ void refreshAndActivateBestAccount(accountManager, warningHandler);
19
39
  }
20
40
 
21
- export function handleNewSessionSwitch(accountManager: AccountManager): void {
22
- void refreshAndActivateBestAccount(accountManager);
41
+ export function handleNewSessionSwitch(
42
+ accountManager: AccountManager,
43
+ warningHandler?: WarningHandler,
44
+ ): void {
45
+ void refreshAndActivateBestAccount(accountManager, warningHandler);
23
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-multicodex",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Codex account rotation extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/selection.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  } from "./usage";
7
7
 
8
8
  export function isAccountAvailable(account: Account, now: number): boolean {
9
+ if (account.needsReauth) return false;
9
10
  return !account.quotaExhaustedUntil || account.quotaExhaustedUntil <= now;
10
11
  }
11
12
 
package/storage.ts CHANGED
@@ -12,6 +12,7 @@ export interface Account {
12
12
  quotaExhaustedUntil?: number;
13
13
  importSource?: "pi-openai-codex";
14
14
  importFingerprint?: string;
15
+ needsReauth?: boolean;
15
16
  }
16
17
 
17
18
  export interface StorageData {