sidekick-shared 0.24.4 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +86 -45
  3. package/dist/accountManager.d.ts +13 -0
  4. package/dist/accountManager.js +60 -32
  5. package/dist/accountRegistry.js +59 -41
  6. package/dist/accounts.js +40 -19
  7. package/dist/autoSwitch.d.ts +2 -1
  8. package/dist/autoSwitch.js +32 -7
  9. package/dist/browser.d.ts +2 -0
  10. package/dist/browser.js +3 -1
  11. package/dist/codexProfiles.d.ts +14 -0
  12. package/dist/codexProfiles.js +268 -116
  13. package/dist/context/sessionContext.js +2 -4
  14. package/dist/ensureDefaultAccounts.js +4 -4
  15. package/dist/index.d.ts +11 -4
  16. package/dist/index.js +28 -7
  17. package/dist/node.d.ts +6 -0
  18. package/dist/node.js +8 -1
  19. package/dist/observedContextWindows.d.ts +13 -0
  20. package/dist/observedContextWindows.js +13 -0
  21. package/dist/parsers/codexHistory.d.ts +30 -0
  22. package/dist/parsers/codexHistory.js +118 -0
  23. package/dist/parsers/mcpToolName.d.ts +26 -0
  24. package/dist/parsers/mcpToolName.js +31 -0
  25. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  26. package/dist/parsers/sessionPathResolver.js +11 -4
  27. package/dist/paths.d.ts +7 -3
  28. package/dist/paths.js +7 -3
  29. package/dist/providers/claudeCode.d.ts +2 -1
  30. package/dist/providers/claudeCode.js +18 -0
  31. package/dist/providers/codex.d.ts +22 -1
  32. package/dist/providers/codex.js +58 -0
  33. package/dist/providers/types.d.ts +5 -0
  34. package/dist/quotaHistory.d.ts +1 -1
  35. package/dist/quotaHistory.js +2 -2
  36. package/dist/quotaSnapshots.js +8 -77
  37. package/dist/sessionPreviews.d.ts +60 -0
  38. package/dist/sessionPreviews.js +234 -0
  39. package/dist/sessionTranscripts.js +10 -4
  40. package/dist/writers/atomic.d.ts +20 -0
  41. package/dist/writers/atomic.js +190 -19
  42. package/package.json +1 -1
@@ -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
- atomicWriteJson(getCodexProfileStatePath(profileId), pending);
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: 4000,
268
+ timeout: PROBE_TIMEOUT_MS,
278
269
  killSignal: 'SIGKILL',
279
270
  });
280
- const stdout = String(result.stdout ?? '').trim();
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: 4000,
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 readCodexAccountMetadata(codexHome) {
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
- const status = getCodexLoginStatus(codexHome);
320
- if (status.loggedIn) {
321
- return {
322
- authMode: status.authMode ?? 'unknown',
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
- if (fs.existsSync(path.join(codexHome, 'auth.json')) ||
329
- fs.existsSync(path.join(codexHome, '.credentials.json'))) {
330
- return true;
331
- }
332
- return getCodexLoginStatus(codexHome).loggedIn;
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 prepareCodexAccount(label) {
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
- const finalized = finalizeCodexAccount(profileId);
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 finalizeCodexAccount(profileId, opts = {}) {
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 (!isCodexProfileAuthenticated(codexHome)) {
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
- const hasCredentialFiles = fs.existsSync(path.join(codexHome, 'auth.json')) ||
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
- success: true,
473
- warning: 'Codex stores credentials in the OS keyring; sidekick cannot swap them per account, so `codex` keeps using the keyring credentials.',
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 performCodexAuthSwap(profile);
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
- atomicWriteFile(stashPath, liveAuthRaw);
537
+ (0, atomic_1.atomicWriteFileSync)(stashPath, liveAuthRaw);
488
538
  }
489
539
  if (liveLegacyRaw) {
490
540
  const legacyStashPath = path.join(getCodexStashDir(), `credentials-${stamp}.json`);
491
- atomicWriteFile(legacyStashPath, liveLegacyRaw);
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
- atomicWriteFile(path.join(profileHome, 'auth.json'), liveAuthRaw);
590
+ (0, atomic_1.atomicWriteFileSync)(path.join(profileHome, 'auth.json'), liveAuthRaw);
541
591
  if (liveLegacyRaw)
542
- atomicWriteFile(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
592
+ (0, atomic_1.atomicWriteFileSync)(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
543
593
  try {
544
- const metadata = readCodexAccountMetadata(profileHome);
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 && getCodexLoginStatus(systemHome).loggedIn) {
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 (detectRunningCodexProcess()) {
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
- atomicWriteFile(targetAuthPath, liveAuthRaw);
719
+ (0, atomic_1.atomicWriteFileSync)(targetAuthPath, liveAuthRaw);
611
720
  if (liveLegacyRaw)
612
- atomicWriteFile(path.join(profileHome, '.credentials.json'), liveLegacyRaw);
613
- const metadata = readCodexAccountMetadata(profileHome);
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
- atomicWriteFile(liveAuthPath, liveAuthRaw);
746
+ (0, atomic_1.atomicWriteFileSync)(liveAuthPath, liveAuthRaw);
637
747
  else
638
748
  fs.rmSync(liveAuthPath, { force: true });
639
749
  if (liveLegacyRaw)
640
- atomicWriteFile(liveLegacyPath, liveLegacyRaw);
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
- atomicWriteFile(liveAuthPath, targetAuthRaw);
760
+ (0, atomic_1.atomicWriteFileSync)(liveAuthPath, targetAuthRaw);
651
761
  if (targetLegacyRaw)
652
- atomicWriteFile(liveLegacyPath, targetLegacyRaw);
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
- atomicWriteFile(liveLegacyPath, targetLegacyRaw);
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 = () => atomicWriteFile(markerPath, new Date().toISOString() + '\n');
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
- if (!getCodexLoginStatus(systemHome).loggedIn) {
711
- atomicWriteFile(liveAuthPath, profileAuthRaw);
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
- atomicWriteFile(liveAuthPath, profileAuthRaw);
881
+ (0, atomic_1.atomicWriteFileSync)(liveAuthPath, profileAuthRaw);
730
882
  }
731
883
  else {
732
- atomicWriteFile(profileAuthPath, liveAuthRaw);
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
- atomicWriteFile(path.join(getCodexProfileHome(matching.id), 'auth.json'), liveAuthRaw);
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
- const match = /^mcp__([^_]+(?:_[^_]+)*)__/.exec(toolName);
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.prepareCodexAccount)('Default');
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.reconcileCodexAuthState)();
106
+ await (0, codexProfiles_1.reconcileCodexAuthStateAsync)();
107
107
  }
108
108
  catch (error) {
109
109
  logFailure(options, 'Codex auth reconciliation failed.', error);