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,606 @@
1
+ import { spawn } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { getAccountsDir, listSavedAccountProfiles, readSavedAccountRegistry, upsertSavedAccountProfile, } from './accountRegistry';
6
+ import { atomicWriteJsonSync as atomicWriteJson } from './writers/atomic';
7
+ import { applyClaudeProfileToLiveHome, listAccounts, readActiveClaudeAccount, storeClaudeProfileCredentials, storeClaudeProfileIdentity, switchToAccount, switchToAccountAsync, } from './accounts';
8
+ import { buildClaudeChildEnv, CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV, ensureClaudeProfileDirs, getClaudeProfileDir, getClaudeProfileHome, isClaudeProfileAuthenticated, readClaudeProfileIdentity, } from './claudeProfiles';
9
+ import { deleteStoredCredentials, readClaudeCredentialStore } from './credentialIO';
10
+ import { recordClaudeCredentialHome } from './claudeCredentialLedger';
11
+ import { finalizeCodexAccount, finalizeCodexAccountAsync, discardCodexLogin, isCodexProfileAuthenticated, isCodexProfileAuthenticatedAsync, listCodexAccounts, prepareCodexAccount, readCodexAccountMetadata, readCodexAccountMetadataAsync, switchToCodexAccount, switchToCodexAccountAsync, } from './codexProfiles';
12
+ import { getCodexLoginHome } from './codexPaths';
13
+ import { readLastSwitch, switchFailure, } from './accountSwitch';
14
+ function getPendingClaudeProfilePath(loginId) {
15
+ return path.join(getClaudeProfileDir(loginId), 'profile.json');
16
+ }
17
+ function readPendingClaudeProfile(loginId) {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(getPendingClaudeProfilePath(loginId), 'utf8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function writePendingClaudeProfile(loginId, pending) {
26
+ ensureClaudeProfileDirs(loginId);
27
+ atomicWriteJson(getPendingClaudeProfilePath(loginId), pending);
28
+ }
29
+ function removePendingClaudeProfile(loginId) {
30
+ try {
31
+ fs.rmSync(getPendingClaudeProfilePath(loginId), { force: true });
32
+ }
33
+ catch {
34
+ /* best effort */
35
+ }
36
+ }
37
+ /**
38
+ * A fresh `CLAUDE_CONFIG_DIR` puts the CLI through first-run onboarding before
39
+ * the login prompt. Seeding the onboarding flag keeps the isolated login on the
40
+ * sign-in flow only; `oauthAccount` is written by the CLI itself.
41
+ */
42
+ function seedClaudeLoginHome(home) {
43
+ const configPath = path.join(home, '.claude.json');
44
+ if (fs.existsSync(configPath))
45
+ return;
46
+ try {
47
+ atomicWriteJson(configPath, { hasCompletedOnboarding: true });
48
+ }
49
+ catch {
50
+ /* the CLI creates the file itself */
51
+ }
52
+ }
53
+ function readClaudeOauthAccount(home) {
54
+ try {
55
+ const raw = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
56
+ return raw?.oauthAccount ?? null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials, oauthAccount) {
63
+ const canonicalHome = getClaudeProfileHome(accountUuid);
64
+ fs.mkdirSync(canonicalHome, { recursive: true, mode: 0o700 });
65
+ if (path.resolve(sourceHome) !== path.resolve(canonicalHome)) {
66
+ try {
67
+ // Credentials go only through the chokepoint below: when this account is
68
+ // the live one its profile home must not receive a CLI-usable copy.
69
+ fs.cpSync(sourceHome, canonicalHome, {
70
+ recursive: true,
71
+ force: true,
72
+ filter: (src) => {
73
+ const base = path.basename(src);
74
+ return base !== '.credentials.json' && !base.startsWith('.oauth_refresh.lock');
75
+ },
76
+ });
77
+ }
78
+ catch {
79
+ // The credential and identity files are re-written below when possible.
80
+ }
81
+ }
82
+ storeClaudeProfileIdentity(accountUuid, oauthAccount);
83
+ storeClaudeProfileCredentials(accountUuid, credentials, 'login');
84
+ }
85
+ function parseClaudeLoginArgs(raw) {
86
+ const trimmed = raw?.trim();
87
+ return trimmed ? trimmed.split(/\s+/) : null;
88
+ }
89
+ /**
90
+ * `claude auth login` signs in without the interactive first-run flow the bare
91
+ * `claude /login` form triggers in a fresh config directory. Override with
92
+ * `SIDEKICK_CLAUDE_LOGIN_ARGS` (space separated) for older CLIs.
93
+ */
94
+ export function resolveClaudeLoginCommand(opts = {}) {
95
+ if (opts.loginCommand)
96
+ return opts.loginCommand;
97
+ return {
98
+ command: 'claude',
99
+ args: parseClaudeLoginArgs(process.env.SIDEKICK_CLAUDE_LOGIN_ARGS) ?? ['auth', 'login'],
100
+ };
101
+ }
102
+ export function beginAccountLogin(provider, label, opts = {}) {
103
+ if (provider === 'codex') {
104
+ const existing = opts.existingAccountId
105
+ ? listCodexAccounts().find((account) => account.id === opts.existingAccountId)
106
+ : undefined;
107
+ if (opts.existingAccountId && !existing) {
108
+ return { success: false, error: `Codex account ${opts.existingAccountId} not found.` };
109
+ }
110
+ // A re-login uses a throwaway label: finalize folds the fresh credentials
111
+ // into the existing profile by identity and keeps its label.
112
+ const effectiveLabel = existing
113
+ ? `${existing.label ?? existing.email ?? 'codex'} (re-login ${randomUUID().slice(0, 8)})`
114
+ : label.trim() || `Codex ${listCodexAccounts().length + 1}`;
115
+ const prepared = prepareCodexAccount(effectiveLabel, { importCurrent: false });
116
+ if (!prepared.success || !prepared.profileId || !prepared.codexHome) {
117
+ return { success: false, error: prepared.error ?? 'Could not prepare Codex account login.' };
118
+ }
119
+ if (prepared.needsLogin === false) {
120
+ return {
121
+ success: true,
122
+ loginId: prepared.profileId,
123
+ alreadyComplete: true,
124
+ configDir: prepared.codexHome,
125
+ existingAccountId: existing?.id,
126
+ };
127
+ }
128
+ const loginCommand = opts.loginCommand ?? { command: 'codex', args: ['login'] };
129
+ return {
130
+ success: true,
131
+ loginId: prepared.profileId,
132
+ command: loginCommand.command,
133
+ args: loginCommand.args,
134
+ env: { CODEX_HOME: prepared.codexHome },
135
+ configDir: prepared.codexHome,
136
+ existingAccountId: existing?.id,
137
+ };
138
+ }
139
+ const existing = opts.existingAccountId
140
+ ? listSavedAccountProfiles('claude-code').find((account) => account.id === opts.existingAccountId)
141
+ : undefined;
142
+ if (opts.existingAccountId && !existing) {
143
+ return { success: false, error: `Claude account ${opts.existingAccountId} not found.` };
144
+ }
145
+ const loginId = randomUUID();
146
+ const home = getClaudeProfileHome(loginId);
147
+ writePendingClaudeProfile(loginId, {
148
+ label: existing?.label ?? label.trim(),
149
+ addedAt: existing?.addedAt ?? new Date().toISOString(),
150
+ existingAccountId: existing?.id,
151
+ });
152
+ seedClaudeLoginHome(home);
153
+ // Claude Code creates this home's Keychain item itself; record the home
154
+ // first so the item is provably Sidekick's even if everything else fails.
155
+ recordClaudeCredentialHome(home, 'login');
156
+ const loginCommand = resolveClaudeLoginCommand(opts);
157
+ return {
158
+ success: true,
159
+ loginId,
160
+ command: loginCommand.command,
161
+ args: loginCommand.args,
162
+ env: { CLAUDE_CONFIG_DIR: home },
163
+ envUnset: [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
164
+ configDir: home,
165
+ existingAccountId: existing?.id,
166
+ };
167
+ }
168
+ function getClaudeAccountLoginStatus(loginId) {
169
+ const home = getClaudeProfileHome(loginId);
170
+ const identity = readClaudeProfileIdentity(home);
171
+ return isClaudeProfileAuthenticated(home)
172
+ ? { state: 'authenticated', email: identity?.email }
173
+ : { state: 'pending' };
174
+ }
175
+ export function getAccountLoginStatus(provider, loginId) {
176
+ if (provider === 'codex') {
177
+ const codexHome = getCodexLoginHome(loginId);
178
+ if (!isCodexProfileAuthenticated(codexHome)) {
179
+ return { state: 'pending' };
180
+ }
181
+ const metadata = readCodexAccountMetadata(codexHome);
182
+ return {
183
+ state: 'authenticated',
184
+ email: metadata.email,
185
+ };
186
+ }
187
+ return getClaudeAccountLoginStatus(loginId);
188
+ }
189
+ /**
190
+ * {@link getAccountLoginStatus} without blocking the event loop on the codex
191
+ * CLI probe. The sync variant blocks up to 4s per call while a codex login is
192
+ * pending — ruinous for callers that poll it on an interval.
193
+ */
194
+ export async function getAccountLoginStatusAsync(provider, loginId) {
195
+ if (provider === 'codex') {
196
+ const codexHome = getCodexLoginHome(loginId);
197
+ if (!(await isCodexProfileAuthenticatedAsync(codexHome))) {
198
+ return { state: 'pending' };
199
+ }
200
+ const metadata = await readCodexAccountMetadataAsync(codexHome);
201
+ return {
202
+ state: 'authenticated',
203
+ email: metadata.email,
204
+ };
205
+ }
206
+ return getClaudeAccountLoginStatus(loginId);
207
+ }
208
+ export function finalizeAccountLogin(provider, loginId, opts = {}) {
209
+ if (provider === 'codex') {
210
+ return finalizeCodexAccount(loginId, opts);
211
+ }
212
+ return finalizeClaudeAccountLogin(loginId, opts);
213
+ }
214
+ /** {@link finalizeAccountLogin} without blocking the event loop on CLI probes. */
215
+ export async function finalizeAccountLoginAsync(provider, loginId, opts = {}) {
216
+ if (provider === 'codex') {
217
+ return finalizeCodexAccountAsync(loginId, opts);
218
+ }
219
+ return finalizeClaudeAccountLogin(loginId, opts);
220
+ }
221
+ function finalizeClaudeAccountLogin(loginId, opts = {}) {
222
+ const home = getClaudeProfileHome(loginId);
223
+ const identity = readClaudeProfileIdentity(home);
224
+ if (!identity) {
225
+ return { success: false, error: 'Claude profile is not authenticated yet.' };
226
+ }
227
+ const read = readClaudeCredentialStore(home);
228
+ if (read.status === 'error') {
229
+ // Transient (locked Keychain, timeout): keep the login home for a retry.
230
+ return { success: false, error: 'Could not read the new Claude login yet; try again.' };
231
+ }
232
+ const warnings = [];
233
+ try {
234
+ if (read.status !== 'ok') {
235
+ return {
236
+ success: false,
237
+ error: 'The sign-in finished without a readable credential; sign in again.',
238
+ };
239
+ }
240
+ const oauthAccount = readClaudeOauthAccount(home);
241
+ if (!oauthAccount) {
242
+ return { success: false, error: 'Could not read Claude profile config.' };
243
+ }
244
+ const pending = readPendingClaudeProfile(loginId);
245
+ const existing = listSavedAccountProfiles('claude-code').find((account) => (account.providerAccountId ?? account.id) === identity.uuid);
246
+ // The chokepoint writes the private backup first, so a failure below
247
+ // still leaves the new login saved.
248
+ copyClaudeProfileToCanonicalHome(home, identity.uuid, read.blob, oauthAccount);
249
+ if (pending?.existingAccountId && pending.existingAccountId !== identity.uuid) {
250
+ warnings.push(`You signed in as ${identity.email}, which is a different account from the one you chose to re-authenticate; it was saved separately.`);
251
+ }
252
+ upsertSavedAccountProfile({
253
+ id: existing?.id ?? identity.uuid,
254
+ providerId: 'claude-code',
255
+ providerAccountId: identity.uuid,
256
+ email: identity.email,
257
+ label: existing?.label ?? (pending?.label || identity.email),
258
+ addedAt: existing?.addedAt ?? pending?.addedAt ?? new Date().toISOString(),
259
+ metadata: {
260
+ ...existing?.metadata,
261
+ email: identity.email,
262
+ origin: 'login',
263
+ },
264
+ });
265
+ }
266
+ catch (err) {
267
+ return { success: false, error: `Could not save the new Claude login: ${err}` };
268
+ }
269
+ finally {
270
+ // Every exit after authentication: the temporary home's Keychain item
271
+ // holds a live refresh token that nothing will ever reference again.
272
+ removePendingClaudeProfile(loginId);
273
+ if (loginId !== identity.uuid) {
274
+ removePendingClaudeProfile(identity.uuid);
275
+ discardClaudeLoginHome(loginId);
276
+ }
277
+ }
278
+ if (opts.activate === false) {
279
+ return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
280
+ }
281
+ if (readActiveClaudeAccount()?.uuid === identity.uuid) {
282
+ // Re-login of the account that is already live: the new token replaces
283
+ // the live one (which may be the dead credential that prompted the login).
284
+ const applied = applyClaudeProfileToLiveHome(identity.uuid);
285
+ if (!applied.success)
286
+ return applied;
287
+ if (applied.warning)
288
+ warnings.push(applied.warning);
289
+ }
290
+ const switched = switchToAccount(identity.uuid);
291
+ return warnings.length
292
+ ? { ...switched, warning: [switched.warning, ...warnings].filter(Boolean).join(' ') }
293
+ : switched;
294
+ }
295
+ function emitStatus(opts, status) {
296
+ opts.onStatus?.(status);
297
+ return status;
298
+ }
299
+ function waitForNextPoll(ms, signal) {
300
+ if (ms <= 0)
301
+ return Promise.resolve(true);
302
+ if (signal?.aborted)
303
+ return Promise.resolve(false);
304
+ return new Promise((resolve) => {
305
+ const timer = setTimeout(() => {
306
+ cleanup();
307
+ resolve(true);
308
+ }, ms);
309
+ const onAbort = () => {
310
+ cleanup();
311
+ resolve(false);
312
+ };
313
+ const cleanup = () => {
314
+ clearTimeout(timer);
315
+ signal?.removeEventListener('abort', onAbort);
316
+ };
317
+ signal?.addEventListener('abort', onAbort, { once: true });
318
+ });
319
+ }
320
+ /** Most recent modification inside an isolated login home (the CLI writes as the user progresses). */
321
+ function latestHomeActivity(home) {
322
+ if (!home)
323
+ return 0;
324
+ let latest = 0;
325
+ const candidates = [
326
+ home,
327
+ ...['.claude.json', '.credentials.json', 'auth.json', 'config.toml'].map((f) => path.join(home, f)),
328
+ ];
329
+ for (const candidate of candidates) {
330
+ try {
331
+ latest = Math.max(latest, fs.statSync(candidate).mtimeMs);
332
+ }
333
+ catch {
334
+ /* absent */
335
+ }
336
+ }
337
+ return latest;
338
+ }
339
+ /** Delete a temporary Claude login home: its Keychain item first, then the directory. */
340
+ function discardClaudeLoginHome(loginId) {
341
+ const dir = getClaudeProfileDir(loginId);
342
+ if (!path.resolve(dir).startsWith(path.resolve(getAccountsDir()) + path.sep))
343
+ return;
344
+ deleteStoredCredentials(getClaudeProfileHome(loginId));
345
+ try {
346
+ fs.rmSync(dir, { recursive: true, force: true });
347
+ }
348
+ catch {
349
+ /* the temporary login home is disposable */
350
+ }
351
+ }
352
+ function discardPendingLogin(provider, loginId) {
353
+ try {
354
+ if (provider === 'claude-code') {
355
+ discardClaudeLoginHome(loginId);
356
+ return;
357
+ }
358
+ discardCodexLogin(loginId);
359
+ }
360
+ catch {
361
+ /* best effort */
362
+ }
363
+ }
364
+ /**
365
+ * Abandon a login started with {@link beginAccountLogin} (cancelled, timed
366
+ * out, or failed): removes the temporary home and, for Claude, its Keychain
367
+ * item. Refuses ids that belong to a saved profile. Returns true when
368
+ * something was discarded.
369
+ */
370
+ export function discardAccountLogin(provider, loginId) {
371
+ const registered = provider === 'codex'
372
+ ? listCodexAccounts().some((account) => account.id === loginId)
373
+ : listSavedAccountProfiles('claude-code').some((account) => account.id === loginId || account.providerAccountId === loginId);
374
+ if (registered)
375
+ return false;
376
+ if (provider === 'claude-code' && !readPendingClaudeProfile(loginId)) {
377
+ // Not a pending login; still drop a stray item for a home we created.
378
+ deleteStoredCredentials(getClaudeProfileHome(loginId));
379
+ return false;
380
+ }
381
+ discardPendingLogin(provider, loginId);
382
+ return true;
383
+ }
384
+ class AccountLoginWriterActiveError extends Error {
385
+ }
386
+ export async function spawnAccountLogin(provider, label, opts = {}) {
387
+ let loginId;
388
+ let result;
389
+ let writerActive = false;
390
+ try {
391
+ result = await runAccountLogin(provider, label, opts, (id) => {
392
+ loginId = id;
393
+ });
394
+ return result;
395
+ }
396
+ catch (error) {
397
+ if (error instanceof AccountLoginWriterActiveError) {
398
+ writerActive = true;
399
+ return {
400
+ success: false,
401
+ error: 'The Codex login process could not be stopped. Its temporary home was preserved; close that process before retrying.',
402
+ };
403
+ }
404
+ throw error;
405
+ }
406
+ finally {
407
+ // Every exit that did not end in a saved login (spawn error, early exit,
408
+ // abort, timeout, a throw) removes the temporary home and its Keychain item.
409
+ if (!writerActive && loginId && (provider === 'codex' || !result?.success)) {
410
+ discardPendingLogin(provider, loginId);
411
+ }
412
+ }
413
+ }
414
+ async function runAccountLogin(provider, label, opts, onBegin) {
415
+ if (opts.signal?.aborted) {
416
+ return { success: false, error: 'Account login aborted.' };
417
+ }
418
+ const begin = beginAccountLogin(provider, label, {
419
+ loginCommand: opts.loginCommand,
420
+ existingAccountId: opts.existingAccountId,
421
+ });
422
+ if (!begin.success)
423
+ return { success: false, error: begin.error };
424
+ onBegin(begin.loginId);
425
+ if (begin.alreadyComplete) {
426
+ return finalizeAccountLoginAsync(provider, begin.loginId, { activate: opts.activate ?? true });
427
+ }
428
+ if (!begin.command) {
429
+ return { success: false, error: 'Account login command was not prepared.' };
430
+ }
431
+ let childExited = false;
432
+ let childExitCode = null;
433
+ const childState = {};
434
+ const inactivityMs = opts.timeoutMs ?? 180_000;
435
+ const maxMs = opts.maxTimeoutMs ?? 900_000;
436
+ const startedAt = Date.now();
437
+ let lastActivity = startedAt;
438
+ let lastSeenMtime = latestHomeActivity(begin.configDir);
439
+ const env = provider === 'codex'
440
+ ? { ...process.env, ...(begin.env ?? {}) }
441
+ : buildClaudeChildEnv(begin.configDir ?? '', process.env);
442
+ for (const name of begin.envUnset ?? [])
443
+ delete env[name];
444
+ let child;
445
+ try {
446
+ child = spawn(begin.command, begin.args ?? [], {
447
+ env,
448
+ stdio: opts.stdio ?? 'inherit',
449
+ });
450
+ }
451
+ catch (err) {
452
+ discardPendingLogin(provider, begin.loginId);
453
+ return { success: false, error: `Could not spawn account login: ${err}` };
454
+ }
455
+ child.on('exit', (code) => {
456
+ childExited = true;
457
+ childExitCode = code;
458
+ });
459
+ child.on('error', (error) => {
460
+ childExited = true;
461
+ childState.spawnError = error;
462
+ });
463
+ const stopCodexChild = async () => {
464
+ if (provider !== 'codex' || childExited)
465
+ return;
466
+ const waitForExit = (ms) => new Promise((resolve) => {
467
+ if (childExited) {
468
+ resolve();
469
+ return;
470
+ }
471
+ const done = () => {
472
+ clearTimeout(timer);
473
+ child.off('exit', done);
474
+ child.off('error', done);
475
+ resolve();
476
+ };
477
+ const timer = setTimeout(done, ms);
478
+ child.once('exit', done);
479
+ child.once('error', done);
480
+ });
481
+ await waitForExit(100);
482
+ try {
483
+ if (!childExited) {
484
+ child.kill();
485
+ await waitForExit(1000);
486
+ }
487
+ if (!childExited) {
488
+ child.kill('SIGKILL');
489
+ await waitForExit(1000);
490
+ }
491
+ }
492
+ catch {
493
+ throw new AccountLoginWriterActiveError();
494
+ }
495
+ if (!childExited)
496
+ throw new AccountLoginWriterActiveError();
497
+ };
498
+ const abort = async () => {
499
+ if (provider === 'codex')
500
+ await stopCodexChild();
501
+ else
502
+ child.kill();
503
+ discardPendingLogin(provider, begin.loginId);
504
+ emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
505
+ return { success: false, error: 'Account login aborted.' };
506
+ };
507
+ try {
508
+ while (true) {
509
+ if (opts.signal?.aborted)
510
+ return await abort();
511
+ const status = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
512
+ if (status.state === 'authenticated') {
513
+ await stopCodexChild();
514
+ return await finalizeAccountLoginAsync(provider, begin.loginId, {
515
+ activate: opts.activate ?? true,
516
+ });
517
+ }
518
+ if (childExited) {
519
+ if (childState.spawnError) {
520
+ const error = `Could not spawn account login: ${childState.spawnError.message}`;
521
+ discardPendingLogin(provider, begin.loginId);
522
+ emitStatus(opts, { state: 'failed', error });
523
+ return { success: false, error };
524
+ }
525
+ const finalStatus = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
526
+ if (finalStatus.state === 'authenticated') {
527
+ await stopCodexChild();
528
+ return await finalizeAccountLoginAsync(provider, begin.loginId, {
529
+ activate: opts.activate ?? true,
530
+ });
531
+ }
532
+ discardPendingLogin(provider, begin.loginId);
533
+ emitStatus(opts, {
534
+ state: 'failed',
535
+ error: `Account login exited before authentication completed${childExitCode === null ? '.' : ` (exit ${childExitCode}).`}`,
536
+ });
537
+ return { success: false, error: 'Account login did not complete.' };
538
+ }
539
+ const now = Date.now();
540
+ const mtime = latestHomeActivity(begin.configDir);
541
+ if (mtime > lastSeenMtime) {
542
+ lastSeenMtime = mtime;
543
+ lastActivity = now;
544
+ }
545
+ const remainingMs = Math.min(inactivityMs - (now - lastActivity), maxMs - (now - startedAt));
546
+ if (remainingMs <= 0) {
547
+ if (provider === 'codex')
548
+ await stopCodexChild();
549
+ else
550
+ child.kill();
551
+ discardPendingLogin(provider, begin.loginId);
552
+ emitStatus(opts, { state: 'failed', error: 'Account login timed out.' });
553
+ return { success: false, error: 'Account login timed out.' };
554
+ }
555
+ const shouldContinue = await waitForNextPoll(Math.min(2_000, remainingMs), opts.signal);
556
+ if (!shouldContinue)
557
+ return await abort();
558
+ }
559
+ }
560
+ finally {
561
+ await stopCodexChild();
562
+ }
563
+ }
564
+ export function switchAccount(provider, id, options = {}) {
565
+ return provider === 'codex' ? switchToCodexAccount(id, options) : switchToAccount(id, options);
566
+ }
567
+ /**
568
+ * {@link switchAccount} without blocking the event loop on CLI/process probes.
569
+ * The Claude path still performs bounded synchronous keychain reads on macOS.
570
+ */
571
+ export async function switchAccountAsync(provider, id, options = {}) {
572
+ return provider === 'codex'
573
+ ? switchToCodexAccountAsync(id, options)
574
+ : switchToAccountAsync(id, options);
575
+ }
576
+ export function getLastSwitch(provider) {
577
+ return readLastSwitch(provider);
578
+ }
579
+ /**
580
+ * Switch back to the account that was live before the last recorded switch.
581
+ * With `token`, the record must match (a stale Undo button never reverts a
582
+ * newer switch).
583
+ */
584
+ export async function undoLastSwitch(provider, token) {
585
+ const record = readLastSwitch(provider);
586
+ if (!record) {
587
+ return switchFailure(provider, '', 'There is no account switch to undo.');
588
+ }
589
+ if (token && token !== record.token) {
590
+ return switchFailure(provider, record.from ?? '', 'A newer account switch has happened since; nothing was undone.');
591
+ }
592
+ if (!record.from) {
593
+ return switchFailure(provider, '', 'The previous state had no saved account to return to.');
594
+ }
595
+ return switchAccountAsync(provider, record.from);
596
+ }
597
+ export function listAllAccounts() {
598
+ return {
599
+ claude: listAccounts(),
600
+ codex: listCodexAccounts(),
601
+ activeByProvider: readSavedAccountRegistry()?.activeByProvider ?? {
602
+ 'claude-code': null,
603
+ codex: null,
604
+ },
605
+ };
606
+ }