sidekick-shared 0.21.2 → 0.21.4

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 (77) hide show
  1. package/README.md +60 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.d.ts +17 -0
  4. package/dist/accountRegistry.js +18 -14
  5. package/dist/accountStatus.js +7 -7
  6. package/dist/accounts.d.ts +13 -0
  7. package/dist/accounts.js +110 -25
  8. package/dist/aggregation/EventAggregator.d.ts +14 -11
  9. package/dist/aggregation/EventAggregator.js +68 -31
  10. package/dist/aggregation/PatternExtractor.js +5 -4
  11. package/dist/autoSwitch.js +6 -12
  12. package/dist/claudeProfiles.js +3 -3
  13. package/dist/codexProfiles.d.ts +15 -1
  14. package/dist/codexProfiles.js +96 -22
  15. package/dist/codexQuota.js +44 -16
  16. package/dist/codexQuotaWatcher.js +19 -4
  17. package/dist/context/composer.js +8 -6
  18. package/dist/context/sessionContext.js +12 -9
  19. package/dist/credentialIO.js +10 -8
  20. package/dist/extractors/sessionAssets.js +9 -1
  21. package/dist/extractors/sources/claudeAssets.js +7 -1
  22. package/dist/extractors/sources/codexAssets.js +14 -3
  23. package/dist/extractors/toolCall.js +1 -4
  24. package/dist/formatters/eventHighlighter.js +148 -28
  25. package/dist/formatters/noiseClassifier.js +3 -11
  26. package/dist/formatters/sessionDump.js +22 -13
  27. package/dist/formatters/toolSummary.js +1 -3
  28. package/dist/index.d.ts +26 -26
  29. package/dist/index.js +10 -4
  30. package/dist/modelContext.js +10 -8
  31. package/dist/modelInfo.js +11 -6
  32. package/dist/multiProviderQuotaService.d.ts +2 -2
  33. package/dist/multiProviderQuotaService.js +14 -10
  34. package/dist/parsers/codexParser.js +170 -103
  35. package/dist/parsers/debugLogParser.js +5 -4
  36. package/dist/parsers/openCodeParser.js +66 -48
  37. package/dist/parsers/planExtractor.js +3 -3
  38. package/dist/parsers/sessionActivityDetector.js +10 -6
  39. package/dist/parsers/sessionPathResolver.js +29 -34
  40. package/dist/parsers/subagentScanner.js +21 -20
  41. package/dist/parsers/subagentTraceParser.js +7 -4
  42. package/dist/phrases.js +11 -11
  43. package/dist/providerStatus.js +3 -4
  44. package/dist/providers/claudeCode.js +3 -1
  45. package/dist/providers/codex.js +35 -14
  46. package/dist/providers/codexDatabase.js +14 -5
  47. package/dist/providers/detect.js +10 -6
  48. package/dist/providers/openCode.js +91 -63
  49. package/dist/providers/openCodeDatabase.js +44 -17
  50. package/dist/quota.d.ts +19 -0
  51. package/dist/quota.js +50 -8
  52. package/dist/quotaHistory.js +10 -3
  53. package/dist/quotaSnapshots.js +2 -2
  54. package/dist/readers/decisions.js +1 -1
  55. package/dist/readers/notes.js +5 -3
  56. package/dist/readers/plans.js +43 -23
  57. package/dist/readers/tasks.js +1 -1
  58. package/dist/report/htmlHelpers.js +135 -16
  59. package/dist/report/htmlReportGenerator.js +38 -12
  60. package/dist/report/transcriptParser.js +8 -2
  61. package/dist/schemas/index.d.ts +4 -4
  62. package/dist/schemas/quota.js +2 -10
  63. package/dist/schemas/sessionEvent.js +20 -10
  64. package/dist/search/advancedFilter.js +3 -3
  65. package/dist/search/sessionSearch.js +10 -4
  66. package/dist/terminalSync.js +4 -2
  67. package/dist/turns/assistantTurn.js +5 -1
  68. package/dist/watchers/eventBridge.js +56 -26
  69. package/dist/watchers/factory.js +5 -2
  70. package/dist/watchers/jsonlTail.js +3 -1
  71. package/dist/watchers/jsonlWatcher.js +128 -52
  72. package/dist/watchers/providerReaderWatcher.js +3 -1
  73. package/dist/watchers/sqliteWatcher.js +52 -22
  74. package/dist/zaiQuota.js +6 -10
  75. package/dist/zaiQuotaApi.js +13 -5
  76. package/dist/zaiQuotaWatcher.js +1 -1
  77. package/package.json +5 -2
@@ -1,4 +1,4 @@
1
- import type { AccountIdentityMetadata, SavedAccountProfile } from './accountRegistry';
1
+ import type { AccountIdentityMetadata, ResolvedActiveAccount, SavedAccountProfile } from './accountRegistry';
2
2
  import type { AccountManagerResult } from './accounts';
