claude-conversation-manager
Version:
A CLI tool for managing Claude Code conversation history with parsing and i18n support
342 lines • 13.1 kB
JavaScript
;
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