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,283 @@
1
+ /**
2
+ * Platform-aware credential I/O for Claude Code.
3
+ *
4
+ * On macOS, Claude Code stores OAuth credentials in the system Keychain
5
+ * (service "Claude Code-credentials", suffixed per config directory). On
6
+ * Linux/WSL/Windows, credentials live in `<home>/.credentials.json`. When the
7
+ * Keychain is unavailable (locked, SSH) Claude Code falls back to the file on
8
+ * macOS too, so reads consult the file after a Keychain miss.
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
+ *
17
+ * Both accounts.ts and credentials.ts consume this module so that all
18
+ * credential access is platform-correct in one place.
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { execFileSync } from 'child_process';
23
+ import { claudeKeychainAccountName, claudeKeychainService, getLiveClaudeHome, } from './claudeProfiles';
24
+ import { forgetClaudeCredentialHome, recordClaudeCredentialHome } from './claudeCredentialLedger';
25
+ import { atomicWriteFileSync } from './writers/atomic';
26
+ export const KEYCHAIN_SERVICE = 'Claude Code-credentials';
27
+ const SECURITY_TIMEOUT_MS = 4000;
28
+ /** `security` exits with this status when no item matches. */
29
+ const SECURITY_ITEM_NOT_FOUND = 44;
30
+ /**
31
+ * Claude Code's ceiling for a `security -i` command line (the tool reads at
32
+ * most 4095 bytes per line). Longer commands go through argv instead.
33
+ */
34
+ export const SECURITY_INTERACTIVE_LINE_LIMIT = 4032;
35
+ /**
36
+ * Written as the item comment (`-j`) on every Keychain item Sidekick creates,
37
+ * so ownership is provable from the item itself even after its directory and
38
+ * the ledger that described it are gone.
39
+ */
40
+ export const SIDEKICK_KEYCHAIN_COMMENT_PREFIX = 'sidekick-agent-hub:';
41
+ /** A credential write that did not land, or could not be verified. */
42
+ export class ClaudeCredentialWriteError extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = 'ClaudeCredentialWriteError';
46
+ }
47
+ }
48
+ function quoteSecurityInteractiveArg(value) {
49
+ // `security -i` is not a POSIX shell: adjacent quoted segments do NOT
50
+ // concatenate (so the shell-style '\'' splice splits the token), and
51
+ // backslash escapes the next character even inside single quotes.
52
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, `\\'`)}'`;
53
+ }
54
+ function getCredentialsFilePath(configDir) {
55
+ return path.join(configDir ?? getLiveClaudeHome(), '.credentials.json');
56
+ }
57
+ function parseStoredText(raw, source) {
58
+ const text = raw.trim();
59
+ if (!text)
60
+ return { status: 'corrupt', source, length: 0 };
61
+ // `security -w` prints hex when the stored bytes are not plain text.
62
+ const decoded = source === 'keychain' && !text.startsWith('{') && /^(?:[0-9a-f]{2})+$/i.test(text)
63
+ ? Buffer.from(text, 'hex').toString('utf8')
64
+ : text;
65
+ try {
66
+ const parsed = JSON.parse(decoded);
67
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
68
+ return { status: 'ok', blob: parsed, source };
69
+ }
70
+ }
71
+ catch {
72
+ /* fall through */
73
+ }
74
+ return { status: 'corrupt', source, length: decoded.length };
75
+ }
76
+ function readCredentialsFileStore(configDir) {
77
+ let raw;
78
+ try {
79
+ raw = fs.readFileSync(getCredentialsFilePath(configDir), 'utf8');
80
+ }
81
+ catch (error) {
82
+ return error.code === 'ENOENT'
83
+ ? { status: 'absent' }
84
+ : { status: 'error', reason: String(error.message ?? error) };
85
+ }
86
+ return parseStoredText(raw, 'file');
87
+ }
88
+ function securityErrorStatus(error) {
89
+ const status = error.status;
90
+ return typeof status === 'number' ? status : null;
91
+ }
92
+ function readKeychainStore(service) {
93
+ try {
94
+ const raw = execFileSync('security', ['find-generic-password', '-a', claudeKeychainAccountName(), '-s', service, '-w'], {
95
+ encoding: 'utf8',
96
+ stdio: ['pipe', 'pipe', 'pipe'],
97
+ timeout: SECURITY_TIMEOUT_MS,
98
+ killSignal: 'SIGKILL',
99
+ });
100
+ return parseStoredText(String(raw), 'keychain');
101
+ }
102
+ catch (error) {
103
+ if (securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND)
104
+ return { status: 'absent' };
105
+ return { status: 'error', reason: String(error?.message ?? error) };
106
+ }
107
+ }
108
+ /**
109
+ * Read one Claude credential store with a definite outcome. On macOS the
110
+ * Keychain item is authoritative; the file is consulted only when the item is
111
+ * absent (Claude Code's own fallback), unless `keychainOnly` is set.
112
+ */
113
+ export function readClaudeCredentialStore(configDir, options = {}) {
114
+ if (process.platform === 'darwin') {
115
+ const keychain = readKeychainStore(claudeKeychainService(configDir));
116
+ if (keychain.status !== 'absent' || options.keychainOnly)
117
+ return keychain;
118
+ }
119
+ return readCredentialsFileStore(configDir);
120
+ }
121
+ /**
122
+ * Read the active Claude Code credentials as a parsed object.
123
+ *
124
+ * - macOS: reads from system Keychain via the `security` CLI, then the file
125
+ * - Linux / WSL / Windows: reads `<home>/.credentials.json`
126
+ *
127
+ * Returns `null` when credentials are absent or unreadable. Callers that must
128
+ * tell "absent" from "could not read" use {@link readClaudeCredentialStore}.
129
+ */
130
+ export function readActiveCredentials(configDir) {
131
+ if (process.platform === 'darwin') {
132
+ const keychain = readKeychainStore(claudeKeychainService(configDir));
133
+ if (keychain.status === 'ok')
134
+ return keychain.blob;
135
+ }
136
+ const file = readCredentialsFileStore(configDir);
137
+ return file.status === 'ok' ? file.blob : null;
138
+ }
139
+ function runKeychainWrite(service, hex, comment) {
140
+ const account = claudeKeychainAccountName();
141
+ const args = ['add-generic-password', '-U', '-a', account, '-s', service];
142
+ const interactive = [
143
+ 'add-generic-password',
144
+ '-U',
145
+ '-a',
146
+ quoteSecurityInteractiveArg(account),
147
+ '-s',
148
+ quoteSecurityInteractiveArg(service),
149
+ ];
150
+ if (comment) {
151
+ args.push('-j', comment);
152
+ interactive.push('-j', quoteSecurityInteractiveArg(comment));
153
+ }
154
+ args.push('-X', hex);
155
+ interactive.push('-X', quoteSecurityInteractiveArg(hex));
156
+ const line = `${interactive.join(' ')}\n`;
157
+ if (Buffer.byteLength(line, 'utf8') <= SECURITY_INTERACTIVE_LINE_LIMIT) {
158
+ execFileSync('security', ['-i'], {
159
+ input: line,
160
+ stdio: ['pipe', 'pipe', 'pipe'],
161
+ timeout: SECURITY_TIMEOUT_MS,
162
+ killSignal: 'SIGKILL',
163
+ });
164
+ return;
165
+ }
166
+ // The same trade-off Claude Code makes for large payloads: the hex is on the
167
+ // command line (visible to `ps`) for the duration of this call.
168
+ execFileSync('security', args, {
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ timeout: SECURITY_TIMEOUT_MS,
171
+ killSignal: 'SIGKILL',
172
+ });
173
+ }
174
+ /**
175
+ * Write a credential blob to a store and verify it by reading it back.
176
+ * `configDir` undefined targets the live home; only the guarded live installer
177
+ * in accounts.ts may do that (see {@link writeLiveClaudeCredentialStoreUnguarded}).
178
+ */
179
+ function writeClaudeCredentialStore(credentials, configDir) {
180
+ const json = JSON.stringify(credentials);
181
+ let expected;
182
+ try {
183
+ expected = JSON.stringify(JSON.parse(json));
184
+ }
185
+ catch {
186
+ throw new ClaudeCredentialWriteError('Refusing to store a credential that is not JSON.');
187
+ }
188
+ if (process.platform === 'darwin') {
189
+ const service = claudeKeychainService(configDir);
190
+ const comment = configDir
191
+ ? `${SIDEKICK_KEYCHAIN_COMMENT_PREFIX}${path.resolve(configDir)}`
192
+ : null;
193
+ try {
194
+ runKeychainWrite(service, Buffer.from(json, 'utf8').toString('hex'), comment);
195
+ }
196
+ catch (error) {
197
+ throw new ClaudeCredentialWriteError(`Keychain write failed: ${error?.message ?? error}`);
198
+ }
199
+ const readBack = readKeychainStore(service);
200
+ if (readBack.status === 'error') {
201
+ throw new ClaudeCredentialWriteError('The Keychain write could not be verified (read-back failed); treating it as not written.');
202
+ }
203
+ if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
204
+ throw new ClaudeCredentialWriteError('The Keychain item did not hold the credential after writing it.');
205
+ }
206
+ return;
207
+ }
208
+ // Linux / WSL / Windows — file-based (tmp + fsync + rename, retried on Windows)
209
+ atomicWriteFileSync(getCredentialsFilePath(configDir), json, 0o600);
210
+ const readBack = readCredentialsFileStore(configDir);
211
+ if (readBack.status !== 'ok' || JSON.stringify(readBack.blob) !== expected) {
212
+ throw new ClaudeCredentialWriteError('The credentials file did not hold the credential after writing it.');
213
+ }
214
+ }
215
+ /**
216
+ * Write credentials to a profile home's Claude Code credential store.
217
+ *
218
+ * - macOS: the suffixed Keychain item for `configDir`
219
+ * - Linux / WSL / Windows: atomic-writes `<configDir>/.credentials.json`
220
+ *
221
+ * `configDir` is required: the live store is written only through the guarded
222
+ * installer in accounts.ts, which refuses downgrades and captures the outgoing
223
+ * login first. Throws {@link ClaudeCredentialWriteError} when the write did
224
+ * not land intact.
225
+ */
226
+ export function writeActiveCredentials(credentials, configDir) {
227
+ if (!configDir) {
228
+ throw new ClaudeCredentialWriteError('writeActiveCredentials() needs a config directory; the live Claude login is only written through a guarded account switch.');
229
+ }
230
+ recordClaudeCredentialHome(configDir, 'profile');
231
+ writeClaudeCredentialStore(credentials, configDir);
232
+ }
233
+ /**
234
+ * Write the live store with no guard. Internal: the only caller is
235
+ * `installIntoLiveClaudeStore` in accounts.ts (a test enforces this).
236
+ */
237
+ export function writeLiveClaudeCredentialStoreUnguarded(credentials) {
238
+ writeClaudeCredentialStore(credentials, undefined);
239
+ }
240
+ function deleteKeychainItem(service, withAccount) {
241
+ try {
242
+ execFileSync('security', [
243
+ 'delete-generic-password',
244
+ ...(withAccount ? ['-a', claudeKeychainAccountName()] : []),
245
+ '-s',
246
+ service,
247
+ ], {
248
+ stdio: ['ignore', 'ignore', 'ignore'],
249
+ timeout: SECURITY_TIMEOUT_MS,
250
+ killSignal: 'SIGKILL',
251
+ });
252
+ return 'deleted';
253
+ }
254
+ catch (error) {
255
+ return securityErrorStatus(error) === SECURITY_ITEM_NOT_FOUND ? 'absent' : 'error';
256
+ }
257
+ }
258
+ /**
259
+ * Remove the credential store of a config directory (an abandoned isolated
260
+ * login, a removed profile, the profile home of the account that became
261
+ * live). Never throws; returns true when the store is gone afterwards.
262
+ */
263
+ export function deleteStoredCredentials(configDir) {
264
+ let gone = true;
265
+ if (process.platform === 'darwin') {
266
+ const service = claudeKeychainService(configDir);
267
+ let outcome = deleteKeychainItem(service, true);
268
+ // Items written before the account name was pinned may carry another one.
269
+ if (outcome === 'absent')
270
+ outcome = deleteKeychainItem(service, false);
271
+ if (outcome === 'error')
272
+ gone = false;
273
+ }
274
+ try {
275
+ fs.rmSync(getCredentialsFilePath(configDir), { force: true });
276
+ }
277
+ catch {
278
+ gone = false;
279
+ }
280
+ if (gone)
281
+ forgetClaudeCredentialHome(configDir);
282
+ return gone;
283
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Read Claude Max OAuth credentials from disk.
3
+ *
4
+ * Shared by sidekick-cli, sidekick-vscode, and any external consumer.
5
+ */
6
+ import { parseClaudeCredentialBlob } from './claudeCredentials';
7
+ import { readActiveCredentials } from './credentialIO';
8
+ function readParsed() {
9
+ try {
10
+ return parseClaudeCredentialBlob(readActiveCredentials());
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /**
17
+ * Reads Claude Max OAuth credentials.
18
+ *
19
+ * On macOS reads from system Keychain; on Linux/Windows reads from
20
+ * `~/.claude/.credentials.json`. Returns `null` if credentials are
21
+ * absent, the token is missing, or the token is expired. Never throws.
22
+ */
23
+ export async function readClaudeMaxCredentials() {
24
+ const parsed = readParsed();
25
+ if (!parsed)
26
+ return null;
27
+ if (parsed.expiresAt && Date.now() > parsed.expiresAt)
28
+ return null;
29
+ return parsed;
30
+ }
31
+ /**
32
+ * Like {@link readClaudeMaxCredentials} but returns the stored credential even
33
+ * when the access token has lapsed: the CLI refreshes it on next use, so an
34
+ * expired access token is still a valid login (until the refresh token expires).
35
+ */
36
+ export function readClaudeMaxCredentialsRaw() {
37
+ return readParsed();
38
+ }
39
+ /**
40
+ * Synchronous convenience — returns just the access token or `null`.
41
+ */
42
+ export function readClaudeMaxAccessTokenSync() {
43
+ const parsed = readParsed();
44
+ if (!parsed)
45
+ return null;
46
+ if (parsed.expiresAt && Date.now() > parsed.expiresAt)
47
+ return null;
48
+ return parsed.accessToken;
49
+ }
@@ -0,0 +1,305 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { getActiveAccountStatus } from './accountStatus';
4
+ import { resolveProjectIdentity } from './paths';
5
+ import { discoverSessionDirectory, encodeWorkspacePath, findSubdirectorySessionDirs, getMostRecentlyActiveSessionDir, getSessionDirectory, } from './parsers/sessionPathResolver';
6
+ import { toLegacyProviderStatus } from './providerStatus';
7
+ import { fetchProviderServiceStatus } from './providerServiceStatus';
8
+ import { getOpenCodeDataDir } from './providers/openCode';
9
+ import { createSessionProviders } from './providers/factory';
10
+ import { detectProvider } from './providers/detect';
11
+ import { OpenCodeDatabase } from './providers/openCodeDatabase';
12
+ import { getClaudeProjectsDir } from './claudePaths';
13
+ /** Collects the Claude Code path-resolution detail used by both host surfaces. */
14
+ export function getSessionDiagnostics(workspacePath) {
15
+ const encodedPath = encodeWorkspacePath(workspacePath);
16
+ const expectedSessionDir = getSessionDirectory(workspacePath);
17
+ const projectsDir = getClaudeProjectsDir();
18
+ let existingProjectDirs = [];
19
+ let expectedDirExists = false;
20
+ try {
21
+ if (fs.existsSync(projectsDir)) {
22
+ existingProjectDirs = fs
23
+ .readdirSync(projectsDir)
24
+ .filter((name) => {
25
+ try {
26
+ return fs.statSync(path.join(projectsDir, name)).isDirectory();
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ })
32
+ .sort();
33
+ }
34
+ expectedDirExists = fs.existsSync(expectedSessionDir);
35
+ }
36
+ catch {
37
+ // Diagnostics remain useful even if a directory cannot be read.
38
+ }
39
+ const discoveredSessionDir = discoverSessionDirectory(workspacePath);
40
+ const workspaceBasename = path.basename(workspacePath).toLowerCase();
41
+ const similarDirs = existingProjectDirs.filter((directory) => {
42
+ const lower = directory.toLowerCase();
43
+ return (lower.includes(workspaceBasename) || workspaceBasename.includes(lower.split('-').pop() || ''));
44
+ });
45
+ const subdirectoryMatches = findSubdirectorySessionDirs(workspacePath);
46
+ return {
47
+ workspacePath,
48
+ encodedPath,
49
+ expectedSessionDir,
50
+ expectedDirExists,
51
+ discoveredSessionDir,
52
+ existingProjectDirs,
53
+ similarDirs,
54
+ platform: process.platform,
55
+ subdirectoryMatches,
56
+ selectedSubdirectoryMatch: subdirectoryMatches.length > 0 ? getMostRecentlyActiveSessionDir(subdirectoryMatches) : null,
57
+ };
58
+ }
59
+ function statusUnavailable() {
60
+ return {
61
+ indicator: 'none',
62
+ description: 'Status unavailable',
63
+ affectedComponents: [],
64
+ activeIncident: null,
65
+ updatedAt: new Date().toISOString(),
66
+ };
67
+ }
68
+ async function checkSessionProvider(providerId, workspacePath) {
69
+ const { providers } = createSessionProviders({ providerIds: [providerId] });
70
+ const provider = providers[0];
71
+ const title = `Session discovery (${provider?.displayName ?? providerId})`;
72
+ try {
73
+ if (!provider)
74
+ throw new Error(`${providerId} could not be initialized.`);
75
+ const sessions = provider.listSessionFilesAsync
76
+ ? await provider.listSessionFilesAsync(workspacePath)
77
+ : provider.findAllSessions(workspacePath);
78
+ if (sessions.length > 0) {
79
+ return {
80
+ id: 'session_discovery',
81
+ status: 'ok',
82
+ title,
83
+ message: `Found ${sessions.length} ${provider.displayName} session${sessions.length === 1 ? '' : 's'} for ${workspacePath}.`,
84
+ };
85
+ }
86
+ const operation = provider.getLastOperationStatus?.();
87
+ const runtime = provider.getRuntimeStatus?.();
88
+ const issue = operation && !operation.usable
89
+ ? operation.runtimeStatus
90
+ : runtime && !runtime.available
91
+ ? runtime
92
+ : undefined;
93
+ return {
94
+ id: 'session_discovery',
95
+ status: 'warning',
96
+ title,
97
+ message: issue?.message ?? `No ${provider.displayName} sessions found for ${workspacePath}.`,
98
+ repair: issue?.kind === 'sqlite_missing'
99
+ ? 'Install sqlite3 and ensure it is on PATH, then rerun sidekick doctor.'
100
+ : `Start ${provider.displayName} in this project, or inspect ${provider.getSessionDirectory(workspacePath)}.`,
101
+ };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ id: 'session_discovery',
106
+ status: 'warning',
107
+ title,
108
+ message: error instanceof Error ? error.message : String(error),
109
+ repair: `Check ${providerId} session storage permissions and rerun sidekick doctor.`,
110
+ };
111
+ }
112
+ finally {
113
+ for (const candidate of providers)
114
+ candidate.dispose();
115
+ }
116
+ }
117
+ export async function runDoctor(options = {}) {
118
+ const project = resolveProjectIdentity(options.cwd);
119
+ const sessions = getSessionDiagnostics(project.cwd);
120
+ const accounts = getActiveAccountStatus(undefined, { selfHeal: false });
121
+ const sessionProvider = options.provider && options.provider !== 'auto' ? options.provider : detectProvider();
122
+ let openCode = options.openCodeStatus;
123
+ if (!openCode) {
124
+ const database = new OpenCodeDatabase(getOpenCodeDataDir());
125
+ database.open();
126
+ openCode = database.getRuntimeStatus();
127
+ database.close();
128
+ }
129
+ const serviceStatus = options.fetchServiceStatuses || !options.fetchStatuses
130
+ ? await (options.fetchServiceStatuses ??
131
+ (async () => {
132
+ const [claude, openai] = await Promise.all([
133
+ fetchProviderServiceStatus('claude-code'),
134
+ fetchProviderServiceStatus('codex'),
135
+ ]);
136
+ return { claude, openai };
137
+ }))().catch(() => {
138
+ const unavailable = (provider) => ({
139
+ availability: 'unavailable',
140
+ provider,
141
+ checkedAt: new Date().toISOString(),
142
+ sourceUrl: `https://status.${provider === 'codex' ? 'openai' : 'claude'}.com/api/v2/summary.json`,
143
+ reason: 'network_error',
144
+ });
145
+ return { claude: unavailable('claude-code'), openai: unavailable('codex') };
146
+ })
147
+ : undefined;
148
+ const providerStatus = serviceStatus
149
+ ? {
150
+ claude: toLegacyProviderStatus(serviceStatus.claude),
151
+ openai: toLegacyProviderStatus(serviceStatus.openai),
152
+ }
153
+ : await options.fetchStatuses().catch(() => ({
154
+ claude: statusUnavailable(),
155
+ openai: statusUnavailable(),
156
+ }));
157
+ const checks = [];
158
+ checks.push(project.hasLegacyMismatch
159
+ ? {
160
+ id: 'project_slug',
161
+ status: 'warning',
162
+ title: 'Project path resolves to a different slug',
163
+ message: `Legacy ${project.legacySlug}; canonical ${project.canonicalSlug}.`,
164
+ repair: `Open the resolved path (${project.resolvedCwd}) and merge any legacy project stores into the canonical slug. Sidekick readers keep the legacy fallback.`,
165
+ }
166
+ : {
167
+ id: 'project_slug',
168
+ status: 'ok',
169
+ title: 'Project identity',
170
+ message: `Canonical slug: ${project.canonicalSlug}`,
171
+ });
172
+ checks.push(await checkSessionProvider(sessionProvider, project.resolvedCwd));
173
+ const openCodeCheck = openCode.available
174
+ ? {
175
+ id: 'opencode_sqlite',
176
+ status: 'ok',
177
+ title: 'OpenCode database',
178
+ message: 'OpenCode database and sqlite3 are available.',
179
+ }
180
+ : openCode.kind === 'db_missing'
181
+ ? {
182
+ id: 'opencode_sqlite',
183
+ status: 'info',
184
+ title: 'OpenCode database',
185
+ message: 'No OpenCode database was found; this is expected if OpenCode is not used.',
186
+ }
187
+ : {
188
+ id: 'opencode_sqlite',
189
+ status: 'warning',
190
+ title: 'OpenCode database',
191
+ message: openCode.message ?? `OpenCode database unavailable (${openCode.kind}).`,
192
+ repair: openCode.kind === 'sqlite_missing'
193
+ ? 'Install sqlite3 and ensure the sqlite3 executable is on PATH, then rerun sidekick doctor.'
194
+ : 'Make sqlite3 executable and verify the OpenCode database is readable.',
195
+ };
196
+ if (sessionProvider !== 'opencode' && openCodeCheck.status === 'warning') {
197
+ openCodeCheck.status = 'info';
198
+ openCodeCheck.message += ' This does not affect the selected session provider.';
199
+ }
200
+ checks.push(openCodeCheck);
201
+ checks.push({
202
+ id: 'accounts',
203
+ status: accounts.ok ? 'ok' : 'info',
204
+ title: 'Accounts and credentials',
205
+ message: accounts.ok
206
+ ? `Detected ${[accounts.claude.present && 'Claude', accounts.codex.present && 'Codex'].filter(Boolean).join(' and ')} credentials.`
207
+ : (accounts.error ?? 'No Claude or Codex credentials were detected.'),
208
+ ...(!accounts.ok
209
+ ? {
210
+ repair: 'Sign in with claude or codex to refresh account quota. Existing sessions can be monitored without saved credentials.',
211
+ }
212
+ : {}),
213
+ });
214
+ const unavailableProviders = serviceStatus
215
+ ? Object.entries(serviceStatus)
216
+ .filter(([, status]) => status.availability === 'unavailable')
217
+ .map(([name]) => name)
218
+ : Object.entries(providerStatus)
219
+ .filter(([, status]) => status.description === 'Status unavailable')
220
+ .map(([name]) => name);
221
+ const degradedProviders = serviceStatus
222
+ ? Object.entries(serviceStatus)
223
+ .filter(([, status]) => status.availability === 'observed' && status.severity !== 'none')
224
+ .map(([name]) => name)
225
+ : Object.entries(providerStatus)
226
+ .filter(([, status]) => status.indicator !== 'none')
227
+ .map(([name]) => name);
228
+ const partialProviders = serviceStatus
229
+ ? Object.entries(serviceStatus)
230
+ .filter(([, status]) => status.availability === 'observed' && status.incidents === null)
231
+ .map(([name]) => name)
232
+ : [];
233
+ const observations = [
234
+ ...(degradedProviders.length
235
+ ? [`Public vendor degradation reported for: ${degradedProviders.join(', ')}.`]
236
+ : []),
237
+ ...(unavailableProviders.length
238
+ ? [`Status pages unavailable for: ${unavailableProviders.join(', ')}.`]
239
+ : []),
240
+ ...(partialProviders.length
241
+ ? [`Incident information unavailable for: ${partialProviders.join(', ')}.`]
242
+ : []),
243
+ ];
244
+ checks.push({
245
+ id: 'provider_api',
246
+ status: degradedProviders.includes(sessionProvider === 'codex' ? 'openai' : sessionProvider === 'claude-code' ? 'claude' : '')
247
+ ? 'warning'
248
+ : observations.length
249
+ ? 'info'
250
+ : 'ok',
251
+ title: 'Public provider service status',
252
+ message: (observations.length
253
+ ? observations.join(' ')
254
+ : 'Claude and OpenAI status pages report normal operation.') +
255
+ ' Public status does not establish request connectivity or authentication.',
256
+ });
257
+ const deprecatedSettings = options.deprecatedSettings ?? [];
258
+ checks.push({
259
+ id: 'deprecated_settings',
260
+ status: deprecatedSettings.length > 0 ? 'warning' : 'ok',
261
+ title: 'Deprecated settings',
262
+ message: deprecatedSettings.length > 0
263
+ ? deprecatedSettings
264
+ .map((setting) => `${setting.key}${setting.replacement ? ` → ${setting.replacement}` : ''}`)
265
+ .join(', ')
266
+ : 'No deprecated Sidekick settings detected.',
267
+ ...(deprecatedSettings.length > 0
268
+ ? {
269
+ repair: 'Move each configured value to its replacement and remove the deprecated setting.',
270
+ }
271
+ : {}),
272
+ });
273
+ const status = checks.some((check) => check.status === 'error')
274
+ ? 'unhealthy'
275
+ : checks.some((check) => check.status === 'warning')
276
+ ? 'attention'
277
+ : 'healthy';
278
+ return {
279
+ schemaVersion: 1,
280
+ generatedAt: new Date().toISOString(),
281
+ project,
282
+ status,
283
+ checks,
284
+ accounts,
285
+ openCode,
286
+ providerStatus,
287
+ ...(serviceStatus ? { serviceStatus } : {}),
288
+ sessions,
289
+ sessionProvider,
290
+ };
291
+ }
292
+ export function formatHealthReport(report) {
293
+ const icon = { ok: '✓', warning: '!', error: '✕', info: '·' };
294
+ const lines = [
295
+ `Sidekick Doctor — ${report.status}`,
296
+ `Project: ${report.project.resolvedCwd}`,
297
+ '',
298
+ ];
299
+ for (const check of report.checks) {
300
+ lines.push(`${icon[check.status]} ${check.title}: ${check.message}`);
301
+ if (check.repair)
302
+ lines.push(` Repair: ${check.repair}`);
303
+ }
304
+ return lines.join('\n');
305
+ }