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
@@ -7,30 +7,90 @@
7
7
  * Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
8
8
  * macOS too, so reads consult the file after a Keychain miss.
9
9
  *
10
+ * Keychain writes mirror Claude Code 2.1.x exactly: the payload is hex-encoded
11
+ * (`-X`) and sent through `security -i` only while the command line fits the
12
+ * tool's line buffer; longer payloads go through argv. `security -i` silently
13
+ * truncates longer lines, which once stored half a credential (the `mcpOAuth`
14
+ * tokens make blobs of 4 KB and more) — a login Claude Code then reports as
15
+ * "Not logged in". Every write is read back and compared before it counts.
16
+ *
10
17
  * Both accounts.ts and credentials.ts consume this module so that all
11
18
  * credential access is platform-correct in one place.
12
19
  */
13
20
  export declare const KEYCHAIN_SERVICE = "Claude Code-credentials";
21
+ /**
22
+ * Claude Code's ceiling for a `security -i` command line (the tool reads at
23
+ * most 4095 bytes per line). Longer commands go through argv instead.
24
+ */
25
+ export declare const SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
26
+ /**
27
+ * Written as the item comment (`-j`) on every Keychain item Sidekick creates,
28
+ * so ownership is provable from the item itself even after its directory and
29
+ * the ledger that described it are gone.
30
+ */
31
+ export declare const SIDEKICK_KEYCHAIN_COMMENT_PREFIX = "sidekick-agent-hub:";
32
+ export type ClaudeCredentialStoreSource = 'keychain' | 'file';
33
+ /**
34
+ * Outcome of reading one credential store. `error` (timeout, locked Keychain)
35
+ * is never treated like `absent`: guard decisions that may overwrite a store
36
+ * only proceed on a definite answer.
37
+ */
38
+ export type ClaudeCredentialStoreRead = {
39
+ status: 'ok';
40
+ blob: Record<string, unknown>;
41
+ source: ClaudeCredentialStoreSource;
42
+ } | {
43
+ status: 'absent';
44
+ } | {
45
+ status: 'corrupt';
46
+ source: ClaudeCredentialStoreSource;
47
+ length: number;
48
+ } | {
49
+ status: 'error';
50
+ reason: string;
51
+ };
52
+ /** A credential write that did not land, or could not be verified. */
53
+ export declare class ClaudeCredentialWriteError extends Error {
54
+ constructor(message: string);
55
+ }
56
+ /**
57
+ * Read one Claude credential store with a definite outcome. On macOS the
58
+ * Keychain item is authoritative; the file is consulted only when the item is
59
+ * absent (Claude Code's own fallback), unless `keychainOnly` is set.
60
+ */
61
+ export declare function readClaudeCredentialStore(configDir?: string, options?: {
62
+ keychainOnly?: boolean;
63
+ }): ClaudeCredentialStoreRead;
14
64
  /**
15
65
  * Read the active Claude Code credentials as a parsed object.
16
66
  *
17
67
  * - macOS: reads from system Keychain via the `security` CLI, then the file
18
68
  * - Linux / WSL / Windows: reads `<home>/.credentials.json`
19
69
  *
20
- * Returns `null` when credentials are absent or unreadable.
70
+ * Returns `null` when credentials are absent or unreadable. Callers that must
71
+ * tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
21
72
  */
22
73
  export declare function readActiveCredentials(configDir?: string): unknown;
23
74
  /**
24
- * Write credentials to the active Claude Code credential store.
75
+ * Write credentials to a profile home's Claude Code credential store.
25
76
  *
26
- * - macOS: writes to system Keychain via the `security` CLI
27
- * - Linux / WSL / Windows: atomic-writes `<home>/.credentials.json`
77
+ * - macOS: the suffixed Keychain item for `configDir`
78
+ * - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
28
79
  *
29
- * Throws on failure.
80
+ * `configDir` is required: the live store is written only through the guarded
81
+ * installer in accounts.ts, which refuses downgrades and captures the outgoing
82
+ * login first. Throws {@link ClaudeCredentialWriteError} when the write did
83
+ * not land intact.
30
84
  */
31
85
  export declare function writeActiveCredentials(credentials: unknown, configDir?: string): void;
