@victor-software-house/pi-multicodex 2.2.0 → 2.2.1

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.
@@ -2,12 +2,8 @@ import {
2
2
  type OAuthCredentials,
3
3
  refreshOpenAICodexToken,
4
4
  } from "@mariozechner/pi-ai/oauth";
5
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
6
5
  import { normalizeUnknownError } from "pi-provider-utils/streams";
7
- import {
8
- loadImportedOpenAICodexAuth,
9
- writeActiveTokenToAuthJson,
10
- } from "./auth";
6
+ import { loadImportedOpenAICodexAuth } from "./auth";
11
7
  import { isAccountAvailable, pickBestAccount } from "./selection";
12
8
  import {
13
9
  type Account,
@@ -48,23 +44,6 @@ export class AccountManager {
48
44
  }
49
45
  }
50
46
 
51
- /**
52
- * Write the active account's tokens to auth.json so pi's background features
53
- * (rename, compaction) can resolve a valid API key via AuthStorage.
54
- */
55
- private syncActiveTokenToAuthJson(account: Account): void {
56
- try {
57
- writeActiveTokenToAuthJson({
58
- access: account.accessToken,
59
- refresh: account.refreshToken,
60
- expires: account.expiresAt,
61
- accountId: account.accountId,
62
- });
63
- } catch {
64
- // Best-effort sync — do not block token resolution.
65
- }
66
- }
67
-
68
47
  onStateChange(handler: StateChangeHandler): () => void {
69
48
  this.stateChangeHandlers.add(handler);
70
49
  return () => {
@@ -366,6 +345,17 @@ export class AccountManager {
366
345
  return changed;
367
346
  }
368
347
 
348
+ detachImportedAuth(email: string): boolean {
349
+ const account = this.getAccount(email);
350
+ if (!account) return false;
351
+ const changed = this.clearImportedLink(account);
352
+ if (changed) {
353
+ this.save();
354
+ this.notifyStateChanged();
355
+ }
356
+ return changed;
357
+ }
358
+
369
359
  async syncImportedOpenAICodexAuth(): Promise<boolean> {
370
360
  const imported = await loadImportedOpenAICodexAuth();
371
361
  if (!imported) return false;
@@ -624,12 +614,11 @@ export class AccountManager {
624
614
  }
625
615
 
626
616
  if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
627
- this.syncActiveTokenToAuthJson(account);
628
617
  return account.accessToken;
629
618
  }
630
619
 
631
- // For the imported pi account, delegate to AuthStorage so we share pi's
632
- // file lock and never race with pi's own refresh path.
620
+ // Imported auth is read-only. MultiCodex never refreshes or writes
621
+ // auth.json and instead requires the user to repair pi auth explicitly.
633
622
  if (account.importSource === "pi-openai-codex") {
634
623
  return this.ensureValidTokenForImportedAccount(account);
635
624
  }
@@ -652,7 +641,6 @@ export class AccountManager {
652
641
  }
653
642
  this.save();
654
643
  this.notifyStateChanged();
655
- this.syncActiveTokenToAuthJson(account);
656
644
  return account.accessToken;
657
645
  } catch (error) {
658
646
  this.markNeedsReauth(account);
@@ -667,17 +655,14 @@ export class AccountManager {
667
655
  }
668
656
 
669
657
  /**
670
- * Refresh path for the imported pi account.
658
+ * Read-only path for imported pi auth.
671
659
  *
672
- * Uses AuthStorage so our refresh is serialised by the same file lock that
673
- * pi's own credential refresh uses. This prevents "refresh_token_reused"
674
- * errors caused by pi and multicodex both refreshing the same token
675
- * simultaneously.
660
+ * MultiCodex may read auth.json to mirror pi's currently active Codex auth,
661
+ * but it must never refresh or write auth.json itself.
676
662
  */
677
663
  private async ensureValidTokenForImportedAccount(
678
664
  account: Account,
679
665
  ): Promise<string> {
680
- // Check if pi already refreshed since our last sync.
681
666
  const latest = await loadImportedOpenAICodexAuth();
682
667
  if (latest && Date.now() < latest.credentials.expires - 5 * 60 * 1000) {
683
668
  account.accessToken = latest.credentials.access;
@@ -696,40 +681,9 @@ export class AccountManager {
696
681
  return account.accessToken;
697
682
  }
698
683
 
699
- // Both our copy and auth.json are expired — let AuthStorage refresh with
700
- // its file lock so only one caller (us or pi) fires the API call.
701
- let apiKey: string | undefined;
702
- try {
703
- const authStorage = AuthStorage.create();
704
- apiKey = await authStorage.getApiKey("openai-codex");
705
- } catch {
706
- // AuthStorage refresh failed; mark for re-auth below.
707
- }
708
- if (!apiKey) {
709
- this.markNeedsReauth(account);
710
- throw new Error(
711
- `${account.email}: token refresh failed — run /login openai-codex to re-authenticate`,
712
- );
713
- }
714
-
715
- // Read the refreshed tokens back from auth.json.
716
- const refreshed = await loadImportedOpenAICodexAuth();
717
- if (refreshed) {
718
- account.accessToken = refreshed.credentials.access;
719
- account.refreshToken = refreshed.credentials.refresh;
720
- account.expiresAt = refreshed.credentials.expires;
721
- account.importFingerprint = refreshed.fingerprint;
722
- const accountId =
723
- typeof refreshed.credentials.accountId === "string"
724
- ? refreshed.credentials.accountId
725
- : undefined;
726
- if (accountId) {
727
- account.accountId = accountId;
728
- }
729
- this.save();
730
- this.notifyStateChanged();
731
- }
732
-
733
- return apiKey;
684
+ this.markNeedsReauth(account);
685
+ throw new Error(
686
+ `${account.email}: imported pi auth is expired — run /login openai-codex to re-authenticate`,
687
+ );
734
688
  }
735
689
  }
package/auth.ts CHANGED
@@ -1,9 +1,4 @@
1
- import {
2
- existsSync,
3
- promises as fs,
4
- readFileSync,
5
- writeFileSync,
6
- } from "node:fs";
1
+ import { promises as fs } from "node:fs";
7
2
  import type { OAuthCredentials } from "@mariozechner/pi-ai/oauth";
8
3
  import { getAgentAuthPath } from "pi-provider-utils/agent-paths";
9
4
 
@@ -127,47 +122,6 @@ export function parseImportedOpenAICodexAuth(
127
122
  };
128
123
  }
129
124
 
130
- /**
131
- * Write the active account's tokens to auth.json so pi's background features
132
- * (rename, compaction, inline suggestions) can resolve a valid API key through
133
- * the normal AuthStorage path.
134
- */
135
- /**
136
- * Synchronously write the active account's tokens to auth.json so pi's
137
- * background features (rename, compaction) can resolve a valid API key.
138
- *
139
- * Uses synchronous I/O to avoid interleaved writes with pi's own code.
140
- */
141
- export function writeActiveTokenToAuthJson(creds: {
142
- access: string;
143
- refresh: string;
144
- expires: number;
145
- accountId?: string;
146
- }): void {
147
- let auth: Record<string, unknown> = {};
148
- try {
149
- if (existsSync(AUTH_FILE)) {
150
- const raw = readFileSync(AUTH_FILE, "utf8");
151
- const parsed = JSON.parse(raw) as unknown;
152
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
153
- auth = parsed as Record<string, unknown>;
154
- }
155
- }
156
- } catch {
157
- // File missing or corrupt — start fresh.
158
- }
159
-
160
- auth["openai-codex"] = {
161
- type: "oauth",
162
- access: creds.access,
163
- refresh: creds.refresh,
164
- expires: creds.expires,
165
- accountId: creds.accountId,
166
- };
167
-
168
- writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
169
- }
170
-
171
125
  export async function loadImportedOpenAICodexAuth(): Promise<
