UNPKG

claude-chat-viewer

Version:

Interactive CLI tool to view and export Claude conversation histories

340 lines (283 loc) 13.4 kB
#!/usr/bin/env node const inquirer = require('inquirer'); const chalk = require('chalk'); const ora = require('ora'); const open = require('open'); const path = require('path'); const autocompletePrompt = require('inquirer-autocomplete-prompt'); // Register the autocomplete prompt type inquirer.registerPrompt('autocomplete', autocompletePrompt); const ConversationParser = require('../lib/conversation-parser'); const HtmlGenerator = require('../lib/html-generator'); const DiaryGenerator = require('../lib/diary-generator'); const ProjectDetector = require('../lib/project-detector'); class ClaudeChatCLI { constructor() { this.parser = new ConversationParser(); this.htmlGenerator = new HtmlGenerator(); this.diaryGenerator = new DiaryGenerator(); this.projectDetector = new ProjectDetector(); this.conversations = []; this.todoTheme = 'grid'; // Default theme this.diaryMode = false; this.parseCliArgs(); } parseCliArgs() { const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--todo-background=')) { this.todoTheme = args[i].split('=')[1]; } else if (args[i] === '--todo-background' && args[i + 1]) { this.todoTheme = args[i + 1]; i++; // Skip next argument } else if (args[i] === '--diary') { this.diaryMode = true; } else if (args[i] === '--help' || args[i] === '-h') { this.showHelp(); process.exit(0); } } // Validate theme const validThemes = ['grid', 'lines', 'graph', 'dots', 'clean']; if (!validThemes.includes(this.todoTheme)) { console.error(chalk.red(`❌ Invalid TODO theme: ${this.todoTheme}`)); console.error(chalk.yellow(`Valid themes: ${validThemes.join(', ')}`)); process.exit(1); } } showHelp() { console.log(chalk.cyan.bold('🤖 Claude Conversation Viewer')); console.log('Encuentra y visualiza tus conversaciones de Claude Code\n'); console.log(chalk.white('Uso:')); console.log(' claude-chat [opciones]\n'); console.log(chalk.white('Opciones:')); console.log(' --diary Generar diario de la conversación actual'); console.log(' --todo-background=TEMA Tema de fondo para TODOs'); console.log(' --help, -h Mostrar esta ayuda\n'); console.log(chalk.white('Modo Diary:')); console.log(' Ejecuta desde un directorio de proyecto para generar un diario'); console.log(' de la conversación actual con preguntas, respuestas y commits\n'); console.log(chalk.white('Temas disponibles para --todo-background:')); console.log(' grid Cuadrícula sutil (por defecto)'); console.log(' lines Papel pautado con líneas horizontales'); console.log(' graph Papel milimetrado denso'); console.log(' dots Bullet journal con puntos'); console.log(' clean Sin fondo especial\n'); console.log(chalk.white('Ejemplos:')); console.log(' claude-chat'); console.log(' claude-chat --diary'); console.log(' claude-chat --todo-background=dots'); console.log(' claude-chat --todo-background lines'); } async run() { const packageJson = require('../package.json'); console.log(chalk.cyan.bold('🤖 Claude Conversation Viewer')); console.log(chalk.yellow(`v${packageJson.version} - ${new Date().toISOString()}`)); if (this.diaryMode) { console.log(chalk.gray('Generando diario de la conversación actual...\n')); try { await this.runDiaryMode(); } catch (error) { console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } } else { console.log(chalk.gray('Encuentra y visualiza tus conversaciones de Claude Code\n')); try { await this.loadConversations(); await this.showMainMenu(); } catch (error) { console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } } } async loadConversations() { const spinner = ora('Escaneando conversaciones...').start(); try { this.conversations = await this.parser.getAllConversations(); spinner.succeed(`✅ Encontradas ${this.conversations.length} conversaciones`); } catch (error) { spinner.fail('Error al cargar conversaciones'); throw error; } } async showMainMenu() { while (true) { console.log('\n' + '='.repeat(80)); console.log(chalk.green(`📋 Total de conversaciones: ${this.conversations.length}\n`)); console.log(chalk.gray('💡 Escribe para filtrar por fecha, proyecto o contenido')); console.log(chalk.gray(' Usa las flechas para navegar, Enter para seleccionar\n')); try { const { action } = await inquirer.prompt([ { type: 'autocomplete', name: 'action', message: '🔍 Buscar conversación:', pageSize: 12, source: async (answersSoFar, input) => { return this.searchConversations(input); } } ]); if (action === 'exit') { console.log(chalk.cyan('👋 ¡Hasta luego!')); process.exit(0); } else if (action === 'reload') { await this.loadConversations(); } else if (typeof action === 'object' && action.conversation) { await this.showConversationDetail(action.conversation); } } catch (error) { // Handle Ctrl+C - exit gracefully console.log(chalk.cyan('\n👋 ¡Hasta luego!')); process.exit(0); } } } searchConversations(input = '') { const searchTerm = input.toLowerCase().trim(); // Filter conversations based on search term let filtered = this.conversations; if (searchTerm) { filtered = this.conversations.filter(conv => { // Search in date/time const dateStr = `${this.parser.formatDateCompact(conv.startTime)} ${this.parser.formatDateCompact(conv.endTime)}`.toLowerCase(); if (dateStr.includes(searchTerm)) return true; // Search in project name if (conv.projectName.toLowerCase().includes(searchTerm)) return true; // Search in first prompt if (conv.firstPrompt.toLowerCase().includes(searchTerm)) return true; // Search in session ID (for direct access) if (conv.sessionId.toLowerCase().includes(searchTerm)) return true; return false; }); } // Format choices const choices = filtered.map(conv => ({ name: this.formatConversationListItem(conv), value: { conversation: conv }, short: conv.projectName })); // Add control options at the end choices.push( new inquirer.Separator(), { name: chalk.cyan('🔄 Recargar conversaciones'), value: 'reload' }, { name: chalk.red('❌ Salir'), value: 'exit' } ); return choices; } formatConversationListItem(conv) { const startDate = chalk.cyan(this.parser.formatDateCompact(conv.startTime)); const endDate = chalk.gray(`- ${this.parser.formatDateCompact(conv.endTime)}`); const messageCount = chalk.yellow(String(conv.messageCount).padStart(4, ' ')); const project = chalk.blue(conv.projectName); const prompt = chalk.white(conv.firstPrompt); return `${startDate} ${endDate} [${messageCount}] ${project}\n 💬 ${prompt}`; } async showConversationDetail(conversation) { console.log('\n' + '='.repeat(80)); console.log(chalk.cyan.bold('📄 Resumen de la conversación\n')); const summary = this.parser.getConversationSummary(conversation); console.log(summary); console.log(chalk.gray('\n💡 Usa las flechas para seleccionar una opción\n')); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: '¿Qué quieres hacer?', choices: [ { name: '🌐 Generar visualización HTML', value: 'generate' }, { name: '← Volver al menú principal', value: 'back' } ], default: 0, loop: false } ]); if (action === 'generate') { await this.generateVisualization(conversation); } } async generateVisualization(conversation) { const spinner = ora('Generando visualización HTML...').start(); try { const result = await this.htmlGenerator.generateConversationHtml( conversation.filePath, process.cwd(), this.todoTheme ); spinner.succeed(`✅ HTML generado: ${result.outputPath}`); console.log(chalk.gray(`📊 ${result.messageCount} mensajes procesados`)); const { openFile } = await inquirer.prompt([ { type: 'confirm', name: 'openFile', message: '¿Abrir el archivo HTML en el navegador?', default: true } ]); if (openFile) { await open(result.outputPath); console.log(chalk.green('🌐 Archivo abierto en el navegador')); } console.log(chalk.cyan('\n✨ ¡Visualización completada!')); } catch (error) { spinner.fail('Error al generar la visualización'); console.error(chalk.red(error.message)); } } async runDiaryMode() { const spinner = ora('Detectando proyecto actual...').start(); try { // Detect current project and find conversation const projectInfo = await this.projectDetector.detectCurrentProject(); spinner.succeed(`✅ Proyecto detectado: ${path.basename(projectInfo.projectPath)}`); console.log(chalk.gray(`📁 Conversación: ${projectInfo.conversationInfo.filename}`)); console.log(chalk.gray(`📅 Última actualización: ${projectInfo.conversationInfo.modified.toLocaleString()}`)); // Generate diary spinner.start('Generando diario...'); const diary = await this.diaryGenerator.generateDiary( projectInfo.conversationPath, { projectPath: projectInfo.projectPath } ); // Format as markdown const markdown = this.diaryGenerator.formatAsMarkdown(diary); // Save to file const outputFilename = `conversation-diary-${new Date().toISOString().split('T')[0]}.md`; const outputPath = path.join(process.cwd(), outputFilename); require('fs').writeFileSync(outputPath, markdown, 'utf-8'); spinner.succeed(`✅ Diario generado: ${outputFilename}`); console.log(chalk.gray(`📊 ${diary.entries.length} entradas procesadas`)); // Ask if user wants to open the file const { openFile } = await inquirer.prompt([ { type: 'confirm', name: 'openFile', message: '¿Abrir el archivo markdown?', default: true } ]); if (openFile) { await open(outputPath); console.log(chalk.green('📖 Archivo abierto')); } console.log(chalk.cyan('\n✨ ¡Diario generado exitosamente!')); } catch (error) { spinner.fail('Error al generar el diario'); console.error(chalk.red(error.message)); if (error.message.includes('No Claude conversation found')) { console.log(chalk.yellow('\n💡 Asegúrate de ejecutar este comando desde un directorio de proyecto')); console.log(chalk.yellow(' donde hayas usado Claude Code anteriormente.')); } } } } // Run the CLI if (require.main === module) { const cli = new ClaudeChatCLI(); cli.run().catch(error => { console.error(chalk.red(`Fatal error: ${error.message}`)); process.exit(1); }); } module.exports = ClaudeChatCLI;