86
+ /**
87
+ * Write the live store with no guard. Internal: the only caller is
88
+ * `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
89
+ */
90
+ export declare function writeLiveClaudeCredentialStoreUnguarded(credentials: unknown): void;
32
91
  /**
33
92
  * Remove the credential store of a config directory (an abandoned isolated
34
- * login, a removed profile). Best effort: never throws.
93
+ * login, a removed profile, the profile home of the account that became
94
+ * live). Never throws; returns true when the store is gone afterwards.
35
95
  */
36
- export declare function deleteStoredCredentials(configDir: string): void;
96
+ export declare function deleteStoredCredentials(configDir: string): boolean;
@@ -8,6 +8,13 @@
8
8
  * Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
9
9
  * macOS too, so reads consult the file after a Keychain miss.
10
10
  *
11
+ * Keychain writes mirror Claude Code 2.1.x exactly: the payload is hex-encoded
12
+ * (`-X`) and sent through `security -i` only while the command line fits the
13
+ * tool's line buffer; longer payloads go through argv. `security -i` silently
14
+ * truncates longer lines, which once stored half a credential (the `mcpOAuth`
15
+ * tokens make blobs of 4 KB and more) — a login Claude Code then reports as
16
+ * "Not logged in". Every write is read back and compared before it counts.
17
+ *
11
18
  * Both accounts.ts and credentials.ts consume this module so that all
12
19
  * credential access is platform-correct in one place.
13
20
  */
@@ -45,17 +52,41 @@ var __importStar = (this && this.__importStar) || (function () {
45
52
  };
46
53
  })();
47
54
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.KEYCHAIN_SERVICE = void 0;
55
+ exports.ClaudeCredentialWriteError = exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX = exports.SECURITY_INTERACTIVE_LINE_LIMIT = exports.KEYCHAIN_SERVICE = void 0;
56
+ exports.readClaudeCredentialStore = readClaudeCredentialStore;
49
57
  exports.readActiveCredentials = readActiveCredentials;
50
58
  exports.writeActiveCredentials = writeActiveCredentials;
59
+ exports.writeLiveClaudeCredentialStoreUnguarded = writeLiveClaudeCredentialStoreUnguarded;
51
60
  exports.deleteStoredCredentials = deleteStoredCredentials;
52
61
  const fs = __importStar(require("fs"));
53
62
  const path = __importStar(require("path"));
54
63
  const child_process_1 = require("child_process");
55
64
  const claudeProfiles_1 = require("./claudeProfiles");
65
+ const claudeCredentialLedger_1 = require("./claudeCredentialLedger");
56
66
  const atomic_1 = require("./writers/atomic");
57
67
  exports.KEYCHAIN_SERVICE = 'Claude Code-credentials';
58
68
  const SECURITY_TIMEOUT_MS = 4000;
