@victor-software-house/pi-multicodex 2.2.0 → 2.3.0
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.
- package/account-manager.ts +75 -320
- package/auth.ts +1 -47
- package/commands.ts +7 -24
- package/hooks.ts +2 -2
- package/index.ts +1 -4
- package/package.json +6 -3
- package/provider.ts +1 -2
- package/schemas/codex-accounts.schema.json +77 -0
- package/status.ts +1 -5
- package/storage.ts +155 -18
- package/stream-wrapper.ts +0 -1
package/account-manager.ts
CHANGED
|
@@ -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,
|
|
@@ -27,6 +23,7 @@ type StateChangeHandler = () => void;
|
|
|
27
23
|
|
|
28
24
|
export class AccountManager {
|
|
29
25
|
private data: StorageData;
|
|
26
|
+
private piAuthAccount?: Account;
|
|
30
27
|
private usageCache = new Map<string, CodexUsageSnapshot>();
|
|
31
28
|
private refreshPromises = new Map<string, Promise<string>>();
|
|
32
29
|
private warningHandler?: WarningHandler;
|
|
@@ -48,23 +45,6 @@ export class AccountManager {
|
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
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
48
|
onStateChange(handler: StateChangeHandler): () => void {
|
|
69
49
|
this.stateChangeHandlers.add(handler);
|
|
70
50
|
return () => {
|
|
@@ -73,13 +53,21 @@ export class AccountManager {
|
|
|
73
53
|
}
|
|
74
54
|
|
|
75
55
|
getAccounts(): Account[] {
|
|
56
|
+
if (this.piAuthAccount) {
|
|
57
|
+
return [...this.data.accounts, this.piAuthAccount];
|
|
58
|
+
}
|
|
76
59
|
return this.data.accounts;
|
|
77
60
|
}
|
|
78
61
|
|
|
79
62
|
getAccount(email: string): Account | undefined {
|
|
63
|
+
if (this.piAuthAccount?.email === email) return this.piAuthAccount;
|
|
80
64
|
return this.data.accounts.find((a) => a.email === email);
|
|
81
65
|
}
|
|
82
66
|
|
|
67
|
+
isPiAuthAccount(account: Account): boolean {
|
|
68
|
+
return this.piAuthAccount !== undefined && account === this.piAuthAccount;
|
|
69
|
+
}
|
|
70
|
+
|
|
83
71
|
setWarningHandler(handler?: WarningHandler): void {
|
|
84
72
|
this.warningHandler = handler;
|
|
85
73
|
}
|
|
@@ -93,32 +81,14 @@ export class AccountManager {
|
|
|
93
81
|
return;
|
|
94
82
|
}
|
|
95
83
|
this.warnedAuthFailureEmails.add(account.email);
|
|
96
|
-
const hint = account
|
|
97
|
-
? "/
|
|
84
|
+
const hint = this.isPiAuthAccount(account)
|
|
85
|
+
? "/login openai-codex"
|
|
98
86
|
: `/multicodex reauth ${account.email}`;
|
|
99
87
|
this.warningHandler?.(
|
|
100
88
|
`Multicodex skipped ${account.email} during rotation: ${normalizeUnknownError(error)}. Account is flagged in /multicodex accounts. Run ${hint} to repair it.`,
|
|
101
89
|
);
|
|
102
90
|
}
|
|
103
91
|
|
|
104
|
-
private updateAccountEmail(account: Account, email: string): boolean {
|
|
105
|
-
if (account.email === email) return false;
|
|
106
|
-
const previousEmail = account.email;
|
|
107
|
-
account.email = email;
|
|
108
|
-
if (this.data.activeEmail === previousEmail) {
|
|
109
|
-
this.data.activeEmail = email;
|
|
110
|
-
}
|
|
111
|
-
if (this.manualEmail === previousEmail) {
|
|
112
|
-
this.manualEmail = email;
|
|
113
|
-
}
|
|
114
|
-
const cached = this.usageCache.get(previousEmail);
|
|
115
|
-
if (cached) {
|
|
116
|
-
this.usageCache.delete(previousEmail);
|
|
117
|
-
this.usageCache.set(email, cached);
|
|
118
|
-
}
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
92
|
private removeAccountRecord(account: Account): boolean {
|
|
123
93
|
const index = this.data.accounts.findIndex(
|
|
124
94
|
(candidate) => candidate.email === account.email,
|
|
@@ -138,25 +108,7 @@ export class AccountManager {
|
|
|
138
108
|
return true;
|
|
139
109
|
}
|
|
140
110
|
|
|
141
|
-
private
|
|
142
|
-
refreshToken: string,
|
|
143
|
-
excludeEmail?: string,
|
|
144
|
-
): Account | undefined {
|
|
145
|
-
return this.data.accounts.find(
|
|
146
|
-
(account) =>
|
|
147
|
-
account.refreshToken === refreshToken && account.email !== excludeEmail,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private applyCredentials(
|
|
152
|
-
account: Account,
|
|
153
|
-
creds: OAuthCredentials,
|
|
154
|
-
options?: {
|
|
155
|
-
importSource?: "pi-openai-codex";
|
|
156
|
-
importMode?: "linked" | "synthetic";
|
|
157
|
-
importFingerprint?: string;
|
|
158
|
-
},
|
|
159
|
-
): boolean {
|
|
111
|
+
private applyCredentials(account: Account, creds: OAuthCredentials): boolean {
|
|
160
112
|
const accountId =
|
|
161
113
|
typeof creds.accountId === "string" ? creds.accountId : undefined;
|
|
162
114
|
let changed = false;
|
|
@@ -176,24 +128,6 @@ export class AccountManager {
|
|
|
176
128
|
account.accountId = accountId;
|
|
177
129
|
changed = true;
|
|
178
130
|
}
|
|
179
|
-
if (
|
|
180
|
-
options?.importSource &&
|
|
181
|
-
account.importSource !== options.importSource
|
|
182
|
-
) {
|
|
183
|
-
account.importSource = options.importSource;
|
|
184
|
-
changed = true;
|
|
185
|
-
}
|
|
186
|
-
if (options?.importMode && account.importMode !== options.importMode) {
|
|
187
|
-
account.importMode = options.importMode;
|
|
188
|
-
changed = true;
|
|
189
|
-
}
|
|
190
|
-
if (
|
|
191
|
-
options?.importFingerprint &&
|
|
192
|
-
account.importFingerprint !== options.importFingerprint
|
|
193
|
-
) {
|
|
194
|
-
account.importFingerprint = options.importFingerprint;
|
|
195
|
-
changed = true;
|
|
196
|
-
}
|
|
197
131
|
if (account.needsReauth) {
|
|
198
132
|
account.needsReauth = undefined;
|
|
199
133
|
this.warnedAuthFailureEmails.delete(account.email);
|
|
@@ -202,66 +136,28 @@ export class AccountManager {
|
|
|
202
136
|
return changed;
|
|
203
137
|
}
|
|
204
138
|
|
|
205
|
-
addOrUpdateAccount(
|
|
206
|
-
email
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
preserveActive?: boolean;
|
|
213
|
-
},
|
|
214
|
-
): Account {
|
|
215
|
-
const existing = this.getAccount(email);
|
|
216
|
-
const duplicate = existing
|
|
217
|
-
? undefined
|
|
218
|
-
: this.findAccountByRefreshToken(creds.refresh);
|
|
219
|
-
let target = existing ?? duplicate;
|
|
220
|
-
let changed = false;
|
|
221
|
-
|
|
222
|
-
if (target) {
|
|
223
|
-
if (
|
|
224
|
-
duplicate?.importSource === "pi-openai-codex" &&
|
|
225
|
-
duplicate.email !== email &&
|
|
226
|
-
!this.getAccount(email)
|
|
227
|
-
) {
|
|
228
|
-
changed = this.updateAccountEmail(duplicate, email) || changed;
|
|
229
|
-
}
|
|
230
|
-
changed =
|
|
231
|
-
this.applyCredentials(target, creds, {
|
|
232
|
-
...options,
|
|
233
|
-
importMode:
|
|
234
|
-
options?.importMode ??
|
|
235
|
-
(duplicate?.importMode === "synthetic" ? "linked" : undefined),
|
|
236
|
-
}) || changed;
|
|
237
|
-
} else {
|
|
238
|
-
target = {
|
|
239
|
-
email,
|
|
240
|
-
accessToken: creds.access,
|
|
241
|
-
refreshToken: creds.refresh,
|
|
242
|
-
expiresAt: creds.expires,
|
|
243
|
-
accountId:
|
|
244
|
-
typeof creds.accountId === "string" ? creds.accountId : undefined,
|
|
245
|
-
importSource: options?.importSource,
|
|
246
|
-
importMode: options?.importMode,
|
|
247
|
-
importFingerprint: options?.importFingerprint,
|
|
248
|
-
};
|
|
249
|
-
this.data.accounts.push(target);
|
|
250
|
-
changed = true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!options?.preserveActive) {
|
|
254
|
-
if (this.data.activeEmail !== target.email) {
|
|
255
|
-
this.setActiveAccount(target.email);
|
|
256
|
-
return target;
|
|
139
|
+
addOrUpdateAccount(email: string, creds: OAuthCredentials): Account {
|
|
140
|
+
const existing = this.data.accounts.find((a) => a.email === email);
|
|
141
|
+
if (existing) {
|
|
142
|
+
const changed = this.applyCredentials(existing, creds);
|
|
143
|
+
if (changed) {
|
|
144
|
+
this.save();
|
|
145
|
+
this.notifyStateChanged();
|
|
257
146
|
}
|
|
147
|
+
return existing;
|
|
258
148
|
}
|
|
259
149
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
150
|
+
const account: Account = {
|
|
151
|
+
email,
|
|
152
|
+
accessToken: creds.access,
|
|
153
|
+
refreshToken: creds.refresh,
|
|
154
|
+
expiresAt: creds.expires,
|
|
155
|
+
accountId:
|
|
156
|
+
typeof creds.accountId === "string" ? creds.accountId : undefined,
|
|
157
|
+
};
|
|
158
|
+
this.data.accounts.push(account);
|
|
159
|
+
this.setActiveAccount(email);
|
|
160
|
+
return account;
|
|
265
161
|
}
|
|
266
162
|
|
|
267
163
|
getActiveAccount(): Account | undefined {
|
|
@@ -307,140 +203,40 @@ export class AccountManager {
|
|
|
307
203
|
this.notifyStateChanged();
|
|
308
204
|
}
|
|
309
205
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
private getLinkedImportedAccount(): Account | undefined {
|
|
317
|
-
return this.data.accounts.find(
|
|
318
|
-
(account) =>
|
|
319
|
-
account.importSource === "pi-openai-codex" &&
|
|
320
|
-
account.importMode !== "synthetic",
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private getSyntheticImportedAccount(): Account | undefined {
|
|
325
|
-
return this.data.accounts.find(
|
|
326
|
-
(account) =>
|
|
327
|
-
account.importSource === "pi-openai-codex" &&
|
|
328
|
-
account.importMode === "synthetic",
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private findManagedImportedTarget(imported: {
|
|
333
|
-
identifier: string;
|
|
334
|
-
credentials: OAuthCredentials;
|
|
335
|
-
}): Account | undefined {
|
|
336
|
-
const byRefreshToken = this.data.accounts.find(
|
|
337
|
-
(account) =>
|
|
338
|
-
account.importMode !== "synthetic" &&
|
|
339
|
-
account.refreshToken === imported.credentials.refresh,
|
|
340
|
-
);
|
|
341
|
-
if (byRefreshToken) {
|
|
342
|
-
return byRefreshToken;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return this.data.accounts.find(
|
|
346
|
-
(account) =>
|
|
347
|
-
account.importMode !== "synthetic" &&
|
|
348
|
-
account.email === imported.identifier,
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private clearImportedLink(account: Account): boolean {
|
|
353
|
-
let changed = false;
|
|
354
|
-
if (account.importSource) {
|
|
355
|
-
account.importSource = undefined;
|
|
356
|
-
changed = true;
|
|
357
|
-
}
|
|
358
|
-
if (account.importMode) {
|
|
359
|
-
account.importMode = undefined;
|
|
360
|
-
changed = true;
|
|
361
|
-
}
|
|
362
|
-
if (account.importFingerprint) {
|
|
363
|
-
account.importFingerprint = undefined;
|
|
364
|
-
changed = true;
|
|
365
|
-
}
|
|
366
|
-
return changed;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async syncImportedOpenAICodexAuth(): Promise<boolean> {
|
|
206
|
+
/**
|
|
207
|
+
* Read pi's openai-codex auth from auth.json and expose it as a
|
|
208
|
+
* memory-only ephemeral account. Never persists to codex-accounts.json.
|
|
209
|
+
* If the identity already exists as a managed account, skip it.
|
|
210
|
+
*/
|
|
211
|
+
async loadPiAuth(): Promise<void> {
|
|
370
212
|
const imported = await loadImportedOpenAICodexAuth();
|
|
371
|
-
if (!imported)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const currentImported = linkedImported ?? syntheticImported;
|
|
376
|
-
if (
|
|
377
|
-
currentImported?.importFingerprint === imported.fingerprint &&
|
|
378
|
-
(currentImported.importMode !== "synthetic" ||
|
|
379
|
-
currentImported.email === imported.identifier)
|
|
380
|
-
) {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const matchingAccount = this.findManagedImportedTarget(imported);
|
|
385
|
-
if (matchingAccount) {
|
|
386
|
-
let changed = this.applyCredentials(
|
|
387
|
-
matchingAccount,
|
|
388
|
-
imported.credentials,
|
|
389
|
-
{
|
|
390
|
-
importSource: "pi-openai-codex",
|
|
391
|
-
importMode: "linked",
|
|
392
|
-
importFingerprint: imported.fingerprint,
|
|
393
|
-
},
|
|
394
|
-
);
|
|
395
|
-
if (linkedImported && linkedImported !== matchingAccount) {
|
|
396
|
-
changed = this.clearImportedLink(linkedImported) || changed;
|
|
397
|
-
}
|
|
398
|
-
if (syntheticImported) {
|
|
399
|
-
changed = this.removeAccountRecord(syntheticImported) || changed;
|
|
400
|
-
}
|
|
401
|
-
if (changed) {
|
|
402
|
-
this.save();
|
|
403
|
-
this.notifyStateChanged();
|
|
404
|
-
}
|
|
405
|
-
return changed;
|
|
213
|
+
if (!imported) {
|
|
214
|
+
this.piAuthAccount = undefined;
|
|
215
|
+
this.notifyStateChanged();
|
|
216
|
+
return;
|
|
406
217
|
}
|
|
407
218
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.save();
|
|
412
|
-
this.notifyStateChanged();
|
|
413
|
-
}
|
|
414
|
-
}
|
|
219
|
+
const alreadyManaged = this.data.accounts.find(
|
|
220
|
+
(a) => a.email === imported.identifier,
|
|
221
|
+
);
|
|
415
222
|
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
syntheticImported,
|
|
421
|
-
imported.identifier,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
changed =
|
|
425
|
-
this.applyCredentials(syntheticImported, imported.credentials, {
|
|
426
|
-
importSource: "pi-openai-codex",
|
|
427
|
-
importMode: "synthetic",
|
|
428
|
-
importFingerprint: imported.fingerprint,
|
|
429
|
-
}) || changed;
|
|
430
|
-
if (changed) {
|
|
431
|
-
this.save();
|
|
432
|
-
this.notifyStateChanged();
|
|
433
|
-
}
|
|
434
|
-
return changed;
|
|
223
|
+
if (alreadyManaged) {
|
|
224
|
+
this.piAuthAccount = undefined;
|
|
225
|
+
this.notifyStateChanged();
|
|
226
|
+
return;
|
|
435
227
|
}
|
|
436
228
|
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
229
|
+
this.piAuthAccount = {
|
|
230
|
+
email: imported.identifier,
|
|
231
|
+
accessToken: imported.credentials.access,
|
|
232
|
+
refreshToken: imported.credentials.refresh,
|
|
233
|
+
expiresAt: imported.credentials.expires,
|
|
234
|
+
accountId:
|
|
235
|
+
typeof imported.credentials.accountId === "string"
|
|
236
|
+
? imported.credentials.accountId
|
|
237
|
+
: undefined,
|
|
238
|
+
};
|
|
239
|
+
this.notifyStateChanged();
|
|
444
240
|
}
|
|
445
241
|
|
|
446
242
|
getAvailableManualAccount(options?: {
|
|
@@ -499,7 +295,9 @@ export class AccountManager {
|
|
|
499
295
|
|
|
500
296
|
private markNeedsReauth(account: Account): void {
|
|
501
297
|
account.needsReauth = true;
|
|
502
|
-
this.
|
|
298
|
+
if (!this.isPiAuthAccount(account)) {
|
|
299
|
+
this.save();
|
|
300
|
+
}
|
|
503
301
|
this.notifyStateChanged();
|
|
504
302
|
}
|
|
505
303
|
|
|
@@ -571,7 +369,7 @@ export class AccountManager {
|
|
|
571
369
|
}): Promise<Account | undefined> {
|
|
572
370
|
const now = Date.now();
|
|
573
371
|
this.clearExpiredExhaustion(now);
|
|
574
|
-
const accounts = this.
|
|
372
|
+
const accounts = this.getAccounts();
|
|
575
373
|
await this.refreshUsageIfStale(accounts, options);
|
|
576
374
|
|
|
577
375
|
const selected = pickBestAccount(accounts, this.usageCache, {
|
|
@@ -615,7 +413,7 @@ export class AccountManager {
|
|
|
615
413
|
|
|
616
414
|
async ensureValidToken(account: Account): Promise<string> {
|
|
617
415
|
if (account.needsReauth) {
|
|
618
|
-
const hint = account
|
|
416
|
+
const hint = this.isPiAuthAccount(account)
|
|
619
417
|
? "/login openai-codex"
|
|
620
418
|
: `/multicodex use ${account.email}`;
|
|
621
419
|
throw new Error(
|
|
@@ -624,14 +422,11 @@ export class AccountManager {
|
|
|
624
422
|
}
|
|
625
423
|
|
|
626
424
|
if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
|
|
627
|
-
this.syncActiveTokenToAuthJson(account);
|
|
628
425
|
return account.accessToken;
|
|
629
426
|
}
|
|
630
427
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (account.importSource === "pi-openai-codex") {
|
|
634
|
-
return this.ensureValidTokenForImportedAccount(account);
|
|
428
|
+
if (this.isPiAuthAccount(account)) {
|
|
429
|
+
return this.ensureValidTokenForPiAuth(account);
|
|
635
430
|
}
|
|
636
431
|
|
|
637
432
|
const inflight = this.refreshPromises.get(account.email);
|
|
@@ -652,7 +447,6 @@ export class AccountManager {
|
|
|
652
447
|
}
|
|
653
448
|
this.save();
|
|
654
449
|
this.notifyStateChanged();
|
|
655
|
-
this.syncActiveTokenToAuthJson(account);
|
|
656
450
|
return account.accessToken;
|
|
657
451
|
} catch (error) {
|
|
658
452
|
this.markNeedsReauth(account);
|
|
@@ -667,23 +461,15 @@ export class AccountManager {
|
|
|
667
461
|
}
|
|
668
462
|
|
|
669
463
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
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.
|
|
464
|
+
* Read-only refresh for the ephemeral pi auth account.
|
|
465
|
+
* Re-reads auth.json for fresh tokens. Never writes anything.
|
|
676
466
|
*/
|
|
677
|
-
private async
|
|
678
|
-
account: Account,
|
|
679
|
-
): Promise<string> {
|
|
680
|
-
// Check if pi already refreshed since our last sync.
|
|
467
|
+
private async ensureValidTokenForPiAuth(account: Account): Promise<string> {
|
|
681
468
|
const latest = await loadImportedOpenAICodexAuth();
|
|
682
469
|
if (latest && Date.now() < latest.credentials.expires - 5 * 60 * 1000) {
|
|
683
470
|
account.accessToken = latest.credentials.access;
|
|
684
471
|
account.refreshToken = latest.credentials.refresh;
|
|
685
472
|
account.expiresAt = latest.credentials.expires;
|
|
686
|
-
account.importFingerprint = latest.fingerprint;
|
|
687
473
|
const accountId =
|
|
688
474
|
typeof latest.credentials.accountId === "string"
|
|
689
475
|
? latest.credentials.accountId
|
|
@@ -691,45 +477,14 @@ export class AccountManager {
|
|
|
691
477
|
if (accountId) {
|
|
692
478
|
account.accountId = accountId;
|
|
693
479
|
}
|
|
694
|
-
this.save();
|
|
695
480
|
this.notifyStateChanged();
|
|
696
481
|
return account.accessToken;
|
|
697
482
|
}
|
|
698
483
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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;
|
|
484
|
+
this.piAuthAccount = undefined;
|
|
485
|
+
this.notifyStateChanged();
|
|
486
|
+
throw new Error(
|
|
487
|
+
`${account.email}: pi auth expired — run /login openai-codex`,
|
|
488
|
+
);
|
|
734
489
|
}
|
|
735
490
|
}
|
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";
|
|
@@ -103,19 +102,14 @@ function getAccountTags(
|
|
|
103
102
|
const manual = accountManager.getManualAccount();
|
|
104
103
|
const quotaHit =
|
|
105
104
|
account.quotaExhaustedUntil && account.quotaExhaustedUntil > Date.now();
|
|
106
|
-
const imported = account.importSource
|
|
107
|
-
? account.importMode === "synthetic"
|
|
108
|
-
? "pi auth only"
|
|
109
|
-
: "pi auth"
|
|
110
|
-
: null;
|
|
111
105
|
return [
|
|
112
106
|
active?.email === account.email ? "active" : null,
|
|
113
107
|
manual?.email === account.email ? "manual" : null,
|
|
108
|
+
accountManager.isPiAuthAccount(account) ? "pi auth" : null,
|
|
114
109
|
account.needsReauth ? "needs reauth" : null,
|
|
115
110
|
isPlaceholderAccount(account) ? "placeholder" : null,
|
|
116
111
|
quotaHit ? "quota" : null,
|
|
117
112
|
isUsageUntouched(usage) ? "untouched" : null,
|
|
118
|
-
imported,
|
|
119
113
|
].filter((value): value is string => Boolean(value));
|
|
120
114
|
}
|
|
121
115
|
|
|
@@ -247,16 +241,6 @@ async function loginAndActivateAccount(
|
|
|
247
241
|
});
|
|
248
242
|
|
|
249
243
|
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();
|
|
259
|
-
}
|
|
260
244
|
accountManager.setManualAccount(account.email);
|
|
261
245
|
ctx.ui.notify(`Now using ${account.email}`, "info");
|
|
262
246
|
return account.email;
|
|
@@ -666,7 +650,6 @@ async function runAccountsSubcommand(
|
|
|
666
650
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
667
651
|
rest: string,
|
|
668
652
|
): Promise<void> {
|
|
669
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
670
653
|
await accountManager.refreshUsageForAllAccounts();
|
|
671
654
|
|
|
672
655
|
if (rest) {
|
|
@@ -729,7 +712,7 @@ async function runRotationSubcommand(
|
|
|
729
712
|
"Current policy: manual account first, then untouched accounts, then earliest weekly reset, then random fallback.",
|
|
730
713
|
"If token validation fails before a request starts, MultiCodex skips that account and retries another one.",
|
|
731
714
|
"If a request hits quota or rate limit before any output streams, MultiCodex marks the account on cooldown and retries.",
|
|
732
|
-
"
|
|
715
|
+
"If pi auth is active, it participates in rotation as an ephemeral account without being persisted.",
|
|
733
716
|
];
|
|
734
717
|
|
|
735
718
|
if (!ctx.hasUI) {
|
|
@@ -758,8 +741,10 @@ async function runVerifySubcommand(
|
|
|
758
741
|
): Promise<void> {
|
|
759
742
|
const storageWritable = await isWritableDirectoryFor(STORAGE_FILE);
|
|
760
743
|
const settingsWritable = await isWritableDirectoryFor(SETTINGS_FILE);
|
|
761
|
-
const authImported = await accountManager.syncImportedOpenAICodexAuth();
|
|
762
744
|
await statusController.loadPreferences(ctx);
|
|
745
|
+
const hasPiAuth = accountManager
|
|
746
|
+
.getAccounts()
|
|
747
|
+
.some((a) => accountManager.isPiAuthAccount(a));
|
|
763
748
|
const accounts = accountManager.getAccounts().length;
|
|
764
749
|
const active = accountManager.getActiveAccount()?.email ?? "none";
|
|
765
750
|
const needsReauth = accountManager.getAccountsNeedingReauth().length;
|
|
@@ -767,7 +752,7 @@ async function runVerifySubcommand(
|
|
|
767
752
|
|
|
768
753
|
if (!ctx.hasUI) {
|
|
769
754
|
ctx.ui.notify(
|
|
770
|
-
`verify: ${ok ? "PASS" : "WARN"} storage=${storageWritable ? "ok" : "fail"} settings=${settingsWritable ? "ok" : "fail"} accounts=${accounts} active=${active}
|
|
755
|
+
`verify: ${ok ? "PASS" : "WARN"} storage=${storageWritable ? "ok" : "fail"} settings=${settingsWritable ? "ok" : "fail"} accounts=${accounts} active=${active} piAuth=${hasPiAuth ? "loaded" : "none"} needsReauth=${needsReauth}`,
|
|
771
756
|
ok ? "info" : "warning",
|
|
772
757
|
);
|
|
773
758
|
return;
|
|
@@ -778,8 +763,8 @@ async function runVerifySubcommand(
|
|
|
778
763
|
`settings directory writable: ${settingsWritable ? "yes" : "no"}`,
|
|
779
764
|
`managed accounts: ${accounts}`,
|
|
780
765
|
`active account: ${active}`,
|
|
766
|
+
`pi auth (ephemeral): ${hasPiAuth ? "loaded" : "none"}`,
|
|
781
767
|
`accounts needing re-authentication: ${needsReauth}`,
|
|
782
|
-
`auth import changed state: ${authImported ? "yes" : "no"}`,
|
|
783
768
|
];
|
|
784
769
|
await ctx.ui.select(`MultiCodex Verify (${ok ? "PASS" : "WARN"})`, lines);
|
|
785
770
|
}
|
|
@@ -878,7 +863,6 @@ async function runRefreshSubcommand(
|
|
|
878
863
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
879
864
|
rest: string,
|
|
880
865
|
): Promise<void> {
|
|
881
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
882
866
|
if (!rest || rest === "all") {
|
|
883
867
|
if (!ctx.hasUI || rest === "all") {
|
|
884
868
|
await refreshAllAccounts(ctx, accountManager);
|
|
@@ -899,7 +883,6 @@ async function runReauthSubcommand(
|
|
|
899
883
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
900
884
|
rest: string,
|
|
901
885
|
): Promise<void> {
|
|
902
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
903
886
|
if (rest) {
|
|
904
887
|
await reauthenticateAccount(pi, ctx, accountManager, rest);
|
|
905
888
|
await statusController.refreshFor(ctx);
|
package/hooks.ts
CHANGED
|
@@ -6,13 +6,13 @@ async function refreshAndActivateBestAccount(
|
|
|
6
6
|
accountManager: AccountManager,
|
|
7
7
|
warningHandler?: WarningHandler,
|
|
8
8
|
): Promise<void> {
|
|
9
|
-
await accountManager.
|
|
9
|
+
await accountManager.loadPiAuth();
|
|
10
10
|
await accountManager.refreshUsageForAllAccounts({ force: true });
|
|
11
11
|
|
|
12
12
|
const needsReauth = accountManager.getAccountsNeedingReauth();
|
|
13
13
|
if (needsReauth.length > 0) {
|
|
14
14
|
const hints = needsReauth.map((a) => {
|
|
15
|
-
const cmd = a
|
|
15
|
+
const cmd = accountManager.isPiAuthAccount(a)
|
|
16
16
|
? "/login openai-codex"
|
|
17
17
|
: `/multicodex use ${a.email}`;
|
|
18
18
|
return `${a.email} (${cmd})`;
|
package/index.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export { AccountManager } from "./account-manager";
|
|
2
|
-
export {
|
|
3
|
-
loadImportedOpenAICodexAuth,
|
|
4
|
-
parseImportedOpenAICodexAuth,
|
|
5
|
-
} from "./auth";
|
|
2
|
+
export { parseImportedOpenAICodexAuth } from "./auth";
|
|
6
3
|
export { default } from "./extension";
|
|
7
4
|
export {
|
|
8
5
|
buildMulticodexProviderConfig,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victor-software-house/pi-multicodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Codex account rotation extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,12 +48,14 @@
|
|
|
48
48
|
"usage.ts",
|
|
49
49
|
"README.md",
|
|
50
50
|
"LICENSE",
|
|
51
|
-
"assets/**"
|
|
51
|
+
"assets/**",
|
|
52
|
+
"schemas/**"
|
|
52
53
|
],
|
|
53
54
|
"scripts": {
|
|
54
55
|
"lint": "biome check .",
|
|
55
56
|
"test": "vitest run -c vitest.config.ts",
|
|
56
57
|
"tsgo": "tsgo -p tsconfig.json",
|
|
58
|
+
"generate:schema": "bun scripts/generate-schema.ts",
|
|
57
59
|
"check": "pnpm lint && pnpm tsgo && pnpm test",
|
|
58
60
|
"pack:dry": "npm pack --dry-run",
|
|
59
61
|
"release:dry": "pnpm exec semantic-release --dry-run"
|
|
@@ -98,6 +100,7 @@
|
|
|
98
100
|
"node": "24.14.0"
|
|
99
101
|
},
|
|
100
102
|
"dependencies": {
|
|
101
|
-
"pi-provider-utils": "^0.0.0"
|
|
103
|
+
"pi-provider-utils": "^0.0.0",
|
|
104
|
+
"zod": "^4.3.6"
|
|
102
105
|
}
|
|
103
106
|
}
|
package/provider.ts
CHANGED
|
@@ -53,8 +53,7 @@ function getActiveApiKey(accountManager: AccountManager): string {
|
|
|
53
53
|
return account.accessToken;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
//
|
|
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
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"description": "JSON Schema reference for editor support",
|
|
7
|
+
"type": "string"
|
|
8
|
+
},
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"exclusiveMinimum": 0,
|
|
12
|
+
"maximum": 9007199254740991,
|
|
13
|
+
"description": "Storage schema version"
|
|
14
|
+
},
|
|
15
|
+
"accounts": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"$ref": "#/$defs/Account"
|
|
19
|
+
},
|
|
20
|
+
"description": "Managed account entries"
|
|
21
|
+
},
|
|
22
|
+
"activeEmail": {
|
|
23
|
+
"description": "Currently active account email",
|
|
24
|
+
"type": "string"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["version", "accounts"],
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"id": "MultiCodexStorage",
|
|
30
|
+
"description": "MultiCodex managed account storage",
|
|
31
|
+
"$defs": {
|
|
32
|
+
"Account": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"email": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"description": "Account email identifier"
|
|
39
|
+
},
|
|
40
|
+
"accessToken": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"minLength": 1,
|
|
43
|
+
"description": "OAuth access token (JWT)"
|
|
44
|
+
},
|
|
45
|
+
"refreshToken": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"minLength": 1,
|
|
48
|
+
"description": "OAuth refresh token"
|
|
49
|
+
},
|
|
50
|
+
"expiresAt": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"description": "Token expiry timestamp (ms since epoch)"
|
|
53
|
+
},
|
|
54
|
+
"accountId": {
|
|
55
|
+
"description": "OpenAI account ID",
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"lastUsed": {
|
|
59
|
+
"description": "Last manual selection timestamp (ms)",
|
|
60
|
+
"type": "number"
|
|
61
|
+
},
|
|
62
|
+
"quotaExhaustedUntil": {
|
|
63
|
+
"description": "Quota cooldown expiry (ms)",
|
|
64
|
+
"type": "number"
|
|
65
|
+
},
|
|
66
|
+
"needsReauth": {
|
|
67
|
+
"description": "Account needs re-authentication",
|
|
68
|
+
"type": "boolean"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": ["email", "accessToken", "refreshToken", "expiresAt"],
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"id": "Account",
|
|
74
|
+
"description": "A managed OpenAI Codex account"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/status.ts
CHANGED
|
@@ -411,11 +411,7 @@ export function createUsageStatusController(accountManager: AccountManager) {
|
|
|
411
411
|
|
|
412
412
|
renderCachedStatus(ctx, livePreviewPreferences ?? preferences);
|
|
413
413
|
|
|
414
|
-
|
|
415
|
-
if (!activeAccount) {
|
|
416
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
417
|
-
activeAccount = accountManager.getActiveAccount();
|
|
418
|
-
}
|
|
414
|
+
const activeAccount = accountManager.getActiveAccount();
|
|
419
415
|
if (!activeAccount) {
|
|
420
416
|
ctx.ui.setStatus(
|
|
421
417
|
STATUS_KEY,
|
package/storage.ts
CHANGED
|
@@ -1,38 +1,169 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getAgentPath } from "pi-provider-utils/agent-paths";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Schema
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const CURRENT_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
const SCHEMA_URL =
|
|
13
|
+
"https://raw.githubusercontent.com/victor-software-house/pi-multicodex/main/schemas/codex-accounts.schema.json";
|
|
14
|
+
|
|
15
|
+
const AccountSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
email: z.string().min(1).meta({ description: "Account email identifier" }),
|
|
18
|
+
accessToken: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.meta({ description: "OAuth access token (JWT)" }),
|
|
22
|
+
refreshToken: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.meta({ description: "OAuth refresh token" }),
|
|
26
|
+
expiresAt: z
|
|
27
|
+
.number()
|
|
28
|
+
.meta({ description: "Token expiry timestamp (ms since epoch)" }),
|
|
29
|
+
accountId: z.string().optional().meta({ description: "OpenAI account ID" }),
|
|
30
|
+
lastUsed: z
|
|
31
|
+
.number()
|
|
32
|
+
.optional()
|
|
33
|
+
.meta({ description: "Last manual selection timestamp (ms)" }),
|
|
34
|
+
quotaExhaustedUntil: z
|
|
35
|
+
.number()
|
|
36
|
+
.optional()
|
|
37
|
+
.meta({ description: "Quota cooldown expiry (ms)" }),
|
|
38
|
+
needsReauth: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.meta({ description: "Account needs re-authentication" }),
|
|
42
|
+
})
|
|
43
|
+
.meta({ id: "Account", description: "A managed OpenAI Codex account" });
|
|
44
|
+
|
|
45
|
+
export const StorageSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
$schema: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.meta({ description: "JSON Schema reference for editor support" }),
|
|
51
|
+
version: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.positive()
|
|
55
|
+
.meta({ description: "Storage schema version" }),
|
|
56
|
+
accounts: z
|
|
57
|
+
.array(AccountSchema)
|
|
58
|
+
.meta({ description: "Managed account entries" }),
|
|
59
|
+
activeEmail: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.meta({ description: "Currently active account email" }),
|
|
63
|
+
})
|
|
64
|
+
.meta({
|
|
65
|
+
id: "MultiCodexStorage",
|
|
66
|
+
description: "MultiCodex managed account storage",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type Account = z.infer<typeof AccountSchema>;
|
|
70
|
+
export type StorageData = z.infer<typeof StorageSchema>;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Migration
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const LEGACY_FIELDS = [
|
|
77
|
+
"importSource",
|
|
78
|
+
"importMode",
|
|
79
|
+
"importFingerprint",
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
function stripLegacyFields(raw: Record<string, unknown>): boolean {
|
|
83
|
+
let stripped = false;
|
|
84
|
+
for (const key of LEGACY_FIELDS) {
|
|
85
|
+
if (key in raw) {
|
|
86
|
+
delete raw[key];
|
|
87
|
+
stripped = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return stripped;
|
|
17
91
|
}
|
|
18
92
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
93
|
+
function migrateRawStorage(raw: unknown): StorageData {
|
|
94
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
95
|
+
return { version: CURRENT_VERSION, accounts: [], activeEmail: undefined };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const record = raw as Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
// Strip legacy import fields from each account
|
|
101
|
+
const rawAccounts = Array.isArray(record.accounts) ? record.accounts : [];
|
|
102
|
+
for (const entry of rawAccounts) {
|
|
103
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
104
|
+
stripLegacyFields(entry as Record<string, unknown>);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add version if missing (pre-v1 files)
|
|
109
|
+
if (!("version" in record) || typeof record.version !== "number") {
|
|
110
|
+
record.version = CURRENT_VERSION;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = StorageSchema.safeParse(record);
|
|
114
|
+
if (result.success) {
|
|
115
|
+
return result.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Schema validation failed — salvage what we can
|
|
119
|
+
const accounts: Account[] = [];
|
|
120
|
+
for (const entry of rawAccounts) {
|
|
121
|
+
const parsed = AccountSchema.safeParse(entry);
|
|
122
|
+
if (parsed.success) {
|
|
123
|
+
accounts.push(parsed.data);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { version: CURRENT_VERSION, accounts, activeEmail: undefined };
|
|
22
127
|
}
|
|
23
128
|
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// I/O
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
24
133
|
export const STORAGE_FILE = getAgentPath("codex-accounts.json");
|
|
25
134
|
|
|
26
135
|
export function loadStorage(): StorageData {
|
|
27
136
|
try {
|
|
28
137
|
if (fs.existsSync(STORAGE_FILE)) {
|
|
29
|
-
|
|
138
|
+
const text = fs.readFileSync(STORAGE_FILE, "utf-8");
|
|
139
|
+
const raw = JSON.parse(text) as Record<string, unknown>;
|
|
140
|
+
const needsMigration =
|
|
141
|
+
!("version" in raw) ||
|
|
142
|
+
raw.version !== CURRENT_VERSION ||
|
|
143
|
+
needsLegacyStrip(raw);
|
|
144
|
+
const data = migrateRawStorage(raw);
|
|
145
|
+
if (needsMigration) {
|
|
146
|
+
saveStorage(data);
|
|
147
|
+
}
|
|
148
|
+
return data;
|
|
30
149
|
}
|
|
31
150
|
} catch (error) {
|
|
32
151
|
console.error("Failed to load multicodex accounts:", error);
|
|
33
152
|
}
|
|
34
153
|
|
|
35
|
-
return { accounts: [] };
|
|
154
|
+
return { version: CURRENT_VERSION, accounts: [], activeEmail: undefined };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function needsLegacyStrip(raw: Record<string, unknown>): boolean {
|
|
158
|
+
const accounts = Array.isArray(raw.accounts) ? raw.accounts : [];
|
|
159
|
+
for (const entry of accounts) {
|
|
160
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
161
|
+
for (const key of LEGACY_FIELDS) {
|
|
162
|
+
if (key in (entry as Record<string, unknown>)) return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
36
167
|
}
|
|
37
168
|
|
|
38
169
|
export function saveStorage(data: StorageData): void {
|
|
@@ -41,7 +172,13 @@ export function saveStorage(data: StorageData): void {
|
|
|
41
172
|
if (!fs.existsSync(dir)) {
|
|
42
173
|
fs.mkdirSync(dir, { recursive: true });
|
|
43
174
|
}
|
|
44
|
-
|
|
175
|
+
const output = {
|
|
176
|
+
$schema: SCHEMA_URL,
|
|
177
|
+
version: CURRENT_VERSION,
|
|
178
|
+
accounts: data.accounts,
|
|
179
|
+
activeEmail: data.activeEmail,
|
|
180
|
+
};
|
|
181
|
+
fs.writeFileSync(STORAGE_FILE, JSON.stringify(output, null, 2));
|
|
45
182
|
} catch (error) {
|
|
46
183
|
console.error("Failed to save multicodex accounts:", error);
|
|
47
184
|
}
|
package/stream-wrapper.ts
CHANGED
|
@@ -39,7 +39,6 @@ export function createStreamWrapper(
|
|
|
39
39
|
|
|
40
40
|
(async () => {
|
|
41
41
|
try {
|
|
42
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
43
42
|
const excludedEmails = new Set<string>();
|
|
44
43
|
for (let attempt = 0; attempt <= MAX_ROTATION_RETRIES; attempt++) {
|
|
45
44
|
const now = Date.now();
|