UNPKG

claude-mem

Version:

Memory compression system for Claude Code - persist context across sessions

1,148 lines (984 loc) 38.2 kB
import { OptionValues } from 'commander'; import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync, copyFileSync, statSync, readdirSync } from 'fs'; import { join, resolve, dirname } from 'path'; import { homedir, platform } from 'os'; import { execSync } from 'child_process'; import { fileURLToPath } from 'url'; import * as p from '@clack/prompts'; import gradient from 'gradient-string'; import chalk from 'chalk'; import boxen from 'boxen'; import { PACKAGE_NAME } from '../shared/config.js'; import type { Settings } from '../shared/types.js'; import { PathDiscovery } from '../services/path-discovery.js'; // Enhanced animation utilities function createLoadingAnimation(message: string) { let interval: NodeJS.Timeout; let frame = 0; const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; return { start() { interval = setInterval(() => { process.stdout.write(`\r${chalk.cyan(frames[frame % frames.length])} ${message}`); frame++; }, 50); // Faster spinner animation (was 80ms) }, stop(result: string, success: boolean = true) { clearInterval(interval); const icon = success ? chalk.green('✓') : chalk.red('✗'); process.stdout.write(`\r${icon} ${result}\n`); } }; } // Create animated rainbow text with adjustable speed function animatedRainbow(text: string, speed: number = 100): Promise<void> { return new Promise((resolve) => { let offset = 0; const maxFrames = 10; const interval = setInterval(() => { // Create a shifted gradient by rotating through different presets const gradients = [fastRainbow, vibrantRainbow, gradient.rainbow, gradient.pastel]; const shifted = gradients[offset % gradients.length](text); process.stdout.write('\r' + shifted); offset++; if (offset >= maxFrames) { clearInterval(interval); resolve(); } }, speed); }); } // Sleep utility for smooth animations const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // Fast rainbow gradient preset with tighter color transitions const fastRainbow = gradient(['#ff0000', '#ff4500', '#ffa500', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#8b00ff']); const vibrantRainbow = gradient(['#ff006e', '#fb5607', '#ffbe0b', '#8338ec', '#3a86ff']); // Installation scope type type InstallScope = 'user' | 'project' | 'local'; // Installation configuration from wizard interface InstallConfig { scope: InstallScope; customPath?: string; hookTimeout: number; forceReinstall: boolean; enableSmartTrash?: boolean; saveMemoriesOnClear?: boolean; } // <Block> Silent Prerequisites validation - no visual output unless error async function validatePrerequisites(): Promise<boolean> { // No announcement, just run checks silently const checks = [ { name: 'Node.js version', check: async () => { const nodeVersion = process.versions.node; const [major] = nodeVersion.split('.').map(Number); return { success: major >= 18, message: major >= 18 ? '' : `Node.js ${nodeVersion} is below required version 18.0.0` }; } }, { name: 'Claude Code CLI', check: async () => { try { const command = platform() === 'win32' ? 'where claude' : 'which claude'; execSync(command, { stdio: 'ignore' }); return { success: true, message: '' }; } catch { return { success: false, message: 'Claude Code CLI not found. Please install: https://docs.anthropic.com/claude/docs/claude-code' }; } } }, { name: 'uv (Python package manager)', check: async () => { try { execSync('which uv', { stdio: 'ignore' }); return { success: true, message: '' }; } catch { // uv not found - we'll install it automatically return { success: true, message: '', needsInstall: true }; } } }, { name: 'Write permissions', check: async () => { const testDir = join(PathDiscovery.getDataDirectory(), 'test-permissions'); try { mkdirSync(testDir, { recursive: true }); writeFileSync(join(testDir, 'test'), 'test'); execSync(`rm -rf ${testDir}`, { stdio: 'ignore' }); return { success: true, message: '' }; } catch { return { success: false, message: 'No write permissions to claude-mem data directory' }; } } } ]; // Run all checks silently let needsUvInstall = false; for (const { name, check } of checks) { const result = await check(); if (!result.success) { // Only show output if there's an error console.log(boxen(chalk.red(`❌ ${name} check failed!\n\n${result.message}`), { padding: 1, margin: 1, borderStyle: 'double', borderColor: 'red' })); return false; } if ((result as any).needsInstall && name === 'uv (Python package manager)') { needsUvInstall = true; } } // Install uv if needed if (needsUvInstall) { const loader = createLoadingAnimation('Installing uv (Python package manager)...'); loader.start(); try { // Use the official uv installer script execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'pipe', shell: '/bin/sh' }); // Add uv to PATH for current session process.env.PATH = `${homedir()}/.cargo/bin:${process.env.PATH}`; loader.stop('uv installed successfully', true); } catch (error) { loader.stop('Failed to install uv automatically', false); console.log(boxen(chalk.yellow(`⚠️ Please install uv manually:\n\n${chalk.cyan('curl -LsSf https://astral.sh/uv/install.sh | sh')}\n\nOr visit: ${chalk.cyan('https://docs.astral.sh/uv/getting-started/installation/')}`), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow' })); return false; } } // Success - no output, just return true return true; } // </Block> // <Block> Claude binary path detection function detectClaudePath(): string | null { try { const command = platform() === 'win32' ? 'where claude' : 'which claude'; const path = execSync(command, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); return path || null; } catch { return null; } } // </Block> // <Block> Installation status detection function detectExistingInstallation(): { hasHooks: boolean; hasChromaMcp: boolean; hasSettings: boolean; scope?: InstallScope; } { const result = { hasHooks: false, hasChromaMcp: false, hasSettings: false, scope: undefined as InstallScope | undefined }; // Check for hooks const hooksDir = PathDiscovery.getHooksDirectory(); result.hasHooks = existsSync(hooksDir) && existsSync(join(hooksDir, 'pre-compact.js')) && existsSync(join(hooksDir, 'session-start.js')); // Check for Chroma MCP server configuration const pathDiscovery = PathDiscovery.getInstance(); const userMcpPath = pathDiscovery.getMcpConfigPath(); const projectMcpPath = pathDiscovery.getProjectMcpConfigPath(); if (existsSync(userMcpPath)) { try { const config = JSON.parse(readFileSync(userMcpPath, 'utf8')); if (config.mcpServers?.['claude-mem']) { result.hasChromaMcp = true; result.scope = 'user'; } } catch {} } if (existsSync(projectMcpPath)) { try { const config = JSON.parse(readFileSync(projectMcpPath, 'utf8')); if (config.mcpServers?.['claude-mem']) { result.hasChromaMcp = true; result.scope = 'project'; } } catch {} } // Check for settings const userSettingsPath = pathDiscovery.getUserSettingsPath(); result.hasSettings = existsSync(userSettingsPath); return result; } // </Block> // <Block> Interactive installation wizard async function runInstallationWizard(existingInstall: ReturnType<typeof detectExistingInstallation>): Promise<InstallConfig | null> { const config: Partial<InstallConfig> = {}; // If existing installation found, ask about reinstallation if (existingInstall.hasHooks || existingInstall.hasChromaMcp) { const shouldReinstall = await p.confirm({ message: '🧠 Existing claude-mem installation detected. Your memories and data are safe!\n\nReinstall to update hooks and configuration?', initialValue: true }); if (p.isCancel(shouldReinstall)) { p.cancel('Installation cancelled'); return null; } if (!shouldReinstall) { p.cancel('Installation cancelled'); return null; } config.forceReinstall = true; } else { config.forceReinstall = false; } // Select installation scope const scope = await p.select({ message: 'Select installation scope', options: [ { value: 'user', label: 'User (Recommended)', hint: 'Install for current user (~/.claude)' }, { value: 'project', label: 'Project', hint: 'Install for current project only (./.mcp.json)' }, { value: 'local', label: 'Local', hint: 'Custom local installation' } ], initialValue: existingInstall.scope || 'user' }); if (p.isCancel(scope)) { p.cancel('Installation cancelled'); return null; } config.scope = scope as InstallScope; // If local scope, ask for custom path if (scope === 'local') { const customPath = await p.text({ message: 'Enter custom installation directory', placeholder: join(process.cwd(), '.claude-mem'), validate: (value) => { if (!value) return 'Path is required'; if (!value.startsWith('/') && !value.startsWith('~')) { return 'Please provide an absolute path'; } } }); if (p.isCancel(customPath)) { p.cancel('Installation cancelled'); return null; } config.customPath = customPath as string; } // Use default hook timeout (3 minutes) config.hookTimeout = 180000; // Always install/reinstall Chroma MCP - it's required for claude-mem to work // Ask about smart trash alias const enableSmartTrash = await p.confirm({ message: 'Enable Smart Trash? This creates an alias for "rm" that moves files to ~/.claude-mem/trash instead of permanently deleting them. You can restore files anytime by typing "claude-mem restore".', initialValue: true }); if (p.isCancel(enableSmartTrash)) { p.cancel('Installation cancelled'); return null; } config.enableSmartTrash = enableSmartTrash; // Ask about save-on-clear const saveMemoriesOnClear = await p.confirm({ message: 'claude-mem is designed to save "memories" when you type /compact. The official compact summary + claude-mem produces the best ongoing results, but sometimes you may want to completely clear the context and still retain the "memories" from your last conversation.\n\nWould you like to save memories when you type "/clear" in Claude Code? When running /clear with this on, it takes about a minute to save memories before your new session starts.', initialValue: false }); if (p.isCancel(saveMemoriesOnClear)) { p.cancel('Installation cancelled'); return null; } config.saveMemoriesOnClear = saveMemoriesOnClear; return config as InstallConfig; } // </Block> // <Block> Backup existing configuration async function backupExistingConfig(): Promise<string | null> { const pathDiscovery = PathDiscovery.getInstance(); const backupDir = join(pathDiscovery.getBackupsDirectory(), new Date().toISOString().replace(/[:.]/g, '-')); try { mkdirSync(backupDir, { recursive: true }); // Backup hooks if they exist const hooksDir = pathDiscovery.getHooksDirectory(); if (existsSync(hooksDir)) { copyFileRecursively(hooksDir, join(backupDir, 'hooks')); } // Backup settings const settingsPath = pathDiscovery.getUserSettingsPath(); if (existsSync(settingsPath)) { copyFileSync(settingsPath, join(backupDir, 'settings.json')); } // Backup Claude settings const claudeSettingsPath = pathDiscovery.getClaudeSettingsPath(); if (existsSync(claudeSettingsPath)) { copyFileSync(claudeSettingsPath, join(backupDir, 'claude-settings.json')); } return backupDir; } catch (error) { return null; } } // </Block> // <Block> Directory structure creation - natural setup flow function ensureDirectoryStructure(): void { const pathDiscovery = PathDiscovery.getInstance(); // Create all data directories pathDiscovery.ensureAllDataDirectories(); // Create all Claude integration directories pathDiscovery.ensureAllClaudeDirectories(); // Create package.json in .claude-mem to fix ESM module issues const packageJsonPath = join(pathDiscovery.getDataDirectory(), 'package.json'); if (!existsSync(packageJsonPath)) { const packageJson = { name: "claude-mem-data", type: "module" }; writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } } // </Block> function copyFileRecursively(src: string, dest: string): void { const stat = statSync(src); if (stat.isDirectory()) { if (!existsSync(dest)) { mkdirSync(dest, { recursive: true }); } const files = readdirSync(src); files.forEach((file: string) => { copyFileRecursively(join(src, file), join(dest, file)); }); } else { copyFileSync(src, dest); } } function writeHookFiles(timeout: number = 180000): void { const pathDiscovery = PathDiscovery.getInstance(); const hooksDir = pathDiscovery.getHooksDirectory(); // Find the installed package hooks directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // DYNAMIC DISCOVERY: Find hooks by walking up from current location let currentDir = __dirname; let packageHooksDir: string | null = null; // Walk up the tree to find the hooks directory for (let i = 0; i < 10; i++) { const hooksPath = join(currentDir, 'hooks'); // Check if this directory has the hook files if (existsSync(join(hooksPath, 'pre-compact.js'))) { packageHooksDir = hooksPath; break; } // Move up one directory const parentDir = dirname(currentDir); if (parentDir === currentDir) { // We've reached the filesystem root break; } currentDir = parentDir; } // If we still haven't found it, use PathDiscovery to find package hooks if (!packageHooksDir) { try { packageHooksDir = pathDiscovery.findPackageHooksDirectory(); } catch (error) { throw new Error('Cannot dynamically locate hooks directory. The package may be corrupted.'); } } // Copy hook files from the package instead of creating wrappers const hooks = ['pre-compact.js', 'session-start.js', 'session-end.js']; for (const hookName of hooks) { const sourcePath = join(packageHooksDir, hookName); const destPath = join(hooksDir, hookName); if (existsSync(sourcePath)) { copyFileSync(sourcePath, destPath); chmodSync(destPath, 0o755); } } // Copy shared directory if it exists const sourceSharedDir = join(packageHooksDir, 'shared'); const destSharedDir = join(hooksDir, 'shared'); if (existsSync(sourceSharedDir)) { copyFileRecursively(sourceSharedDir, destSharedDir); } // Write configuration with custom timeout const hookConfigPath = join(hooksDir, 'config.json'); const hookConfig = { packageName: PACKAGE_NAME, cliCommand: PACKAGE_NAME, backend: 'chroma', timeout }; writeFileSync(hookConfigPath, JSON.stringify(hookConfig, null, 2)); } function ensureClaudeMdInstructions(): void { const pathDiscovery = PathDiscovery.getInstance(); const claudeMdPath = pathDiscovery.getClaudeMdPath(); const claudeMdDir = dirname(claudeMdPath); // Ensure .claude directory exists if (!existsSync(claudeMdDir)) { mkdirSync(claudeMdDir, { recursive: true }); } const instructions = ` <!-- CLAUDE-MEM QUICK REFERENCE --> ## 🧠 Memory System Quick Reference ### Search Your Memories (SIMPLE & POWERFUL) - **Semantic search is king**: \`mcp__claude-mem__chroma_query_documents(["search terms"])\` - **🔒 ALWAYS include project name in query**: \`["claude-mem feature authentication"]\` not just \`["feature authentication"]\` - **Include dates for temporal search**: \`["project-name 2025-09-09 bug fix"]\` finds memories from that date - **Get specific memory**: \`mcp__claude-mem__chroma_get_documents(ids: ["document_id"])\` ### Search Tips That Actually Work - **Project isolation**: Always prefix queries with project name to avoid cross-contamination - **Temporal search**: Include dates (YYYY-MM-DD) in query text to find memories from specific times - **Intent-based**: "implementing oauth" > "oauth implementation code function" - **Multiple queries**: Search with different phrasings for better coverage - **Session-specific**: Include session ID in query when you know it ### What Doesn't Work (Don't Do This!) - ❌ Complex where filters with $and/$or - they cause errors - ❌ Timestamp comparisons ($gte/$lt) - Chroma stores timestamps as strings - ❌ Mixing project filters in where clause - causes "Error finding id" ### Storage - Collection: "claude_memories" - Archives: ~/.claude-mem/archives/ <!-- /CLAUDE-MEM QUICK REFERENCE -->`; // Check if file exists and read content let content = ''; if (existsSync(claudeMdPath)) { content = readFileSync(claudeMdPath, 'utf8'); // Check if instructions already exist (handle both old and new format) const hasOldInstructions = content.includes('<!-- CLAUDE-MEM INSTRUCTIONS -->'); const hasNewInstructions = content.includes('<!-- CLAUDE-MEM QUICK REFERENCE -->'); if (hasOldInstructions || hasNewInstructions) { // Replace existing instructions (handle both old and new markers) let startMarker, endMarker; if (hasOldInstructions) { startMarker = '<!-- CLAUDE-MEM INSTRUCTIONS -->'; endMarker = '<!-- /CLAUDE-MEM INSTRUCTIONS -->'; } else { startMarker = '<!-- CLAUDE-MEM QUICK REFERENCE -->'; endMarker = '<!-- /CLAUDE-MEM QUICK REFERENCE -->'; } const startIndex = content.indexOf(startMarker); const endIndex = content.indexOf(endMarker) + endMarker.length; if (startIndex !== -1 && endIndex !== -1) { content = content.substring(0, startIndex) + instructions.trim() + content.substring(endIndex); } } else { // Append instructions to the end content = content.trim() + '\n' + instructions; } } else { // Create new file with instructions content = instructions.trim(); } // Write the updated content writeFileSync(claudeMdPath, content); } async function installChromaMcp(): Promise<boolean> { const loader = createLoadingAnimation('Installing Chroma MCP server...'); loader.start(); try { await sleep(400); // Realistic timing // Remove existing claude-mem MCP server if it exists (silently ignore errors) try { execSync('claude mcp remove claude-mem', { stdio: 'pipe' }); await sleep(200); } catch { // Ignore errors - server may not exist } // Ensure uv is in PATH (it might be in ~/.cargo/bin if just installed) const uvPath = `${homedir()}/.cargo/bin`; if (existsSync(uvPath) && !process.env.PATH?.includes(uvPath)) { process.env.PATH = `${uvPath}:${process.env.PATH}`; } // Install fresh Chroma MCP server const chromaMcpCommand = `claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${PathDiscovery.getInstance().getChromaDirectory()}`; execSync(chromaMcpCommand, { stdio: 'pipe', env: process.env }); await sleep(300); loader.stop(vibrantRainbow('Chroma MCP server installed successfully! 🚀'), true); return true; } catch (error) { loader.stop('Chroma MCP server installation failed', false); console.log(boxen(chalk.yellow(`⚠️ Manual installation required:\n\n${chalk.cyan(`claude mcp add claude-mem -- uvx chroma-mcp --client-type persistent --data-dir ${PathDiscovery.getInstance().getChromaDirectory()}`)}\n\nMake sure uv is installed: ${chalk.cyan('https://docs.astral.sh/uv/getting-started/installation/')}`), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow' })); return false; } } async function configureHooks(settingsPath: string, config: InstallConfig): Promise<void> { const pathDiscovery = PathDiscovery.getInstance(); const claudeMemHooksDir = pathDiscovery.getHooksDirectory(); const preCompactScript = join(claudeMemHooksDir, 'pre-compact.js'); const sessionStartScript = join(claudeMemHooksDir, 'session-start.js'); const sessionEndScript = join(claudeMemHooksDir, 'session-end.js'); let settings: any = {}; if (existsSync(settingsPath)) { const content = readFileSync(settingsPath, 'utf8'); settings = JSON.parse(content); } // Ensure settings directory exists const settingsDir = dirname(settingsPath); if (!existsSync(settingsDir)) { mkdirSync(settingsDir, { recursive: true }); } // Initialize hooks structure if it doesn't exist if (!settings.hooks) { settings.hooks = {}; } // Remove existing claude-mem hooks to ensure clean installation/update // Non-tool hooks: filter out configs where hooks contain our commands if (settings.hooks.PreCompact) { settings.hooks.PreCompact = settings.hooks.PreCompact.filter((cfg: any) => !cfg.hooks?.some((hook: any) => hook.command?.includes(PACKAGE_NAME) || hook.command?.includes('pre-compact.js') ) ); if (!settings.hooks.PreCompact.length) delete settings.hooks.PreCompact; } if (settings.hooks.SessionStart) { settings.hooks.SessionStart = settings.hooks.SessionStart.filter((cfg: any) => !cfg.hooks?.some((hook: any) => hook.command?.includes(PACKAGE_NAME) || hook.command?.includes('session-start.js') ) ); if (!settings.hooks.SessionStart.length) delete settings.hooks.SessionStart; } if (settings.hooks.SessionEnd) { settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter((cfg: any) => !cfg.hooks?.some((hook: any) => hook.command?.includes(PACKAGE_NAME) || hook.command?.includes('session-end.js') ) ); if (!settings.hooks.SessionEnd.length) delete settings.hooks.SessionEnd; } /** * 🔒 LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits * * OFFICIAL DOCS: Claude Code Hooks Configuration v2025 * Last Verified: 2025-08-31 * * Hook Configuration Structure Requirements: * - Tool-related hooks (PreToolUse, PostToolUse): Use 'matcher' field for tool patterns * - Non-tool hooks (PreCompact, SessionStart, SessionEnd, etc.): NO matcher/pattern field * * Correct Non-Tool Hook Structure: * { * hooks: [{ * type: "command", * command: "/path/to/script.js" * }] * } * * @see https://docs.anthropic.com/en/docs/claude-code/hooks * @see docs/claude-code/hook-configuration.md for full documentation */ // Add PreCompact hook - Non-tool hook (no matcher field) if (!settings.hooks.PreCompact) { settings.hooks.PreCompact = []; } // ✅ CORRECT: Non-tool hooks have no 'pattern' or 'matcher' field settings.hooks.PreCompact.push({ hooks: [ { type: "command", command: preCompactScript, timeout: 180 } ] }); // Add SessionStart hook - Non-tool hook (no matcher field) if (!settings.hooks.SessionStart) { settings.hooks.SessionStart = []; } // ✅ CORRECT: Non-tool hooks have no 'pattern' or 'matcher' field settings.hooks.SessionStart.push({ hooks: [ { type: "command", command: sessionStartScript, timeout: 180 } ] }); // Add SessionEnd hook (only if the file exists) if (existsSync(sessionEndScript)) { if (!settings.hooks.SessionEnd) { settings.hooks.SessionEnd = []; } // ✅ CORRECT: Non-tool hooks have no 'pattern' or 'matcher' field settings.hooks.SessionEnd.push({ hooks: [{ type: "command", command: sessionEndScript, timeout: 180 }] }); } // Write updated settings writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); } async function configureSmartTrashAlias(): Promise<void> { const homeDir = homedir(); const shellConfigs = [ join(homeDir, '.bashrc'), join(homeDir, '.zshrc'), join(homeDir, '.bash_profile') ]; const aliasLine = 'alias rm="claude-mem trash"'; const commentLine = '# claude-mem smart trash alias'; for (const configPath of shellConfigs) { if (!existsSync(configPath)) continue; try { let content = readFileSync(configPath, 'utf8'); // Check if alias already exists if (content.includes(aliasLine)) { continue; // Already configured } // Add the alias const aliasBlock = `\n${commentLine}\n${aliasLine}\n`; content += aliasBlock; writeFileSync(configPath, content); } catch (error) { // Silent fail - not critical } } } function createBackupFilename(originalPath: string): string { const now = new Date(); const timestamp = now.toISOString() .replace(/T/, '-') .replace(/:/g, '') .replace(/\..+/, '') .replace(/-/g, ''); const formatted = `${timestamp.slice(0,8)}-${timestamp.slice(8)}`; return `${originalPath}.backup.${formatted}`; } function installClaudeCommands(force: boolean = false): void { const pathDiscovery = PathDiscovery.getInstance(); const claudeCommandsDir = pathDiscovery.getClaudeCommandsDirectory(); // DYNAMIC DISCOVERY: Find where THIS code is actually running from const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Walk up from current location until we find the package root let currentDir = __dirname; let packageCommandsDir: string | null = null; // Walk up the tree to find the commands directory for (let i = 0; i < 10; i++) { const commandsPath = join(currentDir, 'commands'); // Check if this directory has the command files if (existsSync(join(commandsPath, 'save.md'))) { packageCommandsDir = commandsPath; break; } // Move up one directory const parentDir = dirname(currentDir); if (parentDir === currentDir) { // We've reached the filesystem root break; } currentDir = parentDir; } // If we still haven't found it, use PathDiscovery to find package commands if (!packageCommandsDir) { try { packageCommandsDir = pathDiscovery.findPackageCommandsDirectory(); } catch (error) { throw new Error('Cannot dynamically locate commands directory. The package may be corrupted.'); } } // Create ~/.claude/commands/ directory if it doesn't exist if (!existsSync(claudeCommandsDir)) { mkdirSync(claudeCommandsDir, { recursive: true }); } // Copy command files const commandFiles = ['save.md', 'remember.md', 'claude-mem.md']; const copiedFiles: string[] = []; const skippedFiles: string[] = []; const backedUpFiles: string[] = []; for (const fileName of commandFiles) { const sourcePath = join(packageCommandsDir, fileName); const destPath = join(claudeCommandsDir, fileName); if (existsSync(sourcePath)) { if (existsSync(destPath)) { if (force) { // Create backup and copy new version const backupPath = createBackupFilename(destPath); copyFileSync(destPath, backupPath); copyFileSync(sourcePath, destPath); backedUpFiles.push(fileName); } else { // File already exists, skip it skippedFiles.push(fileName); } } else { // Copy the file copyFileSync(sourcePath, destPath); copiedFiles.push(fileName); } } } // Provide feedback about what happened if (copiedFiles.length > 0) { console.log(` ${chalk.green('✓')} Copied commands: ${copiedFiles.join(', ')}`); } if (backedUpFiles.length > 0) { console.log(` ${chalk.blue('📦')} Backed up and replaced commands: ${backedUpFiles.join(', ')}`); } if (skippedFiles.length > 0) { console.log(` ${chalk.yellow('→')} Skipped existing commands: ${skippedFiles.join(', ')}`); } } async function verifyInstallation(): Promise<void> { const s = p.spinner(); s.start('Verifying installation'); const issues: string[] = []; // Check hooks const pathDiscovery = PathDiscovery.getInstance(); const hooksDir = pathDiscovery.getHooksDirectory(); if (!existsSync(join(hooksDir, 'pre-compact.js'))) { issues.push('Pre-compact hook not found'); } if (!existsSync(join(hooksDir, 'session-start.js'))) { issues.push('Session-start hook not found'); } if (issues.length > 0) { s.stop('Installation verification completed with issues'); p.log.warn('The following issues were detected:'); issues.forEach(issue => p.log.error(` - ${issue}`)); p.log.info('The installation may not work correctly. Consider reinstalling with --force flag.'); } else { s.stop('Installation verified successfully'); } } export async function install(options: OptionValues = {}): Promise<void> { // Simple banner console.log(fastRainbow('\n═══════════════════════════════════════')); console.log(fastRainbow(' CLAUDE-MEM INSTALLER ')); console.log(fastRainbow('═══════════════════════════════════════')); console.log(boxen(vibrantRainbow('🧠 Persistent Memory System for Claude Code\n\n✨ Transform your Claude experience with seamless context preservation\n🚀 Never lose your conversation history again'), { padding: 2, margin: 1, borderStyle: 'double', borderColor: 'magenta', textAlignment: 'center' })); await sleep(500); // Let the banner shine // Check if running with flags (non-interactive mode) const isNonInteractive = options.user || options.project || options.local || options.force; let config: InstallConfig; if (isNonInteractive) { // Non-interactive mode - use flags config = { scope: options.local ? 'local' : options.project ? 'project' : 'user', customPath: options.path, hookTimeout: options.timeout ? parseInt(options.timeout) : 180, forceReinstall: !!options.force, }; } else { // Interactive mode // Validate prerequisites const prereqValid = await validatePrerequisites(); if (!prereqValid) { p.outro('Please fix the prerequisites issues and try again'); process.exit(1); } // Detect existing installation const existingInstall = detectExistingInstallation(); // Run installation wizard const wizardConfig = await runInstallationWizard(existingInstall); if (!wizardConfig) { process.exit(0); } config = wizardConfig; } // Backup existing configuration if force reinstall if (config.forceReinstall) { const backupPath = await backupExistingConfig(); if (backupPath) { p.log.info(`Backup created at: ${backupPath}`); } } // Enhanced installation steps with beautiful progress console.log(vibrantRainbow('\n🚀 Beginning Installation Process\n')); const installationSteps = [ { name: 'Creating directory structure', action: async () => { await sleep(200); ensureDirectoryStructure(); await sleep(100); } }, { name: 'Installing Chroma MCP server', action: async () => { const success = await installChromaMcp(); if (!success) throw new Error('MCP installation failed'); } }, { name: 'Adding CLAUDE.md instructions', action: async () => { await sleep(300); ensureClaudeMdInstructions(); await sleep(200); } }, { name: 'Installing Claude commands', action: async () => { await sleep(200); installClaudeCommands(config.forceReinstall); await sleep(100); } }, { name: 'Installing memory hooks', action: async () => { await sleep(400); writeHookFiles(config.hookTimeout); await sleep(200); } }, { name: 'Configuring Claude settings', action: async () => { await sleep(300); // Determine settings path let settingsPath: string; if (config.scope === 'local' && config.customPath) { settingsPath = join(config.customPath, 'settings.local.json'); } else if (config.scope === 'project') { settingsPath = join(process.cwd(), '.claude', 'settings.json'); } else { settingsPath = PathDiscovery.getInstance().getClaudeSettingsPath(); } await configureHooks(settingsPath, config); // Store backend setting in user settings const pathDiscovery = PathDiscovery.getInstance(); const userSettingsPath = pathDiscovery.getUserSettingsPath(); let userSettings: Settings = {}; if (existsSync(userSettingsPath)) { try { userSettings = JSON.parse(readFileSync(userSettingsPath, 'utf8')); } catch {} } userSettings.backend = 'chroma'; userSettings.installed = true; userSettings.embedded = true; userSettings.saveMemoriesOnClear = config.saveMemoriesOnClear || false; // Detect and store Claude CLI path const claudePath = detectClaudePath(); if (claudePath) { userSettings.claudePath = claudePath; } else { delete userSettings.claudePath; } writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2)); await sleep(200); } } ]; // Add Smart Trash step if enabled if (config.enableSmartTrash) { installationSteps.push({ name: 'Configuring Smart Trash alias', action: async () => { await sleep(200); await configureSmartTrashAlias(); await sleep(100); } }); } // Execute all steps with enhanced progress display for (let i = 0; i < installationSteps.length; i++) { const step = installationSteps[i]; const progress = `[${i + 1}/${installationSteps.length}]`; const loader = createLoadingAnimation(`${chalk.gray(progress)} ${step.name}...`); loader.start(); try { await step.action(); loader.stop(`${chalk.gray(progress)} ${step.name} ${vibrantRainbow('completed! ✨')}`); } catch (error) { loader.stop(`${chalk.gray(progress)} ${step.name} ${chalk.red('failed')}`, false); console.log(boxen(chalk.red(`❌ Installation failed at: ${step.name}\n\nError: ${error}`), { padding: 1, margin: 1, borderStyle: 'double', borderColor: 'red' })); process.exit(1); } await sleep(150); // Smooth progression } // Verification with style console.log(chalk.gray('\n🔍 Verifying Installation\n')); await verifyInstallation(); // Beautiful success message const successTitle = fastRainbow('🎉 INSTALLATION COMPLETE! 🎉'); const saveCommand = config.saveMemoriesOnClear ? `${chalk.cyan('/compact')} or ${chalk.cyan('/clear')}` : chalk.cyan('/compact'); const successMessage = ` ${chalk.bold('How your new memory system works:')} ${chalk.green('•')} When you start Claude Code, claude-mem loads your latest memories automatically ${chalk.green('•')} Save your work by typing ${saveCommand} (takes ~30s to process) ${chalk.green('•')} Ask Claude to search your memories anytime with natural language ${chalk.green('•')} Instructions added to ${chalk.cyan('~/.claude/CLAUDE.md')} teach Claude how to use the system ${chalk.bold('Slash Commands Available:')} ${chalk.cyan('/claude-mem help')} - Show all memory commands and features ${chalk.cyan('/save')} - Quick save of current conversation overview ${chalk.cyan('/remember')} - Search your saved memories ${chalk.bold('Quick Start:')} ${chalk.yellow('1.')} Restart Claude Code to activate your memory system ${chalk.yellow('2.')} Start using Claude normally - memories save automatically ${chalk.yellow('3.')} Search memories by asking: ${chalk.italic('"Search my memories for X"')}`; const finalSmartTrashNote = config.enableSmartTrash ? `\n\n${chalk.blue('🗑️ Smart Trash Enabled:')} ${chalk.gray(' • rm commands now move files to ~/.claude-mem/trash')} ${chalk.gray(' • View trash:')} ${chalk.cyan('claude-mem trash view')} ${chalk.gray(' • Restore files:')} ${chalk.cyan('claude-mem restore')} ${chalk.gray(' • Empty trash:')} ${chalk.cyan('claude-mem trash empty')} ${chalk.yellow(' • Restart terminal for alias to activate')}` : ''; const finalClearHookNote = config.saveMemoriesOnClear ? `\n\n${chalk.magenta('💾 Save-on-clear enabled:')} ${chalk.gray(' • /clear now saves memories automatically (takes ~1 minute)')}` : ''; console.log(boxen(successTitle + successMessage + finalSmartTrashNote + finalClearHookNote, { padding: 2, margin: 1, borderStyle: 'double', borderColor: 'green', backgroundColor: '#001122' })); // Final flourish console.log(fastRainbow('\n✨ Welcome to the future of persistent AI conversations! ✨\n')); }