cccmemory 1.8.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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/ConversationMemory.d.ts +231 -0
  4. package/dist/ConversationMemory.d.ts.map +1 -0
  5. package/dist/ConversationMemory.js +357 -0
  6. package/dist/ConversationMemory.js.map +1 -0
  7. package/dist/cache/QueryCache.d.ts +215 -0
  8. package/dist/cache/QueryCache.d.ts.map +1 -0
  9. package/dist/cache/QueryCache.js +294 -0
  10. package/dist/cache/QueryCache.js.map +1 -0
  11. package/dist/cli/commands.d.ts +9 -0
  12. package/dist/cli/commands.d.ts.map +1 -0
  13. package/dist/cli/commands.js +954 -0
  14. package/dist/cli/commands.js.map +1 -0
  15. package/dist/cli/help.d.ts +16 -0
  16. package/dist/cli/help.d.ts.map +1 -0
  17. package/dist/cli/help.js +361 -0
  18. package/dist/cli/help.js.map +1 -0
  19. package/dist/cli/index.d.ts +30 -0
  20. package/dist/cli/index.d.ts.map +1 -0
  21. package/dist/cli/index.js +111 -0
  22. package/dist/cli/index.js.map +1 -0
  23. package/dist/context/ContextInjector.d.ts +38 -0
  24. package/dist/context/ContextInjector.d.ts.map +1 -0
  25. package/dist/context/ContextInjector.js +235 -0
  26. package/dist/context/ContextInjector.js.map +1 -0
  27. package/dist/documentation/CodeAnalyzer.d.ts +29 -0
  28. package/dist/documentation/CodeAnalyzer.d.ts.map +1 -0
  29. package/dist/documentation/CodeAnalyzer.js +122 -0
  30. package/dist/documentation/CodeAnalyzer.js.map +1 -0
  31. package/dist/documentation/ConversationAnalyzer.d.ts +19 -0
  32. package/dist/documentation/ConversationAnalyzer.d.ts.map +1 -0
  33. package/dist/documentation/ConversationAnalyzer.js +157 -0
  34. package/dist/documentation/ConversationAnalyzer.js.map +1 -0
  35. package/dist/documentation/CrossReferencer.d.ts +67 -0
  36. package/dist/documentation/CrossReferencer.d.ts.map +1 -0
  37. package/dist/documentation/CrossReferencer.js +247 -0
  38. package/dist/documentation/CrossReferencer.js.map +1 -0
  39. package/dist/documentation/DocumentationGenerator.d.ts +22 -0
  40. package/dist/documentation/DocumentationGenerator.d.ts.map +1 -0
  41. package/dist/documentation/DocumentationGenerator.js +57 -0
  42. package/dist/documentation/DocumentationGenerator.js.map +1 -0
  43. package/dist/documentation/MarkdownFormatter.d.ts +26 -0
  44. package/dist/documentation/MarkdownFormatter.d.ts.map +1 -0
  45. package/dist/documentation/MarkdownFormatter.js +301 -0
  46. package/dist/documentation/MarkdownFormatter.js.map +1 -0
  47. package/dist/documentation/types.d.ts +176 -0
  48. package/dist/documentation/types.d.ts.map +1 -0
  49. package/dist/documentation/types.js +5 -0
  50. package/dist/documentation/types.js.map +1 -0
  51. package/dist/embeddings/ConfigManager.d.ts +46 -0
  52. package/dist/embeddings/ConfigManager.d.ts.map +1 -0
  53. package/dist/embeddings/ConfigManager.js +177 -0
  54. package/dist/embeddings/ConfigManager.js.map +1 -0
  55. package/dist/embeddings/EmbeddingConfig.d.ts +39 -0
  56. package/dist/embeddings/EmbeddingConfig.d.ts.map +1 -0
  57. package/dist/embeddings/EmbeddingConfig.js +132 -0
  58. package/dist/embeddings/EmbeddingConfig.js.map +1 -0
  59. package/dist/embeddings/EmbeddingGenerator.d.ts +51 -0
  60. package/dist/embeddings/EmbeddingGenerator.d.ts.map +1 -0
  61. package/dist/embeddings/EmbeddingGenerator.js +157 -0
  62. package/dist/embeddings/EmbeddingGenerator.js.map +1 -0
  63. package/dist/embeddings/EmbeddingProvider.d.ts +34 -0
  64. package/dist/embeddings/EmbeddingProvider.d.ts.map +1 -0
  65. package/dist/embeddings/EmbeddingProvider.js +6 -0
  66. package/dist/embeddings/EmbeddingProvider.js.map +1 -0
  67. package/dist/embeddings/ModelRegistry.d.ts +48 -0
  68. package/dist/embeddings/ModelRegistry.d.ts.map +1 -0
  69. package/dist/embeddings/ModelRegistry.js +170 -0
  70. package/dist/embeddings/ModelRegistry.js.map +1 -0
  71. package/dist/embeddings/VectorStore.d.ts +114 -0
  72. package/dist/embeddings/VectorStore.d.ts.map +1 -0
  73. package/dist/embeddings/VectorStore.js +393 -0
  74. package/dist/embeddings/VectorStore.js.map +1 -0
  75. package/dist/embeddings/providers/OllamaEmbeddings.d.ts +38 -0
  76. package/dist/embeddings/providers/OllamaEmbeddings.d.ts.map +1 -0
  77. package/dist/embeddings/providers/OllamaEmbeddings.js +125 -0
  78. package/dist/embeddings/providers/OllamaEmbeddings.js.map +1 -0
  79. package/dist/embeddings/providers/OpenAIEmbeddings.d.ts +40 -0
  80. package/dist/embeddings/providers/OpenAIEmbeddings.d.ts.map +1 -0
  81. package/dist/embeddings/providers/OpenAIEmbeddings.js +129 -0
  82. package/dist/embeddings/providers/OpenAIEmbeddings.js.map +1 -0
  83. package/dist/embeddings/providers/TransformersEmbeddings.d.ts +38 -0
  84. package/dist/embeddings/providers/TransformersEmbeddings.d.ts.map +1 -0
  85. package/dist/embeddings/providers/TransformersEmbeddings.js +115 -0
  86. package/dist/embeddings/providers/TransformersEmbeddings.js.map +1 -0
  87. package/dist/handoff/SessionHandoffStore.d.ts +80 -0
  88. package/dist/handoff/SessionHandoffStore.d.ts.map +1 -0
  89. package/dist/handoff/SessionHandoffStore.js +314 -0
  90. package/dist/handoff/SessionHandoffStore.js.map +1 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +115 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/mcp-server.d.ts +27 -0
  96. package/dist/mcp-server.d.ts.map +1 -0
  97. package/dist/mcp-server.js +157 -0
  98. package/dist/mcp-server.js.map +1 -0
  99. package/dist/memory/WorkingMemoryStore.d.ts +83 -0
  100. package/dist/memory/WorkingMemoryStore.d.ts.map +1 -0
  101. package/dist/memory/WorkingMemoryStore.js +318 -0
  102. package/dist/memory/WorkingMemoryStore.js.map +1 -0
  103. package/dist/memory/types.d.ts +192 -0
  104. package/dist/memory/types.d.ts.map +1 -0
  105. package/dist/memory/types.js +8 -0
  106. package/dist/memory/types.js.map +1 -0
  107. package/dist/parsers/CodexConversationParser.d.ts +51 -0
  108. package/dist/parsers/CodexConversationParser.d.ts.map +1 -0
  109. package/dist/parsers/CodexConversationParser.js +301 -0
  110. package/dist/parsers/CodexConversationParser.js.map +1 -0
  111. package/dist/parsers/ConversationParser.d.ts +286 -0
  112. package/dist/parsers/ConversationParser.d.ts.map +1 -0
  113. package/dist/parsers/ConversationParser.js +795 -0
  114. package/dist/parsers/ConversationParser.js.map +1 -0
  115. package/dist/parsers/DecisionExtractor.d.ts +144 -0
  116. package/dist/parsers/DecisionExtractor.d.ts.map +1 -0
  117. package/dist/parsers/DecisionExtractor.js +434 -0
  118. package/dist/parsers/DecisionExtractor.js.map +1 -0
  119. package/dist/parsers/GitIntegrator.d.ts +156 -0
  120. package/dist/parsers/GitIntegrator.d.ts.map +1 -0
  121. package/dist/parsers/GitIntegrator.js +348 -0
  122. package/dist/parsers/GitIntegrator.js.map +1 -0
  123. package/dist/parsers/MistakeExtractor.d.ts +151 -0
  124. package/dist/parsers/MistakeExtractor.d.ts.map +1 -0
  125. package/dist/parsers/MistakeExtractor.js +460 -0
  126. package/dist/parsers/MistakeExtractor.js.map +1 -0
  127. package/dist/parsers/RequirementsExtractor.d.ts +166 -0
  128. package/dist/parsers/RequirementsExtractor.d.ts.map +1 -0
  129. package/dist/parsers/RequirementsExtractor.js +338 -0
  130. package/dist/parsers/RequirementsExtractor.js.map +1 -0
  131. package/dist/realtime/ConversationWatcher.d.ts +87 -0
  132. package/dist/realtime/ConversationWatcher.d.ts.map +1 -0
  133. package/dist/realtime/ConversationWatcher.js +204 -0
  134. package/dist/realtime/ConversationWatcher.js.map +1 -0
  135. package/dist/realtime/IncrementalParser.d.ts +83 -0
  136. package/dist/realtime/IncrementalParser.d.ts.map +1 -0
  137. package/dist/realtime/IncrementalParser.js +232 -0
  138. package/dist/realtime/IncrementalParser.js.map +1 -0
  139. package/dist/realtime/LiveExtractor.d.ts +72 -0
  140. package/dist/realtime/LiveExtractor.d.ts.map +1 -0
  141. package/dist/realtime/LiveExtractor.js +288 -0
  142. package/dist/realtime/LiveExtractor.js.map +1 -0
  143. package/dist/search/SemanticSearch.d.ts +121 -0
  144. package/dist/search/SemanticSearch.d.ts.map +1 -0
  145. package/dist/search/SemanticSearch.js +823 -0
  146. package/dist/search/SemanticSearch.js.map +1 -0
  147. package/dist/storage/BackupManager.d.ts +58 -0
  148. package/dist/storage/BackupManager.d.ts.map +1 -0
  149. package/dist/storage/BackupManager.js +223 -0
  150. package/dist/storage/BackupManager.js.map +1 -0
  151. package/dist/storage/ConversationStorage.d.ts +341 -0
  152. package/dist/storage/ConversationStorage.d.ts.map +1 -0
  153. package/dist/storage/ConversationStorage.js +792 -0
  154. package/dist/storage/ConversationStorage.js.map +1 -0
  155. package/dist/storage/DeletionService.d.ts +70 -0
  156. package/dist/storage/DeletionService.d.ts.map +1 -0
  157. package/dist/storage/DeletionService.js +253 -0
  158. package/dist/storage/DeletionService.js.map +1 -0
  159. package/dist/storage/GlobalIndex.d.ts +133 -0
  160. package/dist/storage/GlobalIndex.d.ts.map +1 -0
  161. package/dist/storage/GlobalIndex.js +310 -0
  162. package/dist/storage/GlobalIndex.js.map +1 -0
  163. package/dist/storage/SQLiteManager.d.ts +114 -0
  164. package/dist/storage/SQLiteManager.d.ts.map +1 -0
  165. package/dist/storage/SQLiteManager.js +636 -0
  166. package/dist/storage/SQLiteManager.js.map +1 -0
  167. package/dist/storage/migrations.d.ts +54 -0
  168. package/dist/storage/migrations.d.ts.map +1 -0
  169. package/dist/storage/migrations.js +285 -0
  170. package/dist/storage/migrations.js.map +1 -0
  171. package/dist/storage/schema.sql +436 -0
  172. package/dist/tools/ToolDefinitions.d.ts +946 -0
  173. package/dist/tools/ToolDefinitions.d.ts.map +1 -0
  174. package/dist/tools/ToolDefinitions.js +937 -0
  175. package/dist/tools/ToolDefinitions.js.map +1 -0
  176. package/dist/tools/ToolHandlers.d.ts +791 -0
  177. package/dist/tools/ToolHandlers.d.ts.map +1 -0
  178. package/dist/tools/ToolHandlers.js +3262 -0
  179. package/dist/tools/ToolHandlers.js.map +1 -0
  180. package/dist/types/ToolTypes.d.ts +824 -0
  181. package/dist/types/ToolTypes.d.ts.map +1 -0
  182. package/dist/types/ToolTypes.js +6 -0
  183. package/dist/types/ToolTypes.js.map +1 -0
  184. package/dist/utils/Logger.d.ts +70 -0
  185. package/dist/utils/Logger.d.ts.map +1 -0
  186. package/dist/utils/Logger.js +131 -0
  187. package/dist/utils/Logger.js.map +1 -0
  188. package/dist/utils/McpConfig.d.ts +54 -0
  189. package/dist/utils/McpConfig.d.ts.map +1 -0
  190. package/dist/utils/McpConfig.js +136 -0
  191. package/dist/utils/McpConfig.js.map +1 -0
  192. package/dist/utils/ProjectMigration.d.ts +82 -0
  193. package/dist/utils/ProjectMigration.d.ts.map +1 -0
  194. package/dist/utils/ProjectMigration.js +416 -0
  195. package/dist/utils/ProjectMigration.js.map +1 -0
  196. package/dist/utils/constants.d.ts +75 -0
  197. package/dist/utils/constants.d.ts.map +1 -0
  198. package/dist/utils/constants.js +105 -0
  199. package/dist/utils/constants.js.map +1 -0
  200. package/dist/utils/safeJson.d.ts +37 -0
  201. package/dist/utils/safeJson.d.ts.map +1 -0
  202. package/dist/utils/safeJson.js +48 -0
  203. package/dist/utils/safeJson.js.map +1 -0
  204. package/dist/utils/sanitization.d.ts +45 -0
  205. package/dist/utils/sanitization.d.ts.map +1 -0
  206. package/dist/utils/sanitization.js +153 -0
  207. package/dist/utils/sanitization.js.map +1 -0
  208. package/dist/utils/worktree.d.ts +15 -0
  209. package/dist/utils/worktree.d.ts.map +1 -0
  210. package/dist/utils/worktree.js +86 -0
  211. package/dist/utils/worktree.js.map +1 -0
  212. package/package.json +98 -0
  213. package/scripts/changelog-check.sh +62 -0
  214. package/scripts/check-node.js +17 -0
  215. package/scripts/dev-config.js +56 -0
  216. package/scripts/postinstall.js +117 -0
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Multi-pass JSONL Conversation Parser for Claude Code history.
3
+ *
4
+ * This parser reads conversation history from Claude Code's storage locations
5
+ * (~/.claude/projects) and extracts structured data including messages, tool uses,
6
+ * file edits, and thinking blocks.
7
+ *
8
+ * The parser handles two directory structures:
9
+ * - Modern: ~/.claude/projects/{sanitized-path}
10
+ * - Legacy: ~/.claude/projects/{original-project-name}
11
+ *
12
+ * It performs a multi-pass parsing approach:
13
+ * 1. First pass: Extract conversations and messages
14
+ * 2. Second pass: Link tool uses and results
15
+ * 3. Third pass: Extract file edits from snapshots
16
+ * 4. Fourth pass: Extract thinking blocks
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const parser = new ConversationParser();
21
+ * const result = parser.parseProject('/path/to/project');
22
+ * console.error(`Parsed ${result.conversations.length} conversations`);
23
+ * console.error(`Found ${result.messages.length} messages`);
24
+ * console.error(`Extracted ${result.tool_uses.length} tool uses`);
25
+ * ```
26
+ */
27
+ import { readFileSync, readdirSync, existsSync, statSync, createReadStream } from "fs";
28
+ import { createInterface } from "readline";
29
+ import { join } from "path";
30
+ import { nanoid } from "nanoid";
31
+ import { pathToProjectFolderName } from "../utils/sanitization.js";
32
+ /**
33
+ * Parser for Claude Code conversation history.
34
+ *
35
+ * Extracts structured data from JSONL conversation files stored in
36
+ * ~/.claude/projects. Handles both modern and legacy naming conventions.
37
+ */
38
+ export class ConversationParser {
39
+ /**
40
+ * Parse all conversations for a project.
41
+ *
42
+ * Searches for conversation files in Claude's storage directories and
43
+ * parses them into structured entities. Supports filtering by session ID
44
+ * and handles both modern and legacy directory naming conventions.
45
+ *
46
+ * @param projectPath - Absolute path to the project (used for folder lookup)
47
+ * @param sessionId - Optional session ID to filter for a single conversation
48
+ * @param projectIdentifier - Optional identifier to store as project_path
49
+ * @param lastIndexedMs - Optional timestamp to skip unchanged files (mtime)
50
+ * @returns ParseResult containing all extracted entities
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const parser = new ConversationParser();
55
+ *
56
+ * // Parse all conversations
57
+ * const allResults = parser.parseProject('/Users/me/my-project');
58
+ *
59
+ * // Parse specific session
60
+ * const sessionResults = parser.parseProject('/Users/me/my-project', 'session-123');
61
+ * ```
62
+ */
63
+ parseProject(projectPath, sessionId, projectIdentifier, lastIndexedMs) {
64
+ console.error(`Parsing conversations for project: ${projectPath}`);
65
+ if (sessionId) {
66
+ console.error(`Filtering for session: ${sessionId}`);
67
+ }
68
+ // Convert project path to Claude projects directory name
69
+ const projectDirName = pathToProjectFolderName(projectPath);
70
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
71
+ if (!homeDir) {
72
+ throw new Error("HOME or USERPROFILE environment variable is not set");
73
+ }
74
+ const projectsBaseDir = join(homeDir, ".claude", "projects");
75
+ // Generate path variants to handle Claude Code's potential encoding differences
76
+ // Claude Code may encode hyphens as underscores or vice versa in path components
77
+ const pathVariants = this.generatePathVariants(projectDirName);
78
+ // Collect directories that exist
79
+ const dirsToCheck = [];
80
+ const checkedPaths = [];
81
+ for (const variant of pathVariants) {
82
+ const variantDir = join(projectsBaseDir, variant);
83
+ checkedPaths.push(variantDir);
84
+ if (existsSync(variantDir)) {
85
+ // Check if this directory has any .jsonl files
86
+ try {
87
+ const files = readdirSync(variantDir).filter(f => f.endsWith(".jsonl"));
88
+ if (files.length > 0 && !dirsToCheck.includes(variantDir)) {
89
+ dirsToCheck.push(variantDir);
90
+ console.error(`Found conversation directory: ${variant}`);
91
+ }
92
+ }
93
+ catch (_e) {
94
+ // Directory exists but can't be read, skip it
95
+ }
96
+ }
97
+ }
98
+ if (dirsToCheck.length === 0) {
99
+ console.error(`⚠️ No conversation directories found`);
100
+ console.error(` Checked ${checkedPaths.length} path variants:`);
101
+ for (const path of checkedPaths.slice(0, 5)) {
102
+ console.error(` - ${path}`);
103
+ }
104
+ if (checkedPaths.length > 5) {
105
+ console.error(` ... and ${checkedPaths.length - 5} more`);
106
+ }
107
+ return {
108
+ conversations: [],
109
+ messages: [],
110
+ tool_uses: [],
111
+ tool_results: [],
112
+ file_edits: [],
113
+ thinking_blocks: [],
114
+ indexed_folders: [],
115
+ };
116
+ }
117
+ console.error(`Looking in ${dirsToCheck.length} director(ies): ${dirsToCheck.join(", ")}`);
118
+ // Collect all .jsonl files from all directories
119
+ const fileMap = new Map(); // filename -> full path
120
+ for (const dir of dirsToCheck) {
121
+ const dirFiles = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
122
+ for (const file of dirFiles) {
123
+ const fullPath = join(dir, file);
124
+ // If file already exists in map, keep the one from the first directory (modern takes precedence)
125
+ if (!fileMap.has(file)) {
126
+ fileMap.set(file, fullPath);
127
+ }
128
+ }
129
+ }
130
+ let files = Array.from(fileMap.keys());
131
+ // If session_id provided, filter to only that session file
132
+ if (sessionId) {
133
+ files = files.filter((f) => f === `${sessionId}.jsonl`);
134
+ if (files.length === 0) {
135
+ console.error(`⚠️ Session file not found: ${sessionId}.jsonl`);
136
+ console.error(`Available sessions: ${Array.from(fileMap.keys()).join(", ")}`);
137
+ }
138
+ }
139
+ console.error(`Found ${files.length} conversation file(s) to parse`);
140
+ // Parse each file
141
+ const result = {
142
+ conversations: [],
143
+ messages: [],
144
+ tool_uses: [],
145
+ tool_results: [],
146
+ file_edits: [],
147
+ thinking_blocks: [],
148
+ indexed_folders: dirsToCheck,
149
+ };
150
+ const projectPathForRecords = projectIdentifier || projectPath;
151
+ let skippedCount = 0;
152
+ for (const file of files) {
153
+ const filePath = fileMap.get(file);
154
+ if (filePath) {
155
+ if (lastIndexedMs) {
156
+ try {
157
+ const stats = statSync(filePath);
158
+ if (stats.mtimeMs < lastIndexedMs) {
159
+ skippedCount++;
160
+ continue;
161
+ }
162
+ }
163
+ catch (_e) {
164
+ // If we can't stat the file, try to parse it anyway
165
+ }
166
+ }
167
+ this.parseFile(filePath, result, projectPathForRecords);
168
+ }
169
+ }
170
+ if (skippedCount > 0) {
171
+ console.error(`⏭ Skipped ${skippedCount} unchanged file(s)`);
172
+ }
173
+ console.error(`Parsed ${result.conversations.length} conversations, ${result.messages.length} messages`);
174
+ return result;
175
+ }
176
+ /**
177
+ * Parse conversations across multiple project paths and merge results.
178
+ *
179
+ * @param projectPaths - Project paths to scan for conversation folders
180
+ * @param sessionId - Optional session ID to filter for a single conversation
181
+ * @param projectIdentifier - Optional identifier to store as project_path
182
+ */
183
+ parseProjects(projectPaths, sessionId, projectIdentifier, lastIndexedMs) {
184
+ const combined = {
185
+ conversations: [],
186
+ messages: [],
187
+ tool_uses: [],
188
+ tool_results: [],
189
+ file_edits: [],
190
+ thinking_blocks: [],
191
+ indexed_folders: [],
192
+ parse_errors: [],
193
+ };
194
+ const seen = {
195
+ conversations: new Set(),
196
+ messages: new Set(),
197
+ toolUses: new Set(),
198
+ toolResults: new Set(),
199
+ fileEdits: new Set(),
200
+ thinkingBlocks: new Set(),
201
+ };
202
+ const indexedFolders = new Set();
203
+ for (const path of projectPaths) {
204
+ const result = this.parseProject(path, sessionId, projectIdentifier, lastIndexedMs);
205
+ this.mergeParseResults(combined, result, seen, indexedFolders);
206
+ }
207
+ combined.indexed_folders = Array.from(indexedFolders);
208
+ if (combined.parse_errors && combined.parse_errors.length === 0) {
209
+ delete combined.parse_errors;
210
+ }
211
+ return combined;
212
+ }
213
+ mergeParseResults(target, source, seen, indexedFolders) {
214
+ for (const item of source.conversations) {
215
+ if (!seen.conversations.has(item.id)) {
216
+ seen.conversations.add(item.id);
217
+ target.conversations.push(item);
218
+ }
219
+ }
220
+ for (const item of source.messages) {
221
+ if (!seen.messages.has(item.id)) {
222
+ seen.messages.add(item.id);
223
+ target.messages.push(item);
224
+ }
225
+ }
226
+ for (const item of source.tool_uses) {
227
+ if (!seen.toolUses.has(item.id)) {
228
+ seen.toolUses.add(item.id);
229
+ target.tool_uses.push(item);
230
+ }
231
+ }
232
+ for (const item of source.tool_results) {
233
+ if (!seen.toolResults.has(item.id)) {
234
+ seen.toolResults.add(item.id);
235
+ target.tool_results.push(item);
236
+ }
237
+ }
238
+ for (const item of source.file_edits) {
239
+ if (!seen.fileEdits.has(item.id)) {
240
+ seen.fileEdits.add(item.id);
241
+ target.file_edits.push(item);
242
+ }
243
+ }
244
+ for (const item of source.thinking_blocks) {
245
+ if (!seen.thinkingBlocks.has(item.id)) {
246
+ seen.thinkingBlocks.add(item.id);
247
+ target.thinking_blocks.push(item);
248
+ }
249
+ }
250
+ if (source.indexed_folders) {
251
+ for (const folder of source.indexed_folders) {
252
+ indexedFolders.add(folder);
253
+ }
254
+ }
255
+ if (source.parse_errors && source.parse_errors.length > 0) {
256
+ if (!target.parse_errors) {
257
+ target.parse_errors = [];
258
+ }
259
+ target.parse_errors.push(...source.parse_errors);
260
+ }
261
+ }
262
+ /**
263
+ * Parse conversations directly from a Claude projects folder.
264
+ *
265
+ * This method is used when you already have the path to the conversation
266
+ * folder (e.g., ~/.claude/projects/-Users-me-my-project) rather than
267
+ * a project path that needs to be converted.
268
+ *
269
+ * @param folderPath - Absolute path to the Claude projects folder
270
+ * @param projectIdentifier - Optional identifier to use as project_path in records (defaults to folder path)
271
+ * @returns ParseResult containing all extracted entities
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const parser = new ConversationParser();
276
+ * const result = parser.parseFromFolder('~/.claude/projects/-Users-me-my-project');
277
+ * ```
278
+ */
279
+ parseFromFolder(folderPath, projectIdentifier, lastIndexedMs) {
280
+ const result = {
281
+ conversations: [],
282
+ messages: [],
283
+ tool_uses: [],
284
+ tool_results: [],
285
+ file_edits: [],
286
+ thinking_blocks: [],
287
+ indexed_folders: [folderPath],
288
+ };
289
+ // Use folder path as project identifier if not provided
290
+ const projectPath = projectIdentifier || folderPath;
291
+ if (!existsSync(folderPath)) {
292
+ console.error(`⚠️ Folder does not exist: ${folderPath}`);
293
+ return result;
294
+ }
295
+ // Get all .jsonl files in the folder
296
+ const files = readdirSync(folderPath).filter((f) => f.endsWith(".jsonl"));
297
+ console.error(`Found ${files.length} conversation file(s) in ${folderPath}`);
298
+ // Parse each file, optionally skipping unchanged files in incremental mode
299
+ let skippedCount = 0;
300
+ for (const file of files) {
301
+ const filePath = join(folderPath, file);
302
+ // Skip unchanged files in incremental mode
303
+ if (lastIndexedMs) {
304
+ try {
305
+ const stats = statSync(filePath);
306
+ if (stats.mtimeMs < lastIndexedMs) {
307
+ skippedCount++;
308
+ continue;
309
+ }
310
+ }
311
+ catch (_e) {
312
+ // If we can't stat the file, try to parse it anyway
313
+ }
314
+ }
315
+ this.parseFile(filePath, result, projectPath);
316
+ }
317
+ if (skippedCount > 0) {
318
+ console.error(`⏭ Skipped ${skippedCount} unchanged file(s)`);
319
+ }
320
+ console.error(`Parsed ${result.conversations.length} conversations, ${result.messages.length} messages`);
321
+ return result;
322
+ }
323
+ /**
324
+ * Parse conversations from a Claude projects folder using streaming.
325
+ *
326
+ * This async method uses line-by-line streaming to efficiently handle
327
+ * large JSONL files without loading the entire file into memory.
328
+ * Use this method for large conversation histories.
329
+ *
330
+ * @param folderPath - Absolute path to the Claude projects folder
331
+ * @param projectIdentifier - Optional identifier to use as project_path in records
332
+ * @param lastIndexedMs - Optional timestamp for incremental indexing (skip unchanged files)
333
+ * @returns Promise<ParseResult> containing all extracted entities
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * const parser = new ConversationParser();
338
+ * const result = await parser.parseFromFolderAsync('~/.claude/projects/-Users-me-my-project');
339
+ * ```
340
+ */
341
+ async parseFromFolderAsync(folderPath, projectIdentifier, lastIndexedMs) {
342
+ const result = {
343
+ conversations: [],
344
+ messages: [],
345
+ tool_uses: [],
346
+ tool_results: [],
347
+ file_edits: [],
348
+ thinking_blocks: [],
349
+ indexed_folders: [folderPath],
350
+ };
351
+ // Use folder path as project identifier if not provided
352
+ const projectPath = projectIdentifier || folderPath;
353
+ if (!existsSync(folderPath)) {
354
+ console.error(`⚠️ Folder does not exist: ${folderPath}`);
355
+ return result;
356
+ }
357
+ // Get all .jsonl files in the folder
358
+ const files = readdirSync(folderPath).filter((f) => f.endsWith(".jsonl"));
359
+ console.error(`Found ${files.length} conversation file(s) in ${folderPath}`);
360
+ // Parse each file, optionally skipping unchanged files in incremental mode
361
+ let skippedCount = 0;
362
+ for (const file of files) {
363
+ const filePath = join(folderPath, file);
364
+ // Skip unchanged files in incremental mode
365
+ if (lastIndexedMs) {
366
+ try {
367
+ const stats = statSync(filePath);
368
+ if (stats.mtimeMs < lastIndexedMs) {
369
+ skippedCount++;
370
+ continue;
371
+ }
372
+ }
373
+ catch (_e) {
374
+ // If we can't stat the file, try to parse it anyway
375
+ }
376
+ }
377
+ await this.parseFileAsync(filePath, result, projectPath);
378
+ }
379
+ if (skippedCount > 0) {
380
+ console.error(`⏭ Skipped ${skippedCount} unchanged file(s)`);
381
+ }
382
+ console.error(`Parsed ${result.conversations.length} conversations, ${result.messages.length} messages`);
383
+ return result;
384
+ }
385
+ /**
386
+ * Parse a single .jsonl file using streaming (async).
387
+ *
388
+ * Uses readline interface with createReadStream to read the file
389
+ * line by line, avoiding loading the entire file into memory.
390
+ */
391
+ async parseFileAsync(filePath, result, projectPath) {
392
+ const fileMessages = [];
393
+ // Create readline interface for streaming
394
+ const fileStream = createReadStream(filePath, { encoding: "utf-8" });
395
+ const rl = createInterface({
396
+ input: fileStream,
397
+ crlfDelay: Infinity, // Handle both \n and \r\n
398
+ });
399
+ let lineNumber = 0;
400
+ for await (const line of rl) {
401
+ lineNumber++;
402
+ const trimmedLine = line.trim();
403
+ if (!trimmedLine) {
404
+ continue;
405
+ }
406
+ try {
407
+ const msg = JSON.parse(trimmedLine);
408
+ fileMessages.push(msg);
409
+ }
410
+ catch (error) {
411
+ const errorMsg = error instanceof Error ? error.message : String(error);
412
+ console.error(`Error parsing line ${lineNumber} in ${filePath}: ${errorMsg}`);
413
+ // Track the error
414
+ if (!result.parse_errors) {
415
+ result.parse_errors = [];
416
+ }
417
+ result.parse_errors.push({
418
+ file: filePath,
419
+ line: lineNumber,
420
+ error: errorMsg,
421
+ });
422
+ }
423
+ }
424
+ if (fileMessages.length === 0) {
425
+ return;
426
+ }
427
+ // Run multi-pass extraction (same as sync version)
428
+ this.extractConversation(fileMessages, result, projectPath);
429
+ this.extractMessages(fileMessages, result);
430
+ this.extractToolCalls(fileMessages, result);
431
+ this.extractFileEdits(fileMessages, result);
432
+ this.extractThinkingBlocks(fileMessages, result);
433
+ }
434
+ /**
435
+ * Parse a single .jsonl file
436
+ */
437
+ parseFile(filePath, result, projectPath) {
438
+ const content = readFileSync(filePath, "utf-8");
439
+ const lines = content.split("\n").filter((l) => l.trim());
440
+ // Parse messages from this file
441
+ const fileMessages = [];
442
+ for (let i = 0; i < lines.length; i++) {
443
+ const line = lines[i];
444
+ try {
445
+ const msg = JSON.parse(line);
446
+ fileMessages.push(msg);
447
+ }
448
+ catch (error) {
449
+ const errorMsg = error instanceof Error ? error.message : String(error);
450
+ console.error(`Error parsing line ${i + 1} in ${filePath}: ${errorMsg}`);
451
+ // Track the error
452
+ if (!result.parse_errors) {
453
+ result.parse_errors = [];
454
+ }
455
+ result.parse_errors.push({
456
+ file: filePath,
457
+ line: i + 1,
458
+ error: errorMsg,
459
+ });
460
+ }
461
+ }
462
+ if (fileMessages.length === 0) {
463
+ return;
464
+ }
465
+ // Pass 1: Extract conversation info
466
+ this.extractConversation(fileMessages, result, projectPath);
467
+ // Pass 2: Extract messages
468
+ this.extractMessages(fileMessages, result);
469
+ // Pass 3: Extract tool uses and results
470
+ this.extractToolCalls(fileMessages, result);
471
+ // Pass 4: Extract file edits
472
+ this.extractFileEdits(fileMessages, result);
473
+ // Pass 5: Extract thinking blocks
474
+ this.extractThinkingBlocks(fileMessages, result);
475
+ }
476
+ /**
477
+ * Pass 1: Extract conversation metadata
478
+ */
479
+ extractConversation(messages, result, projectPath) {
480
+ // Get sessionId from first message
481
+ const firstMsg = messages.find((m) => m.sessionId);
482
+ if (!firstMsg || !firstMsg.sessionId) {
483
+ return;
484
+ }
485
+ const sessionId = firstMsg.sessionId;
486
+ // Check if conversation already exists
487
+ if (result.conversations.some((c) => c.id === sessionId)) {
488
+ return;
489
+ }
490
+ // Find timestamps (filter out invalid/NaN timestamps)
491
+ const timestamps = messages
492
+ .filter((m) => !!m.timestamp)
493
+ .map((m) => new Date(m.timestamp).getTime())
494
+ .filter((t) => !isNaN(t))
495
+ .sort((a, b) => a - b);
496
+ if (timestamps.length === 0) {
497
+ return;
498
+ }
499
+ // Get most common git branch and version
500
+ const branches = messages
501
+ .filter((m) => !!m.gitBranch)
502
+ .map((m) => m.gitBranch);
503
+ const versions = messages
504
+ .filter((m) => !!m.version)
505
+ .map((m) => m.version);
506
+ // Detect MCP tool usage
507
+ const mcpUsage = this.detectMcpUsage(messages);
508
+ const conversation = {
509
+ id: sessionId,
510
+ project_path: projectPath,
511
+ first_message_at: timestamps[0],
512
+ last_message_at: timestamps[timestamps.length - 1],
513
+ message_count: messages.filter((m) => m.type === "user" || m.type === "assistant").length,
514
+ git_branch: branches[branches.length - 1],
515
+ claude_version: versions[versions.length - 1],
516
+ metadata: {
517
+ total_messages: messages.length,
518
+ mcp_usage: mcpUsage,
519
+ },
520
+ created_at: timestamps[0],
521
+ updated_at: Date.now(),
522
+ };
523
+ result.conversations.push(conversation);
524
+ }
525
+ /**
526
+ * Detect MCP tool usage in conversation messages
527
+ */
528
+ detectMcpUsage(messages) {
529
+ const servers = new Set();
530
+ for (const msg of messages) {
531
+ const messageData = msg.message;
532
+ if (!messageData?.content || !Array.isArray(messageData.content)) {
533
+ continue;
534
+ }
535
+ for (const item of messageData.content) {
536
+ const contentItem = item;
537
+ if (contentItem.type === "tool_use" && contentItem.name?.startsWith("mcp__")) {
538
+ // Extract server name from tool name
539
+ // Format: mcp__server-name__tool-name
540
+ const parts = contentItem.name.split("__");
541
+ if (parts.length >= 2) {
542
+ servers.add(parts[1]);
543
+ }
544
+ }
545
+ }
546
+ }
547
+ return {
548
+ detected: servers.size > 0,
549
+ servers: Array.from(servers),
550
+ };
551
+ }
552
+ /**
553
+ * Pass 2: Extract individual messages
554
+ */
555
+ extractMessages(messages, result) {
556
+ for (const msg of messages) {
557
+ if (!msg.uuid || !msg.sessionId) {
558
+ continue;
559
+ }
560
+ const message = {
561
+ id: msg.uuid,
562
+ conversation_id: msg.sessionId,
563
+ parent_id: msg.parentUuid || undefined,
564
+ message_type: msg.type,
565
+ role: msg.message?.role,
566
+ content: this.extractContent(msg),
567
+ timestamp: msg.timestamp ? new Date(msg.timestamp).getTime() : Date.now(),
568
+ is_sidechain: msg.isSidechain || false,
569
+ agent_id: msg.agentId,
570
+ request_id: msg.requestId,
571
+ git_branch: msg.gitBranch,
572
+ cwd: msg.cwd,
573
+ metadata: msg,
574
+ };
575
+ result.messages.push(message);
576
+ }
577
+ }
578
+ /**
579
+ * Pass 3: Extract tool uses and results
580
+ */
581
+ extractToolCalls(messages, result) {
582
+ for (const msg of messages) {
583
+ const messageData = msg.message;
584
+ if (!messageData?.content || !Array.isArray(messageData.content) || !msg.uuid) {
585
+ continue;
586
+ }
587
+ const timestamp = msg.timestamp
588
+ ? new Date(msg.timestamp).getTime()
589
+ : Date.now();
590
+ for (const item of messageData.content) {
591
+ const contentItem = item;
592
+ // Tool use
593
+ if (contentItem.type === "tool_use") {
594
+ const toolUse = {
595
+ id: contentItem.id || "",
596
+ message_id: msg.uuid,
597
+ tool_name: contentItem.name || "",
598
+ tool_input: contentItem.input || {},
599
+ timestamp,
600
+ };
601
+ result.tool_uses.push(toolUse);
602
+ }
603
+ // Tool result
604
+ if (contentItem.type === "tool_result") {
605
+ const toolUseResult = msg.toolUseResult;
606
+ const toolResult = {
607
+ id: nanoid(),
608
+ tool_use_id: contentItem.tool_use_id || "",
609
+ message_id: msg.uuid,
610
+ content: typeof contentItem.content === "string" ? contentItem.content : JSON.stringify(contentItem.content),
611
+ is_error: contentItem.is_error || false,
612
+ stdout: toolUseResult?.stdout,
613
+ stderr: toolUseResult?.stderr,
614
+ is_image: toolUseResult?.isImage || false,
615
+ timestamp,
616
+ };
617
+ result.tool_results.push(toolResult);
618
+ }
619
+ }
620
+ }
621
+ }
622
+ /**
623
+ * Pass 4: Extract file edits from snapshots
624
+ */
625
+ extractFileEdits(messages, result) {
626
+ // Build a Set of stored message IDs for quick lookup
627
+ const storedMessageIds = new Set(result.messages.map(m => m.id));
628
+ for (const msg of messages) {
629
+ if (msg.type !== "file-history-snapshot" || !msg.snapshot) {
630
+ continue;
631
+ }
632
+ // Get the message ID that would reference this snapshot
633
+ const messageId = msg.messageId || msg.uuid;
634
+ if (!messageId) {
635
+ continue; // No message ID to reference
636
+ }
637
+ // Skip if the message wasn't stored (e.g., lacks uuid or sessionId)
638
+ if (!storedMessageIds.has(messageId)) {
639
+ // This is expected for file-history-snapshot messages that don't have uuid/sessionId
640
+ continue;
641
+ }
642
+ const snapshot = msg.snapshot;
643
+ const trackedFiles = snapshot.trackedFileBackups || {};
644
+ const conversationId = msg.sessionId;
645
+ if (!conversationId) {
646
+ continue; // Need conversation ID for foreign key
647
+ }
648
+ for (const [filePath, fileInfo] of Object.entries(trackedFiles)) {
649
+ const info = fileInfo;
650
+ const fileEdit = {
651
+ id: nanoid(),
652
+ conversation_id: conversationId,
653
+ file_path: filePath,
654
+ message_id: messageId,
655
+ backup_version: info.version,
656
+ backup_time: info.backupTime
657
+ ? new Date(info.backupTime).getTime()
658
+ : undefined,
659
+ snapshot_timestamp: snapshot.timestamp
660
+ ? new Date(snapshot.timestamp).getTime()
661
+ : Date.now(),
662
+ metadata: info,
663
+ };
664
+ result.file_edits.push(fileEdit);
665
+ }
666
+ }
667
+ }
668
+ /**
669
+ * Pass 5: Extract thinking blocks
670
+ */
671
+ extractThinkingBlocks(messages, result) {
672
+ for (const msg of messages) {
673
+ const messageData = msg.message;
674
+ if (!messageData?.content || !Array.isArray(messageData.content) || !msg.uuid) {
675
+ continue;
676
+ }
677
+ const timestamp = msg.timestamp
678
+ ? new Date(msg.timestamp).getTime()
679
+ : Date.now();
680
+ for (const item of messageData.content) {
681
+ const contentItem = item;
682
+ if (contentItem.type === "thinking") {
683
+ const thinking = {
684
+ id: nanoid(),
685
+ message_id: msg.uuid,
686
+ thinking_content: contentItem.thinking || "",
687
+ signature: contentItem.signature,
688
+ timestamp,
689
+ };
690
+ result.thinking_blocks.push(thinking);
691
+ }
692
+ }
693
+ }
694
+ }
695
+ /**
696
+ * Generate path variants to handle potential encoding differences.
697
+ *
698
+ * Claude Code may encode paths differently than expected:
699
+ * - Hyphens in path components might become underscores
700
+ * - Underscores might become hyphens
701
+ * - Dots might become hyphens (legacy)
702
+ *
703
+ * This method generates multiple variants to try when searching for directories.
704
+ *
705
+ * @example
706
+ * Input: "-Users-myid-GIT-projects-myProject"
707
+ * Output: [
708
+ * "-Users-myid-GIT-projects-myProject", // Original
709
+ * "-Users-myid-GIT_projects-myProject", // Hyphens in components -> underscores
710
+ * "-Users-myid-GIT-projects-myProject", // Dots -> hyphens (legacy)
711
+ * ]
712
+ */
713
+ generatePathVariants(projectDirName) {
714
+ const variants = new Set();
715
+ // 1. Original encoding (as computed by pathToProjectFolderName)
716
+ variants.add(projectDirName);
717
+ // 2. Legacy: dots replaced with hyphens
718
+ const legacyVariant = projectDirName.replace(/\./g, '-');
719
+ variants.add(legacyVariant);
720
+ // 3. Try swapping hyphens and underscores within path components
721
+ // Path format: "-Component1-Component2-Component3" or "Drive-Component1-Component2"
722
+ // We need to be careful not to change the leading hyphen or the separating hyphens
723
+ // Split into components by hyphen (the first element might be empty for Unix paths starting with -)
724
+ const parts = projectDirName.split('-');
725
+ // Try converting internal hyphens within multi-hyphen component names to underscores
726
+ // This handles cases like "GIT-projects" becoming "GIT_projects"
727
+ // Strategy: For each part that looks like it might have been originally hyphenated,
728
+ // create a variant with underscores
729
+ const hyphenToUnderscoreVariant = parts
730
+ .map((part) => {
731
+ // Skip empty parts and single chars (likely path separators)
732
+ if (part.length === 0) {
733
+ return part;
734
+ }
735
+ // Convert any underscores in parts to hyphens (in case source had underscores)
736
+ return part.replace(/_/g, '-');
737
+ })
738
+ .join('-');
739
+ const underscoreToHyphenVariant = parts
740
+ .map((part) => {
741
+ if (part.length === 0) {
742
+ return part;
743
+ }
744
+ // Convert any hyphens that might be internal to underscores
745
+ // This is tricky because hyphens are also used as separators
746
+ return part;
747
+ })
748
+ .join('-');
749
+ variants.add(hyphenToUnderscoreVariant);
750
+ variants.add(underscoreToHyphenVariant);
751
+ // 4. Try a variant where we replace all underscores with hyphens
752
+ const allUnderscoresToHyphens = projectDirName.replace(/_/g, '-');
753
+ variants.add(allUnderscoresToHyphens);
754
+ // 5. Try a variant where path components with hyphens have them as underscores
755
+ // e.g., "GIT-projects" -> "GIT_projects"
756
+ // We need to identify which consecutive hyphens are part of component names vs separators
757
+ // A simple heuristic: look for patterns like "X-Y" where X and Y are both alphanumeric
758
+ const internalHyphensToUnderscores = projectDirName.replace(/([a-zA-Z0-9])[-]([a-zA-Z0-9])/g, '$1_$2');
759
+ variants.add(internalHyphensToUnderscores);
760
+ // 6. Also try the reverse: convert underscores to hyphens
761
+ const internalUnderscoresToHyphens = projectDirName.replace(/([a-zA-Z0-9])[_]([a-zA-Z0-9])/g, '$1-$2');
762
+ variants.add(internalUnderscoresToHyphens);
763
+ // Apply the same transformations to the legacy variant
764
+ const legacyInternalHyphensToUnderscores = legacyVariant.replace(/([a-zA-Z0-9])[-]([a-zA-Z0-9])/g, '$1_$2');
765
+ variants.add(legacyInternalHyphensToUnderscores);
766
+ return Array.from(variants);
767
+ }
768
+ /**
769
+ * Extract text content from message
770
+ */
771
+ extractContent(msg) {
772
+ // System messages
773
+ if (msg.type === "system" && typeof msg.content === "string") {
774
+ return msg.content;
775
+ }
776
+ // Summary messages
777
+ if (msg.type === "summary" && msg.summary) {
778
+ return msg.summary;
779
+ }
780
+ // User/Assistant messages
781
+ const messageData = msg.message;
782
+ if (messageData?.content) {
783
+ if (typeof messageData.content === "string") {
784
+ return messageData.content;
785
+ }
786
+ if (Array.isArray(messageData.content)) {
787
+ // Extract text blocks
788
+ const textBlocks = messageData.content.filter((item) => item.type === "text");
789
+ return textBlocks.map((item) => item.text || "").join("\n");
790
+ }
791
+ }
792
+ return undefined;
793
+ }
794
+ }
795
+ //# sourceMappingURL=ConversationParser.js.map