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.
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/ConversationMemory.d.ts +231 -0
- package/dist/ConversationMemory.d.ts.map +1 -0
- package/dist/ConversationMemory.js +357 -0
- package/dist/ConversationMemory.js.map +1 -0
- package/dist/cache/QueryCache.d.ts +215 -0
- package/dist/cache/QueryCache.d.ts.map +1 -0
- package/dist/cache/QueryCache.js +294 -0
- package/dist/cache/QueryCache.js.map +1 -0
- package/dist/cli/commands.d.ts +9 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +954 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/help.d.ts +16 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +361 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +30 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +111 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context/ContextInjector.d.ts +38 -0
- package/dist/context/ContextInjector.d.ts.map +1 -0
- package/dist/context/ContextInjector.js +235 -0
- package/dist/context/ContextInjector.js.map +1 -0
- package/dist/documentation/CodeAnalyzer.d.ts +29 -0
- package/dist/documentation/CodeAnalyzer.d.ts.map +1 -0
- package/dist/documentation/CodeAnalyzer.js +122 -0
- package/dist/documentation/CodeAnalyzer.js.map +1 -0
- package/dist/documentation/ConversationAnalyzer.d.ts +19 -0
- package/dist/documentation/ConversationAnalyzer.d.ts.map +1 -0
- package/dist/documentation/ConversationAnalyzer.js +157 -0
- package/dist/documentation/ConversationAnalyzer.js.map +1 -0
- package/dist/documentation/CrossReferencer.d.ts +67 -0
- package/dist/documentation/CrossReferencer.d.ts.map +1 -0
- package/dist/documentation/CrossReferencer.js +247 -0
- package/dist/documentation/CrossReferencer.js.map +1 -0
- package/dist/documentation/DocumentationGenerator.d.ts +22 -0
- package/dist/documentation/DocumentationGenerator.d.ts.map +1 -0
- package/dist/documentation/DocumentationGenerator.js +57 -0
- package/dist/documentation/DocumentationGenerator.js.map +1 -0
- package/dist/documentation/MarkdownFormatter.d.ts +26 -0
- package/dist/documentation/MarkdownFormatter.d.ts.map +1 -0
- package/dist/documentation/MarkdownFormatter.js +301 -0
- package/dist/documentation/MarkdownFormatter.js.map +1 -0
- package/dist/documentation/types.d.ts +176 -0
- package/dist/documentation/types.d.ts.map +1 -0
- package/dist/documentation/types.js +5 -0
- package/dist/documentation/types.js.map +1 -0
- package/dist/embeddings/ConfigManager.d.ts +46 -0
- package/dist/embeddings/ConfigManager.d.ts.map +1 -0
- package/dist/embeddings/ConfigManager.js +177 -0
- package/dist/embeddings/ConfigManager.js.map +1 -0
- package/dist/embeddings/EmbeddingConfig.d.ts +39 -0
- package/dist/embeddings/EmbeddingConfig.d.ts.map +1 -0
- package/dist/embeddings/EmbeddingConfig.js +132 -0
- package/dist/embeddings/EmbeddingConfig.js.map +1 -0
- package/dist/embeddings/EmbeddingGenerator.d.ts +51 -0
- package/dist/embeddings/EmbeddingGenerator.d.ts.map +1 -0
- package/dist/embeddings/EmbeddingGenerator.js +157 -0
- package/dist/embeddings/EmbeddingGenerator.js.map +1 -0
- package/dist/embeddings/EmbeddingProvider.d.ts +34 -0
- package/dist/embeddings/EmbeddingProvider.d.ts.map +1 -0
- package/dist/embeddings/EmbeddingProvider.js +6 -0
- package/dist/embeddings/EmbeddingProvider.js.map +1 -0
- package/dist/embeddings/ModelRegistry.d.ts +48 -0
- package/dist/embeddings/ModelRegistry.d.ts.map +1 -0
- package/dist/embeddings/ModelRegistry.js +170 -0
- package/dist/embeddings/ModelRegistry.js.map +1 -0
- package/dist/embeddings/VectorStore.d.ts +114 -0
- package/dist/embeddings/VectorStore.d.ts.map +1 -0
- package/dist/embeddings/VectorStore.js +393 -0
- package/dist/embeddings/VectorStore.js.map +1 -0
- package/dist/embeddings/providers/OllamaEmbeddings.d.ts +38 -0
- package/dist/embeddings/providers/OllamaEmbeddings.d.ts.map +1 -0
- package/dist/embeddings/providers/OllamaEmbeddings.js +125 -0
- package/dist/embeddings/providers/OllamaEmbeddings.js.map +1 -0
- package/dist/embeddings/providers/OpenAIEmbeddings.d.ts +40 -0
- package/dist/embeddings/providers/OpenAIEmbeddings.d.ts.map +1 -0
- package/dist/embeddings/providers/OpenAIEmbeddings.js +129 -0
- package/dist/embeddings/providers/OpenAIEmbeddings.js.map +1 -0
- package/dist/embeddings/providers/TransformersEmbeddings.d.ts +38 -0
- package/dist/embeddings/providers/TransformersEmbeddings.d.ts.map +1 -0
- package/dist/embeddings/providers/TransformersEmbeddings.js +115 -0
- package/dist/embeddings/providers/TransformersEmbeddings.js.map +1 -0
- package/dist/handoff/SessionHandoffStore.d.ts +80 -0
- package/dist/handoff/SessionHandoffStore.d.ts.map +1 -0
- package/dist/handoff/SessionHandoffStore.js +314 -0
- package/dist/handoff/SessionHandoffStore.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +27 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +157 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/memory/WorkingMemoryStore.d.ts +83 -0
- package/dist/memory/WorkingMemoryStore.d.ts.map +1 -0
- package/dist/memory/WorkingMemoryStore.js +318 -0
- package/dist/memory/WorkingMemoryStore.js.map +1 -0
- package/dist/memory/types.d.ts +192 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/parsers/CodexConversationParser.d.ts +51 -0
- package/dist/parsers/CodexConversationParser.d.ts.map +1 -0
- package/dist/parsers/CodexConversationParser.js +301 -0
- package/dist/parsers/CodexConversationParser.js.map +1 -0
- package/dist/parsers/ConversationParser.d.ts +286 -0
- package/dist/parsers/ConversationParser.d.ts.map +1 -0
- package/dist/parsers/ConversationParser.js +795 -0
- package/dist/parsers/ConversationParser.js.map +1 -0
- package/dist/parsers/DecisionExtractor.d.ts +144 -0
- package/dist/parsers/DecisionExtractor.d.ts.map +1 -0
- package/dist/parsers/DecisionExtractor.js +434 -0
- package/dist/parsers/DecisionExtractor.js.map +1 -0
- package/dist/parsers/GitIntegrator.d.ts +156 -0
- package/dist/parsers/GitIntegrator.d.ts.map +1 -0
- package/dist/parsers/GitIntegrator.js +348 -0
- package/dist/parsers/GitIntegrator.js.map +1 -0
- package/dist/parsers/MistakeExtractor.d.ts +151 -0
- package/dist/parsers/MistakeExtractor.d.ts.map +1 -0
- package/dist/parsers/MistakeExtractor.js +460 -0
- package/dist/parsers/MistakeExtractor.js.map +1 -0
- package/dist/parsers/RequirementsExtractor.d.ts +166 -0
- package/dist/parsers/RequirementsExtractor.d.ts.map +1 -0
- package/dist/parsers/RequirementsExtractor.js +338 -0
- package/dist/parsers/RequirementsExtractor.js.map +1 -0
- package/dist/realtime/ConversationWatcher.d.ts +87 -0
- package/dist/realtime/ConversationWatcher.d.ts.map +1 -0
- package/dist/realtime/ConversationWatcher.js +204 -0
- package/dist/realtime/ConversationWatcher.js.map +1 -0
- package/dist/realtime/IncrementalParser.d.ts +83 -0
- package/dist/realtime/IncrementalParser.d.ts.map +1 -0
- package/dist/realtime/IncrementalParser.js +232 -0
- package/dist/realtime/IncrementalParser.js.map +1 -0
- package/dist/realtime/LiveExtractor.d.ts +72 -0
- package/dist/realtime/LiveExtractor.d.ts.map +1 -0
- package/dist/realtime/LiveExtractor.js +288 -0
- package/dist/realtime/LiveExtractor.js.map +1 -0
- package/dist/search/SemanticSearch.d.ts +121 -0
- package/dist/search/SemanticSearch.d.ts.map +1 -0
- package/dist/search/SemanticSearch.js +823 -0
- package/dist/search/SemanticSearch.js.map +1 -0
- package/dist/storage/BackupManager.d.ts +58 -0
- package/dist/storage/BackupManager.d.ts.map +1 -0
- package/dist/storage/BackupManager.js +223 -0
- package/dist/storage/BackupManager.js.map +1 -0
- package/dist/storage/ConversationStorage.d.ts +341 -0
- package/dist/storage/ConversationStorage.d.ts.map +1 -0
- package/dist/storage/ConversationStorage.js +792 -0
- package/dist/storage/ConversationStorage.js.map +1 -0
- package/dist/storage/DeletionService.d.ts +70 -0
- package/dist/storage/DeletionService.d.ts.map +1 -0
- package/dist/storage/DeletionService.js +253 -0
- package/dist/storage/DeletionService.js.map +1 -0
- package/dist/storage/GlobalIndex.d.ts +133 -0
- package/dist/storage/GlobalIndex.d.ts.map +1 -0
- package/dist/storage/GlobalIndex.js +310 -0
- package/dist/storage/GlobalIndex.js.map +1 -0
- package/dist/storage/SQLiteManager.d.ts +114 -0
- package/dist/storage/SQLiteManager.d.ts.map +1 -0
- package/dist/storage/SQLiteManager.js +636 -0
- package/dist/storage/SQLiteManager.js.map +1 -0
- package/dist/storage/migrations.d.ts +54 -0
- package/dist/storage/migrations.d.ts.map +1 -0
- package/dist/storage/migrations.js +285 -0
- package/dist/storage/migrations.js.map +1 -0
- package/dist/storage/schema.sql +436 -0
- package/dist/tools/ToolDefinitions.d.ts +946 -0
- package/dist/tools/ToolDefinitions.d.ts.map +1 -0
- package/dist/tools/ToolDefinitions.js +937 -0
- package/dist/tools/ToolDefinitions.js.map +1 -0
- package/dist/tools/ToolHandlers.d.ts +791 -0
- package/dist/tools/ToolHandlers.d.ts.map +1 -0
- package/dist/tools/ToolHandlers.js +3262 -0
- package/dist/tools/ToolHandlers.js.map +1 -0
- package/dist/types/ToolTypes.d.ts +824 -0
- package/dist/types/ToolTypes.d.ts.map +1 -0
- package/dist/types/ToolTypes.js +6 -0
- package/dist/types/ToolTypes.js.map +1 -0
- package/dist/utils/Logger.d.ts +70 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +131 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/McpConfig.d.ts +54 -0
- package/dist/utils/McpConfig.d.ts.map +1 -0
- package/dist/utils/McpConfig.js +136 -0
- package/dist/utils/McpConfig.js.map +1 -0
- package/dist/utils/ProjectMigration.d.ts +82 -0
- package/dist/utils/ProjectMigration.d.ts.map +1 -0
- package/dist/utils/ProjectMigration.js +416 -0
- package/dist/utils/ProjectMigration.js.map +1 -0
- package/dist/utils/constants.d.ts +75 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +105 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/safeJson.d.ts +37 -0
- package/dist/utils/safeJson.d.ts.map +1 -0
- package/dist/utils/safeJson.js +48 -0
- package/dist/utils/safeJson.js.map +1 -0
- package/dist/utils/sanitization.d.ts +45 -0
- package/dist/utils/sanitization.d.ts.map +1 -0
- package/dist/utils/sanitization.js +153 -0
- package/dist/utils/sanitization.js.map +1 -0
- package/dist/utils/worktree.d.ts +15 -0
- package/dist/utils/worktree.d.ts.map +1 -0
- package/dist/utils/worktree.js +86 -0
- package/dist/utils/worktree.js.map +1 -0
- package/package.json +98 -0
- package/scripts/changelog-check.sh +62 -0
- package/scripts/check-node.js +17 -0
- package/scripts/dev-config.js +56 -0
- 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
|