sidekick-shared 0.27.5 → 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 (314) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +93 -52
  3. package/dist/accountHealth.d.ts +14 -0
  4. package/dist/accountHealth.js +61 -14
  5. package/dist/accountLaunch.d.ts +6 -0
  6. package/dist/accountLaunch.js +61 -4
  7. package/dist/accountLease.d.ts +5 -0
  8. package/dist/accountLease.js +98 -0
  9. package/dist/accountManager.d.ts +7 -0
  10. package/dist/accountManager.js +226 -89
  11. package/dist/accountSync.d.ts +1 -5
  12. package/dist/accountSync.js +22 -6
  13. package/dist/accountSyncTypes.d.ts +9 -0
  14. package/dist/accounts.d.ts +75 -18
  15. package/dist/accounts.js +712 -189
  16. package/dist/browser.d.ts +13 -2
  17. package/dist/browser.js +30 -2
  18. package/dist/claudeCredentialDoctor.d.ts +50 -0
  19. package/dist/claudeCredentialDoctor.js +277 -0
  20. package/dist/claudeCredentialLedger.d.ts +15 -0
  21. package/dist/claudeCredentialLedger.js +130 -0
  22. package/dist/claudeCredentials.d.ts +20 -3
  23. package/dist/claudeCredentials.js +50 -3
  24. package/dist/claudeLiveGuard.d.ts +33 -0
  25. package/dist/claudeLiveGuard.js +46 -0
  26. package/dist/claudePaths.d.ts +11 -0
  27. package/dist/claudePaths.js +61 -0
  28. package/dist/claudeProfiles.d.ts +15 -8
  29. package/dist/claudeProfiles.js +30 -23
  30. package/dist/claudeRefreshLock.d.ts +15 -0
  31. package/dist/claudeRefreshLock.js +145 -0
  32. package/dist/codexAuth.d.ts +4 -0
  33. package/dist/codexAuth.js +18 -0
  34. package/dist/codexCredentialDoctor.d.ts +33 -0
  35. package/dist/codexCredentialDoctor.js +302 -0
  36. package/dist/codexCredentialStore.d.ts +29 -0
  37. package/dist/codexCredentialStore.js +213 -0
  38. package/dist/codexKeepAlive.d.ts +2 -0
  39. package/dist/codexKeepAlive.js +116 -0
  40. package/dist/codexPaths.d.ts +30 -2
  41. package/dist/codexPaths.js +118 -7
  42. package/dist/codexProfiles.d.ts +38 -35
  43. package/dist/codexProfiles.js +848 -715
  44. package/dist/credentialIO.d.ts +67 -7
  45. package/dist/credentialIO.js +211 -47
  46. package/dist/doctor.js +2 -2
  47. package/dist/ensureDefaultAccounts.js +1 -8
  48. package/dist/esm/accountChangeSignal.js +15 -0
  49. package/dist/esm/accountChanges.js +152 -0
  50. package/dist/esm/accountHealth.js +288 -0
  51. package/dist/esm/accountLaunch.js +145 -0
  52. package/dist/esm/accountLease.js +60 -0
  53. package/dist/esm/accountManager.js +606 -0
  54. package/dist/esm/accountRegistry.js +214 -0
  55. package/dist/esm/accountStatus.js +51 -0
  56. package/dist/esm/accountSwitch.js +74 -0
  57. package/dist/esm/accountSync.js +156 -0
  58. package/dist/esm/accountSyncTypes.js +4 -0
  59. package/dist/esm/accounts.js +1291 -0
  60. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  61. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  62. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  63. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  64. package/dist/esm/aggregation/costProvenance.js +31 -0
  65. package/dist/esm/aggregation/snapshot.js +161 -0
  66. package/dist/esm/aggregation/types.js +5 -0
  67. package/dist/esm/analytics/codeImpact.js +18 -0
  68. package/dist/esm/analytics/compactionLedger.js +37 -0
  69. package/dist/esm/analytics/qualityScore.js +112 -0
  70. package/dist/esm/autoSwitch.js +185 -0
  71. package/dist/esm/browser.js +34 -0
  72. package/dist/esm/claudeCredentialDoctor.js +239 -0
  73. package/dist/esm/claudeCredentialLedger.js +91 -0
  74. package/dist/esm/claudeCredentials.js +111 -0
  75. package/dist/esm/claudeLiveGuard.js +43 -0
  76. package/dist/esm/claudePaths.js +22 -0
  77. package/dist/esm/claudeProfiles.js +142 -0
  78. package/dist/esm/claudeRefreshLock.js +106 -0
  79. package/dist/esm/codexAuth.js +101 -0
  80. package/dist/esm/codexCredentialDoctor.js +265 -0
  81. package/dist/esm/codexCredentialStore.js +164 -0
  82. package/dist/esm/codexKeepAlive.js +112 -0
  83. package/dist/esm/codexPaths.js +172 -0
  84. package/dist/esm/codexProfiles.js +1251 -0
  85. package/dist/esm/codexQuota.js +564 -0
  86. package/dist/esm/codexQuotaWatcher.js +363 -0
  87. package/dist/esm/context/composer.js +71 -0
  88. package/dist/esm/context/sessionContext.js +546 -0
  89. package/dist/esm/credentialIO.js +283 -0
  90. package/dist/esm/credentials.js +49 -0
  91. package/dist/esm/doctor.js +305 -0
  92. package/dist/esm/ensureDefaultAccounts.js +62 -0
  93. package/dist/esm/errorHistory.js +56 -0
  94. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  95. package/dist/esm/extractors/gatherAssets.js +32 -0
  96. package/dist/esm/extractors/sessionAssets.js +176 -0
  97. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  98. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  99. package/dist/esm/extractors/tokenUsage.js +41 -0
  100. package/dist/esm/extractors/toolCall.js +142 -0
  101. package/dist/esm/failingTools.js +65 -0
  102. package/dist/esm/fileHead.js +27 -0
  103. package/dist/esm/formatters/eventHighlighter.js +332 -0
  104. package/dist/esm/formatters/noiseClassifier.js +209 -0
  105. package/dist/esm/formatters/sessionDump.js +328 -0
  106. package/dist/esm/formatters/toolSummary.js +226 -0
  107. package/dist/esm/formatting.js +82 -0
  108. package/dist/esm/handoffUrl.js +23 -0
  109. package/dist/esm/historicalStore.js +305 -0
  110. package/dist/esm/humanPrompt.js +88 -0
  111. package/dist/esm/index.js +213 -0
  112. package/dist/esm/keepAlive.js +189 -0
  113. package/dist/esm/liveCredentialGuard.js +33 -0
  114. package/dist/esm/modelAliases.js +51 -0
  115. package/dist/esm/modelCatalog.js +139 -0
  116. package/dist/esm/modelContext.js +280 -0
  117. package/dist/esm/modelInfo.js +832 -0
  118. package/dist/esm/multiProviderQuotaService.js +450 -0
  119. package/dist/esm/node.js +46 -0
  120. package/dist/esm/observedContextWindows.js +165 -0
  121. package/dist/esm/observedSessionCollector.js +939 -0
  122. package/dist/esm/parsers/changelogParser.js +71 -0
  123. package/dist/esm/parsers/codexHistory.js +89 -0
  124. package/dist/esm/parsers/codexParser.js +906 -0
  125. package/dist/esm/parsers/debugLogParser.js +126 -0
  126. package/dist/esm/parsers/jsonl.js +66 -0
  127. package/dist/esm/parsers/mcpToolName.js +28 -0
  128. package/dist/esm/parsers/openCodeParser.js +625 -0
  129. package/dist/esm/parsers/planExtractor.js +335 -0
  130. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  131. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  132. package/dist/esm/parsers/subagentScanner.js +360 -0
  133. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  134. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  135. package/dist/esm/paths.js +134 -0
  136. package/dist/esm/peakHours.js +194 -0
  137. package/dist/esm/phrases.js +1330 -0
  138. package/dist/esm/pricingCatalog.js +247 -0
  139. package/dist/esm/processDetection.js +200 -0
  140. package/dist/esm/projectMigration.js +59 -0
  141. package/dist/esm/promptHistory.js +1221 -0
  142. package/dist/esm/providerFailure.js +362 -0
  143. package/dist/esm/providerIds.js +6 -0
  144. package/dist/esm/providerQuota.js +1 -0
  145. package/dist/esm/providerServiceStatus.js +145 -0
  146. package/dist/esm/providerServiceStatusTypes.js +1 -0
  147. package/dist/esm/providerStatus.js +117 -0
  148. package/dist/esm/providers/claudeCode.js +575 -0
  149. package/dist/esm/providers/claudeListing.js +121 -0
  150. package/dist/esm/providers/codex.js +1034 -0
  151. package/dist/esm/providers/codexDatabase.js +297 -0
  152. package/dist/esm/providers/codexRollouts.js +215 -0
  153. package/dist/esm/providers/detect.js +185 -0
  154. package/dist/esm/providers/diagnostics.js +83 -0
  155. package/dist/esm/providers/directoryListingCache.js +206 -0
  156. package/dist/esm/providers/factory.js +54 -0
  157. package/dist/esm/providers/openCode.js +2008 -0
  158. package/dist/esm/providers/openCodeDatabase.js +419 -0
  159. package/dist/esm/providers/rolloutWalker.js +252 -0
  160. package/dist/esm/providers/types.js +12 -0
  161. package/dist/esm/quota.js +182 -0
  162. package/dist/esm/quotaHistory.js +364 -0
  163. package/dist/esm/quotaPoller.js +230 -0
  164. package/dist/esm/quotaPresentation.js +97 -0
  165. package/dist/esm/quotaResolver.js +231 -0
  166. package/dist/esm/quotaSnapshots.js +130 -0
  167. package/dist/esm/quotaThresholds.js +85 -0
  168. package/dist/esm/readers/decisions.js +24 -0
  169. package/dist/esm/readers/handoff.js +20 -0
  170. package/dist/esm/readers/helpers.js +27 -0
  171. package/dist/esm/readers/history.js +9 -0
  172. package/dist/esm/readers/notes.js +48 -0
  173. package/dist/esm/readers/plans.js +225 -0
  174. package/dist/esm/readers/tasks.js +19 -0
  175. package/dist/esm/report/htmlHelpers.js +280 -0
  176. package/dist/esm/report/htmlReportGenerator.js +704 -0
  177. package/dist/esm/report/index.js +7 -0
  178. package/dist/esm/report/logo.js +2 -0
  179. package/dist/esm/report/openBrowser.js +19 -0
  180. package/dist/esm/report/sessionReportInputs.js +33 -0
  181. package/dist/esm/report/transcriptParser.js +83 -0
  182. package/dist/esm/report/types.js +4 -0
  183. package/dist/esm/schemas/accountManager.js +204 -0
  184. package/dist/esm/schemas/accountStatus.js +25 -0
  185. package/dist/esm/schemas/assistantTurn.js +74 -0
  186. package/dist/esm/schemas/index.js +25 -0
  187. package/dist/esm/schemas/observedSessionV1.js +77 -0
  188. package/dist/esm/schemas/providerFailure.js +37 -0
  189. package/dist/esm/schemas/quota.js +132 -0
  190. package/dist/esm/schemas/quotaHistory.js +40 -0
  191. package/dist/esm/schemas/sessionEvent.js +216 -0
  192. package/dist/esm/schemas/stateFile.js +75 -0
  193. package/dist/esm/schemas/tokenEstimation.js +7 -0
  194. package/dist/esm/schemas/transcript.js +86 -0
  195. package/dist/esm/schemas/usageNormalization.js +38 -0
  196. package/dist/esm/search/advancedFilter.js +197 -0
  197. package/dist/esm/search/sessionSearch.js +66 -0
  198. package/dist/esm/sessionFingerprint.js +40 -0
  199. package/dist/esm/sessionMonitor.js +198 -0
  200. package/dist/esm/sessionPreviews.js +522 -0
  201. package/dist/esm/sessionPromptHistory.js +254 -0
  202. package/dist/esm/sessionPromptSignals.js +364 -0
  203. package/dist/esm/sessionStats.js +160 -0
  204. package/dist/esm/sessionTokenTotals.js +98 -0
  205. package/dist/esm/sessionTranscripts.js +49 -0
  206. package/dist/esm/stateFile.js +119 -0
  207. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  208. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  209. package/dist/esm/statusline/formatter.js +122 -0
  210. package/dist/esm/statusline/index.js +8 -0
  211. package/dist/esm/terminalSync.js +95 -0
  212. package/dist/esm/testing/credentialGuard.js +197 -0
  213. package/dist/esm/testing/index.js +12 -0
  214. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  215. package/dist/esm/tokenEstimation.js +67 -0
  216. package/dist/esm/tokenSummary.js +113 -0
  217. package/dist/esm/toolCategory.js +82 -0
  218. package/dist/esm/transcript.js +257 -0
  219. package/dist/esm/turns/assistantTurn.js +373 -0
  220. package/dist/esm/types/codex.js +23 -0
  221. package/dist/esm/types/decisionLog.js +5 -0
  222. package/dist/esm/types/historicalData.js +47 -0
  223. package/dist/esm/types/knowledgeNote.js +15 -0
  224. package/dist/esm/types/observedSessionV1.js +257 -0
  225. package/dist/esm/types/opencode.js +12 -0
  226. package/dist/esm/types/plan.js +7 -0
  227. package/dist/esm/types/sessionEvent.js +10 -0
  228. package/dist/esm/types/taskPersistence.js +12 -0
  229. package/dist/esm/usage/billingBlocks.js +148 -0
  230. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  231. package/dist/esm/usage/importSessionHistory.js +84 -0
  232. package/dist/esm/usage/subagentUsage.js +18 -0
  233. package/dist/esm/usage/usageEvents.js +402 -0
  234. package/dist/esm/usage/usageReports.js +202 -0
  235. package/dist/esm/usageNormalization.js +95 -0
  236. package/dist/esm/watchers/eventBridge.js +219 -0
  237. package/dist/esm/watchers/factory.js +63 -0
  238. package/dist/esm/watchers/jsonlTail.js +289 -0
  239. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  240. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  241. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  242. package/dist/esm/watchers/types.js +4 -0
  243. package/dist/esm/workspaceRoots.js +56 -0
  244. package/dist/esm/writers/atomic.js +375 -0
  245. package/dist/esm/writers/decisions.js +51 -0
  246. package/dist/esm/writers/notes.js +46 -0
  247. package/dist/esm/writers/tasks.js +63 -0
  248. package/dist/esm/zaiQuota.js +323 -0
  249. package/dist/esm/zaiQuotaApi.js +271 -0
  250. package/dist/esm/zaiQuotaWatcher.js +265 -0
  251. package/dist/fileHead.d.ts +22 -0
  252. package/dist/fileHead.js +33 -0
  253. package/dist/index.d.ts +30 -11
  254. package/dist/index.js +72 -15
  255. package/dist/keepAlive.d.ts +2 -1
  256. package/dist/keepAlive.js +116 -29
  257. package/dist/liveCredentialGuard.d.ts +28 -0
  258. package/dist/liveCredentialGuard.js +36 -0
  259. package/dist/node.d.ts +13 -1
  260. package/dist/node.js +43 -2
  261. package/dist/parsers/codexHistory.js +2 -2
  262. package/dist/parsers/codexParser.d.ts +12 -0
  263. package/dist/parsers/codexParser.js +34 -12
  264. package/dist/parsers/debugLogParser.js +2 -2
  265. package/dist/parsers/sessionPathResolver.js +5 -4
  266. package/dist/processDetection.d.ts +10 -3
  267. package/dist/processDetection.js +44 -11
  268. package/dist/promptHistory.d.ts +32 -0
  269. package/dist/promptHistory.js +115 -59
  270. package/dist/providerFailure.d.ts +12 -6
  271. package/dist/providerFailure.js +45 -0
  272. package/dist/providers/claudeCode.js +7 -99
  273. package/dist/providers/claudeListing.d.ts +22 -0
  274. package/dist/providers/claudeListing.js +158 -0
  275. package/dist/providers/codex.d.ts +3 -1
  276. package/dist/providers/codex.js +26 -227
  277. package/dist/providers/codexRollouts.d.ts +44 -0
  278. package/dist/providers/codexRollouts.js +261 -0
  279. package/dist/providers/detect.js +4 -4
  280. package/dist/readers/plans.js +3 -3
  281. package/dist/schemas/accountManager.d.ts +22 -0
  282. package/dist/schemas/accountManager.js +4 -0
  283. package/dist/schemas/assistantTurn.d.ts +2 -2
  284. package/dist/schemas/index.d.ts +2 -0
  285. package/dist/schemas/index.js +7 -2
  286. package/dist/schemas/providerFailure.d.ts +154 -0
  287. package/dist/schemas/providerFailure.js +40 -0
  288. package/dist/sessionPromptHistory.d.ts +12 -7
  289. package/dist/sessionPromptHistory.js +15 -3
  290. package/dist/sessionPromptSignals.d.ts +9 -0
  291. package/dist/sessionPromptSignals.js +13 -0
  292. package/dist/terminalSync.d.ts +12 -2
  293. package/dist/terminalSync.js +27 -2
  294. package/dist/testing/credentialGuard.d.ts +23 -0
  295. package/dist/testing/credentialGuard.js +233 -0
  296. package/dist/testing/index.d.ts +14 -0
  297. package/dist/testing/index.js +22 -0
  298. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  299. package/dist/testing/sessionLogFixtures.js +169 -0
  300. package/dist/tokenEstimation.d.ts +2 -0
  301. package/dist/tokenEstimation.js +21 -11
  302. package/dist/toolCategory.d.ts +18 -0
  303. package/dist/toolCategory.js +86 -0
  304. package/dist/types/codex.d.ts +4 -0
  305. package/dist/usage/usageEvents.d.ts +16 -0
  306. package/dist/usage/usageEvents.js +53 -9
  307. package/dist/watchers/jsonlTail.d.ts +26 -1
  308. package/dist/watchers/jsonlTail.js +136 -28
  309. package/dist/watchers/jsonlWatcher.js +7 -0
  310. package/dist/watchers/providerReaderWatcher.js +1 -0
  311. package/dist/watchers/types.d.ts +6 -0
  312. package/dist/workspaceRoots.d.ts +23 -0
  313. package/dist/workspaceRoots.js +93 -0
  314. package/package.json +22 -2
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pure helpers over the Claude Code credential blob (`claudeAiOauth`). No I/O.
3
+ */
4
+ import { createHash } from 'crypto';
5
+ /** Lifetime of a Claude Code access token. */
6
+ export const CLAUDE_ACCESS_TOKEN_LIFETIME_MS = 8 * 60 * 60 * 1000;
7
+ /**
8
+ * Refresh-token lifetime observed on Claude Code 2.1.x. Used only to estimate
9
+ * the refresh expiry of snapshots captured before the CLI recorded it.
10
+ */
11
+ export const CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS = 21 * 24 * 60 * 60 * 1000;
12
+ function finiteNumber(value) {
13
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
14
+ }
15
+ /** Parse the stored blob, tolerating the `mcpOAuth` sibling and unknown keys. */
16
+ export function parseClaudeCredentialBlob(blob) {
17
+ if (!blob || typeof blob !== 'object')
18
+ return null;
19
+ const oauth = blob.claudeAiOauth;
20
+ if (!oauth || typeof oauth !== 'object')
21
+ return null;
22
+ const record = oauth;
23
+ if (typeof record.accessToken !== 'string' || !record.accessToken)
24
+ return null;
25
+ return {
26
+ accessToken: record.accessToken,
27
+ refreshToken: typeof record.refreshToken === 'string' ? record.refreshToken : undefined,
28
+ expiresAt: finiteNumber(record.expiresAt),
29
+ refreshTokenExpiresAt: finiteNumber(record.refreshTokenExpiresAt),
30
+ scopes: Array.isArray(record.scopes)
31
+ ? record.scopes.filter((scope) => typeof scope === 'string')
32
+ : undefined,
33
+ subscriptionType: typeof record.subscriptionType === 'string' ? record.subscriptionType : undefined,
34
+ rateLimitTier: typeof record.rateLimitTier === 'string' ? record.rateLimitTier : undefined,
35
+ };
36
+ }
37
+ /**
38
+ * Monotonic freshness clock: every refresh issues a new access token with a
39
+ * later `expiresAt`, so the larger value is the newer credential.
40
+ */
41
+ export function claudeCredentialFreshness(credentials) {
42
+ if (!credentials)
43
+ return 0;
44
+ return credentials.expiresAt ?? credentials.refreshTokenExpiresAt ?? 0;
45
+ }
46
+ /** True when `candidate` is strictly newer than `existing`. */
47
+ export function isNewerClaudeCredential(candidate, existing) {
48
+ return claudeCredentialFreshness(candidate) > claudeCredentialFreshness(existing);
49
+ }
50
+ /**
51
+ * When the refresh-token expiry was recorded, use it; otherwise estimate it
52
+ * from the access-token issue time. Returns `estimated: true` for the latter.
53
+ */
54
+ export function resolveClaudeRefreshExpiry(credentials) {
55
+ if (!credentials)
56
+ return null;
57
+ if (credentials.refreshTokenExpiresAt !== undefined) {
58
+ return { refreshExpiresAt: credentials.refreshTokenExpiresAt, estimated: false };
59
+ }
60
+ if (credentials.expiresAt !== undefined) {
61
+ const issuedAt = credentials.expiresAt - CLAUDE_ACCESS_TOKEN_LIFETIME_MS;
62
+ return {
63
+ refreshExpiresAt: issuedAt + CLAUDE_REFRESH_TOKEN_LIFETIME_ESTIMATE_MS,
64
+ estimated: true,
65
+ };
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * Short, non-reversible fingerprint of the refresh token. Claude Code rotates
71
+ * the refresh token on every refresh, so equal fingerprints mean two stores
72
+ * hold the same link of one chain. Safe to persist and log; tokens never are.
73
+ */
74
+ export function claudeRefreshFingerprint(credentials) {
75
+ const token = credentials?.refreshToken;
76
+ return token ? createHash('sha256').update(token).digest('hex').slice(0, 16) : null;
77
+ }
78
+ function recordOrUndefined(value) {
79
+ return value && typeof value === 'object' && !Array.isArray(value)
80
+ ? value
81
+ : undefined;
82
+ }
83
+ /**
84
+ * The part of a blob a profile home keeps: the Claude login only. MCP OAuth
85
+ * tokens (`mcpOAuth`) rotate too and follow the live store, so a profile copy
86
+ * never duplicates one of their chains (and stays far below the Keychain
87
+ * line limit).
88
+ */
89
+ export function claudeLoginOnly(blob) {
90
+ const oauth = recordOrUndefined(recordOrUndefined(blob)?.claudeAiOauth);
91
+ return oauth ? { claudeAiOauth: oauth } : null;
92
+ }
93
+ /**
94
+ * The blob to install into the live store when `candidate`'s login replaces
95
+ * the live one: everything live holds (its MCP tokens are the freshest links
96
+ * of their chains) with the candidate's `claudeAiOauth`, plus any MCP server
97
+ * only the candidate knows. Mirrors what Claude Code's own `/login` keeps.
98
+ */
99
+ export function mergeClaudeLoginIntoLive(live, candidate) {
100
+ const liveRecord = recordOrUndefined(live) ?? {};
101
+ const candidateRecord = recordOrUndefined(candidate) ?? {};
102
+ const merged = {
103
+ ...liveRecord,
104
+ claudeAiOauth: candidateRecord.claudeAiOauth,
105
+ };
106
+ const liveMcp = recordOrUndefined(liveRecord.mcpOAuth);
107
+ const candidateMcp = recordOrUndefined(candidateRecord.mcpOAuth);
108
+ if (liveMcp || candidateMcp)
109
+ merged.mcpOAuth = { ...candidateMcp, ...liveMcp };
110
+ return merged;
111
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The decision every write into the live Claude credential store must pass.
3
+ * Pure: callers read the stores, this decides.
4
+ *
5
+ * Claude Code rotates the refresh token on every refresh, so the live store
6
+ * always holds the newest link of its chain. Writing an older link over it
7
+ * destroys a working login, and installing another account without saving the
8
+ * live one first loses that account's chain. The rules:
9
+ *
10
+ * - `switch`: another account replaces live. Allowed only after the outgoing
11
+ * live credential was captured (whenever live holds one), and never with a
12
+ * candidate that is the same refresh-token link as live.
13
+ * - `relogin` / `repair`: the same account's credential replaces live. Allowed
14
+ * only when the candidate is strictly fresher (a different refresh token
15
+ * with a later expiry); otherwise live is kept.
16
+ * - `rollback`: restore what a failed switch displaced. Allowed only while the
17
+ * live store still holds exactly what that switch wrote (or is empty or
18
+ * unparseable); a write Claude Code made in between is never clobbered.
19
+ * - An unreadable live store (`error`: timeout, locked Keychain) refuses all.
20
+ */
21
+ import { decideLiveCredentialWrite } from './liveCredentialGuard';
22
+ import { claudeCredentialFreshness, claudeRefreshFingerprint, parseClaudeCredentialBlob, } from './claudeCredentials';
23
+ export function decideLiveClaudeWrite(request) {
24
+ const candidate = parseClaudeCredentialBlob(request.candidate);
25
+ const live = request.live.status === 'ok' ? parseClaudeCredentialBlob(request.live.blob) : null;
26
+ const liveFp = claudeRefreshFingerprint(live);
27
+ return decideLiveCredentialWrite({
28
+ provider: 'Claude',
29
+ intent: request.intent,
30
+ liveStatus: request.live.status,
31
+ candidateValid: candidate !== null,
32
+ liveHasCredential: live !== null,
33
+ liveAccountId: request.liveAccountUuid,
34
+ candidateAccountId: request.candidateAccountUuid,
35
+ sameLink: liveFp !== null && liveFp === claudeRefreshFingerprint(candidate),
36
+ strictlyNewer: claudeCredentialFreshness(candidate) > claudeCredentialFreshness(live),
37
+ capturedOutgoing: request.capturedOutgoing,
38
+ matchesWritten: request.live.status === 'ok' &&
39
+ JSON.stringify(request.live.blob) === JSON.stringify(request.written),
40
+ allowNonOkRollback: true,
41
+ requireDifferentLinkForAdvance: true,
42
+ });
43
+ }
@@ -0,0 +1,22 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ export function getDefaultClaudeHome() {
4
+ return path.join(os.homedir(), '.claude');
5
+ }
6
+ /** True when this process runs against the default `~/.claude` home. */
7
+ export function isDefaultClaudeHome() {
8
+ return !process.env.CLAUDE_CONFIG_DIR?.trim();
9
+ }
10
+ /**
11
+ * The home the `claude` CLI reads and writes for this process: `CLAUDE_CONFIG_DIR`
12
+ * when set, else `~/.claude`. Every "live" credential or identity read goes
13
+ * through this so sidekick follows the same home the CLI does.
14
+ */
15
+ export function getLiveClaudeHome() {
16
+ const explicit = process.env.CLAUDE_CONFIG_DIR?.trim();
17
+ return explicit ? explicit : getDefaultClaudeHome();
18
+ }
19
+ /** Where the `claude` CLI writes session transcripts for this process. */
20
+ export function getClaudeProjectsDir() {
21
+ return path.join(getLiveClaudeHome(), 'projects');
22
+ }
@@ -0,0 +1,142 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { createHash } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { getAccountsDir } from './accountRegistry';
7
+ import { getDefaultClaudeHome, getLiveClaudeHome, isDefaultClaudeHome } from './claudePaths';
8
+ export { getDefaultClaudeHome, getLiveClaudeHome, isDefaultClaudeHome };
9
+ const DEFAULT_CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
10
+ const KEYCHAIN_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/;
11
+ const KEYCHAIN_FALLBACK_ACCOUNT = 'claude-code-user';
12
+ /**
13
+ * Claude Code honours this variable ahead of `CLAUDE_CONFIG_DIR` when it
14
+ * derives the keychain item name. Any `claude` child sidekick spawns must not
15
+ * inherit it, or the login would land in an item nobody else reads.
16
+ */
17
+ export const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = 'CLAUDE_SECURESTORAGE_CONFIG_DIR';
18
+ export function getClaudeProfilesDir() {
19
+ return path.join(getAccountsDir(), 'claude', 'profiles');
20
+ }
21
+ /**
22
+ * True when the live home is one of sidekick's own profile homes: a shell
23
+ * that eval'd `sidekick accounts env` or runs inside `sidekick accounts shell`.
24
+ * That profile's store then reads as the live store, so sync, re-login, and
25
+ * switching must not treat it as live: they would delete the shell's only
26
+ * usable copy and re-point the active account at it.
27
+ */
28
+ export function isLiveClaudeHomeSidekickProfile() {
29
+ const liveHome = path.resolve(getLiveClaudeHome());
30
+ const profilesDir = path.resolve(getClaudeProfilesDir());
31
+ return liveHome === profilesDir || liveHome.startsWith(profilesDir + path.sep);
32
+ }
33
+ export function getClaudeProfileDir(uuid) {
34
+ return path.join(getClaudeProfilesDir(), uuid);
35
+ }
36
+ export function getClaudeProfileHome(uuid) {
37
+ return path.join(getClaudeProfileDir(uuid), 'home');
38
+ }
39
+ /**
40
+ * Sidekick's private backup of a profile's credential. Claude Code never reads
41
+ * it, so it is not a CLI-usable copy: this is where the live account's
42
+ * credential is mirrored while its profile home stays empty.
43
+ */
44
+ export function getClaudeCredentialBackupPath(uuid) {
45
+ return path.join(getAccountsDir(), 'credentials', `${uuid}.credentials.json`);
46
+ }
47
+ export function ensureClaudeProfileDirs(uuid) {
48
+ fs.mkdirSync(getClaudeProfileHome(uuid), { recursive: true, mode: 0o700 });
49
+ }
50
+ /**
51
+ * Claude Code hashes the NFC-normalised config directory; macOS hands paths
52
+ * back decomposed (NFD), so a home directory with a non-ASCII name would
53
+ * otherwise hash differently from the item the CLI wrote.
54
+ */
55
+ export function claudeKeychainSuffix(configDir) {
56
+ return createHash('sha256').update(configDir.normalize('NFC')).digest('hex').slice(0, 8);
57
+ }
58
+ /**
59
+ * Keychain service name for a config directory. Claude Code drops the suffix
60
+ * when `CLAUDE_CONFIG_DIR` is unset — not when the path merely equals
61
+ * `~/.claude` — so the no-argument form follows the process environment.
62
+ */
63
+ export function claudeKeychainService(configDir) {
64
+ const dir = configDir ?? (isDefaultClaudeHome() ? undefined : getLiveClaudeHome());
65
+ return dir
66
+ ? `${DEFAULT_CLAUDE_KEYCHAIN_SERVICE}-${claudeKeychainSuffix(dir)}`
67
+ : DEFAULT_CLAUDE_KEYCHAIN_SERVICE;
68
+ }
69
+ /** The keychain account name Claude Code uses: the unix username, sanitised. */
70
+ export function claudeKeychainAccountName() {
71
+ let name;
72
+ try {
73
+ name = process.env.USER || os.userInfo().username;
74
+ }
75
+ catch {
76
+ name = undefined;
77
+ }
78
+ return name && KEYCHAIN_ACCOUNT_PATTERN.test(name) ? name : KEYCHAIN_FALLBACK_ACCOUNT;
79
+ }
80
+ /** Environment for spawning `claude` against a specific home. */
81
+ export function buildClaudeChildEnv(home, baseEnv = process.env) {
82
+ const env = { ...baseEnv, CLAUDE_CONFIG_DIR: home };
83
+ delete env[CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV];
84
+ return env;
85
+ }
86
+ function readOauthAccount(configPath) {
87
+ try {
88
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
89
+ return raw?.oauthAccount && typeof raw.oauthAccount === 'object' ? raw.oauthAccount : null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Where the identity (`oauthAccount`) of a Claude home lives. Recent Claude
97
+ * Code versions keep it in `<home>/.claude.json`; older ones (and the default
98
+ * home on this machine's history) use `~/.claude.json`. Prefer the in-home file
99
+ * when it carries an identity, otherwise fall back to the legacy location for
100
+ * the default home only.
101
+ */
102
+ export function getClaudeConfigPath(home = getLiveClaudeHome()) {
103
+ const primary = path.join(home, '.claude.json');
104
+ if (readOauthAccount(primary))
105
+ return primary;
106
+ if (path.resolve(home) === path.resolve(getDefaultClaudeHome())) {
107
+ return path.join(os.homedir(), '.claude.json');
108
+ }
109
+ return primary;
110
+ }
111
+ function identityFromOauthAccount(oauthAccount) {
112
+ const email = oauthAccount?.emailAddress;
113
+ const uuid = oauthAccount?.accountUuid;
114
+ return typeof email === 'string' && typeof uuid === 'string' ? { email, uuid } : null;
115
+ }
116
+ /** Identity of the account currently logged in to a Claude home (default: the live home). */
117
+ export function readLiveClaudeIdentity(home = getLiveClaudeHome()) {
118
+ return identityFromOauthAccount(readOauthAccount(getClaudeConfigPath(home)));
119
+ }
120
+ export function readClaudeProfileIdentity(home) {
121
+ return identityFromOauthAccount(readOauthAccount(path.join(home, '.claude.json')));
122
+ }
123
+ export function keychainServiceExists(service) {
124
+ try {
125
+ execFileSync('security', ['find-generic-password', '-s', service], {
126
+ stdio: ['ignore', 'ignore', 'ignore'],
127
+ timeout: 4000,
128
+ killSignal: 'SIGKILL',
129
+ });
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ export function isClaudeProfileAuthenticated(home) {
137
+ const hasCredentials = process.platform === 'darwin'
138
+ ? keychainServiceExists(claudeKeychainService(home)) ||
139
+ fs.existsSync(path.join(home, '.credentials.json'))
140
+ : fs.existsSync(path.join(home, '.credentials.json'));
141
+ return hasCredentials && readClaudeProfileIdentity(home) !== null;
142
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Borrow Claude Code's own token-refresh lock while Sidekick reads and
3
+ * replaces a home's credential.
4
+ *
5
+ * Claude Code 2.1.x serialises refreshes per config directory with a
6
+ * proper-lockfile directory lock at `<configDir>/.oauth_refresh.lock` (stale
7
+ * after 60 s) plus a legacy `<realpath(configDir)>.lock`. Under that lock it
8
+ * re-reads the store before refreshing and adopts a token someone else wrote,
9
+ * so holding both locks keeps a refresh (which rotates the refresh token) out
10
+ * of the window between Sidekick's read and its write.
11
+ *
12
+ * Best effort by construction: this layers on Claude Code internals. If they
13
+ * change we lose only the exclusion; the callers' fingerprint re-checks still
14
+ * catch a store that moved. Held briefly (a few `security` calls), well under
15
+ * the 60 s stale window, and released on every exit.
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ /** Claude Code treats a lock older than this as abandoned (proper-lockfile `stale`). */
20
+ export const CLAUDE_REFRESH_LOCK_STALE_MS = 60_000;
21
+ const DEFAULT_WAIT_MS = 5_000;
22
+ const POLL_MS = 50;
23
+ const sleepView = new Int32Array(new SharedArrayBuffer(4));
24
+ /** Claude Code is refreshing a login in one of the homes; retry shortly. */
25
+ export class ClaudeRefreshLockBusyError extends Error {
26
+ constructor(lockPath) {
27
+ super(`Claude Code is refreshing a login (${lockPath} is held); try again in a few seconds.`);
28
+ this.name = 'ClaudeRefreshLockBusyError';
29
+ }
30
+ }
31
+ export function claudeRefreshLockPaths(home) {
32
+ let real = path.resolve(home);
33
+ try {
34
+ real = fs.realpathSync(home);
35
+ }
36
+ catch {
37
+ /* keep the resolved path */
38
+ }
39
+ return [path.join(home, '.oauth_refresh.lock'), `${real}.lock`];
40
+ }
41
+ function tryAcquire(lockPath, now) {
42
+ try {
43
+ fs.mkdirSync(lockPath);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ if (error.code !== 'EEXIST')
48
+ throw error;
49
+ }
50
+ try {
51
+ const stat = fs.statSync(lockPath);
52
+ if (now - stat.mtimeMs > CLAUDE_REFRESH_LOCK_STALE_MS) {
53
+ // proper-lockfile's own recovery: an abandoned lock is removed and retaken.
54
+ fs.rmSync(lockPath, { recursive: true, force: true });
55
+ fs.mkdirSync(lockPath);
56
+ return true;
57
+ }
58
+ }
59
+ catch {
60
+ /* released or retaken between checks; poll again */
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Run `operation` holding the refresh locks of every existing home (a home
66
+ * that does not exist has no CLI using it). Throws
67
+ * {@link ClaudeRefreshLockBusyError} when a lock stays held past `waitMs`.
68
+ */
69
+ export function withClaudeRefreshLocksSync(homes, operation, options = {}) {
70
+ const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
71
+ const lockPaths = [
72
+ ...new Set(homes
73
+ .filter((home) => {
74
+ try {
75
+ return fs.statSync(home).isDirectory();
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ })
81
+ .flatMap(claudeRefreshLockPaths)),
82
+ ];
83
+ const held = [];
84
+ try {
85
+ const deadline = Date.now() + waitMs;
86
+ for (const lockPath of lockPaths) {
87
+ while (!tryAcquire(lockPath, Date.now())) {
88
+ if (Date.now() >= deadline)
89
+ throw new ClaudeRefreshLockBusyError(lockPath);
90
+ Atomics.wait(sleepView, 0, 0, POLL_MS);
91
+ }
92
+ held.push(lockPath);
93
+ }
94
+ return operation();
95
+ }
96
+ finally {
97
+ for (const lockPath of held.reverse()) {
98
+ try {
99
+ fs.rmSync(lockPath, { recursive: true, force: true });
100
+ }
101
+ catch {
102
+ /* Claude Code recovers a stale lock after 60 s */
103
+ }
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pure helpers over the Codex CLI `auth.json` payload. No filesystem access
3
+ * beyond an optional mtime fallback, no child processes.
4
+ */
5
+ import * as fs from 'fs';
6
+ import { createHash } from 'crypto';
7
+ // Codex refreshes OAuth tokens at most every 8 days; a stored refresh token
8
+ // older than that may already be rejected by the auth server.
9
+ export const STALE_AUTH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
10
+ export function parseJwtPayload(jwt) {
11
+ const parts = jwt.split('.');
12
+ if (parts.length < 2)
13
+ return null;
14
+ try {
15
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
16
+ return JSON.parse(payload);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function parseAuthJson(raw) {
23
+ if (!raw)
24
+ return null;
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ return parsed && typeof parsed === 'object' ? parsed : null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export function readAuthIdentityFromRaw(raw) {
34
+ const parsed = parseAuthJson(raw);
35
+ if (!parsed)
36
+ return null;
37
+ const idToken = parsed.tokens?.id_token;
38
+ const claims = idToken ? parseJwtPayload(idToken) : null;
39
+ const profileClaims = claims?.['https://api.openai.com/profile'];
40
+ const authClaims = claims?.['https://api.openai.com/auth'];
41
+ const email = typeof claims?.email === 'string'
42
+ ? claims.email
43
+ : typeof profileClaims?.email === 'string'
44
+ ? profileClaims.email
45
+ : undefined;
46
+ const workspaceId = typeof authClaims?.chatgpt_account_id === 'string'
47
+ ? authClaims.chatgpt_account_id
48
+ : parsed.tokens?.account_id;
49
+ const planType = typeof authClaims?.chatgpt_plan_type === 'string' ? authClaims.chatgpt_plan_type : undefined;
50
+ const authMode = parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key' ? 'api-key' : 'chatgpt';
51
+ return { email, workspaceId, planType, authMode };
52
+ }
53
+ export function readLastRefresh(raw, fallbackPath) {
54
+ const parsed = parseAuthJson(raw);
55
+ if (parsed?.last_refresh) {
56
+ const ts = Date.parse(parsed.last_refresh);
57
+ if (!Number.isNaN(ts))
58
+ return ts;
59
+ }
60
+ if (fallbackPath) {
61
+ try {
62
+ return fs.statSync(fallbackPath).mtimeMs;
63
+ }
64
+ catch {
65
+ /* fall through */
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ /** Expiry (ms since epoch) of the access token JWT, when it carries `exp`. */
71
+ export function readAccessTokenExpiry(raw) {
72
+ const parsed = parseAuthJson(raw);
73
+ const accessToken = parsed?.tokens?.access_token;
74
+ if (!accessToken)
75
+ return null;
76
+ const claims = parseJwtPayload(accessToken);
77
+ return typeof claims?.exp === 'number' && Number.isFinite(claims.exp) ? claims.exp * 1000 : null;
78
+ }
79
+ /** Workspace id first (a ChatGPT workspace is one seat), then email. */
80
+ export function identitiesMatch(a, b) {
81
+ if (!a || !b)
82
+ return false;
83
+ if (a.workspaceId && b.workspaceId)
84
+ return a.workspaceId === b.workspaceId;
85
+ return Boolean(a.email && b.email && a.email === b.email);
86
+ }
87
+ /** Non-secret chain-link identifier; raw token values never belong in diagnostics. */
88
+ export function codexRefreshFingerprint(raw) {
89
+ const token = parseAuthJson(raw)?.tokens?.refresh_token;
90
+ return typeof token === 'string' && token
91
+ ? createHash('sha256').update(token).digest('hex').slice(0, 16)
92
+ : null;
93
+ }
94
+ /** Compare evidence, never filesystem timestamps. Incomparable credentials tie. */
95
+ export function compareCodexAuthFreshness(a, b) {
96
+ const ar = readLastRefresh(a), br = readLastRefresh(b);
97
+ if (ar !== null && br !== null && ar !== br)
98
+ return ar > br ? 1 : -1;
99
+ const ae = readAccessTokenExpiry(a), be = readAccessTokenExpiry(b);
100
+ return ae !== null && be !== null && ae !== be ? (ae > be ? 1 : -1) : 0;
101
+ }