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,713 @@
1
+ /**
2
+ * @fileoverview Session path resolution for Claude Code projects.
3
+ *
4
+ * This module provides utilities for locating Claude Code session files
5
+ * in ~/.claude/projects/. Claude Code encodes workspace paths by replacing
6
+ * slashes with hyphens, e.g., /home/user/code/project -> home-user-code-project.
7
+ *
8
+ * Session files are stored as [session-uuid].jsonl in the encoded directory.
9
+ *
10
+ * @module sidekick-shared/parsers/sessionPathResolver
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+ import { getClaudeProjectsDir } from '../claudePaths';
16
+ /**
17
+ * Encodes a workspace path to Claude Code's on-disk directory naming scheme
18
+ * under `~/.claude/projects/` — every non-ASCII-alphanumeric character
19
+ * (separators, colons, underscores, dots, spaces) becomes a hyphen, and case
20
+ * is preserved. Exported from the package root as `encodeClaudeWorkspacePath`.
21
+ *
22
+ * Not to be confused with the top-level `encodeWorkspacePath` export from
23
+ * `paths.ts`: that one is Sidekick's own config-store slug (it keeps dots) and
24
+ * does NOT match Claude Code's scheme — using it to build a
25
+ * `~/.claude/projects/` path silently misses for any workspace containing a
26
+ * dot or space. For lookups, prefer `getClaudeSessionDirectory` or the
27
+ * mismatch-tolerant `discoverSessionDirectory` over raw string encoding.
28
+ *
29
+ * @param workspacePath - Absolute path to workspace directory
30
+ * @returns Encoded path string (e.g., "-home-user-code-project")
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * encodeWorkspacePath('/home/user/code/my_project');
35
+ * // => "-home-user-code-my-project"
36
+ *
37
+ * encodeWorkspacePath('C:\\Users\\user\\code\\my_project'); // Windows
38
+ * // => "C--Users-user-code-my-project"
39
+ * ```
40
+ */
41
+ export function encodeWorkspacePath(workspacePath) {
42
+ // Normalize path separators to forward slash
43
+ const normalized = workspacePath.replace(/\\/g, '/');
44
+ // Claude Code replaces every non-alphanumeric path character, including
45
+ // dots and spaces, with a hyphen.
46
+ return normalized.replace(/[^a-zA-Z0-9]/g, '-');
47
+ }
48
+ /**
49
+ * Gets the session directory path for a workspace.
50
+ *
51
+ * Returns the directory where Claude Code stores session files
52
+ * for the given workspace, even if the directory doesn't exist.
53
+ *
54
+ * @param workspacePath - Absolute path to workspace directory
55
+ * @returns Absolute path to session directory
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * getSessionDirectory('/home/user/code/project');
60
+ * // => "/home/user/.claude/projects/-home-user-code-project"
61
+ * ```
62
+ */
63
+ export function getSessionDirectory(workspacePath) {
64
+ const encoded = encodeWorkspacePath(workspacePath);
65
+ return path.join(getClaudeProjectsDir(), encoded);
66
+ }
67
+ /** How recently a file must be modified to be considered "active" (5 minutes) */
68
+ const ACTIVE_SESSION_THRESHOLD_MS = 5 * 60 * 1000;
69
+ /**
70
+ * Finds session directories for Claude Code sessions started from subdirectories
71
+ * of the given workspace path.
72
+ *
73
+ * When VS Code workspace is `/home/user/project` but Claude Code starts from
74
+ * a subdirectory like `/home/user/project/packages/app`, the session goes to
75
+ * `~/.claude/projects/-home-user-project-packages-app/` which we need to find.
76
+ *
77
+ * @param workspacePath - Absolute path to workspace directory
78
+ * @returns Array of matching session directory paths
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // Workspace: /home/user/project
83
+ * // Claude started from: /home/user/project/packages/app
84
+ * findSubdirectorySessionDirs('/home/user/project');
85
+ * // => ["/home/user/.claude/projects/-home-user-project-packages-app"]
86
+ * ```
87
+ */
88
+ export function findSubdirectorySessionDirs(workspacePath) {
89
+ const projectsDir = getClaudeProjectsDir();
90
+ try {
91
+ if (!fs.existsSync(projectsDir)) {
92
+ return [];
93
+ }
94
+ // Encode workspace path to get the prefix to match
95
+ const encodedPrefix = encodeWorkspacePath(workspacePath).toLowerCase();
96
+ // Get all directories in projects folder
97
+ const allDirs = fs.readdirSync(projectsDir).filter((name) => {
98
+ const fullPath = path.join(projectsDir, name);
99
+ try {
100
+ return fs.statSync(fullPath).isDirectory();
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ });
106
+ // Find directories that start with the encoded workspace prefix followed by a hyphen
107
+ // The hyphen requirement prevents /project matching /project-v2
108
+ const matches = [];
109
+ for (const dir of allDirs) {
110
+ const dirLower = dir.toLowerCase();
111
+ // Must start with prefix AND have a hyphen after (indicating subdirectory)
112
+ if (dirLower.startsWith(encodedPrefix + '-')) {
113
+ matches.push(path.join(projectsDir, dir));
114
+ }
115
+ }
116
+ return matches;
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ }
122
+ /**
123
+ * Gets the most recently active session directory from a list of directories.
124
+ *
125
+ * For each directory, finds the most recent .jsonl file by modification time,
126
+ * then returns the directory containing the overall most recent session.
127
+ *
128
+ * @param sessionDirs - Array of session directory paths to check
129
+ * @returns Path to most recently active directory, or null if none have sessions
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * getMostRecentlyActiveSessionDir([
134
+ * '/home/user/.claude/projects/-home-user-project-packages-app',
135
+ * '/home/user/.claude/projects/-home-user-project-packages-lib'
136
+ * ]);
137
+ * // => "/home/user/.claude/projects/-home-user-project-packages-app"
138
+ * ```
139
+ */
140
+ export function getMostRecentlyActiveSessionDir(sessionDirs) {
141
+ let mostRecentDir = null;
142
+ let mostRecentMtime = 0;
143
+ for (const dir of sessionDirs) {
144
+ try {
145
+ // Find most recent .jsonl file in this directory
146
+ const files = fs.readdirSync(dir).filter((file) => file.endsWith('.jsonl'));
147
+ for (const file of files) {
148
+ try {
149
+ const fullPath = path.join(dir, file);
150
+ const stats = fs.statSync(fullPath);
151
+ // Only consider non-empty files
152
+ if (stats.size > 0 && stats.mtime.getTime() > mostRecentMtime) {
153
+ mostRecentMtime = stats.mtime.getTime();
154
+ mostRecentDir = dir;
155
+ }
156
+ }
157
+ catch {
158
+ // Skip files we can't stat
159
+ }
160
+ }
161
+ }
162
+ catch {
163
+ // Skip directories we can't read
164
+ }
165
+ }
166
+ return mostRecentDir;
167
+ }
168
+ /**
169
+ * Discovers the session directory for a workspace by trying multiple strategies.
170
+ *
171
+ * Strategy order:
172
+ * 1. Try the computed encoded path (fast, works if our encoding matches Claude Code's)
173
+ * 2. Scan ~/.claude/projects/ for directories matching the workspace name
174
+ * 3. Scan temp directory for Claude scratchpad directories to find actual encoding
175
+ *
176
+ * @param workspacePath - Absolute path to workspace directory
177
+ * @returns Absolute path to session directory, or null if not found
178
+ */
179
+ export function discoverSessionDirectory(workspacePath) {
180
+ const projectsDir = getClaudeProjectsDir();
181
+ // Strategy 1: Try computed encoded path (exact match)
182
+ const computedDir = getSessionDirectory(workspacePath);
183
+ if (fs.existsSync(computedDir)) {
184
+ return computedDir;
185
+ }
186
+ // Strategy 1.5: Check for subdirectory sessions
187
+ // When Claude Code starts from a subdirectory of the workspace, the session
188
+ // is stored in a directory matching the subdirectory path. We find the most
189
+ // recently active one.
190
+ const subdirMatches = findSubdirectorySessionDirs(workspacePath);
191
+ if (subdirMatches.length > 0) {
192
+ const mostRecent = getMostRecentlyActiveSessionDir(subdirMatches);
193
+ if (mostRecent) {
194
+ return mostRecent;
195
+ }
196
+ }
197
+ // Strategy 2: Scan ~/.claude/projects/ for matching directories
198
+ try {
199
+ if (fs.existsSync(projectsDir)) {
200
+ const existingDirs = fs.readdirSync(projectsDir).filter((name) => {
201
+ const fullPath = path.join(projectsDir, name);
202
+ try {
203
+ return fs.statSync(fullPath).isDirectory();
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ });
209
+ // Try to match by workspace path components
210
+ // Normalize workspace path for comparison
211
+ const normalizedWorkspace = encodeWorkspacePath(workspacePath).toLowerCase();
212
+ for (const dir of existingDirs) {
213
+ // Check if the directory name matches (case-insensitive)
214
+ if (dir.toLowerCase() === normalizedWorkspace) {
215
+ return path.join(projectsDir, dir);
216
+ }
217
+ }
218
+ // Fallback: match by final path component (project name)
219
+ const workspaceBasename = path
220
+ .basename(workspacePath)
221
+ .replace(/[^a-zA-Z0-9]/g, '-')
222
+ .toLowerCase();
223
+ for (const dir of existingDirs) {
224
+ const dirLower = dir.toLowerCase();
225
+ // Check if dir ends with the project name
226
+ if (dirLower.endsWith('-' + workspaceBasename) || dirLower === workspaceBasename) {
227
+ return path.join(projectsDir, dir);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ catch {
233
+ // Ignore errors during discovery
234
+ }
235
+ // Strategy 3: Check temp directory for Claude scratchpad directories
236
+ // Claude creates: <tmpdir>/claude/<encoded-workspace>/<session-uuid>/scratchpad
237
+ try {
238
+ const claudeTempDir = path.join(os.tmpdir(), 'claude');
239
+ if (fs.existsSync(claudeTempDir)) {
240
+ const tempDirs = fs.readdirSync(claudeTempDir).filter((name) => {
241
+ const fullPath = path.join(claudeTempDir, name);
242
+ try {
243
+ return fs.statSync(fullPath).isDirectory();
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ });
249
+ // Match by workspace basename
250
+ const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
251
+ for (const encodedDir of tempDirs) {
252
+ const encodedLower = encodedDir.toLowerCase();
253
+ if (encodedLower.endsWith('-' + workspaceBasename) || encodedLower === workspaceBasename) {
254
+ // Found a match in temp - use this encoding for the session directory
255
+ const sessionDir = path.join(projectsDir, encodedDir);
256
+ if (fs.existsSync(sessionDir)) {
257
+ return sessionDir;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ catch {
264
+ // Ignore errors during temp directory scan
265
+ }
266
+ return null;
267
+ }
268
+ /**
269
+ * Finds the most recently modified session file for a workspace.
270
+ *
271
+ * Prioritizes "active" sessions (modified within last 5 minutes) over
272
+ * stale ones. This helps select the right session when multiple exist.
273
+ *
274
+ * @param workspacePath - Absolute path to workspace directory
275
+ * @returns Path to active session file, or null if none exists
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * const sessionPath = findActiveSession('/home/user/code/project');
280
+ * if (sessionPath) {
281
+ * console.log('Active session:', sessionPath);
282
+ * } else {
283
+ * console.log('No active Claude Code session for this workspace');
284
+ * }
285
+ * ```
286
+ */
287
+ export function findActiveSession(workspacePath) {
288
+ // Use discovery to find the session directory (handles encoding differences)
289
+ const sessionDir = discoverSessionDirectory(workspacePath);
290
+ try {
291
+ // Check if directory was found
292
+ if (!sessionDir) {
293
+ return null;
294
+ }
295
+ const now = Date.now();
296
+ // Find all .jsonl files with stats
297
+ const files = fs
298
+ .readdirSync(sessionDir)
299
+ .filter((file) => file.endsWith('.jsonl'))
300
+ .map((file) => {
301
+ const fullPath = path.join(sessionDir, file);
302
+ const stats = fs.statSync(fullPath);
303
+ const mtime = stats.mtime.getTime();
304
+ return {
305
+ path: fullPath,
306
+ mtime,
307
+ size: stats.size,
308
+ isActive: now - mtime < ACTIVE_SESSION_THRESHOLD_MS,
309
+ };
310
+ })
311
+ // Filter out empty files
312
+ .filter((file) => file.size > 0);
313
+ // Return null if no session files
314
+ if (files.length === 0) {
315
+ return null;
316
+ }
317
+ // Prefer active sessions, then sort by modification time
318
+ files.sort((a, b) => {
319
+ // Active sessions first
320
+ if (a.isActive && !b.isActive)
321
+ return -1;
322
+ if (!a.isActive && b.isActive)
323
+ return 1;
324
+ // Then by modification time (most recent first)
325
+ return b.mtime - a.mtime;
326
+ });
327
+ return files[0].path;
328
+ }
329
+ catch (error) {
330
+ // Handle errors gracefully - missing directory, permission issues, etc.
331
+ console.error('Error finding active session:', error);
332
+ return null;
333
+ }
334
+ }
335
+ /**
336
+ * Finds all session files for a workspace.
337
+ *
338
+ * Returns paths to all .jsonl session files for the workspace,
339
+ * sorted by modification time (most recent first). Useful for
340
+ * session history features.
341
+ *
342
+ * @param workspacePath - Absolute path to workspace directory
343
+ * @returns Array of session file paths (empty if none exist)
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const sessions = findAllSessions('/home/user/code/project');
348
+ * console.log(`Found ${sessions.length} session(s)`);
349
+ * sessions.forEach(session => console.log(session));
350
+ * ```
351
+ */
352
+ export function findAllSessions(workspacePath) {
353
+ // Use discovery to find the session directory (handles encoding differences)
354
+ const sessionDir = discoverSessionDirectory(workspacePath);
355
+ try {
356
+ // Check if directory was found
357
+ if (!sessionDir) {
358
+ return [];
359
+ }
360
+ // Find and sort all .jsonl files
361
+ const files = fs
362
+ .readdirSync(sessionDir)
363
+ .filter((file) => file.endsWith('.jsonl'))
364
+ .map((file) => {
365
+ const fullPath = path.join(sessionDir, file);
366
+ try {
367
+ const stats = fs.statSync(fullPath);
368
+ return {
369
+ path: fullPath,
370
+ mtime: stats.mtime.getTime(),
371
+ };
372
+ }
373
+ catch {
374
+ return null;
375
+ }
376
+ })
377
+ .filter((file) => file !== null)
378
+ .sort((a, b) => b.mtime - a.mtime)
379
+ .map((file) => file.path);
380
+ return files;
381
+ }
382
+ catch (error) {
383
+ // Handle errors gracefully
384
+ console.error('Error finding sessions:', error);
385
+ return [];
386
+ }
387
+ }
388
+ /**
389
+ * Decodes an encoded directory name back to a human-readable path.
390
+ *
391
+ * Claude Code encodes paths by replacing slashes, colons, and underscores with hyphens.
392
+ * This function attempts to reverse that encoding. Note that some ambiguity exists
393
+ * (e.g., was it a slash or underscore?), so we default to slashes for path separators.
394
+ *
395
+ * @param encoded - Encoded directory name (e.g., "-home-user-project")
396
+ * @returns Decoded path (e.g., "/home/user/project")
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * decodeEncodedPath('-home-user-code-project');
401
+ * // => "/home/user/code/project"
402
+ *
403
+ * decodeEncodedPath('C--Users-user-code');
404
+ * // => "C:/Users/user/code"
405
+ * ```
406
+ */
407
+ export function decodeEncodedPath(encoded) {
408
+ // Handle Windows paths (start with drive letter like "C-")
409
+ const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)/);
410
+ if (windowsDriveMatch) {
411
+ const drive = windowsDriveMatch[1];
412
+ const rest = windowsDriveMatch[2];
413
+ // Replace single hyphens with slashes (path separators)
414
+ const decoded = rest.replace(/-/g, '/');
415
+ return `${drive}:/${decoded}`;
416
+ }
417
+ // Handle Unix paths (start with hyphen representing root /)
418
+ if (encoded.startsWith('-')) {
419
+ // Remove leading hyphen, replace remaining hyphens with slashes
420
+ return '/' + encoded.substring(1).replace(/-/g, '/');
421
+ }
422
+ // Fallback: just replace hyphens with slashes
423
+ return encoded.replace(/-/g, '/');
424
+ }
425
+ /**
426
+ * Gets all project folders from ~/.claude/projects/.
427
+ *
428
+ * Returns information about every project directory Claude Code has created,
429
+ * sorted by priority:
430
+ * 1. Exact workspace match (if workspacePath provided)
431
+ * 2. Subdirectories of workspace (if workspacePath provided)
432
+ * 3. Most recently active (based on session file modification times)
433
+ *
434
+ * @param workspacePath - Optional workspace path to prioritize in sorting
435
+ * @returns Array of project folder info, sorted by priority
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const folders = getAllProjectFolders('/home/user/project');
440
+ * // => [
441
+ * // { dir: '/home/user/.claude/projects/-home-user-project', // exact match first
442
+ * // encodedName: '-home-user-project',
443
+ * // name: '/home/user/project',
444
+ * // sessionCount: 3,
445
+ * // lastModified: Date },
446
+ * // { dir: '/home/user/.claude/projects/-home-user-project-subdir', // subdirs next
447
+ * // ... },
448
+ * // ... // then others by recency
449
+ * // ]
450
+ * ```
451
+ */
452
+ export function getAllProjectFolders(workspacePath) {
453
+ const projectsDir = getClaudeProjectsDir();
454
+ const folders = [];
455
+ try {
456
+ if (!fs.existsSync(projectsDir)) {
457
+ return [];
458
+ }
459
+ const entries = fs.readdirSync(projectsDir);
460
+ for (const entry of entries) {
461
+ const fullPath = path.join(projectsDir, entry);
462
+ try {
463
+ const stats = fs.statSync(fullPath);
464
+ if (!stats.isDirectory()) {
465
+ continue;
466
+ }
467
+ // Count session files and find most recent modification
468
+ const sessionFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.jsonl'));
469
+ let lastModified = stats.mtime;
470
+ let sessionCount = 0;
471
+ for (const sessionFile of sessionFiles) {
472
+ try {
473
+ const sessionPath = path.join(fullPath, sessionFile);
474
+ const sessionStats = fs.statSync(sessionPath);
475
+ if (sessionStats.size > 0) {
476
+ sessionCount++;
477
+ if (sessionStats.mtime > lastModified) {
478
+ lastModified = sessionStats.mtime;
479
+ }
480
+ }
481
+ }
482
+ catch {
483
+ // Skip files we can't stat
484
+ }
485
+ }
486
+ folders.push({
487
+ dir: fullPath,
488
+ encodedName: entry,
489
+ name: decodeEncodedPath(entry),
490
+ sessionCount,
491
+ lastModified,
492
+ });
493
+ }
494
+ catch {
495
+ // Skip directories we can't read
496
+ }
497
+ }
498
+ // Sort with workspace prioritization
499
+ // Use encoded names for comparison (decoded paths are lossy — hyphens in
500
+ // directory names become indistinguishable from path separators)
501
+ folders.sort((a, b) => {
502
+ if (workspacePath) {
503
+ const encodedWorkspace = encodeWorkspacePath(workspacePath).toLowerCase();
504
+ const aEncoded = a.encodedName.toLowerCase();
505
+ const bEncoded = b.encodedName.toLowerCase();
506
+ // Priority 1: Exact workspace match comes first
507
+ const aIsExact = aEncoded === encodedWorkspace;
508
+ const bIsExact = bEncoded === encodedWorkspace;
509
+ if (aIsExact && !bIsExact)
510
+ return -1;
511
+ if (!aIsExact && bIsExact)
512
+ return 1;
513
+ // Priority 2: Subdirectories of workspace come next
514
+ const aIsSubdir = aEncoded.startsWith(encodedWorkspace + '-');
515
+ const bIsSubdir = bEncoded.startsWith(encodedWorkspace + '-');
516
+ if (aIsSubdir && !bIsSubdir)
517
+ return -1;
518
+ if (!aIsSubdir && bIsSubdir)
519
+ return 1;
520
+ }
521
+ // Default: sort by most recently active
522
+ return b.lastModified.getTime() - a.lastModified.getTime();
523
+ });
524
+ return folders;
525
+ }
526
+ catch {
527
+ return [];
528
+ }
529
+ }
530
+ /**
531
+ * Finds all session files in a specific directory.
532
+ *
533
+ * Unlike findAllSessions which uses workspace-based discovery, this function
534
+ * accepts a direct path to a session directory and returns all .jsonl files.
535
+ *
536
+ * @param sessionDir - Absolute path to a session directory
537
+ * @returns Array of session file paths, sorted by modification time (most recent first)
538
+ *
539
+ * @example
540
+ * ```typescript
541
+ * findSessionsInDirectory('/home/user/.claude/projects/-home-user-project');
542
+ * // => ['/home/user/.claude/projects/-home-user-project/abc123.jsonl', ...]
543
+ * ```
544
+ */
545
+ export function findSessionsInDirectory(sessionDir) {
546
+ return findSessionFilesWithStats(sessionDir).map((file) => file.path);
547
+ }
548
+ /**
549
+ * The non-empty `.jsonl` files in a session directory with the stat each one
550
+ * already cost, newest first, so callers that need the mtime do not stat the
551
+ * same files a second time.
552
+ */
553
+ export function findSessionFilesWithStats(sessionDir) {
554
+ try {
555
+ if (!fs.existsSync(sessionDir)) {
556
+ return [];
557
+ }
558
+ return fs
559
+ .readdirSync(sessionDir)
560
+ .filter((file) => file.endsWith('.jsonl'))
561
+ .map((file) => {
562
+ const fullPath = path.join(sessionDir, file);
563
+ try {
564
+ const stats = fs.statSync(fullPath);
565
+ return { path: fullPath, mtime: stats.mtime, sizeBytes: stats.size };
566
+ }
567
+ catch {
568
+ return null;
569
+ }
570
+ })
571
+ .filter((f) => f !== null && f.sizeBytes > 0)
572
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
573
+ }
574
+ catch {
575
+ return [];
576
+ }
577
+ }
578
+ // ═══════════════════════════════════════════════════════════════════════
579
+ // Multi-worktree session discovery
580
+ // ═══════════════════════════════════════════════════════════════════════
581
+ /**
582
+ * Detects if a workspace is a git worktree and resolves the main repo path.
583
+ *
584
+ * Git worktrees have a .git file (not directory) containing a gitdir: pointer.
585
+ * This function reads that file and resolves the main repo path.
586
+ *
587
+ * @param workspacePath - Absolute path to workspace directory
588
+ * @returns Main repository path, or null if not a worktree
589
+ */
590
+ export function resolveWorktreeMainRepo(workspacePath) {
591
+ const gitPath = path.join(workspacePath, '.git');
592
+ try {
593
+ const stat = fs.statSync(gitPath);
594
+ if (stat.isDirectory()) {
595
+ // Regular repo, not a worktree
596
+ return null;
597
+ }
598
+ // It's a file — this is a worktree
599
+ const content = fs.readFileSync(gitPath, 'utf-8').trim();
600
+ const match = content.match(/^gitdir:\s*(.+)$/m);
601
+ if (!match)
602
+ return null;
603
+ const gitdir = match[1].trim();
604
+ // gitdir points to something like /main-repo/.git/worktrees/<name>
605
+ // Resolve to absolute path
606
+ const resolvedGitdir = path.isAbsolute(gitdir) ? gitdir : path.resolve(workspacePath, gitdir);
607
+ // Navigate up from .git/worktrees/<name> to the main repo
608
+ const worktreesDir = path.dirname(resolvedGitdir);
609
+ if (path.basename(worktreesDir) !== 'worktrees')
610
+ return null;
611
+ const dotGit = path.dirname(worktreesDir);
612
+ if (path.basename(dotGit) !== '.git')
613
+ return null;
614
+ return path.dirname(dotGit);
615
+ }
616
+ catch {
617
+ return null;
618
+ }
619
+ }
620
+ /**
621
+ * Discovers all worktree sibling paths for a main repository.
622
+ *
623
+ * Reads .git/worktrees/{name}/gitdir to find all worktree paths.
624
+ *
625
+ * @param mainRepoPath - Absolute path to the main repository
626
+ * @returns Array of absolute paths to worktree working directories
627
+ */
628
+ export function discoverWorktreeSiblings(mainRepoPath) {
629
+ const worktreesDir = path.join(mainRepoPath, '.git', 'worktrees');
630
+ const siblings = [];
631
+ try {
632
+ if (!fs.existsSync(worktreesDir))
633
+ return siblings;
634
+ const entries = fs.readdirSync(worktreesDir);
635
+ for (const entry of entries) {
636
+ const gitdirFile = path.join(worktreesDir, entry, 'gitdir');
637
+ try {
638
+ const content = fs.readFileSync(gitdirFile, 'utf-8').trim();
639
+ // gitdir file contains path to the worktree's .git file
640
+ // which is the worktree working directory + /.git
641
+ const worktreeGit = path.isAbsolute(content)
642
+ ? content
643
+ : path.resolve(worktreesDir, entry, content);
644
+ const worktreeDir = path.dirname(worktreeGit);
645
+ if (fs.existsSync(worktreeDir)) {
646
+ siblings.push(worktreeDir);
647
+ }
648
+ }
649
+ catch {
650
+ // Skip entries without gitdir
651
+ }
652
+ }
653
+ }
654
+ catch {
655
+ // worktrees dir doesn't exist or isn't readable
656
+ }
657
+ return siblings;
658
+ }
659
+ /**
660
+ * Finds all sessions across the main repo and all its worktrees.
661
+ *
662
+ * When monitoring from a worktree, sessions started from the main repo
663
+ * or other worktrees are normally invisible. This function consolidates
664
+ * sessions from all related workspaces.
665
+ *
666
+ * @param workspacePath - Absolute path to workspace directory (may be worktree or main repo)
667
+ * @returns Sorted array of session file paths from all related workspaces
668
+ */
669
+ export function findAllSessionsWithWorktrees(workspacePath) {
670
+ const allPaths = new Set();
671
+ // Start with sessions from the given workspace
672
+ for (const s of findAllSessions(workspacePath)) {
673
+ allPaths.add(s);
674
+ }
675
+ // Check if this is a worktree
676
+ const mainRepo = resolveWorktreeMainRepo(workspacePath);
677
+ if (mainRepo) {
678
+ // Add sessions from the main repo
679
+ for (const s of findAllSessions(mainRepo)) {
680
+ allPaths.add(s);
681
+ }
682
+ // Add sessions from sibling worktrees
683
+ for (const sibling of discoverWorktreeSiblings(mainRepo)) {
684
+ if (sibling !== workspacePath) {
685
+ for (const s of findAllSessions(sibling)) {
686
+ allPaths.add(s);
687
+ }
688
+ }
689
+ }
690
+ }
691
+ else {
692
+ // This might be the main repo — check for worktrees
693
+ const siblings = discoverWorktreeSiblings(workspacePath);
694
+ for (const sibling of siblings) {
695
+ for (const s of findAllSessions(sibling)) {
696
+ allPaths.add(s);
697
+ }
698
+ }
699
+ }
700
+ // Sort by mtime (most recent first)
701
+ return Array.from(allPaths)
702
+ .map((sessionPath) => {
703
+ try {
704
+ const stat = fs.statSync(sessionPath);
705
+ return { path: sessionPath, mtime: stat.mtime.getTime() };
706
+ }
707
+ catch {
708
+ return { path: sessionPath, mtime: 0 };
709
+ }
710
+ })
711
+ .sort((a, b) => b.mtime - a.mtime)
712
+ .map((f) => f.path);
713
+ }