@victor-software-house/pi-multicodex 2.0.13 → 2.1.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.
- package/README.md +23 -14
- package/account-manager.ts +205 -36
- package/commands.ts +268 -78
- package/extension.ts +2 -0
- package/package.json +1 -1
- package/stream-wrapper.ts +14 -1
package/README.md
CHANGED
|
@@ -23,12 +23,13 @@ To manage your accounts inside a session, type `/multicodex`.
|
|
|
23
23
|
When you start a session, MultiCodex:
|
|
24
24
|
|
|
25
25
|
1. Imports your existing pi Codex auth automatically (if present).
|
|
26
|
-
2.
|
|
27
|
-
3.
|
|
26
|
+
2. Merges duplicate imported credentials into the managed pool so one account does not consume multiple rotation slots.
|
|
27
|
+
3. Checks usage data across all managed accounts.
|
|
28
|
+
4. Picks the best available account — untouched accounts first, then the one whose weekly reset window ends soonest, then a random available account as fallback.
|
|
28
29
|
|
|
29
|
-
If you pin a specific account
|
|
30
|
+
If you pin a specific account from `/multicodex accounts` or `/multicodex use`, that account is used until it hits quota, fails auth validation, or you clear the override.
|
|
30
31
|
|
|
31
|
-
When a request hits a quota or rate limit **before** any output is streamed, MultiCodex marks that account exhausted, picks the next available one, and retries. This happens up to 5 times transparently. If the manual override account fails, the override is cleared and rotation continues with the remaining accounts. Once output has started streaming, the error is surfaced as-is — no mid-stream account switching.
|
|
32
|
+
When a request hits a quota or rate limit **before** any output is streamed, MultiCodex marks that account exhausted, picks the next available one, and retries. This happens up to 5 times transparently. If token validation or token refresh fails before the request starts, MultiCodex skips that account and retries another healthy one. If the manual override account fails, the override is cleared and rotation continues with the remaining accounts. Once output has started streaming, the error is surfaced as-is — no mid-stream account switching.
|
|
32
33
|
|
|
33
34
|
## Commands
|
|
34
35
|
|
|
@@ -37,27 +38,35 @@ Everything lives under one command: `/multicodex`.
|
|
|
37
38
|
| Command | What it does |
|
|
38
39
|
|---|---|
|
|
39
40
|
| `/multicodex` | Open the main interactive menu |
|
|
40
|
-
| `/multicodex
|
|
41
|
-
| `/multicodex use [identifier]` |
|
|
41
|
+
| `/multicodex accounts [identifier]` | Inspect account health, select an account, add one, or directly activate/login by identifier |
|
|
42
|
+
| `/multicodex use [identifier]` | Alias for `/multicodex accounts [identifier]` |
|
|
43
|
+
| `/multicodex show` | Alias for the account-management view; in non-interactive mode it prints per-account health lines |
|
|
44
|
+
| `/multicodex refresh [identifier\|all]` | Refresh token validity and usage data for one account or all accounts |
|
|
45
|
+
| `/multicodex reauth [identifier]` | Re-authenticate one account explicitly |
|
|
42
46
|
| `/multicodex footer` | Configure the usage footer display |
|
|
43
47
|
| `/multicodex rotation` | Show the current rotation policy |
|
|
44
|
-
| `/multicodex verify` | Check
|
|
48
|
+
| `/multicodex verify` | Check storage, settings, auth import, and reauth health |
|
|
45
49
|
| `/multicodex path` | Print storage and settings file locations |
|
|
46
50
|
| `/multicodex reset [manual\|quota\|all]` | Clear manual override, quota cooldowns, or both |
|
|
47
51
|
| `/multicodex help` | Print a compact usage line |
|
|
48
52
|
|
|
49
|
-
All subcommands support dynamic autocomplete.
|
|
53
|
+
All subcommands support dynamic autocomplete. Account-focused subcommands autocomplete from the managed account list.
|
|
50
54
|
|
|
51
|
-
Commands that do not need a UI panel (`show`, `verify`, `path`, `reset`, `help`) work in non-interactive mode too.
|
|
55
|
+
Commands that do not need a UI panel (`show`, `refresh`, `verify`, `path`, `reset`, `help`) work in non-interactive mode too.
|
|
52
56
|
|
|
53
|
-
## Account
|
|
57
|
+
## Account manager
|
|
54
58
|
|
|
55
|
-
The `/multicodex
|
|
59
|
+
The `/multicodex accounts` panel merges the old `show` and `use` flows into one place.
|
|
56
60
|
|
|
57
61
|

