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

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.
@@ -4,7 +4,10 @@ import {
4
4
  } from "@mariozechner/pi-ai/oauth";
5
5
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
6
6
  import { normalizeUnknownError } from "pi-provider-utils/streams";
7
- import { loadImportedOpenAICodexAuth } from "./auth";
7
+ import {
8
+ loadImportedOpenAICodexAuth,
9
+ writeActiveTokenToAuthJson,
10
+ } from "./auth";
8
11
  import { isAccountAvailable, pickBestAccount } from "./selection";
9
12
  import {
10
13
  type Account,
@@ -44,6 +47,19 @@ export class AccountManager {
44
47
  }
45
48
  }
46
49
 
50
+ /**
51
+ * Write the active account's tokens to auth.json so pi's background features
52
+ * (rename, compaction) can resolve a valid API key via AuthStorage.
53
+ */
54
+ private syncActiveTokenToAuthJson(account: Account): void {
55
+ writeActiveTokenToAuthJson({
56
+ access: account.accessToken,
57
+ refresh: account.refreshToken,
58
+ expires: account.expiresAt,
59
+ accountId: account.accountId,
60
+ }).catch(() => {});
61
+ }
62
+
47
63
  onStateChange(handler: StateChangeHandler): () => void {
48
64
  this.stateChangeHandlers.add(handler);
49
65
  return () => {
@@ -80,6 +96,7 @@ export class AccountManager {
80
96
  existing.expiresAt = creds.expires;
81
97
  existing.importSource = options?.importSource;
82
98
  existing.importFingerprint = options?.importFingerprint;
99
+ existing.needsReauth = undefined;
83
100
  if (accountId) {
84
101
  existing.accountId = accountId;
85
102
  }
@@ -231,10 +248,22 @@ export class AccountManager {
231
248
  return this.usageCache.get(email);
232
249
  }
233
250
 
251
+ getAccountsNeedingReauth(): Account[] {
252
+ return this.data.accounts.filter((a) => a.needsReauth);
253
+ }
254
+
255
+ private markNeedsReauth(account: Account): void {
256
+ account.needsReauth = true;
257
+ this.save();
258
+ this.notifyStateChanged();
259
+ }
260
+
234
261
  async refreshUsageForAccount(
235
262
  account: Account,
236
263
  options?: { force?: boolean; signal?: AbortSignal },
237
264
  ): Promise<CodexUsageSnapshot | undefined> {
265
+ if (account.needsReauth) return this.usageCache.get(account.email);
266
+
238
267
  const cached = this.usageCache.get(account.email);
239
268
  const now = Date.now();
240
269
  if (
@@ -340,7 +369,17 @@ export class AccountManager {
340
369
  }
341
370
 
342
371
  async ensureValidToken(account: Account): Promise<string> {
372
+ if (account.needsReauth) {
373
+ const hint = account.importSource
374
+ ? "/login openai-codex"
375
+ : `/multicodex use ${account.email}`;
376
+ throw new Error(
377
+ `${account.email}: re-authentication required — run ${hint}`,
378
+ );
379
+ }
380
+
343
381
  if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
382
+ this.syncActiveTokenToAuthJson(account);
344
383
  return account.accessToken;
345
384
  }
346
385
 
@@ -368,7 +407,11 @@ export class AccountManager {
368
407
  }
369
408
  this.save();
370
409
  this.notifyStateChanged();
410
+ this.syncActiveTokenToAuthJson(account);
371
411
  return account.accessToken;
412
+ } catch (error) {
413
+ this.markNeedsReauth(account);
414
+ throw error;
372
415
  } finally {
373
416
  this.refreshPromises.delete(account.email);
374
417
  }
@@ -410,11 +453,17 @@ export class AccountManager {
410
453
 
411
454
  // Both our copy and auth.json are expired — let AuthStorage refresh with
412
455
  // 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");
456
+ let apiKey: string | undefined;
457
+ try {
458
+ const authStorage = AuthStorage.create();
459
+ apiKey = await authStorage.getApiKey("openai-codex");
460
+ } catch {
461
+ // AuthStorage refresh failed; mark for re-auth below.
462
+ }
415
463
  if (!apiKey) {
464
+ this.markNeedsReauth(account);
416
465
  throw new Error(
417
- "OpenAI Codex: token refresh failed — please re-authenticate with /login",
466
+ `${account.email}: token refresh failed — run /login openai-codex to re-authenticate`,
418
467
  );
419
468
  }
420
469
 
package/auth.ts CHANGED
@@ -85,6 +85,39 @@ export function parseImportedOpenAICodexAuth(
85
85
  };
86
86
  }
87
87
 
88
+ /**
89
+ * Write the active account's tokens to auth.json so pi's background features
90
+ * (rename, compaction, inline suggestions) can resolve a valid API key through
91
+ * the normal AuthStorage path.
92
+ */
93
+ export async function writeActiveTokenToAuthJson(creds: {
94
+ access: string;
95
+ refresh: string;
96
+ expires: number;
97
+ accountId?: string;
98
+ }): Promise<void> {
99
+ let auth: Record<string, unknown> = {};
100
+ try {
101
+ const raw = await fs.readFile(AUTH_FILE, "utf8");
102
+ const parsed = JSON.parse(raw) as unknown;
103
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
104
+ auth = parsed as Record<string, unknown>;
105
+ }
106
+ } catch {
107
+ // File missing or corrupt — start fresh.
108
+ }
109
+
110
+ auth["openai-codex"] = {
111
+ type: "oauth",
112
+ access: creds.access,
113
+ refresh: creds.refresh,
114
+ expires: creds.expires,
115
+ accountId: creds.accountId,
116
+ };
117
+
118
+ await fs.writeFile(AUTH_FILE, JSON.stringify(auth, null, 2));
119
+ }
120
+
88
121
  export async function loadImportedOpenAICodexAuth(): Promise<
89
122
  ImportedOpenAICodexAuth | undefined
90
123
  > {
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.11",
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 {