UNPKG

claude-conversation-manager

Version:

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

342 lines 13.1 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.ConversationManager = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const os = __importStar(require("os")); const parser_1 = require("./parser"); class ConversationManager { config; constructor(customConfig) { const homeDir = os.homedir(); const claudeDir = path.join(homeDir, '.claude'); this.config = { claudeConfigPath: claudeDir, projectsPath: path.join(claudeDir, 'projects'), backupPath: path.join(claudeDir, 'backups'), maxBackups: 10, autoBackup: true, ...customConfig }; } /** * Initialize manager and ensure directories exist */ async initialize() { await fs.ensureDir(this.config.backupPath); if (!await fs.pathExists(this.config.projectsPath)) { throw new Error(`Claude Code projects directory not found: ${this.config.projectsPath}`); } } /** * List all conversations with metadata */ async listConversations() { const conversations = []; // Scan all project directories const projectDirs = await fs.readdir(this.config.projectsPath); for (const projectDir of projectDirs) { const projectPath = path.join(this.config.projectsPath, projectDir); const stat = await fs.stat(projectPath); if (!stat.isDirectory()) continue; // Scan conversation files in project directory const files = await fs.readdir(projectPath); const jsonlFiles = files.filter(f => f.endsWith('.jsonl')); for (const file of jsonlFiles) { const filePath = path.join(projectPath, file); const conversation = await parser_1.ConversationParser.parseFile(filePath); if (conversation) { conversations.push(conversation.metadata); } } } // Sort by modification date (newest first) return conversations.sort((a, b) => b.modified.getTime() - a.modified.getTime()); } /** * List conversations with grouping by project and summary * This helps identify multiple files that belong to the same conversation */ async listConversationsGrouped() { const conversations = await this.listConversations(); const grouped = {}; for (const conv of conversations) { // Create a unique key based on project path and summary const key = `${conv.workingDirectory}|||${conv.summary}`; if (!grouped[key]) { grouped[key] = []; } grouped[key].push(conv); } // Sort each group by creation date for (const key of Object.keys(grouped)) { grouped[key].sort((a, b) => a.created.getTime() - b.created.getTime()); } return grouped; } /** * Search conversations by criteria */ async searchConversations(options) { const allConversations = await this.listConversations(); return allConversations.filter(conv => { // Keyword search in summary if (options.keyword) { const keyword = options.keyword.toLowerCase(); if (!conv.summary.toLowerCase().includes(keyword)) { return false; } } // Project path filter if (options.project) { const project = options.project.toLowerCase(); if (!conv.projectPath.toLowerCase().includes(project)) { return false; } } // Date range filter if (options.dateFrom && conv.modified < options.dateFrom) { return false; } if (options.dateTo && conv.modified > options.dateTo) { return false; } // Size filter if (options.minSize && conv.fileSize < options.minSize) { return false; } if (options.maxSize && conv.fileSize > options.maxSize) { return false; } // Corruption filter if (options.corrupted !== undefined && conv.isCorrupted !== options.corrupted) { return false; } // Git branch filter if (options.hasGitBranch !== undefined) { const hasGit = !!conv.gitBranch; if (hasGit !== options.hasGitBranch) { return false; } } return true; }); } /** * Get conversation details by ID */ async getConversation(conversationId) { const filePath = await this.findConversationFile(conversationId); if (!filePath) return null; return parser_1.ConversationParser.parseFile(filePath); } /** * Delete a conversation by ID */ async deleteConversation(conversationId, createBackup = true) { const filePath = await this.findConversationFile(conversationId); if (!filePath) return false; if (createBackup && this.config.autoBackup) { await this.backupConversation(conversationId); } await fs.remove(filePath); return true; } /** * Delete multiple conversations */ async deleteConversations(conversationIds, createBackup = true) { if (createBackup && this.config.autoBackup) { await this.createBackup(); } const deleted = []; const failed = []; for (const id of conversationIds) { const success = await this.deleteConversation(id, false); // Skip individual backup since we did bulk backup if (success) { deleted.push(id); } else { failed.push(id); } } return { deleted, failed }; } /** * Delete all conversations for a project */ async deleteProjectConversations(projectPath, createBackup = true) { const conversations = await this.searchConversations({ project: projectPath }); const ids = conversations.map(c => c.id); const result = await this.deleteConversations(ids, createBackup); return result.deleted.length; } /** * Repair corrupted conversations */ async repairCorruptedConversations() { const corruptedConversations = await this.searchConversations({ corrupted: true }); const repaired = []; const failed = []; for (const conv of corruptedConversations) { const filePath = await this.findConversationFile(conv.id); if (filePath) { const result = await parser_1.ConversationParser.repairFile(filePath); if (result.success) { repaired.push(conv.id); } else { failed.push(conv.id); } } } return { repaired, failed }; } /** * Find conversation file path by ID */ async findConversationFile(conversationId) { const projectDirs = await fs.readdir(this.config.projectsPath); for (const projectDir of projectDirs) { const filePath = path.join(this.config.projectsPath, projectDir, `${conversationId}.jsonl`); if (await fs.pathExists(filePath)) { return filePath; } } return null; } /** * Create backup of specific conversation */ async backupConversation(conversationId) { const filePath = await this.findConversationFile(conversationId); if (!filePath) return null; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFileName = `conversation-${conversationId}-${timestamp}.jsonl`; const backupPath = path.join(this.config.backupPath, backupFileName); await fs.copy(filePath, backupPath); return backupPath; } /** * Create full backup of all conversations */ async createBackup() { const timestamp = new Date(); const dateStr = timestamp.toISOString().replace(/[:.]/g, '-'); const backupDir = path.join(this.config.backupPath, `backup-${dateStr}`); await fs.ensureDir(backupDir); await fs.copy(this.config.projectsPath, backupDir); // Calculate backup info const conversations = await this.listConversations(); const projectPaths = [...new Set(conversations.map(c => c.projectPath))]; const totalSize = conversations.reduce((sum, c) => sum + c.fileSize, 0); const backupInfo = { timestamp, filename: `backup-${dateStr}`, size: totalSize, conversationCount: conversations.length, projectPaths }; // Save backup metadata await fs.writeJson(path.join(backupDir, 'backup-info.json'), backupInfo, { spaces: 2 }); // Clean up old backups if needed await this.cleanupOldBackups(); return backupInfo; } /** * List available backups */ async listBackups() { const backups = []; if (!await fs.pathExists(this.config.backupPath)) { return backups; } const entries = await fs.readdir(this.config.backupPath); for (const entry of entries) { const backupDir = path.join(this.config.backupPath, entry); const infoFile = path.join(backupDir, 'backup-info.json'); if (await fs.pathExists(infoFile)) { try { const info = await fs.readJson(infoFile); info.timestamp = new Date(info.timestamp); backups.push(info); } catch (error) { // Skip corrupted backup info files } } } return backups.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } /** * Clean up old backups beyond the configured limit */ async cleanupOldBackups() { const backups = await this.listBackups(); if (backups.length <= this.config.maxBackups) { return; } const toDelete = backups.slice(this.config.maxBackups); for (const backup of toDelete) { const backupDir = path.join(this.config.backupPath, backup.filename); await fs.remove(backupDir); } } /** * Get storage statistics */ async getStorageStats() { const conversations = await this.listConversations(); const projectPaths = new Set(conversations.map(c => c.projectPath)); const corruptedCount = conversations.filter(c => c.isCorrupted).length; const sortedBySize = [...conversations].sort((a, b) => b.fileSize - a.fileSize); const sortedByDate = [...conversations].sort((a, b) => a.created.getTime() - b.created.getTime()); return { totalConversations: conversations.length, totalSize: conversations.reduce((sum, c) => sum + c.fileSize, 0), projectCount: projectPaths.size, corruptedCount, largestConversation: sortedBySize[0] || null, oldestConversation: sortedByDate[0] || null }; } } exports.ConversationManager = ConversationManager; //# sourceMappingURL=manager.js.map