UNPKG

create-ai-chat-context-experimental

Version:

Phase 2: TypeScript rewrite - AI Chat Context & Memory System with conversation extraction and AICF format support (powered by aicf-core v2.1.0).

185 lines 7.69 kB
/** * This file is part of create-ai-chat-context-experimental. * Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). * See LICENSE file for details. */ /** * Import Claude Command * Phase 5.4: Import Command Implementation - October 2025 * * Imports Claude conversation exports and generates memory files * * TODO: Future Enhancement (Phase 5.5+) * After writing to cache, trigger consolidation pipeline: * 1. CacheConsolidationAgent.consolidate() → .aicf/recent/ * 2. SessionConsolidationAgent.consolidate() → .aicf/sessions/ * 3. MemoryDropoffAgent.dropoff() → .aicf/medium/old/archive/ * * Current behavior: Writes to .cache/llm/claude/ only (cache-first approach ✅) * Missing: Automatic consolidation trigger (requires WatcherCommand integration) */ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'; import { join, basename } from 'path'; import chalk from 'chalk'; import ora from 'ora'; import { randomUUID } from 'crypto'; import { ClaudeParser } from '../parsers/ClaudeParser.js'; import { Ok, Err } from '../types/result.js'; /** * Import Claude conversation exports and generate memory files */ export class ImportClaudeCommand { cwd; output; parser; constructor(options = {}) { this.cwd = options.cwd || process.cwd(); this.output = options.output || '.cache/llm/claude'; this.parser = new ClaudeParser(); } /** * Execute import command */ async execute(filePath) { const spinner = ora(); try { // Step 1: Validate file exists if (!existsSync(filePath)) { return Err(new Error(`Claude export file not found: ${filePath}`)); } spinner.start(`📂 Reading Claude export: ${basename(filePath)}`); // Step 2: Read and parse JSON const fileContent = readFileSync(filePath, 'utf-8'); let exportData; try { exportData = JSON.parse(fileContent); } catch { spinner.fail('Invalid JSON format'); return Err(new Error('Claude export file is not valid JSON')); } spinner.succeed('✅ File loaded'); // Step 3: Parse using ClaudeParser spinner.start('🔍 Parsing Claude export...'); const parseResult = this.parser.parse(exportData); if (!parseResult.ok) { spinner.fail('Failed to parse Claude export'); return Err(parseResult.error); } const messages = parseResult.value; spinner.succeed(`✅ Parsed ${messages.length} messages`); if (messages.length === 0) { spinner.warn('⚠️ No messages found in export'); return Ok({ conversationId: 'empty', messageCount: 0, outputPath: this.output, filesCreated: [], message: 'No messages to import', }); } // Step 4: Create output directory spinner.start('📁 Creating output directory...'); const outputDir = join(this.cwd, this.output); mkdirSync(outputDir, { recursive: true }); spinner.succeed('✅ Output directory ready'); // Step 5: Generate checkpoint file spinner.start('💾 Generating checkpoint file...'); const firstMessage = messages[0]; if (!firstMessage) { spinner.fail('❌ No messages to checkpoint'); return Err(new Error('No messages extracted')); } const conversationId = firstMessage.conversationId; const checkpointId = randomUUID(); const checkpointFile = join(outputDir, `checkpoint-${checkpointId}.json`); const checkpoint = { sessionId: checkpointId, conversationId, source: 'claude', timestamp: new Date().toISOString(), messages: messages.map((msg) => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp, })), }; writeFileSync(checkpointFile, JSON.stringify(checkpoint, null, 2), 'utf-8'); spinner.succeed('✅ Checkpoint created'); // Step 6: Generate memory files spinner.start('📝 Generating memory files...'); const filesCreated = [checkpointFile]; // Generate AICF file const aicfContent = this.generateAICFContent(conversationId, messages.length); const aicfFile = join(outputDir, `${conversationId}.aicf`); writeFileSync(aicfFile, aicfContent, 'utf-8'); filesCreated.push(aicfFile); // Generate Markdown file const mdContent = this.generateMarkdownContent(conversationId, messages); const mdFile = join(outputDir, `${conversationId}.md`); writeFileSync(mdFile, mdContent, 'utf-8'); filesCreated.push(mdFile); spinner.succeed('✅ Memory files generated'); // Step 7: Display summary console.log(); console.log(chalk.green('✅ Claude Import Complete')); console.log(); console.log(chalk.dim('Summary:')); console.log(chalk.dim(` Conversation ID: ${conversationId}`)); console.log(chalk.dim(` Messages: ${messages.length}`)); console.log(chalk.dim(` Output: ${this.output}`)); console.log(chalk.dim(` Files Created: ${filesCreated.length}`)); console.log(); console.log(chalk.dim('Files:')); filesCreated.forEach((file) => { const relativePath = file.replace(this.cwd, '.'); console.log(chalk.dim(` - ${relativePath}`)); }); console.log(); return Ok({ conversationId, messageCount: messages.length, outputPath: this.output, filesCreated, message: `Successfully imported ${messages.length} messages from Claude export`, }); } catch (error) { spinner.fail('❌ Import failed'); return Err(error instanceof Error ? error : new Error(String(error))); } } /** * Generate AICF content */ generateAICFContent(conversationId, messageCount) { const timestamp = new Date().toISOString(); return `@CONVERSATION|id=${conversationId}|source=claude|timestamp=${timestamp} @METADATA|messages=${messageCount}|format=aicf|version=1.0 @IMPORT|source=claude-export|timestamp=${timestamp}|user=system `; } /** * Generate Markdown content */ generateMarkdownContent(conversationId, messages) { const lines = []; lines.push(`# ${conversationId}`); lines.push(''); lines.push(`**Source:** Claude Export`); lines.push(`**Imported:** ${new Date().toISOString()}`); lines.push(`**Messages:** ${messages.length}`); lines.push(''); lines.push('---'); lines.push(''); for (const msg of messages) { const role = msg.role === 'user' ? '👤 User' : '🤖 Claude'; lines.push(`## ${role}`); lines.push(''); lines.push(msg.content); lines.push(''); } return lines.join('\n'); } } //# sourceMappingURL=ImportClaudeCommand.js.map