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,575 @@
1
+ /**
2
+ * Claude Code session provider for the shared package.
3
+ * Reads JSONL session files from ~/.claude/projects/.
4
+ *
5
+ * Implements the full SessionProviderBase interface with incremental
6
+ * reading via ClaudeCodeReader, subagent scanning, and cross-session search.
7
+ *
8
+ * Ported from sidekick-vscode/src/services/providers/ClaudeCodeSessionProvider.ts
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { StringDecoder } from 'string_decoder';
13
+ import { readSessionContextSnapshot } from '../context/sessionContext';
14
+ import { readSessionFileStats } from '../sessionStats';
15
+ import { JsonlParser } from '../parsers/jsonl';
16
+ import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
17
+ import { ProviderDiagnosticTracker } from './diagnostics';
18
+ import { DirectoryListingCache, splitWatchedPath } from './directoryListingCache';
19
+ import { encodeWorkspacePath as encodeWsPath, getSessionDirectory as getSessionDir, discoverSessionDirectory as discoverSessionDir, findActiveSession as findActiveSessionPath, findAllSessions as findAllSessionPaths, findSessionsInDirectory as findSessionsInDir, decodeEncodedPath, getAllProjectFolders as getAllProjectFoldersRaw, findSessionFilesWithStats, } from '../parsers/sessionPathResolver';
20
+ import { scanSubagentDir } from '../parsers/subagentScanner';
21
+ import { getModelContextWindowSize } from '../modelContext';
22
+ import { extractSessionEvents } from '../schemas/sessionEvent';
23
+ import { getClaudeProjectsDir } from '../claudePaths';
24
+ import { listClaudeSessionFilesAsync, workspaceDirectoryFilter } from './claudeListing';
25
+ /** Type guard for content blocks with a `type` string property */
26
+ function isTypedBlock(block) {
27
+ return (block !== null &&
28
+ typeof block === 'object' &&
29
+ typeof block.type === 'string');
30
+ }
31
+ /**
32
+ * Extracts searchable text from a session event object.
33
+ */
34
+ function extractSearchableText(event) {
35
+ const content = event.message?.content;
36
+ if (!content)
37
+ return '';
38
+ if (typeof content === 'string')
39
+ return content;
40
+ if (Array.isArray(content)) {
41
+ const parts = [];
42
+ for (const block of content) {
43
+ if (block && typeof block === 'object') {
44
+ const b = block;
45
+ if (typeof b.text === 'string')
46
+ parts.push(b.text);
47
+ if (typeof b.thinking === 'string')
48
+ parts.push(b.thinking);
49
+ if (typeof b.content === 'string')
50
+ parts.push(b.content);
51
+ if (b.input && typeof b.input === 'object')
52
+ parts.push(JSON.stringify(b.input));
53
+ }
54
+ }
55
+ return parts.join(' ');
56
+ }
57
+ return '';
58
+ }
59
+ /**
60
+ * Project-directory filter for a workspace: the encoded workspace path, a
61
+ * worktree suffix of it, or a directory ending in the workspace's basename.
62
+ * Without a workspace every directory matches.
63
+ */
64
+ function extractClaudeLabelFromPrefix(chunk) {
65
+ for (const line of chunk.split('\n')) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed)
68
+ continue;
69
+ try {
70
+ const event = JSON.parse(trimmed);
71
+ if (event.type !== 'user')
72
+ continue;
73
+ const content = event.message?.content;
74
+ if (!content)
75
+ continue;
76
+ let text = null;
77
+ if (typeof content === 'string') {
78
+ text = content.trim();
79
+ }
80
+ else if (Array.isArray(content)) {
81
+ const textBlock = content.find((block) => isTypedBlock(block) &&
82
+ block.type === 'text' &&
83
+ typeof block.text === 'string' &&
84
+ block.text.trim().length > 0);
85
+ if (textBlock && isTypedBlock(textBlock) && typeof textBlock.text === 'string') {
86
+ text = textBlock.text.trim();
87
+ }
88
+ }
89
+ if (text) {
90
+ const compact = text.replace(/\s+/g, ' ');
91
+ return compact.length > 60 ? compact.substring(0, 57) + '...' : compact;
92
+ }
93
+ }
94
+ catch {
95
+ // Skip malformed lines.
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * Incremental JSONL reader for Claude Code session files.
102
+ *
103
+ * Tracks byte position in the file and uses JsonlParser for
104
+ * streaming line-buffered parsing of new content.
105
+ */
106
+ class ClaudeCodeReader {
107
+ sessionPath;
108
+ parser;
109
+ filePosition = 0;
110
+ /** Cursor after the last complete line, safe to persist across restarts. */
111
+ committedPosition = 0;
112
+ events = [];
113
+ _wasTruncated = false;
114
+ decoder = new StringDecoder('utf8');
115
+ /** Split lines of one response repeat its usage; count each response once. */
116
+ usageDeduper;
117
+ constructor(sessionPath, usageDeduper) {
118
+ this.sessionPath = sessionPath;
119
+ this.usageDeduper = usageDeduper ?? new ClaudeUsageDeduper();
120
+ this.parser = new JsonlParser({
121
+ onEvent: (raw) => {
122
+ for (const event of extractSessionEvents(raw)) {
123
+ this.events.push(this.usageDeduper.apply(normalizeClaudeUsage(event)));
124
+ }
125
+ },
126
+ onError: (_err, _line) => {
127
+ // Silently skip parse errors — no logging framework dependency
128
+ },
129
+ });
130
+ }
131
+ readNew() {
132
+ this.events = [];
133
+ this._wasTruncated = false;
134
+ try {
135
+ if (!fs.existsSync(this.sessionPath)) {
136
+ return [];
137
+ }
138
+ const stats = fs.statSync(this.sessionPath);
139
+ const currentSize = stats.size;
140
+ // Handle truncation
141
+ if (currentSize < this.filePosition) {
142
+ this._wasTruncated = true;
143
+ this.filePosition = 0;
144
+ this.committedPosition = 0;
145
+ this.parser.reset();
146
+ this.decoder = new StringDecoder('utf8');
147
+ this.usageDeduper.reset();
148
+ }
149
+ // No new content
150
+ if (currentSize <= this.filePosition) {
151
+ return [];
152
+ }
153
+ // Read new bytes from last position
154
+ const fd = fs.openSync(this.sessionPath, 'r');
155
+ const bufferSize = currentSize - this.filePosition;
156
+ const buffer = Buffer.alloc(bufferSize);
157
+ let bytesRead = 0;
158
+ try {
159
+ bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
160
+ }
161
+ finally {
162
+ fs.closeSync(fd);
163
+ }
164
+ const bytes = buffer.subarray(0, bytesRead);
165
+ const newline = bytes.lastIndexOf(0x0a);
166
+ if (newline >= 0)
167
+ this.committedPosition = this.filePosition + newline + 1;
168
+ const chunk = this.decoder.write(bytes);
169
+ this.parser.processChunk(chunk);
170
+ this.filePosition += bytesRead;
171
+ }
172
+ catch (error) {
173
+ console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
174
+ }
175
+ return this.events;
176
+ }
177
+ readAll() {
178
+ this.reset();
179
+ return this.readNew();
180
+ }
181
+ reset() {
182
+ this.filePosition = 0;
183
+ this.committedPosition = 0;
184
+ this.parser.reset();
185
+ this.decoder = new StringDecoder('utf8');
186
+ this.usageDeduper.reset();
187
+ this._wasTruncated = false;
188
+ }
189
+ exists() {
190
+ return fs.existsSync(this.sessionPath);
191
+ }
192
+ flush() {
193
+ const finalChunk = this.decoder.end();
194
+ if (finalChunk)
195
+ this.parser.processChunk(finalChunk);
196
+ this.parser.flush();
197
+ this.decoder = new StringDecoder('utf8');
198
+ }
199
+ getPosition() {
200
+ return this.committedPosition;
201
+ }
202
+ seekTo(position) {
203
+ this.filePosition = position;
204
+ this.committedPosition = position;
205
+ this.parser.reset();
206
+ this.decoder = new StringDecoder('utf8');
207
+ }
208
+ wasTruncated() {
209
+ return this._wasTruncated;
210
+ }
211
+ }
212
+ /**
213
+ * Session provider for Claude Code CLI.
214
+ *
215
+ * Implements the full SessionProviderBase interface, delegating path
216
+ * resolution to sessionPathResolver, parsing to JsonlParser, and
217
+ * subagent scanning to subagentScanner.
218
+ */
219
+ export class ClaudeCodeProvider {
220
+ id = 'claude-code';
221
+ displayName = 'Claude Code';
222
+ diagnostics;
223
+ /** Runtime-reported context window limit (overrides static map when set). */
224
+ dynamicContextWindowLimit = null;
225
+ /** Project-directory listings reused across async enumerations. */
226
+ directoryCache = new DirectoryListingCache();
227
+ constructor(options = {}) {
228
+ this.diagnostics = new ProviderDiagnosticTracker(this.id, options);
229
+ }
230
+ // --- Path resolution ---
231
+ getSessionDirectory(workspacePath) {
232
+ return getSessionDir(workspacePath);
233
+ }
234
+ discoverSessionDirectory(workspacePath) {
235
+ return discoverSessionDir(workspacePath);
236
+ }
237
+ // --- Session discovery ---
238
+ findActiveSession(workspacePath) {
239
+ return findActiveSessionPath(workspacePath);
240
+ }
241
+ findAllSessions(workspacePath) {
242
+ return findAllSessionPaths(workspacePath);
243
+ }
244
+ findSessionById(workspacePath, sessionId) {
245
+ const normalizedId = sessionId.trim();
246
+ if (!normalizedId || path.basename(normalizedId) !== normalizedId)
247
+ return null;
248
+ try {
249
+ const directories = new Set();
250
+ directories.add(this.getSessionDirectory(workspacePath));
251
+ const discovered = this.discoverSessionDirectory(workspacePath);
252
+ if (discovered)
253
+ directories.add(discovered);
254
+ for (const folder of this.getAllProjectFolders(workspacePath))
255
+ directories.add(folder.dir);
256
+ for (const directory of directories) {
257
+ const direct = path.join(directory, `${normalizedId}.jsonl`);
258
+ try {
259
+ if (fs.statSync(direct).isFile()) {
260
+ this.diagnostics.available('findSessionById');
261
+ return direct;
262
+ }
263
+ }
264
+ catch {
265
+ // Continue with directory-shaped lookup for worktree layouts.
266
+ }
267
+ const match = this.findSessionsInDirectory(directory).find((candidate) => this.getSessionId(candidate) === normalizedId);
268
+ if (match) {
269
+ this.diagnostics.available('findSessionById');
270
+ return match;
271
+ }
272
+ }
273
+ this.recordHomeStatus('findSessionById');
274
+ return null;
275
+ }
276
+ catch {
277
+ this.recordHomeStatus('findSessionById');
278
+ return null;
279
+ }
280
+ }
281
+ /** Backward-compatible alias for findAllSessions. */
282
+ findSessionFiles(workspacePath) {
283
+ return this.findAllSessions(workspacePath);
284
+ }
285
+ findSessionsInDirectory(dir) {
286
+ return findSessionsInDir(dir);
287
+ }
288
+ getAllProjectFolders(workspacePath) {
289
+ return getAllProjectFoldersRaw(workspacePath);
290
+ }
291
+ listAllSessionFiles() {
292
+ const results = [];
293
+ const seen = new Set();
294
+ for (const folder of this.getAllProjectFolders()) {
295
+ // The directory listing already stat'ed each file; reuse it instead of
296
+ // stat'ing every session a second time.
297
+ for (const file of findSessionFilesWithStats(folder.dir)) {
298
+ if (seen.has(file.path))
299
+ continue;
300
+ seen.add(file.path);
301
+ results.push({ path: file.path, mtime: file.mtime, sizeBytes: file.sizeBytes });
302
+ }
303
+ }
304
+ this.recordHomeStatus('listAllSessionFiles');
305
+ return results;
306
+ }
307
+ /**
308
+ * Enumerate session files across project directories through the
309
+ * directory cache: one stat per project directory, a readdir only for
310
+ * directories whose mtime changed, and a stat only for files new to a
311
+ * changed directory (or recently modified, with `revalidateRecent`).
312
+ *
313
+ * With `limit`, directories are visited newest-mtime first and the walk
314
+ * stops once the remaining directories are older than the newest `limit`
315
+ * files found. A directory's mtime ignores appends, so a cold limited walk
316
+ * can miss a long-running session in an old directory until its next watch
317
+ * event; `since` filters files and shares the caveat.
318
+ */
319
+ async listSessionFilesAsync(workspacePath, options = {}) {
320
+ try {
321
+ return await listClaudeSessionFilesAsync(workspacePath, options, this.directoryCache);
322
+ }
323
+ finally {
324
+ this.recordHomeStatus('listSessionFilesAsync');
325
+ }
326
+ }
327
+ /**
328
+ * Resolve a recursive-watch event under the projects root with one stat.
329
+ * Sessions live exactly one directory below the root; deeper paths are
330
+ * subagent transcripts and are ignored. A path at the root itself is a
331
+ * project directory appearing or vanishing, which needs a full listing.
332
+ */
333
+ async statWatchedSessionFile(root, relativePath, workspacePath) {
334
+ const segments = splitWatchedPath(relativePath);
335
+ if (!segments)
336
+ return { status: 'unknown' };
337
+ if (segments.length > 2)
338
+ return { status: 'ignored' };
339
+ const fullPath = path.join(root, ...segments);
340
+ if (segments.length === 1) {
341
+ this.directoryCache.invalidate(fullPath);
342
+ try {
343
+ const stat = await fs.promises.stat(fullPath);
344
+ return stat.isDirectory() ? { status: 'unknown' } : { status: 'ignored' };
345
+ }
346
+ catch {
347
+ return { status: 'unknown' };
348
+ }
349
+ }
350
+ const [directoryName, fileName] = segments;
351
+ if (!this.isSessionFile(fileName))
352
+ return { status: 'ignored' };
353
+ if (!workspaceDirectoryFilter(workspacePath)(directoryName))
354
+ return { status: 'ignored' };
355
+ const sessionId = this.getSessionId(fullPath);
356
+ const stat = await this.directoryCache.refreshFile(fullPath);
357
+ if (!stat || stat.sizeBytes <= 0)
358
+ return { status: 'missing', path: fullPath, sessionId };
359
+ return {
360
+ status: 'present',
361
+ file: {
362
+ path: fullPath,
363
+ mtime: new Date(stat.mtimeMs),
364
+ sizeBytes: stat.sizeBytes,
365
+ sessionId,
366
+ },
367
+ };
368
+ }
369
+ // --- File identification ---
370
+ isSessionFile(filename) {
371
+ return filename.endsWith('.jsonl');
372
+ }
373
+ getSessionId(sessionPath) {
374
+ return path.basename(sessionPath, '.jsonl');
375
+ }
376
+ encodeWorkspacePath(workspacePath) {
377
+ return encodeWsPath(workspacePath);
378
+ }
379
+ extractSessionLabel(sessionPath) {
380
+ try {
381
+ const fd = fs.openSync(sessionPath, 'r');
382
+ const buffer = Buffer.alloc(8192);
383
+ const bytesRead = fs.readSync(fd, buffer, 0, 8192, 0);
384
+ fs.closeSync(fd);
385
+ if (bytesRead === 0)
386
+ return null;
387
+ return extractClaudeLabelFromPrefix(buffer.toString('utf-8', 0, bytesRead));
388
+ }
389
+ catch {
390
+ return null;
391
+ }
392
+ }
393
+ async extractSessionLabelsAsync(sessionPaths) {
394
+ const results = new Map();
395
+ for (const sessionPath of sessionPaths) {
396
+ let handle = null;
397
+ try {
398
+ handle = await fs.promises.open(sessionPath, 'r');
399
+ const buffer = Buffer.alloc(8192);
400
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
401
+ results.set(sessionPath, extractClaudeLabelFromPrefix(buffer.toString('utf8', 0, bytesRead)));
402
+ }
403
+ catch {
404
+ results.set(sessionPath, null);
405
+ }
406
+ finally {
407
+ await handle?.close().catch(() => undefined);
408
+ }
409
+ await new Promise((resolve) => setImmediate(resolve));
410
+ }
411
+ return results;
412
+ }
413
+ // --- Data reading ---
414
+ createReader(sessionPath) {
415
+ return new ClaudeCodeReader(sessionPath);
416
+ }
417
+ // --- Subagent support ---
418
+ scanSubagents(sessionDir, sessionId) {
419
+ return scanSubagentDir(sessionDir, sessionId);
420
+ }
421
+ // --- Cross-session search ---
422
+ searchInSession(sessionPath, query, maxResults) {
423
+ const results = [];
424
+ const queryLower = query.toLowerCase();
425
+ try {
426
+ const content = fs.readFileSync(sessionPath, 'utf8');
427
+ const lines = content.split('\n');
428
+ const projectDir = path.basename(path.dirname(sessionPath));
429
+ const projectPath = decodeEncodedPath(projectDir);
430
+ for (const line of lines) {
431
+ if (results.length >= maxResults)
432
+ break;
433
+ if (!line.trim() || !line.toLowerCase().includes(queryLower))
434
+ continue;
435
+ try {
436
+ const event = JSON.parse(line);
437
+ const text = extractSearchableText(event);
438
+ if (!text)
439
+ continue;
440
+ const textLower = text.toLowerCase();
441
+ const matchIdx = textLower.indexOf(queryLower);
442
+ if (matchIdx < 0)
443
+ continue;
444
+ const start = Math.max(0, matchIdx - 40);
445
+ const end = Math.min(text.length, matchIdx + query.length + 40);
446
+ const snippet = (start > 0 ? '...' : '') +
447
+ text.substring(start, end) +
448
+ (end < text.length ? '...' : '');
449
+ results.push({
450
+ sessionPath,
451
+ line: snippet.replace(/\n/g, ' '),
452
+ eventType: event.type || 'unknown',
453
+ timestamp: event.timestamp || '',
454
+ projectPath,
455
+ });
456
+ }
457
+ catch {
458
+ // Skip malformed JSON
459
+ }
460
+ }
461
+ }
462
+ catch {
463
+ // Skip unreadable files
464
+ }
465
+ return results;
466
+ }
467
+ getProjectsBaseDir() {
468
+ return getClaudeProjectsDir();
469
+ }
470
+ getWatchRoots() {
471
+ return [this.getProjectsBaseDir()];
472
+ }
473
+ // --- Stats ---
474
+ readSessionStats(sessionPath) {
475
+ // One reader pass through the shared aggregator: cache-inclusive per-model
476
+ // totals, cost with provenance, a tool success/failure split, and the label
477
+ // from the first user prompt in the events already read.
478
+ return readSessionFileStats(this, sessionPath);
479
+ }
480
+ readSessionContextSnapshot(sessionPath, options = {}) {
481
+ return readSessionContextSnapshot(this, sessionPath, options);
482
+ }
483
+ // --- Optional methods ---
484
+ getRuntimeStatus() {
485
+ try {
486
+ fs.accessSync(this.getProjectsBaseDir(), fs.constants.R_OK);
487
+ return { available: true, kind: 'available' };
488
+ }
489
+ catch {
490
+ return {
491
+ available: false,
492
+ kind: 'home_unavailable',
493
+ message: 'Claude Code session home is unavailable.',
494
+ };
495
+ }
496
+ }
497
+ getLastOperationStatus() {
498
+ return this.diagnostics.getLastOperationStatus();
499
+ }
500
+ getContextWindowLimit(modelId) {
501
+ if (this.dynamicContextWindowLimit)
502
+ return this.dynamicContextWindowLimit;
503
+ return getModelContextWindowSize(modelId);
504
+ }
505
+ /** Set a runtime-reported context window limit (overrides static map). */
506
+ setDynamicContextWindowLimit(limit) {
507
+ this.dynamicContextWindowLimit = limit;
508
+ }
509
+ /**
510
+ * Returns the latest assistant message's token usage snapshot.
511
+ *
512
+ * Reads the session JSONL file backwards to find the most recent assistant
513
+ * message with usage data, avoiding the need to parse the entire file.
514
+ */
515
+ getCurrentUsageSnapshot(sessionPath) {
516
+ try {
517
+ if (!fs.existsSync(sessionPath))
518
+ return null;
519
+ // Read the last portion of the file to find the most recent assistant message
520
+ const stats = fs.statSync(sessionPath);
521
+ const readSize = Math.min(stats.size, 64 * 1024); // Last 64KB
522
+ const fd = fs.openSync(sessionPath, 'r');
523
+ const buffer = Buffer.alloc(readSize);
524
+ fs.readSync(fd, buffer, 0, readSize, stats.size - readSize);
525
+ fs.closeSync(fd);
526
+ const chunk = buffer.toString('utf-8');
527
+ const lines = chunk.split('\n').reverse();
528
+ for (const line of lines) {
529
+ const trimmed = line.trim();
530
+ if (!trimmed || !trimmed.startsWith('{'))
531
+ continue;
532
+ try {
533
+ const event = JSON.parse(trimmed);
534
+ if (event.type === 'assistant' && event.message?.usage) {
535
+ const u = event.message.usage;
536
+ return {
537
+ inputTokens: u.input_tokens || 0,
538
+ outputTokens: u.output_tokens || 0,
539
+ cacheWriteTokens: u.cache_creation_input_tokens || 0,
540
+ cacheReadTokens: u.cache_read_input_tokens || 0,
541
+ model: event.message.model || 'unknown',
542
+ timestamp: new Date(event.timestamp || Date.now()),
543
+ reportedCost: u.reported_cost,
544
+ };
545
+ }
546
+ }
547
+ catch {
548
+ // Skip malformed lines
549
+ }
550
+ }
551
+ return null;
552
+ }
553
+ catch {
554
+ return null;
555
+ }
556
+ }
557
+ // --- Lifecycle ---
558
+ dispose() {
559
+ this.dynamicContextWindowLimit = null;
560
+ this.directoryCache.invalidate();
561
+ }
562
+ recordHomeStatus(operation) {
563
+ const status = this.getRuntimeStatus();
564
+ if (status.available) {
565
+ this.diagnostics.available(operation);
566
+ return;
567
+ }
568
+ this.diagnostics.degraded(operation, status, {
569
+ kind: 'home_unavailable',
570
+ severity: 'info',
571
+ phase: 'enumerate',
572
+ message: status.message ?? 'Claude Code session home is unavailable.',
573
+ });
574
+ }
575
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Claude Code session-file listing, kept apart from the provider class so a
3
+ * caller that only lists files does not bundle the provider's stats,
4
+ * context and transcript code.
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { getClaudeProjectsDir } from '../claudePaths';
9
+ import { encodeWorkspacePath as encodeWsPath } from '../parsers/sessionPathResolver';
10
+ import { DirectoryListingCache } from './directoryListingCache';
11
+ /** Matches project directory names that may hold sessions for `workspacePath`. */
12
+ export function workspaceDirectoryFilter(workspacePath) {
13
+ if (!workspacePath)
14
+ return () => true;
15
+ const encodedWorkspace = encodeWsPath(workspacePath).toLowerCase();
16
+ const workspaceBasename = path
17
+ .basename(workspacePath)
18
+ .replace(/[^a-zA-Z0-9]/g, '-')
19
+ .toLowerCase();
20
+ return (directoryName) => {
21
+ const name = directoryName.toLowerCase();
22
+ return (name === encodedWorkspace ||
23
+ name.startsWith(`${encodedWorkspace}-`) ||
24
+ name === workspaceBasename ||
25
+ name.endsWith(`-${workspaceBasename}`));
26
+ };
27
+ }
28
+ /**
29
+ * Enumerate session files across project directories through the
30
+ * directory cache: one stat per project directory, a readdir only for
31
+ * directories whose mtime changed, and a stat only for files new to a
32
+ * changed directory (or recently modified, with `revalidateRecent`).
33
+ *
34
+ * With `limit`, directories are visited newest-mtime first and the walk
35
+ * stops once the remaining directories are older than the newest `limit`
36
+ * files found. A directory's mtime ignores appends, so a cold limited walk
37
+ * can miss a long-running session in an old directory until its next watch
38
+ * event; `since` filters files and shares the caveat.
39
+ */
40
+ export async function listClaudeSessionFilesAsync(workspacePath, options = {}, cache = new DirectoryListingCache()) {
41
+ const projectsRoot = getClaudeProjectsDir();
42
+ if (options.stats) {
43
+ options.stats.directoriesListed ??= 0;
44
+ options.stats.directoriesStatted ??= 0;
45
+ options.stats.filesStatted ??= 0;
46
+ }
47
+ const root = await cache.listDirectory(projectsRoot, () => false, {
48
+ revalidateRecent: options.revalidateRecent,
49
+ stats: options.stats,
50
+ });
51
+ if (!root) {
52
+ return [];
53
+ }
54
+ const limit = options.limit !== undefined && Number.isFinite(options.limit)
55
+ ? Math.max(0, Math.floor(options.limit))
56
+ : null;
57
+ if (limit === 0) {
58
+ return [];
59
+ }
60
+ const since = options.since !== undefined && Number.isFinite(options.since) ? options.since : null;
61
+ const matchesWorkspace = workspaceDirectoryFilter(workspacePath);
62
+ const candidates = [];
63
+ let sinceYield = 0;
64
+ for (const name of root.subdirectories) {
65
+ if (!matchesWorkspace(name))
66
+ continue;
67
+ const directory = path.join(projectsRoot, name);
68
+ try {
69
+ const stat = await fs.promises.stat(directory);
70
+ if (options.stats) {
71
+ options.stats.directoriesStatted = (options.stats.directoriesStatted ?? 0) + 1;
72
+ }
73
+ if (stat.isDirectory())
74
+ candidates.push({ directory, stat });
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ if (++sinceYield >= 50) {
80
+ sinceYield = 0;
81
+ await new Promise((resolve) => setImmediate(resolve));
82
+ }
83
+ }
84
+ if (limit !== null)
85
+ candidates.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
86
+ const byNewest = (left, right) => right.mtime.getTime() - left.mtime.getTime();
87
+ const results = [];
88
+ let cutoffMs = null;
89
+ for (const candidate of candidates) {
90
+ if (limit !== null && cutoffMs !== null && candidate.stat.mtimeMs < cutoffMs)
91
+ break;
92
+ const listing = await cache.listDirectory(candidate.directory, (name) => name.endsWith('.jsonl'), {
93
+ dirStat: candidate.stat,
94
+ revalidateRecent: options.revalidateRecent,
95
+ stats: options.stats,
96
+ });
97
+ if (!listing)
98
+ continue;
99
+ for (const [name, stat] of listing.files) {
100
+ if (stat.sizeBytes <= 0)
101
+ continue;
102
+ if (since !== null && stat.mtimeMs < since)
103
+ continue;
104
+ const sessionPath = path.join(candidate.directory, name);
105
+ results.push({
106
+ path: sessionPath,
107
+ mtime: new Date(stat.mtimeMs),
108
+ sizeBytes: stat.sizeBytes,
109
+ sessionId: path.basename(sessionPath, '.jsonl'),
110
+ });
111
+ }
112
+ if (limit !== null && results.length >= limit) {
113
+ results.sort(byNewest);
114
+ results.length = limit;
115
+ cutoffMs = results[limit - 1].mtime.getTime();
116
+ }
117
+ await new Promise((resolve) => setImmediate(resolve));
118
+ }
119
+ results.sort(byNewest);
120
+ return limit !== null && results.length > limit ? results.slice(0, limit) : results;
121
+ }