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,214 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { getConfigDir } from './paths';
4
+ import { atomicWriteJsonSync, withFileLockSync } from './writers/atomic';
5
+ import { _emitAccountsChanged } from './accountChangeSignal';
6
+ export { ACCOUNT_PROVIDER_IDS } from './providerIds';
7
+ export function getAccountsDir() {
8
+ return path.join(getConfigDir(), 'accounts');
9
+ }
10
+ function getRegistryPath() {
11
+ return path.join(getAccountsDir(), 'accounts.json');
12
+ }
13
+ function getRegistryLockPath() {
14
+ return getRegistryPath() + '.lock';
15
+ }
16
+ function ensureAccountsDir() {
17
+ fs.mkdirSync(getAccountsDir(), { recursive: true, mode: 0o700 });
18
+ }
19
+ function writeRegistryUnlocked(registry, options = {}) {
20
+ atomicWriteJsonSync(getRegistryPath(), normalizeRegistry(registry));
21
+ if (!options.silent)
22
+ _emitAccountsChanged('local');
23
+ }
24
+ /**
25
+ * Run `operation` while holding the registry lock. For callers that need a
26
+ * read-modify-write across several steps (the legacy Claude facade's
27
+ * add/remove); inside it use the `*Unlocked` writers, never the locking ones:
28
+ * the lock is not re-entrant.
29
+ */
30
+ export function withSavedAccountRegistryLock(operation) {
31
+ ensureAccountsDir();
32
+ return withFileLockSync(getRegistryLockPath(), operation);
33
+ }
34
+ /**
35
+ * Serializes registry read-modify-write cycles across processes. Without the
36
+ * lock, two concurrent mutators (CLI, editor extension, external consumers)
37
+ * read the same base state and the second write silently drops the first
38
+ * mutation. Lock ordering: callers that also hold a provider auth-swap lock
39
+ * take that lock first and this one inside it, never the reverse.
40
+ */
41
+ function mutateSavedAccountRegistry(mutator, options = {}) {
42
+ return withSavedAccountRegistryLock(() => mutateSavedAccountRegistryUnlocked(mutator, options));
43
+ }
44
+ function mutateSavedAccountRegistryUnlocked(mutator, options = {}) {
45
+ const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
46
+ writeRegistryUnlocked(next, options);
47
+ return next;
48
+ }
49
+ function createEmptyRegistry() {
50
+ return {
51
+ version: 2,
52
+ activeByProvider: {
53
+ 'claude-code': null,
54
+ codex: null,
55
+ },
56
+ accounts: [],
57
+ };
58
+ }
59
+ function normalizeRegistry(registry) {
60
+ const normalized = createEmptyRegistry();
61
+ normalized.activeByProvider['claude-code'] = registry.activeByProvider?.['claude-code'] ?? null;
62
+ normalized.activeByProvider.codex = registry.activeByProvider?.codex ?? null;
63
+ normalized.accounts = Array.isArray(registry.accounts)
64
+ ? registry.accounts
65
+ .filter((account) => !!account &&
66
+ (account.providerId === 'claude-code' || account.providerId === 'codex') &&
67
+ typeof account.id === 'string' &&
68
+ typeof account.addedAt === 'string')
69
+ .map((account) => ({
70
+ ...account,
71
+ label: account.label || undefined,
72
+ email: account.email || undefined,
73
+ providerAccountId: account.providerAccountId || undefined,
74
+ metadata: account.metadata
75
+ ? {
76
+ email: account.metadata.email || undefined,
77
+ workspaceId: account.metadata.workspaceId || undefined,
78
+ planType: account.metadata.planType || undefined,
79
+ authMode: account.metadata.authMode || undefined,
80
+ origin: account.metadata.origin || undefined,
81
+ }
82
+ : undefined,
83
+ }))
84
+ : [];
85
+ return normalized;
86
+ }
87
+ function migrateLegacyRegistry(legacy) {
88
+ return {
89
+ version: 2,
90
+ activeByProvider: {
91
+ 'claude-code': legacy.activeAccountUuid,
92
+ codex: null,
93
+ },
94
+ accounts: legacy.accounts.map((account) => ({
95
+ id: account.uuid,
96
+ providerId: 'claude-code',
97
+ providerAccountId: account.uuid,
98
+ email: account.email,
99
+ label: account.label,
100
+ addedAt: account.addedAt,
101
+ metadata: {
102
+ email: account.email,
103
+ },
104
+ })),
105
+ };
106
+ }
107
+ export function readSavedAccountRegistry() {
108
+ try {
109
+ const content = fs.readFileSync(getRegistryPath(), 'utf8');
110
+ const parsed = JSON.parse(content);
111
+ if (parsed?.version === 2) {
112
+ return normalizeRegistry(parsed);
113
+ }
114
+ if (parsed?.version === 1 && Array.isArray(parsed.accounts)) {
115
+ return migrateLegacyRegistry(parsed);
116
+ }
117
+ return null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ export function writeSavedAccountRegistry(registry) {
124
+ ensureAccountsDir();
125
+ withFileLockSync(getRegistryLockPath(), () => writeRegistryUnlocked(registry));
126
+ }
127
+ export function listSavedAccountProfiles(providerId) {
128
+ const registry = readSavedAccountRegistry();
129
+ if (!registry)
130
+ return [];
131
+ return providerId
132
+ ? registry.accounts.filter((account) => account.providerId === providerId)
133
+ : registry.accounts;
134
+ }
135
+ export function getActiveSavedAccount(providerId) {
136
+ const registry = readSavedAccountRegistry();
137
+ if (!registry)
138
+ return null;
139
+ const activeId = registry.activeByProvider[providerId];
140
+ if (!activeId)
141
+ return null;
142
+ return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
143
+ }
144
+ export function upsertSavedAccountProfile(profile) {
145
+ return mutateSavedAccountRegistry(upsertProfile(profile));
146
+ }
147
+ export function setActiveSavedAccount(providerId, accountId, options = {}) {
148
+ return mutateSavedAccountRegistry(setActive(providerId, accountId), options);
149
+ }
150
+ function setActive(providerId, accountId) {
151
+ return (registry) => {
152
+ registry.activeByProvider[providerId] = accountId;
153
+ return registry;
154
+ };
155
+ }
156
+ function upsertProfile(profile) {
157
+ return (registry) => {
158
+ const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
159
+ if (index >= 0) {
160
+ registry.accounts[index] = profile;
161
+ }
162
+ else {
163
+ registry.accounts.push(profile);
164
+ }
165
+ return registry;
166
+ };
167
+ }
168
+ /** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
169
+ export function upsertSavedAccountProfileUnlocked(profile) {
170
+ return mutateSavedAccountRegistryUnlocked(upsertProfile(profile));
171
+ }
172
+ /** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
173
+ export function setActiveSavedAccountUnlocked(providerId, accountId, options = {}) {
174
+ return mutateSavedAccountRegistryUnlocked(setActive(providerId, accountId), options);
175
+ }
176
+ function replaceProfiles(providerId, accounts, activeId) {
177
+ return (registry) => {
178
+ registry.accounts = registry.accounts
179
+ .filter((account) => account.providerId !== providerId)
180
+ .concat(accounts);
181
+ registry.activeByProvider[providerId] = activeId;
182
+ return registry;
183
+ };
184
+ }
185
+ export function replaceSavedAccountProfiles(providerId, accounts, activeId) {
186
+ return mutateSavedAccountRegistry(replaceProfiles(providerId, accounts, activeId));
187
+ }
188
+ /**
189
+ * Same as {@link replaceSavedAccountProfiles} without taking the registry
190
+ * lock: for callers already inside {@link withSavedAccountRegistryLock}.
191
+ */
192
+ export function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
193
+ return mutateSavedAccountRegistryUnlocked(replaceProfiles(providerId, accounts, activeId));
194
+ }
195
+ export function removeSavedAccountProfile(providerId, accountId) {
196
+ ensureAccountsDir();
197
+ return withFileLockSync(getRegistryLockPath(), () => removeSavedAccountProfileUnlocked(providerId, accountId));
198
+ }
199
+ /** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
200
+ export function removeSavedAccountProfileUnlocked(providerId, accountId) {
201
+ const registry = readSavedAccountRegistry();
202
+ if (!registry)
203
+ return null;
204
+ const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
205
+ if (index === -1)
206
+ return null;
207
+ const [removed] = registry.accounts.splice(index, 1);
208
+ if (registry.activeByProvider[providerId] === accountId) {
209
+ registry.activeByProvider[providerId] =
210
+ registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
211
+ }
212
+ writeRegistryUnlocked(registry);
213
+ return removed;
214
+ }
@@ -0,0 +1,51 @@
1
+ import { resolveActiveClaudeAccount } from './accounts';
2
+ import { resolveActiveCodexAccount } from './codexProfiles';
3
+ function errorMessage(error) {
4
+ return error instanceof Error ? error.message : String(error);
5
+ }
6
+ /**
7
+ * Reads active Claude Code and Codex account status in one filesystem pass.
8
+ *
9
+ * If reading either provider throws, the result still has provider-shaped
10
+ * fields so startup flows can render a consistent "not configured" state.
11
+ */
12
+ export function getActiveAccountStatus(error, options = {}) {
13
+ try {
14
+ const claudeAccount = resolveActiveClaudeAccount(options);
15
+ const codexAccount = resolveActiveCodexAccount(options);
16
+ const claude = claudeAccount.source !== 'none'
17
+ ? {
18
+ present: true,
19
+ ...(claudeAccount.registryAccountId
20
+ ? { accountId: claudeAccount.registryAccountId }
21
+ : {}),
22
+ email: claudeAccount.email,
23
+ label: claudeAccount.label ?? claudeAccount.email,
24
+ }
25
+ : { present: false };
26
+ const codex = codexAccount.source !== 'none'
27
+ ? {
28
+ present: true,
29
+ ...(codexAccount.registryAccountId
30
+ ? { accountId: codexAccount.registryAccountId }
31
+ : {}),
32
+ email: codexAccount.email,
33
+ label: codexAccount.label ?? codexAccount.email ?? codexAccount.providerAccountId,
34
+ }
35
+ : { present: false };
36
+ return {
37
+ ok: claude.present || codex.present,
38
+ claude,
39
+ codex,
40
+ error,
41
+ };
42
+ }
43
+ catch (caught) {
44
+ return {
45
+ ok: false,
46
+ claude: { present: false },
47
+ codex: { present: false },
48
+ error: error ?? errorMessage(caught),
49
+ };
50
+ }
51
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Shared shape of a verified account switch plus the undo record. The
3
+ * provider-specific switch cores live in accounts.ts (Claude) and
4
+ * codexProfiles.ts (Codex); accountManager.ts exposes the provider-agnostic
5
+ * `switchAccount*` and `undoLastSwitch` entry points.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { randomUUID } from 'crypto';
10
+ import { getAccountsDir } from './accountRegistry';
11
+ import { atomicWriteJsonSync } from './writers/atomic';
12
+ function providerDirName(provider) {
13
+ return provider === 'codex' ? 'codex' : 'claude';
14
+ }
15
+ export function getLastSwitchPath(provider) {
16
+ return path.join(getAccountsDir(), providerDirName(provider), 'last-switch.json');
17
+ }
18
+ export function readLastSwitch(provider) {
19
+ try {
20
+ const parsed = JSON.parse(fs.readFileSync(getLastSwitchPath(provider), 'utf8'));
21
+ return parsed && typeof parsed.token === 'string' && typeof parsed.to === 'string'
22
+ ? parsed
23
+ : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function writeLastSwitch(provider, from, to) {
30
+ const record = {
31
+ token: randomUUID(),
32
+ provider,
33
+ from,
34
+ to,
35
+ at: new Date().toISOString(),
36
+ };
37
+ atomicWriteJsonSync(getLastSwitchPath(provider), record);
38
+ return record;
39
+ }
40
+ export function clearLastSwitch(provider) {
41
+ try {
42
+ fs.rmSync(getLastSwitchPath(provider), { force: true });
43
+ }
44
+ catch {
45
+ /* best effort */
46
+ }
47
+ }
48
+ export function switchFailure(provider, accountId, error, extra = {}) {
49
+ return {
50
+ success: false,
51
+ error,
52
+ provider,
53
+ accountId,
54
+ previousAccountId: null,
55
+ verified: false,
56
+ verification: 'none',
57
+ warnings: [],
58
+ hints: [],
59
+ runningConsumers: [],
60
+ ...extra,
61
+ };
62
+ }
63
+ /** Fold per-consumer warnings into a result; keeps `warning` (legacy) in sync. */
64
+ export function finishSwitchResult(result) {
65
+ const warnings = [
66
+ ...result.warnings,
67
+ ...result.runningConsumers.map((consumer) => consumer.reachability),
68
+ ];
69
+ return {
70
+ ...result,
71
+ warnings,
72
+ warning: warnings.length ? warnings.join(' ') : undefined,
73
+ };
74
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Continuous reconciliation between the live provider logins and sidekick's
3
+ * saved profiles. One engine replaces the old bootstrap/reconcile/backup
4
+ * paths: it registers logins sidekick has never seen, folds rotated tokens
5
+ * back into their profile, merges duplicate Codex seats, and re-points the
6
+ * active pointer at whatever is really logged in.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { cleanupAbandonedClaudeLogins, syncClaudeLiveStateUnlocked, withClaudeAuthSwapLock, } from './accounts';
11
+ import { getLiveClaudeHome, readLiveClaudeIdentity } from './claudeProfiles';
12
+ import { cleanupAbandonedCodexLogins, cleanupAbandonedCodexLoginsAsync, hasPendingCodexRepair, syncCodexLiveStateUnlocked, withCodexAuthSwapLock, } from './codexProfiles';
13
+ import { getSystemCodexHome } from './codexPaths';
14
+ import { inspectRunningAccountConsumers, inspectRunningAccountConsumersSync, } from './processDetection';
15
+ import { emptyProviderSyncReport, } from './accountSyncTypes';
16
+ const DEFAULT_KEYCHAIN_FOLD_INTERVAL_MS = 10 * 60 * 1000;
17
+ const fingerprints = {
18
+ 'claude-code': {},
19
+ codex: {},
20
+ };
21
+ let syncInProgress = false;
22
+ function statStamp(filePath) {
23
+ try {
24
+ const stat = fs.statSync(filePath);
25
+ return `${stat.mtimeMs}:${stat.size}`;
26
+ }
27
+ catch {
28
+ return 'absent';
29
+ }
30
+ }
31
+ function claudeFingerprint() {
32
+ const home = getLiveClaudeHome();
33
+ const identity = readLiveClaudeIdentity();
34
+ // Fingerprint the identity, not the config file: Claude Code rewrites
35
+ // `~/.claude.json` on nearly every prompt, which would defeat the
36
+ // `onlyIfChanged` short-circuit and the macOS keychain fold interval.
37
+ return [
38
+ identity?.uuid ?? 'none',
39
+ identity?.email ?? '',
40
+ statStamp(path.join(home, '.credentials.json')),
41
+ ].join('|');
42
+ }
43
+ function codexFingerprint() {
44
+ const home = getSystemCodexHome();
45
+ return [
46
+ statStamp(path.join(home, 'auth.json')),
47
+ statStamp(path.join(home, '.credentials.json')),
48
+ ].join('|');
49
+ }
50
+ function sourceForReason(reason) {
51
+ return reason === 'pre-switch' ? 'switch' : 'live-sync';
52
+ }
53
+ function shouldSync(provider, fingerprint, options) {
54
+ if (!options.onlyIfChanged || (provider === 'codex' && hasPendingCodexRepair()))
55
+ return true;
56
+ const state = fingerprints[provider];
57
+ if (state.fingerprint !== fingerprint)
58
+ return true;
59
+ if (provider === 'claude-code' && process.platform === 'darwin') {
60
+ // The Keychain item can change without touching any watched file.
61
+ return (state.lastFoldAt ?? 0) + options.keychainFoldIntervalMs <= options.now;
62
+ }
63
+ return false;
64
+ }
65
+ /** Sync one provider under its swap lock; never throws. */
66
+ function syncProvider(provider, reason, options, scan) {
67
+ const fingerprint = provider === 'codex' ? codexFingerprint() : claudeFingerprint();
68
+ if (!shouldSync(provider, fingerprint, options)) {
69
+ return { ...emptyProviderSyncReport(), skipped: 'unchanged' };
70
+ }
71
+ const source = sourceForReason(reason);
72
+ let report;
73
+ try {
74
+ if (provider === 'codex')
75
+ scan ??= inspectRunningAccountConsumersSync('codex');
76
+ report =
77
+ provider === 'codex'
78
+ ? withCodexAuthSwapLock(() => syncCodexLiveStateUnlocked(source, { scan }))
79
+ : withClaudeAuthSwapLock(() => syncClaudeLiveStateUnlocked(source));
80
+ }
81
+ catch (err) {
82
+ return { warnings: [`Could not acquire the ${provider} account-switch lock: ${err}`] };
83
+ }
84
+ fingerprints[provider] = {
85
+ // Re-fingerprint after the sync: folding rewrites nothing in the live home,
86
+ // but registering touches accounts.json, which is also watched.
87
+ fingerprint: provider === 'codex' ? codexFingerprint() : claudeFingerprint(),
88
+ lastFoldAt: options.now,
89
+ };
90
+ return report;
91
+ }
92
+ /**
93
+ * Reconcile the live logins with the saved profiles for both providers.
94
+ * Safe to call from watchers: re-entrant calls return an empty report, and
95
+ * `onlyIfChanged` short-circuits when the live files are unchanged.
96
+ */
97
+ function syncLiveAccountStateCore(options = {}, scan) {
98
+ const reason = options.reason ?? 'manual';
99
+ const now = options.now ?? Date.now();
100
+ const resolved = {
101
+ onlyIfChanged: options.onlyIfChanged ?? (reason === 'watch' || reason === 'poll'),
102
+ keychainFoldIntervalMs: options.keychainFoldIntervalMs ?? DEFAULT_KEYCHAIN_FOLD_INTERVAL_MS,
103
+ now,
104
+ };
105
+ const providers = options.providers ?? ['claude-code', 'codex'];
106
+ const report = {
107
+ claude: { ...emptyProviderSyncReport(), skipped: 'not-requested' },
108
+ codex: { ...emptyProviderSyncReport(), skipped: 'not-requested' },
109
+ ranAt: now,
110
+ reason,
111
+ };
112
+ if (syncInProgress) {
113
+ report.claude.skipped = 'in-progress';
114
+ report.codex.skipped = 'in-progress';
115
+ return report;
116
+ }
117
+ syncInProgress = true;
118
+ try {
119
+ if (providers.includes('claude-code'))
120
+ report.claude = syncProvider('claude-code', reason, resolved);
121
+ if (providers.includes('codex'))
122
+ report.codex = syncProvider('codex', reason, resolved, scan);
123
+ }
124
+ finally {
125
+ syncInProgress = false;
126
+ }
127
+ return report;
128
+ }
129
+ export function syncLiveAccountStateSync(options = {}) {
130
+ return syncLiveAccountStateCore(options);
131
+ }
132
+ /** Promise-returning twin of {@link syncLiveAccountStateSync} for async hosts. */
133
+ export async function syncLiveAccountState(options = {}) {
134
+ const scan = !options.providers || options.providers.includes('codex')
135
+ ? await inspectRunningAccountConsumers('codex')
136
+ : undefined;
137
+ return syncLiveAccountStateCore(options, scan);
138
+ }
139
+ /** Forget the change fingerprints (tests, or after an external reset). */
140
+ export function _resetSyncFingerprints() {
141
+ fingerprints['claude-code'] = {};
142
+ fingerprints.codex = {};
143
+ }
144
+ /** Remove isolated login homes that never authenticated (both providers). */
145
+ export function cleanupAbandonedLogins(olderThanMs = 6 * 60 * 60 * 1000, now = Date.now()) {
146
+ return {
147
+ claude: cleanupAbandonedClaudeLogins(olderThanMs, now),
148
+ codex: cleanupAbandonedCodexLogins(olderThanMs, now),
149
+ };
150
+ }
151
+ export async function cleanupAbandonedLoginsAsync(olderThanMs = 24 * 60 * 60 * 1000, now = Date.now()) {
152
+ return {
153
+ claude: cleanupAbandonedClaudeLogins(olderThanMs, now),
154
+ codex: await cleanupAbandonedCodexLoginsAsync(olderThanMs, now),
155
+ };
156
+ }
@@ -0,0 +1,4 @@
1
+ /** Reports produced by the live-state sync engine (see accountSync.ts). */
2
+ export function emptyProviderSyncReport() {
3
+ return { warnings: [] };
4
+ }