sidekick-shared 0.21.1 → 0.21.3

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 +45 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.js +18 -14
  4. package/dist/accountStatus.js +4 -1
  5. package/dist/accounts.js +60 -25
  6. package/dist/aggregation/EventAggregator.d.ts +14 -11
  7. package/dist/aggregation/EventAggregator.js +68 -31
  8. package/dist/aggregation/PatternExtractor.js +5 -4
  9. package/dist/autoSwitch.js +6 -12
  10. package/dist/claudeProfiles.js +3 -3
  11. package/dist/codexProfiles.js +45 -22
  12. package/dist/codexQuota.js +44 -16
  13. package/dist/codexQuotaWatcher.js +10 -3
  14. package/dist/context/composer.js +8 -6
  15. package/dist/context/sessionContext.js +12 -9
  16. package/dist/credentialIO.js +10 -8
  17. package/dist/extractors/sessionAssets.js +9 -1
  18. package/dist/extractors/sources/claudeAssets.js +7 -1
  19. package/dist/extractors/sources/codexAssets.js +14 -3
  20. package/dist/extractors/toolCall.js +1 -4
  21. package/dist/formatters/eventHighlighter.js +148 -28
  22. package/dist/formatters/noiseClassifier.js +3 -11
  23. package/dist/formatters/sessionDump.js +22 -13
  24. package/dist/formatters/toolSummary.js +1 -3
  25. package/dist/index.d.ts +25 -23
  26. package/dist/index.js +12 -3
  27. package/dist/modelContext.js +10 -8
  28. package/dist/modelInfo.js +11 -6
  29. package/dist/multiProviderQuotaService.js +10 -7
  30. package/dist/parsers/codexParser.js +170 -103
  31. package/dist/parsers/debugLogParser.js +5 -4
  32. package/dist/parsers/openCodeParser.js +66 -48
  33. package/dist/parsers/planExtractor.js +3 -3
  34. package/dist/parsers/sessionActivityDetector.js +10 -6
  35. package/dist/parsers/sessionPathResolver.js +29 -34
  36. package/dist/parsers/subagentScanner.js +21 -20
  37. package/dist/parsers/subagentTraceParser.js +7 -4
  38. package/dist/phrases.js +11 -11
  39. package/dist/providerStatus.js +3 -4
  40. package/dist/providers/claudeCode.js +3 -1
  41. package/dist/providers/codex.js +35 -14
  42. package/dist/providers/codexDatabase.js +14 -5
  43. package/dist/providers/detect.js +10 -6
  44. package/dist/providers/openCode.d.ts +4 -4
  45. package/dist/providers/openCode.js +96 -68
  46. package/dist/providers/openCodeDatabase.js +44 -17
  47. package/dist/quota.d.ts +19 -0
  48. package/dist/quota.js +50 -8
  49. package/dist/quotaHistory.js +10 -3
  50. package/dist/quotaSnapshots.d.ts +1 -1
  51. package/dist/quotaSnapshots.js +2 -2
  52. package/dist/readers/decisions.js +1 -1
  53. package/dist/readers/notes.js +5 -3
  54. package/dist/readers/plans.js +43 -23
  55. package/dist/readers/tasks.js +1 -1
  56. package/dist/report/htmlHelpers.js +135 -16
  57. package/dist/report/htmlReportGenerator.js +38 -12
  58. package/dist/report/transcriptParser.js +8 -2
  59. package/dist/schemas/index.d.ts +4 -4
  60. package/dist/schemas/quota.js +2 -10
  61. package/dist/schemas/sessionEvent.js +20 -10
  62. package/dist/search/advancedFilter.js +3 -3
  63. package/dist/search/sessionSearch.js +10 -4
  64. package/dist/terminalSync.js +4 -2
  65. package/dist/turns/assistantTurn.js +5 -1
  66. package/dist/watchers/eventBridge.js +56 -26
  67. package/dist/watchers/factory.js +5 -2
  68. package/dist/watchers/jsonlTail.js +3 -1
  69. package/dist/watchers/jsonlWatcher.js +128 -52
  70. package/dist/watchers/providerReaderWatcher.js +3 -1
  71. package/dist/watchers/sqliteWatcher.js +52 -22
  72. package/dist/zaiQuota.d.ts +6 -6
  73. package/dist/zaiQuota.js +6 -10
  74. package/dist/zaiQuotaApi.d.ts +33 -0
  75. package/dist/zaiQuotaApi.js +279 -0
  76. package/dist/zaiQuotaWatcher.js +10 -9
  77. package/package.json +5 -2
