@victor-software-house/pi-multicodex 2.0.8 → 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.
- package/account-manager.ts +99 -0
- package/commands.ts +16 -6
- package/extension.ts +4 -2
- package/hooks.ts +27 -4
- package/package.json +2 -1
- package/selection.ts +1 -0
- package/storage.ts +1 -0
package/account-manager.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type OAuthCredentials,
|
|
3
3
|
refreshOpenAICodexToken,
|
|
4
4
|
} from "@mariozechner/pi-ai/oauth";
|
|
5
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
5
6
|
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
6
7
|
import { loadImportedOpenAICodexAuth } from "./auth";
|
|
7
8
|
import { isAccountAvailable, pickBestAccount } from "./selection";
|
|
@@ -79,6 +80,7 @@ export class AccountManager {
|
|
|
79
80
|
existing.expiresAt = creds.expires;
|
|
80
81
|
existing.importSource = options?.importSource;
|
|
81
82
|
existing.importFingerprint = options?.importFingerprint;
|
|
83
|
+
existing.needsReauth = undefined;
|
|
82
84
|
if (accountId) {
|
|
83
85
|
existing.accountId = accountId;
|
|
84
86
|
}
|
|
@@ -230,10 +232,22 @@ export class AccountManager {
|
|
|
230
232
|
return this.usageCache.get(email);
|
|
231
233
|
}
|
|
232
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
|
+
|
|
233
245
|
async refreshUsageForAccount(
|
|
234
246
|
account: Account,
|
|
235
247
|
options?: { force?: boolean; signal?: AbortSignal },
|
|
236
248
|
): Promise<CodexUsageSnapshot | undefined> {
|
|
249
|
+
if (account.needsReauth) return this.usageCache.get(account.email);
|
|
250
|
+
|
|
237
251
|
const cached = this.usageCache.get(account.email);
|
|
238
252
|
const now = Date.now();
|
|
239
253
|
if (
|
|
@@ -339,10 +353,25 @@ export class AccountManager {
|
|
|
339
353
|
}
|
|
340
354
|
|
|
341
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
|
+
|
|
342
365
|
if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
|
|
343
366
|
return account.accessToken;
|
|
344
367
|
}
|
|
345
368
|
|
|
369
|
+
// For the imported pi account, delegate to AuthStorage so we share pi's
|
|
370
|
+
// file lock and never race with pi's own refresh path.
|
|
371
|
+
if (account.importSource === "pi-openai-codex") {
|
|
372
|
+
return this.ensureValidTokenForImportedAccount(account);
|
|
373
|
+
}
|
|
374
|
+
|
|
346
375
|
const inflight = this.refreshPromises.get(account.email);
|
|
347
376
|
if (inflight) {
|
|
348
377
|
return inflight;
|
|
@@ -362,6 +391,9 @@ export class AccountManager {
|
|
|
362
391
|
this.save();
|
|
363
392
|
this.notifyStateChanged();
|
|
364
393
|
return account.accessToken;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
this.markNeedsReauth(account);
|
|
396
|
+
throw error;
|
|
365
397
|
} finally {
|
|
366
398
|
this.refreshPromises.delete(account.email);
|
|
367
399
|
}
|
|
@@ -370,4 +402,71 @@ export class AccountManager {
|
|
|
370
402
|
this.refreshPromises.set(account.email, promise);
|
|
371
403
|
return promise;
|
|
372
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Refresh path for the imported pi account.
|
|
408
|
+
*
|
|
409
|
+
* Uses AuthStorage so our refresh is serialised by the same file lock that
|
|
410
|
+
* pi's own credential refresh uses. This prevents "refresh_token_reused"
|
|
411
|
+
* errors caused by pi and multicodex both refreshing the same token
|
|
412
|
+
* simultaneously.
|
|
413
|
+
*/
|
|
414
|
+
private async ensureValidTokenForImportedAccount(
|
|
415
|
+
account: Account,
|
|
416
|
+
): Promise<string> {
|
|
417
|
+
// Check if pi already refreshed since our last sync.
|
|
418
|
+
const latest = await loadImportedOpenAICodexAuth();
|
|
419
|
+
if (latest && Date.now() < latest.credentials.expires - 5 * 60 * 1000) {
|
|
420
|
+
account.accessToken = latest.credentials.access;
|
|
421
|
+
account.refreshToken = latest.credentials.refresh;
|
|
422
|
+
account.expiresAt = latest.credentials.expires;
|
|
423
|
+
account.importFingerprint = latest.fingerprint;
|
|
424
|
+
const accountId =
|
|
425
|
+
typeof latest.credentials.accountId === "string"
|
|
426
|
+
? latest.credentials.accountId
|
|
427
|
+
: undefined;
|
|
428
|
+
if (accountId) {
|
|
429
|
+
account.accountId = accountId;
|
|
430
|
+
}
|
|
431
|
+
this.save();
|
|
432
|
+
this.notifyStateChanged();
|
|
433
|
+
return account.accessToken;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Both our copy and auth.json are expired — let AuthStorage refresh with
|
|
437
|
+
// its file lock so only one caller (us or pi) fires the API call.
|
|
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
|
+
}
|
|
445
|
+
if (!apiKey) {
|
|
446
|
+
this.markNeedsReauth(account);
|
|
447
|
+
throw new Error(
|
|
448
|
+
`${account.email}: token refresh failed — run /login openai-codex to re-authenticate`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Read the refreshed tokens back from auth.json.
|
|
453
|
+
const refreshed = await loadImportedOpenAICodexAuth();
|
|
454
|
+
if (refreshed) {
|
|
455
|
+
account.accessToken = refreshed.credentials.access;
|
|
456
|
+
account.refreshToken = refreshed.credentials.refresh;
|
|
457
|
+
account.expiresAt = refreshed.credentials.expires;
|
|
458
|
+
account.importFingerprint = refreshed.fingerprint;
|
|
459
|
+
const accountId =
|
|
460
|
+
typeof refreshed.credentials.accountId === "string"
|
|
461
|
+
? refreshed.credentials.accountId
|
|
462
|
+
: undefined;
|
|
463
|
+
if (accountId) {
|
|
464
|
+
account.accountId = accountId;
|
|
465
|
+
}
|
|
466
|
+
this.save();
|
|
467
|
+
this.notifyStateChanged();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return apiKey;
|
|
471
|
+
}
|
|
373
472
|
}
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
22
|
-
|
|
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.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Codex account rotation extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
90
90
|
"@types/node": "^25.5.0",
|
|
91
91
|
"@typescript/native-preview": "7.0.0-dev.20260314.1",
|
|
92
|
+
"conventional-changelog-conventionalcommits": "^9.3.0",
|
|
92
93
|
"semantic-release": "^25.0.3",
|
|
93
94
|
"typescript": "^5.9.3",
|
|
94
95
|
"vitest": "^4.1.0"
|
package/selection.ts
CHANGED