3
3
  export interface CodexAccountManagerResult extends AccountManagerResult {
4
4
  needsLogin?: boolean;
@@ -14,6 +14,20 @@ export declare function isCodexProfileAuthenticated(codexHome: string): boolean;
14
14
  export declare function listCodexAccounts(): SavedAccountProfile[];
15
15
  export declare function getActiveCodexAccount(): SavedAccountProfile | null;
16
16
  export declare function resolveSidekickCodexHome(): string;
17
+ /**
18
+ * Resolves the *currently logged-in* Codex account for display, preferring the
19
+ * live `auth.json` identity over the saved registry pointer (which only sidekick's
20
+ * own switch flow updates and therefore goes stale after a native `codex login`).
21
+ *
22
+ * Uses the cheap JWT decode from `auth.json` directly — NOT `readCodexAccountMetadata`,
23
+ * whose fallback spawns `codex login status` (a multi-second subprocess) unsuitable
24
+ * for a render path.
25
+ *
26
+ * Safe self-heal: when the live identity unambiguously matches a saved profile that
27
+ * isn't the current active pointer, the pointer is re-pointed so registry-keyed data
28
+ * (quota history, auto-switch) tracks reality too. Never creates or deletes profiles.
29
+ */
30
+ export declare function resolveActiveCodexAccount(): ResolvedActiveAccount;
17
31
  export declare function getCodexExecutionEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
18
32
  export declare function prepareCodexAccount(label: string): CodexAccountManagerResult;
19
33
  export declare function finalizeCodexAccount(profileId: string, opts?: {
@@ -42,6 +42,7 @@ exports.isCodexProfileAuthenticated = isCodexProfileAuthenticated;
42
42
  exports.listCodexAccounts = listCodexAccounts;
43
43
  exports.getActiveCodexAccount = getActiveCodexAccount;
44
44
  exports.resolveSidekickCodexHome = resolveSidekickCodexHome;
45
+ exports.resolveActiveCodexAccount = resolveActiveCodexAccount;
45
46
  exports.getCodexExecutionEnv = getCodexExecutionEnv;
46
47
  exports.prepareCodexAccount = prepareCodexAccount;
47
48
  exports.finalizeCodexAccount = finalizeCodexAccount;
@@ -121,7 +122,9 @@ function atomicWriteJson(filePath, data, mode = 0o600) {
121
122
  try {
122
123
  fs.unlinkSync(tmp);
123
124
  }
124
- catch { /* nothing to clean up */ }
125
+ catch {
126
+ /* nothing to clean up */
127
+ }
125
128
  throw err;
126
129
  }
127
130
  }
@@ -139,7 +142,9 @@ function atomicWriteFile(filePath, content, mode = 0o600) {
139
142
  try {
140
143
  fs.unlinkSync(tmp);
141
144
  }
142
- catch { /* nothing to clean up */ }
145
+ catch {
146
+ /* nothing to clean up */
147
+ }
143
148
  throw err;
144
149
  }
145
150
  }
@@ -216,12 +221,8 @@ function readAuthIdentityFromRaw(raw) {
216
221
  const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
217
222
  ? authClaims.chatgpt_account_id
218
223
  : parsed.tokens?.account_id;
219
- const planType = typeof authClaims?.chatgpt_plan_type === 'string'
220
- ? authClaims.chatgpt_plan_type
221
- : undefined;
222
- const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key'
223
- ? 'api-key'
224
- : 'chatgpt';
224
+ const planType = typeof authClaims?.chatgpt_plan_type === 'string' ? authClaims.chatgpt_plan_type : undefined;
225
+ const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key' ? 'api-key' : 'chatgpt';
225
226
  return { email, workspaceId, planType, authMode };
226
227
  }
227
228
  function readLastRefresh(raw, fallbackPath) {
@@ -235,7 +236,9 @@ function readLastRefresh(raw, fallbackPath) {
235
236
  try {
236
237
  return fs.statSync(fallbackPath).mtimeMs;
237
238
  }
238
- catch { /* fall through */ }
239
+ catch {
240
+ /* fall through */
241
+ }
239
242
  }
240
243
  return null;
241
244
  }
@@ -271,6 +274,8 @@ function getCodexLoginStatus(codexHome) {
271
274
  const result = (0, child_process_1.spawnSync)('codex', ['login', 'status'], {
272
275
  encoding: 'utf8',
273
276
  env,
277
+ timeout: 4000,
278
+ killSignal: 'SIGKILL',
274
279
  });
275
280
  const stdout = String(result.stdout ?? '').trim();
276
281
  if (result.status === 0 && /^Logged in/i.test(stdout)) {
@@ -292,7 +297,11 @@ function detectRunningCodexProcess() {
292
297
  if (process.platform === 'win32')
293
298
  return false;
294
299
  try {
295
- return (0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], { encoding: 'utf8' }).status === 0;
300
+ return ((0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], {
301
+ encoding: 'utf8',
302
+ timeout: 4000,
303
+ killSignal: 'SIGKILL',
304
+ }).status === 0);
296
305
  }
297
306
  catch {
298
307
  return false;
@@ -324,8 +333,7 @@ function isCodexProfileAuthenticated(codexHome) {
324
333
  }
325
334
  function ensureUniqueCodexLabel(label, excludeId) {
326
335
  const normalized = label.trim().toLowerCase();
327
- const conflict = listCodexAccounts().find(account => account.id !== excludeId &&
328
- (account.label ?? '').trim().toLowerCase() === normalized);
336
+ const conflict = listCodexAccounts().find((account) => account.id !== excludeId && (account.label ?? '').trim().toLowerCase() === normalized);
329
337
  return conflict ? `A Codex account named "${label}" already exists.` : null;
330
338
  }
331
339
  function listCodexAccounts() {
@@ -339,6 +347,56 @@ function resolveSidekickCodexHome() {
339
347
  // home (or an explicit CODEX_HOME) is always the single live home.
340
348
  return getSystemCodexHome();
341
349
  }
350
+ /**
351
+ * Resolves the *currently logged-in* Codex account for display, preferring the
352
+ * live `auth.json` identity over the saved registry pointer (which only sidekick's
353
+ * own switch flow updates and therefore goes stale after a native `codex login`).
354
+ *
355
+ * Uses the cheap JWT decode from `auth.json` directly — NOT `readCodexAccountMetadata`,
356
+ * whose fallback spawns `codex login status` (a multi-second subprocess) unsuitable
357
+ * for a render path.
358
+ *
359
+ * Safe self-heal: when the live identity unambiguously matches a saved profile that
360
+ * isn't the current active pointer, the pointer is re-pointed so registry-keyed data
361
+ * (quota history, auto-switch) tracks reality too. Never creates or deletes profiles.
362
+ */
363
+ function resolveActiveCodexAccount() {
364
+ const identity = readAuthIdentityFromRaw(readFileOrNull(path.join(resolveSidekickCodexHome(), 'auth.json')));
365
+ if (identity && (identity.email || identity.workspaceId)) {
366
+ const match = findProfileForIdentity(identity);
367
+ if (match) {
368
+ const active = (0, accountRegistry_1.getActiveSavedAccount)('codex');
369
+ if (!active || active.id !== match.id) {
370
+ // Self-heal is best-effort: a registry write failure (read-only/full
371
+ // disk) must never break display, extension activation, or the quota
372
+ // watcher's hot path. We still return the correct live identity below.
373
+ try {
374
+ (0, accountRegistry_1.setActiveSavedAccount)('codex', match.id);
375
+ }
376
+ catch {
377
+ /* keep going with the live identity */
378
+ }
379
+ }
380
+ }
381
+ return {
382
+ email: identity.email ?? match?.email ?? match?.metadata?.email,
383
+ label: match?.label,
384
+ providerAccountId: match?.providerAccountId ?? identity.workspaceId,
385
+ source: 'live',
386
+ };
387
+ }
388
+ // api-key auth, unparseable token, or logged out → fall back to the registry.
389
+ const active = getActiveCodexAccount();
390
+ if (active) {
391
+ return {
392
+ email: active.email ?? active.metadata?.email,
393
+ label: active.label,
394
+ providerAccountId: active.providerAccountId,
395
+ source: 'registry',
396
+ };
397
+ }
398
+ return { source: 'none' };
399
+ }
342
400
  function getCodexExecutionEnv(baseEnv = process.env) {
343
401
  return {
344
402
  ...baseEnv,
@@ -440,12 +498,12 @@ function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
440
498
  function findProfileForIdentity(identity) {
441
499
  const profiles = listCodexAccounts();
442
500
  if (identity?.workspaceId) {
443
- const byWorkspace = profiles.find(profile => profile.metadata?.workspaceId === identity.workspaceId);
501
+ const byWorkspace = profiles.find((profile) => profile.metadata?.workspaceId === identity.workspaceId);
444
502
  if (byWorkspace)
445
503
  return byWorkspace;
446
504
  }
447
505
  if (identity?.email) {
448
- const byEmail = profiles.find(profile => (profile.email ?? profile.metadata?.email) === identity.email);
506
+ const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email);
449
507
  if (byEmail)
450
508
  return byEmail;
451
509
  }
@@ -488,7 +546,9 @@ function syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
488
546
  metadata: { ...profile.metadata, ...metadata },
489
547
  });
490
548
  }
491
- catch { /* metadata refresh is best-effort */ }
549
+ catch {
550
+ /* metadata refresh is best-effort */
551
+ }
492
552
  return { syncedProfileId: profile.id };
493
553
  }
494
554
  catch (err) {
@@ -513,10 +573,16 @@ function performCodexAuthSwap(target) {
513
573
  const targetLegacyRaw = readFileOrNull(path.join(profileHome, '.credentials.json'));
514
574
  const targetName = target.label ?? target.email ?? target.id;
515
575
  if (!targetAuthRaw && !targetLegacyRaw) {
516
- return { success: false, error: `No stored credentials for "${targetName}". Remove and re-add this account.` };
576
+ return {
577
+ success: false,
578
+ error: `No stored credentials for "${targetName}". Remove and re-add this account.`,
579
+ };
517
580
  }
518
581
  if (targetAuthRaw && !parseAuthJson(targetAuthRaw)) {
519
- return { success: false, error: `Stored credentials for "${targetName}" are corrupted. Remove and re-add this account.` };
582
+ return {
583
+ success: false,
584
+ error: `Stored credentials for "${targetName}" are corrupted. Remove and re-add this account.`,
585
+ };
520
586
  }
521
587
  const warnings = [];
522
588
  if (detectRunningCodexProcess()) {
@@ -530,7 +596,9 @@ function performCodexAuthSwap(target) {
530
596
  const targetIdentity = readAuthIdentityFromRaw(targetAuthRaw);
531
597
  const targetWorkspaceId = target.metadata?.workspaceId ?? targetIdentity?.workspaceId;
532
598
  const targetEmail = target.email ?? target.metadata?.email ?? targetIdentity?.email;
533
- const liveMatchesTarget = Boolean((liveIdentity?.workspaceId && targetWorkspaceId && liveIdentity.workspaceId === targetWorkspaceId) ||
599
+ const liveMatchesTarget = Boolean((liveIdentity?.workspaceId &&
600
+ targetWorkspaceId &&
601
+ liveIdentity.workspaceId === targetWorkspaceId) ||
534
602
  (liveIdentity?.email && targetEmail && liveIdentity.email === targetEmail) ||
535
603
  (liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
536
604
  (!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
@@ -547,7 +615,9 @@ function performCodexAuthSwap(target) {
547
615
  metadata: { ...target.metadata, ...metadata },
548
616
  });
549
617
  }
550
- catch { /* backup refresh is best-effort */ }
618
+ catch {
619
+ /* backup refresh is best-effort */
620
+ }
551
621
  (0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
552
622
  return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
553
623
  }
@@ -569,7 +639,9 @@ function performCodexAuthSwap(target) {
569
639
  else
570
640
  fs.rmSync(liveLegacyPath, { force: true });
571
641
  }
572
- catch { /* rollback is best-effort */ }
642
+ catch {
643
+ /* rollback is best-effort */
644
+ }
573
645
  };
574
646
  try {
575
647
  if (targetAuthRaw) {
@@ -599,7 +671,7 @@ function performCodexAuthSwap(target) {
599
671
  return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
600
672
  }
601
673
  function switchToCodexAccount(profileId) {
602
- const target = listCodexAccounts().find(account => account.id === profileId);
674
+ const target = listCodexAccounts().find((account) => account.id === profileId);
603
675
  if (!target) {
604
676
  return { success: false, error: `Codex account ${profileId} not found.` };
605
677
  }
@@ -641,7 +713,9 @@ function reconcileCodexAuthState() {
641
713
  }
642
714
  const liveIdentity = readAuthIdentityFromRaw(liveAuthRaw);
643
715
  const profileIdentity = readAuthIdentityFromRaw(profileAuthRaw);
644
- const sameIdentity = Boolean((liveIdentity?.workspaceId && profileIdentity?.workspaceId && liveIdentity.workspaceId === profileIdentity.workspaceId) ||
716
+ const sameIdentity = Boolean((liveIdentity?.workspaceId &&
717
+ profileIdentity?.workspaceId &&
718
+ liveIdentity.workspaceId === profileIdentity.workspaceId) ||
645
719
  (liveIdentity?.email && liveIdentity.email === profileIdentity?.email));
646
720
  if (sameIdentity) {
647
721
  const liveRefresh = readLastRefresh(liveAuthRaw, liveAuthPath);
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const codexProfiles_1 = require("./codexProfiles");
44
44
  const quotaSnapshots_1 = require("./quotaSnapshots");
45
+ const quota_1 = require("./quota");
45
46
  const codex_1 = require("./providers/codex");
46
47
  const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
47
48
  const DEFAULT_MAX_SESSION_FILES = 50;
@@ -79,6 +80,11 @@ function isPreferredQuotaHit(candidate, current) {
79
80
  function normalizePercent(value) {
80
81
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
81
82
  }
83
+ function windowMinutesToMs(value, fallbackMs) {
84
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
85
+ ? value * 60_000
86
+ : fallbackMs;
87
+ }
82
88
  function timestampToIso(seconds) {
83
89
  if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) {
84
90
  return '';
@@ -89,8 +95,9 @@ function accountEmail(account) {
89
95
  return account?.email ?? account?.metadata?.email;
90
96
  }
91
97
  function enrichCodexQuota(state, account) {
98
+ const withProjections = (0, quota_1.withQuotaProjections)(state);
92
99
  return {
93
- ...state,
100
+ ...withProjections,
94
101
  runtimeProvider: 'codex',
95
102
  providerId: 'codex',
96
103
  accountLabel: account?.label,
@@ -183,12 +190,18 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
183
190
  const secondary = rateLimits?.secondary;
184
191
  if (!primary && !secondary)
185
192
  return null;
186
- return {
193
+ return (0, quota_1.withQuotaProjections)({
187
194
  fiveHour: primary
188
- ? { utilization: normalizePercent(primary.used_percent), resetsAt: timestampToIso(primary.resets_at) }
195
+ ? {
196
+ utilization: normalizePercent(primary.used_percent),
197
+ resetsAt: timestampToIso(primary.resets_at),
198
+ }
189
199
  : { utilization: 0, resetsAt: '' },
190
200
  sevenDay: secondary
191
- ? { utilization: normalizePercent(secondary.used_percent), resetsAt: timestampToIso(secondary.resets_at) }
201
+ ? {
202
+ utilization: normalizePercent(secondary.used_percent),
203
+ resetsAt: timestampToIso(secondary.resets_at),
204
+ }
192
205
  : { utilization: 0, resetsAt: '' },
193
206
  available: true,
194
207
  providerId: 'codex',
@@ -202,7 +215,11 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
202
215
  credits: rateLimits?.credits,
203
216
  planType: rateLimits?.plan_type,
204
217
  rateLimitReachedType: rateLimits?.rate_limit_reached_type,
205
- };
218
+ }, {
219
+ fiveHourWindowMs: windowMinutesToMs(primary?.window_minutes, quota_1.FIVE_HOUR_WINDOW_MS),
220
+ sevenDayWindowMs: windowMinutesToMs(secondary?.window_minutes, quota_1.SEVEN_DAY_WINDOW_MS),
221
+ capturedAt,
222
+ });
206
223
  }
207
224
  function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
208
225
  return readLatestCodexQuotaHitFromRollouts(sessionPaths, options)?.quota ?? null;
@@ -260,23 +277,30 @@ function resolveCodexQuotaFromLocalSources(options = {}) {
260
277
  const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
261
278
  const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
262
279
  let ownProvider = false;
263
- const provider = options.provider ?? (() => {
264
- ownProvider = true;
265
- return new codex_1.CodexProvider();
266
- })();
280
+ const provider = options.provider ??
281
+ (() => {
282
+ ownProvider = true;
283
+ return new codex_1.CodexProvider();
284
+ })();
267
285
  try {
268
286
  const candidates = [];
269
287
  if (options.workspacePath) {
270
288
  const workspaceSessions = provider.findAllSessions(options.workspacePath);
271
- const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
289
+ const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, {
290
+ maxTailBytes,
291
+ maxSessionFiles,
292
+ });
272
293
  if (workspaceHit)
273
294
  candidates.push(workspaceHit);
274
295
  }
275
296
  const accountSessions = findAccountRolloutFiles(options.codexHome);
276
- const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
297
+ const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, {
298
+ maxTailBytes,
299
+ maxSessionFiles,
300
+ });
277
301
  if (accountHit)
278
302
  candidates.push(accountHit);
279
- const latestHit = candidates.reduce((latest, candidate) => isPreferredQuotaHit(candidate, latest) ? candidate : latest, null);
303
+ const latestHit = candidates.reduce((latest, candidate) => (isPreferredQuotaHit(candidate, latest) ? candidate : latest), null);
280
304
  if (latestHit) {
281
305
  if (account)
282
306
  writeSnapshot('codex', account.id, latestHit.quota);
@@ -368,7 +392,9 @@ async function fetchCodexQuotaFromApi(options = {}) {
368
392
  ? 'server'
369
393
  : 'unknown',
370
394
  httpStatus: response.status,
371
- retryAfterMs: response.status === 429 ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,
395
+ retryAfterMs: response.status === 429
396
+ ? parseRetryAfterMs(response.headers.get('retry-after'))
397
+ : undefined,
372
398
  providerId: 'codex',
373
399
  source: 'api',
374
400
  capturedAt,
@@ -376,7 +402,7 @@ async function fetchCodexQuotaFromApi(options = {}) {
376
402
  sevenDayLabel: 'Secondary',
377
403
  };
378
404
  }
379
- const payload = await response.json();
405
+ const payload = (await response.json());
380
406
  const quota = quotaFromCodexRateLimits(rateLimitsFromUsagePayload(payload), 'api', capturedAt);
381
407
  if (!quota) {
382
408
  return {
@@ -464,7 +490,9 @@ function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
464
490
  try {
465
491
  fs.closeSync(fd);
466
492
  }
467
- catch { /* ignore */ }
493
+ catch {
494
+ /* ignore */
495
+ }
468
496
  }
469
497
  }
470
498
  return null;
@@ -500,5 +528,5 @@ function findRolloutFiles(sessionsDir) {
500
528
  }
501
529
  visit(sessionsDir);
502
530
  results.sort((a, b) => b.mtime - a.mtime);
503
- return results.map(item => item.path);
531
+ return results.map((item) => item.path);
504
532
  }
@@ -93,9 +93,18 @@ class CodexQuotaWatcher {
93
93
  running = false;
94
94
  constructor(workspacePath, options = {}) {
95
95
  this.workspacePath = workspacePath;
96
- this.discoveryPollIntervalMs = options.discoveryPollIntervalMs ?? DEFAULT_DISCOVERY_POLL_INTERVAL_MS;
96
+ this.discoveryPollIntervalMs =
97
+ options.discoveryPollIntervalMs ?? DEFAULT_DISCOVERY_POLL_INTERVAL_MS;
97
98
  this.providerFactory = options.providerFactory ?? (() => new codex_1.CodexProvider());
98
- this.getActiveAccount = options.getActiveAccount ?? codexProfiles_1.getActiveCodexAccount;
99
+ // Self-heal the registry pointer to the live login before reading it, so the
100
+ // history key (account.id) and the display label track the currently
101
+ // logged-in account even after a native `codex login`. Overridable for tests.
102
+ this.getActiveAccount =
103
+ options.getActiveAccount ??
104
+ (() => {
105
+ (0, codexProfiles_1.resolveActiveCodexAccount)();
106
+ return (0, codexProfiles_1.getActiveCodexAccount)();
107
+ });
99
108
  this.readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
100
109
  this.writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
101
110
  this.watchFile = options.watchFile ?? fs.watch;
@@ -207,8 +216,14 @@ class CodexQuotaWatcher {
207
216
  runtimeProvider: 'codex',
208
217
  providerId: account.id,
209
218
  workspaceId: this.workspaceId,
210
- fiveHour: { utilization: liveQuota.fiveHour.utilization, resetsAt: liveQuota.fiveHour.resetsAt },
211
- sevenDay: { utilization: liveQuota.sevenDay.utilization, resetsAt: liveQuota.sevenDay.resetsAt },
219
+ fiveHour: {
220
+ utilization: liveQuota.fiveHour.utilization,
221
+ resetsAt: liveQuota.fiveHour.resetsAt,
222
+ },
223
+ sevenDay: {
224
+ utilization: liveQuota.sevenDay.utilization,
225
+ resetsAt: liveQuota.sevenDay.resetsAt,
226
+ },
212
227
  available: liveQuota.available,
213
228
  error: liveQuota.error,
214
229
  source: liveQuota.source,
@@ -29,7 +29,9 @@ async function composeContext(slug, fidelity, provider, workspacePath) {
29
29
  try {
30
30
  sessionSummaries.push(provider.readSessionStats(sp));
31
31
  }
32
- catch { /* skip */ }
32
+ catch {
33
+ /* skip */
34
+ }
33
35
  }
34
36
  // Apply fidelity filtering
35
37
  let tasks;
@@ -40,23 +42,23 @@ async function composeContext(slug, fidelity, provider, workspacePath) {
40
42
  case 'full':
41
43
  tasks = allTasks;
42
44
  decisions = allDecisions;
43
- notes = allNotes.filter(n => n.status === 'active' || n.status === 'needs_review');
45
+ notes = allNotes.filter((n) => n.status === 'active' || n.status === 'needs_review');
44
46
  if (history) {
45
47
  stats = { tokens: { ...history.allTime.tokens }, cost: history.allTime.totalCost };
46
48
  }
47
49
  break;
48
50
  case 'compact':
49
- tasks = allTasks.filter(t => t.status === 'pending');
51
+ tasks = allTasks.filter((t) => t.status === 'pending');
50
52
  decisions = allDecisions.slice(0, 10);
51
- notes = allNotes.filter(n => n.status === 'active');
53
+ notes = allNotes.filter((n) => n.status === 'active');
52
54
  if (history) {
53
55
  stats = { tokens: { ...history.allTime.tokens }, cost: history.allTime.totalCost };
54
56
  }
55
57
  break;
56
58
  case 'brief':
57
- tasks = allTasks.filter(t => t.status === 'pending').slice(0, 3);
59
+ tasks = allTasks.filter((t) => t.status === 'pending').slice(0, 3);
58
60
  decisions = []; // Count only
59
- notes = allNotes.filter(n => n.importance === 'critical');
61
+ notes = allNotes.filter((n) => n.importance === 'critical');
60
62
  stats = null;
61
63
  break;
62
64
  }
@@ -65,7 +65,7 @@ function buildSessionContextSnapshot(events, options = {}) {
65
65
  const allSources = sourceState.sources;
66
66
  const limitedSources = limitSources(allSources, options.sourceLimit ?? DEFAULT_CONTEXT_SOURCE_LIMIT);
67
67
  const breakdown = buildLayerBreakdown(limitedSources);
68
- const layers = breakdown.map(row => row.layer);
68
+ const layers = breakdown.map((row) => row.layer);
69
69
  const sourceTokenTotal = allSources.reduce((sum, source) => sum + source.tokenEstimate, 0);
70
70
  const contextTokens = metrics.currentContextSize > 0 ? metrics.currentContextSize : sourceTokenTotal;
71
71
  const { pressure, ratio } = calculateSessionContextPressure(contextTokens, contextWindow);
@@ -381,16 +381,16 @@ function limitSources(sources, limit) {
381
381
  return [];
382
382
  if (sources.length <= limit)
383
383
  return [...sources];
384
- const pinned = sources.filter(source => source.sourceType === 'system' ||
384
+ const pinned = sources.filter((source) => source.sourceType === 'system' ||
385
385
  source.sourceType === 'runtime' ||
386
386
  source.sourceType === 'rate_limit');
387
387
  const pinnedLimit = Math.min(pinned.length, Math.max(1, Math.floor(limit / 4)));
388
388
  const pinnedKept = pinned.slice(0, pinnedLimit);
389
- const pinnedIds = new Set(pinnedKept.map(source => source.id));
390
- const remaining = sources.filter(source => !pinnedIds.has(source.id));
389
+ const pinnedIds = new Set(pinnedKept.map((source) => source.id));
390
+ const remaining = sources.filter((source) => !pinnedIds.has(source.id));
391
391
  const latest = remaining.slice(-(limit - pinnedKept.length));
392
- const keepIds = new Set([...pinnedKept, ...latest].map(source => source.id));
393
- return sources.filter(source => keepIds.has(source.id));
392
+ const keepIds = new Set([...pinnedKept, ...latest].map((source) => source.id));
393
+ return sources.filter((source) => keepIds.has(source.id));
394
394
  }
395
395
  function latestModel(events) {
396
396
  for (let i = events.length - 1; i >= 0; i--) {
@@ -440,7 +440,7 @@ function inferSourceFile(input) {
440
440
  }
441
441
  const files = input.files;
442
442
  if (Array.isArray(files)) {
443
- const first = files.find(value => typeof value === 'string' && value.trim());
443
+ const first = files.find((value) => typeof value === 'string' && value.trim());
444
444
  if (typeof first === 'string')
445
445
  return first.trim();
446
446
  }
@@ -451,7 +451,10 @@ function inferCommand(input) {
451
451
  if (typeof command === 'string' && command.trim())
452
452
  return command.trim();
453
453
  if (Array.isArray(command)) {
454
- return command.map(part => String(part)).join(' ').trim() || undefined;
454
+ return (command
455
+ .map((part) => String(part))
456
+ .join(' ')
457
+ .trim() || undefined);
455
458
  }
456
459
  return undefined;
457
460
  }
@@ -469,7 +472,7 @@ function extractText(content) {
469
472
  return content;
470
473
  if (Array.isArray(content)) {
471
474
  return content
472
- .map(block => isRecord(block) ? blockText(block) : '')
475
+ .map((block) => (isRecord(block) ? blockText(block) : ''))
473
476
  .filter(Boolean)
474
477
  .join('\n');
475
478
  }
@@ -66,9 +66,7 @@ function getCredentialsFilePath(configDir) {
66
66
  function readActiveCredentials(configDir) {
67
67
  if (process.platform === 'darwin') {
68
68
  try {
69
- const raw = (0, child_process_1.execFileSync)('security', [
70
- 'find-generic-password', '-s', (0, claudeProfiles_1.claudeKeychainService)(configDir), '-w',
71
- ], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
69
+ const raw = (0, child_process_1.execFileSync)('security', ['find-generic-password', '-s', (0, claudeProfiles_1.claudeKeychainService)(configDir), '-w'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 4000, killSignal: 'SIGKILL' });
72
70
  return JSON.parse(raw.trim());
73
71
  }
74
72
  catch {
@@ -96,11 +94,15 @@ function writeActiveCredentials(credentials, configDir) {
96
94
  JSON.parse(json); // validate round-trip
97
95
  if (process.platform === 'darwin') {
98
96
  (0, child_process_1.execFileSync)('security', [
99
- 'add-generic-password', '-U',
100
- '-s', (0, claudeProfiles_1.claudeKeychainService)(configDir),
101
- '-a', process.env.USER || 'user',
102
- '-w', json,
103
- ], { stdio: ['pipe', 'pipe', 'pipe'] });
97
+ 'add-generic-password',
98
+ '-U',
99
+ '-s',
100
+ (0, claudeProfiles_1.claudeKeychainService)(configDir),
101
+ '-a',
102
+ process.env.USER || 'user',
103
+ '-w',
104
+ json,
105
+ ], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 4000, killSignal: 'SIGKILL' });
104
106
  return;
105
107
  }
106
108
  // Linux / WSL / Windows — file-based
@@ -62,7 +62,15 @@ const os = __importStar(require("node:os"));
62
62
  const node_path_1 = require("node:path");
63
63
  const URL_RE = /(?:https?|file):\/\/[^\s"'<>)\]`\\]+/g;
64
64
  const PATH_RE = /(?:~|\.{0,2}\/)?[A-Za-z0-9_@.-]*(?:\/[A-Za-z0-9_@.-]+)+(?::\d+){0,2}|[A-Za-z0-9_@-]+\.[A-Za-z0-9]{1,8}(?::\d+){0,2}/g;
65
- const SHELL_TAGS = new Set(['sh', 'bash', 'shell', 'zsh', 'console', 'shellscript', 'shell-session']);
65
+ const SHELL_TAGS = new Set([
66
+ 'sh',
67
+ 'bash',
68
+ 'shell',
69
+ 'zsh',
70
+ 'console',
71
+ 'shellscript',
72
+ 'shell-session',
73
+ ]);
66
74
  exports.DEFAULT_CAPS = {
67
75
  command: 60,
68
76
  path: 60,
@@ -149,7 +149,13 @@ function accumClaude(filePath, cwd, acc) {
149
149
  }
150
150
  function readClaudeAssets(cwd, limit = 3) {
151
151
  const sessions = claudeSessions(cwd, limit);
152
- const acc = { urls: [], paths: [], commands: [], plans: [], hadSession: sessions.length > 0 };
152
+ const acc = {
153
+ urls: [],
154
+ paths: [],
155
+ commands: [],
156
+ plans: [],
157
+ hadSession: sessions.length > 0,
158
+ };
153
159
  for (const session of sessions)
154
160
  accumClaude(session, cwd, acc);
155
161
  return acc;
@@ -99,7 +99,9 @@ function rolloutFiles(limit = 150) {
99
99
  if (entry.isDirectory()) {
100
100
  walk(fullPath);
101
101
  }
102
- else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
102
+ else if (entry.isFile() &&
103
+ entry.name.startsWith('rollout-') &&
104
+ entry.name.endsWith('.jsonl')) {
103
105
  try {
104
106
  if (seen.has(fullPath))
105
107
  continue;
@@ -117,7 +119,10 @@ function rolloutFiles(limit = 150) {
117
119
  if (dirExists(sessionsDir))
118
120
  walk(sessionsDir);
119
121
  }
120
- return entries.sort((a, b) => b.mtime - a.mtime).slice(0, limit).map((entry) => entry.path);
122
+ return entries
123
+ .sort((a, b) => b.mtime - a.mtime)
124
+ .slice(0, limit)
125
+ .map((entry) => entry.path);
121
126
  }
122
127
  function codexSessions(cwd, limit = 3) {
123
128
  const exactCwd = (0, node_path_1.resolve)(cwd);
@@ -182,7 +187,13 @@ function addExecutedCommandAssets(acc, command, cwd, timestamp, meta) {
182
187
  function readCodexAssets(cwd, limit = 3) {
183
188
  const exactCwd = (0, node_path_1.resolve)(cwd);
184
189
  const files = codexSessions(exactCwd, limit);
185
- const acc = { urls: [], paths: [], commands: [], plans: [], hadSession: files.length > 0 };
190
+ const acc = {
191
+ urls: [],
192
+ paths: [],
193
+ commands: [],
194
+ plans: [],
195
+ hadSession: files.length > 0,
196
+ };
186
197
  const skip = (line) => line.includes('"type":"function_call_output"') ||
187
198
  line.includes('"type":"reasoning"') ||
188
199
  line.includes('"type":"token_count"');
@@ -37,10 +37,7 @@ function extractToolCalls(event) {
37
37
  const timestamp = new Date(event.timestamp);
38
38
  const calls = [];
39
39
  for (const block of content) {
40
- if (block &&
41
- typeof block === 'object' &&
42
- 'type' in block &&
43
- block.type === 'tool_use') {
40
+ if (block && typeof block === 'object' && 'type' in block && block.type === 'tool_use') {
44
41
  const toolBlock = block;
45
42
  if (toolBlock.name) {
46
43
  calls.push({