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,300 @@
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
+ }
48
+ function asRecord(input) {
49
+ if (!input || typeof input !== "object" || Array.isArray(input))
50
+ return undefined;
51
+ return input;
52
+ }
53
+ function readString(input) {
54
+ return typeof input === "string" && input.length > 0 ? input : undefined;
55
+ }
56
+ function readNumber(input) {
57
+ return typeof input === "number" && Number.isFinite(input) ? input : undefined;
58
+ }
59
+ function readTimestamp(input) {
60
+ const value = readNumber(input);
61
+ if (value === undefined)
62
+ return undefined;
63
+ return value >= 1e12 ? value : value * 1000;
64
+ }
65
+ function pickRecord(source, keys) {
66
+ for (const key of keys) {
67
+ const value = asRecord(source[key]);
68
+ if (value)
69
+ return value;
70
+ }
71
+ return undefined;
72
+ }
73
+ function pickWindow(source, key) {
74
+ const rateLimit = pickRecord(source, ["rate_limit", "rateLimit"]);
75
+ const rateLimitKey = key === "primary" ? "primary_window" : "secondary_window";
76
+ const rateLimitWindow = rateLimit ? asRecord(rateLimit[rateLimitKey]) : undefined;
77
+ const windows = pickRecord(source, ["windows", "usage_windows", "quota_windows"]);
78
+ const block = windows ? asRecord(windows[key]) : undefined;
79
+ const fallback = asRecord(source[key]);
80
+ const raw = rateLimitWindow ?? block ?? fallback;
81
+ if (!raw) {
82
+ return {
83
+ entitlement: undefined,
84
+ remaining: undefined,
85
+ used: undefined,
86
+ resetAt: undefined,
87
+ };
88
+ }
89
+ const entitlement = readNumber(raw.entitlement);
90
+ const remainingPercent = readNumber(raw.remaining_percent);
91
+ const usedPercent = readNumber(raw.used_percent);
92
+ const remaining = readNumber(raw.remaining) ?? remainingPercent;
93
+ const used = readNumber(raw.used) ?? usedPercent;
94
+ const percentBased = remainingPercent !== undefined || usedPercent !== undefined;
95
+ return {
96
+ entitlement: entitlement ?? (percentBased ? 100 : undefined),
97
+ remaining: remaining ?? (used !== undefined ? Math.max(0, 100 - used) : undefined),
98
+ used,
99
+ resetAt: readTimestamp(raw.resetAt ?? raw.reset_at),
100
+ };
101
+ }
102
+ function normalizeUsageStatus(payload, now) {
103
+ const source = asRecord(payload) ?? {};
104
+ const account = pickRecord(source, ["account", "identity", "user"]) ?? {};
105
+ const credits = pickRecord(source, ["credits", "credit_balance", "credit"]) ?? {};
106
+ return {
107
+ identity: {
108
+ accountId: readString(account.id) ?? readString(source.account_id) ?? readString(source.accountId),
109
+ email: readString(account.email) ?? readString(source.email),
110
+ plan: readString(account.plan) ?? readString(source.plan) ?? readString(source.plan_type),
111
+ },
112
+ windows: {
113
+ primary: pickWindow(source, "primary"),
114
+ secondary: pickWindow(source, "secondary"),
115
+ },
116
+ credits: {
117
+ total: readNumber(credits.total),
118
+ remaining: readNumber(credits.remaining),
119
+ used: readNumber(credits.used),
120
+ },
121
+ updatedAt: now(),
122
+ };
123
+ }
124
+ async function parseJsonResponse(response) {
125
+ const contentType = response.headers.get("content-type") ?? "";
126
+ if (!contentType.toLowerCase().includes("application/json"))
127
+ return undefined;
128
+ try {
129
+ return await response.json();
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ }
135
+ function isTimeoutError(error) {
136
+ if (!error || typeof error !== "object")
137
+ return false;
138
+ const err = error;
139
+ if (err.name === "AbortError")
140
+ return true;
141
+ const message = typeof err.message === "string" ? err.message.toLowerCase() : "";
142
+ return message.includes("timeout");
143
+ }
144
+ function buildHeaders(input) {
145
+ const headers = new Headers({
146
+ Accept: "application/json",
147
+ "User-Agent": "Codex CLI",
148
+ });
149
+ if (input.access)
150
+ headers.set("Authorization", `Bearer ${input.access}`);
151
+ if (input.accountId)
152
+ headers.set("ChatGPT-Account-Id", input.accountId);
153
+ return headers;
154
+ }
155
+ async function requestUsage(input) {
156
+ return input.fetchImpl(CODEX_USAGE_URL, {
157
+ method: "GET",
158
+ headers: buildHeaders({
159
+ access: input.oauth.access,
160
+ accountId: input.accountId,
161
+ }),
162
+ signal: input.signal,
163
+ });
164
+ }
165
+ export async function fetchCodexStatus(input) {
166
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
167
+ const now = input.now ?? Date.now;
168
+ const explicitAccountId = input.accountId;
169
+ const timeoutMs = input.timeoutMs ?? CODEX_USAGE_TIMEOUT_MS;
170
+ let oauth = input.oauth;
171
+ let authPatch;
172
+ for (let attempt = 0; attempt < 2; attempt += 1) {
173
+ let response;
174
+ const controller = new AbortController();
175
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
176
+ try {
177
+ response = await requestUsage({
178
+ oauth,
179
+ accountId: explicitAccountId ?? oauth.accountId,
180
+ fetchImpl,
181
+ signal: controller.signal,
182
+ });
183
+ }
184
+ catch (error) {
185
+ clearTimeout(timeout);
186
+ if (isTimeoutError(error)) {
187
+ return {
188
+ ok: false,
189
+ error: {
190
+ kind: "timeout",
191
+ message: "codex usage request timed out",
192
+ },
193
+ };
194
+ }
195
+ return {
196
+ ok: false,
197
+ error: {
198
+ kind: "network_error",
199
+ message: error instanceof Error ? error.message : String(error),
200
+ },
201
+ };
202
+ }
203
+ finally {
204
+ clearTimeout(timeout);
205
+ }
206
+ if (response.status === 401 && attempt === 0 && input.refreshTokens) {
207
+ let refreshed;
208
+ try {
209
+ refreshed = await input.refreshTokens(oauth);
210
+ }
211
+ catch (error) {
212
+ const mappedError = mapRefreshTokenError(error);
213
+ return {
214
+ ok: false,
215
+ error: mappedError,
216
+ };
217
+ }
218
+ if (!refreshed || !refreshed.access) {
219
+ return {
220
+ ok: false,
221
+ error: {
222
+ kind: "unauthorized",
223
+ status: 401,
224
+ message: "codex usage request unauthorized",
225
+ },
226
+ };
227
+ }
228
+ oauth = refreshed;
229
+ authPatch = {
230
+ access: refreshed.access,
231
+ refresh: refreshed.refresh,
232
+ expires: refreshed.expires,
233
+ accountId: refreshed.accountId,
234
+ };
235
+ continue;
236
+ }
237
+ if (response.status === 429) {
238
+ return {
239
+ ok: false,
240
+ error: {
241
+ kind: "rate_limited",
242
+ status: 429,
243
+ message: "codex usage request was rate limited",
244
+ },
245
+ };
246
+ }
247
+ if (response.status >= 500 && response.status <= 599) {
248
+ return {
249
+ ok: false,
250
+ error: {
251
+ kind: "server_error",
252
+ status: response.status,
253
+ message: "codex usage request failed with server error",
254
+ },
255
+ };
256
+ }
257
+ if (response.status === 401) {
258
+ return {
259
+ ok: false,
260
+ error: {
261
+ kind: "unauthorized",
262
+ status: 401,
263
+ message: "codex usage request unauthorized",
264
+ },
265
+ };
266
+ }
267
+ if (!response.ok) {
268
+ return {
269
+ ok: false,
270
+ error: {
271
+ kind: "network_error",
272
+ message: `codex usage request failed with status ${response.status}`,
273
+ },
274
+ };
275
+ }
276
+ const payload = await parseJsonResponse(response);
277
+ if (payload === undefined) {
278
+ return {
279
+ ok: false,
280
+ error: {
281
+ kind: "invalid_response",
282
+ message: "codex usage response was not json",
283
+ },
284
+ };
285
+ }
286
+ return {
287
+ ok: true,
288
+ status: normalizeUsageStatus(payload, now),
289
+ ...(authPatch ? { authPatch } : {}),
290
+ };
291
+ }
292
+ return {
293
+ ok: false,
294
+ error: {
295
+ kind: "unauthorized",
296
+ status: 401,
297
+ message: "codex usage request unauthorized",
298
+ },
299
+ };
300
+ }
@@ -0,0 +1,49 @@
1
+ type CodexUsageWindow = {
2
+ entitlement?: number;
3
+ remaining?: number;
4
+ used?: number;
5
+ resetAt?: number;
6
+ };
7
+ export type CodexAccountSnapshot = {
8
+ plan?: string;
9
+ usage5h?: CodexUsageWindow;
10
+ usageWeek?: CodexUsageWindow;
11
+ updatedAt?: number;
12
+ error?: string;
13
+ };
14
+ export type CodexAccountEntry = {
15
+ name?: string;
16
+ providerId?: string;
17
+ workspaceName?: string;
18
+ refresh?: string;
19
+ access?: string;
20
+ expires?: number;
21
+ accountId?: string;
22
+ email?: string;
23
+ addedAt?: number;
24
+ lastUsed?: number;
25
+ source?: string;
26
+ snapshot?: CodexAccountSnapshot;
27
+ };
28
+ export type CodexStoreFile = {
29
+ accounts: Record<string, CodexAccountEntry>;
30
+ active?: string;
31
+ activeAccountNames?: string[];
32
+ autoRefresh?: boolean;
33
+ refreshMinutes?: number;
34
+ lastSnapshotRefresh?: number;
35
+ bootstrapAuthImportTried?: boolean;
36
+ bootstrapAuthImportAt?: number;
37
+ };
38
+ export declare function normalizeCodexStore(input: unknown): CodexStoreFile;
39
+ export declare function parseCodexStore(raw: string): CodexStoreFile;
40
+ export declare function getActiveCodexAccount(store: CodexStoreFile): {
41
+ name: string;
42
+ entry: CodexAccountEntry;
43
+ } | undefined;
44
+ export declare function codexStorePath(): string;
45
+ export declare function readCodexStore(filePath?: string): Promise<CodexStoreFile>;
46
+ export declare function writeCodexStore(store: CodexStoreFile, options?: {
47
+ filePath?: string;
48
+ }): Promise<void>;
49
+ export {};
@@ -0,0 +1,267 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { codexAccountsPath, legacyCodexStorePath } from "./store-paths.js";
4
+ function asRecord(input) {
5
+ if (!input || typeof input !== "object" || Array.isArray(input))
6
+ return undefined;
7
+ return input;
8
+ }
9
+ function pickString(input) {
10
+ return typeof input === "string" && input.length > 0 ? input : undefined;
11
+ }
12
+ function pickNumber(input) {
13
+ return typeof input === "number" && Number.isFinite(input) ? input : undefined;
14
+ }
15
+ function pickBoolean(input) {
16
+ return typeof input === "boolean" ? input : undefined;
17
+ }
18
+ function pickUsageWindow(input) {
19
+ const source = asRecord(input);
20
+ if (!source)
21
+ return undefined;
22
+ const next = {};
23
+ if (pickNumber(source.entitlement) !== undefined)
24
+ next.entitlement = pickNumber(source.entitlement);
25
+ if (pickNumber(source.remaining) !== undefined)
26
+ next.remaining = pickNumber(source.remaining);
27
+ if (pickNumber(source.used) !== undefined)
28
+ next.used = pickNumber(source.used);
29
+ if (pickNumber(source.resetAt) !== undefined)
30
+ next.resetAt = pickNumber(source.resetAt);
31
+ return Object.keys(next).length > 0 ? next : undefined;
32
+ }
33
+ function pickSnapshot(input) {
34
+ const source = asRecord(input);
35
+ if (!source)
36
+ return undefined;
37
+ const next = {};
38
+ if (pickString(source.plan))
39
+ next.plan = pickString(source.plan);
40
+ const usage5h = pickUsageWindow(source.usage5h);
41
+ if (usage5h)
42
+ next.usage5h = usage5h;
43
+ const usageWeek = pickUsageWindow(source.usageWeek);
44
+ if (usageWeek)
45
+ next.usageWeek = usageWeek;
46
+ if (pickNumber(source.updatedAt) !== undefined)
47
+ next.updatedAt = pickNumber(source.updatedAt);
48
+ if (pickString(source.error))
49
+ next.error = pickString(source.error);
50
+ return Object.keys(next).length > 0 ? next : undefined;
51
+ }
52
+ function pickEntry(input) {
53
+ const source = asRecord(input);
54
+ if (!source)
55
+ return undefined;
56
+ const next = {};
57
+ if (pickString(source.name))
58
+ next.name = pickString(source.name);
59
+ if (pickString(source.providerId))
60
+ next.providerId = pickString(source.providerId);
61
+ if (pickString(source.workspaceName))
62
+ next.workspaceName = pickString(source.workspaceName);
63
+ if (pickString(source.refresh))
64
+ next.refresh = pickString(source.refresh);
65
+ if (pickString(source.access))
66
+ next.access = pickString(source.access);
67
+ if (pickNumber(source.expires) !== undefined)
68
+ next.expires = pickNumber(source.expires);
69
+ if (pickString(source.accountId))
70
+ next.accountId = pickString(source.accountId);
71
+ if (pickString(source.email))
72
+ next.email = pickString(source.email);
73
+ if (pickNumber(source.addedAt) !== undefined)
74
+ next.addedAt = pickNumber(source.addedAt);
75
+ if (pickNumber(source.lastUsed) !== undefined)
76
+ next.lastUsed = pickNumber(source.lastUsed);
77
+ if (pickString(source.source))
78
+ next.source = pickString(source.source);
79
+ const snapshot = pickSnapshot(source.snapshot);
80
+ if (snapshot)
81
+ next.snapshot = snapshot;
82
+ return next;
83
+ }
84
+ function pickActiveAccountNames(input, accounts) {
85
+ if (!Array.isArray(input))
86
+ return undefined;
87
+ const seen = new Set();
88
+ const next = [];
89
+ for (const item of input) {
90
+ const name = pickString(item);
91
+ if (!name || seen.has(name) || !accounts[name])
92
+ continue;
93
+ seen.add(name);
94
+ next.push(name);
95
+ }
96
+ return next.length > 0 ? next : undefined;
97
+ }
98
+ function normalizeNewStore(source) {
99
+ const accounts = {};
100
+ const sourceAccounts = asRecord(source.accounts);
101
+ if (sourceAccounts) {
102
+ for (const [name, value] of Object.entries(sourceAccounts)) {
103
+ const entry = pickEntry(value);
104
+ if (!entry)
105
+ continue;
106
+ accounts[name] = {
107
+ ...entry,
108
+ ...(entry.name ? {} : { name }),
109
+ };
110
+ }
111
+ }
112
+ const store = { accounts };
113
+ const active = pickString(source.active);
114
+ if (active && accounts[active])
115
+ store.active = active;
116
+ const activeNames = pickActiveAccountNames(source.activeAccountNames, accounts);
117
+ if (activeNames)
118
+ store.activeAccountNames = activeNames;
119
+ if (pickBoolean(source.autoRefresh) !== undefined)
120
+ store.autoRefresh = pickBoolean(source.autoRefresh);
121
+ if (pickNumber(source.refreshMinutes) !== undefined)
122
+ store.refreshMinutes = pickNumber(source.refreshMinutes);
123
+ if (pickNumber(source.lastSnapshotRefresh) !== undefined)
124
+ store.lastSnapshotRefresh = pickNumber(source.lastSnapshotRefresh);
125
+ if (pickBoolean(source.bootstrapAuthImportTried) !== undefined) {
126
+ store.bootstrapAuthImportTried = pickBoolean(source.bootstrapAuthImportTried);
127
+ }
128
+ if (pickNumber(source.bootstrapAuthImportAt) !== undefined) {
129
+ store.bootstrapAuthImportAt = pickNumber(source.bootstrapAuthImportAt);
130
+ }
131
+ return store;
132
+ }
133
+ function normalizeLegacyStore(source) {
134
+ const legacyAccount = asRecord(source.account);
135
+ const legacyStatus = asRecord(source.status);
136
+ const legacyPremium = asRecord(legacyStatus?.premium);
137
+ const accountId = pickString(source.activeAccountId) ?? pickString(legacyAccount?.id);
138
+ const email = pickString(source.activeEmail) ?? pickString(legacyAccount?.email);
139
+ const plan = pickString(legacyAccount?.plan);
140
+ const entitlement = pickNumber(legacyPremium?.entitlement);
141
+ const remaining = pickNumber(legacyPremium?.remaining);
142
+ const updatedAt = pickNumber(source.lastStatusRefresh);
143
+ const hasLegacy = Boolean(accountId
144
+ || email
145
+ || plan
146
+ || entitlement !== undefined
147
+ || remaining !== undefined);
148
+ const store = {
149
+ accounts: {},
150
+ };
151
+ if (pickBoolean(source.bootstrapAuthImportTried) !== undefined) {
152
+ store.bootstrapAuthImportTried = pickBoolean(source.bootstrapAuthImportTried);
153
+ }
154
+ if (pickNumber(source.bootstrapAuthImportAt) !== undefined) {
155
+ store.bootstrapAuthImportAt = pickNumber(source.bootstrapAuthImportAt);
156
+ }
157
+ if (updatedAt !== undefined)
158
+ store.lastSnapshotRefresh = updatedAt;
159
+ if (!hasLegacy)
160
+ return store;
161
+ const name = accountId ?? email ?? "default";
162
+ const snapshot = {};
163
+ if (plan)
164
+ snapshot.plan = plan;
165
+ if (entitlement !== undefined || remaining !== undefined) {
166
+ snapshot.usage5h = {
167
+ ...(entitlement !== undefined ? { entitlement } : {}),
168
+ ...(remaining !== undefined ? { remaining } : {}),
169
+ };
170
+ }
171
+ if (updatedAt !== undefined)
172
+ snapshot.updatedAt = updatedAt;
173
+ store.accounts[name] = {
174
+ name,
175
+ providerId: "codex",
176
+ ...(accountId ? { accountId } : {}),
177
+ ...(email ? { email } : {}),
178
+ ...(Object.keys(snapshot).length > 0 ? { snapshot } : {}),
179
+ };
180
+ store.active = name;
181
+ return store;
182
+ }
183
+ function mergeLegacyIntoStore(store, source) {
184
+ const legacy = normalizeLegacyStore(source);
185
+ if (Object.keys(legacy.accounts).length === 0) {
186
+ return {
187
+ ...store,
188
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
189
+ ...(legacy.bootstrapAuthImportTried !== undefined ? { bootstrapAuthImportTried: legacy.bootstrapAuthImportTried } : {}),
190
+ ...(legacy.bootstrapAuthImportAt !== undefined ? { bootstrapAuthImportAt: legacy.bootstrapAuthImportAt } : {}),
191
+ };
192
+ }
193
+ if (Object.keys(store.accounts).length > 0) {
194
+ return {
195
+ ...store,
196
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
197
+ bootstrapAuthImportTried: store.bootstrapAuthImportTried ?? legacy.bootstrapAuthImportTried,
198
+ bootstrapAuthImportAt: store.bootstrapAuthImportAt ?? legacy.bootstrapAuthImportAt,
199
+ };
200
+ }
201
+ return {
202
+ ...legacy,
203
+ ...store,
204
+ accounts: {
205
+ ...legacy.accounts,
206
+ ...store.accounts,
207
+ },
208
+ active: store.active ?? legacy.active,
209
+ activeAccountNames: store.activeAccountNames ?? legacy.activeAccountNames,
210
+ autoRefresh: store.autoRefresh ?? legacy.autoRefresh,
211
+ refreshMinutes: store.refreshMinutes ?? legacy.refreshMinutes,
212
+ lastSnapshotRefresh: store.lastSnapshotRefresh ?? legacy.lastSnapshotRefresh,
213
+ bootstrapAuthImportTried: store.bootstrapAuthImportTried ?? legacy.bootstrapAuthImportTried,
214
+ bootstrapAuthImportAt: store.bootstrapAuthImportAt ?? legacy.bootstrapAuthImportAt,
215
+ };
216
+ }
217
+ export function normalizeCodexStore(input) {
218
+ const source = asRecord(input);
219
+ if (!source)
220
+ return { accounts: {} };
221
+ if (source.accounts && asRecord(source.accounts)) {
222
+ return mergeLegacyIntoStore(normalizeNewStore(source), source);
223
+ }
224
+ return normalizeLegacyStore(source);
225
+ }
226
+ export function parseCodexStore(raw) {
227
+ const parsed = raw ? JSON.parse(raw) : {};
228
+ return normalizeCodexStore(parsed);
229
+ }
230
+ export function getActiveCodexAccount(store) {
231
+ if (store.active && store.accounts[store.active]) {
232
+ return {
233
+ name: store.active,
234
+ entry: store.accounts[store.active],
235
+ };
236
+ }
237
+ const first = Object.entries(store.accounts)[0];
238
+ if (!first)
239
+ return undefined;
240
+ return {
241
+ name: first[0],
242
+ entry: first[1],
243
+ };
244
+ }
245
+ export function codexStorePath() {
246
+ return codexAccountsPath();
247
+ }
248
+ export async function readCodexStore(filePath = codexStorePath()) {
249
+ const raw = await fs.readFile(filePath, "utf8").catch(async (error) => {
250
+ if (error.code !== "ENOENT")
251
+ throw error;
252
+ if (filePath !== codexStorePath())
253
+ return "";
254
+ return fs.readFile(legacyCodexStorePath(), "utf8").catch((legacyError) => {
255
+ if (legacyError.code === "ENOENT")
256
+ return "";
257
+ throw legacyError;
258
+ });
259
+ });
260
+ return parseCodexStore(raw);
261
+ }
262
+ export async function writeCodexStore(store, options) {
263
+ const file = options?.filePath ?? codexStorePath();
264
+ const next = normalizeCodexStore(store);
265
+ await fs.mkdir(path.dirname(file), { recursive: true });
266
+ await fs.writeFile(file, JSON.stringify(next, null, 2), { mode: 0o600 });
267
+ }
@@ -0,0 +1,15 @@
1
+ import type { CommonSettingsStore } from "./common-settings-store.js";
2
+ export type CommonSettingsActionType = "toggle-experimental-slash-commands" | "toggle-network-retry";
3
+ type WriteMeta = {
4
+ reason?: string;
5
+ source?: string;
6
+ actionType?: string;
7
+ };
8
+ export declare function applyCommonSettingsAction(input: {
9
+ action: {
10
+ type: CommonSettingsActionType;
11
+ };
12
+ readSettings: () => Promise<CommonSettingsStore>;
13
+ writeSettings: (settings: CommonSettingsStore, meta?: WriteMeta) => Promise<void>;
14
+ }): Promise<boolean>;
15
+ export {};
@@ -0,0 +1,22 @@
1
+ export async function applyCommonSettingsAction(input) {
2
+ const settings = await input.readSettings();
3
+ if (input.action.type === "toggle-experimental-slash-commands") {
4
+ settings.experimentalSlashCommandsEnabled = settings.experimentalSlashCommandsEnabled !== true;
5
+ await input.writeSettings(settings, {
6
+ reason: "toggle-experimental-slash-commands",
7
+ source: "applyCommonSettingsAction",
8
+ actionType: "toggle-experimental-slash-commands",
9
+ });
10
+ return true;
11
+ }
12
+ if (input.action.type === "toggle-network-retry") {
13
+ settings.networkRetryEnabled = settings.networkRetryEnabled !== true;
14
+ await input.writeSettings(settings, {
15
+ reason: "toggle-network-retry",
16
+ source: "applyCommonSettingsAction",
17
+ actionType: "toggle-network-retry",
18
+ });
19
+ return true;
20
+ }
21
+ return false;
22
+ }
@@ -0,0 +1,17 @@
1
+ export type CommonSettingsStore = {
2
+ networkRetryEnabled?: boolean;
3
+ experimentalSlashCommandsEnabled?: boolean;
4
+ experimentalStatusSlashCommandEnabled?: boolean;
5
+ };
6
+ export declare function normalizeCommonSettingsStore(input: CommonSettingsStore | undefined): CommonSettingsStore;
7
+ export declare function parseCommonSettingsStore(raw: string): CommonSettingsStore;
8
+ export declare function commonSettingsPath(): string;
9
+ export declare function readCommonSettingsStore(options?: {
10
+ filePath?: string;
11
+ }): Promise<CommonSettingsStore>;
12
+ export declare function readCommonSettingsStoreSync(options?: {
13
+ filePath?: string;
14
+ }): CommonSettingsStore | undefined;
15
+ export declare function writeCommonSettingsStore(store: CommonSettingsStore, options?: {
16
+ filePath?: string;
17
+ }): Promise<void>;