opencode-copilot-account-switcher 0.12.4 → 0.13.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.
- package/dist/codex-oauth.d.ts +39 -0
- package/dist/codex-oauth.js +316 -0
- package/dist/codex-status-command.js +85 -28
- package/dist/codex-store.d.ts +36 -13
- package/dist/codex-store.js +231 -39
- package/dist/menu-runtime.d.ts +69 -0
- package/dist/menu-runtime.js +108 -0
- package/dist/plugin.js +141 -682
- package/dist/providers/codex-menu-adapter.d.ts +47 -0
- package/dist/providers/codex-menu-adapter.js +307 -0
- package/dist/providers/copilot-menu-adapter.d.ts +65 -0
- package/dist/providers/copilot-menu-adapter.js +763 -0
- package/dist/providers/descriptor.js +7 -2
- package/dist/providers/registry.js +3 -2
- package/dist/ui/menu.d.ts +26 -2
- package/dist/ui/menu.js +194 -41
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type TokenResponse = {
|
|
2
|
+
id_token?: string;
|
|
3
|
+
access_token?: string;
|
|
4
|
+
refresh_token?: string;
|
|
5
|
+
expires_in?: number;
|
|
6
|
+
};
|
|
7
|
+
export type IdTokenClaims = {
|
|
8
|
+
chatgpt_account_id?: string;
|
|
9
|
+
organizations?: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
}>;
|
|
12
|
+
email?: string;
|
|
13
|
+
"https://api.openai.com/auth"?: {
|
|
14
|
+
chatgpt_account_id?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type CodexOAuthAccount = {
|
|
18
|
+
refresh?: string;
|
|
19
|
+
access?: string;
|
|
20
|
+
expires?: number;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
};
|
|
24
|
+
type OAuthMode = "browser" | "headless";
|
|
25
|
+
type RunCodexOAuthInput = {
|
|
26
|
+
now?: () => number;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
29
|
+
selectMode?: () => Promise<OAuthMode | undefined>;
|
|
30
|
+
runBrowserAuth?: () => Promise<TokenResponse>;
|
|
31
|
+
runDeviceAuth?: () => Promise<TokenResponse>;
|
|
32
|
+
openUrl?: (url: string) => Promise<void>;
|
|
33
|
+
log?: (message: string) => void;
|
|
34
|
+
};
|
|
35
|
+
export declare function parseJwtClaims(token: string): IdTokenClaims | undefined;
|
|
36
|
+
export declare function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined;
|
|
37
|
+
export declare function extractAccountId(tokens: TokenResponse): string | undefined;
|
|
38
|
+
export declare function runCodexOAuth(input?: RunCodexOAuthInput): Promise<CodexOAuthAccount | undefined>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output, platform } from "node:process";
|
|
7
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
8
|
+
const ISSUER = "https://auth.openai.com";
|
|
9
|
+
const OAUTH_PORT = 1455;
|
|
10
|
+
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
12
|
+
const USER_AGENT = `opencode-copilot-account-switcher (${platform} ${os.release()}; ${os.arch()})`;
|
|
13
|
+
const HTML_SUCCESS = `<!doctype html>
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<title>Codex Authorization Successful</title>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>Authorization Successful</h1>
|
|
20
|
+
<p>You can close this window and return to OpenCode.</p>
|
|
21
|
+
<script>
|
|
22
|
+
setTimeout(() => window.close(), 2000)
|
|
23
|
+
</script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>`;
|
|
26
|
+
const htmlError = (message) => `<!doctype html>
|
|
27
|
+
<html>
|
|
28
|
+
<head>
|
|
29
|
+
<title>Codex Authorization Failed</title>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<h1>Authorization Failed</h1>
|
|
33
|
+
<p>${message}</p>
|
|
34
|
+
</body>
|
|
35
|
+
</html>`;
|
|
36
|
+
function base64UrlEncode(input) {
|
|
37
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
38
|
+
return Buffer.from(bytes)
|
|
39
|
+
.toString("base64")
|
|
40
|
+
.replace(/\+/g, "-")
|
|
41
|
+
.replace(/\//g, "_")
|
|
42
|
+
.replace(/=+$/g, "");
|
|
43
|
+
}
|
|
44
|
+
function generateRandomString(length) {
|
|
45
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
46
|
+
const bytes = randomBytes(length);
|
|
47
|
+
return Array.from(bytes, (byte) => chars[byte % chars.length]).join("");
|
|
48
|
+
}
|
|
49
|
+
async function generatePKCE() {
|
|
50
|
+
const verifier = generateRandomString(43);
|
|
51
|
+
const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
|
|
52
|
+
return { verifier, challenge };
|
|
53
|
+
}
|
|
54
|
+
function generateState() {
|
|
55
|
+
return base64UrlEncode(randomBytes(32));
|
|
56
|
+
}
|
|
57
|
+
export function parseJwtClaims(token) {
|
|
58
|
+
const parts = token.split(".");
|
|
59
|
+
if (parts.length !== 3)
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function extractAccountIdFromClaims(claims) {
|
|
69
|
+
return (claims.chatgpt_account_id
|
|
70
|
+
|| claims["https://api.openai.com/auth"]?.chatgpt_account_id
|
|
71
|
+
|| claims.organizations?.[0]?.id);
|
|
72
|
+
}
|
|
73
|
+
export function extractAccountId(tokens) {
|
|
74
|
+
if (tokens.id_token) {
|
|
75
|
+
const claims = parseJwtClaims(tokens.id_token);
|
|
76
|
+
const accountId = claims && extractAccountIdFromClaims(claims);
|
|
77
|
+
if (accountId)
|
|
78
|
+
return accountId;
|
|
79
|
+
}
|
|
80
|
+
if (!tokens.access_token)
|
|
81
|
+
return undefined;
|
|
82
|
+
const claims = parseJwtClaims(tokens.access_token);
|
|
83
|
+
return claims ? extractAccountIdFromClaims(claims) : undefined;
|
|
84
|
+
}
|
|
85
|
+
function extractEmail(tokens) {
|
|
86
|
+
if (tokens.id_token) {
|
|
87
|
+
const claims = parseJwtClaims(tokens.id_token);
|
|
88
|
+
if (claims?.email)
|
|
89
|
+
return claims.email;
|
|
90
|
+
}
|
|
91
|
+
if (!tokens.access_token)
|
|
92
|
+
return undefined;
|
|
93
|
+
return parseJwtClaims(tokens.access_token)?.email;
|
|
94
|
+
}
|
|
95
|
+
function buildAuthorizeUrl(redirectUri, pkce, state) {
|
|
96
|
+
const params = new URLSearchParams({
|
|
97
|
+
response_type: "code",
|
|
98
|
+
client_id: CLIENT_ID,
|
|
99
|
+
redirect_uri: redirectUri,
|
|
100
|
+
scope: "openid profile email offline_access",
|
|
101
|
+
code_challenge: pkce.challenge,
|
|
102
|
+
code_challenge_method: "S256",
|
|
103
|
+
id_token_add_organizations: "true",
|
|
104
|
+
codex_cli_simplified_flow: "true",
|
|
105
|
+
state,
|
|
106
|
+
originator: "opencode",
|
|
107
|
+
});
|
|
108
|
+
return `${ISSUER}/oauth/authorize?${params.toString()}`;
|
|
109
|
+
}
|
|
110
|
+
async function promptText(message) {
|
|
111
|
+
const rl = createInterface({ input, output });
|
|
112
|
+
try {
|
|
113
|
+
return (await rl.question(message)).trim();
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
rl.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function selectModeDefault() {
|
|
120
|
+
const value = (await promptText("OpenAI/Codex login mode ([1] browser, [2] headless, Enter to cancel): ")).toLowerCase();
|
|
121
|
+
if (!value)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (value === "1" || value === "browser" || value === "b")
|
|
124
|
+
return "browser";
|
|
125
|
+
if (value === "2" || value === "headless" || value === "h" || value === "device")
|
|
126
|
+
return "headless";
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
async function openUrlDefault(url) {
|
|
130
|
+
if (process.platform === "win32") {
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
const child = spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", windowsHide: true });
|
|
133
|
+
child.on("error", reject);
|
|
134
|
+
child.on("exit", (code) => {
|
|
135
|
+
if (code && code !== 0)
|
|
136
|
+
reject(new Error(`failed to open browser: ${code}`));
|
|
137
|
+
else
|
|
138
|
+
resolve();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const command = process.platform === "darwin" ? "open" : "xdg-open";
|
|
144
|
+
await new Promise((resolve, reject) => {
|
|
145
|
+
const child = spawn(command, [url], { stdio: "ignore" });
|
|
146
|
+
child.on("error", reject);
|
|
147
|
+
child.on("exit", (code) => {
|
|
148
|
+
if (code && code !== 0)
|
|
149
|
+
reject(new Error(`failed to open browser: ${code}`));
|
|
150
|
+
else
|
|
151
|
+
resolve();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async function exchangeCodeForTokens(input) {
|
|
156
|
+
const response = await input.fetchImpl(`${ISSUER}/oauth/token`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
159
|
+
body: new URLSearchParams({
|
|
160
|
+
grant_type: "authorization_code",
|
|
161
|
+
code: input.code,
|
|
162
|
+
redirect_uri: input.redirectUri,
|
|
163
|
+
client_id: CLIENT_ID,
|
|
164
|
+
code_verifier: input.verifier,
|
|
165
|
+
}).toString(),
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Token exchange failed: ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
return response.json();
|
|
171
|
+
}
|
|
172
|
+
async function runBrowserAuthDefault(input) {
|
|
173
|
+
const pkce = await generatePKCE();
|
|
174
|
+
const state = generateState();
|
|
175
|
+
const redirectUri = `http://localhost:${OAUTH_PORT}/auth/callback`;
|
|
176
|
+
const authUrl = buildAuthorizeUrl(redirectUri, pkce, state);
|
|
177
|
+
const tokens = await new Promise((resolve, reject) => {
|
|
178
|
+
let closed = false;
|
|
179
|
+
const finish = (handler) => {
|
|
180
|
+
if (closed)
|
|
181
|
+
return;
|
|
182
|
+
closed = true;
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
void server.close(() => handler());
|
|
185
|
+
};
|
|
186
|
+
const respond = (res, status, body) => {
|
|
187
|
+
res.statusCode = status;
|
|
188
|
+
res.setHeader("Content-Type", "text/html");
|
|
189
|
+
res.end(body);
|
|
190
|
+
};
|
|
191
|
+
const server = createServer((req, res) => {
|
|
192
|
+
const url = new URL(req.url ?? "/", redirectUri);
|
|
193
|
+
if (url.pathname !== "/auth/callback") {
|
|
194
|
+
respond(res, 404, htmlError("Not found"));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const code = url.searchParams.get("code");
|
|
198
|
+
const returnedState = url.searchParams.get("state");
|
|
199
|
+
const error = url.searchParams.get("error");
|
|
200
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
201
|
+
if (error) {
|
|
202
|
+
const message = errorDescription || error;
|
|
203
|
+
respond(res, 400, htmlError(message));
|
|
204
|
+
finish(() => reject(new Error(message)));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!code) {
|
|
208
|
+
respond(res, 400, htmlError("Missing authorization code"));
|
|
209
|
+
finish(() => reject(new Error("Missing authorization code")));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (returnedState !== state) {
|
|
213
|
+
respond(res, 400, htmlError("Invalid state - potential CSRF attack"));
|
|
214
|
+
finish(() => reject(new Error("Invalid state - potential CSRF attack")));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
respond(res, 200, HTML_SUCCESS);
|
|
218
|
+
void exchangeCodeForTokens({
|
|
219
|
+
code,
|
|
220
|
+
redirectUri,
|
|
221
|
+
verifier: pkce.verifier,
|
|
222
|
+
fetchImpl: input.fetchImpl,
|
|
223
|
+
}).then((result) => finish(() => resolve(result)), (error) => finish(() => reject(error instanceof Error ? error : new Error(String(error)))));
|
|
224
|
+
});
|
|
225
|
+
server.on("error", reject);
|
|
226
|
+
server.listen(OAUTH_PORT, async () => {
|
|
227
|
+
try {
|
|
228
|
+
input.log("Opening browser for OpenAI/Codex authorization...");
|
|
229
|
+
await input.openUrl(authUrl);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
finish(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const timeout = setTimeout(() => {
|
|
236
|
+
finish(() => reject(new Error("OAuth callback timeout - authorization took too long")));
|
|
237
|
+
}, input.timeoutMs);
|
|
238
|
+
});
|
|
239
|
+
return tokens;
|
|
240
|
+
}
|
|
241
|
+
async function runDeviceAuthDefault(input) {
|
|
242
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
243
|
+
const deviceResponse = await input.fetchImpl(`${ISSUER}/api/accounts/deviceauth/usercode`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
"User-Agent": USER_AGENT,
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({ client_id: CLIENT_ID }),
|
|
250
|
+
});
|
|
251
|
+
if (!deviceResponse.ok)
|
|
252
|
+
throw new Error("Failed to initiate device authorization");
|
|
253
|
+
const deviceData = await deviceResponse.json();
|
|
254
|
+
const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000;
|
|
255
|
+
input.log(`Open ${ISSUER}/codex/device and enter code: ${deviceData.user_code}`);
|
|
256
|
+
while (true) {
|
|
257
|
+
if (Date.now() >= deadline) {
|
|
258
|
+
throw new Error("Device authorization timeout - authorization took too long");
|
|
259
|
+
}
|
|
260
|
+
const response = await input.fetchImpl(`${ISSUER}/api/accounts/deviceauth/token`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
"User-Agent": USER_AGENT,
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
device_auth_id: deviceData.device_auth_id,
|
|
268
|
+
user_code: deviceData.user_code,
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
if (response.ok) {
|
|
272
|
+
const data = await response.json();
|
|
273
|
+
return exchangeCodeForTokens({
|
|
274
|
+
code: data.authorization_code,
|
|
275
|
+
redirectUri: `${ISSUER}/deviceauth/callback`,
|
|
276
|
+
verifier: data.code_verifier,
|
|
277
|
+
fetchImpl: input.fetchImpl,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (response.status !== 403 && response.status !== 404) {
|
|
281
|
+
throw new Error(`Device authorization failed: ${response.status}`);
|
|
282
|
+
}
|
|
283
|
+
if (Date.now() + interval + OAUTH_POLLING_SAFETY_MARGIN_MS >= deadline) {
|
|
284
|
+
throw new Error("Device authorization timeout - authorization took too long");
|
|
285
|
+
}
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, interval + OAUTH_POLLING_SAFETY_MARGIN_MS));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function normalizeTokens(tokens, now) {
|
|
290
|
+
const refresh = tokens.refresh_token;
|
|
291
|
+
const access = tokens.access_token;
|
|
292
|
+
if (!refresh && !access)
|
|
293
|
+
return undefined;
|
|
294
|
+
return {
|
|
295
|
+
refresh,
|
|
296
|
+
access,
|
|
297
|
+
expires: now() + (tokens.expires_in ?? 3600) * 1000,
|
|
298
|
+
accountId: extractAccountId(tokens),
|
|
299
|
+
email: extractEmail(tokens),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export async function runCodexOAuth(input = {}) {
|
|
303
|
+
const now = input.now ?? Date.now;
|
|
304
|
+
const timeoutMs = input.timeoutMs ?? OAUTH_TIMEOUT_MS;
|
|
305
|
+
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
|
|
306
|
+
const selectMode = input.selectMode ?? selectModeDefault;
|
|
307
|
+
const openUrl = input.openUrl ?? openUrlDefault;
|
|
308
|
+
const log = input.log ?? console.log;
|
|
309
|
+
const mode = await selectMode();
|
|
310
|
+
if (!mode)
|
|
311
|
+
return undefined;
|
|
312
|
+
const runBrowserAuth = input.runBrowserAuth ?? (() => runBrowserAuthDefault({ fetchImpl, openUrl, log, timeoutMs }));
|
|
313
|
+
const runDeviceAuth = input.runDeviceAuth ?? (() => runDeviceAuthDefault({ fetchImpl, log, timeoutMs }));
|
|
314
|
+
const tokens = mode === "headless" ? await runDeviceAuth() : await runBrowserAuth();
|
|
315
|
+
return normalizeTokens(tokens, now);
|
|
316
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveCodexAuthSource } from "./codex-auth-source.js";
|
|
2
2
|
import { fetchCodexStatus } from "./codex-status-fetcher.js";
|
|
3
|
-
import { readCodexStore, writeCodexStore } from "./codex-store.js";
|
|
3
|
+
import { getActiveCodexAccount, normalizeCodexStore, readCodexStore, writeCodexStore, } from "./codex-store.js";
|
|
4
4
|
import { readAuth } from "./store.js";
|
|
5
5
|
export class CodexStatusCommandHandledError extends Error {
|
|
6
6
|
constructor() {
|
|
@@ -64,25 +64,58 @@ function renderStatus(status) {
|
|
|
64
64
|
].join("\n");
|
|
65
65
|
}
|
|
66
66
|
function renderCachedStatus(store) {
|
|
67
|
+
const active = getActiveCodexAccount(store);
|
|
68
|
+
const entry = active?.entry;
|
|
69
|
+
const snapshot = entry?.snapshot;
|
|
67
70
|
return [
|
|
68
71
|
"[identity]",
|
|
69
|
-
`account: ${value(
|
|
70
|
-
`email: ${value(
|
|
71
|
-
`plan: ${value(
|
|
72
|
+
`account: ${value(entry?.accountId ?? active?.name)}`,
|
|
73
|
+
`email: ${value(entry?.email)}`,
|
|
74
|
+
`plan: ${value(snapshot?.plan)}`,
|
|
72
75
|
"[usage]",
|
|
73
|
-
renderWindow("5h",
|
|
76
|
+
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
77
|
+
"week: n/a",
|
|
78
|
+
"credits: n/a",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
function getCachedAccountForSource(store, input) {
|
|
82
|
+
const accountId = input.accountId;
|
|
83
|
+
if (accountId) {
|
|
84
|
+
const match = Object.entries(store.accounts).find(([, entry]) => entry.accountId === accountId);
|
|
85
|
+
if (match) {
|
|
86
|
+
return {
|
|
87
|
+
name: match[0],
|
|
88
|
+
entry: match[1],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return getActiveCodexAccount(store);
|
|
93
|
+
}
|
|
94
|
+
function renderCachedStatusForAccount(store, input) {
|
|
95
|
+
const active = getCachedAccountForSource(store, input);
|
|
96
|
+
const entry = active?.entry;
|
|
97
|
+
const snapshot = entry?.snapshot;
|
|
98
|
+
return [
|
|
99
|
+
"[identity]",
|
|
100
|
+
`account: ${value(entry?.accountId ?? active?.name)}`,
|
|
101
|
+
`email: ${value(entry?.email)}`,
|
|
102
|
+
`plan: ${value(snapshot?.plan)}`,
|
|
103
|
+
"[usage]",
|
|
104
|
+
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
74
105
|
"week: n/a",
|
|
75
106
|
"credits: n/a",
|
|
76
107
|
].join("\n");
|
|
77
108
|
}
|
|
78
109
|
function hasCachedStore(store) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
||
|
|
84
|
-
||
|
|
85
|
-
||
|
|
110
|
+
const active = getActiveCodexAccount(store);
|
|
111
|
+
const entry = active?.entry;
|
|
112
|
+
const usage5h = entry?.snapshot?.usage5h;
|
|
113
|
+
return Boolean(active
|
|
114
|
+
|| entry?.accountId
|
|
115
|
+
|| entry?.email
|
|
116
|
+
|| entry?.snapshot?.plan
|
|
117
|
+
|| usage5h?.entitlement !== undefined
|
|
118
|
+
|| usage5h?.remaining !== undefined);
|
|
86
119
|
}
|
|
87
120
|
async function defaultLoadAuth(client) {
|
|
88
121
|
return defaultLoadAuthWithFallback({
|
|
@@ -197,11 +230,12 @@ export async function handleCodexStatusCommand(input) {
|
|
|
197
230
|
},
|
|
198
231
|
}));
|
|
199
232
|
if (!fetched.ok) {
|
|
200
|
-
const
|
|
233
|
+
const cachedRaw = await readStore().catch(() => ({}));
|
|
234
|
+
const cached = normalizeCodexStore(cachedRaw);
|
|
201
235
|
if (hasCachedStore(cached)) {
|
|
202
236
|
await showToast({
|
|
203
237
|
client: input.client,
|
|
204
|
-
message: `Codex status fetch failed (${fetched.error.message}); showing cached snapshot.\n${
|
|
238
|
+
message: `Codex status fetch failed (${fetched.error.message}); showing cached snapshot.\n${renderCachedStatusForAccount(cached, { accountId: source.accountId })}`,
|
|
205
239
|
variant: "warning",
|
|
206
240
|
});
|
|
207
241
|
}
|
|
@@ -224,22 +258,45 @@ export async function handleCodexStatusCommand(input) {
|
|
|
224
258
|
});
|
|
225
259
|
});
|
|
226
260
|
}
|
|
227
|
-
const
|
|
261
|
+
const previousRaw = await readStore().catch(() => ({}));
|
|
262
|
+
const previousStore = normalizeCodexStore(previousRaw);
|
|
263
|
+
const previousActive = getActiveCodexAccount(previousStore);
|
|
264
|
+
const nextActive = fetched.status.identity.accountId
|
|
265
|
+
?? source.accountId
|
|
266
|
+
?? previousActive?.entry.accountId
|
|
267
|
+
?? previousActive?.name
|
|
268
|
+
?? "default";
|
|
269
|
+
const previousEntry = previousStore.accounts[nextActive] ?? {};
|
|
228
270
|
const nextStore = {
|
|
229
271
|
...previousStore,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
272
|
+
active: nextActive,
|
|
273
|
+
lastSnapshotRefresh: fetched.status.updatedAt,
|
|
274
|
+
accounts: {
|
|
275
|
+
...previousStore.accounts,
|
|
276
|
+
[nextActive]: {
|
|
277
|
+
...previousEntry,
|
|
278
|
+
name: previousEntry.name ?? nextActive,
|
|
279
|
+
providerId: previousEntry.providerId ?? "codex",
|
|
280
|
+
accountId: fetched.status.identity.accountId ?? previousEntry.accountId ?? source.accountId,
|
|
281
|
+
email: fetched.status.identity.email ?? previousEntry.email,
|
|
282
|
+
lastUsed: fetched.status.updatedAt,
|
|
283
|
+
snapshot: {
|
|
284
|
+
...(previousEntry.snapshot ?? {}),
|
|
285
|
+
plan: fetched.status.identity.plan ?? previousEntry.snapshot?.plan,
|
|
286
|
+
usage5h: {
|
|
287
|
+
entitlement: fetched.status.windows.primary.entitlement,
|
|
288
|
+
remaining: fetched.status.windows.primary.remaining,
|
|
289
|
+
used: fetched.status.windows.primary.used,
|
|
290
|
+
resetAt: fetched.status.windows.primary.resetAt,
|
|
291
|
+
},
|
|
292
|
+
usageWeek: {
|
|
293
|
+
entitlement: fetched.status.windows.secondary.entitlement,
|
|
294
|
+
remaining: fetched.status.windows.secondary.remaining,
|
|
295
|
+
used: fetched.status.windows.secondary.used,
|
|
296
|
+
resetAt: fetched.status.windows.secondary.resetAt,
|
|
297
|
+
},
|
|
298
|
+
updatedAt: fetched.status.updatedAt,
|
|
299
|
+
},
|
|
243
300
|
},
|
|
244
301
|
},
|
|
245
302
|
};
|
package/dist/codex-store.d.ts
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
+
type CodexUsageWindow = {
|
|
2
|
+
entitlement?: number;
|
|
3
|
+
remaining?: number;
|
|
4
|
+
used?: number;
|
|
5
|
+
resetAt?: number;
|
|
6
|
+
};
|
|
1
7
|
export type CodexAccountSnapshot = {
|
|
2
|
-
id?: string;
|
|
3
|
-
email?: string;
|
|
4
8
|
plan?: string;
|
|
9
|
+
usage5h?: CodexUsageWindow;
|
|
10
|
+
usageWeek?: CodexUsageWindow;
|
|
11
|
+
updatedAt?: number;
|
|
12
|
+
error?: string;
|
|
5
13
|
};
|
|
6
|
-
export type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
export type CodexAccountEntry = {
|
|
15
|
+
name?: string;
|
|
16
|
+
providerId?: string;
|
|
17
|
+
refresh?: string;
|
|
18
|
+
access?: string;
|
|
19
|
+
expires?: number;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
email?: string;
|
|
22
|
+
addedAt?: number;
|
|
23
|
+
lastUsed?: number;
|
|
24
|
+
source?: string;
|
|
25
|
+
snapshot?: CodexAccountSnapshot;
|
|
11
26
|
};
|
|
12
27
|
export type CodexStoreFile = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
accounts: Record<string, CodexAccountEntry>;
|
|
29
|
+
active?: string;
|
|
30
|
+
activeAccountNames?: string[];
|
|
31
|
+
autoRefresh?: boolean;
|
|
32
|
+
refreshMinutes?: number;
|
|
33
|
+
lastSnapshotRefresh?: number;
|
|
34
|
+
bootstrapAuthImportTried?: boolean;
|
|
35
|
+
bootstrapAuthImportAt?: number;
|
|
19
36
|
};
|
|
37
|
+
export declare function normalizeCodexStore(input: unknown): CodexStoreFile;
|
|
20
38
|
export declare function parseCodexStore(raw: string): CodexStoreFile;
|
|
39
|
+
export declare function getActiveCodexAccount(store: CodexStoreFile): {
|
|
40
|
+
name: string;
|
|
41
|
+
entry: CodexAccountEntry;
|
|
42
|
+
} | undefined;
|
|
21
43
|
export declare function codexStorePath(): string;
|
|
22
44
|
export declare function readCodexStore(filePath?: string): Promise<CodexStoreFile>;
|
|
23
45
|
export declare function writeCodexStore(store: CodexStoreFile, options?: {
|
|
24
46
|
filePath?: string;
|
|
25
47
|
}): Promise<void>;
|
|
48
|
+
export {};
|