sidekick-shared 0.27.6 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +58 -18
  3. package/dist/browser.d.ts +13 -2
  4. package/dist/browser.js +30 -2
  5. package/dist/claudePaths.d.ts +11 -0
  6. package/dist/claudePaths.js +61 -0
  7. package/dist/claudeProfiles.d.ts +2 -9
  8. package/dist/claudeProfiles.js +10 -25
  9. package/dist/codexPaths.d.ts +22 -0
  10. package/dist/codexPaths.js +67 -0
  11. package/dist/codexProfiles.d.ts +1 -4
  12. package/dist/codexProfiles.js +5 -43
  13. package/dist/doctor.js +2 -2
  14. package/dist/esm/accountChangeSignal.js +15 -0
  15. package/dist/esm/accountChanges.js +152 -0
  16. package/dist/esm/accountHealth.js +288 -0
  17. package/dist/esm/accountLaunch.js +145 -0
  18. package/dist/esm/accountLease.js +60 -0
  19. package/dist/esm/accountManager.js +606 -0
  20. package/dist/esm/accountRegistry.js +214 -0
  21. package/dist/esm/accountStatus.js +51 -0
  22. package/dist/esm/accountSwitch.js +74 -0
  23. package/dist/esm/accountSync.js +156 -0
  24. package/dist/esm/accountSyncTypes.js +4 -0
  25. package/dist/esm/accounts.js +1291 -0
  26. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  27. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  28. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  29. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  30. package/dist/esm/aggregation/costProvenance.js +31 -0
  31. package/dist/esm/aggregation/snapshot.js +161 -0
  32. package/dist/esm/aggregation/types.js +5 -0
  33. package/dist/esm/analytics/codeImpact.js +18 -0
  34. package/dist/esm/analytics/compactionLedger.js +37 -0
  35. package/dist/esm/analytics/qualityScore.js +112 -0
  36. package/dist/esm/autoSwitch.js +185 -0
  37. package/dist/esm/browser.js +34 -0
  38. package/dist/esm/claudeCredentialDoctor.js +239 -0
  39. package/dist/esm/claudeCredentialLedger.js +91 -0
  40. package/dist/esm/claudeCredentials.js +111 -0
  41. package/dist/esm/claudeLiveGuard.js +43 -0
  42. package/dist/esm/claudePaths.js +22 -0
  43. package/dist/esm/claudeProfiles.js +142 -0
  44. package/dist/esm/claudeRefreshLock.js +106 -0
  45. package/dist/esm/codexAuth.js +101 -0
  46. package/dist/esm/codexCredentialDoctor.js +265 -0
  47. package/dist/esm/codexCredentialStore.js +164 -0
  48. package/dist/esm/codexKeepAlive.js +112 -0
  49. package/dist/esm/codexPaths.js +172 -0
  50. package/dist/esm/codexProfiles.js +1251 -0
  51. package/dist/esm/codexQuota.js +564 -0
  52. package/dist/esm/codexQuotaWatcher.js +363 -0
  53. package/dist/esm/context/composer.js +71 -0
  54. package/dist/esm/context/sessionContext.js +546 -0
  55. package/dist/esm/credentialIO.js +283 -0
  56. package/dist/esm/credentials.js +49 -0
  57. package/dist/esm/doctor.js +305 -0
  58. package/dist/esm/ensureDefaultAccounts.js +62 -0
  59. package/dist/esm/errorHistory.js +56 -0
  60. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  61. package/dist/esm/extractors/gatherAssets.js +32 -0
  62. package/dist/esm/extractors/sessionAssets.js +176 -0
  63. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  64. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  65. package/dist/esm/extractors/tokenUsage.js +41 -0
  66. package/dist/esm/extractors/toolCall.js +142 -0
  67. package/dist/esm/failingTools.js +65 -0
  68. package/dist/esm/fileHead.js +27 -0
  69. package/dist/esm/formatters/eventHighlighter.js +332 -0
  70. package/dist/esm/formatters/noiseClassifier.js +209 -0
  71. package/dist/esm/formatters/sessionDump.js +328 -0
  72. package/dist/esm/formatters/toolSummary.js +226 -0
  73. package/dist/esm/formatting.js +82 -0
  74. package/dist/esm/handoffUrl.js +23 -0
  75. package/dist/esm/historicalStore.js +305 -0
  76. package/dist/esm/humanPrompt.js +88 -0
  77. package/dist/esm/index.js +213 -0
  78. package/dist/esm/keepAlive.js +189 -0
  79. package/dist/esm/liveCredentialGuard.js +33 -0
  80. package/dist/esm/modelAliases.js +51 -0
  81. package/dist/esm/modelCatalog.js +139 -0
  82. package/dist/esm/modelContext.js +280 -0
  83. package/dist/esm/modelInfo.js +832 -0
  84. package/dist/esm/multiProviderQuotaService.js +450 -0
  85. package/dist/esm/node.js +46 -0
  86. package/dist/esm/observedContextWindows.js +165 -0
  87. package/dist/esm/observedSessionCollector.js +939 -0
  88. package/dist/esm/parsers/changelogParser.js +71 -0
  89. package/dist/esm/parsers/codexHistory.js +89 -0
  90. package/dist/esm/parsers/codexParser.js +906 -0
  91. package/dist/esm/parsers/debugLogParser.js +126 -0
  92. package/dist/esm/parsers/jsonl.js +66 -0
  93. package/dist/esm/parsers/mcpToolName.js +28 -0
  94. package/dist/esm/parsers/openCodeParser.js +625 -0
  95. package/dist/esm/parsers/planExtractor.js +335 -0
  96. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  97. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  98. package/dist/esm/parsers/subagentScanner.js +360 -0
  99. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  100. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  101. package/dist/esm/paths.js +134 -0
  102. package/dist/esm/peakHours.js +194 -0
  103. package/dist/esm/phrases.js +1330 -0
  104. package/dist/esm/pricingCatalog.js +247 -0
  105. package/dist/esm/processDetection.js +200 -0
  106. package/dist/esm/projectMigration.js +59 -0
  107. package/dist/esm/promptHistory.js +1221 -0
  108. package/dist/esm/providerFailure.js +362 -0
  109. package/dist/esm/providerIds.js +6 -0
  110. package/dist/esm/providerQuota.js +1 -0
  111. package/dist/esm/providerServiceStatus.js +145 -0
  112. package/dist/esm/providerServiceStatusTypes.js +1 -0
  113. package/dist/esm/providerStatus.js +117 -0
  114. package/dist/esm/providers/claudeCode.js +575 -0
  115. package/dist/esm/providers/claudeListing.js +121 -0
  116. package/dist/esm/providers/codex.js +1034 -0
  117. package/dist/esm/providers/codexDatabase.js +297 -0
  118. package/dist/esm/providers/codexRollouts.js +215 -0
  119. package/dist/esm/providers/detect.js +185 -0
  120. package/dist/esm/providers/diagnostics.js +83 -0
  121. package/dist/esm/providers/directoryListingCache.js +206 -0
  122. package/dist/esm/providers/factory.js +54 -0
  123. package/dist/esm/providers/openCode.js +2008 -0
  124. package/dist/esm/providers/openCodeDatabase.js +419 -0
  125. package/dist/esm/providers/rolloutWalker.js +252 -0
  126. package/dist/esm/providers/types.js +12 -0
  127. package/dist/esm/quota.js +182 -0
  128. package/dist/esm/quotaHistory.js +364 -0
  129. package/dist/esm/quotaPoller.js +230 -0
  130. package/dist/esm/quotaPresentation.js +97 -0
  131. package/dist/esm/quotaResolver.js +231 -0
  132. package/dist/esm/quotaSnapshots.js +130 -0
  133. package/dist/esm/quotaThresholds.js +85 -0
  134. package/dist/esm/readers/decisions.js +24 -0
  135. package/dist/esm/readers/handoff.js +20 -0
  136. package/dist/esm/readers/helpers.js +27 -0
  137. package/dist/esm/readers/history.js +9 -0
  138. package/dist/esm/readers/notes.js +48 -0
  139. package/dist/esm/readers/plans.js +225 -0
  140. package/dist/esm/readers/tasks.js +19 -0
  141. package/dist/esm/report/htmlHelpers.js +280 -0
  142. package/dist/esm/report/htmlReportGenerator.js +704 -0
  143. package/dist/esm/report/index.js +7 -0
  144. package/dist/esm/report/logo.js +2 -0
  145. package/dist/esm/report/openBrowser.js +19 -0
  146. package/dist/esm/report/sessionReportInputs.js +33 -0
  147. package/dist/esm/report/transcriptParser.js +83 -0
  148. package/dist/esm/report/types.js +4 -0
  149. package/dist/esm/schemas/accountManager.js +204 -0
  150. package/dist/esm/schemas/accountStatus.js +25 -0
  151. package/dist/esm/schemas/assistantTurn.js +74 -0
  152. package/dist/esm/schemas/index.js +25 -0
  153. package/dist/esm/schemas/observedSessionV1.js +77 -0
  154. package/dist/esm/schemas/providerFailure.js +37 -0
  155. package/dist/esm/schemas/quota.js +132 -0
  156. package/dist/esm/schemas/quotaHistory.js +40 -0
  157. package/dist/esm/schemas/sessionEvent.js +216 -0
  158. package/dist/esm/schemas/stateFile.js +75 -0
  159. package/dist/esm/schemas/tokenEstimation.js +7 -0
  160. package/dist/esm/schemas/transcript.js +86 -0
  161. package/dist/esm/schemas/usageNormalization.js +38 -0
  162. package/dist/esm/search/advancedFilter.js +197 -0
  163. package/dist/esm/search/sessionSearch.js +66 -0
  164. package/dist/esm/sessionFingerprint.js +40 -0
  165. package/dist/esm/sessionMonitor.js +198 -0
  166. package/dist/esm/sessionPreviews.js +522 -0
  167. package/dist/esm/sessionPromptHistory.js +254 -0
  168. package/dist/esm/sessionPromptSignals.js +364 -0
  169. package/dist/esm/sessionStats.js +160 -0
  170. package/dist/esm/sessionTokenTotals.js +98 -0
  171. package/dist/esm/sessionTranscripts.js +49 -0
  172. package/dist/esm/stateFile.js +119 -0
  173. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  174. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  175. package/dist/esm/statusline/formatter.js +122 -0
  176. package/dist/esm/statusline/index.js +8 -0
  177. package/dist/esm/terminalSync.js +95 -0
  178. package/dist/esm/testing/credentialGuard.js +197 -0
  179. package/dist/esm/testing/index.js +12 -0
  180. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  181. package/dist/esm/tokenEstimation.js +67 -0
  182. package/dist/esm/tokenSummary.js +113 -0
  183. package/dist/esm/toolCategory.js +82 -0
  184. package/dist/esm/transcript.js +257 -0
  185. package/dist/esm/turns/assistantTurn.js +373 -0
  186. package/dist/esm/types/codex.js +23 -0
  187. package/dist/esm/types/decisionLog.js +5 -0
  188. package/dist/esm/types/historicalData.js +47 -0
  189. package/dist/esm/types/knowledgeNote.js +15 -0
  190. package/dist/esm/types/observedSessionV1.js +257 -0
  191. package/dist/esm/types/opencode.js +12 -0
  192. package/dist/esm/types/plan.js +7 -0
  193. package/dist/esm/types/sessionEvent.js +10 -0
  194. package/dist/esm/types/taskPersistence.js +12 -0
  195. package/dist/esm/usage/billingBlocks.js +148 -0
  196. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  197. package/dist/esm/usage/importSessionHistory.js +84 -0
  198. package/dist/esm/usage/subagentUsage.js +18 -0
  199. package/dist/esm/usage/usageEvents.js +402 -0
  200. package/dist/esm/usage/usageReports.js +202 -0
  201. package/dist/esm/usageNormalization.js +95 -0
  202. package/dist/esm/watchers/eventBridge.js +219 -0
  203. package/dist/esm/watchers/factory.js +63 -0
  204. package/dist/esm/watchers/jsonlTail.js +289 -0
  205. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  206. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  207. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  208. package/dist/esm/watchers/types.js +4 -0
  209. package/dist/esm/workspaceRoots.js +56 -0
  210. package/dist/esm/writers/atomic.js +375 -0
  211. package/dist/esm/writers/decisions.js +51 -0
  212. package/dist/esm/writers/notes.js +46 -0
  213. package/dist/esm/writers/tasks.js +63 -0
  214. package/dist/esm/zaiQuota.js +323 -0
  215. package/dist/esm/zaiQuotaApi.js +271 -0
  216. package/dist/esm/zaiQuotaWatcher.js +265 -0
  217. package/dist/fileHead.d.ts +22 -0
  218. package/dist/fileHead.js +33 -0
  219. package/dist/index.d.ts +16 -7
  220. package/dist/index.js +49 -15
  221. package/dist/node.d.ts +13 -1
  222. package/dist/node.js +43 -2
  223. package/dist/parsers/codexHistory.js +2 -2
  224. package/dist/parsers/codexParser.d.ts +12 -0
  225. package/dist/parsers/codexParser.js +34 -12
  226. package/dist/parsers/debugLogParser.js +2 -2
  227. package/dist/parsers/sessionPathResolver.js +5 -4
  228. package/dist/promptHistory.d.ts +32 -0
  229. package/dist/promptHistory.js +115 -59
  230. package/dist/providerFailure.d.ts +12 -6
  231. package/dist/providerFailure.js +45 -0
  232. package/dist/providers/claudeCode.js +7 -99
  233. package/dist/providers/claudeListing.d.ts +22 -0
  234. package/dist/providers/claudeListing.js +158 -0
  235. package/dist/providers/codex.d.ts +3 -1
  236. package/dist/providers/codex.js +26 -227
  237. package/dist/providers/codexRollouts.d.ts +44 -0
  238. package/dist/providers/codexRollouts.js +261 -0
  239. package/dist/providers/detect.js +4 -4
  240. package/dist/readers/plans.js +3 -3
  241. package/dist/schemas/index.d.ts +2 -0
  242. package/dist/schemas/index.js +7 -2
  243. package/dist/schemas/providerFailure.d.ts +154 -0
  244. package/dist/schemas/providerFailure.js +40 -0
  245. package/dist/sessionPromptHistory.d.ts +12 -7
  246. package/dist/sessionPromptHistory.js +15 -3
  247. package/dist/sessionPromptSignals.d.ts +9 -0
  248. package/dist/sessionPromptSignals.js +13 -0
  249. package/dist/testing/credentialGuard.d.ts +23 -0
  250. package/dist/testing/credentialGuard.js +233 -0
  251. package/dist/testing/index.d.ts +14 -0
  252. package/dist/testing/index.js +22 -0
  253. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  254. package/dist/testing/sessionLogFixtures.js +169 -0
  255. package/dist/tokenEstimation.d.ts +2 -0
  256. package/dist/tokenEstimation.js +21 -11
  257. package/dist/toolCategory.d.ts +18 -0
  258. package/dist/toolCategory.js +86 -0
  259. package/dist/types/codex.d.ts +4 -0
  260. package/dist/usage/usageEvents.d.ts +16 -0
  261. package/dist/usage/usageEvents.js +53 -9
  262. package/dist/watchers/jsonlTail.d.ts +26 -1
  263. package/dist/watchers/jsonlTail.js +136 -28
  264. package/dist/watchers/jsonlWatcher.js +7 -0
  265. package/dist/watchers/providerReaderWatcher.js +1 -0
  266. package/dist/watchers/types.d.ts +6 -0
  267. package/dist/workspaceRoots.d.ts +23 -0
  268. package/dist/workspaceRoots.js +93 -0
  269. package/package.json +22 -2
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Per-account credential health. Health is derived from a secret-free sidecar
3
+ * (`health.json`) next to each profile, written whenever sidekick stores a
4
+ * credential, so reading health never spawns `security` or the provider CLI.
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { getClaudeCredentialBackupPath, getClaudeProfileDir, getClaudeProfileHome, readLiveClaudeIdentity, } from './claudeProfiles';
9
+ import { claudeCredentialFreshness, claudeRefreshFingerprint, parseClaudeCredentialBlob, resolveClaudeRefreshExpiry, } from './claudeCredentials';
10
+ import { readActiveCredentials, readClaudeCredentialStore } from './credentialIO';
11
+ import { identitiesMatch, codexRefreshFingerprint, compareCodexAuthFreshness, readAccessTokenExpiry, readAuthIdentityFromRaw, readLastRefresh, STALE_AUTH_THRESHOLD_MS, } from './codexAuth';
12
+ import { getCodexBackupPath, getCodexProfileDir, getCodexProfileHome, getSystemCodexHome, readFileOrNull, } from './codexPaths';
13
+ import { getActiveSavedAccount, listSavedAccountProfiles, } from './accountRegistry';
14
+ import { readCodexBackup, readCodexCredentialStore } from './codexCredentialStore';
15
+ import { atomicWriteJsonSync } from './writers/atomic';
16
+ /** Refresh tokens that expire within this window count as `expiring`. */
17
+ const REFRESH_EXPIRY_WARNING_MS = 3 * 24 * 60 * 60 * 1000;
18
+ export function getHealthSidecarPath(provider, accountId) {
19
+ const dir = provider === 'codex' ? getCodexProfileDir(accountId) : getClaudeProfileDir(accountId);
20
+ return path.join(dir, 'health.json');
21
+ }
22
+ export function readHealthSidecar(provider, accountId) {
23
+ try {
24
+ const parsed = JSON.parse(fs.readFileSync(getHealthSidecarPath(provider, accountId), 'utf8'));
25
+ return parsed?.version === 1 && typeof parsed.capturedAt === 'number' ? parsed : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function writeSidecar(provider, accountId, sidecar) {
32
+ try {
33
+ atomicWriteJsonSync(getHealthSidecarPath(provider, accountId), sidecar);
34
+ }
35
+ catch {
36
+ // Health is advisory; a failed sidecar write must not fail the credential write.
37
+ }
38
+ }
39
+ /** Build the sidecar for a Claude credential blob. */
40
+ export function claudeHealthSidecarFromBlob(blob, source, now = Date.now()) {
41
+ const credentials = parseClaudeCredentialBlob(blob);
42
+ if (!credentials)
43
+ return null;
44
+ const refresh = resolveClaudeRefreshExpiry(credentials);
45
+ return {
46
+ version: 1,
47
+ capturedAt: now,
48
+ accessExpiresAt: credentials.expiresAt,
49
+ refreshExpiresAt: refresh?.refreshExpiresAt,
50
+ refreshExpiryEstimated: refresh?.estimated,
51
+ hasRefreshToken: Boolean(credentials.refreshToken),
52
+ authMode: 'oauth',
53
+ refreshFp: claudeRefreshFingerprint(credentials) ?? undefined,
54
+ source,
55
+ };
56
+ }
57
+ /** Record the health of a Claude profile from the credential just stored. */
58
+ export function writeClaudeHealthSidecar(accountId, blob, source, now = Date.now()) {
59
+ const sidecar = claudeHealthSidecarFromBlob(blob, source, now);
60
+ if (sidecar)
61
+ writeSidecar('claude-code', accountId, sidecar);
62
+ return sidecar;
63
+ }
64
+ /** Build the sidecar for a raw Codex `auth.json`. */
65
+ export function codexHealthSidecarFromAuth(authRaw, source, now = Date.now(), fallbackPath) {
66
+ const identity = readAuthIdentityFromRaw(authRaw);
67
+ if (!identity)
68
+ return null;
69
+ const lastRefreshAt = readLastRefresh(authRaw, fallbackPath) ?? undefined;
70
+ return {
71
+ version: 1,
72
+ capturedAt: now,
73
+ accessExpiresAt: readAccessTokenExpiry(authRaw) ?? undefined,
74
+ lastRefreshAt,
75
+ hasRefreshToken: codexRefreshFingerprint(authRaw) !== null,
76
+ refreshFp: codexRefreshFingerprint(authRaw) ?? undefined,
77
+ authMode: identity.authMode,
78
+ source,
79
+ };
80
+ }
81
+ /** Record the health of a Codex profile from the auth file just stored. */
82
+ export function writeCodexHealthSidecar(accountId, authRaw, source, now = Date.now()) {
83
+ const sidecar = codexHealthSidecarFromAuth(authRaw, source, now);
84
+ if (sidecar)
85
+ writeSidecar('codex', accountId, sidecar);
86
+ return sidecar;
87
+ }
88
+ function stateFromClaudeSidecar(sidecar, now) {
89
+ const accessKnown = sidecar.accessExpiresAt !== undefined;
90
+ const accessExpired = accessKnown && sidecar.accessExpiresAt <= now;
91
+ if (sidecar.refreshExpiresAt !== undefined) {
92
+ if (sidecar.refreshExpiresAt <= now) {
93
+ return {
94
+ state: 'expired',
95
+ reason: sidecar.refreshExpiryEstimated
96
+ ? 'Refresh token has most likely expired (estimated from the snapshot age).'
97
+ : 'Refresh token expired; sign in again.',
98
+ };
99
+ }
100
+ if (sidecar.refreshExpiresAt - now <= REFRESH_EXPIRY_WARNING_MS) {
101
+ return { state: 'expiring', reason: 'Refresh token expires within 3 days.' };
102
+ }
103
+ }
104
+ if (sidecar.hasRefreshToken === false && accessExpired) {
105
+ return { state: 'expired', reason: 'Access token expired and no refresh token is stored.' };
106
+ }
107
+ if (accessExpired) {
108
+ return { state: 'expiring', reason: 'Access token expired; the CLI refreshes it on next use.' };
109
+ }
110
+ if (!accessKnown && sidecar.refreshExpiresAt === undefined) {
111
+ return { state: 'unknown', reason: 'The stored credential carries no expiry.' };
112
+ }
113
+ return { state: 'fresh' };
114
+ }
115
+ function stateFromCodexSidecar(sidecar, now) {
116
+ if (sidecar.authMode === 'api-key') {
117
+ return { state: 'unknown', reason: 'API-key login; expiry is not observable locally.' };
118
+ }
119
+ if (sidecar.accessExpiresAt !== undefined && sidecar.accessExpiresAt > now) {
120
+ return { state: 'fresh' };
121
+ }
122
+ if (sidecar.lastRefreshAt !== undefined &&
123
+ now - sidecar.lastRefreshAt > STALE_AUTH_THRESHOLD_MS) {
124
+ const days = Math.floor((now - sidecar.lastRefreshAt) / (24 * 60 * 60 * 1000));
125
+ return {
126
+ state: 'expiring',
127
+ reason: `Not refreshed in ${days} days; codex may ask you to log in again.`,
128
+ };
129
+ }
130
+ if (sidecar.accessExpiresAt !== undefined) {
131
+ return { state: 'expiring', reason: 'Access token expired; codex refreshes it on next use.' };
132
+ }
133
+ return { state: 'unknown', reason: 'The stored credential carries no expiry.' };
134
+ }
135
+ function isLiveClaudeAccount(accountId) {
136
+ return readLiveClaudeIdentity()?.uuid === accountId;
137
+ }
138
+ function isLiveCodexAccount(profile) {
139
+ const live = readAuthIdentityFromRaw(readFileOrNull(path.join(getSystemCodexHome(), 'auth.json')));
140
+ if (!live)
141
+ return false;
142
+ return identitiesMatch(live, {
143
+ email: profile.email ?? profile.metadata?.email,
144
+ workspaceId: profile.metadata?.workspaceId,
145
+ });
146
+ }
147
+ function readClaudeBackupBlob(accountId) {
148
+ try {
149
+ return JSON.parse(fs.readFileSync(getClaudeCredentialBackupPath(accountId), 'utf8'));
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ /**
156
+ * The credential a Claude profile would use right now: the live store for the
157
+ * live account (its profile home is empty by design), otherwise the fresher of
158
+ * the profile-home store and Sidekick's backup.
159
+ */
160
+ export function readEffectiveClaudeCredential(accountId) {
161
+ if (isLiveClaudeAccount(accountId))
162
+ return readActiveCredentials();
163
+ const store = readClaudeCredentialStore(getClaudeProfileHome(accountId));
164
+ const fromStore = store.status === 'ok' ? store.blob : null;
165
+ const backup = readClaudeBackupBlob(accountId);
166
+ const storeFreshness = claudeCredentialFreshness(parseClaudeCredentialBlob(fromStore));
167
+ const backupFreshness = claudeCredentialFreshness(parseClaudeCredentialBlob(backup));
168
+ if (parseClaudeCredentialBlob(backup) && backupFreshness > storeFreshness)
169
+ return backup;
170
+ return parseClaudeCredentialBlob(fromStore) ? fromStore : null;
171
+ }
172
+ function probeClaudeStore(accountId, now, persist = true) {
173
+ const blob = readEffectiveClaudeCredential(accountId);
174
+ if (!blob)
175
+ return null;
176
+ // A probe reads; it must not rewrite provenance (keep-alive relies on it).
177
+ const source = readHealthSidecar('claude-code', accountId)?.source ??
178
+ (isLiveClaudeAccount(accountId) ? 'live-sync' : 'migration');
179
+ return persist
180
+ ? writeClaudeHealthSidecar(accountId, blob, source, now)
181
+ : claudeHealthSidecarFromBlob(blob, source, now);
182
+ }
183
+ function probeCodexStore(accountId, now, persist = true) {
184
+ const profile = listSavedAccountProfiles('codex').find((p) => p.id === accountId);
185
+ const live = profile !== undefined && isLiveCodexAccount(profile);
186
+ const home = readCodexCredentialStore(live ? getSystemCodexHome() : getCodexProfileHome(accountId));
187
+ const backup = live ? null : readCodexBackup(accountId);
188
+ let raw = home.status === 'ok' ? home.snapshot.authRaw : null;
189
+ if (backup?.status === 'ok' &&
190
+ (!raw || compareCodexAuthFreshness(backup.snapshot.authRaw, raw) > 0))
191
+ raw = backup.snapshot.authRaw;
192
+ const source = readHealthSidecar('codex', accountId)?.source ?? (live ? 'live-sync' : 'migration');
193
+ const sidecar = codexHealthSidecarFromAuth(raw, source, now);
194
+ if (sidecar && persist)
195
+ writeSidecar('codex', accountId, sidecar);
196
+ return sidecar;
197
+ }
198
+ function hasStoredClaudeCredential(accountId) {
199
+ const home = getClaudeProfileHome(accountId);
200
+ return (fs.existsSync(path.join(home, '.credentials.json')) ||
201
+ fs.existsSync(path.join(getClaudeProfileDir(accountId), 'health.json')) ||
202
+ fs.existsSync(getClaudeCredentialBackupPath(accountId)) ||
203
+ (process.platform === 'darwin' && fs.existsSync(path.join(home, '.claude.json'))));
204
+ }
205
+ function hasStoredCodexCredential(accountId) {
206
+ const home = getCodexProfileHome(accountId);
207
+ return (fs.existsSync(getCodexBackupPath(accountId)) ||
208
+ fs.existsSync(getCodexBackupPath(accountId, true)) ||
209
+ fs.existsSync(path.join(home, 'auth.json')) ||
210
+ fs.existsSync(path.join(home, '.credentials.json')));
211
+ }
212
+ /**
213
+ * Health of one saved account. `probe: 'cache'` never spawns anything; a
214
+ * missing sidecar on a profile that has credentials is reported as `unknown`
215
+ * (never `expired`) until a store probe or the next sync rewrites it.
216
+ */
217
+ export function getAccountHealth(provider, accountId, options = {}) {
218
+ const now = options.now ?? Date.now();
219
+ const probe = options.probe ?? 'cache';
220
+ const profile = listSavedAccountProfiles(provider).find((p) => p.id === accountId);
221
+ const isLive = provider === 'codex'
222
+ ? profile !== undefined && isLiveCodexAccount(profile)
223
+ : isLiveClaudeAccount(accountId);
224
+ let sidecar = null;
225
+ if (probe === 'store') {
226
+ sidecar =
227
+ provider === 'codex'
228
+ ? probeCodexStore(accountId, now, options.persist)
229
+ : probeClaudeStore(accountId, now, options.persist);
230
+ }
231
+ sidecar ??= readHealthSidecar(provider, accountId);
232
+ if (!sidecar && provider === 'codex') {
233
+ // Codex sidecars are cheap to derive from the plaintext file.
234
+ sidecar = probeCodexStore(accountId, now, options.persist);
235
+ }
236
+ if (!sidecar && probe === 'auto' && hasStoredClaudeCredential(accountId)) {
237
+ // First look at a profile that predates the health model: read its store
238
+ // once and record the sidecar so later reads are cache hits.
239
+ sidecar = probeClaudeStore(accountId, now, options.persist);
240
+ }
241
+ const base = { providerId: provider, accountId, checkedAt: now, isLive };
242
+ if (!sidecar) {
243
+ const stored = provider === 'codex'
244
+ ? hasStoredCodexCredential(accountId)
245
+ : hasStoredClaudeCredential(accountId);
246
+ return stored
247
+ ? { ...base, state: 'unknown', reason: 'Credential health has not been recorded yet.' }
248
+ : { ...base, state: 'missing', reason: 'No stored credentials; sign in again.' };
249
+ }
250
+ const verdict = provider === 'codex'
251
+ ? stateFromCodexSidecar(sidecar, now)
252
+ : stateFromClaudeSidecar(sidecar, now);
253
+ return {
254
+ ...base,
255
+ ...verdict,
256
+ accessExpiresAt: sidecar.accessExpiresAt,
257
+ refreshExpiresAt: sidecar.refreshExpiresAt,
258
+ refreshExpiryEstimated: sidecar.refreshExpiryEstimated,
259
+ lastRefreshAt: sidecar.lastRefreshAt,
260
+ };
261
+ }
262
+ /** Saved accounts with their health, in registry order, grouped by provider. */
263
+ export function listAccountsWithHealth(provider, options = {}) {
264
+ const resolved = { probe: 'auto', ...options };
265
+ const providers = provider ? [provider] : ['claude-code', 'codex'];
266
+ const views = [];
267
+ for (const providerId of providers) {
268
+ const active = getActiveSavedAccount(providerId);
269
+ for (const profile of listSavedAccountProfiles(providerId)) {
270
+ views.push({
271
+ id: profile.id,
272
+ providerId,
273
+ label: profile.label,
274
+ email: profile.email ?? profile.metadata?.email,
275
+ planType: profile.metadata?.planType,
276
+ isActive: active?.id === profile.id,
277
+ health: getAccountHealth(providerId, profile.id, resolved),
278
+ source: profile.metadata?.origin === 'live-sync' ? 'learned' : 'registered',
279
+ addedAt: profile.addedAt,
280
+ });
281
+ }
282
+ }
283
+ return views;
284
+ }
285
+ /** Sidecar helper shared by the credential chokepoints: extracts the Claude credential shape. */
286
+ export function describeClaudeCredential(blob) {
287
+ return parseClaudeCredentialBlob(blob);
288
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Environment for running a provider CLI against a saved profile without
3
+ * touching the live home: two accounts can run side by side, each with its
4
+ * own refresh-token chain.
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { listSavedAccountProfiles } from './accountRegistry';
9
+ import { getAccountHealth } from './accountHealth';
10
+ import { CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV, getClaudeProfileHome, getLiveClaudeHome, isDefaultClaudeHome, } from './claudeProfiles';
11
+ import { restoreClaudeProfileFromBackup } from './accounts';
12
+ import { isSidekickCodexHome } from './codexCredentialStore';
13
+ import { restoreCodexProfileFromBackup, isLiveCodexProfile } from './codexProfiles';
14
+ import { forceFileCredentialStore, getCodexCredentialStoreMode, getCodexProfileHome, getSystemCodexHome, readFileOrNull, } from './codexPaths';
15
+ import { atomicWriteFileSync, atomicWriteJsonSync } from './writers/atomic';
16
+ function materializeClaudeHome(home) {
17
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
18
+ const liveHome = getLiveClaudeHome();
19
+ const settingsTarget = path.join(home, 'settings.json');
20
+ if (!fs.existsSync(settingsTarget)) {
21
+ const settings = readFileOrNull(path.join(liveHome, 'settings.json'));
22
+ if (settings)
23
+ atomicWriteFileSync(settingsTarget, settings);
24
+ }
25
+ const configPath = path.join(home, '.claude.json');
26
+ let config = {};
27
+ try {
28
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
29
+ }
30
+ catch {
31
+ /* fresh */
32
+ }
33
+ if (config.hasCompletedOnboarding !== true) {
34
+ atomicWriteJsonSync(configPath, { ...config, hasCompletedOnboarding: true });
35
+ }
36
+ }
37
+ function materializeCodexHome(home) {
38
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
39
+ const configTarget = path.join(home, 'config.toml');
40
+ const current = readFileOrNull(configTarget);
41
+ const source = current ?? readFileOrNull(path.join(getSystemCodexHome(), 'config.toml')) ?? '';
42
+ const fileMode = forceFileCredentialStore(source);
43
+ if (fileMode !== current)
44
+ atomicWriteFileSync(configTarget, fileMode);
45
+ }
46
+ export function getAccountLaunchEnv(provider, accountId, options = {}) {
47
+ const profile = listSavedAccountProfiles(provider).find((p) => p.id === accountId);
48
+ const command = provider === 'codex' ? 'codex' : 'claude';
49
+ const home = provider === 'codex'
50
+ ? getCodexProfileHome(accountId)
51
+ : getClaudeProfileHome(profile?.providerAccountId ?? accountId);
52
+ const health = getAccountHealth(provider, accountId);
53
+ const warnings = [];
54
+ const base = {
55
+ provider,
56
+ accountId,
57
+ label: profile?.label,
58
+ email: profile?.email ?? profile?.metadata?.email,
59
+ home,
60
+ env: provider === 'codex' ? { CODEX_HOME: home } : { CLAUDE_CONFIG_DIR: home },
61
+ envUnset: provider === 'codex' ? [] : [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
62
+ command,
63
+ health,
64
+ warnings,
65
+ };
66
+ if (!profile) {
67
+ return { ...base, error: `${provider} account ${accountId} not found.` };
68
+ }
69
+ if (provider === 'codex' && isSidekickCodexHome(getSystemCodexHome())) {
70
+ return {
71
+ ...base,
72
+ error: 'Unset CODEX_HOME before managing accounts; it names a Sidekick-owned home.',
73
+ };
74
+ }
75
+ if (provider === 'codex' && !['file', 'unknown'].includes(getCodexCredentialStoreMode())) {
76
+ return { ...base, error: 'Codex account launches require readable file-mode credentials.' };
77
+ }
78
+ if (provider === 'codex' && isLiveCodexProfile(profile)) {
79
+ const liveHome = getSystemCodexHome();
80
+ return {
81
+ ...base,
82
+ home: liveHome,
83
+ env: { CODEX_HOME: liveHome },
84
+ warnings: ['This account is the live login, so the session uses the normal Codex home.'],
85
+ };
86
+ }
87
+ if (provider === 'claude-code' && health.isLive) {
88
+ // The live account's only CLI-usable copy is the live store; a session in
89
+ // its profile home would be a second copy racing it over the refresh
90
+ // token. Point the session at the live home instead.
91
+ const liveHome = getLiveClaudeHome();
92
+ const customHome = !isDefaultClaudeHome();
93
+ return {
94
+ ...base,
95
+ home: liveHome,
96
+ env: customHome ? { CLAUDE_CONFIG_DIR: liveHome } : {},
97
+ envUnset: customHome
98
+ ? [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]
99
+ : ['CLAUDE_CONFIG_DIR', CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
100
+ warnings: ['This account is the live login, so the session uses the normal Claude home.'],
101
+ };
102
+ }
103
+ if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
104
+ return {
105
+ ...base,
106
+ error: `Stored credentials for "${profile.label ?? profile.email ?? accountId}" have ${health.state === 'missing' ? 'not been saved' : 'expired'}; sign in again before launching.`,
107
+ };
108
+ }
109
+ if (health.isLive) {
110
+ warnings.push('This account is also the live login. Two sessions refreshing the same login can invalidate each other; prefer switching instead.');
111
+ }
112
+ if (health.state === 'expiring' && health.reason)
113
+ warnings.push(health.reason);
114
+ if (provider === 'claude-code' && options.restoreBackup !== false) {
115
+ if (restoreClaudeProfileFromBackup(accountId) === 'restored') {
116
+ warnings.push("Restored this account's saved login into its profile home. If a session elsewhere still uses this login, close it first: two copies of one login invalidate each other.");
117
+ }
118
+ }
119
+ if (provider === 'codex') {
120
+ const restored = restoreCodexProfileFromBackup(accountId, {
121
+ restore: options.restoreBackup !== false,
122
+ });
123
+ if (restored === 'none')
124
+ return { ...base, error: 'No safe inactive Codex credential is available; sign in again.' };
125
+ if (restored === 'live')
126
+ return { ...base, home: getSystemCodexHome(), env: { CODEX_HOME: getSystemCodexHome() } };
127
+ }
128
+ if (provider === 'codex' &&
129
+ options.materialize === false &&
130
+ !['file', 'unknown'].includes(getCodexCredentialStoreMode(home))) {
131
+ return { ...base, error: 'The isolated Codex home must use file credential storage.' };
132
+ }
133
+ if (options.materialize !== false) {
134
+ try {
135
+ if (provider === 'codex')
136
+ materializeCodexHome(home);
137
+ else
138
+ materializeClaudeHome(home);
139
+ }
140
+ catch (err) {
141
+ warnings.push(`Could not prepare the isolated home: ${err}`);
142
+ }
143
+ }
144
+ return base;
145
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Keep-alive leases: while keep-alive runs a provider CLI inside a profile
3
+ * home, that home's credential may be mid-refresh. A switch to that account
4
+ * would copy the pre-refresh token into the live store just as the CLI kills
5
+ * it, so switches refuse a leased target. Leases are acquired under the
6
+ * provider's swap lock (the same lock a switch holds), expire on their own,
7
+ * and are ignored once their process is gone.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { getClaudeProfileDir } from './claudeProfiles';
12
+ import { getCodexProfileDir } from './codexPaths';
13
+ import { atomicWriteJsonSync } from './writers/atomic';
14
+ function leasePath(provider, accountId) {
15
+ const dir = provider === 'codex' ? getCodexProfileDir(accountId) : getClaudeProfileDir(accountId);
16
+ return path.join(dir, 'keepalive.lease');
17
+ }
18
+ function processAlive(pid) {
19
+ if (pid === process.pid)
20
+ return true;
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ return error.code === 'EPERM';
27
+ }
28
+ }
29
+ export function isAccountLeaseHeld(provider, accountId, now = Date.now()) {
30
+ try {
31
+ const lease = JSON.parse(fs.readFileSync(leasePath(provider, accountId), 'utf8'));
32
+ return (typeof lease.pid === 'number' &&
33
+ typeof lease.expiresAt === 'number' &&
34
+ lease.expiresAt > now &&
35
+ processAlive(lease.pid));
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Take the lease; the caller holds the provider's swap lock. False when held elsewhere. */
42
+ export function acquireAccountLease(provider, accountId, durationMs, now = Date.now()) {
43
+ if (isAccountLeaseHeld(provider, accountId, now))
44
+ return false;
45
+ const target = leasePath(provider, accountId);
46
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
47
+ atomicWriteJsonSync(target, {
48
+ pid: process.pid,
49
+ expiresAt: now + durationMs,
50
+ });
51
+ return true;
52
+ }
53
+ export function releaseAccountLease(provider, accountId) {
54
+ try {
55
+ fs.rmSync(leasePath(provider, accountId), { force: true });
56
+ }
57
+ catch {
58
+ /* expires on its own */
59
+ }
60
+ }