69
+ /** `security` exits with this status when no item matches. */
70
+ const SECURITY_ITEM_NOT_FOUND = 44;
71
+ /**
72
+ * Claude Code's ceiling for a `security -i` command line (the tool reads at
73
+ * most 4095 bytes per line). Longer commands go through argv instead.
74
+ */
75
+ exports.SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
76
+ /**
77
+ * Written as the item comment (`-j`) on every Keychain item Sidekick creates,
78
+ * so ownership is provable from the item itself even after its directory and
79
+ * the ledger that described it are gone.
80
+ */
81
+ exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX = 'sidekick-agent-hub:';
82
+ /** A credential write that did not land, or could not be verified. */
83
+ class ClaudeCredentialWriteError extends Error {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = 'ClaudeCredentialWriteError';
87
+ }
88
+ }
89
+ exports.ClaudeCredentialWriteError = ClaudeCredentialWriteError;
59
90
  function quoteSecurityInteractiveArg(value) {
60
91
  // `security -i` is not a POSIX shell: adjacent quoted segments do NOT
61
92
  // concatenate (so the shell-style '\'' splice splits the token), and
@@ -65,27 +96,69 @@ function quoteSecurityInteractiveArg(value) {
65
96
  function getCredentialsFilePath(configDir) {
66
97
  return path.join(configDir ?? (0, claudeProfiles_1.getLiveClaudeHome)(), '.credentials.json');
67
98
  }
68
- function readCredentialsFile(configDir) {
99
+ function parseStoredText(raw, source) {
100
+ const text = raw.trim();
101
+ if (!text)
102
+ return { status: 'corrupt', source, length: 0 };
103
+ // `security -w` prints hex when the stored bytes are not plain text.
104
+ const decoded = source === 'keychain' && !text.startsWith('{') && /^(?:[0-9a-f]{2})+$/i.test(text)
105
+ ? Buffer.from(text, 'hex').toString('utf8')
106
+ : text;
69
107
  try {
70
- return JSON.parse(fs.readFileSync(getCredentialsFilePath(configDir), 'utf8'));
108
+ const parsed = JSON.parse(decoded);
109
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
110
+ return { status: 'ok', blob: parsed, source };
111
+ }
71
112
  }
72
113
  catch {
73
- return null;
114
+ /* fall through */
74
115
  }
116
+ return { status: 'corrupt', source, length: decoded.length };
75
117
  }
76
- function readKeychainItem(service) {
118
+ function readCredentialsFileStore(configDir) {
119
+ let raw;
77
120
  try {
78
- const raw = (0, child_process_1.execFileSync)('security', ['find-generic-password', '-s', service, '-w'], {
121
+ raw = fs.readFileSync(getCredentialsFilePath(configDir), 'utf8');
122
+ }
123
+ catch (error) {
124
+ return error.code === 'ENOENT'
125
+ ? { status: 'absent' }
126
+ : { status: 'error', reason: String(error.message ?? error) };
127
+ }
128
+ return parseStoredText(raw, 'file');
129
+ }
130
+ function securityErrorStatus(error) {
131
+ const status = error.status;
132
+ return typeof status === 'number' ? status : null;
133
+ }
134
+ function readKeychainStore(service) {
135
+ try {
136
+ const raw = (0, child_process_1.execFileSync)('security', ['find-generic-password', '-a', (0, claudeProfiles_1.claudeKeychainAccountName)(), '-s', service, '-w'], {
79
137
  encoding: 'utf8',
80
138
  stdio: ['pipe', 'pipe', 'pipe'],
81
139
  timeout: SECURITY_TIMEOUT_MS,
82
140
  killSignal: 'SIGKILL',
83
141
  });
84
- return JSON.parse(raw.trim());
142
+ return parseStoredText(String(raw), 'keychain');
85
143
  }
86
- catch {
87
- return null;
144
+ catch (error) {
145
+ if (securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND)
146
+ return { status: 'absent' };
147
+ return { status: 'error', reason: String(error?.message ?? error) };
148
+ }
149
+ }
150
+ /**
151
+ * Read one Claude credential store with a definite outcome. On macOS the
152
+ * Keychain item is authoritative; the file is consulted only when the item is
153
+ * absent (Claude Code's own fallback), unless `keychainOnly` is set.
154
+ */
155
+ function readClaudeCredentialStore(configDir, options = {}) {
156
+ if (process.platform === 'darwin') {
157
+ const keychain = readKeychainStore((0, claudeProfiles_1.claudeKeychainService)(configDir));
158
+ if (keychain.status !== 'absent' || options.keychainOnly)
159
+ return keychain;
88
160
  }
161
+ return readCredentialsFileStore(configDir);
89
162
  }
90
163
  /**
91
164
  * Read the active Claude Code credentials as a parsed object.
@@ -93,69 +166,160 @@ function readKeychainItem(service) {
93
166
  * - macOS: reads from system Keychain via the `security` CLI, then the file
94
167
  * - Linux / WSL / Windows: reads `<home>/.credentials.json`
95
168
  *
96
- * Returns `null` when credentials are absent or unreadable.
169
+ * Returns `null` when credentials are absent or unreadable. Callers that must
170
+ * tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
97
171
  */
98
172
  function readActiveCredentials(configDir) {
99
173
  if (process.platform === 'darwin') {
100
- return readKeychainItem((0, claudeProfiles_1.claudeKeychainService)(configDir)) ?? readCredentialsFile(configDir);
174
+ const keychain = readKeychainStore((0, claudeProfiles_1.claudeKeychainService)(configDir));
175
+ if (keychain.status === 'ok')
176
+ return keychain.blob;
101
177
  }
102
- return readCredentialsFile(configDir);
178
+ const file = readCredentialsFileStore(configDir);
179
+ return file.status === 'ok' ? file.blob : null;
180
+ }
181
+ function runKeychainWrite(service, hex, comment) {
182
+ const account = (0, claudeProfiles_1.claudeKeychainAccountName)();
183
+ const args = ['add-generic-password', '-U', '-a', account, '-s', service];
184
+ const interactive = [
185
+ 'add-generic-password',
186
+ '-U',
187
+ '-a',
188
+ quoteSecurityInteractiveArg(account),
189
+ '-s',
190
+ quoteSecurityInteractiveArg(service),
191
+ ];
192
+ if (comment) {
193
+ args.push('-j', comment);
194
+ interactive.push('-j', quoteSecurityInteractiveArg(comment));
195
+ }
196
+ args.push('-X', hex);
197
+ interactive.push('-X', quoteSecurityInteractiveArg(hex));
198
+ const line = `${interactive.join(' ')}\n`;
199
+ if (Buffer.byteLength(line, 'utf8') <= exports.SECURITY_INTERACTIVE_LINE_LIMIT) {
200
+ (0, child_process_1.execFileSync)('security', ['-i'], {
201
+ input: line,
202
+ stdio: ['pipe', 'pipe', 'pipe'],
203
+ timeout: SECURITY_TIMEOUT_MS,
204
+ killSignal: 'SIGKILL',
205
+ });
206
+ return;
207
+ }
208
+ // The same trade-off Claude Code makes for large payloads: the hex is on the
209
+ // command line (visible to `ps`) for the duration of this call.
210
+ (0, child_process_1.execFileSync)('security', args, {
211
+ stdio: ['ignore', 'pipe', 'pipe'],
212
+ timeout: SECURITY_TIMEOUT_MS,
213
+ killSignal: 'SIGKILL',
214
+ });
103
215
  }
104
216
  /**
105
- * Write credentials to the active Claude Code credential store.
106
- *
107
- * - macOS: writes to system Keychain via the `security` CLI
108
- * - Linux / WSL / Windows: atomic-writes `<home>/.credentials.json`
109
- *
110
- * Throws on failure.
217
+ * Write a credential blob to a store and verify it by reading it back.
218
+ * `configDir` undefined targets the live home; only the guarded live installer
219
+ * in accounts.ts may do that (see {@link writeLiveClaudeCredentialStoreUnguarded}).
111
220
  */
112
- function writeActiveCredentials(credentials, configDir) {
221
+ function writeClaudeCredentialStore(credentials, configDir) {
113
222
  const json = JSON.stringify(credentials);
114
- JSON.parse(json); // validate round-trip
223
+ let expected;
224
+ try {
225
+ expected = JSON.stringify(JSON.parse(json));
226
+ }
227
+ catch {
228
+ throw new ClaudeCredentialWriteError('Refusing to store a credential that is not JSON.');
229
+ }
115
230
  if (process.platform === 'darwin') {
116
231
  const service = (0, claudeProfiles_1.claudeKeychainService)(configDir);
117
- const command = [
118
- 'add-generic-password',
119
- '-U',
232
+ const comment = configDir
233
+ ? `${exports.SIDEKICK_KEYCHAIN_COMMENT_PREFIX}${path.resolve(configDir)}`
234
+ : null;
235
+ try {
236
+ runKeychainWrite(service, Buffer.from(json, 'utf8').toString('hex'), comment);
237
+ }
238
+ catch (error) {
239
+ throw new ClaudeCredentialWriteError(`Keychain write failed: ${error?.message ?? error}`);
240
+ }
241
+ const readBack = readKeychainStore(service);
242
+ if (readBack.status === 'error') {
243
+ throw new ClaudeCredentialWriteError('The Keychain write could not be verified (read-back failed); treating it as not written.');
244
+ }
245
+ if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
246
+ throw new ClaudeCredentialWriteError('The Keychain item did not hold the credential after writing it.');
247
+ }
248
+ return;
249
+ }
250
+ // Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
251
+ (0, atomic_1.atomicWriteFileSync)(getCredentialsFilePath(configDir), json, 0o600);
252
+ const readBack = readCredentialsFileStore(configDir);
253
+ if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
254
+ throw new ClaudeCredentialWriteError('The credentials file did not hold the credential after writing it.');
255
+ }
256
+ }
257
+ /**
258
+ * Write credentials to a profile home's Claude Code credential store.
259
+ *
260
+ * - macOS: the suffixed Keychain item for `configDir`
261
+ * - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
262
+ *
263
+ * `configDir` is required: the live store is written only through the guarded
264
+ * installer in accounts.ts, which refuses downgrades and captures the outgoing
265
+ * login first. Throws {@link ClaudeCredentialWriteError} when the write did
266
+ * not land intact.
267
+ */
268
+ function writeActiveCredentials(credentials, configDir) {
269
+ if (!configDir) {
270
+ throw new ClaudeCredentialWriteError('writeActiveCredentials() needs a config directory; the live Claude login is only written through a guarded account switch.');
271
+ }
272
+ (0, claudeCredentialLedger_1.recordClaudeCredentialHome)(configDir, 'profile');
273
+ writeClaudeCredentialStore(credentials, configDir);
274
+ }
275
+ /**
276
+ * Write the live store with no guard. Internal: the only caller is
277
+ * `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
278
+ */
279
+ function writeLiveClaudeCredentialStoreUnguarded(credentials) {
280
+ writeClaudeCredentialStore(credentials, undefined);
281
+ }
282
+ function deleteKeychainItem(service, withAccount) {
283
+ try {
284
+ (0, child_process_1.execFileSync)('security', [
285
+ 'delete-generic-password',
286
+ ...(withAccount ? ['-a', (0, claudeProfiles_1.claudeKeychainAccountName)()] : []),
120
287
  '-s',
121
- quoteSecurityInteractiveArg(service),
122
- '-a',
123
- quoteSecurityInteractiveArg((0, claudeProfiles_1.claudeKeychainAccountName)()),
124
- '-w',
125
- quoteSecurityInteractiveArg(json),
126
- ].join(' ');
127
- (0, child_process_1.execFileSync)('security', ['-i'], {
128
- input: command + '\n',
129
- stdio: ['pipe', 'pipe', 'pipe'],
288
+ service,
289
+ ], {
290
+ stdio: ['ignore', 'ignore', 'ignore'],
130
291
  timeout: SECURITY_TIMEOUT_MS,
131
292
  killSignal: 'SIGKILL',
132
293
  });
133
- return;
294
+ return 'deleted';
295
+ }
296
+ catch (error) {
297
+ return securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND ? 'absent' : 'error';
134
298
  }
135
- // Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
136
- (0, atomic_1.atomicWriteFileSync)(getCredentialsFilePath(configDir), json, 0o600);
137
299
  }
138
300
  /**
139
301
  * Remove the credential store of a config directory (an abandoned isolated
140
- * login, a removed profile). Best effort: never throws.
302
+ * login, a removed profile, the profile home of the account that became
303
+ * live). Never throws; returns true when the store is gone afterwards.
141
304
  */
142
305
  function deleteStoredCredentials(configDir) {
306
+ let gone = true;
143
307
  if (process.platform === 'darwin') {
144
- try {
145
- (0, child_process_1.execFileSync)('security', ['delete-generic-password', '-s', (0, claudeProfiles_1.claudeKeychainService)(configDir)], {
146
- stdio: ['ignore', 'ignore', 'ignore'],
147
- timeout: SECURITY_TIMEOUT_MS,
148
- killSignal: 'SIGKILL',
149
- });
150
- }
151
- catch {
152
- /* no item, or keychain unavailable */
153
- }
308
+ const service = (0, claudeProfiles_1.claudeKeychainService)(configDir);
309
+ let outcome = deleteKeychainItem(service, true);
310
+ // Items written before the account name was pinned may carry another one.
311
+ if (outcome === 'absent')
312
+ outcome = deleteKeychainItem(service, false);
313
+ if (outcome === 'error')
314
+ gone = false;
154
315
  }
155
316
  try {
156
317
  fs.rmSync(getCredentialsFilePath(configDir), { force: true });
157
318
  }
158
319
  catch {
159
- /* best effort */
320
+ gone = false;
160
321
  }
322
+ if (gone)
323
+ (0, claudeCredentialLedger_1.forgetClaudeCredentialHome)(configDir);
324
+ return gone;
161
325
  }
package/dist/doctor.js CHANGED
@@ -37,7 +37,6 @@ exports.getSessionDiagnostics = getSessionDiagnostics;
37
37
  exports.runDoctor = runDoctor;
38
38
  exports.formatHealthReport = formatHealthReport;
39
39
  const fs = __importStar(require("fs"));
40
- const os = __importStar(require("os"));
41
40
  const path = __importStar(require("path"));
42
41
  const accountStatus_1 = require("./accountStatus");
43
42
  const paths_1 = require("./paths");
@@ -48,11 +47,12 @@ const openCode_1 = require("./providers/openCode");
48
47
  const factory_1 = require("./providers/factory");
49
48
  const detect_1 = require("./providers/detect");
50
49
  const openCodeDatabase_1 = require("./providers/openCodeDatabase");
50
+ const claudePaths_1 = require("./claudePaths");
51
51
  /** Collects the Claude Code path-resolution detail used by both host surfaces. */
52
52
  function getSessionDiagnostics(workspacePath) {
53
53
  const encodedPath = (0, sessionPathResolver_1.encodeWorkspacePath)(workspacePath);
54
54
  const expectedSessionDir = (0, sessionPathResolver_1.getSessionDirectory)(workspacePath);
55
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
55
+ const projectsDir = (0, claudePaths_1.getClaudeProjectsDir)();
56
56
  let existingProjectDirs = [];
57
57
  let expectedDirExists = false;
58
58
  try {
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureDefaultAccounts = ensureDefaultAccounts;
4
4
  const accounts_1 = require("./accounts");
5
- const codexProfiles_1 = require("./codexProfiles");
6
5
  const accountSync_1 = require("./accountSync");
7
6
  function logFailure(options, message, error) {
8
7
  try {
@@ -35,13 +34,7 @@ async function ensureDefaultAccounts(options) {
35
34
  logFailure(options, 'Claude auth reconciliation failed.', error);
36
35
  }
37
36
  try {
38
- await (0, codexProfiles_1.reconcileCodexAuthStateAsync)();
39
- }
40
- catch (error) {
41
- logFailure(options, 'Codex auth reconciliation failed.', error);
42
- }
43
- try {
44
- (0, accountSync_1.cleanupAbandonedLogins)();
37
+ await (0, accountSync_1.cleanupAbandonedLoginsAsync)();
45
38
  }
46
39
  catch (error) {
47
40
  logFailure(options, 'Abandoned login cleanup failed.', error);
@@ -0,0 +1,15 @@
1
+ const listeners = new Set();
2
+ export function _onRawAccountsChanged(listener) {
3
+ listeners.add(listener);
4
+ return () => listeners.delete(listener);
5
+ }
6
+ export function _emitAccountsChanged(reason) {
7
+ for (const listener of listeners) {
8
+ try {
9
+ listener(reason);
10
+ }
11
+ catch {
12
+ // Account notifications are observational and cannot break mutations.
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,152 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { _emitAccountsChanged, _onRawAccountsChanged, } from './accountChangeSignal';
5
+ import { getActiveAccountStatus } from './accountStatus';
6
+ import { getAccountsDir } from './accountRegistry';
7
+ import { syncLiveAccountStateSync } from './accountSync';
8
+ import { getLiveClaudeHome } from './claudeProfiles';
9
+ import { getSystemCodexHome } from './codexPaths';
10
+ const listeners = new Set();
11
+ let watchers = [];
12
+ let pollTimer;
13
+ let stopRawListener;
14
+ let lastFingerprint;
15
+ let debounceTimer;
16
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
17
+ let activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
18
+ /**
19
+ * Observe login, logout, and active-account switches. Local Sidekick mutations
20
+ * signal immediately; filesystem watchers cover external CLIs; a low-frequency
21
+ * catch-up poll protects network mounts and watcher-hostile filesystems.
22
+ */
23
+ export function onAccountsChanged(listener, options = {}) {
24
+ listeners.add(listener);
25
+ // The fastest interval any live subscriber asked for wins; a later, faster
26
+ // subscriber restarts the timer rather than being silently ignored.
27
+ const requestedPollMs = Math.max(1_000, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
28
+ if (!stopRawListener) {
29
+ activePollIntervalMs = requestedPollMs;
30
+ ensureMonitoring();
31
+ }
32
+ else if (requestedPollMs < activePollIntervalMs) {
33
+ activePollIntervalMs = requestedPollMs;
34
+ restartPollTimer();
35
+ }
36
+ if (options.emitCurrent)
37
+ emitIfChanged('poll', true, listener);
38
+ return {
39
+ dispose: () => {
40
+ listeners.delete(listener);
41
+ if (listeners.size === 0)
42
+ stopMonitoring();
43
+ },
44
+ };
45
+ }
46
+ function ensureMonitoring() {
47
+ if (stopRawListener)
48
+ return;
49
+ lastFingerprint = fingerprint(getActiveAccountStatus());
50
+ stopRawListener = _onRawAccountsChanged((reason) => scheduleCheck(reason));
51
+ for (const target of watchTargets()) {
52
+ try {
53
+ const watcher = fs.watch(target, { persistent: false }, () => {
54
+ scheduleCheck('filesystem');
55
+ });
56
+ watcher.on('error', () => {
57
+ watchers = watchers.filter((candidate) => candidate !== watcher);
58
+ try {
59
+ watcher.close();
60
+ }
61
+ catch {
62
+ // The catch-up poll remains active.
63
+ }
64
+ });
65
+ watchers.push(watcher);
66
+ }
67
+ catch {
68
+ // Missing/unwatchable directories are covered by the catch-up poll.
69
+ }
70
+ }
71
+ startPollTimer();
72
+ }
73
+ function startPollTimer() {
74
+ pollTimer = setInterval(() => emitIfChanged('poll'), activePollIntervalMs);
75
+ pollTimer.unref?.();
76
+ }
77
+ function restartPollTimer() {
78
+ if (pollTimer)
79
+ clearInterval(pollTimer);
80
+ startPollTimer();
81
+ }
82
+ function stopMonitoring() {
83
+ stopRawListener?.();
84
+ stopRawListener = undefined;
85
+ for (const watcher of watchers)
86
+ watcher.close();
87
+ watchers = [];
88
+ if (pollTimer)
89
+ clearInterval(pollTimer);
90
+ pollTimer = undefined;
91
+ if (debounceTimer)
92
+ clearTimeout(debounceTimer);
93
+ debounceTimer = undefined;
94
+ lastFingerprint = undefined;
95
+ activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
96
+ }
97
+ function watchTargets() {
98
+ return Array.from(new Set([
99
+ getAccountsDir(),
100
+ getLiveClaudeHome(),
101
+ path.join(os.homedir(), '.claude'),
102
+ getSystemCodexHome(),
103
+ ]));
104
+ }
105
+ function scheduleCheck(reason) {
106
+ if (debounceTimer)
107
+ clearTimeout(debounceTimer);
108
+ debounceTimer = setTimeout(() => {
109
+ debounceTimer = undefined;
110
+ // Never force: watch targets include ~/.claude and the Codex home, whose
111
+ // history files churn on every prompt. Only a real status change may wake
112
+ // subscribers, or dormant quota pollers would poll on unrelated writes.
113
+ emitIfChanged(reason);
114
+ }, 25);
115
+ debounceTimer.unref?.();
116
+ }
117
+ function emitIfChanged(reason, force = false, singleListener) {
118
+ // External CLIs write the live homes directly: fold their logins into the
119
+ // saved profiles before reporting status, so a native `claude /login` is
120
+ // registered and its rotated tokens are backed up without a host round-trip.
121
+ // `local` signals come from our own registry writes and need no sync.
122
+ if (reason === 'filesystem' || reason === 'poll') {
123
+ try {
124
+ syncLiveAccountStateSync({ reason: reason === 'poll' ? 'poll' : 'watch' });
125
+ }
126
+ catch {
127
+ // Sync problems surface through the health model, never through the watcher.
128
+ }
129
+ }
130
+ const status = getActiveAccountStatus();
131
+ const nextFingerprint = fingerprint(status);
132
+ if (!force && nextFingerprint === lastFingerprint)
133
+ return;
134
+ lastFingerprint = nextFingerprint;
135
+ const event = { reason, status };
136
+ const targets = singleListener ? [singleListener] : [...listeners];
137
+ for (const listener of targets) {
138
+ try {
139
+ listener(event);
140
+ }
141
+ catch {
142
+ // A host callback cannot stop monitoring.
143
+ }
144
+ }
145
+ }
146
+ function fingerprint(status) {
147
+ return JSON.stringify(status);
148
+ }
149
+ /** Internal bridge for account mutations outside the registry module. */
150
+ export function _notifyAccountsChanged() {
151
+ _emitAccountsChanged('local');
152
+ }