opencode-copilot-account-switcher 0.11.0 → 0.12.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.
@@ -0,0 +1,16 @@
1
+ export type OpenAIOAuthAuth = {
2
+ type: "oauth";
3
+ refresh?: string;
4
+ access?: string;
5
+ expires?: number;
6
+ accountId?: string;
7
+ };
8
+ export type CodexAuthSource = {
9
+ providerId: "openai";
10
+ oauth: OpenAIOAuthAuth;
11
+ accountId?: string;
12
+ suggestedWriteBack?: {
13
+ accountId: string;
14
+ };
15
+ };
16
+ export declare function resolveCodexAuthSource(auth: unknown): CodexAuthSource | undefined;
@@ -0,0 +1,63 @@
1
+ function asRecord(input) {
2
+ if (!input || typeof input !== "object" || Array.isArray(input))
3
+ return undefined;
4
+ return input;
5
+ }
6
+ function readString(input) {
7
+ return typeof input === "string" && input.length > 0 ? input : undefined;
8
+ }
9
+ function decodeJwtPayload(token) {
10
+ const parts = token.split(".");
11
+ if (parts.length < 2)
12
+ return undefined;
13
+ const payloadPart = parts[1];
14
+ if (!payloadPart)
15
+ return undefined;
16
+ try {
17
+ const json = Buffer.from(payloadPart, "base64url").toString("utf8");
18
+ return asRecord(JSON.parse(json));
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ function extractAccountIdFromClaims(access) {
25
+ if (!access)
26
+ return undefined;
27
+ const claims = decodeJwtPayload(access);
28
+ if (!claims)
29
+ return undefined;
30
+ return readString(claims.accountId) ?? readString(claims.account_id);
31
+ }
32
+ export function resolveCodexAuthSource(auth) {
33
+ const authRecord = asRecord(auth);
34
+ if (!authRecord)
35
+ return undefined;
36
+ const openai = asRecord(authRecord.openai);
37
+ if (!openai || openai.type !== "oauth")
38
+ return undefined;
39
+ const oauth = openai;
40
+ const directAccountId = readString(oauth.accountId);
41
+ if (directAccountId) {
42
+ return {
43
+ providerId: "openai",
44
+ oauth,
45
+ accountId: directAccountId,
46
+ };
47
+ }
48
+ const accountIdFromClaims = extractAccountIdFromClaims(readString(oauth.access));
49
+ if (!accountIdFromClaims) {
50
+ return {
51
+ providerId: "openai",
52
+ oauth,
53
+ };
54
+ }
55
+ return {
56
+ providerId: "openai",
57
+ oauth,
58
+ accountId: accountIdFromClaims,
59
+ suggestedWriteBack: {
60
+ accountId: accountIdFromClaims,
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,53 @@
1
+ import { type OpenAIOAuthAuth } from "./codex-auth-source.js";
2
+ import { type CodexStatusFetcherResult } from "./codex-status-fetcher.js";
3
+ import { type CodexStoreFile } from "./codex-store.js";
4
+ type ToastVariant = "info" | "success" | "warning" | "error";
5
+ type ToastClient = {
6
+ tui?: {
7
+ showToast?: (options: {
8
+ body: {
9
+ message: string;
10
+ variant: ToastVariant;
11
+ };
12
+ query?: undefined;
13
+ }) => Promise<unknown>;
14
+ };
15
+ auth?: {
16
+ get?: (input: {
17
+ path: {
18
+ id: string;
19
+ };
20
+ throwOnError?: boolean;
21
+ }) => Promise<unknown>;
22
+ set?: (input: {
23
+ path: {
24
+ id: string;
25
+ };
26
+ body: {
27
+ type: "oauth";
28
+ refresh?: string;
29
+ access?: string;
30
+ expires?: number;
31
+ accountId?: string;
32
+ };
33
+ }) => Promise<unknown>;
34
+ };
35
+ };
36
+ type AuthPayload = {
37
+ openai?: OpenAIOAuthAuth;
38
+ } & Record<string, unknown>;
39
+ export declare class CodexStatusCommandHandledError extends Error {
40
+ constructor();
41
+ }
42
+ export declare function handleCodexStatusCommand(input: {
43
+ client?: ToastClient;
44
+ loadAuth?: () => Promise<AuthPayload | undefined>;
45
+ persistAuth?: (auth: AuthPayload) => Promise<void>;
46
+ fetchStatus?: (input: {
47
+ oauth: OpenAIOAuthAuth;
48
+ accountId?: string;
49
+ }) => Promise<CodexStatusFetcherResult>;
50
+ readStore?: () => Promise<CodexStoreFile>;
51
+ writeStore?: (store: CodexStoreFile) => Promise<void>;
52
+ }): Promise<never>;
53
+ export {};
@@ -0,0 +1,215 @@
1
+ import { resolveCodexAuthSource } from "./codex-auth-source.js";
2
+ import { fetchCodexStatus } from "./codex-status-fetcher.js";
3
+ import { readCodexStore, writeCodexStore } from "./codex-store.js";
4
+ export class CodexStatusCommandHandledError extends Error {
5
+ constructor() {
6
+ super("codex-status-command-handled");
7
+ this.name = "CodexStatusCommandHandledError";
8
+ }
9
+ }
10
+ function asRecord(value) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ return undefined;
13
+ return value;
14
+ }
15
+ function pickString(value) {
16
+ return typeof value === "string" && value.length > 0 ? value : undefined;
17
+ }
18
+ function pickNumber(value) {
19
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
20
+ }
21
+ async function showToast(input) {
22
+ const show = input.client?.tui?.showToast;
23
+ if (!show)
24
+ return;
25
+ try {
26
+ await show({
27
+ body: {
28
+ message: input.message,
29
+ variant: input.variant,
30
+ },
31
+ });
32
+ }
33
+ catch {
34
+ // fail open for toast dispatch
35
+ }
36
+ }
37
+ function ratio(remaining, entitlement) {
38
+ if (remaining === undefined && entitlement === undefined)
39
+ return "n/a";
40
+ return `${remaining ?? "n/a"}/${entitlement ?? "n/a"}`;
41
+ }
42
+ function value(value) {
43
+ return value === undefined ? "n/a" : String(value);
44
+ }
45
+ function renderStatus(status) {
46
+ return [
47
+ "Codex status updated.",
48
+ "[identity]",
49
+ `account: ${value(status.identity.accountId)}`,
50
+ `email: ${value(status.identity.email)}`,
51
+ `plan: ${value(status.identity.plan)}`,
52
+ "[usage]",
53
+ `primary: ${ratio(status.windows.primary.remaining, status.windows.primary.entitlement)}`,
54
+ `secondary: ${ratio(status.windows.secondary.remaining, status.windows.secondary.entitlement)}`,
55
+ `credits: ${ratio(status.credits.remaining, status.credits.total)}`,
56
+ ].join("\n");
57
+ }
58
+ function renderCachedStatus(store) {
59
+ return [
60
+ "[identity]",
61
+ `account: ${value(store.account?.id ?? store.activeAccountId)}`,
62
+ `email: ${value(store.account?.email ?? store.activeEmail)}`,
63
+ `plan: ${value(store.account?.plan)}`,
64
+ "[usage]",
65
+ `primary: ${ratio(store.status?.premium?.remaining, store.status?.premium?.entitlement)}`,
66
+ "secondary: n/a",
67
+ "credits: n/a",
68
+ ].join("\n");
69
+ }
70
+ function hasCachedStore(store) {
71
+ return Boolean(store.activeAccountId
72
+ || store.activeEmail
73
+ || store.account?.id
74
+ || store.account?.email
75
+ || store.account?.plan
76
+ || store.status?.premium?.entitlement !== undefined
77
+ || store.status?.premium?.remaining !== undefined);
78
+ }
79
+ async function defaultLoadAuth(client) {
80
+ const getAuth = client?.auth?.get;
81
+ if (!getAuth)
82
+ return undefined;
83
+ try {
84
+ const result = await getAuth({ path: { id: "openai" }, throwOnError: true });
85
+ const withData = asRecord(result)?.data;
86
+ const payload = asRecord(withData) ?? asRecord(result);
87
+ if (!payload)
88
+ return undefined;
89
+ return {
90
+ openai: payload,
91
+ };
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ async function defaultPersistAuth(client, auth) {
98
+ const setAuth = client?.auth?.set;
99
+ if (!setAuth)
100
+ return;
101
+ const openai = asRecord(auth.openai);
102
+ if (!openai)
103
+ return;
104
+ await setAuth({
105
+ path: { id: "openai" },
106
+ body: {
107
+ type: "oauth",
108
+ refresh: pickString(openai.refresh),
109
+ access: pickString(openai.access),
110
+ expires: pickNumber(openai.expires),
111
+ accountId: pickString(openai.accountId),
112
+ },
113
+ });
114
+ }
115
+ function patchAuth(auth, patch) {
116
+ const base = asRecord(auth) ?? {};
117
+ const openai = asRecord(base.openai) ?? { type: "oauth" };
118
+ return {
119
+ ...base,
120
+ openai: {
121
+ ...openai,
122
+ type: "oauth",
123
+ ...(patch.access !== undefined ? { access: patch.access } : {}),
124
+ ...(patch.refresh !== undefined ? { refresh: patch.refresh } : {}),
125
+ ...(patch.expires !== undefined ? { expires: patch.expires } : {}),
126
+ ...(patch.accountId !== undefined ? { accountId: patch.accountId } : {}),
127
+ },
128
+ };
129
+ }
130
+ export async function handleCodexStatusCommand(input) {
131
+ const loadAuth = input.loadAuth ?? (() => defaultLoadAuth(input.client));
132
+ const persistAuth = input.persistAuth ?? ((nextAuth) => defaultPersistAuth(input.client, nextAuth));
133
+ const fetchStatus = input.fetchStatus ?? ((next) => fetchCodexStatus(next));
134
+ const readStore = input.readStore ?? (() => readCodexStore());
135
+ const writeStore = input.writeStore ?? ((store) => writeCodexStore(store));
136
+ await showToast({
137
+ client: input.client,
138
+ message: "Fetching Codex status...",
139
+ variant: "info",
140
+ });
141
+ const auth = await loadAuth().catch(() => undefined);
142
+ const source = resolveCodexAuthSource(auth);
143
+ if (!source) {
144
+ await showToast({
145
+ client: input.client,
146
+ message: "OpenAI OAuth auth is missing for Codex status.",
147
+ variant: "error",
148
+ });
149
+ throw new CodexStatusCommandHandledError();
150
+ }
151
+ const fetched = await fetchStatus({
152
+ oauth: source.oauth,
153
+ accountId: source.accountId,
154
+ }).catch((error) => ({
155
+ ok: false,
156
+ error: {
157
+ kind: "network_error",
158
+ message: error instanceof Error ? error.message : String(error),
159
+ },
160
+ }));
161
+ if (!fetched.ok) {
162
+ const cached = await readStore().catch(() => ({}));
163
+ if (hasCachedStore(cached)) {
164
+ await showToast({
165
+ client: input.client,
166
+ message: `Codex status fetch failed (${fetched.error.message}); showing cached snapshot.\n${renderCachedStatus(cached)}`,
167
+ variant: "warning",
168
+ });
169
+ }
170
+ else {
171
+ await showToast({
172
+ client: input.client,
173
+ message: `Codex status fetch failed: ${fetched.error.message}`,
174
+ variant: "error",
175
+ });
176
+ }
177
+ throw new CodexStatusCommandHandledError();
178
+ }
179
+ if (fetched.authPatch) {
180
+ const nextAuth = patchAuth(auth, fetched.authPatch);
181
+ await persistAuth(nextAuth).catch(async (error) => {
182
+ await showToast({
183
+ client: input.client,
184
+ message: `Codex auth refresh succeeded but auth persistence failed: ${error instanceof Error ? error.message : String(error)}`,
185
+ variant: "warning",
186
+ });
187
+ });
188
+ }
189
+ const previousStore = await readStore().catch(() => ({}));
190
+ const nextStore = {
191
+ ...previousStore,
192
+ activeProvider: "codex",
193
+ activeAccountId: fetched.status.identity.accountId ?? source.accountId ?? previousStore.activeAccountId,
194
+ activeEmail: fetched.status.identity.email ?? previousStore.activeEmail,
195
+ lastStatusRefresh: fetched.status.updatedAt,
196
+ account: {
197
+ id: fetched.status.identity.accountId ?? previousStore.account?.id,
198
+ email: fetched.status.identity.email ?? previousStore.account?.email,
199
+ plan: fetched.status.identity.plan ?? previousStore.account?.plan,
200
+ },
201
+ status: {
202
+ premium: {
203
+ entitlement: fetched.status.windows.primary.entitlement,
204
+ remaining: fetched.status.windows.primary.remaining,
205
+ },
206
+ },
207
+ };
208
+ await writeStore(nextStore);
209
+ await showToast({
210
+ client: input.client,
211
+ message: renderStatus(fetched.status),
212
+ variant: "success",
213
+ });
214
+ throw new CodexStatusCommandHandledError();
215
+ }
@@ -0,0 +1,66 @@
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: "rate_limited";
27
+ status: 429;
28
+ message: string;
29
+ } | {
30
+ kind: "timeout";
31
+ message: string;
32
+ } | {
33
+ kind: "server_error";
34
+ status: number;
35
+ message: string;
36
+ } | {
37
+ kind: "invalid_response";
38
+ message: string;
39
+ } | {
40
+ kind: "unauthorized";
41
+ status: 401;
42
+ message: string;
43
+ } | {
44
+ kind: "network_error";
45
+ message: string;
46
+ };
47
+ export type CodexStatusFetcherResult = {
48
+ ok: true;
49
+ status: CodexStatusSnapshot;
50
+ authPatch?: {
51
+ access?: string;
52
+ refresh?: string;
53
+ expires?: number;
54
+ accountId?: string;
55
+ };
56
+ } | {
57
+ ok: false;
58
+ error: CodexStatusError;
59
+ };
60
+ export declare function fetchCodexStatus(input: {
61
+ oauth: OpenAIOAuthAuth;
62
+ accountId?: string;
63
+ fetchImpl?: typeof globalThis.fetch;
64
+ now?: () => number;
65
+ refreshTokens?: (oauth: OpenAIOAuthAuth) => Promise<OpenAIOAuthAuth | undefined>;
66
+ }): Promise<CodexStatusFetcherResult>;
@@ -0,0 +1,232 @@
1
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
2
+ function asRecord(input) {
3
+ if (!input || typeof input !== "object" || Array.isArray(input))
4
+ return undefined;
5
+ return input;
6
+ }
7
+ function readString(input) {
8
+ return typeof input === "string" && input.length > 0 ? input : undefined;
9
+ }
10
+ function readNumber(input) {
11
+ return typeof input === "number" && Number.isFinite(input) ? input : undefined;
12
+ }
13
+ function pickRecord(source, keys) {
14
+ for (const key of keys) {
15
+ const value = asRecord(source[key]);
16
+ if (value)
17
+ return value;
18
+ }
19
+ return undefined;
20
+ }
21
+ function pickWindow(source, key) {
22
+ const windows = pickRecord(source, ["windows", "usage_windows", "quota_windows"]);
23
+ const block = windows ? asRecord(windows[key]) : undefined;
24
+ const fallback = asRecord(source[key]);
25
+ const raw = block ?? fallback;
26
+ if (!raw) {
27
+ return {
28
+ entitlement: undefined,
29
+ remaining: undefined,
30
+ used: undefined,
31
+ resetAt: undefined,
32
+ };
33
+ }
34
+ return {
35
+ entitlement: readNumber(raw.entitlement),
36
+ remaining: readNumber(raw.remaining),
37
+ used: readNumber(raw.used),
38
+ resetAt: readNumber(raw.resetAt ?? raw.reset_at),
39
+ };
40
+ }
41
+ function normalizeUsageStatus(payload, now) {
42
+ const source = asRecord(payload) ?? {};
43
+ const account = pickRecord(source, ["account", "identity", "user"]) ?? {};
44
+ const credits = pickRecord(source, ["credits", "credit_balance", "credit"]) ?? {};
45
+ return {
46
+ identity: {
47
+ accountId: readString(account.id) ?? readString(source.account_id) ?? readString(source.accountId),
48
+ email: readString(account.email) ?? readString(source.email),
49
+ plan: readString(account.plan) ?? readString(source.plan),
50
+ },
51
+ windows: {
52
+ primary: pickWindow(source, "primary"),
53
+ secondary: pickWindow(source, "secondary"),
54
+ },
55
+ credits: {
56
+ total: readNumber(credits.total),
57
+ remaining: readNumber(credits.remaining),
58
+ used: readNumber(credits.used),
59
+ },
60
+ updatedAt: now(),
61
+ };
62
+ }
63
+ async function parseJsonResponse(response) {
64
+ const contentType = response.headers.get("content-type") ?? "";
65
+ if (!contentType.toLowerCase().includes("application/json"))
66
+ return undefined;
67
+ try {
68
+ return await response.json();
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ function isTimeoutError(error) {
75
+ if (!error || typeof error !== "object")
76
+ return false;
77
+ const err = error;
78
+ if (err.name === "AbortError")
79
+ return true;
80
+ const message = typeof err.message === "string" ? err.message.toLowerCase() : "";
81
+ return message.includes("timeout");
82
+ }
83
+ function buildHeaders(input) {
84
+ const headers = new Headers({
85
+ Accept: "application/json",
86
+ "User-Agent": "Codex CLI",
87
+ });
88
+ if (input.access)
89
+ headers.set("Authorization", `Bearer ${input.access}`);
90
+ if (input.accountId)
91
+ headers.set("ChatGPT-Account-Id", input.accountId);
92
+ return headers;
93
+ }
94
+ async function requestUsage(input) {
95
+ return input.fetchImpl(CODEX_USAGE_URL, {
96
+ method: "GET",
97
+ headers: buildHeaders({
98
+ access: input.oauth.access,
99
+ accountId: input.accountId,
100
+ }),
101
+ });
102
+ }
103
+ export async function fetchCodexStatus(input) {
104
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
105
+ const now = input.now ?? Date.now;
106
+ const explicitAccountId = input.accountId;
107
+ let oauth = input.oauth;
108
+ let authPatch;
109
+ for (let attempt = 0; attempt < 2; attempt += 1) {
110
+ let response;
111
+ try {
112
+ response = await requestUsage({
113
+ oauth,
114
+ accountId: explicitAccountId ?? oauth.accountId,
115
+ fetchImpl,
116
+ });
117
+ }
118
+ catch (error) {
119
+ if (isTimeoutError(error)) {
120
+ return {
121
+ ok: false,
122
+ error: {
123
+ kind: "timeout",
124
+ message: "codex usage request timed out",
125
+ },
126
+ };
127
+ }
128
+ return {
129
+ ok: false,
130
+ error: {
131
+ kind: "network_error",
132
+ message: error instanceof Error ? error.message : String(error),
133
+ },
134
+ };
135
+ }
136
+ if (response.status === 401 && attempt === 0 && input.refreshTokens) {
137
+ let refreshed;
138
+ try {
139
+ refreshed = await input.refreshTokens(oauth);
140
+ }
141
+ catch (error) {
142
+ return {
143
+ ok: false,
144
+ error: {
145
+ kind: "network_error",
146
+ message: error instanceof Error ? error.message : String(error),
147
+ },
148
+ };
149
+ }
150
+ if (!refreshed || !refreshed.access) {
151
+ return {
152
+ ok: false,
153
+ error: {
154
+ kind: "unauthorized",
155
+ status: 401,
156
+ message: "codex usage request unauthorized",
157
+ },
158
+ };
159
+ }
160
+ oauth = refreshed;
161
+ authPatch = {
162
+ access: refreshed.access,
163
+ refresh: refreshed.refresh,
164
+ expires: refreshed.expires,
165
+ accountId: refreshed.accountId,
166
+ };
167
+ continue;
168
+ }
169
+ if (response.status === 429) {
170
+ return {
171
+ ok: false,
172
+ error: {
173
+ kind: "rate_limited",
174
+ status: 429,
175
+ message: "codex usage request was rate limited",
176
+ },
177
+ };
178
+ }
179
+ if (response.status >= 500 && response.status <= 599) {
180
+ return {
181
+ ok: false,
182
+ error: {
183
+ kind: "server_error",
184
+ status: response.status,
185
+ message: "codex usage request failed with server error",
186
+ },
187
+ };
188
+ }
189
+ if (response.status === 401) {
190
+ return {
191
+ ok: false,
192
+ error: {
193
+ kind: "unauthorized",
194
+ status: 401,
195
+ message: "codex usage request unauthorized",
196
+ },
197
+ };
198
+ }
199
+ if (!response.ok) {
200
+ return {
201
+ ok: false,
202
+ error: {
203
+ kind: "network_error",
204
+ message: `codex usage request failed with status ${response.status}`,
205
+ },
206
+ };
207
+ }
208
+ const payload = await parseJsonResponse(response);
209
+ if (payload === undefined) {
210
+ return {
211
+ ok: false,
212
+ error: {
213
+ kind: "invalid_response",
214
+ message: "codex usage response was not json",
215
+ },
216
+ };
217
+ }
218
+ return {
219
+ ok: true,
220
+ status: normalizeUsageStatus(payload, now),
221
+ ...(authPatch ? { authPatch } : {}),
222
+ };
223
+ }
224
+ return {
225
+ ok: false,
226
+ error: {
227
+ kind: "unauthorized",
228
+ status: 401,
229
+ message: "codex usage request unauthorized",
230
+ },
231
+ };
232
+ }
@@ -0,0 +1,25 @@
1
+ export type CodexAccountSnapshot = {
2
+ id?: string;
3
+ email?: string;
4
+ plan?: string;
5
+ };
6
+ export type CodexStatusSnapshot = {
7
+ premium?: {
8
+ entitlement?: number;
9
+ remaining?: number;
10
+ };
11
+ };
12
+ export type CodexStoreFile = {
13
+ activeProvider?: string;
14
+ activeAccountId?: string;
15
+ activeEmail?: string;
16
+ lastStatusRefresh?: number;
17
+ account?: CodexAccountSnapshot;
18
+ status?: CodexStatusSnapshot;
19
+ };
20
+ export declare function parseCodexStore(raw: string): CodexStoreFile;
21
+ export declare function codexStorePath(): string;
22
+ export declare function readCodexStore(filePath?: string): Promise<CodexStoreFile>;
23
+ export declare function writeCodexStore(store: CodexStoreFile, options?: {
24
+ filePath?: string;
25
+ }): Promise<void>;