UNPKG

claude-conversation-manager

Version:

A CLI tool for managing Claude Code conversation history with parsing and i18n support

299 lines 13.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConversationParser = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); class ConversationParser { /** * Parse a conversation file from disk */ static async parseFile(filePath) { try { if (!await fs.pathExists(filePath)) { return null; } const content = await fs.readFile(filePath, 'utf-8'); const lines = content.trim().split('\n').filter(line => line.trim()); if (lines.length === 0) { return null; } const messages = []; let summary; // Parse each line as JSON for (const line of lines) { try { const data = JSON.parse(line); if (data.type === 'summary') { summary = data; } else if (data.type === 'user' || data.type === 'assistant') { messages.push(data); } } catch (parseError) { // Skip corrupted lines but continue parsing console.warn(`Skipping corrupted line in ${filePath}:`, line.substring(0, 100)); } } const metadata = await this.extractMetadata(filePath, messages, summary); return { filePath, metadata, messages, summary }; } catch (error) { console.error(`Failed to parse conversation file ${filePath}:`, error); return null; } } /** * Extract metadata from conversation file */ static async extractMetadata(filePath, messages, summary) { const stats = await fs.stat(filePath); const fileName = path.basename(filePath, '.jsonl'); // Extract project path from file path // e.g. ~/.claude/projects/C--Users-user-Desktop-project/ -> C:\Users\user\Desktop\project const projectMatch = filePath.match(/projects[\/\\](.+?)[\/\\][^\/\\]+\.jsonl$/); let projectPath = 'Unknown'; let workingDirectory = 'Unknown'; if (projectMatch) { const encodedPath = projectMatch[1]; // Decode the path: C--Users-user-Desktop-project -> C:\Users\user\Desktop\project workingDirectory = encodedPath.replace(/--/g, ':\\').replace(/-/g, '\\'); // Extract just the folder name for display const pathParts = workingDirectory.split('\\'); projectPath = pathParts[pathParts.length - 1] || 'Unknown'; } // Find timestamps const timestamps = messages .map(m => new Date(m.timestamp)) .filter(d => !isNaN(d.getTime())) .sort((a, b) => a.getTime() - b.getTime()); const created = timestamps[0] || stats.birthtime; const modified = timestamps[timestamps.length - 1] || stats.mtime; // Detect corruption const corruptionCheck = this.checkCorruption(filePath, messages, stats.size); // Extract git branch from first message if available const gitBranch = messages[0]?.gitBranch; return { id: fileName, projectPath, workingDirectory, summary: summary?.summary || this.generateAutoSummary(messages), messageCount: messages.length, fileSize: stats.size, created, modified, isCorrupted: corruptionCheck.isCorrupted, corruptionReason: corruptionCheck.reason, gitBranch }; } /** * Check if a conversation file is corrupted */ static checkCorruption(filePath, messages, fileSize) { // Check for extremely large files (>10MB) - increased threshold for valid conversation chains if (fileSize > 10 * 1024 * 1024) { return { isCorrupted: true, reason: 'File too large (>10MB)' }; } // Check for extremely large single messages (>100KB) - increased threshold for context continuation for (const message of messages) { const messageSize = JSON.stringify(message).length; // Context continuation messages can be larger, so only flag truly excessive sizes if (messageSize > 100 * 1024) { return { isCorrupted: true, reason: 'Contains oversized message (>100KB)' }; } } // Check for missing essential fields if (messages.length === 0) { return { isCorrupted: true, reason: 'No valid messages found' }; } // More lenient timestamp validation - Claude Code creates valid files with various timestamp formats const invalidTimestamps = messages.filter(m => !m.timestamp || isNaN(new Date(m.timestamp).getTime())); if (invalidTimestamps.length > messages.length * 0.8) { // Changed from 0.5 to 0.8 return { isCorrupted: true, reason: 'Too many invalid timestamps' }; } // More lenient UUID validation - some continuation files may not have UUIDs for all messages const missingUuids = messages.filter(m => !m.uuid); if (missingUuids.length === messages.length) { // Only flag if ALL messages are missing UUIDs return { isCorrupted: true, reason: 'All messages missing UUIDs' }; } // Additional check: Validate JSON structure integrity try { for (const message of messages) { if (!message.type || (message.type !== 'user' && message.type !== 'assistant')) { // Allow other message types that might be valid in conversation chains if (!message.type || typeof message.type !== 'string') { return { isCorrupted: true, reason: 'Invalid message type structure' }; } } } } catch (error) { return { isCorrupted: true, reason: 'JSON structure validation failed' }; } return { isCorrupted: false }; } /** * Validate and potentially repair a conversation file */ static async repairFile(filePath) { try { const conversation = await this.parseFile(filePath); if (!conversation) { return { success: false, message: 'Could not parse conversation file' }; } if (!conversation.metadata.isCorrupted) { return { success: true, message: 'File is not corrupted' }; } // Attempt basic repairs const repairedMessages = conversation.messages.filter(m => { // Remove messages with invalid structure return m.uuid && m.timestamp && m.type && m.message; }); if (repairedMessages.length === 0) { return { success: false, message: 'No valid messages found for repair' }; } // Create minimal conversation structure if needed if (!conversation.summary) { conversation.summary = { type: 'summary', summary: 'Repaired conversation', leafUuid: repairedMessages[0]?.uuid || 'unknown' }; } // Write repaired file await this.writeConversationFile(filePath, conversation.summary, repairedMessages); return { success: true, message: `Repaired conversation with ${repairedMessages.length} messages` }; } catch (error) { return { success: false, message: `Repair failed: ${error}` }; } } /** * Generate automatic summary from messages (mimics Claude Code behavior) */ static generateAutoSummary(messages) { if (!messages || messages.length === 0) { return 'Empty conversation'; } // Try different strategies to extract meaningful content // Strategy 1: Find first user message with substantial content (new array format) const firstUserMessage = messages.find(m => m.type === 'user' && m.message?.content && typeof m.message.content === 'object' && Array.isArray(m.message.content)); if (firstUserMessage && Array.isArray(firstUserMessage.message.content)) { const textContent = firstUserMessage.message.content .filter(c => c && c.type === 'text' && c.text) .map(c => c.text) .join(' '); const cleaned = textContent.trim().replace(/\s+/g, ' '); if (cleaned.length > 10) { // At least some meaningful content return cleaned.length > 80 ? cleaned.substring(0, 77) + '...' : cleaned; } } // Strategy 2: Find any user message with string content (legacy format) const stringUserMessage = messages.find(m => m.type === 'user' && m.message?.content && typeof m.message.content === 'string'); if (stringUserMessage && typeof stringUserMessage.message.content === 'string') { const content = stringUserMessage.message.content.trim(); if (content.length > 10) { return content.length > 80 ? content.substring(0, 77) + '...' : content; } } // Strategy 3: Look for any meaningful text in any message type for (const message of messages) { if (message.message?.content) { let text = ''; if (typeof message.message.content === 'string') { text = message.message.content; } else if (Array.isArray(message.message.content)) { text = message.message.content .filter(c => c && c.type === 'text' && c.text) .map(c => c.text) .join(' '); } const cleaned = text.trim().replace(/\s+/g, ' '); if (cleaned.length > 10) { // Skip common system messages if (!cleaned.toLowerCase().includes('api error') && !cleaned.toLowerCase().includes('command-message') && !cleaned.toLowerCase().includes('claude ai usage limit')) { return cleaned.length > 80 ? cleaned.substring(0, 77) + '...' : cleaned; } } } } // Strategy 4: Check if it's a conversation continuation file const hasContextContinuation = messages.some(m => m.message && typeof m.message === 'object' && JSON.stringify(m.message).includes('context')); if (hasContextContinuation) { return '[Conversation continuation]'; } // Strategy 5: Check if it has only system/error messages const hasOnlySystemMessages = messages.every(m => !m.type || m.type !== 'user' || !m.message?.content || (typeof m.message.content === 'string' && (m.message.content.includes('API Error') || m.message.content.includes('usage limit') || m.message.content.length < 10))); if (hasOnlySystemMessages && messages.length > 0) { return '[System messages only]'; } // Fallback return messages.length === 0 ? 'Empty conversation' : 'No meaningful content found'; } /** * Write conversation data back to file */ static async writeConversationFile(filePath, summary, messages) { const lines = [ JSON.stringify(summary), ...messages.map(msg => JSON.stringify(msg)) ]; await fs.writeFile(filePath, lines.join('\n') + '\n', 'utf-8'); } } exports.ConversationParser = ConversationParser; //# sourceMappingURL=parser.js.map