UNPKG

claude-memory

Version:

Transform AI conversations into persistent project intelligence - Universal memory system for Claude

1,440 lines (1,274 loc) โ€ข 132 kB
#!/usr/bin/env node /** * Claude Memory - Universal AI Memory System * * Transform AI conversations into persistent project intelligence * * Usage: claude-memory <command> [args...] */ import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; import yaml from 'js-yaml'; // Import from lib modules import { ClaudeMemory } from '../lib/ClaudeMemory.js'; import { validatePath } from '../lib/utils/validators.js'; import { sanitizeInput, sanitizeDescription } from '../lib/utils/sanitizers.js'; import { formatOutput } from '../lib/utils/formatters.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageRoot = path.resolve(__dirname, '..'); // Load package.json for version info const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); // Global quiet mode flag let globalQuietMode = false; // Global output format let globalOutputFormat = 'text'; // Global verbose mode flag let globalVerboseMode = false; // Global dry run mode flag let globalDryRunMode = false; // Global config path override let globalConfigPath = null; // Global force mode flag let globalForceMode = false; // Global debug mode flag let globalDebugMode = false; // Helper to create memory instance with global flags function createMemory(projectPath, projectName = null, options = {}) { // Pass dry run mode through options if (globalDryRunMode) { options.dryRun = true; } // Pass force mode through options if (globalForceMode) { options.force = true; } // Check for config path from environment variable or global configPath const envConfigPath = process.env.CLAUDE_MEMORY_CONFIG; const finalConfigPath = globalConfigPath || envConfigPath; if (finalConfigPath) { options.configPath = finalConfigPath; } const memory = new ClaudeMemory(projectPath, projectName, options); memory.quietMode = globalQuietMode; memory.outputFormat = globalOutputFormat; memory.verboseMode = globalVerboseMode; memory.debugMode = globalDebugMode; return memory; } // Command implementations const commands = { async init(projectName, projectPath = process.cwd()) { debug('Init command called', { projectName, projectPath }); // Handle various argument patterns if (projectName && (projectName.startsWith('/') || projectName.startsWith('.') || projectName.includes('/'))) { projectPath = projectName; projectName = path.basename(projectPath); debug('Detected path-like project name, extracted name from path', { projectName, projectPath }); } if (!projectName) { projectName = path.basename(projectPath); debug('No project name provided, using directory name', { projectName }); } log('๐Ÿง  Initializing Claude Memory...'); log(`๐Ÿ“ Project: ${projectName}`); log(`๐Ÿ“‚ Path: ${projectPath}`); // Ensure project directory exists if (!fs.existsSync(projectPath)) { verbose(`Creating project directory: ${projectPath}`); fs.mkdirSync(projectPath, { recursive: true }); } process.chdir(projectPath); verbose(`Changed to project directory: ${projectPath}`); // Initialize memory system verbose('Creating memory system instance...'); const memory = createMemory(projectPath, projectName); const sessionId = memory.startSession('Project Setup', { project: projectName, initialized: new Date().toISOString(), tool: 'claude-memory' }); memory.recordDecision( 'Install Claude Memory', 'Enable persistent AI memory across sessions for better project intelligence', ['Manual documentation', 'External tools', 'No memory system'] ); // Update .gitignore commands.updateGitignore(projectPath); // Update package.json if it exists commands.updatePackageJson(projectPath); memory.log('โœ… Claude Memory initialized!'); log(`๐Ÿ“‹ Session ID: ${sessionId}`); log(''); log('๐Ÿš€ Next Steps:'); log('1. Tell Claude: "Load project memory and continue development"'); log('2. Start any conversation with memory-aware context'); log('3. Watch Claude learn and remember your project'); log(''); log('๐Ÿ“– See CLAUDE.md for project memory overview'); log('๐Ÿ’ก Use "cmem stats" to view memory statistics'); }, async stats(projectPath) { // Use current directory if no path provided const targetPath = projectPath || process.cwd(); try { const memory = createMemory(targetPath); const stats = memory.getMemoryStats(); const recentSessions = memory.getSessionHistory(3); const recentDecisions = memory.getRecentDecisions(3); // Structure data for different output formats const statsData = { statistics: { sessions: stats.sessions, decisions: stats.decisions, patterns: stats.patterns, tasks: stats.tasks, actions: stats.actions, knowledgeItems: stats.totalKnowledgeItems, knowledgeCategories: stats.knowledgeCategories }, recentSessions: recentSessions.map(s => ({ name: s.name, date: s.startTime.split('T')[0] })), recentDecisions: recentDecisions.map(d => ({ decision: d.decision, timestamp: d.timestamp })) }; // Output based on format if (globalOutputFormat === 'json') { output(formatOutput(statsData, 'json')); } else if (globalOutputFormat === 'yaml') { output(formatOutput(statsData, 'yaml')); } else { // Text format (default) console.log('\n๐Ÿ“Š Claude Memory Statistics\n'); console.log(`Sessions: ${stats.sessions}`); console.log(`Decisions: ${stats.decisions}`); console.log(`Patterns: ${stats.patterns}`); console.log(`Tasks: ${stats.tasks}`); console.log(`Actions: ${stats.actions}`); console.log(`Knowledge Items: ${stats.totalKnowledgeItems} (${stats.knowledgeCategories} categories)`); if (recentSessions.length > 0) { console.log('\n๐Ÿ•’ Recent Sessions:'); recentSessions.forEach(s => { console.log(` โ€ข ${s.name} (${s.startTime.split('T')[0]})`); }); } if (recentDecisions.length > 0) { console.log('\n๐Ÿค” Recent Decisions:'); recentDecisions.forEach(d => { console.log(` โ€ข ${d.decision}`); }); } } } catch (error) { if (error.message.includes('not initialized')) { console.error('โŒ Claude Memory not initialized in this directory'); console.log('๐Ÿ’ก Run: claude-memory init'); console.log(' Or specify a path: claude-memory stats /path/to/project'); } else { console.error('โŒ Error reading memory:', error.message); console.log('๐Ÿ’ก Try: claude-memory init'); console.log(' Or: claude-memory stats /path/to/project'); } } }, async search(...args) { let query = null; let projectPath = null; let outputFormat = globalOutputFormat; // Use global default let typeFilter = null; let limit = null; // Parse arguments and flags for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--json') { outputFormat = 'json'; // Override global setting } else if (arg === '--type' && args[i + 1]) { typeFilter = args[i + 1]; i++; } else if (arg === '--limit' && args[i + 1]) { limit = parseInt(args[i + 1]); i++; } else if (arg === '--help' || arg === '-h') { commands.showContextualHelp('search'); process.exit(0); } else if (arg?.startsWith('--')) { console.error(`โŒ Unknown flag: ${arg}`); console.log('Usage: claude-memory search "query" [--json] [--type TYPE] [--limit N] [path]'); return; } else if (!query && !arg?.startsWith('/') && !arg?.startsWith('.')) { // First non-flag, non-path argument is query query = arg; } else if (!projectPath) { // Path-like argument projectPath = arg; } } if (!query) { console.error('โŒ Search query required'); console.log('Usage: claude-memory search "query" [--json] [--type TYPE] [--limit N] [path]'); console.log(''); console.log('Examples:'); console.log(' claude-memory search "API"'); console.log(' claude-memory search "config" --type knowledge'); console.log(' claude-memory search "bug" --json --limit 5'); console.log(' claude-memory search "database" --type decisions --json'); return; } // Use current directory if no path provided const targetPath = projectPath || process.cwd(); try { const memory = new ClaudeMemory(targetPath); let results = memory.searchMemory(query); // Apply type filter if (typeFilter) { const validTypes = ['decisions', 'patterns', 'tasks', 'knowledge']; if (!validTypes.includes(typeFilter)) { console.error(`โŒ Invalid type filter: ${typeFilter}`); console.log(`Valid types: ${validTypes.join(', ')}`); return; } // Filter to only the specified type const filteredResults = { decisions: [], patterns: [], tasks: [], knowledge: [] }; filteredResults[typeFilter] = results[typeFilter] || []; results = filteredResults; } // Apply limit to each result type if (limit && limit > 0) { results.decisions = results.decisions.slice(0, limit); results.patterns = results.patterns.slice(0, limit); results.tasks = results.tasks.slice(0, limit); results.knowledge = results.knowledge.slice(0, limit); } if (outputFormat === 'json') { // JSON output const jsonOutput = { query, typeFilter, limit, totalResults: results.decisions.length + results.patterns.length + results.tasks.length + results.knowledge.length, results }; console.log(JSON.stringify(jsonOutput, null, 2)); return; } // Text output const typeText = typeFilter ? ` (type: ${typeFilter})` : ''; const limitText = limit ? ` (limit: ${limit})` : ''; console.log(`\n๐Ÿ” Search results for: "${query}"${typeText}${limitText}\n`); if (results.decisions.length > 0) { console.log('๐Ÿ“‹ Decisions:'); results.decisions.forEach(d => { console.log(` โ€ข ${d.decision} (${d.timestamp.split('T')[0]})`); console.log(` ${d.reasoning}`); if (d.alternatives?.length > 0) { console.log(` Alternatives: ${d.alternatives.join(', ')}`); } }); console.log(); } if (results.patterns.length > 0) { console.log('๐Ÿงฉ Patterns:'); results.patterns.forEach(p => { const priorityEmoji = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐ŸŸข' }[p.priority] || 'โšช'; console.log(` ${priorityEmoji} ${p.pattern}: ${p.description}`); if (p.effectiveness !== null && p.effectiveness !== undefined) { console.log(` Effectiveness: ${p.effectiveness}`); } if (p.status === 'resolved' && p.solution) { console.log(` โœ… Solution: ${p.solution}`); } }); console.log(); } if (results.tasks && results.tasks.length > 0) { console.log('โœ… Tasks:'); results.tasks.forEach(t => { const statusIcon = t.status === 'completed' ? 'โœ…' : t.status === 'in-progress' ? '๐Ÿ”„' : '๐Ÿ“'; console.log(` ${statusIcon} ${t.description} (${t.priority})`); if (t.assignee) console.log(` Assigned: ${t.assignee}`); if (t.dueDate) console.log(` Due: ${t.dueDate}`); if (t.completedAt) console.log(` Completed: ${t.completedAt.split('T')[0]}`); }); console.log(); } if (results.knowledge.length > 0) { console.log('๐Ÿ’ก Knowledge:'); results.knowledge.forEach(k => { console.log(` โ€ข [${k.category}] ${k.key}: ${k.value}`); console.log(` Updated: ${k.lastUpdated.split('T')[0]}`); }); console.log(); } const totalResults = results.decisions.length + results.patterns.length + results.tasks.length + results.knowledge.length; if (totalResults === 0) { console.log('No results found.'); } else { console.log(`๐Ÿ“Š Found ${totalResults} result${totalResults === 1 ? '' : 's'}`); } } catch (error) { if (outputFormat === 'json') { console.error(JSON.stringify({ error: error.message })); } else { console.error('โŒ Error searching memory:', error.message); } } }, async decision(decision, reasoning, alternatives = '', projectPath = process.cwd()) { if (!decision || !reasoning) { console.error('โŒ Decision and reasoning required'); console.log('Usage: claude-memory decision "Decision text" "Reasoning" "alt1,alt2"'); return; } try { // Sanitize inputs const sanitizedDecision = sanitizeDescription(decision, 200); const sanitizedReasoning = sanitizeDescription(reasoning, 1000); const validatedPath = validatePath(projectPath); const memory = createMemory(validatedPath); const alternativesArray = alternatives ? alternatives.split(',').map(s => sanitizeInput(s.trim(), 100)).filter(s => s.length > 0) : []; const id = memory.recordDecision(sanitizedDecision, sanitizedReasoning, alternativesArray); memory.log(`โœ… Decision recorded: ${sanitizedDecision}`); memory.log(`๐Ÿ“‹ Decision ID: ${id}`); } catch (error) { console.error('โŒ Error recording decision:', error.message); } }, async pattern(action, ...args) { const projectPath = process.cwd(); // Handle help flags if (action === '--help' || action === '-h') { commands.showContextualHelp('pattern'); process.exit(0); } if (action === 'add') { const pattern = args[0]; const description = args[1]; let effectiveness = null; let priority = 'medium'; // Parse optional flags for (let i = 2; i < args.length; i++) { if (args[i] === '--effectiveness' && args[i + 1]) { const val = parseFloat(args[i + 1]); if (!isNaN(val) && val >= 0 && val <= 1) { effectiveness = val; } else { console.error('โŒ Effectiveness must be a number between 0 and 1'); return; } i++; } else if (args[i] === '--priority' && args[i + 1]) { if (['critical', 'high', 'medium', 'low'].includes(args[i + 1])) { priority = args[i + 1]; } else { console.error('โŒ Priority must be: critical, high, medium, or low'); return; } i++; } } if (!pattern || !description) { console.error('โŒ Pattern name and description required'); console.log('Usage: claude-memory pattern add "Pattern name" "Description"'); console.log(' [--effectiveness 0.8] [--priority high]'); return; } try { const memory = new ClaudeMemory(projectPath); const patternId = memory.learnPattern(pattern, description, '', 1, effectiveness, priority); console.log(`โœ… Pattern added: ${pattern}`); console.log(`๐Ÿ“‹ Pattern ID: ${patternId}`); console.log(`๐Ÿ“ Description: ${description}`); console.log(`๐ŸŽฏ Priority: ${priority}`); if (effectiveness !== null) { console.log(`๐Ÿ“Š Effectiveness: ${effectiveness}`); } } catch (error) { console.error('โŒ Error adding pattern:', error.message); } } else if (action === 'list') { let priorityFilter = null; // Parse optional flags for (let i = 0; i < args.length; i++) { if (args[i] === '--priority' && args[i + 1]) { if (['critical', 'high', 'medium', 'low'].includes(args[i + 1])) { priorityFilter = args[i + 1]; } else { console.error('โŒ Priority must be: critical, high, medium, or low'); return; } i++; } } try { const memory = new ClaudeMemory(projectPath); let patterns = memory.patterns.filter(p => p.status === 'open'); if (priorityFilter) { patterns = patterns.filter(p => p.priority === priorityFilter); } if (patterns.length === 0) { const filterMsg = priorityFilter ? ` with priority '${priorityFilter}'` : ''; console.log(`๐Ÿ“‹ No open patterns found${filterMsg}.`); return; } // Group by priority const byPriority = { critical: patterns.filter(p => p.priority === 'critical'), high: patterns.filter(p => p.priority === 'high'), medium: patterns.filter(p => p.priority === 'medium'), low: patterns.filter(p => p.priority === 'low') }; const filterMsg = priorityFilter ? ` (${priorityFilter} priority)` : ''; console.log(`๐Ÿ“‹ Patterns${filterMsg} (${patterns.length} total)\n`); ['critical', 'high', 'medium', 'low'].forEach(priority => { if (byPriority[priority].length > 0) { byPriority[priority].forEach(p => { const priorityEmoji = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐ŸŸข' }[priority]; console.log(`[${p.id}] ${priorityEmoji} ${priority.toUpperCase()}: ${p.pattern}`); console.log(` ${p.description}`); if (p.effectiveness !== null && p.effectiveness !== undefined) { console.log(` Effectiveness: ${p.effectiveness}`); } console.log(); }); } }); } catch (error) { console.error('โŒ Error listing patterns:', error.message); } } else if (action === 'search') { const query = args[0]; if (!query) { console.error('โŒ Search query required'); console.log('Usage: claude-memory pattern search "query"'); return; } try { const memory = new ClaudeMemory(projectPath); const patterns = memory.patterns.filter(p => p.pattern.toLowerCase().includes(query.toLowerCase()) || p.description.toLowerCase().includes(query.toLowerCase()) ); if (patterns.length === 0) { console.log(`๐Ÿ” No patterns found for: "${query}"`); return; } console.log(`๐Ÿ” Pattern search results for: "${query}" (${patterns.length} found)\n`); patterns.forEach(p => { const status = p.status === 'resolved' ? 'โœ…' : '๐ŸŸข'; const priorityEmoji = { critical: '๐Ÿ”ด', high: '๐ŸŸ ', medium: '๐ŸŸก', low: '๐ŸŸข' }[p.priority]; console.log(`${status} [${p.id}] ${priorityEmoji} ${p.pattern}`); console.log(` ${p.description}`); if (p.effectiveness !== null && p.effectiveness !== undefined) { console.log(` Effectiveness: ${p.effectiveness} | Priority: ${p.priority}`); } if (p.status === 'resolved') { console.log(` Solution: ${p.solution}`); } console.log(); }); } catch (error) { console.error('โŒ Error searching patterns:', error.message); } } else if (action === 'resolve') { const patternId = args[0]; const solution = args[1]; if (!patternId || !solution) { console.error('โŒ Pattern ID and solution required'); console.log('Usage: claude-memory pattern resolve <pattern-id> "solution"'); return; } try { const memory = new ClaudeMemory(projectPath); const success = memory.resolvePattern(patternId, solution); if (success) { console.log(`โœ… Pattern resolved: ${patternId}`); console.log(`๐Ÿ’ก Solution: ${solution}`); } else { console.error(`โŒ Pattern not found: ${patternId}`); } } catch (error) { console.error('โŒ Error resolving pattern:', error.message); } } else { // Backward compatibility: Traditional pattern learning const pattern = action; const description = args[0]; let effectiveness = null; let priority = 'medium'; // Smart argument parsing for backward compatibility if (args[1]) { const arg1 = args[1]; const num = parseFloat(arg1); // Check if it's a valid effectiveness score (0-1) if (!isNaN(num) && num >= 0 && num <= 1) { effectiveness = num; // Check for priority as third argument if (args[2] && ['critical', 'high', 'medium', 'low'].includes(args[2])) { priority = args[2]; } } else if (['critical', 'high', 'medium', 'low'].includes(arg1)) { // First optional arg is priority priority = arg1; } } if (!pattern || !description) { console.error('โŒ Pattern name and description required'); console.log('\nUsage:'); console.log(' claude-memory pattern add "Pattern name" "Description" [--effectiveness 0.8] [--priority high]'); console.log(' claude-memory pattern list [--priority high]'); console.log(' claude-memory pattern search "query"'); console.log(' claude-memory pattern resolve <pattern-id> "solution"'); console.log('\nBackward compatibility:'); console.log(' claude-memory pattern "Pattern name" "Description" [effectiveness] [priority]'); return; } try { const memory = new ClaudeMemory(projectPath); const patternId = memory.learnPattern(pattern, description, '', 1, effectiveness, priority); console.log(`โœ… Pattern learned: ${pattern}`); console.log(`๐Ÿ“‹ Pattern ID: ${patternId}`); console.log(`๐Ÿ“ Description: ${description}`); console.log(`๐ŸŽฏ Priority: ${priority}`); if (effectiveness !== null) { console.log(`๐Ÿ“Š Effectiveness: ${effectiveness}`); } } catch (error) { console.error('โŒ Error learning pattern:', error.message); } } }, async task(action, ...args) { const projectPath = process.cwd(); // Handle help flags if (action === '--help' || action === '-h') { commands.showContextualHelp('task'); process.exit(0); } if (action === 'add') { const description = args[0]; let priority = 'medium'; let assignee = null; let dueDate = null; // Parse optional flags for (let i = 1; i < args.length; i++) { if (args[i] === '--priority' && args[i + 1]) { priority = args[i + 1]; i++; } else if (args[i] === '--assignee' && args[i + 1]) { assignee = args[i + 1]; i++; } else if (args[i] === '--due' && args[i + 1]) { dueDate = args[i + 1]; i++; } } if (!description) { console.error('โŒ Task description required'); console.log('Usage: claude-memory task add "description" ' + '[--priority high|medium|low] [--assignee name] [--due date]'); return; } try { // Sanitize and validate inputs const sanitizedDescription = sanitizeDescription(description, 300); const validPriorities = ['high', 'medium', 'low']; const validPriority = validPriorities.includes(priority) ? priority : 'medium'; const sanitizedAssignee = assignee ? sanitizeInput(assignee, 50) : null; const memory = new ClaudeMemory(projectPath); const taskId = memory.addTask(sanitizedDescription, validPriority, 'open', sanitizedAssignee, dueDate); console.log(`โœ… Task added: ${sanitizedDescription}`); console.log(`๐Ÿ“‹ Task ID: ${taskId}`); console.log(`๐ŸŽฏ Priority: ${validPriority}`); } catch (error) { console.error('โŒ Error adding task:', error.message); } } else if (action === 'complete') { const taskId = args[0]; const outcome = args[1] || ''; if (!taskId) { console.error('โŒ Task ID required'); console.log('Usage: claude-memory task complete <task-id> ["outcome"]'); return; } try { const memory = new ClaudeMemory(projectPath); const success = memory.completeTask(taskId, outcome); if (success) { console.log(`โœ… Task completed: ${taskId}`); if (outcome) console.log(`๐Ÿ“ Outcome: ${outcome}`); } else { console.error(`โŒ Task not found: ${taskId}`); } } catch (error) { console.error('โŒ Error completing task:', error.message); } } else if (action === 'list') { const status = args[0]; try { const memory = new ClaudeMemory(projectPath); const tasks = memory.getTasks(status); console.log(`\n๐Ÿ“‹ Tasks${status ? ` (${status})` : ''}:\n`); if (tasks.length === 0) { console.log('No tasks found.'); } else { tasks.forEach(task => { const statusIcon = task.status === 'completed' ? 'โœ…' : task.status === 'in-progress' ? '๐Ÿ”„' : '๐Ÿ“'; console.log(`${statusIcon} ${task.id}: ${task.description}`); console.log(` Priority: ${task.priority} | Status: ${task.status}`); if (task.assignee) console.log(` Assigned: ${task.assignee}`); if (task.dueDate) console.log(` Due: ${task.dueDate}`); if (task.completedAt) console.log(` Completed: ${task.completedAt.split('T')[0]}`); console.log(''); }); } } catch (error) { console.error('โŒ Error listing tasks:', error.message); } } else if (action === 'add-bulk') { const filePath = args[0]; if (!filePath) { console.error('โŒ JSON file path required'); console.log('Usage: claude-memory task add-bulk <tasks.json>'); console.log('\nExample JSON format:'); console.log(JSON.stringify({ tasks: [ { description: 'Task 1', priority: 'high', assignee: 'Alice' }, { description: 'Task 2', priority: 'medium' } ] }, null, 2)); return; } try { // Read and parse the JSON file const resolvedPath = path.resolve(filePath); if (!fs.existsSync(resolvedPath)) { console.error(`โŒ File not found: ${filePath}`); return; } const fileContent = fs.readFileSync(resolvedPath, 'utf8'); const data = JSON.parse(fileContent); // Validate against schema (removed for now, will add validation later) if (!data.tasks || !Array.isArray(data.tasks)) { console.error('โŒ Invalid JSON format. Must have a "tasks" array.'); return; } const memory = new ClaudeMemory(projectPath); let addedCount = 0; const taskIds = []; // Add each task for (const task of data.tasks) { if (!task.description) { console.warn('โš ๏ธ Skipping task without description'); continue; } const sanitizedDescription = sanitizeDescription(task.description, 300); const priority = ['high', 'medium', 'low'].includes(task.priority) ? task.priority : 'medium'; const status = ['pending', 'in-progress', 'completed'].includes(task.status) ? task.status : 'pending'; const assignee = task.assignee ? sanitizeInput(task.assignee, 50) : null; const dueDate = task.dueDate || null; const taskId = memory.addTask(sanitizedDescription, priority, status, assignee, dueDate); taskIds.push(taskId); addedCount++; verbose(`Added task ${taskId}: ${sanitizedDescription}`); } console.log(`โœ… Bulk import complete: ${addedCount} tasks added`); console.log(`๐Ÿ“‹ Task IDs: ${taskIds.join(', ')}`); } catch (error) { if (error.code === 'ENOENT') { console.error(`โŒ File not found: ${filePath}`); } else if (error instanceof SyntaxError) { console.error(`โŒ Invalid JSON in file: ${error.message}`); } else { console.error('โŒ Error importing tasks:', error.message); } } } else if (action === 'export') { const format = args[0] || 'json'; const status = args[1]; // optional status filter try { const memory = new ClaudeMemory(projectPath); const tasks = memory.getTasks(status); // Transform tasks to export format const exportData = { exportedAt: new Date().toISOString(), totalTasks: tasks.length, tasks: tasks.map(task => ({ id: task.id, description: task.description, priority: task.priority, status: task.status, assignee: task.assignee, dueDate: task.dueDate, createdAt: task.createdAt, completedAt: task.completedAt, outcome: task.outcome })) }; if (format === 'json') { output(JSON.stringify(exportData, null, 2)); } else if (format === 'github-issues') { // Format for GitHub issue creation console.log('# Tasks for GitHub Issues\n'); tasks.forEach(task => { console.log(`## ${task.description}`); console.log(`Priority: ${task.priority}`); console.log(`Status: ${task.status}`); if (task.assignee) console.log(`Assignee: ${task.assignee}`); if (task.dueDate) console.log(`Due: ${task.dueDate}`); console.log('\n---\n'); }); } else { console.error(`โŒ Unknown export format: ${format}`); console.log('Valid formats: json, github-issues'); } } catch (error) { console.error('โŒ Error exporting tasks:', error.message); } } else { console.error('โŒ Task action must be: add, complete, list, add-bulk, or export'); } }, async backup(projectPath) { // Use current directory if no path provided const targetPath = projectPath || process.cwd(); try { const memory = new ClaudeMemory(targetPath); memory.backup(); console.log('โœ… Memory backed up'); } catch (error) { console.error('โŒ Error backing up memory:', error.message); } }, async export(...args) { let filename = null; let projectPath = null; let sanitized = false; let format = 'json'; let types = null; let dateFrom = null; let dateTo = null; let includeMetadata = true; // Parse arguments and flags for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--sanitized') { sanitized = true; } else if (arg === '--format' && args[i + 1]) { format = args[i + 1].toLowerCase(); i++; } else if (arg === '--types' && args[i + 1]) { types = args[i + 1].split(',').map(t => t.trim()); i++; } else if (arg === '--from' && args[i + 1]) { dateFrom = new Date(args[i + 1]); i++; } else if (arg === '--to' && args[i + 1]) { dateTo = new Date(args[i + 1]); i++; } else if (arg === '--no-metadata') { includeMetadata = false; } else if (arg === '--help' || arg === '-h') { commands.showContextualHelp('export'); process.exit(0); } else if (arg?.startsWith('--')) { console.error(`โŒ Unknown flag: ${arg}`); console.log('Usage: claude-memory export [filename] [options] [path]'); console.log('Options:'); console.log(' --format <type> Output format: json, yaml, csv, markdown (default: json)'); console.log(' --types <list> Comma-separated list of types to export'); console.log(' (tasks, patterns, decisions, knowledge, sessions)'); console.log(' --from <date> Export items from this date (ISO format)'); console.log(' --to <date> Export items up to this date (ISO format)'); console.log(' --sanitized Remove sensitive information'); console.log(' --no-metadata Exclude metadata from export'); return; } else if (!filename && !arg?.startsWith('/') && !arg?.startsWith('.')) { // First non-flag, non-path argument is filename filename = sanitizeInput(arg, 100); } else if (!projectPath) { // Path-like argument projectPath = arg; } } // Validate format const validFormats = ['json', 'yaml', 'csv', 'markdown']; if (!validFormats.includes(format)) { console.error(`โŒ Invalid format: ${format}`); console.log(`Valid formats: ${validFormats.join(', ')}`); return; } // Validate types if specified const validTypes = ['tasks', 'patterns', 'decisions', 'knowledge', 'sessions']; if (types) { const invalidTypes = types.filter(t => !validTypes.includes(t)); if (invalidTypes.length > 0) { console.error(`โŒ Invalid types: ${invalidTypes.join(', ')}`); console.log(`Valid types: ${validTypes.join(', ')}`); return; } } // Use current directory if no path provided const targetPath = validatePath(projectPath || process.cwd()); try { const memory = new ClaudeMemory(targetPath); let data = memory.exportMemory(); // Filter by types if specified if (types) { const filteredData = { exportedAt: data.exportedAt, metadata: includeMetadata ? data.metadata : undefined }; types.forEach(type => { if (data[type]) { filteredData[type] = data[type]; } }); data = filteredData; } // Apply date filtering if (dateFrom || dateTo) { data = this.filterByDateRange(data, dateFrom, dateTo); } // Sanitize data if requested if (sanitized) { data = this.sanitizeExportData(data); } // Transform data based on format let output; let extension; switch (format) { case 'json': output = JSON.stringify(data, null, 2); extension = 'json'; break; case 'yaml': output = this.convertToYAML(data); extension = 'yaml'; break; case 'csv': output = this.convertToCSV(data); extension = 'csv'; break; case 'markdown': output = this.convertToMarkdown(data); extension = 'md'; break; } // Generate filename if not provided const dateStr = new Date().toISOString().split('T')[0]; const sanitizedSuffix = sanitized ? '-sanitized' : ''; const typesSuffix = types ? `-${types.join('-')}` : ''; const exportFile = filename || `claude-memory-export-${dateStr}${typesSuffix}${sanitizedSuffix}.${extension}`; // Write the file fs.writeFileSync(exportFile, output); // Report success console.log(`โœ… Memory exported to: ${exportFile}`); // Count exported items let itemCount = 0; const exportedTypes = []; Object.keys(data).forEach(key => { if (Array.isArray(data[key])) { itemCount += data[key].length; exportedTypes.push(`${key}: ${data[key].length}`); } }); console.log(`๐Ÿ“Š Exported ${itemCount} items (${exportedTypes.join(', ')})`); console.log(`๐Ÿ“„ Format: ${format.toUpperCase()}`); if (sanitized) { console.log('๐Ÿงน Data sanitized (sensitive information removed)'); } if (dateFrom || dateTo) { const fromStr = dateFrom ? dateFrom.toISOString().split('T')[0] : 'beginning'; const toStr = dateTo ? dateTo.toISOString().split('T')[0] : 'now'; console.log(`๐Ÿ“… Date range: ${fromStr} to ${toStr}`); } } catch (error) { console.error('โŒ Error exporting memory:', error.message); } }, sanitizeExportData(data) { // Create a deep copy and remove sensitive information const sanitized = JSON.parse(JSON.stringify(data)); // Remove or anonymize sensitive fields if (sanitized.sessions) { sanitized.sessions = sanitized.sessions.map(session => ({ ...session, context: sanitized.context ? {} : session.context // Remove context details })); } if (sanitized.decisions) { sanitized.decisions = sanitized.decisions.map(decision => ({ ...decision, reasoning: decision.reasoning?.length > 100 ? decision.reasoning.substring(0, 100) + '...' : decision.reasoning })); } // Remove personal identifiers if (sanitized.tasks) { sanitized.tasks = sanitized.tasks.map(task => ({ ...task, assignee: task.assignee ? 'REDACTED' : null })); } return sanitized; }, filterByDateRange(data, dateFrom, dateTo) { const filtered = { ...data }; // Helper function to check if date is in range const isInRange = (dateStr) => { const date = new Date(dateStr); if (dateFrom && date < dateFrom) return false; if (dateTo && date > dateTo) return false; return true; }; // Filter tasks if (filtered.tasks) { filtered.tasks = filtered.tasks.filter(task => isInRange(task.createdAt || task.timestamp) ); } // Filter patterns if (filtered.patterns) { filtered.patterns = filtered.patterns.filter(pattern => isInRange(pattern.createdAt || pattern.timestamp) ); } // Filter decisions if (filtered.decisions) { filtered.decisions = filtered.decisions.filter(decision => isInRange(decision.timestamp) ); } // Filter sessions if (filtered.sessions) { filtered.sessions = filtered.sessions.filter(session => isInRange(session.startTime) ); } // Filter knowledge if (filtered.knowledge) { // Knowledge is structured differently - it's an object of categories const filteredKnowledge = {}; Object.entries(filtered.knowledge).forEach(([category, items]) => { const filteredItems = {}; Object.entries(items).forEach(([key, data]) => { if (isInRange(data.lastUpdated || data.timestamp)) { filteredItems[key] = data; } }); if (Object.keys(filteredItems).length > 0) { filteredKnowledge[category] = filteredItems; } }); filtered.knowledge = filteredKnowledge; } return filtered; }, convertToYAML(data) { // Use js-yaml library for proper YAML conversion return yaml.dump(data, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }); }, convertToCSV(data) { const csvLines = []; // Helper to escape CSV values const escapeCSV = (value) => { if (value === null || value === undefined) return ''; const str = String(value); if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, '""')}"`; } return str; }; // Export each data type as a separate CSV section if (data.tasks && data.tasks.length > 0) { csvLines.push('=== TASKS ==='); csvLines.push('ID,Description,Priority,Status,Assignee,Due Date,Created At,Completed At'); data.tasks.forEach(task => { csvLines.push([ escapeCSV(task.id), escapeCSV(task.description), escapeCSV(task.priority), escapeCSV(task.status), escapeCSV(task.assignee), escapeCSV(task.dueDate), escapeCSV(task.createdAt), escapeCSV(task.completedAt) ].join(',')); }); csvLines.push(''); } if (data.patterns && data.patterns.length > 0) { csvLines.push('=== PATTERNS ==='); csvLines.push('ID,Pattern,Description,Priority,Effectiveness,Status,Solution'); data.patterns.forEach(pattern => { csvLines.push([ escapeCSV(pattern.id), escapeCSV(pattern.pattern), escapeCSV(pattern.description), escapeCSV(pattern.priority), escapeCSV(pattern.effectiveness), escapeCSV(pattern.status), escapeCSV(pattern.solution) ].join(',')); }); csvLines.push(''); } if (data.decisions && data.decisions.length > 0) { csvLines.push('=== DECISIONS ==='); csvLines.push('ID,Decision,Reasoning,Alternatives,Timestamp'); data.decisions.forEach(decision => { csvLines.push([ escapeCSV(decision.id), escapeCSV(decision.decision), escapeCSV(decision.reasoning), escapeCSV(decision.alternativesConsidered), escapeCSV(decision.timestamp) ].join(',')); }); csvLines.push(''); } return csvLines.join('\n'); }, convertToMarkdown(data) { const mdLines = []; // Header mdLines.push('# Claude Memory Export'); mdLines.push(`\n**Exported**: ${data.exportedAt || new Date().toISOString()}`); if (data.metadata) { mdLines.push(`\n**Project**: ${data.metadata.projectName || 'Unknown'}`); mdLines.push(`**Version**: ${data.metadata.version || 'Unknown'}`); } // Tasks if (data.tasks && data.tasks.length > 0) { mdLines.push('\n## Tasks\n'); // Group by status const tasksByStatus = {}; data.tasks.forEach(task => { const status = task.status || 'pending'; if (!tasksByStatus[status]) tasksByStatus[status] = []; tasksByStatus[status].push(task); }); Object.entries(tasksByStatus).forEach(([status, tasks]) => { mdLines.push(`### ${status.charAt(0).toUpperCase() + status.slice(1)}\n`); tasks.forEach(task => { const checkbox = task.status === 'completed' ? '[x]' : '[ ]'; mdLines.push(`- ${checkbox} **${task.description}** (${task.priority})`); if (task.assignee) mdLines.push(` - Assigned to: ${task.assignee}`); if (task.dueDate) mdLines.push(` - Due: ${task.dueDate}`); if (task.completedAt) mdLines.push(` - Completed: ${task.completedAt}`); }); mdLines.push(''); }); } // Patterns if (data.patterns && data.patterns.length > 0) { mdLines.push('\n## Patterns\n'); // Group by priority const patternsByPriority = {}; data.patterns.forEach(pattern => { const priority = pattern.priority || 'medium'; if (!patternsByPriority[priority]) patternsByPriority[priority] = []; patternsByPriority[priority].push(pattern); }); ['critical', 'high', 'medium', 'low'].forEach(priority => { if (patternsByPriority[priority]) { mdLines.push(`### ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority\n`); patternsByPriority[priority].forEach(pattern => { mdLines.push(`#### ${pattern.pattern}`); mdLines.push(`\n${pattern.description}`); if (pattern.effectiveness !== null && pattern.effectiveness !== undefined) { mdLines.push(`\n- **Effectiveness**: ${pattern.effectiveness}`); } if (pattern.status === 'resolved' && pattern.solution) { mdLines.push(`- **Solution**: ${pattern.solution}`); } mdLines.push(''); }); } }); } // Decisions if (data.decisions && data.decisions.length > 0) { mdLines.push('\n## Decisions\n'); data.decisions.forEach(decision => { mdLines.push(`### ${decision.decision}`); mdLines.push(`\n**Reasoning**: ${decision.reasoning}`); if (decision.alternativesConsidered) { mdLines.push(`\n**Alternatives Considered**: ${decision.alternativesConsidered}`); } mdLines.push(`\n*${new Date(decision.timestamp).toLocaleDateString()}*\n`); }); } // Knowledge if (data.knowledge && Object.keys(data.knowledge).length > 0) { mdLines.push('\n## Knowledge Base\n'); Object.entries(data.knowledge).forEach(([category, items]) => { mdLines.push(`### ${category}\n`); Object.entries(items).forEach(([key, data]) => { mdLines.push(`- **${key}**: ${data.value}`); mdLines.push(` - Updated: ${data.lastUpdated}`); }); mdLines.push(''); }); } // Sessions if (data.sessions && data.sessions.length > 0) { mdLines.push('\n## Sessions\n'); data.sessions.forEach(session => { const status = session.status || (session.endTime ? 'completed' : 'active'); mdLines.push(`### ${session.name}`); mdLines.push(`- **Status**: ${status}`); mdLines.push(`- **Started**: ${session.startTime}`); if (session.endTime) { mdLines.push(`- **Ended**: ${session.endTime}`); } if (session.outcome) { mdLines.push(`- **Outcome**: ${session.outcome}`); } mdLines.push(''); }); } return mdLines.join('\n'); }, session(action, ...args) { const projectPath = process.cwd(); // Handle help flags if (action === '--help' || action === '-h') { commands.showContextualHelp('session'); process.exit(0); } if (action === 'start') { const sessionName = args[0]; const context = args[1] || '{}'; if (!sessionName) { console.error('โŒ Session name required'); console.log('Usage: claude-memory session start "Session Name" [context]'); return; } try { const memory = new ClaudeMemory(projectPath); let contextObj = {}; try { contextObj = JSON.parse(context); } catch (e) { console.warn('โš ๏ธ Invalid context JSON, using empty context'); } const sessionId = memory.startSession(sessionName, contextObj); console.log(`๐Ÿš€ Started session: ${sessionName}`); console.log(`๐Ÿ“‹ Session ID: ${sessionId}`); } catch (error) { console.error('โŒ Error starting session:', error.message); } } else if (action === 'end') { const sessionIdOrOutcome = args[0]; const outcome = args[1] || 'Session completed'; try { const memory = new ClaudeMemory(projectPath); // Check if first arg is a session ID if (sessionIdOrOutcome && sessionIdOrOutcome.match(/^\d{4}-\d{2}-\d{2}-/)) { const success = memory.endSessionById(sessionIdOrOutcome, outcome); if (success) { console.log(`โœ… Session ${sessionIdOrOutcome} ended: ${outcome}`); } else { console.error(`โŒ Session not found or already ended: ${sessionIdOrOutcome}`); } } else { // End current session const success = memory.endSession(sessionIdOrOutcome || outcome); if (success) { console.log(`โœ… Current session ended: ${sessionIdOrOutcome || outcome}`); } else { console.log('โ„น๏ธ No active session to end'); } } } catch (error) { console.error('โŒ Error ending session:', error.message); } } else if (action === 'list') { try { const memory = new ClaudeMemory(projectPath); const sessions = memory.getSessionHistory(10); console.log('\n๐Ÿ“š Recent Sessions:'); sessions.forEach(session => { console.log(` ${session.id} - ${session.name} (${session.status})`); }); } catch (error) { console.error('โŒ Error listing sessions:', error.message); } } else if (action === 'cleanup') { try { const memory = new ClaudeMemory(projectPath); const cleanedCount = memory.cleanupSessions(); console.log(`โœ… Cleaned up ${cleanedCount} active sessions`); } catch (error) { console.error('โŒ Error cleaning up sessions:', error.message); } } else { console.error('โŒ Session action must be: start, end, list, or cleanup'); } }, async import(...args) { let filename = null; let projectPath = null; let mode = 'merge'; // merge or replace let types = null; let dryRun = globalDryRunMode; // Parse arguments and flags for (let i = 0; i < args.length; i++) { const arg = args[i]; const nextArg = args[i + 1]; if (arg === '--mode' && nextArg) { if (!['merge', 'replace'].includes(nextArg)) { console.error(`โŒ Invalid mode: ${nextArg}. Valid options: merge, replace`); process.exit(1); } mode = nextArg; i++; } else if (arg === '--types' && nextArg) { types = nextArg.split(',').map(t => t.trim()); i++; } else if (arg === '--dry-run') { dryRun = true; } else if (arg === '--help' || arg === '-h') { commands.showContextualHelp('import'); process.exit(0); } else if (arg?.startsWith('--')) { console.error(`โŒ Unknown flag: ${arg}`); console.log('Usage: claude-memory import <filename> [options] [path]'); console.log('Options:'); console.log(' --mode <mode> Import mode: merge (default) or replace'); console.log(' --types <list> Comma-separated list of types to import'); console.log(' (tasks, pattern