opencode-openai-account-switcher 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +151 -0
  2. package/README.md +43 -0
  3. package/dist/auth-store.d.ts +13 -0
  4. package/dist/auth-store.js +43 -0
  5. package/dist/codex-auth-source.d.ts +16 -0
  6. package/dist/codex-auth-source.js +63 -0
  7. package/dist/codex-invalid-account.d.ts +32 -0
  8. package/dist/codex-invalid-account.js +106 -0
  9. package/dist/codex-network-retry.d.ts +2 -0
  10. package/dist/codex-network-retry.js +9 -0
  11. package/dist/codex-status-command.d.ts +56 -0
  12. package/dist/codex-status-command.js +341 -0
  13. package/dist/codex-status-fetcher.d.ts +71 -0
  14. package/dist/codex-status-fetcher.js +300 -0
  15. package/dist/codex-store.d.ts +49 -0
  16. package/dist/codex-store.js +267 -0
  17. package/dist/common-settings-actions.d.ts +15 -0
  18. package/dist/common-settings-actions.js +22 -0
  19. package/dist/common-settings-store.d.ts +17 -0
  20. package/dist/common-settings-store.js +72 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/menu-runtime.d.ts +81 -0
  24. package/dist/menu-runtime.js +141 -0
  25. package/dist/network-retry-engine.d.ts +33 -0
  26. package/dist/network-retry-engine.js +62 -0
  27. package/dist/plugin-hooks.d.ts +49 -0
  28. package/dist/plugin-hooks.js +123 -0
  29. package/dist/plugin.d.ts +2 -0
  30. package/dist/plugin.js +127 -0
  31. package/dist/providers/codex-menu-adapter.d.ts +58 -0
  32. package/dist/providers/codex-menu-adapter.js +429 -0
  33. package/dist/providers/descriptor.d.ts +24 -0
  34. package/dist/providers/descriptor.js +16 -0
  35. package/dist/providers/registry.d.ts +15 -0
  36. package/dist/providers/registry.js +49 -0
  37. package/dist/retry/codex-policy.d.ts +5 -0
  38. package/dist/retry/codex-policy.js +75 -0
  39. package/dist/retry/common-policy.d.ts +37 -0
  40. package/dist/retry/common-policy.js +68 -0
  41. package/dist/store-paths.d.ts +4 -0
  42. package/dist/store-paths.js +22 -0
  43. package/dist/ui/ansi.d.ts +18 -0
  44. package/dist/ui/ansi.js +32 -0
  45. package/dist/ui/confirm.d.ts +1 -0
  46. package/dist/ui/confirm.js +14 -0
  47. package/dist/ui/menu.d.ts +168 -0
  48. package/dist/ui/menu.js +305 -0
  49. package/dist/ui/select.d.ts +36 -0
  50. package/dist/ui/select.js +350 -0
  51. package/dist/upstream/codex-loader-adapter.d.ts +99 -0
  52. package/dist/upstream/codex-loader-adapter.js +80 -0
  53. package/dist/upstream/codex-plugin.snapshot.d.ts +32 -0
  54. package/dist/upstream/codex-plugin.snapshot.js +638 -0
  55. package/package.json +40 -0
  56. package/scripts/sync-codex-upstream.mjs +348 -0
