claude-memory
Version:
Transform AI conversations into persistent project intelligence - Universal memory system for Claude
1,440 lines (1,274 loc) โข 122 kB
JavaScript
#!/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