claude-memory
Version:
Transform AI conversations into persistent project intelligence - Universal memory system for Claude
1,186 lines (1,012 loc) • 39.2 kB
JavaScript
/**
* Claude Memory Core Class
* Handles all memory operations and data management
*/
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs';
import { createHash } from 'crypto';
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'));
export class ClaudeMemory {
constructor(projectRoot, projectName = 'My Project', options = {}) {
const startTime = Date.now();
this.projectRoot = projectRoot;
this.projectName = projectName;
this.claudeDir = path.join(projectRoot, '.claude');
this.memoryFile = path.join(this.claudeDir, 'memory.json');
// Allow custom config path from options
if (options.configPath) {
this.configFile = path.resolve(options.configPath);
} else {
this.configFile = path.join(this.claudeDir, 'config.json');
}
this.claudeFile = path.join(projectRoot, 'CLAUDE.md');
this.currentSession = null;
this.options = options;
this.quietMode = false; // Can be set externally
this.outputFormat = 'text'; // Can be set externally
this.verboseMode = false; // Can be set externally
this.debugMode = false; // Can be set externally
this.dryRun = options.dryRun || false; // Dry run mode
this.force = options.force || false; // Force mode
this.ensureDirectories();
this.loadConfig();
this.loadMemory();
// Auto-session management
if (this.config.autoSession !== false && !options.noAutoSession) {
this.checkAutoSession();
}
// Auto-backup check
if (this.config.autoBackup !== false) {
this.checkAutoBackup();
}
// Log initialization time in debug mode
const initTime = Date.now() - startTime;
this.debug(`ClaudeMemory initialized in ${initTime}ms`, {
projectRoot: this.projectRoot,
projectName: this.projectName,
configFile: this.configFile,
memoryFile: this.memoryFile
});
}
// Output helpers for quiet mode
log(message) {
if (!this.quietMode) {
console.log(message);
}
}
// Essential output (always shown)
output(message) {
console.log(message);
}
// Verbose output (only shown in verbose mode)
verbose(message) {
if (this.verboseMode) {
console.log(`[VERBOSE] ${message}`);
}
}
// Debug output (only shown in debug mode)
debug(message, data = null) {
if (this.debugMode) {
const timestamp = new Date().toISOString();
console.log(`[DEBUG ${timestamp}] ${message}`);
if (data !== null) {
console.log(JSON.stringify(data, null, 2));
}
}
}
// Prompt for confirmation (skipped in force mode)
async confirmPrompt(message, defaultAnswer = false) {
if (this.force) {
this.verbose(`[FORCE] Skipping confirmation: ${message}`);
return true;
}
// For now, return the default answer since we don't have readline implemented
// This is a placeholder for future implementation
return defaultAnswer;
}
ensureDirectories() {
const dirs = [
this.claudeDir,
path.join(this.claudeDir, 'backups'),
path.join(this.claudeDir, 'context'),
path.join(this.claudeDir, 'summaries')
];
this.debug('Ensuring directories exist', { dirs });
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
if (this.dryRun) {
this.verbose(`[DRY RUN] Would create directory: ${dir}`);
} else {
this.debug(`Creating directory: ${dir}`);
fs.mkdirSync(dir, { recursive: true });
}
} else {
this.debug(`Directory exists: ${dir}`);
}
});
if (!fs.existsSync(this.memoryFile)) {
const initialMemory = {
sessions: [],
decisions: [],
patterns: [],
actions: [],
knowledge: {},
tasks: [],
created: new Date().toISOString(),
version: packageJson.version,
projectName: this.projectName
};
if (this.dryRun) {
this.verbose(`[DRY RUN] Would create memory file: ${this.memoryFile}`);
} else {
fs.writeFileSync(this.memoryFile, JSON.stringify(initialMemory, null, 2));
}
}
}
loadConfig() {
const defaultConfig = {
autoSession: true,
autoSessionHours: 4,
autoBackup: true,
backupInterval: 10, // actions
maxBackupDays: 7,
tokenOptimization: true,
silentMode: false
};
this.debug(`Loading config from: ${this.configFile}`);
try {
if (fs.existsSync(this.configFile)) {
const userConfig = JSON.parse(fs.readFileSync(this.configFile, 'utf8'));
this.config = { ...defaultConfig, ...userConfig };
this.debug('Config loaded successfully', this.config);
// Show verbose message if using custom config path
if (this.options.configPath) {
this.verbose(`Using custom config file: ${this.configFile}`);
this.verbose(`Loaded config: ${JSON.stringify(this.config, null, 2)}`);
}
} else {
this.config = defaultConfig;
// Only create default config in .claude directory
if (!this.options.configPath) {
if (this.dryRun) {
this.verbose(`[DRY RUN] Would create config file: ${this.configFile}`);
} else {
fs.writeFileSync(this.configFile, JSON.stringify(defaultConfig, null, 2));
}
}
}
} catch (error) {
this.config = defaultConfig;
if (this.options.configPath) {
this.verbose(`Failed to load custom config: ${error.message}`);
}
}
}
loadMemory() {
try {
const data = JSON.parse(fs.readFileSync(this.memoryFile, 'utf8'));
this.sessions = data.sessions || [];
this.decisions = data.decisions || [];
this.patterns = data.patterns || [];
this.actions = data.actions || [];
this.knowledge = data.knowledge || {};
this.tasks = data.tasks || [];
this.metadata = {
created: data.created,
version: packageJson.version, // Always use current package version
projectName: data.projectName || this.projectName,
lastBackup: data.lastBackup,
actionsSinceBackup: data.actionsSinceBackup || 0
};
// Version migration - update if memory file has old version
if (data.version !== packageJson.version) {
if (!this.options.silent && !this.config.silentMode) {
console.log(`🔄 Migrating memory from v${data.version || 'unknown'} to v${packageJson.version}`);
}
this.saveMemory(); // Save with updated version
}
// Find current active session
this.currentSession = this.sessions.find(s => s.status === 'active') || null;
} catch (error) {
this.sessions = [];
this.decisions = [];
this.patterns = [];
this.actions = [];
this.knowledge = {};
this.tasks = [];
this.metadata = {
created: new Date().toISOString(),
version: packageJson.version,
projectName: this.projectName,
lastBackup: null,
actionsSinceBackup: 0
};
}
}
saveMemory() {
const data = {
sessions: this.sessions,
decisions: this.decisions,
patterns: this.patterns,
actions: this.actions,
knowledge: this.knowledge,
tasks: this.tasks,
created: this.metadata.created,
version: this.metadata.version,
projectName: this.metadata.projectName,
lastBackup: this.metadata.lastBackup,
actionsSinceBackup: this.metadata.actionsSinceBackup,
lastUpdated: new Date().toISOString()
};
if (this.dryRun) {
this.verbose(`[DRY RUN] Would save memory to: ${this.memoryFile}`);
this.verbose(`[DRY RUN] Memory data: ${this.sessions.length} sessions, ${this.decisions.length} decisions, ${this.patterns.length} patterns`);
} else {
fs.writeFileSync(this.memoryFile, JSON.stringify(data, null, 2));
}
}
checkAutoSession() {
if (!this.currentSession) {
// Auto-start session based on time of day
const hour = new Date().getHours();
let sessionName;
if (hour < 6) sessionName = 'Late Night Development';
else if (hour < 12) sessionName = 'Morning Development';
else if (hour < 17) sessionName = 'Afternoon Development';
else if (hour < 21) sessionName = 'Evening Development';
else sessionName = 'Night Development';
if (!this.options.silent && !this.config.silentMode) {
console.log(`🤖 Auto-starting session: ${sessionName}`);
}
this.startSession(sessionName, { auto: true, startTime: new Date().toISOString() });
} else {
// Check if session is too old
const sessionAge = Date.now() - new Date(this.currentSession.startTime).getTime();
const maxAge = this.config.autoSessionHours * 60 * 60 * 1000;
if (sessionAge > maxAge) {
// Generate summary of session activity
const sessionActions = this.actions.filter(a => a.sessionId === this.currentSession.id);
const summary = `Completed ${sessionActions.length} actions`;
if (!this.options.silent && !this.config.silentMode) {
console.log(`🔄 Auto-rotating session after ${this.config.autoSessionHours} hours`);
}
this.endSession(summary);
this.checkAutoSession(); // Recursive to start new session
}
}
}
checkAutoBackup() {
const shouldBackup = !this.metadata.lastBackup ||
this.metadata.actionsSinceBackup >= this.config.backupInterval ||
(Date.now() - new Date(this.metadata.lastBackup).getTime() > 24 * 60 * 60 * 1000);
if (shouldBackup) {
this.backup();
this.cleanOldBackups();
}
}
cleanOldBackups() {
const backupsDir = path.join(this.claudeDir, 'backups');
const maxAge = this.config.maxBackupDays * 24 * 60 * 60 * 1000;
if (fs.existsSync(backupsDir)) {
const backups = fs.readdirSync(backupsDir);
const now = Date.now();
backups.forEach(backup => {
const backupPath = path.join(backupsDir, backup);
const stats = fs.statSync(backupPath);
if (now - stats.mtime.getTime() > maxAge) {
fs.rmSync(backupPath, { recursive: true, force: true });
}
});
}
}
startSession(sessionName, context = {}) {
const sessionId = this.generateSessionId(sessionName);
this.currentSession = {
id: sessionId,
name: sessionName,
startTime: new Date().toISOString(),
context,
status: 'active'
};
this.sessions.push(this.currentSession);
this.saveMemory();
this.recordAction('session_started', { sessionName, context });
return sessionId;
}
endSession(outcome = '') {
if (this.currentSession) {
this.currentSession.endTime = new Date().toISOString();
this.currentSession.status = 'completed';
this.currentSession.outcome = outcome;
this.currentSession = null; // Clear current session
this.saveMemory();
this.recordAction('session_ended', { outcome });
return true;
}
return false;
}
endSessionById(sessionId, outcome = '') {
const session = this.sessions.find(s => s.id === sessionId);
if (session && session.status === 'active') {
session.endTime = new Date().toISOString();
session.status = 'completed';
session.outcome = outcome;
if (this.currentSession && this.currentSession.id === sessionId) {
this.currentSession = null;
}
this.saveMemory();
this.recordAction('session_ended', { sessionId, outcome });
return true;
}
return false;
}
cleanupSessions() {
// End all active sessions
let cleanedCount = 0;
this.sessions.forEach(session => {
if (session.status === 'active') {
session.endTime = new Date().toISOString();
session.status = 'completed';
session.outcome = 'Session cleaned up';
cleanedCount++;
}
});
this.currentSession = null;
this.saveMemory();
return cleanedCount;
}
recordDecision(decision, reasoning, alternatives = [], outcome = null) {
const decisionRecord = {
id: this.generateId(),
sessionId: this.currentSession?.id,
timestamp: new Date().toISOString(),
decision,
reasoning,
alternatives,
outcome,
context: this.currentSession?.context || {}
};
this.decisions.push(decisionRecord);
this.saveMemory();
this.updateClaudeFile();
this.recordAction('decision_recorded', { decision, decisionId: decisionRecord.id });
return decisionRecord.id;
}
learnPattern(pattern, description, _context = '', frequency = 1, effectiveness = null, priority = 'medium') {
const existingPattern = this.patterns.find(p => p.pattern === pattern);
let patternId;
if (existingPattern) {
existingPattern.frequency += frequency;
existingPattern.lastSeen = new Date().toISOString();
if (effectiveness !== null) {
existingPattern.effectiveness = effectiveness;
}
if (priority) {
existingPattern.priority = priority;
}
patternId = existingPattern.id;
} else {
patternId = this.generateId();
this.patterns.push({
id: patternId,
pattern,
description,
frequency,
effectiveness,
priority: priority || 'medium',
status: 'open',
firstSeen: new Date().toISOString(),
lastSeen: new Date().toISOString()
});
}
this.saveMemory();
this.updateClaudeFile();
this.recordAction('pattern_learned', { pattern, description, patternId });
return patternId;
}
resolvePattern(patternId, solution) {
const pattern = this.patterns.find(p => p.id === patternId);
if (pattern) {
pattern.status = 'resolved';
pattern.solution = solution;
pattern.resolvedAt = new Date().toISOString();
this.saveMemory();
this.updateClaudeFile();
this.recordAction('pattern_resolved', { patternId, solution });
return true;
}
return false;
}
storeKnowledge(key, value, category = 'general') {
if (!this.knowledge[category]) {
this.knowledge[category] = {};
}
this.knowledge[category][key] = {
value,
lastUpdated: new Date().toISOString(),
sessionId: this.currentSession?.id
};
this.saveMemory();
this.updateClaudeFile();
this.recordAction('knowledge_stored', { key, category, value });
}
addTask(description, priority = 'medium', status = 'open', assignee = null, dueDate = null) {
const task = {
id: this.generateId(),
description,
priority,
status,
assignee,
dueDate,
createdAt: new Date().toISOString(),
sessionId: this.currentSession?.id
};
this.tasks.push(task);
this.saveMemory();
this.updateClaudeFile();
this.recordAction('task_added', { taskId: task.id, description, priority });
return task.id;
}
completeTask(taskId, outcome = '') {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
task.status = 'completed';
task.completedAt = new Date().toISOString();
task.outcome = outcome;
this.saveMemory();
this.updateClaudeFile();
this.recordAction('task_completed', { taskId, outcome });
return true;
}
return false;
}
updateTaskStatus(taskId, status) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
task.status = status;
task.lastUpdated = new Date().toISOString();
this.saveMemory();
this.updateClaudeFile();
this.recordAction('task_updated', { taskId, status });
return true;
}
return false;
}
getTasks(status = null) {
if (status) {
return this.tasks.filter(t => t.status === status);
}
return this.tasks;
}
recordAction(actionType, details = {}, result = '') {
const actionRecord = {
id: this.generateId(),
sessionId: this.currentSession?.id,
timestamp: new Date().toISOString(),
actionType,
details,
result
};
this.actions.push(actionRecord);
this.metadata.actionsSinceBackup = (this.metadata.actionsSinceBackup || 0) + 1;
this.saveMemory();
// Check if auto-backup needed
if (this.config.autoBackup) {
this.checkAutoBackup();
}
return actionRecord.id;
}
searchMemory(query) {
const results = {
decisions: this.decisions.filter(d =>
d.decision.toLowerCase().includes(query.toLowerCase()) ||
d.reasoning.toLowerCase().includes(query.toLowerCase())
),
patterns: this.patterns.filter(p =>
p.pattern.toLowerCase().includes(query.toLowerCase()) ||
p.description.toLowerCase().includes(query.toLowerCase())
),
tasks: this.tasks.filter(t =>
t.description.toLowerCase().includes(query.toLowerCase()) ||
(t.assignee && t.assignee.toLowerCase().includes(query.toLowerCase()))
),
knowledge: []
};
Object.entries(this.knowledge).forEach(([category, items]) => {
Object.entries(items).forEach(([key, data]) => {
if (key.toLowerCase().includes(query.toLowerCase()) ||
String(data.value).toLowerCase().includes(query.toLowerCase())) {
results.knowledge.push({ category, key, ...data });
}
});
});
return results;
}
getMemoryStats() {
return {
sessions: this.sessions.length,
decisions: this.decisions.length,
patterns: this.patterns.length,
actions: this.actions.length,
tasks: this.tasks.length,
knowledgeCategories: Object.keys(this.knowledge).length,
totalKnowledgeItems: Object.values(this.knowledge).reduce((sum, cat) => sum + Object.keys(cat).length, 0)
};
}
getSessionHistory(limit = 10) {
return this.sessions
.sort((a, b) => new Date(b.startTime) - new Date(a.startTime))
.slice(0, limit);
}
getRecentDecisions(limit = 5) {
return this.decisions
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.slice(0, limit);
}
backup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.join(this.claudeDir, 'backups', timestamp);
if (this.dryRun) {
this.verbose(`[DRY RUN] Would create backup in: ${backupDir}`);
this.verbose(`[DRY RUN] Would backup: memory.json and CLAUDE.md`);
return;
}
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
fs.copyFileSync(this.memoryFile, path.join(backupDir, 'memory.json'));
if (fs.existsSync(this.claudeFile)) {
fs.copyFileSync(this.claudeFile, path.join(backupDir, 'CLAUDE.md'));
}
// Update backup metadata
this.metadata.lastBackup = new Date().toISOString();
this.metadata.actionsSinceBackup = 0;
this.saveMemory();
if (!this.options.silent && !this.config.silentMode) {
console.log('💾 Auto-backup created');
}
}
exportMemory() {
return {
sessions: this.sessions,
decisions: this.decisions,
patterns: this.patterns,
actions: this.actions,
knowledge: this.knowledge,
tasks: this.tasks,
metadata: this.metadata,
exportedAt: new Date().toISOString()
};
}
updateClaudeFile() {
const content = this.generateClaudeContent();
this.updateClaudeFileWithMerge(content);
// Generate context files for full information
this.generateContextFiles();
}
updateClaudeFileWithMerge(newAutoContent) {
// Check if file exists and parse manual sections
let manualSections = {};
let fileStats = null;
if (fs.existsSync(this.claudeFile)) {
fileStats = fs.statSync(this.claudeFile);
const existingContent = fs.readFileSync(this.claudeFile, 'utf8');
manualSections = this.parseManualSections(existingContent);
// Create backup before modifying
this.createClaudeBackup(existingContent);
}
// Generate merged content with manual sections preserved
const mergedContent = this.mergeManualAndAutoContent(newAutoContent, manualSections);
// Write the merged content
if (this.dryRun) {
this.verbose(`[DRY RUN] Would update CLAUDE.md file: ${this.claudeFile}`);
this.verbose(`[DRY RUN] Preserved manual sections: ${Object.keys(manualSections).join(', ') || 'none'}`);
} else {
fs.writeFileSync(this.claudeFile, mergedContent);
}
// Log merge action
this.recordAction('claude_file_updated', {
hasManualSections: Object.keys(manualSections).length > 0,
backupCreated: fileStats !== null,
preservedSections: Object.keys(manualSections)
});
}
parseManualSections(content) {
const sections = {};
const manualSectionRegex = /<!-- BEGIN MANUAL SECTION: ([^>]+) -->([\s\S]*?)<!-- END MANUAL SECTION: \1 -->/g;
let match;
while ((match = manualSectionRegex.exec(content)) !== null) {
const sectionName = match[1].trim();
const sectionContent = match[2].trim();
sections[sectionName] = sectionContent;
}
return sections;
}
mergeManualAndAutoContent(autoContent, manualSections) {
let mergedContent = autoContent;
// Insert manual sections at appropriate locations
if (manualSections['Project Notes']) {
// Insert project notes after Knowledge Base but before Open Patterns
const insertPoint = mergedContent.indexOf('### Open Patterns');
if (insertPoint !== -1) {
const beforePattern = mergedContent.substring(0, insertPoint);
const afterPattern = mergedContent.substring(insertPoint);
const projectNotesSection = '### Project Notes\n' +
'<!-- BEGIN MANUAL SECTION: Project Notes -->\n' +
`${manualSections['Project Notes']}\n` +
'<!-- END MANUAL SECTION: Project Notes -->\n\n';
mergedContent = beforePattern + projectNotesSection + afterPattern;
}
}
if (manualSections['Custom Commands']) {
// Insert custom commands after Commands & Workflows
const insertPoint = mergedContent.indexOf('## Session Continuation');
if (insertPoint !== -1) {
const beforeContinuation = mergedContent.substring(0, insertPoint);
const afterContinuation = mergedContent.substring(insertPoint);
const customCommandsSection = '### Custom Commands\n' +
'<!-- BEGIN MANUAL SECTION: Custom Commands -->\n' +
`${manualSections['Custom Commands']}\n` +
'<!-- END MANUAL SECTION: Custom Commands -->\n\n';
mergedContent = beforeContinuation + customCommandsSection + afterContinuation;
}
}
// Add any other manual sections at the end
const handledSections = ['Project Notes', 'Custom Commands'];
const otherSections = Object.keys(manualSections).filter(name => !handledSections.includes(name));
if (otherSections.length > 0) {
mergedContent += '\n\n## Manual Sections\n';
otherSections.forEach(sectionName => {
mergedContent += `\n### ${sectionName}\n` +
`<!-- BEGIN MANUAL SECTION: ${sectionName} -->\n` +
`${manualSections[sectionName]}\n` +
`<!-- END MANUAL SECTION: ${sectionName} -->\n`;
});
}
return mergedContent;
}
createClaudeBackup(content) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(this.claudeDir, 'backups', `CLAUDE-${timestamp}.md`);
if (this.dryRun) {
this.verbose(`[DRY RUN] Would create CLAUDE.md backup: ${backupFile}`);
return;
}
// Ensure backup directory exists
const backupDir = path.dirname(backupFile);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
fs.writeFileSync(backupFile, content);
// Clean old CLAUDE.md backups (keep last 5)
this.cleanClaudeBackups();
}
cleanClaudeBackups() {
const backupsDir = path.join(this.claudeDir, 'backups');
if (!fs.existsSync(backupsDir)) return;
const claudeBackups = fs.readdirSync(backupsDir)
.filter(file => file.startsWith('CLAUDE-') && file.endsWith('.md'))
.map(file => ({
name: file,
path: path.join(backupsDir, file),
mtime: fs.statSync(path.join(backupsDir, file)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
// Keep only the 5 most recent CLAUDE.md backups
if (claudeBackups.length > 5) {
claudeBackups.slice(5).forEach(backup => {
fs.unlinkSync(backup.path);
});
}
}
generateContextFiles() {
const contextDir = path.join(this.claudeDir, 'context');
if (this.dryRun) {
this.verbose(`[DRY RUN] Would generate context files in: ${contextDir}`);
this.verbose(`[DRY RUN] Would create: knowledge.md, patterns.md, decisions.md, tasks.md`);
return;
}
try {
// Generate knowledge.md
const knowledgeContent = this.generateKnowledgeContext();
fs.writeFileSync(path.join(contextDir, 'knowledge.md'), knowledgeContent);
// Generate patterns.md
const patternsContent = this.generatePatternsContext();
fs.writeFileSync(path.join(contextDir, 'patterns.md'), patternsContent);
// Generate decisions.md
const decisionsContent = this.generateDecisionsContext();
fs.writeFileSync(path.join(contextDir, 'decisions.md'), decisionsContent);
// Generate tasks.md
const tasksContent = this.generateTasksContext();
fs.writeFileSync(path.join(contextDir, 'tasks.md'), tasksContent);
} catch (error) {
console.error('Error generating context files:', error);
}
}
generateKnowledgeContext() {
const categories = Object.keys(this.knowledge || {}).sort();
const totalItems = categories.reduce((sum, cat) => sum + Object.keys(this.knowledge[cat] || {}).length, 0);
let content = `# Project Knowledge Base
*Generated: ${new Date().toISOString()} | ${totalItems} items across ${categories.length} categories*
## Navigation
${categories.map(cat => `- [${cat}](#${cat}) (${Object.keys(this.knowledge[cat] || {}).length} items)`).join('\n')}
`;
categories.forEach(category => {
const items = this.knowledge[category] || {};
const sortedKeys = Object.keys(items).sort();
content += `## ${category}\n`;
sortedKeys.forEach(key => {
const item = items[key];
content += `### ${key}\n`;
content += `**Value**: ${item.value}\n`;
if (item.context) content += `**Context**: ${item.context}\n`;
content += `**Updated**: ${item.lastUpdated || 'Unknown'}\n`;
if (item.references) content += `**References**: ${item.references.join(', ')}\n`;
content += '\n';
});
});
return content;
}
generatePatternsContext() {
const openPatterns = this.patterns.filter(p => p.status === 'open');
const resolvedPatterns = this.patterns.filter(p => p.status === 'resolved');
let content = `# Project Patterns
*Generated: ${new Date().toISOString()} | ${this.patterns.length} total patterns*
## Summary
- Open Patterns: ${openPatterns.length}
- Resolved Patterns: ${resolvedPatterns.length}
## Open Patterns
`;
const priorityOrder = ['critical', 'high', 'medium', 'low'];
priorityOrder.forEach(priority => {
const patterns = openPatterns.filter(p => p.priority === priority);
if (patterns.length > 0) {
content += `### ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority\n`;
patterns.forEach(p => {
content += `#### ${p.pattern} (ID: ${p.id})\n`;
content += `- **Description**: ${p.description}\n`;
content += `- **Effectiveness**: ${p.effectiveness}\n`;
content += `- **First Seen**: ${p.firstSeen}\n`;
content += `- **Last Seen**: ${p.lastSeen}\n`;
content += `- **Frequency**: ${p.frequency}\n\n`;
});
}
});
content += '## Resolved Patterns\n';
resolvedPatterns.slice(-10).reverse().forEach(p => {
content += `### ${p.pattern} (ID: ${p.id})\n`;
content += `- **Solution**: ${p.solution || 'No solution recorded'}\n`;
content += `- **Resolved**: ${p.resolvedAt || 'Unknown'}\n\n`;
});
return content;
}
generateDecisionsContext() {
let content = `# Decision Log
*Generated: ${new Date().toISOString()} | ${this.decisions.length} total decisions*
## Recent Decisions
`;
this.decisions.slice(-50).reverse().forEach(d => {
content += `### ${new Date(d.timestamp).toLocaleDateString()}: ${d.decision}\n`;
content += `**ID**: ${d.id}\n`;
content += `**Reasoning**: ${d.reasoning}\n`;
content += `**Alternatives Considered**: ${d.alternatives.join(', ')}\n`;
if (d.outcome) content += `**Outcome**: ${d.outcome}\n`;
content += `**Session**: ${d.sessionId || 'Unknown'}\n\n`;
});
return content;
}
generateTasksContext() {
const openTasks = this.getTasks('open');
const inProgressTasks = this.getTasks('in-progress');
const completedTasks = this.getTasks('completed');
let content = `# Task Management
*Generated: ${new Date().toISOString()} | ${this.tasks.length} total tasks*
## Summary
- Open: ${openTasks.length}
- In Progress: ${inProgressTasks.length}
- Completed: ${completedTasks.length}
## Open Tasks
`;
['high', 'medium', 'low'].forEach(priority => {
const tasks = openTasks.filter(t => t.priority === priority);
if (tasks.length > 0) {
content += `### ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority\n`;
tasks.forEach(t => {
content += `- [ ] **${t.description}** (ID: ${t.id})\n`;
if (t.assignee) content += ` - Assigned: ${t.assignee}\n`;
content += ` - Created: ${new Date(t.createdAt).toLocaleDateString()}\n`;
});
content += '\n';
}
});
content += '## In Progress\n';
inProgressTasks.forEach(t => {
content += `- [~] **${t.description}** (ID: ${t.id})\n`;
content += ` - Priority: ${t.priority}\n`;
if (t.assignee) content += ` - Assigned: ${t.assignee}\n`;
});
content += '\n## Recently Completed\n';
completedTasks.slice(-20).reverse().forEach(t => {
content += `- [x] **${t.description}** (ID: ${t.id})\n`;
content += ` - Completed: ${new Date(t.completedAt).toLocaleDateString()}\n`;
if (t.outcome) content += ` - Outcome: ${t.outcome}\n`;
});
return content;
}
generateClaudeContent() {
// Token optimization: reduce content when enabled
const isOptimized = this.config.tokenOptimization !== false;
const recentDecisions = this.getRecentDecisions(isOptimized ? 3 : 5);
const openPatterns = this.patterns.filter(p => p.status === 'open').slice(0, isOptimized ? 3 : 5);
const recentlyResolved = this.patterns.filter(p => p.status === 'resolved').slice(isOptimized ? -2 : -3);
const activeTasks = this.getTasks('open').slice(0, isOptimized ? 5 : 10);
const inProgressTasks = this.getTasks('in-progress').slice(0, isOptimized ? 3 : 5);
const recentlyCompleted = this.getTasks('completed').slice(isOptimized ? -2 : -3);
// Only show high-priority patterns when optimized
const criticalPatterns = openPatterns.filter(p => p.priority === 'critical');
const highPatterns = isOptimized ? [] : openPatterns.filter(p => p.priority === 'high');
const mediumPatterns = isOptimized ? [] : openPatterns.filter(p => p.priority === 'medium');
return `# Claude Project Memory
## Active Session
- **Current**: ${this.currentSession?.name || 'No active session'}
- **Started**: ${new Date().toISOString().split('T')[0]}
- **Project**: ${this.metadata.projectName}
## Key Project Knowledge
### Critical Information
- **Project Name**: ${this.metadata.projectName}
- **Claude Memory**: v${this.metadata.version}
- **Memory Created**: ${this.metadata.created?.split('T')[0]}
### Knowledge Base
${Object.keys(this.knowledge).length > 0
? Object.entries(this.knowledge).map(([category, items]) => {
const itemCount = Object.keys(items).length;
const sampleItems = Object.entries(items).slice(0, isOptimized ? 2 : 3);
return `#### ${category} (${itemCount} items)
${sampleItems.map(([key, data]) => {
// Show full value unless token optimization is on
const displayValue = isOptimized && data.value.length > 120
? data.value.substring(0, 120) + '...'
: data.value;
return `- **${key}**: ${displayValue}`;
}).join('\n')}${itemCount > sampleItems.length ? `\n- ... and ${itemCount - sampleItems.length} more items` : ''}`;
}).join('\n\n') + '\n'
: '- No knowledge stored yet\n'}
### Recent Changes
${(() => {
// Get recent items from the last 7 days
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
// Recent decisions
const recentDecisions = this.decisions
.filter(d => new Date(d.timestamp) > sevenDaysAgo)
.slice(0, 3);
// Recent patterns
const recentPatterns = this.patterns
.filter(p => new Date(p.lastSeen || p.firstSeen) > sevenDaysAgo)
.slice(0, 3);
// Recent knowledge additions
const recentKnowledge = [];
Object.entries(this.knowledge).forEach(([category, items]) => {
Object.entries(items).forEach(([key, data]) => {
if (new Date(data.lastUpdated) > sevenDaysAgo) {
recentKnowledge.push({ category, key, ...data });
}
});
});
const topKnowledge = recentKnowledge
.sort((a, b) => new Date(b.lastUpdated) - new Date(a.lastUpdated))
.slice(0, 3);
const changes = [];
if (recentDecisions.length > 0) {
changes.push('#### Recent Decisions');
recentDecisions.forEach(d => {
const dateStr = new Date(d.timestamp).toISOString().split('T')[0];
changes.push(`- **${dateStr}**: ${d.decision.substring(0, 80)}${d.decision.length > 80 ? '...' : ''}`);
});
}
if (recentPatterns.length > 0) {
if (changes.length > 0) changes.push('');
changes.push('#### Recent Patterns');
recentPatterns.forEach(p => {
const dateStr = new Date(p.lastSeen || p.firstSeen).toISOString().split('T')[0];
changes.push(`- **${dateStr}**: ${p.pattern} (${p.priority})`);
});
}
if (topKnowledge.length > 0) {
if (changes.length > 0) changes.push('');
changes.push('#### Recent Knowledge Updates');
topKnowledge.forEach(k => {
const dateStr = new Date(k.lastUpdated).toISOString().split('T')[0];
changes.push(`- **${dateStr}**: ${k.category}/${k.key}`);
});
}
return changes.length > 0 ? changes.join('\n') + '\n' : '- No recent changes in the last 7 days\n';
})()}
### Open Patterns
${criticalPatterns.length
? `#### Critical Priority
${criticalPatterns.map(p =>
`- **${p.pattern}**: ${p.description}${p.effectiveness ? ` (effectiveness: ${p.effectiveness})` : ''}`
).join('\n')}\n`
: ''}${highPatterns.length
? `#### High Priority
${highPatterns.map(p =>
`- **${p.pattern}**: ${p.description}${p.effectiveness ? ` (effectiveness: ${p.effectiveness})` : ''}`
).join('\n')}\n`
: ''}${mediumPatterns.length
? `#### Medium Priority
${mediumPatterns.map(p =>
`- **${p.pattern}**: ${p.description}${p.effectiveness ? ` (effectiveness: ${p.effectiveness})` : ''}`
).join('\n')}\n`
: ''}
${recentlyResolved.length
? `### Recently Resolved
${recentlyResolved.map(p =>
`- **${p.pattern}**: ${p.solution} (${p.resolvedAt?.split('T')[0]})`
).join('\n')}\n`
: ''}
### Project Conventions
<!-- Discovered during development -->
## Task Management
### Active Tasks
${activeTasks.length
? activeTasks.map(t =>
`- [ ] **${t.description}** (${t.priority}${t.dueDate ? `, due: ${t.dueDate}` : ''}${
t.assignee ? `, assigned: ${t.assignee}` : ''})`
).join('\n')
: '- No active tasks'}
### In Progress
${inProgressTasks.length
? inProgressTasks.map(t =>
`- 🔄 **${t.description}** (${t.priority}${t.assignee ? `, assigned: ${t.assignee}` : ''})`
).join('\n')
: '- No tasks in progress'}
${recentlyCompleted.length
? `### Recently Completed
${recentlyCompleted.map(t =>
`- [x] **${t.description}** (completed: ${t.completedAt?.split('T')[0]})`
).join('\n')}\n`
: ''}
## Recent Decisions Log
${recentDecisions.map(d => `
### ${d.timestamp.split('T')[0]}: ${d.decision}
**Decision**: ${d.decision}
**Reasoning**: ${d.reasoning}
${d.alternatives.length ? `**Alternatives Considered**: ${d.alternatives.join(', ')}` : ''}
`).join('\n')}
## Commands & Workflows
### Claude Memory Commands
\`\`\`bash
# Session management
claude-memory session start "Session Name"
claude-memory session end ["outcome"]
claude-memory session cleanup
# Task management
claude-memory task add "description" [--priority high|medium|low] [--assignee name]
claude-memory task complete <task-id>
claude-memory task list [status]
# Pattern management
claude-memory pattern add "Pattern" "Description" [--effectiveness 0.8] [--priority high]
claude-memory pattern list [--priority high]
claude-memory pattern search "query"
claude-memory pattern resolve <pattern-id> "solution"
# Decision tracking
claude-memory decision "Choice" "Reasoning" "alternatives"
# Knowledge management
claude-memory knowledge add "key" "value" --category category
claude-memory knowledge get "key" [category]
claude-memory knowledge list [category]
# Export and reporting (v1.10.0)
claude-memory export [file.json] [--types tasks,patterns] [--format json|yaml|csv|markdown]
claude-memory import <file.json> [--mode merge|replace] [--types tasks,patterns]
claude-memory report [--type summary|tasks|patterns|decisions|progress|sprint] [--save]
# Memory utilities
claude-memory stats
claude-memory search "query"
claude-memory handoff [--format markdown|json]
\`\`\`
## Full Context Files
For complete information without truncation:
- 📚 **Knowledge Base**: \`.claude/context/knowledge.md\` (${
Object.keys(this.knowledge).reduce((sum, cat) => sum + Object.keys(this.knowledge[cat] || {}).length, 0)
} items)
- 🧩 **All Patterns**: \`.claude/context/patterns.md\` (${this.patterns.length} patterns)
- 🎯 **Decision Log**: \`.claude/context/decisions.md\` (${this.decisions.length} decisions)
- ✅ **Task Details**: \`.claude/context/tasks.md\` (${this.tasks.length} tasks)
## Session Continuation
To resume work, tell Claude:
"Load project memory for ${this.metadata.projectName} and continue development"
`;
}
generateSessionId(sessionName) {
const date = new Date().toISOString().split('T')[0];
const slug = sessionName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
return `${date}-${slug}`;
}
generateId() {
return createHash('md5').update(Date.now().toString() + Math.random().toString()).digest('hex').substring(0, 8);
}
}