@@ -42,7 +42,10 @@ function parseTodoDependencies(content, allTodos) {
42
42
  const match = content.match(depPattern);
43
43
  if (!match)
44
44
  return [];
45
- const refs = match[1].split(/\s+and\s+|,\s*|&\s*/).map(r => r.trim()).filter(Boolean);
45
+ const refs = match[1]
46
+ .split(/\s+and\s+|,\s*|&\s*/)
47
+ .map((r) => r.trim())
48
+ .filter(Boolean);
46
49
  const result = [];
47
50
  for (const ref of refs) {
48
51
  const refLower = ref.toLowerCase();
@@ -235,7 +238,8 @@ class EventAggregator {
235
238
  contextSize = inputTok + cacheWrite + cacheRead;
236
239
  }
237
240
  // Compaction detection
238
- if (this.previousContextSize > 0 && contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
241
+ if (this.previousContextSize > 0 &&
242
+ contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
239
243
  this.compactionEvents.push({
240
244
  timestamp: new Date(event.timestamp),
241
245
  contextBefore: this.previousContextSize,
@@ -263,8 +267,14 @@ class EventAggregator {
263
267
  : 0;
264
268
  const evPriced = event.cost != null || pricing != null;
265
269
  const acc = this.modelUsage.get(model) ?? {
266
- calls: 0, tokens: 0, inputTokens: 0, outputTokens: 0,
267
- cacheWriteTokens: 0, cacheReadTokens: 0, cost: 0, priced: true,
270
+ calls: 0,
271
+ tokens: 0,
272
+ inputTokens: 0,
273
+ outputTokens: 0,
274
+ cacheWriteTokens: 0,
275
+ cacheReadTokens: 0,
276
+ cost: 0,
277
+ priced: true,
268
278
  };
269
279
  acc.calls++;
270
280
  acc.tokens += inputTok + outputTok;
@@ -339,8 +349,10 @@ class EventAggregator {
339
349
  .sort((a, b) => b.calls - a.calls);
340
350
  }
341
351
  getToolStats() {
342
- return Array.from(this.toolAnalytics.values())
343
- .sort((a, b) => (b.successCount + b.failureCount + b.pendingCount) - (a.successCount + a.failureCount + a.pendingCount));
352
+ return Array.from(this.toolAnalytics.values()).sort((a, b) => b.successCount +
353
+ b.failureCount +
354
+ b.pendingCount -
355
+ (a.successCount + a.failureCount + a.pendingCount));
344
356
  }
345
357
  getCompactionEvents() {
346
358
  return [...this.compactionEvents];
@@ -349,7 +361,7 @@ class EventAggregator {
349
361
  return [...this.truncationEvents];
350
362
  }
351
363
  getBurnRate() {
352
- const points = this.burnSamples.map(s => s.tokens);
364
+ const points = this.burnSamples.map((s) => s.tokens);
353
365
  const tokensPerMinute = points.length > 0 ? points[points.length - 1] : 0;
354
366
  return {
355
367
  tokensPerMinute,
@@ -384,7 +396,7 @@ class EventAggregator {
384
396
  const records = this.latencyRecords;
385
397
  const totalFirstToken = records.reduce((sum, r) => sum + r.firstTokenLatencyMs, 0);
386
398
  const totalResponse = records.reduce((sum, r) => sum + r.totalResponseTimeMs, 0);
387
- const maxFirstToken = Math.max(...records.map(r => r.firstTokenLatencyMs));
399
+ const maxFirstToken = Math.max(...records.map((r) => r.firstTokenLatencyMs));
388
400
  const last = records[records.length - 1];
389
401
  return {
390
402
  recentLatencies: [...records],
@@ -421,8 +433,8 @@ class EventAggregator {
421
433
  contextTimeline: [...this.contextTimeline],
422
434
  timeline: this.getTimeline(),
423
435
  latencyStats: this.getLatencyStats(),
424
- toolFrequency: this.toolFrequency.getTopN(20).map(e => ({ name: e.key, count: e.count })),
425
- wordFrequency: this.wordFrequency.getTopN(20).map(e => ({ name: e.key, count: e.count })),
436
+ toolFrequency: this.toolFrequency.getTopN(20).map((e) => ({ name: e.key, count: e.count })),
437
+ wordFrequency: this.wordFrequency.getTopN(20).map((e) => ({ name: e.key, count: e.count })),
426
438
  patterns: this.patternExtractor.getPatterns().slice(0, 20),
427
439
  heatmapBuckets: this.heatmapTracker.getBuckets(),
428
440
  };
@@ -560,7 +572,9 @@ class EventAggregator {
560
572
  this.activeTaskId = state.activeTaskId;
561
573
  this.subagents = [...state.subagents];
562
574
  this.currentPermissionMode = state.permissionMode ?? null;
563
- this.permissionModeHistory = state.permissionModeHistory ? [...state.permissionModeHistory] : [];
575
+ this.permissionModeHistory = state.permissionModeHistory
576
+ ? [...state.permissionModeHistory]
577
+ : [];
564
578
  this.contextTimeline = state.contextTimeline ? [...state.contextTimeline] : [];
565
579
  this.contextTurnIndex = state.contextTurnIndex ?? 0;
566
580
  this.timeline = [...state.timeline];
@@ -639,7 +653,8 @@ class EventAggregator {
639
653
  contextSize = inputTok + cacheWrite + cacheRead;
640
654
  }
641
655
  // Compaction detection
642
- if (this.previousContextSize > 0 && contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
656
+ if (this.previousContextSize > 0 &&
657
+ contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
643
658
  this.compactionEvents.push({
644
659
  timestamp: new Date(timestamp),
645
660
  contextBefore: this.previousContextSize,
@@ -659,9 +674,15 @@ class EventAggregator {
659
674
  // `priced` flag so one unpriced event taints the aggregate (UI renders "—").
660
675
  const perModelKey = model ?? this.currentModel ?? 'unknown';
661
676
  const acc = this.modelUsage.get(perModelKey) ?? {
662
- calls: 0, tokens: 0, inputTokens: 0, outputTokens: 0,
663
- cacheWriteTokens: 0, cacheReadTokens: 0, reasoningTokens: 0,
664
- cost: 0, priced: true,
677
+ calls: 0,
678
+ tokens: 0,
679
+ inputTokens: 0,
680
+ outputTokens: 0,
681
+ cacheWriteTokens: 0,
682
+ cacheReadTokens: 0,
683
+ reasoningTokens: 0,
684
+ cost: 0,
685
+ priced: true,
665
686
  };
666
687
  acc.calls++;
667
688
  acc.tokens += inputTok + outputTok;
@@ -696,7 +717,7 @@ class EventAggregator {
696
717
  this.lastBurnSampleTime = now;
697
718
  // Trim to window
698
719
  const cutoff = now - this.burnWindowMs;
699
- this.burnSamples = this.burnSamples.filter(s => s.time >= cutoff);
720
+ this.burnSamples = this.burnSamples.filter((s) => s.time >= cutoff);
700
721
  }
701
722
  }
702
723
  // ═══════════════════════════════════════════════════════════════════════
@@ -716,7 +737,8 @@ class EventAggregator {
716
737
  // First assistant response with text
717
738
  this.pendingUserRequest.firstResponseReceived = true;
718
739
  this.pendingUserRequest.firstResponseTimestamp = now;
719
- this.pendingUserRequest.firstTokenLatencyMs = now.getTime() - this.pendingUserRequest.timestamp.getTime();
740
+ this.pendingUserRequest.firstTokenLatencyMs =
741
+ now.getTime() - this.pendingUserRequest.timestamp.getTime();
720
742
  }
721
743
  if (event.message.usage && this.pendingUserRequest.firstResponseReceived) {
722
744
  // Assistant with usage -> complete the cycle
@@ -751,7 +773,12 @@ class EventAggregator {
751
773
  continue;
752
774
  // Record in toolAnalytics
753
775
  const analytics = this.toolAnalytics.get(name) ?? {
754
- name, successCount: 0, failureCount: 0, totalDuration: 0, completedCount: 0, pendingCount: 0,
776
+ name,
777
+ successCount: 0,
778
+ failureCount: 0,
779
+ totalDuration: 0,
780
+ completedCount: 0,
781
+ pendingCount: 0,
755
782
  };
756
783
  analytics.pendingCount++;
757
784
  this.toolAnalytics.set(name, analytics);
@@ -825,7 +852,12 @@ class EventAggregator {
825
852
  const name = event.toolName;
826
853
  // Update analytics
827
854
  const analytics = this.toolAnalytics.get(name) ?? {
828
- name, successCount: 0, failureCount: 0, totalDuration: 0, completedCount: 0, pendingCount: 0,
855
+ name,
856
+ successCount: 0,
857
+ failureCount: 0,
858
+ totalDuration: 0,
859
+ completedCount: 0,
860
+ pendingCount: 0,
829
861
  };
830
862
  analytics.pendingCount++;
831
863
  this.toolAnalytics.set(name, analytics);
@@ -1380,11 +1412,11 @@ class EventAggregator {
1380
1412
  }
1381
1413
  }
1382
1414
  convertExtractedPlanToPlanState(extracted) {
1383
- const completed = extracted.steps.filter(s => s.status === 'completed').length;
1415
+ const completed = extracted.steps.filter((s) => s.status === 'completed').length;
1384
1416
  const total = extracted.steps.length;
1385
1417
  return {
1386
1418
  active: true,
1387
- steps: extracted.steps.map(s => ({
1419
+ steps: extracted.steps.map((s) => ({
1388
1420
  id: s.id,
1389
1421
  description: s.description,
1390
1422
  status: s.status,
@@ -1406,7 +1438,9 @@ class EventAggregator {
1406
1438
  if (Array.isArray(content)) {
1407
1439
  for (const block of content) {
1408
1440
  if (block.type === 'tool_result') {
1409
- const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '');
1441
+ const text = typeof block.content === 'string'
1442
+ ? block.content
1443
+ : JSON.stringify(block.content || '');
1410
1444
  this.contextAttribution.toolOutputs += this.estimateTokens(text);
1411
1445
  }
1412
1446
  else if (block.type === 'text') {
@@ -1470,7 +1504,9 @@ class EventAggregator {
1470
1504
  if (Array.isArray(content)) {
1471
1505
  for (const block of content) {
1472
1506
  if (block.type === 'tool_result') {
1473
- const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '');
1507
+ const text = typeof block.content === 'string'
1508
+ ? block.content
1509
+ : JSON.stringify(block.content || '');
1474
1510
  this.contextAttribution.toolOutputs += this.estimateTokens(text);
1475
1511
  }
1476
1512
  else if (block.type === 'text') {
@@ -1521,8 +1557,10 @@ class EventAggregator {
1521
1557
  }
1522
1558
  else if (event.type === 'tool_result') {
1523
1559
  const rawContent = raw?.content;
1524
- const text = typeof rawContent === 'string' ? rawContent
1525
- : rawContent ? JSON.stringify(rawContent)
1560
+ const text = typeof rawContent === 'string'
1561
+ ? rawContent
1562
+ : rawContent
1563
+ ? JSON.stringify(rawContent)
1526
1564
  : event.summary || '';
1527
1565
  if (text)
1528
1566
  this.contextAttribution.toolOutputs += this.estimateTokens(text);
@@ -1573,16 +1611,15 @@ class EventAggregator {
1573
1611
  if (event.message.model)
1574
1612
  metadata.model = event.message.model;
1575
1613
  if (event.message.usage) {
1576
- metadata.tokenCount = event.message.usage.input_tokens + event.message.usage.output_tokens;
1614
+ metadata.tokenCount =
1615
+ event.message.usage.input_tokens + event.message.usage.output_tokens;
1577
1616
  }
1578
1617
  break;
1579
1618
  case 'tool_use': {
1580
1619
  tlType = 'tool_call';
1581
1620
  // Use rich tool summary formatter
1582
1621
  const toolName = event.tool?.name ?? 'unknown';
1583
- const toolSummary = event.tool?.input
1584
- ? (0, toolSummary_1.formatToolSummary)(toolName, event.tool.input)
1585
- : '';
1622
+ const toolSummary = event.tool?.input ? (0, toolSummary_1.formatToolSummary)(toolName, event.tool.input) : '';
1586
1623
  description = toolSummary ? `${toolName}: ${toolSummary}` : toolName;
1587
1624
  noiseLevel = 'system';
1588
1625
  if (event.tool)
@@ -1702,7 +1739,7 @@ class EventAggregator {
1702
1739
  if (typeof content === 'string')
1703
1740
  return content.length > 0;
1704
1741
  if (Array.isArray(content)) {
1705
- return content.some(b => b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
1742
+ return content.some((b) => b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
1706
1743
  }
1707
1744
  return false;
1708
1745
  }
@@ -1756,7 +1793,7 @@ class EventAggregator {
1756
1793
  }
1757
1794
  // Word frequency: extract significant words from summary
1758
1795
  if (summary) {
1759
- const words = summary.split(/\s+/).filter(w => w.length > 2);
1796
+ const words = summary.split(/\s+/).filter((w) => w.length > 2);
1760
1797
  for (const word of words) {
1761
1798
  this.wordFrequency.increment(word.toLowerCase(), timestamp);
1762
1799
  }
@@ -126,7 +126,10 @@ class PatternExtractor {
126
126
  }
127
127
  // ── Private ──
128
128
  tokenize(text) {
129
- return text.split(/\s+/).filter(t => t.length > 0).slice(0, this.maxDepth * 4);
129
+ return text
130
+ .split(/\s+/)
131
+ .filter((t) => t.length > 0)
132
+ .slice(0, this.maxDepth * 4);
130
133
  }
131
134
  similarity(tokens, cluster) {
132
135
  const len = Math.min(tokens.length, this.maxDepth);
@@ -139,9 +142,7 @@ class PatternExtractor {
139
142
  return matches / len;
140
143
  }
141
144
  buildTemplate(cluster) {
142
- return cluster.tokens
143
- .map((tok, i) => (cluster.mask[i] ? '<*>' : tok))
144
- .join(' ');
145
+ return cluster.tokens.map((tok, i) => (cluster.mask[i] ? '<*>' : tok)).join(' ');
145
146
  }
146
147
  totalClusters() {
147
148
  let total = 0;
@@ -38,7 +38,7 @@ function decideAutoSwitch(_provider, active, candidates, config) {
38
38
  return null;
39
39
  const activeRemaining = quotaRemaining(active.quota);
40
40
  const best = candidates
41
- .filter(candidate => candidate.accountId !== active.accountId &&
41
+ .filter((candidate) => candidate.accountId !== active.accountId &&
42
42
  candidate.switchable !== false &&
43
43
  candidate.quota?.available)
44
44
  .sort((a, b) => quotaRemaining(b.quota) - quotaRemaining(a.quota))[0];
@@ -89,7 +89,7 @@ class AutoSwitchController {
89
89
  start() {
90
90
  if (this.subscription)
91
91
  return;
92
- this.subscription = this.quotaService.onUpdate(state => this.handleUpdate(state));
92
+ this.subscription = this.quotaService.onUpdate((state) => this.handleUpdate(state));
93
93
  }
94
94
  stop() {
95
95
  this.subscription?.dispose();
@@ -112,27 +112,21 @@ class AutoSwitchController {
112
112
  }
113
113
  handleProviderUpdate(runtimeProvider, activeQuota) {
114
114
  const providerId = runtimeStateToProvider(runtimeProvider);
115
- const activeAccount = providerId === 'claude-code'
116
- ? this.getActiveClaudeAccount()
117
- : this.getActiveCodexAccount();
115
+ const activeAccount = providerId === 'claude-code' ? this.getActiveClaudeAccount() : this.getActiveCodexAccount();
118
116
  if (!activeAccount)
119
117
  return;
120
118
  const activeAccountId = accountIdFor(providerId, activeAccount);
121
119
  if (quotaUtilization(activeQuota) < this.config.thresholdPct) {
122
120
  this.switchedDuringCrossing.delete(providerId);
123
121
  }
124
- const accounts = providerId === 'claude-code'
125
- ? this.getClaudeAccounts()
126
- : this.getCodexAccounts();
122
+ const accounts = providerId === 'claude-code' ? this.getClaudeAccounts() : this.getCodexAccounts();
127
123
  if (accounts.length <= 1)
128
124
  return;
129
- const candidates = accounts.map(account => {
125
+ const candidates = accounts.map((account) => {
130
126
  const accountId = accountIdFor(providerId, account);
131
127
  return {
132
128
  accountId,
133
- quota: accountId === activeAccountId
134
- ? activeQuota
135
- : this.readSnapshot(providerId, accountId),
129
+ quota: accountId === activeAccountId ? activeQuota : this.readSnapshot(providerId, accountId),
136
130
  };
137
131
  });
138
132
  const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config);
@@ -69,9 +69,7 @@ function readClaudeProfileIdentity(home) {
69
69
  const raw = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
70
70
  const email = raw?.oauthAccount?.emailAddress;
71
71
  const uuid = raw?.oauthAccount?.accountUuid;
72
- return typeof email === 'string' && typeof uuid === 'string'
73
- ? { email, uuid }
74
- : null;
72
+ return typeof email === 'string' && typeof uuid === 'string' ? { email, uuid } : null;
75
73
  }
76
74
  catch {
77
75
  return null;
@@ -81,6 +79,8 @@ function keychainServiceExists(service) {
81
79
  try {
82
80
  (0, child_process_1.execFileSync)('security', ['find-generic-password', '-s', service], {
83
81
  stdio: ['ignore', 'ignore', 'ignore'],
82
+ timeout: 4000,
83
+ killSignal: 'SIGKILL',
84
84
  });
85
85
  return true;
86
86
  }
@@ -121,7 +121,9 @@ function atomicWriteJson(filePath, data, mode = 0o600) {
121
121
  try {
122
122
  fs.unlinkSync(tmp);
123
123
  }
124
- catch { /* nothing to clean up */ }
124
+ catch {
125
+ /* nothing to clean up */
126
+ }
125
127
  throw err;
126
128
  }
127
129
  }
@@ -139,7 +141,9 @@ function atomicWriteFile(filePath, content, mode = 0o600) {
139
141
  try {
140
142
  fs.unlinkSync(tmp);
141
143
  }
142
- catch { /* nothing to clean up */ }
144
+ catch {
145
+ /* nothing to clean up */
146
+ }
143
147
  throw err;
144
148
  }
145
149
  }
@@ -216,12 +220,8 @@ function readAuthIdentityFromRaw(raw) {
216
220
  const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
217
221
  ? authClaims.chatgpt_account_id
218
222
  : 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';
223
+ const planType = typeof authClaims?.chatgpt_plan_type === 'string' ? authClaims.chatgpt_plan_type : undefined;
224
+ const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key' ? 'api-key' : 'chatgpt';
225
225
  return { email, workspaceId, planType, authMode };
226
226
  }
227
227
  function readLastRefresh(raw, fallbackPath) {
@@ -235,7 +235,9 @@ function readLastRefresh(raw, fallbackPath) {
235
235
  try {
236
236
  return fs.statSync(fallbackPath).mtimeMs;
237
237
  }
238
- catch { /* fall through */ }
238
+ catch {
239
+ /* fall through */
240
+ }
239
241
  }
240
242
  return null;
241
243
  }
@@ -271,6 +273,8 @@ function getCodexLoginStatus(codexHome) {
271
273
  const result = (0, child_process_1.spawnSync)('codex', ['login', 'status'], {
272
274
  encoding: 'utf8',
273
275
  env,
276
+ timeout: 4000,
277
+ killSignal: 'SIGKILL',
274
278
  });
275
279
  const stdout = String(result.stdout ?? '').trim();
276
280
  if (result.status === 0 && /^Logged in/i.test(stdout)) {
@@ -292,7 +296,11 @@ function detectRunningCodexProcess() {
292
296
  if (process.platform === 'win32')
293
297
  return false;
294
298
  try {
295
- return (0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], { encoding: 'utf8' }).status === 0;
299
+ return ((0, child_process_1.spawnSync)('pgrep', ['-x', 'codex'], {
300
+ encoding: 'utf8',
301
+ timeout: 4000,
302
+ killSignal: 'SIGKILL',
303
+ }).status === 0);
296
304
  }
297
305
  catch {
298
306
  return false;
@@ -324,8 +332,7 @@ function isCodexProfileAuthenticated(codexHome) {
324
332
  }
325
333
  function ensureUniqueCodexLabel(label, excludeId) {
326
334
  const normalized = label.trim().toLowerCase();
327
- const conflict = listCodexAccounts().find(account => account.id !== excludeId &&
328
- (account.label ?? '').trim().toLowerCase() === normalized);
335
+ const conflict = listCodexAccounts().find((account) => account.id !== excludeId && (account.label ?? '').trim().toLowerCase() === normalized);
329
336
  return conflict ? `A Codex account named "${label}" already exists.` : null;
330
337
  }
331
338
  function listCodexAccounts() {
@@ -440,12 +447,12 @@ function stashLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
440
447
  function findProfileForIdentity(identity) {
441
448
  const profiles = listCodexAccounts();
442
449
  if (identity?.workspaceId) {
443
- const byWorkspace = profiles.find(profile => profile.metadata?.workspaceId === identity.workspaceId);
450
+ const byWorkspace = profiles.find((profile) => profile.metadata?.workspaceId === identity.workspaceId);
444
451
  if (byWorkspace)
445
452
  return byWorkspace;
446
453
  }
447
454
  if (identity?.email) {
448
- const byEmail = profiles.find(profile => (profile.email ?? profile.metadata?.email) === identity.email);
455
+ const byEmail = profiles.find((profile) => (profile.email ?? profile.metadata?.email) === identity.email);
449
456
  if (byEmail)
450
457
  return byEmail;
451
458
  }
@@ -488,7 +495,9 @@ function syncBackLiveCodexAuth(liveAuthRaw, liveLegacyRaw) {
488
495
  metadata: { ...profile.metadata, ...metadata },
489
496
  });
490
497
  }
491
- catch { /* metadata refresh is best-effort */ }
498
+ catch {
499
+ /* metadata refresh is best-effort */
500
+ }
492
501
  return { syncedProfileId: profile.id };
493
502
  }
494
503
  catch (err) {
@@ -513,10 +522,16 @@ function performCodexAuthSwap(target) {
513
522
  const targetLegacyRaw = readFileOrNull(path.join(profileHome, '.credentials.json'));
514
523
  const targetName = target.label ?? target.email ?? target.id;
515
524
  if (!targetAuthRaw && !targetLegacyRaw) {
516
- return { success: false, error: `No stored credentials for "${targetName}". Remove and re-add this account.` };
525
+ return {
526
+ success: false,
527
+ error: `No stored credentials for "${targetName}". Remove and re-add this account.`,
528
+ };
517
529
  }
518
530
  if (targetAuthRaw && !parseAuthJson(targetAuthRaw)) {
519
- return { success: false, error: `Stored credentials for "${targetName}" are corrupted. Remove and re-add this account.` };
531
+ return {
532
+ success: false,
533
+ error: `Stored credentials for "${targetName}" are corrupted. Remove and re-add this account.`,
534
+ };
520
535
  }
521
536
  const warnings = [];
522
537
  if (detectRunningCodexProcess()) {
@@ -530,7 +545,9 @@ function performCodexAuthSwap(target) {
530
545
  const targetIdentity = readAuthIdentityFromRaw(targetAuthRaw);
531
546
  const targetWorkspaceId = target.metadata?.workspaceId ?? targetIdentity?.workspaceId;
532
547
  const targetEmail = target.email ?? target.metadata?.email ?? targetIdentity?.email;
533
- const liveMatchesTarget = Boolean((liveIdentity?.workspaceId && targetWorkspaceId && liveIdentity.workspaceId === targetWorkspaceId) ||
548
+ const liveMatchesTarget = Boolean((liveIdentity?.workspaceId &&
549
+ targetWorkspaceId &&
550
+ liveIdentity.workspaceId === targetWorkspaceId) ||
534
551
  (liveIdentity?.email && targetEmail && liveIdentity.email === targetEmail) ||
535
552
  (liveAuthRaw !== null && liveAuthRaw === targetAuthRaw) ||
536
553
  (!liveAuthRaw && !targetAuthRaw && liveLegacyRaw !== null && liveLegacyRaw === targetLegacyRaw));
@@ -547,7 +564,9 @@ function performCodexAuthSwap(target) {
547
564
  metadata: { ...target.metadata, ...metadata },
548
565
  });
549
566
  }
550
- catch { /* backup refresh is best-effort */ }
567
+ catch {
568
+ /* backup refresh is best-effort */
569
+ }
551
570
  (0, accountRegistry_1.setActiveSavedAccount)('codex', target.id);
552
571
  return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
553
572
  }
@@ -569,7 +588,9 @@ function performCodexAuthSwap(target) {
569
588
  else
570
589
  fs.rmSync(liveLegacyPath, { force: true });
571
590
  }
572
- catch { /* rollback is best-effort */ }
591
+ catch {
592
+ /* rollback is best-effort */
593
+ }
573
594
  };
574
595
  try {
575
596
  if (targetAuthRaw) {
@@ -599,7 +620,7 @@ function performCodexAuthSwap(target) {
599
620
  return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
600
621
  }
601
622
  function switchToCodexAccount(profileId) {
602
- const target = listCodexAccounts().find(account => account.id === profileId);
623
+ const target = listCodexAccounts().find((account) => account.id === profileId);
603
624
  if (!target) {
604
625
  return { success: false, error: `Codex account ${profileId} not found.` };
605
626
  }
@@ -641,7 +662,9 @@ function reconcileCodexAuthState() {
641
662
  }
642
663
  const liveIdentity = readAuthIdentityFromRaw(liveAuthRaw);
643
664
  const profileIdentity = readAuthIdentityFromRaw(profileAuthRaw);
644
- const sameIdentity = Boolean((liveIdentity?.workspaceId && profileIdentity?.workspaceId && liveIdentity.workspaceId === profileIdentity.workspaceId) ||
665
+ const sameIdentity = Boolean((liveIdentity?.workspaceId &&
666
+ profileIdentity?.workspaceId &&
667
+ liveIdentity.workspaceId === profileIdentity.workspaceId) ||
645
668
  (liveIdentity?.email && liveIdentity.email === profileIdentity?.email));
646
669
  if (sameIdentity) {
647
670
  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
  }