opencode-copilot-account-switcher 0.13.2 → 0.13.4
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/dist/codex-invalid-account.d.ts +32 -0
- package/dist/codex-invalid-account.js +106 -0
- package/dist/codex-oauth.d.ts +26 -1
- package/dist/codex-oauth.js +28 -0
- package/dist/codex-status-command.js +75 -22
- package/dist/codex-status-fetcher.d.ts +5 -0
- package/dist/codex-status-fetcher.js +57 -4
- package/dist/codex-store.d.ts +1 -0
- package/dist/codex-store.js +2 -0
- package/dist/menu-runtime.d.ts +1 -0
- package/dist/plugin-hooks.js +4 -2
- package/dist/providers/codex-menu-adapter.js +57 -1
- package/dist/ui/menu.d.ts +1 -0
- package/dist/ui/menu.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CodexAccountEntry, CodexStoreFile } from "./codex-store.js";
|
|
2
|
+
export type CodexRecoveryCandidate = {
|
|
3
|
+
name: string;
|
|
4
|
+
entry: CodexAccountEntry;
|
|
5
|
+
};
|
|
6
|
+
export type CodexSetAuthInput = {
|
|
7
|
+
path: {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
body: {
|
|
11
|
+
type: "oauth";
|
|
12
|
+
refresh?: string;
|
|
13
|
+
access?: string;
|
|
14
|
+
expires?: number;
|
|
15
|
+
accountId?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type RecoverInvalidCodexAccountResult = {
|
|
19
|
+
removed: string;
|
|
20
|
+
replacement?: string;
|
|
21
|
+
switched: boolean;
|
|
22
|
+
weekRecoveryOnly: boolean;
|
|
23
|
+
noCandidates: boolean;
|
|
24
|
+
store: CodexStoreFile;
|
|
25
|
+
};
|
|
26
|
+
export declare function getCodexDisplayName(entry: CodexAccountEntry | undefined, fallbackName: string): string;
|
|
27
|
+
export declare function sortCodexRecoveryCandidates(candidates: CodexRecoveryCandidate[]): CodexRecoveryCandidate[];
|
|
28
|
+
export declare function recoverInvalidCodexAccount(input: {
|
|
29
|
+
store: CodexStoreFile;
|
|
30
|
+
invalidAccountName: string;
|
|
31
|
+
setAuth?: (next: CodexSetAuthInput) => Promise<unknown>;
|
|
32
|
+
}): Promise<RecoverInvalidCodexAccountResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function pickPositiveNumber(value) {
|
|
2
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
3
|
+
return 0;
|
|
4
|
+
return value > 0 ? value : 0;
|
|
5
|
+
}
|
|
6
|
+
function pickFiniteNumber(value) {
|
|
7
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
8
|
+
return undefined;
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function getWeekRemaining(entry) {
|
|
12
|
+
return pickPositiveNumber(entry.snapshot?.usageWeek?.remaining);
|
|
13
|
+
}
|
|
14
|
+
function get5hRemaining(entry) {
|
|
15
|
+
return pickPositiveNumber(entry.snapshot?.usage5h?.remaining);
|
|
16
|
+
}
|
|
17
|
+
function compareResetAt(a, b) {
|
|
18
|
+
const aMissing = a === undefined;
|
|
19
|
+
const bMissing = b === undefined;
|
|
20
|
+
if (aMissing && bMissing)
|
|
21
|
+
return 0;
|
|
22
|
+
if (aMissing)
|
|
23
|
+
return 1;
|
|
24
|
+
if (bMissing)
|
|
25
|
+
return -1;
|
|
26
|
+
if (a === b)
|
|
27
|
+
return 0;
|
|
28
|
+
return a < b ? -1 : 1;
|
|
29
|
+
}
|
|
30
|
+
export function getCodexDisplayName(entry, fallbackName) {
|
|
31
|
+
return entry?.workspaceName
|
|
32
|
+
?? entry?.name
|
|
33
|
+
?? entry?.email
|
|
34
|
+
?? entry?.accountId
|
|
35
|
+
?? fallbackName;
|
|
36
|
+
}
|
|
37
|
+
export function sortCodexRecoveryCandidates(candidates) {
|
|
38
|
+
const withIndex = candidates.map((candidate, index) => ({ candidate, index }));
|
|
39
|
+
const weekPositive = withIndex.filter(({ candidate }) => getWeekRemaining(candidate.entry) > 0);
|
|
40
|
+
const pool = weekPositive.length > 0 ? weekPositive : withIndex;
|
|
41
|
+
const has5hPositiveInPool = pool.some(({ candidate }) => get5hRemaining(candidate.entry) > 0);
|
|
42
|
+
return pool
|
|
43
|
+
.slice()
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
if (weekPositive.length > 0 && has5hPositiveInPool) {
|
|
46
|
+
const a5h = get5hRemaining(a.candidate.entry);
|
|
47
|
+
const b5h = get5hRemaining(b.candidate.entry);
|
|
48
|
+
if (a5h > 0 && b5h <= 0)
|
|
49
|
+
return -1;
|
|
50
|
+
if (a5h <= 0 && b5h > 0)
|
|
51
|
+
return 1;
|
|
52
|
+
const by5hResetAt = compareResetAt(pickFiniteNumber(a.candidate.entry.snapshot?.usage5h?.resetAt), pickFiniteNumber(b.candidate.entry.snapshot?.usage5h?.resetAt));
|
|
53
|
+
if (by5hResetAt !== 0)
|
|
54
|
+
return by5hResetAt;
|
|
55
|
+
}
|
|
56
|
+
const byWeekResetAt = compareResetAt(pickFiniteNumber(a.candidate.entry.snapshot?.usageWeek?.resetAt), pickFiniteNumber(b.candidate.entry.snapshot?.usageWeek?.resetAt));
|
|
57
|
+
if (byWeekResetAt !== 0)
|
|
58
|
+
return byWeekResetAt;
|
|
59
|
+
return a.index - b.index;
|
|
60
|
+
})
|
|
61
|
+
.map(({ candidate }) => candidate);
|
|
62
|
+
}
|
|
63
|
+
export async function recoverInvalidCodexAccount(input) {
|
|
64
|
+
const store = {
|
|
65
|
+
...input.store,
|
|
66
|
+
accounts: { ...input.store.accounts },
|
|
67
|
+
};
|
|
68
|
+
delete store.accounts[input.invalidAccountName];
|
|
69
|
+
const candidates = Object.entries(store.accounts).map(([name, entry]) => ({
|
|
70
|
+
name,
|
|
71
|
+
entry,
|
|
72
|
+
}));
|
|
73
|
+
const sorted = sortCodexRecoveryCandidates(candidates);
|
|
74
|
+
const replacement = sorted[0];
|
|
75
|
+
const noCandidates = !replacement;
|
|
76
|
+
const switched = Boolean(replacement);
|
|
77
|
+
const weekRecoveryOnly = Boolean(replacement
|
|
78
|
+
&& getWeekRemaining(replacement.entry) > 0
|
|
79
|
+
&& get5hRemaining(replacement.entry) <= 0);
|
|
80
|
+
if (replacement) {
|
|
81
|
+
store.active = replacement.name;
|
|
82
|
+
}
|
|
83
|
+
else if (store.active === input.invalidAccountName) {
|
|
84
|
+
delete store.active;
|
|
85
|
+
}
|
|
86
|
+
if (replacement && input.setAuth) {
|
|
87
|
+
await input.setAuth({
|
|
88
|
+
path: { id: "openai" },
|
|
89
|
+
body: {
|
|
90
|
+
type: "oauth",
|
|
91
|
+
refresh: replacement.entry.refresh,
|
|
92
|
+
access: replacement.entry.access,
|
|
93
|
+
expires: replacement.entry.expires,
|
|
94
|
+
accountId: replacement.entry.accountId,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
removed: input.invalidAccountName,
|
|
100
|
+
replacement: replacement?.name,
|
|
101
|
+
switched,
|
|
102
|
+
weekRecoveryOnly,
|
|
103
|
+
noCandidates,
|
|
104
|
+
store,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/codex-oauth.d.ts
CHANGED
|
@@ -7,11 +7,33 @@ export type TokenResponse = {
|
|
|
7
7
|
export type IdTokenClaims = {
|
|
8
8
|
chatgpt_account_id?: string;
|
|
9
9
|
organizations?: Array<{
|
|
10
|
-
id
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
display_name?: string;
|
|
13
|
+
workspace_name?: string;
|
|
14
|
+
slug?: string;
|
|
11
15
|
}>;
|
|
16
|
+
organization?: {
|
|
17
|
+
id?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
display_name?: string;
|
|
20
|
+
workspace_name?: string;
|
|
21
|
+
slug?: string;
|
|
22
|
+
};
|
|
23
|
+
workspace?: {
|
|
24
|
+
id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
display_name?: string;
|
|
27
|
+
workspace_name?: string;
|
|
28
|
+
slug?: string;
|
|
29
|
+
};
|
|
30
|
+
workspace_name?: string;
|
|
12
31
|
email?: string;
|
|
13
32
|
"https://api.openai.com/auth"?: {
|
|
14
33
|
chatgpt_account_id?: string;
|
|
34
|
+
workspace_name?: string;
|
|
35
|
+
workspace_id?: string;
|
|
36
|
+
organization_id?: string;
|
|
15
37
|
};
|
|
16
38
|
};
|
|
17
39
|
export type CodexOAuthAccount = {
|
|
@@ -20,6 +42,7 @@ export type CodexOAuthAccount = {
|
|
|
20
42
|
expires?: number;
|
|
21
43
|
accountId?: string;
|
|
22
44
|
email?: string;
|
|
45
|
+
workspaceName?: string;
|
|
23
46
|
};
|
|
24
47
|
type OAuthMode = "browser" | "headless";
|
|
25
48
|
type RunCodexOAuthInput = {
|
|
@@ -35,5 +58,7 @@ type RunCodexOAuthInput = {
|
|
|
35
58
|
export declare function parseJwtClaims(token: string): IdTokenClaims | undefined;
|
|
36
59
|
export declare function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined;
|
|
37
60
|
export declare function extractAccountId(tokens: TokenResponse): string | undefined;
|
|
61
|
+
export declare function extractWorkspaceNameFromClaims(claims: IdTokenClaims): string | undefined;
|
|
62
|
+
export declare function extractWorkspaceName(tokens: TokenResponse): string | undefined;
|
|
38
63
|
export declare function runCodexOAuth(input?: RunCodexOAuthInput): Promise<CodexOAuthAccount | undefined>;
|
|
39
64
|
export {};
|
package/dist/codex-oauth.js
CHANGED
|
@@ -82,6 +82,32 @@ export function extractAccountId(tokens) {
|
|
|
82
82
|
const claims = parseJwtClaims(tokens.access_token);
|
|
83
83
|
return claims ? extractAccountIdFromClaims(claims) : undefined;
|
|
84
84
|
}
|
|
85
|
+
function pickWorkspaceLikeLabel(input) {
|
|
86
|
+
if (!input)
|
|
87
|
+
return undefined;
|
|
88
|
+
return input.workspace_name ?? input.display_name ?? input.name ?? input.slug ?? input.id;
|
|
89
|
+
}
|
|
90
|
+
export function extractWorkspaceNameFromClaims(claims) {
|
|
91
|
+
return (claims.workspace_name
|
|
92
|
+
|| claims["https://api.openai.com/auth"]?.workspace_name
|
|
93
|
+
|| claims["https://api.openai.com/auth"]?.workspace_id
|
|
94
|
+
|| claims["https://api.openai.com/auth"]?.organization_id
|
|
95
|
+
|| pickWorkspaceLikeLabel(claims.workspace)
|
|
96
|
+
|| pickWorkspaceLikeLabel(claims.organization)
|
|
97
|
+
|| pickWorkspaceLikeLabel(claims.organizations?.[0]));
|
|
98
|
+
}
|
|
99
|
+
export function extractWorkspaceName(tokens) {
|
|
100
|
+
if (tokens.id_token) {
|
|
101
|
+
const claims = parseJwtClaims(tokens.id_token);
|
|
102
|
+
const workspaceName = claims && extractWorkspaceNameFromClaims(claims);
|
|
103
|
+
if (workspaceName)
|
|
104
|
+
return workspaceName;
|
|
105
|
+
}
|
|
106
|
+
if (!tokens.access_token)
|
|
107
|
+
return undefined;
|
|
108
|
+
const claims = parseJwtClaims(tokens.access_token);
|
|
109
|
+
return claims ? extractWorkspaceNameFromClaims(claims) : undefined;
|
|
110
|
+
}
|
|
85
111
|
function extractEmail(tokens) {
|
|
86
112
|
if (tokens.id_token) {
|
|
87
113
|
const claims = parseJwtClaims(tokens.id_token);
|
|
@@ -291,12 +317,14 @@ function normalizeTokens(tokens, now) {
|
|
|
291
317
|
const access = tokens.access_token;
|
|
292
318
|
if (!refresh && !access)
|
|
293
319
|
return undefined;
|
|
320
|
+
const workspaceName = extractWorkspaceName(tokens);
|
|
294
321
|
return {
|
|
295
322
|
refresh,
|
|
296
323
|
access,
|
|
297
324
|
expires: now() + (tokens.expires_in ?? 3600) * 1000,
|
|
298
325
|
accountId: extractAccountId(tokens),
|
|
299
326
|
email: extractEmail(tokens),
|
|
327
|
+
...(workspaceName ? { workspaceName } : {}),
|
|
300
328
|
};
|
|
301
329
|
}
|
|
302
330
|
export async function runCodexOAuth(input = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveCodexAuthSource } from "./codex-auth-source.js";
|
|
2
2
|
import { fetchCodexStatus } from "./codex-status-fetcher.js";
|
|
3
3
|
import { getActiveCodexAccount, normalizeCodexStore, readCodexStore, writeCodexStore, } from "./codex-store.js";
|
|
4
|
+
import { getCodexDisplayName, recoverInvalidCodexAccount } from "./codex-invalid-account.js";
|
|
4
5
|
import { readAuth } from "./store.js";
|
|
5
6
|
export class CodexStatusCommandHandledError extends Error {
|
|
6
7
|
constructor() {
|
|
@@ -44,6 +45,12 @@ function ratio(remaining, entitlement) {
|
|
|
44
45
|
function value(value) {
|
|
45
46
|
return value === undefined ? "n/a" : String(value);
|
|
46
47
|
}
|
|
48
|
+
function pickWorkspaceLabel(input) {
|
|
49
|
+
return input.workspaceName
|
|
50
|
+
?? input.name
|
|
51
|
+
?? input.email
|
|
52
|
+
?? input.accountId;
|
|
53
|
+
}
|
|
47
54
|
function renderWindow(label, window) {
|
|
48
55
|
if (window.entitlement === 100 && window.remaining !== undefined) {
|
|
49
56
|
return `${label}: ${window.remaining}% left`;
|
|
@@ -51,16 +58,16 @@ function renderWindow(label, window) {
|
|
|
51
58
|
return `${label}: ${ratio(window.remaining, window.entitlement)}`;
|
|
52
59
|
}
|
|
53
60
|
function renderStatus(status) {
|
|
61
|
+
const identity = status.identity;
|
|
54
62
|
return [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
`账号: ${value(identity.accountId ?? identity.email)}`,
|
|
64
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
65
|
+
workspaceName: identity.workspaceName,
|
|
66
|
+
email: identity.email,
|
|
67
|
+
accountId: identity.accountId,
|
|
68
|
+
}))}`,
|
|
61
69
|
renderWindow("5h", status.windows.primary),
|
|
62
70
|
renderWindow("week", status.windows.secondary),
|
|
63
|
-
`credits: ${ratio(status.credits.remaining, status.credits.total)}`,
|
|
64
71
|
].join("\n");
|
|
65
72
|
}
|
|
66
73
|
function renderCachedStatus(store) {
|
|
@@ -68,14 +75,15 @@ function renderCachedStatus(store) {
|
|
|
68
75
|
const entry = active?.entry;
|
|
69
76
|
const snapshot = entry?.snapshot;
|
|
70
77
|
return [
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
`账号: ${value(entry?.accountId ?? active?.name ?? entry?.email)}`,
|
|
79
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
80
|
+
workspaceName: entry?.workspaceName,
|
|
81
|
+
name: entry?.name ?? active?.name,
|
|
82
|
+
email: entry?.email,
|
|
83
|
+
accountId: entry?.accountId,
|
|
84
|
+
}))}`,
|
|
76
85
|
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
77
|
-
"week
|
|
78
|
-
"credits: n/a",
|
|
86
|
+
renderWindow("week", snapshot?.usageWeek ?? {}),
|
|
79
87
|
].join("\n");
|
|
80
88
|
}
|
|
81
89
|
function getCachedAccountForSource(store, input) {
|
|
@@ -96,14 +104,15 @@ function renderCachedStatusForAccount(store, input) {
|
|
|
96
104
|
const entry = active?.entry;
|
|
97
105
|
const snapshot = entry?.snapshot;
|
|
98
106
|
return [
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
`账号: ${value(entry?.accountId ?? active?.name ?? entry?.email)}`,
|
|
108
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
109
|
+
workspaceName: entry?.workspaceName,
|
|
110
|
+
name: entry?.name ?? active?.name,
|
|
111
|
+
email: entry?.email,
|
|
112
|
+
accountId: entry?.accountId,
|
|
113
|
+
}))}`,
|
|
104
114
|
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
105
|
-
"week
|
|
106
|
-
"credits: n/a",
|
|
115
|
+
renderWindow("week", snapshot?.usageWeek ?? {}),
|
|
107
116
|
].join("\n");
|
|
108
117
|
}
|
|
109
118
|
function hasCachedStore(store) {
|
|
@@ -230,12 +239,55 @@ export async function handleCodexStatusCommand(input) {
|
|
|
230
239
|
},
|
|
231
240
|
}));
|
|
232
241
|
if (!fetched.ok) {
|
|
242
|
+
if (fetched.error.kind === "invalid_account") {
|
|
243
|
+
const currentRaw = await readStore().catch(() => ({}));
|
|
244
|
+
const currentStore = normalizeCodexStore(currentRaw);
|
|
245
|
+
const invalid = getCachedAccountForSource(currentStore, { accountId: source.accountId });
|
|
246
|
+
const invalidName = invalid?.name ?? currentStore.active;
|
|
247
|
+
if (invalidName && currentStore.accounts[invalidName]) {
|
|
248
|
+
const recovered = await recoverInvalidCodexAccount({
|
|
249
|
+
store: currentStore,
|
|
250
|
+
invalidAccountName: invalidName,
|
|
251
|
+
setAuth: input.client?.auth?.set
|
|
252
|
+
? async (next) => {
|
|
253
|
+
const authClient = input.client?.auth;
|
|
254
|
+
const setAuth = input.client?.auth?.set;
|
|
255
|
+
if (!setAuth)
|
|
256
|
+
return;
|
|
257
|
+
await setAuth.call(authClient, next);
|
|
258
|
+
}
|
|
259
|
+
: undefined,
|
|
260
|
+
});
|
|
261
|
+
await writeStore(recovered.store);
|
|
262
|
+
const removedDisplay = getCodexDisplayName(invalid?.entry, recovered.removed);
|
|
263
|
+
const messageLines = [`无效账号${removedDisplay}已移除,请及时检查核对`];
|
|
264
|
+
if (recovered.replacement) {
|
|
265
|
+
const replacementEntry = recovered.store.accounts[recovered.replacement];
|
|
266
|
+
const replacementDisplay = getCodexDisplayName(replacementEntry, recovered.replacement);
|
|
267
|
+
messageLines.push(`已切换到${replacementDisplay}`);
|
|
268
|
+
if (recovered.weekRecoveryOnly) {
|
|
269
|
+
messageLines.push("请检查账号状态");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
await showToast({
|
|
273
|
+
client: input.client,
|
|
274
|
+
message: messageLines.join("\n"),
|
|
275
|
+
variant: "warning",
|
|
276
|
+
});
|
|
277
|
+
throw new CodexStatusCommandHandledError();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
233
280
|
const cachedRaw = await readStore().catch(() => ({}));
|
|
234
281
|
const cached = normalizeCodexStore(cachedRaw);
|
|
235
282
|
if (hasCachedStore(cached)) {
|
|
236
283
|
await showToast({
|
|
237
284
|
client: input.client,
|
|
238
|
-
message: `Codex status fetch failed
|
|
285
|
+
message: `Codex status fetch failed: ${fetched.error.message}`,
|
|
286
|
+
variant: "warning",
|
|
287
|
+
});
|
|
288
|
+
await showToast({
|
|
289
|
+
client: input.client,
|
|
290
|
+
message: renderCachedStatusForAccount(cached, { accountId: source.accountId }),
|
|
239
291
|
variant: "warning",
|
|
240
292
|
});
|
|
241
293
|
}
|
|
@@ -279,6 +331,7 @@ export async function handleCodexStatusCommand(input) {
|
|
|
279
331
|
providerId: previousEntry.providerId ?? "codex",
|
|
280
332
|
accountId: fetched.status.identity.accountId ?? previousEntry.accountId ?? source.accountId,
|
|
281
333
|
email: fetched.status.identity.email ?? previousEntry.email,
|
|
334
|
+
workspaceName: fetched.status.identity.workspaceName ?? previousEntry.workspaceName,
|
|
282
335
|
lastUsed: fetched.status.updatedAt,
|
|
283
336
|
snapshot: {
|
|
284
337
|
...(previousEntry.snapshot ?? {}),
|
|
@@ -23,6 +23,10 @@ export type CodexStatusSnapshot = {
|
|
|
23
23
|
updatedAt: number;
|
|
24
24
|
};
|
|
25
25
|
export type CodexStatusError = {
|
|
26
|
+
kind: "invalid_account";
|
|
27
|
+
status: 400;
|
|
28
|
+
message: string;
|
|
29
|
+
} | {
|
|
26
30
|
kind: "rate_limited";
|
|
27
31
|
status: 429;
|
|
28
32
|
message: string;
|
|
@@ -62,5 +66,6 @@ export declare function fetchCodexStatus(input: {
|
|
|
62
66
|
accountId?: string;
|
|
63
67
|
fetchImpl?: typeof globalThis.fetch;
|
|
64
68
|
now?: () => number;
|
|
69
|
+
timeoutMs?: number;
|
|
65
70
|
refreshTokens?: (oauth: OpenAIOAuthAuth) => Promise<OpenAIOAuthAuth | undefined>;
|
|
66
71
|
}): Promise<CodexStatusFetcherResult>;
|
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
|
|
2
|
+
const CODEX_USAGE_TIMEOUT_MS = 15_000;
|
|
3
|
+
function isInvalidAccountRefreshError(error) {
|
|
4
|
+
if (!error || typeof error !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const value = error;
|
|
7
|
+
return value.kind === "invalid_account"
|
|
8
|
+
&& value.status === 400
|
|
9
|
+
&& typeof value.message === "string"
|
|
10
|
+
&& value.message.length > 0;
|
|
11
|
+
}
|
|
12
|
+
function readErrorStatus(error) {
|
|
13
|
+
if (!error || typeof error !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
const value = error.status;
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
function readErrorMessage(error) {
|
|
19
|
+
if (error instanceof Error)
|
|
20
|
+
return error.message;
|
|
21
|
+
if (typeof error === "string")
|
|
22
|
+
return error;
|
|
23
|
+
if (error && typeof error === "object") {
|
|
24
|
+
const message = error.message;
|
|
25
|
+
if (typeof message === "string" && message.length > 0)
|
|
26
|
+
return message;
|
|
27
|
+
}
|
|
28
|
+
return String(error);
|
|
29
|
+
}
|
|
30
|
+
function mapRefreshTokenError(error) {
|
|
31
|
+
if (isInvalidAccountRefreshError(error))
|
|
32
|
+
return error;
|
|
33
|
+
const message = readErrorMessage(error);
|
|
34
|
+
const status = readErrorStatus(error);
|
|
35
|
+
const hasRefresh400Message = /refresh/i.test(message) && /\b400\b/.test(message);
|
|
36
|
+
if (status === 400 || hasRefresh400Message) {
|
|
37
|
+
return {
|
|
38
|
+
kind: "invalid_account",
|
|
39
|
+
status: 400,
|
|
40
|
+
message,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: "network_error",
|
|
45
|
+
message,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
2
48
|
function asRecord(input) {
|
|
3
49
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
4
50
|
return undefined;
|
|
@@ -113,24 +159,30 @@ async function requestUsage(input) {
|
|
|
113
159
|
access: input.oauth.access,
|
|
114
160
|
accountId: input.accountId,
|
|
115
161
|
}),
|
|
162
|
+
signal: input.signal,
|
|
116
163
|
});
|
|
117
164
|
}
|
|
118
165
|
export async function fetchCodexStatus(input) {
|
|
119
166
|
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
|
|
120
167
|
const now = input.now ?? Date.now;
|
|
121
168
|
const explicitAccountId = input.accountId;
|
|
169
|
+
const timeoutMs = input.timeoutMs ?? CODEX_USAGE_TIMEOUT_MS;
|
|
122
170
|
let oauth = input.oauth;
|
|
123
171
|
let authPatch;
|
|
124
172
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
125
173
|
let response;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
126
176
|
try {
|
|
127
177
|
response = await requestUsage({
|
|
128
178
|
oauth,
|
|
129
179
|
accountId: explicitAccountId ?? oauth.accountId,
|
|
130
180
|
fetchImpl,
|
|
181
|
+
signal: controller.signal,
|
|
131
182
|
});
|
|
132
183
|
}
|
|
133
184
|
catch (error) {
|
|
185
|
+
clearTimeout(timeout);
|
|
134
186
|
if (isTimeoutError(error)) {
|
|
135
187
|
return {
|
|
136
188
|
ok: false,
|
|
@@ -148,18 +200,19 @@ export async function fetchCodexStatus(input) {
|
|
|
148
200
|
},
|
|
149
201
|
};
|
|
150
202
|
}
|
|
203
|
+
finally {
|
|
204
|
+
clearTimeout(timeout);
|
|
205
|
+
}
|
|
151
206
|
if (response.status === 401 && attempt === 0 && input.refreshTokens) {
|
|
152
207
|
let refreshed;
|
|
153
208
|
try {
|
|
154
209
|
refreshed = await input.refreshTokens(oauth);
|
|
155
210
|
}
|
|
156
211
|
catch (error) {
|
|
212
|
+
const mappedError = mapRefreshTokenError(error);
|
|
157
213
|
return {
|
|
158
214
|
ok: false,
|
|
159
|
-
error:
|
|
160
|
-
kind: "network_error",
|
|
161
|
-
message: error instanceof Error ? error.message : String(error),
|
|
162
|
-
},
|
|
215
|
+
error: mappedError,
|
|
163
216
|
};
|
|
164
217
|
}
|
|
165
218
|
if (!refreshed || !refreshed.access) {
|
package/dist/codex-store.d.ts
CHANGED
package/dist/codex-store.js
CHANGED
|
@@ -60,6 +60,8 @@ function pickEntry(input) {
|
|
|
60
60
|
next.name = pickString(source.name);
|
|
61
61
|
if (pickString(source.providerId))
|
|
62
62
|
next.providerId = pickString(source.providerId);
|
|
63
|
+
if (pickString(source.workspaceName))
|
|
64
|
+
next.workspaceName = pickString(source.workspaceName);
|
|
63
65
|
if (pickString(source.refresh))
|
|
64
66
|
next.refresh = pickString(source.refresh);
|
|
65
67
|
if (pickString(source.access))
|
package/dist/menu-runtime.d.ts
CHANGED
package/dist/plugin-hooks.js
CHANGED
|
@@ -421,6 +421,8 @@ function pruneTouchWriteCache(input) {
|
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
export function buildPluginHooks(input) {
|
|
424
|
+
const authProvider = input.auth.provider ?? COPILOT_PROVIDER_DESCRIPTOR.providerIDs[0] ?? "github-copilot";
|
|
425
|
+
const enableCopilotAuthLoader = isCopilotProviderID(authProvider);
|
|
424
426
|
const compactionLoopSafetyBypass = createCompactionLoopSafetyBypass();
|
|
425
427
|
const loadStore = input.loadStore ?? readStoreSafe;
|
|
426
428
|
const loadStoreSync = input.loadStoreSync ?? readStoreSafeSync;
|
|
@@ -1121,9 +1123,9 @@ export function buildPluginHooks(input) {
|
|
|
1121
1123
|
return {
|
|
1122
1124
|
auth: {
|
|
1123
1125
|
...input.auth,
|
|
1124
|
-
provider:
|
|
1126
|
+
provider: authProvider,
|
|
1125
1127
|
methods: input.auth.methods,
|
|
1126
|
-
loader,
|
|
1128
|
+
loader: enableCopilotAuthLoader ? loader : undefined,
|
|
1127
1129
|
},
|
|
1128
1130
|
config: async (config) => {
|
|
1129
1131
|
if (!config.command)
|
|
@@ -3,6 +3,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
3
3
|
import { fetchCodexStatus } from "../codex-status-fetcher.js";
|
|
4
4
|
import { runCodexOAuth } from "../codex-oauth.js";
|
|
5
5
|
import { getActiveCodexAccount, readCodexStore, writeCodexStore, } from "../codex-store.js";
|
|
6
|
+
import { recoverInvalidCodexAccount } from "../codex-invalid-account.js";
|
|
6
7
|
import { readAuth } from "../store.js";
|
|
7
8
|
function pickName(input) {
|
|
8
9
|
const accountId = input.accountId?.trim();
|
|
@@ -44,6 +45,13 @@ function toMenuQuota(entry) {
|
|
|
44
45
|
completions: undefined,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
function withoutRecoveryWarning(snapshot) {
|
|
49
|
+
if (!snapshot)
|
|
50
|
+
return undefined;
|
|
51
|
+
const next = { ...snapshot };
|
|
52
|
+
delete next.recoveryWarning;
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
47
55
|
async function promptText(message) {
|
|
48
56
|
const rl = createInterface({ input, output });
|
|
49
57
|
try {
|
|
@@ -66,8 +74,11 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
66
74
|
const authorizeOpenAIOAuth = inputDeps.runCodexOAuth ?? runCodexOAuth;
|
|
67
75
|
const refreshSnapshots = async (store) => {
|
|
68
76
|
const names = Object.keys(store.accounts);
|
|
77
|
+
const pendingRecoveryWarnings = new Map();
|
|
69
78
|
for (const name of names) {
|
|
70
79
|
const entry = store.accounts[name];
|
|
80
|
+
if (!entry)
|
|
81
|
+
continue;
|
|
71
82
|
const oauth = toOAuth(entry);
|
|
72
83
|
if (!oauth.access && !oauth.refresh) {
|
|
73
84
|
store.accounts[name] = {
|
|
@@ -88,6 +99,30 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
88
99
|
},
|
|
89
100
|
}));
|
|
90
101
|
if (!result.ok) {
|
|
102
|
+
if (result.error.kind === "invalid_account") {
|
|
103
|
+
const recovered = await recoverInvalidCodexAccount({
|
|
104
|
+
store,
|
|
105
|
+
invalidAccountName: name,
|
|
106
|
+
setAuth: async (next) => {
|
|
107
|
+
await inputDeps.client.auth.set(next);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
store.accounts = recovered.store.accounts;
|
|
111
|
+
store.active = recovered.store.active;
|
|
112
|
+
if (recovered.replacement) {
|
|
113
|
+
const replacement = store.accounts[recovered.replacement];
|
|
114
|
+
const weekRemaining = replacement?.snapshot?.usageWeek?.remaining ?? 0;
|
|
115
|
+
const fiveHourRemaining = replacement?.snapshot?.usage5h?.remaining ?? 0;
|
|
116
|
+
if (weekRemaining > 0 && fiveHourRemaining <= 0) {
|
|
117
|
+
pendingRecoveryWarnings.set(recovered.replacement, {
|
|
118
|
+
code: "week_recovery_only",
|
|
119
|
+
removed: recovered.removed,
|
|
120
|
+
replacement: recovered.replacement,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
91
126
|
store.accounts[name] = {
|
|
92
127
|
...entry,
|
|
93
128
|
snapshot: {
|
|
@@ -113,10 +148,11 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
113
148
|
...(result.authPatch?.accountId !== undefined ? { accountId: result.authPatch.accountId } : {}),
|
|
114
149
|
name: nextName,
|
|
115
150
|
providerId: "openai",
|
|
151
|
+
workspaceName: result.status.identity.workspaceName ?? entry.workspaceName,
|
|
116
152
|
accountId: result.status.identity.accountId ?? result.authPatch?.accountId ?? entry.accountId,
|
|
117
153
|
email: result.status.identity.email ?? entry.email,
|
|
118
154
|
snapshot: {
|
|
119
|
-
...(entry.snapshot ?? {}),
|
|
155
|
+
...(withoutRecoveryWarning(entry.snapshot) ?? {}),
|
|
120
156
|
plan: result.status.identity.plan ?? entry.snapshot?.plan,
|
|
121
157
|
usage5h: {
|
|
122
158
|
entitlement: result.status.windows.primary.entitlement,
|
|
@@ -141,6 +177,24 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
141
177
|
if (store.active === name || !store.active)
|
|
142
178
|
store.active = nextName;
|
|
143
179
|
}
|
|
180
|
+
for (const [name, entry] of Object.entries(store.accounts)) {
|
|
181
|
+
const warning = pendingRecoveryWarnings.get(name);
|
|
182
|
+
const snapshot = withoutRecoveryWarning(entry.snapshot);
|
|
183
|
+
store.accounts[name] = {
|
|
184
|
+
...entry,
|
|
185
|
+
...(snapshot ? { snapshot } : {}),
|
|
186
|
+
};
|
|
187
|
+
if (!warning)
|
|
188
|
+
continue;
|
|
189
|
+
const nextSnapshot = {
|
|
190
|
+
...(store.accounts[name].snapshot ?? {}),
|
|
191
|
+
};
|
|
192
|
+
nextSnapshot.recoveryWarning = warning;
|
|
193
|
+
store.accounts[name] = {
|
|
194
|
+
...store.accounts[name],
|
|
195
|
+
snapshot: nextSnapshot,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
144
198
|
};
|
|
145
199
|
return {
|
|
146
200
|
key: "codex",
|
|
@@ -201,6 +255,7 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
201
255
|
fallback: `openai-${now()}`,
|
|
202
256
|
}),
|
|
203
257
|
providerId: "openai",
|
|
258
|
+
workspaceName: oauth.workspaceName,
|
|
204
259
|
refresh,
|
|
205
260
|
access,
|
|
206
261
|
expires: oauth.expires,
|
|
@@ -215,6 +270,7 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
215
270
|
return Object.entries(store.accounts).map(([name, entry], index) => ({
|
|
216
271
|
id: entry.accountId ?? name,
|
|
217
272
|
name: entry.email ?? entry.accountId ?? name,
|
|
273
|
+
workspaceName: entry.workspaceName,
|
|
218
274
|
index,
|
|
219
275
|
isCurrent: store.active === name,
|
|
220
276
|
source: entry.source,
|
package/dist/ui/menu.d.ts
CHANGED
package/dist/ui/menu.js
CHANGED
|
@@ -318,6 +318,7 @@ export function buildMenuItems(input) {
|
|
|
318
318
|
const numbered = `${account.index + 1}. ${account.name}`;
|
|
319
319
|
const label = `${numbered}${currentBadge}${statusBadge ? " " + statusBadge : ""}${quotaBadge}`;
|
|
320
320
|
const detail = [
|
|
321
|
+
account.workspaceName,
|
|
321
322
|
account.lastUsed ? formatRelativeTime(account.lastUsed) : undefined,
|
|
322
323
|
account.plan,
|
|
323
324
|
account.models ? `${account.models.enabled}/${account.models.enabled + account.models.disabled} mods` : undefined,
|