@@ -0,0 +1,341 @@
1
+ import { resolveCodexAuthSource } from "./codex-auth-source.js";
2
+ import { fetchCodexStatus } from "./codex-status-fetcher.js";
3
+ import { getActiveCodexAccount, normalizeCodexStore, readCodexStore, writeCodexStore, } from "./codex-store.js";
4
+ import { getCodexDisplayName, recoverInvalidCodexAccount } from "./codex-invalid-account.js";
5
+ import { readAuth } from "./auth-store.js";
6
+ export class CodexStatusCommandHandledError extends Error {
7
+ constructor() {
8
+ super("codex-status-command-handled");
9
+ this.name = "CodexStatusCommandHandledError";
10
+ }
11
+ }
12
+ function asRecord(value) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value))
14
+ return undefined;
15
+ return value;
16
+ }
17
+ function pickString(value) {
18
+ return typeof value === "string" && value.length > 0 ? value : undefined;
19
+ }
20
+ function pickNumber(value) {
21
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
22
+ }
23
+ async function showToast(input) {
24
+ const tui = input.client?.tui;
25
+ const show = input.client?.tui?.showToast;
26
+ if (!show)
27
+ return;
28
+ try {
29
+ await show.call(tui, {
30
+ body: {
31
+ message: input.message,
32
+ variant: input.variant,
33
+ },
34
+ });
35
+ }
36
+ catch {
37
+ // fail open for toast dispatch
38
+ }
39
+ }
40
+ function ratio(remaining, entitlement) {
41
+ if (remaining === undefined && entitlement === undefined)
42
+ return "n/a";
43
+ return `${remaining ?? "n/a"}/${entitlement ?? "n/a"}`;
44
+ }
45
+ function value(value) {
46
+ return value === undefined ? "n/a" : String(value);
47
+ }
48
+ function pickWorkspaceLabel(input) {
49
+ return input.workspaceName
50
+ ?? input.name
51
+ ?? input.email
52
+ ?? input.accountId;
53
+ }
54
+ function renderWindow(label, window) {
55
+ if (window.entitlement === 100 && window.remaining !== undefined) {
56
+ return `${label}: ${window.remaining}% left`;
57
+ }
58
+ return `${label}: ${ratio(window.remaining, window.entitlement)}`;
59
+ }
60
+ function renderStatus(status) {
61
+ const identity = status.identity;
62
+ return [
63
+ `账号: ${value(identity.accountId ?? identity.email)}`,
64
+ `Workspace: ${value(pickWorkspaceLabel({
65
+ workspaceName: identity.workspaceName,
66
+ email: identity.email,
67
+ accountId: identity.accountId,
68
+ }))}`,
69
+ renderWindow("5h", status.windows.primary),
70
+ renderWindow("week", status.windows.secondary),
71
+ ].join("\n");
72
+ }
73
+ function getCachedAccountForSource(store, input) {
74
+ const accountId = input.accountId;
75
+ if (accountId) {
76
+ const match = Object.entries(store.accounts).find(([, entry]) => entry.accountId === accountId);
77
+ if (match) {
78
+ return {
79
+ name: match[0],
80
+ entry: match[1],
81
+ };
82
+ }
83
+ }
84
+ return getActiveCodexAccount(store);
85
+ }
86
+ function renderCachedStatusForAccount(store, input) {
87
+ const active = getCachedAccountForSource(store, input);
88
+ const entry = active?.entry;
89
+ const snapshot = entry?.snapshot;
90
+ return [
91
+ `账号: ${value(entry?.accountId ?? active?.name ?? entry?.email)}`,
92
+ `Workspace: ${value(pickWorkspaceLabel({
93
+ workspaceName: entry?.workspaceName,
94
+ name: entry?.name ?? active?.name,
95
+ email: entry?.email,
96
+ accountId: entry?.accountId,
97
+ }))}`,
98
+ renderWindow("5h", snapshot?.usage5h ?? {}),
99
+ renderWindow("week", snapshot?.usageWeek ?? {}),
100
+ ].join("\n");
101
+ }
102
+ function hasCachedStore(store) {
103
+ const active = getActiveCodexAccount(store);
104
+ const entry = active?.entry;
105
+ const usage5h = entry?.snapshot?.usage5h;
106
+ return Boolean(active
107
+ || entry?.accountId
108
+ || entry?.email
109
+ || entry?.snapshot?.plan
110
+ || usage5h?.entitlement !== undefined
111
+ || usage5h?.remaining !== undefined);
112
+ }
113
+ function mapAuthEntryToOpenAI(entry) {
114
+ if (!entry)
115
+ return undefined;
116
+ return {
117
+ type: "oauth",
118
+ refresh: entry.refresh,
119
+ access: entry.access,
120
+ expires: entry.expires,
121
+ accountId: entry.accountId,
122
+ };
123
+ }
124
+ async function defaultLoadAuthWithFallback(input) {
125
+ const client = input.client;
126
+ const authClient = client?.auth;
127
+ const getAuth = client?.auth?.get;
128
+ if (getAuth) {
129
+ try {
130
+ const result = await getAuth.call(authClient, { path: { id: "openai" }, throwOnError: true });
131
+ const withData = asRecord(result)?.data;
132
+ const payload = asRecord(withData) ?? asRecord(result);
133
+ if (payload) {
134
+ return {
135
+ openai: payload,
136
+ };
137
+ }
138
+ }
139
+ catch {
140
+ // fall through to auth.json fallback
141
+ }
142
+ }
143
+ const authEntries = await input.readAuthEntries().catch(() => ({}));
144
+ const openai = mapAuthEntryToOpenAI(authEntries.openai);
145
+ if (!openai)
146
+ return undefined;
147
+ return {
148
+ openai,
149
+ };
150
+ }
151
+ async function defaultPersistAuth(client, auth) {
152
+ const authClient = client?.auth;
153
+ const setAuth = client?.auth?.set;
154
+ if (!setAuth)
155
+ return;
156
+ const openai = asRecord(auth.openai);
157
+ if (!openai)
158
+ return;
159
+ await setAuth.call(authClient, {
160
+ path: { id: "openai" },
161
+ body: {
162
+ type: "oauth",
163
+ refresh: pickString(openai.refresh),
164
+ access: pickString(openai.access),
165
+ expires: pickNumber(openai.expires),
166
+ accountId: pickString(openai.accountId),
167
+ },
168
+ });
169
+ }
170
+ function patchAuth(auth, patch) {
171
+ const base = asRecord(auth) ?? {};
172
+ const openai = asRecord(base.openai) ?? { type: "oauth" };
173
+ return {
174
+ ...base,
175
+ openai: {
176
+ ...openai,
177
+ type: "oauth",
178
+ ...(patch.access !== undefined ? { access: patch.access } : {}),
179
+ ...(patch.refresh !== undefined ? { refresh: patch.refresh } : {}),
180
+ ...(patch.expires !== undefined ? { expires: patch.expires } : {}),
181
+ ...(patch.accountId !== undefined ? { accountId: patch.accountId } : {}),
182
+ },
183
+ };
184
+ }
185
+ export async function handleCodexStatusCommand(input) {
186
+ const loadAuth = input.loadAuth ?? (() => defaultLoadAuthWithFallback({
187
+ client: input.client,
188
+ readAuthEntries: input.readAuthEntries ?? readAuth,
189
+ }));
190
+ const persistAuth = input.persistAuth ?? ((nextAuth) => defaultPersistAuth(input.client, nextAuth));
191
+ const fetchStatus = input.fetchStatus ?? ((next) => fetchCodexStatus(next));
192
+ const readStore = input.readStore ?? (() => readCodexStore());
193
+ const writeStore = input.writeStore ?? ((store) => writeCodexStore(store));
194
+ await showToast({
195
+ client: input.client,
196
+ message: "Fetching Codex status...",
197
+ variant: "info",
198
+ });
199
+ const auth = await loadAuth().catch(() => undefined);
200
+ const source = resolveCodexAuthSource(auth);
201
+ if (!source) {
202
+ await showToast({
203
+ client: input.client,
204
+ message: "OpenAI OAuth auth is missing for Codex status.",
205
+ variant: "error",
206
+ });
207
+ throw new CodexStatusCommandHandledError();
208
+ }
209
+ const fetched = await fetchStatus({
210
+ oauth: source.oauth,
211
+ accountId: source.accountId,
212
+ }).catch((error) => ({
213
+ ok: false,
214
+ error: {
215
+ kind: "network_error",
216
+ message: error instanceof Error ? error.message : String(error),
217
+ },
218
+ }));
219
+ if (!fetched.ok) {
220
+ if (fetched.error.kind === "invalid_account") {
221
+ const currentRaw = await readStore().catch(() => ({}));
222
+ const currentStore = normalizeCodexStore(currentRaw);
223
+ const invalid = getCachedAccountForSource(currentStore, { accountId: source.accountId });
224
+ const invalidName = invalid?.name ?? currentStore.active;
225
+ if (invalidName && currentStore.accounts[invalidName]) {
226
+ const recovered = await recoverInvalidCodexAccount({
227
+ store: currentStore,
228
+ invalidAccountName: invalidName,
229
+ setAuth: input.client?.auth?.set
230
+ ? async (next) => {
231
+ const authClient = input.client?.auth;
232
+ const setAuth = input.client?.auth?.set;
233
+ if (!setAuth)
234
+ return;
235
+ await setAuth.call(authClient, next);
236
+ }
237
+ : undefined,
238
+ });
239
+ await writeStore(recovered.store);
240
+ const removedDisplay = getCodexDisplayName(invalid?.entry, recovered.removed);
241
+ const messageLines = [`无效账号${removedDisplay}已移除,请及时检查核对`];
242
+ if (recovered.replacement) {
243
+ const replacementEntry = recovered.store.accounts[recovered.replacement];
244
+ const replacementDisplay = getCodexDisplayName(replacementEntry, recovered.replacement);
245
+ messageLines.push(`已切换到${replacementDisplay}`);
246
+ if (recovered.weekRecoveryOnly) {
247
+ messageLines.push("请检查账号状态");
248
+ }
249
+ }
250
+ await showToast({
251
+ client: input.client,
252
+ message: messageLines.join("\n"),
253
+ variant: "warning",
254
+ });
255
+ throw new CodexStatusCommandHandledError();
256
+ }
257
+ }
258
+ const cachedRaw = await readStore().catch(() => ({}));
259
+ const cached = normalizeCodexStore(cachedRaw);
260
+ if (hasCachedStore(cached)) {
261
+ await showToast({
262
+ client: input.client,
263
+ message: `Codex status fetch failed: ${fetched.error.message}`,
264
+ variant: "warning",
265
+ });
266
+ await showToast({
267
+ client: input.client,
268
+ message: renderCachedStatusForAccount(cached, { accountId: source.accountId }),
269
+ variant: "warning",
270
+ });
271
+ }
272
+ else {
273
+ await showToast({
274
+ client: input.client,
275
+ message: `Codex status fetch failed: ${fetched.error.message}`,
276
+ variant: "error",
277
+ });
278
+ }
279
+ throw new CodexStatusCommandHandledError();
280
+ }
281
+ if (fetched.authPatch) {
282
+ const nextAuth = patchAuth(auth, fetched.authPatch);
283
+ await persistAuth(nextAuth).catch(async (error) => {
284
+ await showToast({
285
+ client: input.client,
286
+ message: `Codex auth refresh succeeded but auth persistence failed: ${error instanceof Error ? error.message : String(error)}`,
287
+ variant: "warning",
288
+ });
289
+ });
290
+ }
291
+ const previousRaw = await readStore().catch(() => ({}));
292
+ const previousStore = normalizeCodexStore(previousRaw);
293
+ const previousActive = getActiveCodexAccount(previousStore);
294
+ const nextActive = fetched.status.identity.accountId
295
+ ?? source.accountId
296
+ ?? previousActive?.entry.accountId
297
+ ?? previousActive?.name
298
+ ?? "default";
299
+ const previousEntry = previousStore.accounts[nextActive] ?? {};
300
+ const nextStore = {
301
+ ...previousStore,
302
+ active: nextActive,
303
+ lastSnapshotRefresh: fetched.status.updatedAt,
304
+ accounts: {
305
+ ...previousStore.accounts,
306
+ [nextActive]: {
307
+ ...previousEntry,
308
+ name: previousEntry.name ?? nextActive,
309
+ providerId: previousEntry.providerId ?? "codex",
310
+ accountId: fetched.status.identity.accountId ?? previousEntry.accountId ?? source.accountId,
311
+ email: fetched.status.identity.email ?? previousEntry.email,
312
+ workspaceName: fetched.status.identity.workspaceName ?? previousEntry.workspaceName,
313
+ lastUsed: fetched.status.updatedAt,
314
+ snapshot: {
315
+ ...(previousEntry.snapshot ?? {}),
316
+ plan: fetched.status.identity.plan ?? previousEntry.snapshot?.plan,
317
+ usage5h: {
318
+ entitlement: fetched.status.windows.primary.entitlement,
319
+ remaining: fetched.status.windows.primary.remaining,
320
+ used: fetched.status.windows.primary.used,
321
+ resetAt: fetched.status.windows.primary.resetAt,
322
+ },
323
+ usageWeek: {
324
+ entitlement: fetched.status.windows.secondary.entitlement,
325
+ remaining: fetched.status.windows.secondary.remaining,
326
+ used: fetched.status.windows.secondary.used,
327
+ resetAt: fetched.status.windows.secondary.resetAt,
328
+ },
329
+ updatedAt: fetched.status.updatedAt,
330
+ },
331
+ },
332
+ },
333
+ };
334
+ await writeStore(nextStore);
335
+ await showToast({
336
+ client: input.client,
337
+ message: renderStatus(fetched.status),
338
+ variant: "success",
339
+ });
340
+ throw new CodexStatusCommandHandledError();
341
+ }
@@ -0,0 +1,71 @@
1
+ import type { OpenAIOAuthAuth } from "./codex-auth-source.js";
2
+ export type CodexWindowSnapshot = {
3
+ entitlement?: number;
4
+ remaining?: number;
5
+ used?: number;
6
+ resetAt?: number;
7
+ };
8
+ export type CodexStatusSnapshot = {
9
+ identity: {
10
+ accountId?: string;
11
+ email?: string;
12
+ plan?: string;
13
+ };
14
+ windows: {
15
+ primary: CodexWindowSnapshot;
16
+ secondary: CodexWindowSnapshot;
17
+ };
18
+ credits: {
19
+ total?: number;
20
+ remaining?: number;
21
+ used?: number;
22
+ };
23
+ updatedAt: number;
24
+ };
25
+ export type CodexStatusError = {
26
+ kind: "invalid_account";
27
+ status: 400;
28
+ message: string;
29
+ } | {
30
+ kind: "rate_limited";
31
+ status: 429;
32
+ message: string;
33
+ } | {
34
+ kind: "timeout";
35
+ message: string;
36
+ } | {
37
+ kind: "server_error";
38
+ status: number;
39
+ message: string;
40
+ } | {
41
+ kind: "invalid_response";
42
+ message: string;
43
+ } | {
44
+ kind: "unauthorized";
45
+ status: 401;
46
+ message: string;
47
+ } | {
48
+ kind: "network_error";
49
+ message: string;
50
+ };
51
+ export type CodexStatusFetcherResult = {
52
+ ok: true;
53
+ status: CodexStatusSnapshot;
54
+ authPatch?: {
55
+ access?: string;
56
+ refresh?: string;
57
+ expires?: number;
58
+ accountId?: string;
59
+ };
60
+ } | {
61
+ ok: false;
62
+ error: CodexStatusError;
63
+ };
64
+ export declare function fetchCodexStatus(input: {
65
+ oauth: OpenAIOAuthAuth;
66
+ accountId?: string;
67
+ fetchImpl?: typeof globalThis.fetch;
68
+ now?: () => number;
69
+ timeoutMs?: number;
70
+ refreshTokens?: (oauth: OpenAIOAuthAuth) => Promise<OpenAIOAuthAuth | undefined>;
71
+ }): Promise<CodexStatusFetcherResult>;