@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.
- package/account-manager.ts +53 -4
- package/auth.ts +33 -0
- package/commands.ts +16 -6
- package/extension.ts +4 -2
- package/hooks.ts +27 -4
- package/package.json +1 -1
- package/selection.ts +1 -0
- package/storage.ts +1 -0
package/account-manager.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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(
|
|
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
package/selection.ts
CHANGED