sidekick-shared 0.18.5 → 0.19.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/README.md +2 -2
- package/dist/accounts.d.ts +1 -0
- package/dist/codexProfiles.d.ts +3 -2
- package/dist/codexProfiles.js +382 -52
- package/dist/ensureDefaultAccounts.js +6 -0
- package/dist/modelContext.js +3 -1
- package/dist/modelInfo.js +69 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,11 +33,11 @@ npm install sidekick-shared
|
|
|
33
33
|
| **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
|
|
34
34
|
| **Schemas** | Zod schemas for runtime JSONL event validation (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`) |
|
|
35
35
|
| **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks) |
|
|
36
|
-
| **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Opus 4.7 / Sonnet 4.7 1M and GPT-5.x variants), pricing tables with optional LiteLLM hydration, null-aware cost (`calculateCost()`), provenance-preserving cost (`calculateCostWithProvenance()`, `mergeCostSources()`), and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
|
|
36
|
+
| **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x variants), pricing tables with optional LiteLLM hydration, null-aware cost (`calculateCost()`), provenance-preserving cost (`calculateCostWithProvenance()`, `mergeCostSources()`), and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
|
|
37
37
|
| **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
|
|
38
38
|
| **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex quota updates behind one typed `{ claude?, codex? }` event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback |
|
|
39
39
|
| **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, and `getActiveAccountStatus()` for a single-pass active-account read across providers |
|
|
40
|
-
| **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — with
|
|
40
|
+
| **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring |
|
|
41
41
|
| **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
|
|
42
42
|
| **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
|
|
43
43
|
|
package/dist/accounts.d.ts
CHANGED
package/dist/codexProfiles.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function getActiveCodexAccount(): SavedAccountProfile | null;
|
|
|
14
14
|
export declare function resolveSidekickCodexHome(): string;
|
|
15
15
|
export declare function getCodexExecutionEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
16
16
|
export declare function prepareCodexAccount(label: string): CodexAccountManagerResult;
|
|
17
|
-
export declare function finalizeCodexAccount(profileId: string):
|
|
18
|
-
export declare function switchToCodexAccount(profileId: string):
|
|
17
|
+
export declare function finalizeCodexAccount(profileId: string): CodexAccountManagerResult;
|
|
18
|
+
export declare function switchToCodexAccount(profileId: string): CodexAccountManagerResult;
|
|
19
|
+
export declare function reconcileCodexAuthState(): void;
|
|
19
20
|
export declare function removeCodexAccount(profileId: string): AccountManagerResult;
|
package/dist/codexProfiles.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.getCodexExecutionEnv = getCodexExecutionEnv;
|
|
|
44
44
|
exports.prepareCodexAccount = prepareCodexAccount;
|
|
45
45
|
exports.finalizeCodexAccount = finalizeCodexAccount;
|
|
46
46
|
exports.switchToCodexAccount = switchToCodexAccount;
|
|
47
|
+
exports.reconcileCodexAuthState = reconcileCodexAuthState;
|
|
47
48
|
exports.removeCodexAccount = removeCodexAccount;
|
|
48
49
|
const fs = __importStar(require("fs"));
|
|
49
50
|
const os = __importStar(require("os"));
|
|
@@ -51,6 +52,9 @@ const path = __importStar(require("path"));
|
|
|
51
52
|
const child_process_1 = require("child_process");
|
|
52
53
|
const crypto_1 = require("crypto");
|
|
53
54
|
const accountRegistry_1 = require("./accountRegistry");
|
|
55
|
+
// Codex refreshes OAuth tokens at most every 8 days; a stored refresh token
|
|
56
|
+
// older than that may already be rejected by the auth server.
|
|
57
|
+
const STALE_AUTH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
|
|
54
58
|
function getDefaultSystemCodexHome() {
|
|
55
59
|
return path.join(os.homedir(), '.codex');
|
|
56
60
|
}
|
|
@@ -77,12 +81,15 @@ function getCodexMonitoringHomes() {
|
|
|
77
81
|
const explicitHome = getExplicitCodexHome();
|
|
78
82
|
if (explicitHome)
|
|
79
83
|
return [explicitHome];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// The system home is the single live home; profile homes only matter for
|
|
85
|
+
// sessions recorded back when they doubled as live CODEX_HOMEs.
|
|
86
|
+
const homes = [getDefaultSystemCodexHome()];
|
|
87
|
+
for (const profile of listCodexAccounts()) {
|
|
88
|
+
const profileHome = getCodexProfileHome(profile.id);
|
|
89
|
+
if (fs.existsSync(path.join(profileHome, 'sessions'))) {
|
|
90
|
+
homes.push(profileHome);
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
|
-
homes.push(getDefaultSystemCodexHome());
|
|
86
93
|
return dedupePaths(homes);
|
|
87
94
|
}
|
|
88
95
|
function getCodexProfilesDir() {
|
|
@@ -104,8 +111,43 @@ function atomicWriteJson(filePath, data, mode = 0o600) {
|
|
|
104
111
|
const tmp = filePath + '.tmp';
|
|
105
112
|
const json = JSON.stringify(data, null, 2);
|
|
106
113
|
JSON.parse(json);
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
|
|
116
|
+
fs.renameSync(tmp, filePath);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
try {
|
|
120
|
+
fs.unlinkSync(tmp);
|
|
121
|
+
}
|
|
122
|
+
catch { /* nothing to clean up */ }
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// auth.json must be copied byte-for-byte: re-serializing would drop fields
|
|
127
|
+
// added by newer codex versions, and the rotated refresh token inside is
|
|
128
|
+
// only valid in its freshest form.
|
|
129
|
+
function atomicWriteFile(filePath, content, mode = 0o600) {
|
|
130
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
131
|
+
const tmp = filePath + '.tmp';
|
|
132
|
+
try {
|
|
133
|
+
fs.writeFileSync(tmp, content, { encoding: 'utf8', mode });
|
|
134
|
+
fs.renameSync(tmp, filePath);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
try {
|
|
138
|
+
fs.unlinkSync(tmp);
|
|
139
|
+
}
|
|
140
|
+
catch { /* nothing to clean up */ }
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function readFileOrNull(filePath) {
|
|
145
|
+
try {
|
|
146
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
109
151
|
}
|
|
110
152
|
function readPendingProfile(profileId) {
|
|
111
153
|
try {
|
|
@@ -146,40 +188,65 @@ function parseJwtPayload(jwt) {
|
|
|
146
188
|
return null;
|
|
147
189
|
}
|
|
148
190
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return {};
|
|
191
|
+
function parseAuthJson(raw) {
|
|
192
|
+
if (!raw)
|
|
193
|
+
return null;
|
|
153
194
|
try {
|
|
154
|
-
|
|
155
|
-
const idToken = parsed.tokens?.id_token;
|
|
156
|
-
const claims = idToken ? parseJwtPayload(idToken) : null;
|
|
157
|
-
const profileClaims = claims?.['https://api.openai.com/profile'];
|
|
158
|
-
const authClaims = claims?.['https://api.openai.com/auth'];
|
|
159
|
-
const email = typeof claims?.email === 'string'
|
|
160
|
-
? claims.email
|
|
161
|
-
: typeof profileClaims?.email === 'string'
|
|
162
|
-
? profileClaims.email
|
|
163
|
-
: undefined;
|
|
164
|
-
const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
|
|
165
|
-
? authClaims.chatgpt_account_id
|
|
166
|
-
: parsed.tokens?.account_id;
|
|
167
|
-
const planType = typeof authClaims?.chatgpt_plan_type === 'string'
|
|
168
|
-
? authClaims.chatgpt_plan_type
|
|
169
|
-
: undefined;
|
|
170
|
-
const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key'
|
|
171
|
-
? 'api-key'
|
|
172
|
-
: 'chatgpt';
|
|
173
|
-
return {
|
|
174
|
-
email,
|
|
175
|
-
workspaceId,
|
|
176
|
-
planType,
|
|
177
|
-
authMode,
|
|
178
|
-
};
|
|
195
|
+
return JSON.parse(raw);
|
|
179
196
|
}
|
|
180
197
|
catch {
|
|
181
|
-
return
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function readAuthIdentityFromRaw(raw) {
|
|
202
|
+
const parsed = parseAuthJson(raw);
|
|
203
|
+
if (!parsed)
|
|
204
|
+
return null;
|
|
205
|
+
const idToken = parsed.tokens?.id_token;
|
|
206
|
+
const claims = idToken ? parseJwtPayload(idToken) : null;
|
|
207
|
+
const profileClaims = claims?.['https://api.openai.com/profile'];
|
|
208
|
+
const authClaims = claims?.['https://api.openai.com/auth'];
|
|
209
|
+
const email = typeof claims?.email === 'string'
|
|
210
|
+
? claims.email
|
|
211
|
+
: typeof profileClaims?.email === 'string'
|
|
212
|
+
? profileClaims.email
|
|
213
|
+
: undefined;
|
|
214
|
+
const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
|
|
215
|
+
? authClaims.chatgpt_account_id
|
|
216
|
+
: parsed.tokens?.account_id;
|
|
217
|
+
const planType = typeof authClaims?.chatgpt_plan_type === 'string'
|
|
218
|
+
? authClaims.chatgpt_plan_type
|
|
219
|
+
: undefined;
|
|
220
|
+
const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key'
|
|
221
|
+
? 'api-key'
|
|
222
|
+
: 'chatgpt';
|
|
223
|
+
return { email, workspaceId, planType, authMode };
|
|
224
|
+
}
|
|
225
|
+
function readLastRefresh(raw, fallbackPath) {
|
|
226
|
+
const parsed = parseAuthJson(raw);
|
|
227
|
+
if (parsed?.last_refresh) {
|
|
228
|
+
const ts = Date.parse(parsed.last_refresh);
|
|
229
|
+
if (!Number.isNaN(ts))
|
|
230
|
+
return ts;
|
|
182
231
|
}
|
|
232
|
+
if (fallbackPath) {
|
|
233
|
+
try {
|
|
234
|
+
return fs.statSync(fallbackPath).mtimeMs;
|
|
235
|
+
}
|
|
236
|
+
catch { /* fall through */ }
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function readMetadataFromAuthJson(codexHome) {
|
|
241
|
+
const identity = readAuthIdentityFromRaw(readFileOrNull(path.join(codexHome, 'auth.json')));
|
|
242
|
+
if (!identity)
|
|
243
|
+
return {};
|
|
244
|
+
return {
|
|
245
|
+
email: identity.email,
|
|
246
|
+
workspaceId: identity.workspaceId,
|
|
247
|
+
planType: identity.planType,
|
|
248
|
+
authMode: identity.authMode,
|
|
249
|
+
};
|
|
183
250
|
}
|
|
184
251
|
function readMetadataFromLegacyCredentials(codexHome) {
|
|
185
252
|
const legacyPath = path.join(codexHome, '.credentials.json');
|
|
@@ -219,6 +286,16 @@ function getCodexLoginStatus(codexHome) {
|
|
|
219
286
|
}
|
|
220
287
|
return { loggedIn: false };
|
|
221
288
|
}
|
|
289
|
+
function detectRunningCodexProcess() {
|
|
290
|
+
if (process.platform === 'win32')
|
|
291
|
+
return false;
|
|
292
|
+
try {
|
|
293
|
+
return (0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], { encoding: 'utf8' }).status === 0;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
222
299
|
function readCodexAccountMetadata(codexHome) {
|
|
223
300
|
const fromAuth = readMetadataFromAuthJson(codexHome);
|
|
224
301
|
if (fromAuth.email || fromAuth.workspaceId || fromAuth.planType || fromAuth.authMode) {
|
|
@@ -256,15 +333,8 @@ function getActiveCodexAccount() {
|
|
|
256
333
|
return (0, accountRegistry_1.getActiveSavedAccount)('codex');
|
|
257
334
|
}
|
|
258
335
|
function resolveSidekickCodexHome() {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return explicitHome;
|
|
262
|
-
const active = getActiveCodexAccount();
|
|
263
|
-
if (active) {
|
|
264
|
-
const managedHome = getCodexProfileHome(active.id);
|
|
265
|
-
if (fs.existsSync(managedHome))
|
|
266
|
-
return managedHome;
|
|
267
|
-
}
|
|
336
|
+
// Account switching swaps auth.json inside the system home, so the system
|
|
337
|
+
// home (or an explicit CODEX_HOME) is always the single live home.
|
|
268
338
|
return getSystemCodexHome();
|
|
269
339
|
}
|
|
270
340
|
function getCodexExecutionEnv(baseEnv = process.env) {
|
|
@@ -318,24 +388,284 @@ function finalizeCodexAccount(profileId) {
|
|
|
318
388
|
return { success: false, error: 'Codex profile is not authenticated yet.' };
|
|
319
389
|
}
|
|
320
390
|
const metadata = readCodexAccountMetadata(codexHome);
|
|
321
|
-
|
|
391
|
+
const profile = {
|
|
322
392
|
id: profileId,
|
|
323
393
|
providerId: 'codex',
|
|
324
394
|
label: pending.label,
|
|
325
395
|
email: metadata.email,
|
|
326
396
|
addedAt: pending.addedAt,
|
|
327
397
|
metadata,
|
|
328
|
-
}
|
|
329
|
-
(0, accountRegistry_1.
|
|
330
|
-
|
|
398
|
+
};
|
|
399
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
400
|
+
const hasCredentialFiles = fs.existsSync(path.join(codexHome, 'auth.json')) ||
|
|
401
|
+
fs.existsSync(path.join(codexHome, '.credentials.json'));
|
|
402
|
+
if (!hasCredentialFiles) {
|
|
403
|
+
// Authenticated via the OS keyring — there are no credential files to
|
|
404
|
+
// swap, so the registry pointer is all we can update.
|
|
405
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', profileId);
|
|
406
|
+
return {
|
|
407
|
+
success: true,
|
|
408
|
+
warning: 'Codex stores credentials in the OS keyring; sidekick cannot swap them per account, so `codex` keeps using the keyring credentials.',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return performCodexAuthSwap(profile);
|
|
412
|
+
}
|
|
413
|
+
function getCodexStashDir() {
|
|
414
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
415
|
+
}
|
|
416
|
+
function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
417
|
+
try {
|
|
418
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
419
|
+
let stashPath = null;
|
|
420
|
+
if (liveAuthRaw) {
|
|
421
|
+
stashPath = path.join(getCodexStashDir(), `auth-${stamp}.json`);
|
|
422
|
+
atomicWriteFile(stashPath, liveAuthRaw);
|
|
423
|
+
}
|
|
424
|
+
if (liveLegacyRaw) {
|
|
425
|
+
const legacyStashPath = path.join(getCodexStashDir(), `credentials-${stamp}.json`);
|
|
426
|
+
atomicWriteFile(legacyStashPath, liveLegacyRaw);
|
|
427
|
+
stashPath = stashPath ?? legacyStashPath;
|
|
428
|
+
}
|
|
429
|
+
return stashPath;
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function findProfileForIdentity(identity) {
|
|
436
|
+
const profiles = listCodexAccounts();
|
|
437
|
+
if (identity?.workspaceId) {
|
|
438
|
+
const byWorkspace = profiles.find(profile => profile.metadata?.workspaceId === identity.workspaceId);
|
|
439
|
+
if (byWorkspace)
|
|
440
|
+
return byWorkspace;
|
|
441
|
+
}
|
|
442
|
+
if (identity?.email) {
|
|
443
|
+
const byEmail = profiles.find(profile => (profile.email ?? profile.metadata?.email) === identity.email);
|
|
444
|
+
if (byEmail)
|
|
445
|
+
return byEmail;
|
|
446
|
+
}
|
|
447
|
+
if (!identity?.workspaceId && !identity?.email) {
|
|
448
|
+
// API-key auth or unparseable tokens carry no identity; assume the live
|
|
449
|
+
// file belongs to whichever account the registry says is active.
|
|
450
|
+
return getActiveCodexAccount();
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
// Codex rotates the refresh token whenever it refreshes auth.json, so the
|
|
455
|
+
// live file is always the freshest copy of its account. Before replacing it,
|
|
456
|
+
// preserve it in the matching profile's backup — or stash it if it belongs to
|
|
457
|
+
// no saved account. Best-effort: never throws.
|
|
458
|
+
function syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
459
|
+
if (!liveAuthRaw && !liveLegacyRaw)
|
|
460
|
+
return {};
|
|
461
|
+
try {
|
|
462
|
+
const identity = readAuthIdentityFromRaw(liveAuthRaw);
|
|
463
|
+
const profile = findProfileForIdentity(identity);
|
|
464
|
+
if (!profile) {
|
|
465
|
+
const stashPath = stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw);
|
|
466
|
+
return {
|
|
467
|
+
stashPath: stashPath ?? undefined,
|
|
468
|
+
warning: stashPath
|
|
469
|
+
? `Live Codex credentials did not match any saved account; stashed at ${stashPath}.`
|
|
470
|
+
: 'Live Codex credentials did not match any saved account and could not be stashed.',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const profileHome = getCodexProfileHome(profile.id);
|
|
474
|
+
if (liveAuthRaw)
|
|
475
|
+
atomicWriteFile(path.join(profileHome, 'auth.json'), liveAuthRaw);
|
|
476
|
+
if (liveLegacyRaw)
|
|
477
|
+
atomicWriteFile(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
|
|
478
|
+
try {
|
|
479
|
+
const metadata = readCodexAccountMetadata(profileHome);
|
|
480
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
481
|
+
...profile,
|
|
482
|
+
email: metadata.email ?? profile.email,
|
|
483
|
+
metadata: { ...profile.metadata, ...metadata },
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch { /* metadata refresh is best-effort */ }
|
|
487
|
+
return { syncedProfileId: profile.id };
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
return { warning: `Could not back up live Codex credentials: ${err}` };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function performCodexAuthSwap(target) {
|
|
494
|
+
const systemHome = getSystemCodexHome();
|
|
495
|
+
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
496
|
+
const liveLegacyPath = path.join(systemHome, '.credentials.json');
|
|
497
|
+
const liveAuthRaw = readFileOrNull(liveAuthPath);
|
|
498
|
+
const liveLegacyRaw = readFileOrNull(liveLegacyPath);
|
|
499
|
+
if (!liveAuthRaw && !liveLegacyRaw && getCodexLoginStatus(systemHome).loggedIn) {
|
|
500
|
+
return {
|
|
501
|
+
success: false,
|
|
502
|
+
error: 'Codex stores credentials in the OS keyring; file-based account switching is not supported. Set `cli_auth_credentials_store = "file"` in ~/.codex/config.toml and run `codex login` again.',
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const profileHome = getCodexProfileHome(target.id);
|
|
506
|
+
const targetAuthPath = path.join(profileHome, 'auth.json');
|
|
507
|
+
const targetAuthRaw = readFileOrNull(targetAuthPath);
|
|
508
|
+
const targetLegacyRaw = readFileOrNull(path.join(profileHome, '.credentials.json'));
|
|
509
|
+
const targetName = target.label ?? target.email ?? target.id;
|
|
510
|
+
if (!targetAuthRaw && !targetLegacyRaw) {
|
|
511
|
+
return { success: false, error: `No stored credentials for "${targetName}". Remove and re-add this account.` };
|
|
512
|
+
}
|
|
513
|
+
if (targetAuthRaw && !parseAuthJson(targetAuthRaw)) {
|
|
514
|
+
return { success: false, error: `Stored credentials for "${targetName}" are corrupted. Remove and re-add this account.` };
|
|
515
|
+
}
|
|
516
|
+
const warnings = [];
|
|
517
|
+
if (detectRunningCodexProcess()) {
|
|
518
|
+
warnings.push('A codex process appears to be running; restart codex sessions so they pick up the switched account.');
|
|
519
|
+
}
|
|
520
|
+
// If the live file already belongs to the target account it is the freshest
|
|
521
|
+
// copy (rotated refresh token included) — never replace it with a staler
|
|
522
|
+
// backup, which would permanently invalidate the login. Just refresh the
|
|
523
|
+
// backup and the registry pointer.
|
|
524
|
+
const liveIdentity = readAuthIdentityFromRaw(liveAuthRaw);
|
|
525
|
+
const targetIdentity = readAuthIdentityFromRaw(targetAuthRaw);
|
|
526
|
+
const targetWorkspaceId = target.metadata?.workspaceId ?? targetIdentity?.workspaceId;
|
|
527
|
+
const targetEmail = target.email ?? target.metadata?.email ?? targetIdentity?.email;
|
|
528
|
+
const liveMatchesTarget = Boolean((liveIdentity?.workspaceId && targetWorkspaceId && liveIdentity.workspaceId === targetWorkspaceId) ||
|
|
529
|
+
(liveIdentity?.email && targetEmail && liveIdentity.email === targetEmail) ||
|
|
530
|
+
(liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
|
|
531
|
+
(!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
|
|
532
|
+
if (liveMatchesTarget) {
|
|
533
|
+
try {
|
|
534
|
+
if (liveAuthRaw)
|
|
535
|
+
atomicWriteFile(targetAuthPath, liveAuthRaw);
|
|
536
|
+
if (liveLegacyRaw)
|
|
537
|
+
atomicWriteFile(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
|
|
538
|
+
const metadata = readCodexAccountMetadata(profileHome);
|
|
539
|
+
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
540
|
+
...target,
|
|
541
|
+
email: metadata.email ?? target.email,
|
|
542
|
+
metadata: { ...target.metadata, ...metadata },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
catch { /* backup refresh is best-effort */ }
|
|
546
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
547
|
+
return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
|
|
548
|
+
}
|
|
549
|
+
const targetLastRefresh = readLastRefresh(targetAuthRaw, targetAuthPath);
|
|
550
|
+
if (targetLastRefresh !== null && Date.now() - targetLastRefresh > STALE_AUTH_THRESHOLD_MS) {
|
|
551
|
+
warnings.push(`Stored credentials for "${targetName}" have not been refreshed in over 8 days; codex may ask you to log in again.`);
|
|
552
|
+
}
|
|
553
|
+
const syncBack = syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw);
|
|
554
|
+
if (syncBack.warning)
|
|
555
|
+
warnings.push(syncBack.warning);
|
|
556
|
+
const restoreLiveFiles = () => {
|
|
557
|
+
try {
|
|
558
|
+
if (liveAuthRaw)
|
|
559
|
+
atomicWriteFile(liveAuthPath, liveAuthRaw);
|
|
560
|
+
else
|
|
561
|
+
fs.rmSync(liveAuthPath, { force: true });
|
|
562
|
+
if (liveLegacyRaw)
|
|
563
|
+
atomicWriteFile(liveLegacyPath, liveLegacyRaw);
|
|
564
|
+
else
|
|
565
|
+
fs.rmSync(liveLegacyPath, { force: true });
|
|
566
|
+
}
|
|
567
|
+
catch { /* rollback is best-effort */ }
|
|
568
|
+
};
|
|
569
|
+
try {
|
|
570
|
+
if (targetAuthRaw) {
|
|
571
|
+
atomicWriteFile(liveAuthPath, targetAuthRaw);
|
|
572
|
+
if (targetLegacyRaw)
|
|
573
|
+
atomicWriteFile(liveLegacyPath, targetLegacyRaw);
|
|
574
|
+
else if (liveLegacyRaw)
|
|
575
|
+
fs.rmSync(liveLegacyPath, { force: true });
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
atomicWriteFile(liveLegacyPath, targetLegacyRaw);
|
|
579
|
+
if (liveAuthRaw)
|
|
580
|
+
fs.rmSync(liveAuthPath, { force: true });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
restoreLiveFiles();
|
|
585
|
+
return { success: false, error: `Failed to write Codex credentials: ${err}` };
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
restoreLiveFiles();
|
|
592
|
+
return { success: false, error: `Failed to update account registry: ${err}` };
|
|
593
|
+
}
|
|
594
|
+
return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
|
|
331
595
|
}
|
|
332
596
|
function switchToCodexAccount(profileId) {
|
|
333
597
|
const target = listCodexAccounts().find(account => account.id === profileId);
|
|
334
598
|
if (!target) {
|
|
335
599
|
return { success: false, error: `Codex account ${profileId} not found.` };
|
|
336
600
|
}
|
|
337
|
-
|
|
338
|
-
|
|
601
|
+
return performCodexAuthSwap(target);
|
|
602
|
+
}
|
|
603
|
+
// One-time migration for installs created when profile homes doubled as live
|
|
604
|
+
// CODEX_HOMEs: the active profile's auth.json may hold a fresher rotated
|
|
605
|
+
// refresh token than the system home. Best-effort: never throws.
|
|
606
|
+
function reconcileCodexAuthState() {
|
|
607
|
+
try {
|
|
608
|
+
const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
|
|
609
|
+
if (fs.existsSync(markerPath))
|
|
610
|
+
return;
|
|
611
|
+
const writeMarker = () => atomicWriteFile(markerPath, new Date().toISOString() + '\n');
|
|
612
|
+
const active = getActiveCodexAccount();
|
|
613
|
+
if (!active) {
|
|
614
|
+
writeMarker();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const profileHome = getCodexProfileHome(active.id);
|
|
618
|
+
const profileAuthPath = path.join(profileHome, 'auth.json');
|
|
619
|
+
const profileAuthRaw = readFileOrNull(profileAuthPath);
|
|
620
|
+
if (!profileAuthRaw) {
|
|
621
|
+
writeMarker();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const systemHome = getSystemCodexHome();
|
|
625
|
+
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
626
|
+
const liveAuthRaw = readFileOrNull(liveAuthPath);
|
|
627
|
+
if (!liveAuthRaw) {
|
|
628
|
+
// No live credentials (account was added via isolated login and never
|
|
629
|
+
// promoted). Promote the active profile's copy unless codex is logged
|
|
630
|
+
// in through the OS keyring.
|
|
631
|
+
if (!getCodexLoginStatus(systemHome).loggedIn) {
|
|
632
|
+
atomicWriteFile(liveAuthPath, profileAuthRaw);
|
|
633
|
+
}
|
|
634
|
+
writeMarker();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const liveIdentity = readAuthIdentityFromRaw(liveAuthRaw);
|
|
638
|
+
const profileIdentity = readAuthIdentityFromRaw(profileAuthRaw);
|
|
639
|
+
const sameIdentity = Boolean((liveIdentity?.workspaceId && profileIdentity?.workspaceId && liveIdentity.workspaceId === profileIdentity.workspaceId) ||
|
|
640
|
+
(liveIdentity?.email && liveIdentity.email === profileIdentity?.email));
|
|
641
|
+
if (sameIdentity) {
|
|
642
|
+
const liveRefresh = readLastRefresh(liveAuthRaw, liveAuthPath);
|
|
643
|
+
const profileRefresh = readLastRefresh(profileAuthRaw, profileAuthPath);
|
|
644
|
+
if (profileRefresh !== null && (liveRefresh === null || profileRefresh > liveRefresh)) {
|
|
645
|
+
// The profile copy was the live home under the old model and holds
|
|
646
|
+
// the valid rotated refresh token — promote it.
|
|
647
|
+
stashLiveCodexAuth(liveAuthRaw, null);
|
|
648
|
+
atomicWriteFile(liveAuthPath, profileAuthRaw);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
atomicWriteFile(profileAuthPath, liveAuthRaw);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
// The live credentials belong to a different account; the live state
|
|
656
|
+
// wins — point the registry at the matching saved profile if there is
|
|
657
|
+
// one, and refresh its backup.
|
|
658
|
+
const matching = findProfileForIdentity(liveIdentity);
|
|
659
|
+
if (matching && matching.id !== active.id) {
|
|
660
|
+
atomicWriteFile(path.join(getCodexProfileHome(matching.id), 'auth.json'), liveAuthRaw);
|
|
661
|
+
(0, accountRegistry_1.setActiveSavedAccount)('codex', matching.id);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
writeMarker();
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// Reconciliation must never break startup.
|
|
668
|
+
}
|
|
339
669
|
}
|
|
340
670
|
function removeCodexAccount(profileId) {
|
|
341
671
|
const removed = (0, accountRegistry_1.removeSavedAccountProfile)('codex', profileId);
|
|
@@ -96,5 +96,11 @@ function ensureDefaultCodexAccount(options) {
|
|
|
96
96
|
async function ensureDefaultAccounts(options) {
|
|
97
97
|
const claude = await ensureDefaultClaudeAccount(options);
|
|
98
98
|
const codex = ensureDefaultCodexAccount(options);
|
|
99
|
+
try {
|
|
100
|
+
(0, codexProfiles_1.reconcileCodexAuthState)();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logFailure(options, 'Codex auth reconciliation failed.', error);
|
|
104
|
+
}
|
|
99
105
|
return { claude, codex };
|
|
100
106
|
}
|
package/dist/modelContext.js
CHANGED
|
@@ -8,7 +8,9 @@ exports.DEFAULT_CONTEXT_WINDOW = void 0;
|
|
|
8
8
|
exports.getModelContextWindowSize = getModelContextWindowSize;
|
|
9
9
|
/** Known model context window sizes (in tokens). */
|
|
10
10
|
const MODEL_CONTEXT_SIZES = {
|
|
11
|
-
// Claude — native 1M context (Opus 4.6+, Sonnet 4.6+)
|
|
11
|
+
// Claude — native 1M context (Fable 5, Opus 4.6+, Sonnet 4.6+)
|
|
12
|
+
'claude-fable-5': 1_000_000,
|
|
13
|
+
'claude-opus-4-8': 1_000_000,
|
|
12
14
|
'claude-opus-4-7': 1_000_000,
|
|
13
15
|
'claude-opus-4-6': 1_000_000,
|
|
14
16
|
'claude-sonnet-4-7': 1_000_000,
|
package/dist/modelInfo.js
CHANGED
|
@@ -44,10 +44,26 @@ const modelContext_1 = require("./modelContext");
|
|
|
44
44
|
* Sources:
|
|
45
45
|
* - Anthropic: https://www.anthropic.com/pricing
|
|
46
46
|
* - OpenAI: https://openai.com/api/pricing/
|
|
47
|
-
* Snapshot taken: 2026-
|
|
47
|
+
* Snapshot taken: 2026-06-09. Runtime LiteLLM hydration refreshes this.
|
|
48
|
+
*
|
|
49
|
+
* Anthropic keys appear in both dashed (`claude-opus-4-8`, the real model-ID
|
|
50
|
+
* form) and dotted (`claude-opus-4.8`, the LiteLLM catalog form) spellings —
|
|
51
|
+
* prefix matching cannot bridge the two, so both are needed.
|
|
48
52
|
*/
|
|
49
53
|
const PRICING_TABLE = {
|
|
50
54
|
// ── Anthropic: Claude ──
|
|
55
|
+
'claude-fable-5': {
|
|
56
|
+
inputCostPerMillion: 10.0,
|
|
57
|
+
outputCostPerMillion: 50.0,
|
|
58
|
+
cacheWriteCostPerMillion: 12.5,
|
|
59
|
+
cacheReadCostPerMillion: 1.0,
|
|
60
|
+
},
|
|
61
|
+
'claude-haiku-4-5': {
|
|
62
|
+
inputCostPerMillion: 1.0,
|
|
63
|
+
outputCostPerMillion: 5.0,
|
|
64
|
+
cacheWriteCostPerMillion: 1.25,
|
|
65
|
+
cacheReadCostPerMillion: 0.1,
|
|
66
|
+
},
|
|
51
67
|
'claude-haiku-4.5': {
|
|
52
68
|
inputCostPerMillion: 1.0,
|
|
53
69
|
outputCostPerMillion: 5.0,
|
|
@@ -60,12 +76,24 @@ const PRICING_TABLE = {
|
|
|
60
76
|
cacheWriteCostPerMillion: 1.0,
|
|
61
77
|
cacheReadCostPerMillion: 0.08,
|
|
62
78
|
},
|
|
79
|
+
'claude-sonnet-4-6': {
|
|
80
|
+
inputCostPerMillion: 3.0,
|
|
81
|
+
outputCostPerMillion: 15.0,
|
|
82
|
+
cacheWriteCostPerMillion: 3.75,
|
|
83
|
+
cacheReadCostPerMillion: 0.3,
|
|
84
|
+
},
|
|
63
85
|
'claude-sonnet-4.6': {
|
|
64
86
|
inputCostPerMillion: 3.0,
|
|
65
87
|
outputCostPerMillion: 15.0,
|
|
66
88
|
cacheWriteCostPerMillion: 3.75,
|
|
67
89
|
cacheReadCostPerMillion: 0.3,
|
|
68
90
|
},
|
|
91
|
+
'claude-sonnet-4-5': {
|
|
92
|
+
inputCostPerMillion: 3.0,
|
|
93
|
+
outputCostPerMillion: 15.0,
|
|
94
|
+
cacheWriteCostPerMillion: 3.75,
|
|
95
|
+
cacheReadCostPerMillion: 0.3,
|
|
96
|
+
},
|
|
69
97
|
'claude-sonnet-4.5': {
|
|
70
98
|
inputCostPerMillion: 3.0,
|
|
71
99
|
outputCostPerMillion: 15.0,
|
|
@@ -78,11 +106,41 @@ const PRICING_TABLE = {
|
|
|
78
106
|
cacheWriteCostPerMillion: 3.75,
|
|
79
107
|
cacheReadCostPerMillion: 0.3,
|
|
80
108
|
},
|
|
109
|
+
'claude-opus-4-8': {
|
|
110
|
+
inputCostPerMillion: 5.0,
|
|
111
|
+
outputCostPerMillion: 25.0,
|
|
112
|
+
cacheWriteCostPerMillion: 6.25,
|
|
113
|
+
cacheReadCostPerMillion: 0.5,
|
|
114
|
+
},
|
|
115
|
+
'claude-opus-4.8': {
|
|
116
|
+
inputCostPerMillion: 5.0,
|
|
117
|
+
outputCostPerMillion: 25.0,
|
|
118
|
+
cacheWriteCostPerMillion: 6.25,
|
|
119
|
+
cacheReadCostPerMillion: 0.5,
|
|
120
|
+
},
|
|
121
|
+
'claude-opus-4-7': {
|
|
122
|
+
inputCostPerMillion: 5.0,
|
|
123
|
+
outputCostPerMillion: 25.0,
|
|
124
|
+
cacheWriteCostPerMillion: 6.25,
|
|
125
|
+
cacheReadCostPerMillion: 0.5,
|
|
126
|
+
},
|
|
127
|
+
'claude-opus-4.7': {
|
|
128
|
+
inputCostPerMillion: 5.0,
|
|
129
|
+
outputCostPerMillion: 25.0,
|
|
130
|
+
cacheWriteCostPerMillion: 6.25,
|
|
131
|
+
cacheReadCostPerMillion: 0.5,
|
|
132
|
+
},
|
|
133
|
+
'claude-opus-4-6': {
|
|
134
|
+
inputCostPerMillion: 5.0,
|
|
135
|
+
outputCostPerMillion: 25.0,
|
|
136
|
+
cacheWriteCostPerMillion: 6.25,
|
|
137
|
+
cacheReadCostPerMillion: 0.5,
|
|
138
|
+
},
|
|
81
139
|
'claude-opus-4.6': {
|
|
82
|
-
inputCostPerMillion:
|
|
83
|
-
outputCostPerMillion:
|
|
84
|
-
cacheWriteCostPerMillion:
|
|
85
|
-
cacheReadCostPerMillion:
|
|
140
|
+
inputCostPerMillion: 5.0,
|
|
141
|
+
outputCostPerMillion: 25.0,
|
|
142
|
+
cacheWriteCostPerMillion: 6.25,
|
|
143
|
+
cacheReadCostPerMillion: 0.5,
|
|
86
144
|
},
|
|
87
145
|
'claude-opus-4.5': {
|
|
88
146
|
inputCostPerMillion: 5.0,
|
|
@@ -90,6 +148,7 @@ const PRICING_TABLE = {
|
|
|
90
148
|
cacheWriteCostPerMillion: 6.25,
|
|
91
149
|
cacheReadCostPerMillion: 0.5,
|
|
92
150
|
},
|
|
151
|
+
// Opus 4.0 / 4.1 — pre-4.5 pricing tier
|
|
93
152
|
'claude-opus-4': {
|
|
94
153
|
inputCostPerMillion: 15.0,
|
|
95
154
|
outputCostPerMillion: 75.0,
|
|
@@ -199,7 +258,7 @@ function _clearPricingOverrides() {
|
|
|
199
258
|
overrideSortedKeys = [];
|
|
200
259
|
}
|
|
201
260
|
// ── Model ID Parsing ──
|
|
202
|
-
const CLAUDE_RE = /^claude-(haiku|sonnet|opus)-([0-9.]+)/i;
|
|
261
|
+
const CLAUDE_RE = /^claude-(haiku|sonnet|opus|fable)-([0-9.]+)/i;
|
|
203
262
|
const LEGACY_CLAUDE_RE = /^claude-([0-9]+(?:[-.][0-9]+)?)-(haiku|sonnet|opus)(?:-|$)/i;
|
|
204
263
|
const GPT_RE = /^gpt-([0-9][0-9.A-Za-z-]*)/i;
|
|
205
264
|
const O_SERIES_RE = /^o([0-9]+)(-mini|-pro)?/i;
|
|
@@ -390,9 +449,10 @@ function shortModelName(modelId) {
|
|
|
390
449
|
return modelId;
|
|
391
450
|
}
|
|
392
451
|
const CLAUDE_FAMILY_RANK = {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
452
|
+
fable: 0,
|
|
453
|
+
opus: 1,
|
|
454
|
+
sonnet: 2,
|
|
455
|
+
haiku: 3,
|
|
396
456
|
};
|
|
397
457
|
function versionRank(version) {
|
|
398
458
|
if (!version)
|
package/package.json
CHANGED