|
|
58
62
|
|
|
59
63
|
- **Enter** activates the highlighted account.
|
|
60
|
-
- **
|
|
64
|
+
- **U** refreshes token and usage health for the selected account.
|
|
65
|
+
- **R** re-authenticates the selected account.
|
|
66
|
+
- **N** starts login for a new managed account.
|
|
67
|
+
- **Backspace** removes the selected account after confirmation.
|
|
68
|
+
|
|
69
|
+
Each row shows the account identifier, active/manual state, reauth state, quota state, linked imported auth state, and cached 5-hour and weekly usage windows.
|
|
61
70
|
|
|
62
71
|
When you remove an active account, MultiCodex switches to the next available one automatically.
|
|
63
72
|
|
|
@@ -74,8 +83,8 @@ You can customize which fields appear and their ordering with `/multicodex foote
|
|
|
74
83
|
## What it does under the hood
|
|
75
84
|
|
|
76
85
|
- **Provider override.** MultiCodex registers itself as the `openai-codex` provider. You do not need to select a different provider or change your model — it works with whatever Codex model you already use.
|
|
77
|
-
- **Auth import.** When pi has stored Codex OAuth credentials, MultiCodex imports them automatically
|
|
78
|
-
- **Token refresh.** OAuth tokens are refreshed before expiry so requests do not fail due to stale credentials.
|
|
86
|
+
- **Auth import.** When pi has stored Codex OAuth credentials, MultiCodex imports them automatically and merges duplicate credentials into existing managed accounts when possible.
|
|
87
|
+
- **Token refresh.** OAuth tokens are refreshed before expiry so requests do not fail due to stale credentials. You can also force a health refresh with `/multicodex refresh` or re-authenticate explicitly with `/multicodex reauth`.
|
|
79
88
|
- **Usage tracking.** Usage data is fetched from the Codex API and cached for 5 minutes per account. The footer renders cached data immediately and refreshes in the background.
|
|
80
89
|
- **Quota cooldown.** When an account is exhausted, it stays on cooldown until its next known reset time (or 1 hour if the reset time is unknown).
|
|
81
90
|
- **Shared utility seams.** Provider mirroring, stream primitives, and `~/.pi/agent/*` path helpers are shared with `pi-credential-vault` through `@victor-software-house/pi-provider-utils`. MultiCodex still owns account storage, token policy, footer behavior, and command UX.
|
package/account-manager.ts
CHANGED
|
@@ -32,6 +32,7 @@ export class AccountManager {
|
|
|
32
32
|
private warningHandler?: WarningHandler;
|
|
33
33
|
private manualEmail?: string;
|
|
34
34
|
private stateChangeHandlers = new Set<StateChangeHandler>();
|
|
35
|
+
private warnedAuthFailureEmails = new Set<string>();
|
|
35
36
|
|
|
36
37
|
constructor() {
|
|
37
38
|
this.data = loadStorage();
|
|
@@ -83,39 +84,171 @@ export class AccountManager {
|
|
|
83
84
|
this.warningHandler = handler;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
resetSessionWarnings(): void {
|
|
88
|
+
this.warnedAuthFailureEmails.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
notifyRotationSkipForAuthFailure(account: Account, error: unknown): void {
|
|
92
|
+
if (this.warnedAuthFailureEmails.has(account.email)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.warnedAuthFailureEmails.add(account.email);
|
|
96
|
+
const hint = account.importSource
|
|
97
|
+
? "/multicodex reauth"
|
|
98
|
+
: `/multicodex reauth ${account.email}`;
|
|
99
|
+
this.warningHandler?.(
|
|
100
|
+
`Multicodex skipped ${account.email} during rotation: ${normalizeUnknownError(error)}. Account is flagged in /multicodex accounts. Run ${hint} to repair it.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
private removeAccountRecord(account: Account): boolean {
|
|
123
|
+
const index = this.data.accounts.findIndex(
|
|
124
|
+
(candidate) => candidate.email === account.email,
|
|
125
|
+
);
|
|
126
|
+
if (index < 0) return false;
|
|
127
|
+
const removedEmail = this.data.accounts[index]?.email;
|
|
128
|
+
this.data.accounts.splice(index, 1);
|
|
129
|
+
if (removedEmail) {
|
|
130
|
+
this.usageCache.delete(removedEmail);
|
|
131
|
+
if (this.manualEmail === removedEmail) {
|
|
132
|
+
this.manualEmail = undefined;
|
|
133
|
+
}
|
|
134
|
+
if (this.data.activeEmail === removedEmail) {
|
|
135
|
+
this.data.activeEmail = this.data.accounts[0]?.email;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private findAccountByRefreshToken(
|
|
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
|
+
importFingerprint?: string;
|
|
157
|
+
},
|
|
158
|
+
): boolean {
|
|
159
|
+
const accountId =
|
|
160
|
+
typeof creds.accountId === "string" ? creds.accountId : undefined;
|
|
161
|
+
let changed = false;
|
|
162
|
+
if (account.accessToken !== creds.access) {
|
|
163
|
+
account.accessToken = creds.access;
|
|
164
|
+
changed = true;
|
|
165
|
+
}
|
|
166
|
+
if (account.refreshToken !== creds.refresh) {
|
|
167
|
+
account.refreshToken = creds.refresh;
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
if (account.expiresAt !== creds.expires) {
|
|
171
|
+
account.expiresAt = creds.expires;
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
if (accountId && account.accountId !== accountId) {
|
|
175
|
+
account.accountId = accountId;
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
options?.importSource &&
|
|
180
|
+
account.importSource !== options.importSource
|
|
181
|
+
) {
|
|
182
|
+
account.importSource = options.importSource;
|
|
183
|
+
changed = true;
|
|
184
|
+
}
|
|
185
|
+
if (
|
|
186
|
+
options?.importFingerprint &&
|
|
187
|
+
account.importFingerprint !== options.importFingerprint
|
|
188
|
+
) {
|
|
189
|
+
account.importFingerprint = options.importFingerprint;
|
|
190
|
+
changed = true;
|
|
191
|
+
}
|
|
192
|
+
if (account.needsReauth) {
|
|
193
|
+
account.needsReauth = undefined;
|
|
194
|
+
this.warnedAuthFailureEmails.delete(account.email);
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
return changed;
|
|
198
|
+
}
|
|
199
|
+
|
|
86
200
|
addOrUpdateAccount(
|
|
87
201
|
email: string,
|
|
88
202
|
creds: OAuthCredentials,
|
|
89
203
|
options?: {
|
|
90
204
|
importSource?: "pi-openai-codex";
|
|
91
205
|
importFingerprint?: string;
|
|
206
|
+
preserveActive?: boolean;
|
|
92
207
|
},
|
|
93
|
-
):
|
|
208
|
+
): Account {
|
|
94
209
|
const existing = this.getAccount(email);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
210
|
+
const duplicate = existing
|
|
211
|
+
? undefined
|
|
212
|
+
: this.findAccountByRefreshToken(creds.refresh);
|
|
213
|
+
let target = existing ?? duplicate;
|
|
214
|
+
let changed = false;
|
|
215
|
+
|
|
216
|
+
if (target) {
|
|
217
|
+
if (
|
|
218
|
+
duplicate?.importSource === "pi-openai-codex" &&
|
|
219
|
+
duplicate.email !== email &&
|
|
220
|
+
!this.getAccount(email)
|
|
221
|
+
) {
|
|
222
|
+
changed = this.updateAccountEmail(duplicate, email) || changed;
|
|
106
223
|
}
|
|
224
|
+
changed = this.applyCredentials(target, creds, options) || changed;
|
|
107
225
|
} else {
|
|
108
|
-
|
|
226
|
+
target = {
|
|
109
227
|
email,
|
|
110
228
|
accessToken: creds.access,
|
|
111
229
|
refreshToken: creds.refresh,
|
|
112
230
|
expiresAt: creds.expires,
|
|
113
|
-
accountId
|
|
231
|
+
accountId:
|
|
232
|
+
typeof creds.accountId === "string" ? creds.accountId : undefined,
|
|
114
233
|
importSource: options?.importSource,
|
|
115
234
|
importFingerprint: options?.importFingerprint,
|
|
116
|
-
}
|
|
235
|
+
};
|
|
236
|
+
this.data.accounts.push(target);
|
|
237
|
+
changed = true;
|
|
117
238
|
}
|
|
118
|
-
|
|
239
|
+
|
|
240
|
+
if (!options?.preserveActive) {
|
|
241
|
+
if (this.data.activeEmail !== target.email) {
|
|
242
|
+
this.setActiveAccount(target.email);
|
|
243
|
+
return target;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (changed) {
|
|
248
|
+
this.save();
|
|
249
|
+
this.notifyStateChanged();
|
|
250
|
+
}
|
|
251
|
+
return target;
|
|
119
252
|
}
|
|
120
253
|
|
|
121
254
|
getActiveAccount(): Account | undefined {
|
|
@@ -172,23 +305,68 @@ export class AccountManager {
|
|
|
172
305
|
if (!imported) return false;
|
|
173
306
|
|
|
174
307
|
const existingImported = this.getImportedAccount();
|
|
175
|
-
if (
|
|
176
|
-
existingImported?.importFingerprint === imported.fingerprint &&
|
|
177
|
-
existingImported.email === imported.identifier
|
|
178
|
-
) {
|
|
308
|
+
if (existingImported?.importFingerprint === imported.fingerprint) {
|
|
179
309
|
return false;
|
|
180
310
|
}
|
|
181
311
|
|
|
182
|
-
|
|
312
|
+
const matchingAccount = this.findAccountByRefreshToken(
|
|
313
|
+
imported.credentials.refresh,
|
|
314
|
+
existingImported?.email,
|
|
315
|
+
);
|
|
316
|
+
if (matchingAccount) {
|
|
317
|
+
const wasActiveImported =
|
|
318
|
+
existingImported && this.data.activeEmail === existingImported.email;
|
|
319
|
+
const wasManualImported =
|
|
320
|
+
existingImported && this.manualEmail === existingImported.email;
|
|
321
|
+
let changed = this.applyCredentials(
|
|
322
|
+
matchingAccount,
|
|
323
|
+
imported.credentials,
|
|
324
|
+
{
|
|
325
|
+
importSource: "pi-openai-codex",
|
|
326
|
+
importFingerprint: imported.fingerprint,
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
if (existingImported && existingImported !== matchingAccount) {
|
|
330
|
+
changed = this.removeAccountRecord(existingImported) || changed;
|
|
331
|
+
if (wasActiveImported) {
|
|
332
|
+
this.data.activeEmail = matchingAccount.email;
|
|
333
|
+
}
|
|
334
|
+
if (wasManualImported) {
|
|
335
|
+
this.manualEmail = matchingAccount.email;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (changed) {
|
|
339
|
+
this.save();
|
|
340
|
+
this.notifyStateChanged();
|
|
341
|
+
}
|
|
342
|
+
return changed;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (existingImported) {
|
|
183
346
|
const target = this.getAccount(imported.identifier);
|
|
184
|
-
|
|
185
|
-
|
|
347
|
+
let changed = false;
|
|
348
|
+
if (!target && existingImported.email !== imported.identifier) {
|
|
349
|
+
changed = this.updateAccountEmail(
|
|
350
|
+
existingImported,
|
|
351
|
+
imported.identifier,
|
|
352
|
+
);
|
|
186
353
|
}
|
|
354
|
+
changed =
|
|
355
|
+
this.applyCredentials(existingImported, imported.credentials, {
|
|
356
|
+
importSource: "pi-openai-codex",
|
|
357
|
+
importFingerprint: imported.fingerprint,
|
|
358
|
+
}) || changed;
|
|
359
|
+
if (changed) {
|
|
360
|
+
this.save();
|
|
361
|
+
this.notifyStateChanged();
|
|
362
|
+
}
|
|
363
|
+
return changed;
|
|
187
364
|
}
|
|
188
365
|
|
|
189
366
|
this.addOrUpdateAccount(imported.identifier, imported.credentials, {
|
|
190
367
|
importSource: "pi-openai-codex",
|
|
191
368
|
importFingerprint: imported.fingerprint,
|
|
369
|
+
preserveActive: true,
|
|
192
370
|
});
|
|
193
371
|
return true;
|
|
194
372
|
}
|
|
@@ -230,19 +408,10 @@ export class AccountManager {
|
|
|
230
408
|
}
|
|
231
409
|
|
|
232
410
|
removeAccount(email: string): boolean {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
this.data.accounts.splice(index, 1);
|
|
239
|
-
this.usageCache.delete(email);
|
|
240
|
-
if (this.manualEmail === email) {
|
|
241
|
-
this.manualEmail = undefined;
|
|
242
|
-
}
|
|
243
|
-
if (this.data.activeEmail === email) {
|
|
244
|
-
this.data.activeEmail = this.data.accounts[0]?.email;
|
|
245
|
-
}
|
|
411
|
+
const account = this.getAccount(email);
|
|
412
|
+
if (!account) return false;
|
|
413
|
+
const removed = this.removeAccountRecord(account);
|
|
414
|
+
if (!removed) return false;
|
|
246
415
|
this.save();
|
|
247
416
|
this.notifyStateChanged();
|
|
248
417
|
return true;
|
package/commands.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { getAgentSettingsPath } from "pi-provider-utils/agent-paths";
|
|
18
18
|
import { normalizeUnknownError } from "pi-provider-utils/streams";
|
|
19
19
|
import type { AccountManager } from "./account-manager";
|
|
20
|
+
import { writeActiveTokenToAuthJson } from "./auth";
|
|
20
21
|
import { openLoginInBrowser } from "./browser";
|
|
21
22
|
import type { createUsageStatusController } from "./status";
|
|
22
23
|
import { STORAGE_FILE } from "./storage";
|
|
@@ -24,12 +25,15 @@ import { formatResetAt, isUsageUntouched } from "./usage";
|
|
|
24
25
|
|
|
25
26
|
const SETTINGS_FILE = getAgentSettingsPath();
|
|
26
27
|
const NO_ACCOUNTS_MESSAGE =
|
|
27
|
-
"No managed accounts found.
|
|
28
|
+
"No managed accounts found. Open /multicodex accounts to add one.";
|
|
28
29
|
const HELP_TEXT =
|
|
29
|
-
"Usage: /multicodex [
|
|
30
|
+
"Usage: /multicodex [accounts [identifier]|use [identifier]|show|refresh [identifier|all]|reauth [identifier]|footer|rotation|verify|path|reset [manual|quota|all]|help]";
|
|
30
31
|
const SUBCOMMANDS = [
|
|
31
|
-
"
|
|
32
|
+
"accounts",
|
|
32
33
|
"use",
|
|
34
|
+
"show",
|
|
35
|
+
"refresh",
|
|
36
|
+
"reauth",
|
|
33
37
|
"footer",
|
|
34
38
|
"rotation",
|
|
35
39
|
"verify",
|
|
@@ -44,7 +48,10 @@ type ResetTarget = (typeof RESET_TARGETS)[number];
|
|
|
44
48
|
|
|
45
49
|
type AccountPanelResult =
|
|
46
50
|
| { action: "select"; email: string }
|
|
51
|
+
| { action: "refresh"; email: string }
|
|
52
|
+
| { action: "reauth"; email: string }
|
|
47
53
|
| { action: "remove"; email: string }
|
|
54
|
+
| { action: "add" }
|
|
48
55
|
| undefined;
|
|
49
56
|
|
|
50
57
|
function toAutocompleteItems(values: readonly string[]): AutocompleteItem[] {
|
|
@@ -80,18 +87,6 @@ function parseResetTarget(value: string): ResetTarget | undefined {
|
|
|
80
87
|
return undefined;
|
|
81
88
|
}
|
|
82
89
|
|
|
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(", ")})`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
90
|
function formatAccountStatusLine(
|
|
96
91
|
accountManager: AccountManager,
|
|
97
92
|
email: string,
|
|
@@ -104,7 +99,7 @@ function formatAccountStatusLine(
|
|
|
104
99
|
const quotaHit =
|
|
105
100
|
account.quotaExhaustedUntil && account.quotaExhaustedUntil > Date.now();
|
|
106
101
|
const untouched = isUsageUntouched(usage) ? "untouched" : null;
|
|
107
|
-
const imported = account.importSource ? "
|
|
102
|
+
const imported = account.importSource ? "linked-auth" : null;
|
|
108
103
|
const reauth = account.needsReauth ? "needs reauth" : null;
|
|
109
104
|
const tags = [
|
|
110
105
|
active?.email === account.email ? "active" : null,
|
|
@@ -134,7 +129,8 @@ function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
|
|
|
134
129
|
return matches.length > 0 ? toAutocompleteItems(matches) : null;
|
|
135
130
|
}
|
|
136
131
|
|
|
137
|
-
function
|
|
132
|
+
function getAccountCompletions(
|
|
133
|
+
subcommand: "accounts" | "use" | "reauth",
|
|
138
134
|
prefix: string,
|
|
139
135
|
accountManager: AccountManager,
|
|
140
136
|
): AutocompleteItem[] | null {
|
|
@@ -143,7 +139,26 @@ function getUseCompletions(
|
|
|
143
139
|
.map((account) => account.email)
|
|
144
140
|
.filter((value) => value.startsWith(prefix));
|
|
145
141
|
if (matches.length === 0) return null;
|
|
146
|
-
return matches.map((value) => ({
|
|
142
|
+
return matches.map((value) => ({
|
|
143
|
+
value: `${subcommand} ${value}`,
|
|
144
|
+
label: value,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getRefreshCompletions(
|
|
149
|
+
prefix: string,
|
|
150
|
+
accountManager: AccountManager,
|
|
151
|
+
): AutocompleteItem[] | null {
|
|
152
|
+
const values = [
|
|
153
|
+
"all",
|
|
154
|
+
...accountManager.getAccounts().map((account) => account.email),
|
|
155
|
+
].filter((value, index, array) => array.indexOf(value) === index);
|
|
156
|
+
const matches = values.filter((value) => value.startsWith(prefix));
|
|
157
|
+
if (matches.length === 0) return null;
|
|
158
|
+
return matches.map((value) => ({
|
|
159
|
+
value: `refresh ${value}`,
|
|
160
|
+
label: value,
|
|
161
|
+
}));
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
function getResetCompletions(prefix: string): AutocompleteItem[] | null {
|
|
@@ -169,8 +184,17 @@ function getCommandCompletions(
|
|
|
169
184
|
const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
|
|
170
185
|
const rest = trimmedStart.slice(firstSpaceIndex + 1).trimStart();
|
|
171
186
|
|
|
187
|
+
if (subcommand === "accounts") {
|
|
188
|
+
return getAccountCompletions("accounts", rest, accountManager);
|
|
189
|
+
}
|
|
172
190
|
if (subcommand === "use") {
|
|
173
|
-
return
|
|
191
|
+
return getAccountCompletions("use", rest, accountManager);
|
|
192
|
+
}
|
|
193
|
+
if (subcommand === "reauth") {
|
|
194
|
+
return getAccountCompletions("reauth", rest, accountManager);
|
|
195
|
+
}
|
|
196
|
+
if (subcommand === "refresh") {
|
|
197
|
+
return getRefreshCompletions(rest, accountManager);
|
|
174
198
|
}
|
|
175
199
|
if (subcommand === "reset") {
|
|
176
200
|
return getResetCompletions(rest);
|
|
@@ -184,7 +208,7 @@ async function loginAndActivateAccount(
|
|
|
184
208
|
ctx: ExtensionCommandContext,
|
|
185
209
|
accountManager: AccountManager,
|
|
186
210
|
identifier: string,
|
|
187
|
-
): Promise<
|
|
211
|
+
): Promise<string | undefined> {
|
|
188
212
|
try {
|
|
189
213
|
ctx.ui.notify(
|
|
190
214
|
`Starting login for ${identifier}... Check your browser.`,
|
|
@@ -200,13 +224,23 @@ async function loginAndActivateAccount(
|
|
|
200
224
|
onPrompt: async ({ message }) => (await ctx.ui.input(message)) || "",
|
|
201
225
|
});
|
|
202
226
|
|
|
203
|
-
accountManager.addOrUpdateAccount(identifier, creds);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const account = accountManager.addOrUpdateAccount(identifier, creds);
|
|
228
|
+
if (account.importSource) {
|
|
229
|
+
writeActiveTokenToAuthJson({
|
|
230
|
+
access: creds.access,
|
|
231
|
+
refresh: creds.refresh,
|
|
232
|
+
expires: creds.expires,
|
|
233
|
+
accountId:
|
|
234
|
+
typeof creds.accountId === "string" ? creds.accountId : undefined,
|
|
235
|
+
});
|
|
236
|
+
await accountManager.syncImportedOpenAICodexAuth();
|
|
237
|
+
}
|
|
238
|
+
accountManager.setManualAccount(account.email);
|
|
239
|
+
ctx.ui.notify(`Now using ${account.email}`, "info");
|
|
240
|
+
return account.email;
|
|
207
241
|
} catch (error) {
|
|
208
242
|
ctx.ui.notify(`Login failed: ${normalizeUnknownError(error)}`, "error");
|
|
209
|
-
return
|
|
243
|
+
return undefined;
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
|
|
@@ -220,12 +254,12 @@ async function useOrLoginAccount(
|
|
|
220
254
|
if (existing) {
|
|
221
255
|
try {
|
|
222
256
|
await accountManager.ensureValidToken(existing);
|
|
223
|
-
accountManager.setManualAccount(
|
|
224
|
-
ctx.ui.notify(`Now using ${
|
|
257
|
+
accountManager.setManualAccount(existing.email);
|
|
258
|
+
ctx.ui.notify(`Now using ${existing.email}`, "info");
|
|
225
259
|
return;
|
|
226
260
|
} catch {
|
|
227
261
|
ctx.ui.notify(
|
|
228
|
-
`Stored auth for ${
|
|
262
|
+
`Stored auth for ${existing.email} is no longer valid. Starting login again.`,
|
|
229
263
|
"warning",
|
|
230
264
|
);
|
|
231
265
|
}
|
|
@@ -234,33 +268,100 @@ async function useOrLoginAccount(
|
|
|
234
268
|
await loginAndActivateAccount(pi, ctx, accountManager, identifier);
|
|
235
269
|
}
|
|
236
270
|
|
|
237
|
-
async function
|
|
271
|
+
async function refreshSingleAccount(
|
|
272
|
+
ctx: ExtensionCommandContext,
|
|
273
|
+
accountManager: AccountManager,
|
|
274
|
+
email: string,
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
const account = accountManager.getAccount(email);
|
|
277
|
+
if (!account) {
|
|
278
|
+
ctx.ui.notify(`Unknown account: ${email}`, "warning");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
await accountManager.ensureValidToken(account);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
ctx.ui.notify(
|
|
286
|
+
`refresh ${email}: ${normalizeUnknownError(error)}`,
|
|
287
|
+
"warning",
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await accountManager.refreshUsageForAccount(account, { force: true });
|
|
293
|
+
ctx.ui.notify(
|
|
294
|
+
`refreshed ${formatAccountStatusLine(accountManager, email)}`,
|
|
295
|
+
"info",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function refreshAllAccounts(
|
|
300
|
+
ctx: ExtensionCommandContext,
|
|
301
|
+
accountManager: AccountManager,
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
await accountManager.refreshUsageForAllAccounts({ force: true });
|
|
304
|
+
const accounts = accountManager.getAccounts();
|
|
305
|
+
const needsReauth = accountManager.getAccountsNeedingReauth().length;
|
|
306
|
+
const summary =
|
|
307
|
+
accounts.length === 0
|
|
308
|
+
? NO_ACCOUNTS_MESSAGE
|
|
309
|
+
: `refreshed ${accounts.length} account(s); reauth needed=${needsReauth}`;
|
|
310
|
+
ctx.ui.notify(summary, needsReauth > 0 ? "warning" : "info");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function reauthenticateAccount(
|
|
314
|
+
pi: ExtensionAPI,
|
|
315
|
+
ctx: ExtensionCommandContext,
|
|
316
|
+
accountManager: AccountManager,
|
|
317
|
+
email: string,
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
const account = accountManager.getAccount(email);
|
|
320
|
+
if (!account) {
|
|
321
|
+
ctx.ui.notify(`Unknown account: ${email}`, "warning");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await loginAndActivateAccount(pi, ctx, accountManager, account.email);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function promptForNewAccountIdentifier(
|
|
328
|
+
ctx: ExtensionCommandContext,
|
|
329
|
+
): Promise<string | undefined> {
|
|
330
|
+
const identifier = (await ctx.ui.input("Account identifier"))?.trim();
|
|
331
|
+
if (!identifier) {
|
|
332
|
+
ctx.ui.notify("Account creation cancelled.", "warning");
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return identifier;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function openAccountManagementPanel(
|
|
238
339
|
ctx: ExtensionCommandContext,
|
|
239
340
|
accountManager: AccountManager,
|
|
240
341
|
): Promise<AccountPanelResult> {
|
|
241
342
|
const accounts = accountManager.getAccounts();
|
|
242
343
|
const items = accounts.map((account) => ({
|
|
243
344
|
value: account.email,
|
|
244
|
-
label:
|
|
245
|
-
quotaExhaustedUntil: account.quotaExhaustedUntil,
|
|
246
|
-
needsReauth: account.needsReauth,
|
|
247
|
-
}),
|
|
345
|
+
label: formatAccountStatusLine(accountManager, account.email),
|
|
248
346
|
}));
|
|
249
347
|
|
|
250
348
|
return ctx.ui.custom<AccountPanelResult>((_tui, theme, _kb, done) => {
|
|
251
349
|
const container = new Container();
|
|
252
350
|
container.addChild(
|
|
253
|
-
new Text(theme.fg("accent", theme.bold("
|
|
351
|
+
new Text(theme.fg("accent", theme.bold("MultiCodex Accounts")), 1, 0),
|
|
254
352
|
);
|
|
255
353
|
container.addChild(
|
|
256
354
|
new Text(
|
|
257
|
-
theme.fg(
|
|
355
|
+
theme.fg(
|
|
356
|
+
"dim",
|
|
357
|
+
"Enter: use U: refresh R: re-auth N: add Backspace: remove Esc: cancel",
|
|
358
|
+
),
|
|
258
359
|
1,
|
|
259
360
|
0,
|
|
260
361
|
),
|
|
261
362
|
);
|
|
262
363
|
|
|
263
|
-
const selectList = new SelectList(items,
|
|
364
|
+
const selectList = new SelectList(items, 12, getSelectListTheme());
|
|
264
365
|
selectList.onSelect = (item) => {
|
|
265
366
|
done({ action: "select", email: item.value });
|
|
266
367
|
};
|
|
@@ -271,8 +372,20 @@ async function openAccountSelectionPanel(
|
|
|
271
372
|
render: (width: number) => container.render(width),
|
|
272
373
|
invalidate: () => container.invalidate(),
|
|
273
374
|
handleInput: (data: string) => {
|
|
375
|
+
if (data.toLowerCase() === "n") {
|
|
376
|
+
done({ action: "add" });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const selected = selectList.getSelectedItem();
|
|
380
|
+
if (selected && data.toLowerCase() === "u") {
|
|
381
|
+
done({ action: "refresh", email: selected.value });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (selected && data.toLowerCase() === "r") {
|
|
385
|
+
done({ action: "reauth", email: selected.value });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
274
388
|
if (matchesKey(data, Key.backspace)) {
|
|
275
|
-
const selected = selectList.getSelectedItem();
|
|
276
389
|
if (selected) {
|
|
277
390
|
done({ action: "remove", email: selected.value });
|
|
278
391
|
}
|
|
@@ -284,7 +397,8 @@ async function openAccountSelectionPanel(
|
|
|
284
397
|
});
|
|
285
398
|
}
|
|
286
399
|
|
|
287
|
-
async function
|
|
400
|
+
async function openAccountManagementFlow(
|
|
401
|
+
pi: ExtensionAPI,
|
|
288
402
|
ctx: ExtensionCommandContext,
|
|
289
403
|
accountManager: AccountManager,
|
|
290
404
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
@@ -292,20 +406,42 @@ async function openAccountSelectionFlow(
|
|
|
292
406
|
while (true) {
|
|
293
407
|
const accounts = accountManager.getAccounts();
|
|
294
408
|
if (accounts.length === 0) {
|
|
295
|
-
ctx
|
|
296
|
-
return;
|
|
409
|
+
const identifier = await promptForNewAccountIdentifier(ctx);
|
|
410
|
+
if (!identifier) return;
|
|
411
|
+
await loginAndActivateAccount(pi, ctx, accountManager, identifier);
|
|
412
|
+
await statusController.refreshFor(ctx);
|
|
413
|
+
continue;
|
|
297
414
|
}
|
|
298
415
|
|
|
299
|
-
const result = await
|
|
416
|
+
const result = await openAccountManagementPanel(ctx, accountManager);
|
|
300
417
|
if (!result) return;
|
|
301
418
|
|
|
419
|
+
if (result.action === "add") {
|
|
420
|
+
const identifier = await promptForNewAccountIdentifier(ctx);
|
|
421
|
+
if (!identifier) continue;
|
|
422
|
+
await loginAndActivateAccount(pi, ctx, accountManager, identifier);
|
|
423
|
+
await statusController.refreshFor(ctx);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
302
427
|
if (result.action === "select") {
|
|
303
|
-
accountManager
|
|
304
|
-
ctx.ui.notify(`Now using ${result.email}`, "info");
|
|
428
|
+
await useOrLoginAccount(pi, ctx, accountManager, result.email);
|
|
305
429
|
await statusController.refreshFor(ctx);
|
|
306
430
|
return;
|
|
307
431
|
}
|
|
308
432
|
|
|
433
|
+
if (result.action === "refresh") {
|
|
434
|
+
await refreshSingleAccount(ctx, accountManager, result.email);
|
|
435
|
+
await statusController.refreshFor(ctx);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (result.action === "reauth") {
|
|
440
|
+
await reauthenticateAccount(pi, ctx, accountManager, result.email);
|
|
441
|
+
await statusController.refreshFor(ctx);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
309
445
|
const accountToRemove = accountManager.getAccount(result.email);
|
|
310
446
|
if (!accountToRemove) continue;
|
|
311
447
|
|
|
@@ -325,32 +461,50 @@ async function openAccountSelectionFlow(
|
|
|
325
461
|
}
|
|
326
462
|
}
|
|
327
463
|
|
|
328
|
-
async function
|
|
464
|
+
async function runAccountsSubcommand(
|
|
465
|
+
pi: ExtensionAPI,
|
|
329
466
|
ctx: ExtensionCommandContext,
|
|
330
467
|
accountManager: AccountManager,
|
|
468
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
469
|
+
rest: string,
|
|
331
470
|
): Promise<void> {
|
|
332
471
|
await accountManager.syncImportedOpenAICodexAuth();
|
|
333
472
|
await accountManager.refreshUsageForAllAccounts();
|
|
473
|
+
|
|
474
|
+
if (rest) {
|
|
475
|
+
await useOrLoginAccount(pi, ctx, accountManager, rest);
|
|
476
|
+
await statusController.refreshFor(ctx);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
334
480
|
const accounts = accountManager.getAccounts();
|
|
335
481
|
if (accounts.length === 0) {
|
|
336
|
-
ctx.
|
|
482
|
+
if (!ctx.hasUI) {
|
|
483
|
+
ctx.ui.notify(NO_ACCOUNTS_MESSAGE, "warning");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await openAccountManagementFlow(pi, ctx, accountManager, statusController);
|
|
337
487
|
return;
|
|
338
488
|
}
|
|
339
489
|
|
|
340
490
|
if (!ctx.hasUI) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
ctx.ui.notify(
|
|
344
|
-
`multicodex: accounts=${accounts.length} active=${active} manual=${manual}`,
|
|
345
|
-
"info",
|
|
491
|
+
const lines = accounts.map((account) =>
|
|
492
|
+
formatAccountStatusLine(accountManager, account.email),
|
|
346
493
|
);
|
|
494
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
347
495
|
return;
|
|
348
496
|
}
|
|
349
497
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
498
|
+
await openAccountManagementFlow(pi, ctx, accountManager, statusController);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function runShowSubcommand(
|
|
502
|
+
pi: ExtensionAPI,
|
|
503
|
+
ctx: ExtensionCommandContext,
|
|
504
|
+
accountManager: AccountManager,
|
|
505
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
506
|
+
): Promise<void> {
|
|
507
|
+
await runAccountsSubcommand(pi, ctx, accountManager, statusController, "");
|
|
354
508
|
}
|
|
355
509
|
|
|
356
510
|
async function runFooterSubcommand(
|
|
@@ -374,16 +528,14 @@ async function runRotationSubcommand(
|
|
|
374
528
|
ctx: ExtensionCommandContext,
|
|
375
529
|
): Promise<void> {
|
|
376
530
|
const lines = [
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
531
|
+
"Current policy: manual account first, then untouched accounts, then earliest weekly reset, then random fallback.",
|
|
532
|
+
"If token validation fails before a request starts, MultiCodex skips that account and retries another one.",
|
|
533
|
+
"If a request hits quota or rate limit before any output streams, MultiCodex marks the account on cooldown and retries.",
|
|
534
|
+
"Imported pi auth is merged into the managed pool so duplicate credentials do not consume extra rotation slots.",
|
|
380
535
|
];
|
|
381
536
|
|
|
382
537
|
if (!ctx.hasUI) {
|
|
383
|
-
ctx.ui.notify(
|
|
384
|
-
"rotation: manual->untouched->earliest-weekly-reset->random, cooldown=next-reset-or-1h",
|
|
385
|
-
"info",
|
|
386
|
-
);
|
|
538
|
+
ctx.ui.notify(lines.join(" "), "info");
|
|
387
539
|
return;
|
|
388
540
|
}
|
|
389
541
|
|
|
@@ -412,11 +564,12 @@ async function runVerifySubcommand(
|
|
|
412
564
|
await statusController.loadPreferences(ctx);
|
|
413
565
|
const accounts = accountManager.getAccounts().length;
|
|
414
566
|
const active = accountManager.getActiveAccount()?.email ?? "none";
|
|
415
|
-
const
|
|
567
|
+
const needsReauth = accountManager.getAccountsNeedingReauth().length;
|
|
568
|
+
const ok = storageWritable && settingsWritable && needsReauth === 0;
|
|
416
569
|
|
|
417
570
|
if (!ctx.hasUI) {
|
|
418
571
|
ctx.ui.notify(
|
|
419
|
-
`verify: ${ok ? "PASS" : "WARN"} storage=${storageWritable ? "ok" : "fail"} settings=${settingsWritable ? "ok" : "fail"} accounts=${accounts} active=${active} authImport=${authImported ? "updated" : "unchanged"}`,
|
|
572
|
+
`verify: ${ok ? "PASS" : "WARN"} storage=${storageWritable ? "ok" : "fail"} settings=${settingsWritable ? "ok" : "fail"} accounts=${accounts} active=${active} authImport=${authImported ? "updated" : "unchanged"} needsReauth=${needsReauth}`,
|
|
420
573
|
ok ? "info" : "warning",
|
|
421
574
|
);
|
|
422
575
|
return;
|
|
@@ -427,6 +580,7 @@ async function runVerifySubcommand(
|
|
|
427
580
|
`settings directory writable: ${settingsWritable ? "yes" : "no"}`,
|
|
428
581
|
`managed accounts: ${accounts}`,
|
|
429
582
|
`active account: ${active}`,
|
|
583
|
+
`accounts needing re-authentication: ${needsReauth}`,
|
|
430
584
|
`auth import changed state: ${authImported ? "yes" : "no"}`,
|
|
431
585
|
];
|
|
432
586
|
await ctx.ui.select(`MultiCodex Verify (${ok ? "PASS" : "WARN"})`, lines);
|
|
@@ -519,7 +673,7 @@ function runHelpSubcommand(ctx: ExtensionCommandContext): void {
|
|
|
519
673
|
ctx.ui.notify(HELP_TEXT, "info");
|
|
520
674
|
}
|
|
521
675
|
|
|
522
|
-
async function
|
|
676
|
+
async function runRefreshSubcommand(
|
|
523
677
|
pi: ExtensionAPI,
|
|
524
678
|
ctx: ExtensionCommandContext,
|
|
525
679
|
accountManager: AccountManager,
|
|
@@ -527,22 +681,42 @@ async function runUseSubcommand(
|
|
|
527
681
|
rest: string,
|
|
528
682
|
): Promise<void> {
|
|
529
683
|
await accountManager.syncImportedOpenAICodexAuth();
|
|
684
|
+
if (!rest || rest === "all") {
|
|
685
|
+
if (!ctx.hasUI || rest === "all") {
|
|
686
|
+
await refreshAllAccounts(ctx, accountManager);
|
|
687
|
+
await statusController.refreshFor(ctx);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await openAccountManagementFlow(pi, ctx, accountManager, statusController);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
await refreshSingleAccount(ctx, accountManager, rest);
|
|
694
|
+
await statusController.refreshFor(ctx);
|
|
695
|
+
}
|
|
530
696
|
|
|
697
|
+
async function runReauthSubcommand(
|
|
698
|
+
pi: ExtensionAPI,
|
|
699
|
+
ctx: ExtensionCommandContext,
|
|
700
|
+
accountManager: AccountManager,
|
|
701
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
702
|
+
rest: string,
|
|
703
|
+
): Promise<void> {
|
|
704
|
+
await accountManager.syncImportedOpenAICodexAuth();
|
|
531
705
|
if (rest) {
|
|
532
|
-
await
|
|
706
|
+
await reauthenticateAccount(pi, ctx, accountManager, rest);
|
|
533
707
|
await statusController.refreshFor(ctx);
|
|
534
708
|
return;
|
|
535
709
|
}
|
|
536
|
-
|
|
537
710
|
if (!ctx.hasUI) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
"warning"
|
|
541
|
-
|
|
711
|
+
const active = accountManager.getActiveAccount();
|
|
712
|
+
if (!active) {
|
|
713
|
+
ctx.ui.notify(NO_ACCOUNTS_MESSAGE, "warning");
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
await reauthenticateAccount(pi, ctx, accountManager, active.email);
|
|
542
717
|
return;
|
|
543
718
|
}
|
|
544
|
-
|
|
545
|
-
await openAccountSelectionFlow(ctx, accountManager, statusController);
|
|
719
|
+
await openAccountManagementFlow(pi, ctx, accountManager, statusController);
|
|
546
720
|
}
|
|
547
721
|
|
|
548
722
|
async function runSubcommand(
|
|
@@ -553,12 +727,26 @@ async function runSubcommand(
|
|
|
553
727
|
accountManager: AccountManager,
|
|
554
728
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
555
729
|
): Promise<void> {
|
|
730
|
+
if (subcommand === "accounts" || subcommand === "use") {
|
|
731
|
+
await runAccountsSubcommand(
|
|
732
|
+
pi,
|
|
733
|
+
ctx,
|
|
734
|
+
accountManager,
|
|
735
|
+
statusController,
|
|
736
|
+
rest,
|
|
737
|
+
);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
556
740
|
if (subcommand === "show") {
|
|
557
|
-
await runShowSubcommand(ctx, accountManager);
|
|
741
|
+
await runShowSubcommand(pi, ctx, accountManager, statusController);
|
|
558
742
|
return;
|
|
559
743
|
}
|
|
560
|
-
if (subcommand === "
|
|
561
|
-
await
|
|
744
|
+
if (subcommand === "refresh") {
|
|
745
|
+
await runRefreshSubcommand(pi, ctx, accountManager, statusController, rest);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (subcommand === "reauth") {
|
|
749
|
+
await runReauthSubcommand(pi, ctx, accountManager, statusController, rest);
|
|
562
750
|
return;
|
|
563
751
|
}
|
|
564
752
|
if (subcommand === "footer") {
|
|
@@ -592,8 +780,9 @@ async function openMainPanel(
|
|
|
592
780
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
593
781
|
): Promise<void> {
|
|
594
782
|
const actions = [
|
|
595
|
-
"
|
|
596
|
-
"
|
|
783
|
+
"accounts: inspect, select, refresh, re-authenticate, add, or remove managed account",
|
|
784
|
+
"refresh: force a health and usage refresh",
|
|
785
|
+
"reauth: re-authenticate an account",
|
|
597
786
|
"footer: footer settings panel",
|
|
598
787
|
"rotation: current rotation behavior",
|
|
599
788
|
"verify: runtime health checks",
|
|
@@ -626,7 +815,8 @@ export function registerCommands(
|
|
|
626
815
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
627
816
|
): void {
|
|
628
817
|
pi.registerCommand("multicodex", {
|
|
629
|
-
description:
|
|
818
|
+
description:
|
|
819
|
+
"Manage MultiCodex accounts, health, rotation, and footer settings",
|
|
630
820
|
getArgumentCompletions: (argumentPrefix: string) =>
|
|
631
821
|
getCommandCompletions(argumentPrefix, accountManager),
|
|
632
822
|
handler: async (
|
package/extension.ts
CHANGED
|
@@ -28,6 +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
|
+
accountManager.resetSessionWarnings();
|
|
31
32
|
handleSessionStart(accountManager, (msg) => ctx.ui.notify(msg, "warning"));
|
|
32
33
|
statusController.startAutoRefresh();
|
|
33
34
|
void (async () => {
|
|
@@ -41,6 +42,7 @@ export default function multicodexExtension(pi: ExtensionAPI) {
|
|
|
41
42
|
(event: { reason?: string }, ctx: ExtensionContext) => {
|
|
42
43
|
lastContext = ctx;
|
|
43
44
|
if (event.reason === "new") {
|
|
45
|
+
accountManager.resetSessionWarnings();
|
|
44
46
|
handleNewSessionSwitch(accountManager, (msg) =>
|
|
45
47
|
ctx.ui.notify(msg, "warning"),
|
|
46
48
|
);
|
package/package.json
CHANGED
package/stream-wrapper.ts
CHANGED
|
@@ -64,7 +64,20 @@ export function createStreamWrapper(
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
let token: string;
|
|
68
|
+
try {
|
|
69
|
+
token = await accountManager.ensureValidToken(account);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
accountManager.notifyRotationSkipForAuthFailure(account, error);
|
|
72
|
+
if (usingManual) {
|
|
73
|
+
accountManager.clearManualAccount();
|
|
74
|
+
}
|
|
75
|
+
excludedEmails.add(account.email);
|
|
76
|
+
if (attempt < MAX_ROTATION_RETRIES) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
68
81
|
const abortController = createLinkedAbortController(options?.signal);
|
|
69
82
|
|
|
70
83
|
const internalModel: Model<"openai-codex-responses"> = {
|