sidekick-shared 0.24.3 → 0.24.5
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/CHANGELOG.md +436 -0
- package/LICENSE +21 -0
- package/README.md +172 -60
- package/dist/accountManager.d.ts +13 -0
- package/dist/accountManager.js +60 -32
- package/dist/accountRegistry.js +59 -41
- package/dist/accounts.js +40 -19
- package/dist/autoSwitch.d.ts +2 -1
- package/dist/autoSwitch.js +32 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +3 -1
- package/dist/codexProfiles.d.ts +14 -0
- package/dist/codexProfiles.js +268 -116
- package/dist/context/sessionContext.js +2 -4
- package/dist/ensureDefaultAccounts.js +4 -4
- package/dist/index.d.ts +14 -7
- package/dist/index.js +33 -7
- package/dist/modelInfo.js +11 -2
- package/dist/node.d.ts +6 -0
- package/dist/node.js +8 -1
- package/dist/observedContextWindows.d.ts +20 -4
- package/dist/observedContextWindows.js +47 -7
- package/dist/parsers/codexHistory.d.ts +30 -0
- package/dist/parsers/codexHistory.js +118 -0
- package/dist/parsers/mcpToolName.d.ts +26 -0
- package/dist/parsers/mcpToolName.js +31 -0
- package/dist/parsers/sessionPathResolver.d.ts +11 -4
- package/dist/parsers/sessionPathResolver.js +11 -4
- package/dist/paths.d.ts +43 -4
- package/dist/paths.js +66 -6
- package/dist/peakHours.d.ts +16 -2
- package/dist/peakHours.js +18 -3
- package/dist/pricingCatalog.d.ts +7 -3
- package/dist/pricingCatalog.js +3 -2
- package/dist/providers/claudeCode.d.ts +2 -1
- package/dist/providers/claudeCode.js +18 -0
- package/dist/providers/codex.d.ts +22 -1
- package/dist/providers/codex.js +58 -0
- package/dist/providers/types.d.ts +5 -0
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +2 -2
- package/dist/quotaSnapshots.js +8 -77
- package/dist/sessionPreviews.d.ts +60 -0
- package/dist/sessionPreviews.js +234 -0
- package/dist/sessionTranscripts.js +10 -4
- package/dist/writers/atomic.d.ts +20 -0
- package/dist/writers/atomic.js +190 -19
- package/dist/zaiQuota.d.ts +87 -0
- package/dist/zaiQuota.js +49 -0
- package/dist/zaiQuotaWatcher.d.ts +7 -0
- package/dist/zaiQuotaWatcher.js +3 -0
- package/package.json +60 -2
package/dist/codexProfiles.js
CHANGED
|
@@ -38,16 +38,22 @@ exports.getCodexMonitoringHomes = getCodexMonitoringHomes;
|
|
|
38
38
|
exports.getCodexProfilesDir = getCodexProfilesDir;
|
|
39
39
|
exports.getCodexProfileHome = getCodexProfileHome;
|
|
40
40
|
exports.readCodexAccountMetadata = readCodexAccountMetadata;
|
|
41
|
+
exports.readCodexAccountMetadataAsync = readCodexAccountMetadataAsync;
|
|
41
42
|
exports.isCodexProfileAuthenticated = isCodexProfileAuthenticated;
|
|
43
|
+
exports.isCodexProfileAuthenticatedAsync = isCodexProfileAuthenticatedAsync;
|
|
42
44
|
exports.listCodexAccounts = listCodexAccounts;
|
|
43
45
|
exports.getActiveCodexAccount = getActiveCodexAccount;
|
|
44
46
|
exports.resolveSidekickCodexHome = resolveSidekickCodexHome;
|
|
45
47
|
exports.resolveActiveCodexAccount = resolveActiveCodexAccount;
|
|
46
48
|
exports.getCodexExecutionEnv = getCodexExecutionEnv;
|
|
47
49
|
exports.prepareCodexAccount = prepareCodexAccount;
|
|
50
|
+
exports.prepareCodexAccountAsync = prepareCodexAccountAsync;
|
|
48
51
|
exports.finalizeCodexAccount = finalizeCodexAccount;
|
|
52
|
+
exports.finalizeCodexAccountAsync = finalizeCodexAccountAsync;
|
|
49
53
|
exports.switchToCodexAccount = switchToCodexAccount;
|
|
54
|
+
exports.switchToCodexAccountAsync = switchToCodexAccountAsync;
|
|
50
55
|
exports.reconcileCodexAuthState = reconcileCodexAuthState;
|
|
56
|
+
exports.reconcileCodexAuthStateAsync = reconcileCodexAuthStateAsync;
|
|
51
57
|
exports.removeCodexAccount = removeCodexAccount;
|
|
52
58
|
const fs = __importStar(require("fs"));
|
|
53
59
|
const os = __importStar(require("os"));
|
|
@@ -55,6 +61,10 @@ const path = __importStar(require("path"));
|
|
|
55
61
|
const child_process_1 = require("child_process");
|
|
56
62
|
const crypto_1 = require("crypto");
|
|
57
63
|
const accountRegistry_1 = require("./accountRegistry");
|
|
64
|
+
// auth.json must be copied byte-for-byte (atomicWriteFile, not atomicWriteJson):
|
|
65
|
+
// re-serializing would drop fields added by newer codex versions, and the
|
|
66
|
+
// rotated refresh token inside is only valid in its freshest form.
|
|
67
|
+
const atomic_1 = require("./writers/atomic");
|
|
58
68
|
// Codex refreshes OAuth tokens at most every 8 days; a stored refresh token
|
|
59
69
|
// older than that may already be rejected by the auth server.
|
|
60
70
|
const STALE_AUTH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
|
|
@@ -110,44 +120,6 @@ function getCodexProfileStatePath(profileId) {
|
|
|
110
120
|
function ensureCodexProfileDirs(profileId) {
|
|
111
121
|
fs.mkdirSync(getCodexProfileHome(profileId), { recursive: true, mode: 0o700 });
|
|
112
122
|
}
|
|
113
|
-
function atomicWriteJson(filePath, data, mode = 0o600) {
|
|
114
|
-
const tmp = filePath + '.tmp';
|
|
115
|
-
const json = JSON.stringify(data, null, 2);
|
|
116
|
-
JSON.parse(json);
|
|
117
|
-
try {
|
|
118
|
-
fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
|
|
119
|
-
fs.renameSync(tmp, filePath);
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(tmp);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
/* nothing to clean up */
|
|
127
|
-
}
|
|
128
|
-
throw err;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// auth.json must be copied byte-for-byte: re-serializing would drop fields
|
|
132
|
-
// added by newer codex versions, and the rotated refresh token inside is
|
|
133
|
-
// only valid in its freshest form.
|
|
134
|
-
function atomicWriteFile(filePath, content, mode = 0o600) {
|
|
135
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
136
|
-
const tmp = filePath + '.tmp';
|
|
137
|
-
try {
|
|
138
|
-
fs.writeFileSync(tmp, content, { encoding: 'utf8', mode });
|
|
139
|
-
fs.renameSync(tmp, filePath);
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
try {
|
|
143
|
-
fs.unlinkSync(tmp);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
/* nothing to clean up */
|
|
147
|
-
}
|
|
148
|
-
throw err;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
123
|
function readFileOrNull(filePath) {
|
|
152
124
|
try {
|
|
153
125
|
return fs.readFileSync(filePath, 'utf8');
|
|
@@ -166,7 +138,7 @@ function readPendingProfile(profileId) {
|
|
|
166
138
|
}
|
|
167
139
|
function writePendingProfile(profileId, pending) {
|
|
168
140
|
ensureCodexProfileDirs(profileId);
|
|
169
|
-
|
|
141
|
+
(0, atomic_1.atomicWriteJsonSync)(getCodexProfileStatePath(profileId), pending);
|
|
170
142
|
}
|
|
171
143
|
function copyIfExists(source, destination) {
|
|
172
144
|
if (!fs.existsSync(source))
|
|
@@ -268,38 +240,63 @@ function readMetadataFromLegacyCredentials(codexHome) {
|
|
|
268
240
|
}
|
|
269
241
|
return {};
|
|
270
242
|
}
|
|
243
|
+
const PROBE_TIMEOUT_MS = 4000;
|
|
244
|
+
function parseCodexLoginStatusOutput(status, stdout) {
|
|
245
|
+
const trimmed = stdout.trim();
|
|
246
|
+
if (status === 0 && /^Logged in/i.test(trimmed)) {
|
|
247
|
+
if (/API key/i.test(trimmed)) {
|
|
248
|
+
return { loggedIn: true, authMode: 'api-key' };
|
|
249
|
+
}
|
|
250
|
+
if (/ChatGPT/i.test(trimmed)) {
|
|
251
|
+
return { loggedIn: true, authMode: 'chatgpt' };
|
|
252
|
+
}
|
|
253
|
+
return { loggedIn: true };
|
|
254
|
+
}
|
|
255
|
+
return { loggedIn: false };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Blocks the caller's event loop for up to {@link PROBE_TIMEOUT_MS}. Async code
|
|
259
|
+
* paths must use {@link getCodexLoginStatusAsync} instead — external consumers
|
|
260
|
+
* embed this package in processes where a blocked event loop freezes all IPC.
|
|
261
|
+
*/
|
|
271
262
|
function getCodexLoginStatus(codexHome) {
|
|
272
263
|
try {
|
|
273
264
|
const env = { ...process.env, CODEX_HOME: codexHome };
|
|
274
265
|
const result = (0, child_process_1.spawnSync)('codex', ['login', 'status'], {
|
|
275
266
|
encoding: 'utf8',
|
|
276
267
|
env,
|
|
277
|
-
timeout:
|
|
268
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
278
269
|
killSignal: 'SIGKILL',
|
|
279
270
|
});
|
|
280
|
-
|
|
281
|
-
if (result.status === 0 && /^Logged in/i.test(stdout)) {
|
|
282
|
-
if (/API key/i.test(stdout)) {
|
|
283
|
-
return { loggedIn: true, authMode: 'api-key' };
|
|
284
|
-
}
|
|
285
|
-
if (/ChatGPT/i.test(stdout)) {
|
|
286
|
-
return { loggedIn: true, authMode: 'chatgpt' };
|
|
287
|
-
}
|
|
288
|
-
return { loggedIn: true };
|
|
289
|
-
}
|
|
271
|
+
return parseCodexLoginStatusOutput(result.status, String(result.stdout ?? ''));
|
|
290
272
|
}
|
|
291
273
|
catch {
|
|
292
274
|
// Ignore missing CLI or spawn errors.
|
|
293
275
|
}
|
|
294
276
|
return { loggedIn: false };
|
|
295
277
|
}
|
|
278
|
+
function getCodexLoginStatusAsync(codexHome) {
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
try {
|
|
281
|
+
const env = { ...process.env, CODEX_HOME: codexHome };
|
|
282
|
+
(0, child_process_1.execFile)('codex', ['login', 'status'], { encoding: 'utf8', env, timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error, stdout) => {
|
|
283
|
+
// A missing CLI, non-zero exit, or timeout all mean "not logged in".
|
|
284
|
+
resolve(error ? { loggedIn: false } : parseCodexLoginStatusOutput(0, String(stdout ?? '')));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
resolve({ loggedIn: false });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/** Sync sibling of {@link detectRunningCodexProcessAsync}; see the event-loop caveat above. */
|
|
296
293
|
function detectRunningCodexProcess() {
|
|
297
294
|
if (process.platform === 'win32')
|
|
298
295
|
return false;
|
|
299
296
|
try {
|
|
300
297
|
return ((0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], {
|
|
301
298
|
encoding: 'utf8',
|
|
302
|
-
timeout:
|
|
299
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
303
300
|
killSignal: 'SIGKILL',
|
|
304
301
|
}).status === 0);
|
|
305
302
|
}
|
|
@@ -307,7 +304,20 @@ function detectRunningCodexProcess() {
|
|
|
307
304
|
return false;
|
|
308
305
|
}
|
|
309
306
|
}
|
|
310
|
-
function
|
|
307
|
+
function detectRunningCodexProcessAsync() {
|
|
308
|
+
if (process.platform === 'win32')
|
|
309
|
+
return Promise.resolve(false);
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
try {
|
|
312
|
+
(0, child_process_1.execFile)('pgrep', ['-x', 'codex'], { encoding: 'utf8', timeout: PROBE_TIMEOUT_MS, killSignal: 'SIGKILL' }, (error) => resolve(!error));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
resolve(false);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/** File-based metadata only — never spawns the codex CLI. */
|
|
320
|
+
function readCodexAccountMetadataFromFiles(codexHome) {
|
|
311
321
|
const fromAuth = readMetadataFromAuthJson(codexHome);
|
|
312
322
|
if (fromAuth.email || fromAuth.workspaceId || fromAuth.planType || fromAuth.authMode) {
|
|
313
323
|
return fromAuth;
|
|
@@ -316,20 +326,30 @@ function readCodexAccountMetadata(codexHome) {
|
|
|
316
326
|
if (fromLegacy.authMode) {
|
|
317
327
|
return fromLegacy;
|
|
318
328
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
function metadataFromLoginStatus(status) {
|
|
332
|
+
return status.loggedIn ? { authMode: status.authMode ?? 'unknown' } : {};
|
|
333
|
+
}
|
|
334
|
+
function readCodexAccountMetadata(codexHome) {
|
|
335
|
+
return (readCodexAccountMetadataFromFiles(codexHome) ??
|
|
336
|
+
metadataFromLoginStatus(getCodexLoginStatus(codexHome)));
|
|
337
|
+
}
|
|
338
|
+
/** {@link readCodexAccountMetadata} without blocking the event loop on the CLI probe. */
|
|
339
|
+
async function readCodexAccountMetadataAsync(codexHome) {
|
|
340
|
+
return (readCodexAccountMetadataFromFiles(codexHome) ??
|
|
341
|
+
metadataFromLoginStatus(await getCodexLoginStatusAsync(codexHome)));
|
|
342
|
+
}
|
|
343
|
+
function hasCodexCredentialFiles(codexHome) {
|
|
344
|
+
return (fs.existsSync(path.join(codexHome, 'auth.json')) ||
|
|
345
|
+
fs.existsSync(path.join(codexHome, '.credentials.json')));
|
|
326
346
|
}
|
|
327
347
|
function isCodexProfileAuthenticated(codexHome) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return
|
|
348
|
+
return hasCodexCredentialFiles(codexHome) || getCodexLoginStatus(codexHome).loggedIn;
|
|
349
|
+
}
|
|
350
|
+
/** {@link isCodexProfileAuthenticated} without blocking the event loop on the CLI probe. */
|
|
351
|
+
async function isCodexProfileAuthenticatedAsync(codexHome) {
|
|
352
|
+
return hasCodexCredentialFiles(codexHome) || (await getCodexLoginStatusAsync(codexHome)).loggedIn;
|
|
333
353
|
}
|
|
334
354
|
function ensureUniqueCodexLabel(label, excludeId) {
|
|
335
355
|
const normalized = label.trim().toLowerCase();
|
|
@@ -405,14 +425,14 @@ function getCodexExecutionEnv(baseEnv = process.env) {
|
|
|
405
425
|
CODEX_HOME: resolveSidekickCodexHome(),
|
|
406
426
|
};
|
|
407
427
|
}
|
|
408
|
-
function
|
|
428
|
+
function prepareCodexAccountCore(label) {
|
|
409
429
|
const trimmedLabel = label.trim();
|
|
410
430
|
if (!trimmedLabel) {
|
|
411
|
-
return { success: false, error: 'Codex accounts require a non-empty label.' };
|
|
431
|
+
return { result: { success: false, error: 'Codex accounts require a non-empty label.' } };
|
|
412
432
|
}
|
|
413
433
|
const labelError = ensureUniqueCodexLabel(trimmedLabel);
|
|
414
434
|
if (labelError) {
|
|
415
|
-
return { success: false, error: labelError };
|
|
435
|
+
return { result: { success: false, error: labelError } };
|
|
416
436
|
}
|
|
417
437
|
const profileId = (0, crypto_1.randomUUID)();
|
|
418
438
|
const codexHome = getCodexProfileHome(profileId);
|
|
@@ -425,55 +445,85 @@ function prepareCodexAccount(label) {
|
|
|
425
445
|
copySourceCodexConfig(sourceHome, codexHome);
|
|
426
446
|
const imported = importCurrentCodexAuth(sourceHome, codexHome);
|
|
427
447
|
if (imported) {
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
...finalized,
|
|
431
|
-
profileId,
|
|
432
|
-
codexHome,
|
|
433
|
-
needsLogin: false,
|
|
434
|
-
};
|
|
448
|
+
return { finalize: { profileId, codexHome } };
|
|
435
449
|
}
|
|
436
|
-
return {
|
|
437
|
-
success: true,
|
|
438
|
-
profileId,
|
|
439
|
-
codexHome,
|
|
440
|
-
needsLogin: true,
|
|
441
|
-
};
|
|
450
|
+
return { result: { success: true, profileId, codexHome, needsLogin: true } };
|
|
442
451
|
}
|
|
443
|
-
function
|
|
452
|
+
function prepareCodexAccount(label) {
|
|
453
|
+
const step = prepareCodexAccountCore(label);
|
|
454
|
+
if ('finalize' in step) {
|
|
455
|
+
const finalized = finalizeCodexAccount(step.finalize.profileId);
|
|
456
|
+
return { ...finalized, ...step.finalize, needsLogin: false };
|
|
457
|
+
}
|
|
458
|
+
return step.result;
|
|
459
|
+
}
|
|
460
|
+
/** {@link prepareCodexAccount} without blocking the event loop on CLI probes. */
|
|
461
|
+
async function prepareCodexAccountAsync(label) {
|
|
462
|
+
const step = prepareCodexAccountCore(label);
|
|
463
|
+
if ('finalize' in step) {
|
|
464
|
+
const finalized = await finalizeCodexAccountAsync(step.finalize.profileId);
|
|
465
|
+
return { ...finalized, ...step.finalize, needsLogin: false };
|
|
466
|
+
}
|
|
467
|
+
return step.result;
|
|
468
|
+
}
|
|
469
|
+
function finalizeCodexAccountCore(profileId, opts, probes) {
|
|
444
470
|
const pending = readPendingProfile(profileId);
|
|
445
471
|
if (!pending) {
|
|
446
|
-
return { success: false, error: `Codex profile ${profileId} was not prepared.` };
|
|
472
|
+
return { result: { success: false, error: `Codex profile ${profileId} was not prepared.` } };
|
|
447
473
|
}
|
|
448
474
|
const codexHome = getCodexProfileHome(profileId);
|
|
449
|
-
if (!
|
|
450
|
-
return { success: false, error: 'Codex profile is not authenticated yet.' };
|
|
475
|
+
if (!probes.authenticated) {
|
|
476
|
+
return { result: { success: false, error: 'Codex profile is not authenticated yet.' } };
|
|
451
477
|
}
|
|
452
|
-
const metadata = readCodexAccountMetadata(codexHome);
|
|
453
478
|
const profile = {
|
|
454
479
|
id: profileId,
|
|
455
480
|
providerId: 'codex',
|
|
456
481
|
label: pending.label,
|
|
457
|
-
email: metadata.email,
|
|
482
|
+
email: probes.metadata.email,
|
|
458
483
|
addedAt: pending.addedAt,
|
|
459
|
-
metadata,
|
|
484
|
+
metadata: probes.metadata,
|
|
460
485
|
};
|
|
461
486
|
(0, accountRegistry_1.upsertSavedAccountProfile)(profile);
|
|
462
487
|
if (opts.activate === false) {
|
|
463
|
-
return { success: true };
|
|
488
|
+
return { result: { success: true } };
|
|
464
489
|
}
|
|
465
|
-
|
|
466
|
-
fs.existsSync(path.join(codexHome, '.credentials.json'));
|
|
467
|
-
if (!hasCredentialFiles) {
|
|
490
|
+
if (!hasCodexCredentialFiles(codexHome)) {
|
|
468
491
|
// Authenticated via the OS keyring — there are no credential files to
|
|
469
492
|
// swap, so the registry pointer is all we can update.
|
|
470
493
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', profileId);
|
|
471
494
|
return {
|
|
472
|
-
|
|
473
|
-
|
|
495
|
+
result: {
|
|
496
|
+
success: true,
|
|
497
|
+
warning: 'Codex stores credentials in the OS keyring; sidekick cannot swap them per account, so `codex` keeps using the keyring credentials.',
|
|
498
|
+
},
|
|
474
499
|
};
|
|
475
500
|
}
|
|
476
|
-
return
|
|
501
|
+
return { swap: profile };
|
|
502
|
+
}
|
|
503
|
+
function finalizeCodexAccount(profileId, opts = {}) {
|
|
504
|
+
if (!readPendingProfile(profileId)) {
|
|
505
|
+
return { success: false, error: `Codex profile ${profileId} was not prepared.` };
|
|
506
|
+
}
|
|
507
|
+
const codexHome = getCodexProfileHome(profileId);
|
|
508
|
+
const authenticated = isCodexProfileAuthenticated(codexHome);
|
|
509
|
+
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
510
|
+
authenticated,
|
|
511
|
+
metadata: authenticated ? readCodexAccountMetadata(codexHome) : {},
|
|
512
|
+
});
|
|
513
|
+
return 'swap' in step ? performCodexAuthSwap(step.swap) : step.result;
|
|
514
|
+
}
|
|
515
|
+
/** {@link finalizeCodexAccount} without blocking the event loop on CLI probes. */
|
|
516
|
+
async function finalizeCodexAccountAsync(profileId, opts = {}) {
|
|
517
|
+
if (!readPendingProfile(profileId)) {
|
|
518
|
+
return { success: false, error: `Codex profile ${profileId} was not prepared.` };
|
|
519
|
+
}
|
|
520
|
+
const codexHome = getCodexProfileHome(profileId);
|
|
521
|
+
const authenticated = await isCodexProfileAuthenticatedAsync(codexHome);
|
|
522
|
+
const step = finalizeCodexAccountCore(profileId, opts, {
|
|
523
|
+
authenticated,
|
|
524
|
+
metadata: authenticated ? await readCodexAccountMetadataAsync(codexHome) : {},
|
|
525
|
+
});
|
|
526
|
+
return 'swap' in step ? performCodexAuthSwapAsync(step.swap) : step.result;
|
|
477
527
|
}
|
|
478
528
|
function getCodexStashDir() {
|
|
479
529
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', 'stash');
|
|
@@ -484,11 +534,11 @@ function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
|
484
534
|
let stashPath = null;
|
|
485
535
|
if (liveAuthRaw) {
|
|
486
536
|
stashPath = path.join(getCodexStashDir(), `auth-${stamp}.json`);
|
|
487
|
-
|
|
537
|
+
(0, atomic_1.atomicWriteFileSync)(stashPath, liveAuthRaw);
|
|
488
538
|
}
|
|
489
539
|
if (liveLegacyRaw) {
|
|
490
540
|
const legacyStashPath = path.join(getCodexStashDir(), `credentials-${stamp}.json`);
|
|
491
|
-
|
|
541
|
+
(0, atomic_1.atomicWriteFileSync)(legacyStashPath, liveLegacyRaw);
|
|
492
542
|
stashPath = stashPath ?? legacyStashPath;
|
|
493
543
|
}
|
|
494
544
|
return stashPath;
|
|
@@ -537,11 +587,13 @@ function syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
|
537
587
|
}
|
|
538
588
|
const profileHome = getCodexProfileHome(profile.id);
|
|
539
589
|
if (liveAuthRaw)
|
|
540
|
-
|
|
590
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(profileHome, 'auth.json'), liveAuthRaw);
|
|
541
591
|
if (liveLegacyRaw)
|
|
542
|
-
|
|
592
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
|
|
543
593
|
try {
|
|
544
|
-
|
|
594
|
+
// File-based metadata only: this runs with auth files just written, and
|
|
595
|
+
// the CLI-probe fallback must never spawn while the swap lock is held.
|
|
596
|
+
const metadata = readCodexAccountMetadataFromFiles(profileHome) ?? {};
|
|
545
597
|
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
546
598
|
...profile,
|
|
547
599
|
email: metadata.email ?? profile.email,
|
|
@@ -557,13 +609,70 @@ function syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
|
|
|
557
609
|
return { warning: `Could not back up live Codex credentials: ${err}` };
|
|
558
610
|
}
|
|
559
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Serializes live-auth mutations across processes. Codex rotates refresh
|
|
614
|
+
* tokens, so two interleaved swaps stashing and restoring auth.json can
|
|
615
|
+
* resurrect a stale token and permanently invalidate the login. Lock ordering:
|
|
616
|
+
* this lock is taken first and the registry lock (inside setActiveSavedAccount
|
|
617
|
+
* / upsertSavedAccountProfile) inside it — never the reverse.
|
|
618
|
+
*/
|
|
619
|
+
function withCodexAuthSwapLock(operation) {
|
|
620
|
+
const lockDir = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex');
|
|
621
|
+
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
|
622
|
+
return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* CLI probes are resolved before the auth-swap lock is taken — never spawn a
|
|
626
|
+
* child process while holding it. The keyring probe is skipped (false) when
|
|
627
|
+
* live credential files exist, because the swap only consults it in their
|
|
628
|
+
* absence.
|
|
629
|
+
*/
|
|
630
|
+
function resolveCodexSwapProbesSync() {
|
|
631
|
+
const systemHome = getSystemCodexHome();
|
|
632
|
+
return {
|
|
633
|
+
codexRunning: detectRunningCodexProcess(),
|
|
634
|
+
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
635
|
+
? false
|
|
636
|
+
: getCodexLoginStatus(systemHome).loggedIn,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function resolveCodexSwapProbesAsync() {
|
|
640
|
+
const systemHome = getSystemCodexHome();
|
|
641
|
+
return {
|
|
642
|
+
codexRunning: await detectRunningCodexProcessAsync(),
|
|
643
|
+
systemKeyringLoggedIn: hasCodexCredentialFiles(systemHome)
|
|
644
|
+
? false
|
|
645
|
+
: (await getCodexLoginStatusAsync(systemHome)).loggedIn,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
560
648
|
function performCodexAuthSwap(target) {
|
|
649
|
+
const probes = resolveCodexSwapProbesSync();
|
|
650
|
+
try {
|
|
651
|
+
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, probes));
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
// Keep the non-throwing result contract when the lock cannot be acquired.
|
|
655
|
+
return { success: false, error: `Could not acquire the account-switch lock: ${err}` };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function performCodexAuthSwapAsync(target) {
|
|
659
|
+
const probes = await resolveCodexSwapProbesAsync();
|
|
660
|
+
try {
|
|
661
|
+
// The sync lock is fine here: probes are pre-resolved, so the critical
|
|
662
|
+
// section is only fast local-filesystem work.
|
|
663
|
+
return withCodexAuthSwapLock(() => performCodexAuthSwapCore(target, probes));
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
return { success: false, error: `Could not acquire the account-switch lock: ${err}` };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function performCodexAuthSwapCore(target, probes) {
|
|
561
670
|
const systemHome = getSystemCodexHome();
|
|
562
671
|
const liveAuthPath = path.join(systemHome, 'auth.json');
|
|
563
672
|
const liveLegacyPath = path.join(systemHome, '.credentials.json');
|
|
564
673
|
const liveAuthRaw = readFileOrNull(liveAuthPath);
|
|
565
674
|
const liveLegacyRaw = readFileOrNull(liveLegacyPath);
|
|
566
|
-
if (!liveAuthRaw && !liveLegacyRaw &&
|
|
675
|
+
if (!liveAuthRaw && !liveLegacyRaw && probes.systemKeyringLoggedIn) {
|
|
567
676
|
return {
|
|
568
677
|
success: false,
|
|
569
678
|
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.',
|
|
@@ -587,7 +696,7 @@ function performCodexAuthSwap(target) {
|
|
|
587
696
|
};
|
|
588
697
|
}
|
|
589
698
|
const warnings = [];
|
|
590
|
-
if (
|
|
699
|
+
if (probes.codexRunning) {
|
|
591
700
|
warnings.push('A codex process appears to be running; restart codex sessions so they pick up the switched account.');
|
|
592
701
|
}
|
|
593
702
|
// If the live file already belongs to the target account it is the freshest
|
|
@@ -607,10 +716,11 @@ function performCodexAuthSwap(target) {
|
|
|
607
716
|
if (liveMatchesTarget) {
|
|
608
717
|
try {
|
|
609
718
|
if (liveAuthRaw)
|
|
610
|
-
|
|
719
|
+
(0, atomic_1.atomicWriteFileSync)(targetAuthPath, liveAuthRaw);
|
|
611
720
|
if (liveLegacyRaw)
|
|
612
|
-
|
|
613
|
-
|
|
721
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
|
|
722
|
+
// File-based metadata only — see the swap-lock note above.
|
|
723
|
+
const metadata = readCodexAccountMetadataFromFiles(profileHome) ?? {};
|
|
614
724
|
(0, accountRegistry_1.upsertSavedAccountProfile)({
|
|
615
725
|
...target,
|
|
616
726
|
email: metadata.email ?? target.email,
|
|
@@ -633,11 +743,11 @@ function performCodexAuthSwap(target) {
|
|
|
633
743
|
const restoreLiveFiles = () => {
|
|
634
744
|
try {
|
|
635
745
|
if (liveAuthRaw)
|
|
636
|
-
|
|
746
|
+
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, liveAuthRaw);
|
|
637
747
|
else
|
|
638
748
|
fs.rmSync(liveAuthPath, { force: true });
|
|
639
749
|
if (liveLegacyRaw)
|
|
640
|
-
|
|
750
|
+
(0, atomic_1.atomicWriteFileSync)(liveLegacyPath, liveLegacyRaw);
|
|
641
751
|
else
|
|
642
752
|
fs.rmSync(liveLegacyPath, { force: true });
|
|
643
753
|
}
|
|
@@ -647,14 +757,14 @@ function performCodexAuthSwap(target) {
|
|
|
647
757
|
};
|
|
648
758
|
try {
|
|
649
759
|
if (targetAuthRaw) {
|
|
650
|
-
|
|
760
|
+
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, targetAuthRaw);
|
|
651
761
|
if (targetLegacyRaw)
|
|
652
|
-
|
|
762
|
+
(0, atomic_1.atomicWriteFileSync)(liveLegacyPath, targetLegacyRaw);
|
|
653
763
|
else if (liveLegacyRaw)
|
|
654
764
|
fs.rmSync(liveLegacyPath, { force: true });
|
|
655
765
|
}
|
|
656
766
|
else {
|
|
657
|
-
|
|
767
|
+
(0, atomic_1.atomicWriteFileSync)(liveLegacyPath, targetLegacyRaw);
|
|
658
768
|
if (liveAuthRaw)
|
|
659
769
|
fs.rmSync(liveAuthPath, { force: true });
|
|
660
770
|
}
|
|
@@ -679,15 +789,56 @@ function switchToCodexAccount(profileId) {
|
|
|
679
789
|
}
|
|
680
790
|
return performCodexAuthSwap(target);
|
|
681
791
|
}
|
|
792
|
+
/** {@link switchToCodexAccount} without blocking the event loop on CLI probes. */
|
|
793
|
+
async function switchToCodexAccountAsync(profileId) {
|
|
794
|
+
const target = listCodexAccounts().find((account) => account.id === profileId);
|
|
795
|
+
if (!target) {
|
|
796
|
+
return { success: false, error: `Codex account ${profileId} not found.` };
|
|
797
|
+
}
|
|
798
|
+
return performCodexAuthSwapAsync(target);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The reconcile flow consults the keyring probe only when the migration has
|
|
802
|
+
* not run yet and the system home has no auth.json; skip the spawn otherwise.
|
|
803
|
+
*/
|
|
804
|
+
function needsReconcileKeyringProbe() {
|
|
805
|
+
const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
|
|
806
|
+
if (fs.existsSync(markerPath))
|
|
807
|
+
return false;
|
|
808
|
+
return !fs.existsSync(path.join(getSystemCodexHome(), 'auth.json'));
|
|
809
|
+
}
|
|
682
810
|
// One-time migration for installs created when profile homes doubled as live
|
|
683
811
|
// CODEX_HOMEs: the active profile's auth.json may hold a fresher rotated
|
|
684
812
|
// refresh token than the system home. Best-effort: never throws.
|
|
685
813
|
function reconcileCodexAuthState() {
|
|
814
|
+
try {
|
|
815
|
+
const systemKeyringLoggedIn = needsReconcileKeyringProbe()
|
|
816
|
+
? getCodexLoginStatus(getSystemCodexHome()).loggedIn
|
|
817
|
+
: false;
|
|
818
|
+
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
// Reconciliation must never break startup — including a lock timeout.
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/** {@link reconcileCodexAuthState} without blocking the event loop on CLI probes. */
|
|
825
|
+
async function reconcileCodexAuthStateAsync() {
|
|
826
|
+
try {
|
|
827
|
+
const systemKeyringLoggedIn = needsReconcileKeyringProbe()
|
|
828
|
+
? (await getCodexLoginStatusAsync(getSystemCodexHome())).loggedIn
|
|
829
|
+
: false;
|
|
830
|
+
withCodexAuthSwapLock(() => reconcileCodexAuthStateCore({ systemKeyringLoggedIn }));
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
// Reconciliation must never break startup — including a lock timeout.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function reconcileCodexAuthStateCore(probes) {
|
|
686
837
|
try {
|
|
687
838
|
const markerPath = path.join((0, accountRegistry_1.getAccountsDir)(), 'codex', '.live-auth-migrated-v1');
|
|
688
839
|
if (fs.existsSync(markerPath))
|
|
689
840
|
return;
|
|
690
|
-
const writeMarker = () =>
|
|
841
|
+
const writeMarker = () => (0, atomic_1.atomicWriteFileSync)(markerPath, new Date().toISOString() + '\n');
|
|
691
842
|
const active = getActiveCodexAccount();
|
|
692
843
|
if (!active) {
|
|
693
844
|
writeMarker();
|
|
@@ -706,9 +857,10 @@ function reconcileCodexAuthState() {
|
|
|
706
857
|
if (!liveAuthRaw) {
|
|
707
858
|
// No live credentials (account was added via isolated login and never
|
|
708
859
|
// promoted). Promote the active profile's copy unless codex is logged
|
|
709
|
-
// in through the OS keyring.
|
|
710
|
-
|
|
711
|
-
|
|
860
|
+
// in through the OS keyring. The probe was resolved before the swap
|
|
861
|
+
// lock was taken — never spawn while holding it.
|
|
862
|
+
if (!probes.systemKeyringLoggedIn) {
|
|
863
|
+
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
|
|
712
864
|
}
|
|
713
865
|
writeMarker();
|
|
714
866
|
return;
|
|
@@ -726,10 +878,10 @@ function reconcileCodexAuthState() {
|
|
|
726
878
|
// The profile copy was the live home under the old model and holds
|
|
727
879
|
// the valid rotated refresh token — promote it.
|
|
728
880
|
stashLiveCodexAuth(liveAuthRaw, null);
|
|
729
|
-
|
|
881
|
+
(0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
|
|
730
882
|
}
|
|
731
883
|
else {
|
|
732
|
-
|
|
884
|
+
(0, atomic_1.atomicWriteFileSync)(profileAuthPath, liveAuthRaw);
|
|
733
885
|
}
|
|
734
886
|
}
|
|
735
887
|
else {
|
|
@@ -738,7 +890,7 @@ function reconcileCodexAuthState() {
|
|
|
738
890
|
// one, and refresh its backup.
|
|
739
891
|
const matching = findProfileForIdentity(liveIdentity);
|
|
740
892
|
if (matching && matching.id !== active.id) {
|
|
741
|
-
|
|
893
|
+
(0, atomic_1.atomicWriteFileSync)(path.join(getCodexProfileHome(matching.id), 'auth.json'), liveAuthRaw);
|
|
742
894
|
(0, accountRegistry_1.setActiveSavedAccount)('codex', matching.id);
|
|
743
895
|
}
|
|
744
896
|
}
|
|
@@ -12,6 +12,7 @@ exports.createSessionContextProjector = createSessionContextProjector;
|
|
|
12
12
|
exports.readSessionContextSnapshot = readSessionContextSnapshot;
|
|
13
13
|
const EventAggregator_1 = require("../aggregation/EventAggregator");
|
|
14
14
|
const modelContext_1 = require("../modelContext");
|
|
15
|
+
const mcpToolName_1 = require("../parsers/mcpToolName");
|
|
15
16
|
const tokenEstimation_1 = require("../tokenEstimation");
|
|
16
17
|
const DEFAULT_CONTEXT_SOURCE_LIMIT = 200;
|
|
17
18
|
const DEFAULT_SNIPPET_MAX_CHARS = 240;
|
|
@@ -490,10 +491,7 @@ function inferMcpServer(toolName, input) {
|
|
|
490
491
|
const explicit = input._sidekickMcpServerName ?? input.server_name ?? input.serverName;
|
|
491
492
|
if (typeof explicit === 'string' && explicit.trim())
|
|
492
493
|
return explicit.trim();
|
|
493
|
-
|
|
494
|
-
if (match)
|
|
495
|
-
return match[1];
|
|
496
|
-
return undefined;
|
|
494
|
+
return (0, mcpToolName_1.parseMcpToolName)(toolName)?.serverName;
|
|
497
495
|
}
|
|
498
496
|
function extractText(content) {
|
|
499
497
|
if (typeof content === 'string')
|
|
@@ -72,14 +72,14 @@ async function ensureDefaultClaudeAccount(options) {
|
|
|
72
72
|
function cleanupPendingCodexProfile(profileId) {
|
|
73
73
|
fs.rmSync(path.join((0, codexProfiles_1.getCodexProfilesDir)(), profileId), { recursive: true, force: true });
|
|
74
74
|
}
|
|
75
|
-
function ensureDefaultCodexAccount(options) {
|
|
75
|
+
async function ensureDefaultCodexAccount(options) {
|
|
76
76
|
try {
|
|
77
77
|
if ((0, codexProfiles_1.getActiveCodexAccount)())
|
|
78
78
|
return 'skipped';
|
|
79
79
|
const systemAuthPath = path.join((0, codexProfiles_1.getSystemCodexHome)(), 'auth.json');
|
|
80
80
|
if (!fs.existsSync(systemAuthPath))
|
|
81
81
|
return 'skipped';
|
|
82
|
-
const result = (0, codexProfiles_1.
|
|
82
|
+
const result = await (0, codexProfiles_1.prepareCodexAccountAsync)('Default');
|
|
83
83
|
if (result.success && !result.needsLogin)
|
|
84
84
|
return 'registered';
|
|
85
85
|
if (result.profileId) {
|
|
@@ -95,7 +95,7 @@ function ensureDefaultCodexAccount(options) {
|
|
|
95
95
|
}
|
|
96
96
|
async function ensureDefaultAccounts(options) {
|
|
97
97
|
const claude = await ensureDefaultClaudeAccount(options);
|
|
98
|
-
const codex = ensureDefaultCodexAccount(options);
|
|
98
|
+
const codex = await ensureDefaultCodexAccount(options);
|
|
99
99
|
try {
|
|
100
100
|
(0, accounts_1.reconcileClaudeAuthState)();
|
|
101
101
|
}
|
|
@@ -103,7 +103,7 @@ async function ensureDefaultAccounts(options) {
|
|
|
103
103
|
logFailure(options, 'Claude auth reconciliation failed.', error);
|
|
104
104
|
}
|
|
105
105
|
try {
|
|
106
|
-
(0, codexProfiles_1.
|
|
106
|
+
await (0, codexProfiles_1.reconcileCodexAuthStateAsync)();
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
109
|
logFailure(options, 'Codex auth reconciliation failed.', error);
|