172
126
  ImportedOpenAICodexAuth | undefined
173
127
  > {
package/commands.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  import { getAgentSettingsPath } from "pi-provider-utils/agent-paths";
19
19
  import { normalizeUnknownError } from "pi-provider-utils/streams";
20
20
  import type { AccountManager } from "./account-manager";
21
- import { writeActiveTokenToAuthJson } from "./auth";
22
21
  import { openLoginInBrowser } from "./browser";
23
22
  import type { createUsageStatusController } from "./status";
24
23
  import { type Account, STORAGE_FILE } from "./storage";
@@ -246,16 +245,10 @@ async function loginAndActivateAccount(
246
245
  onPrompt: async ({ message }) => (await ctx.ui.input(message)) || "",
247
246
  });
248
247
 
248
+ const existing = accountManager.getAccount(identifier);
249
249
  const account = accountManager.addOrUpdateAccount(identifier, creds);
250
- if (account.importSource) {
251
- writeActiveTokenToAuthJson({
252
- access: creds.access,
253
- refresh: creds.refresh,
254
- expires: creds.expires,
255
- accountId:
256
- typeof creds.accountId === "string" ? creds.accountId : undefined,
257
- });
258
- await accountManager.syncImportedOpenAICodexAuth();
250
+ if (existing?.importSource) {
251
+ accountManager.detachImportedAuth(account.email);
259
252
  }
260
253
  accountManager.setManualAccount(account.email);
261
254
  ctx.ui.notify(`Now using ${account.email}`, "info");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-multicodex",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Codex account rotation extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/provider.ts CHANGED
@@ -53,8 +53,7 @@ function getActiveApiKey(accountManager: AccountManager): string {
53
53
  return account.accessToken;
54
54
  }
55
55
  }
56
- // Placeholder AuthStorage will override on every actual API call
57
- // as long as auth.json has valid tokens.
56
+ // Fallback placeholder until MultiCodex resolves a usable managed account.
58
57
  return "pending-login";
59
58
  }
60
59