#!/usr/bin/env node /** * Smart Install Script for claude-mem * * Ensures Bun runtime and uv (Python package manager) are installed * (auto-installs if missing) and handles dependency installation when needed. * * Resolves the install directory from CLAUDE_PLUGIN_ROOT (set by Claude Code * for both cache and marketplace installs), falling back to script location * and legacy paths. */ import { existsSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from 'fs'; import { execSync, spawnSync } from 'child_process'; import { join, dirname } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; // Early exit if plugin is disabled in Claude Code settings (#781) function isPluginDisabledInClaudeSettings() { try { const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'); const settingsPath = join(configDir, 'settings.json'); if (!existsSync(settingsPath)) return false; const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); return settings?.enabledPlugins?.['claude-mem@thedotmack'] === false; } catch { return false; } } if (isPluginDisabledInClaudeSettings()) { process.exit(0); } const IS_WINDOWS = process.platform === 'win32'; /** * Resolve the plugin root directory where dependencies should be installed. * * Priority: * 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks — works for * both cache-based and marketplace installs) * 2. Script location (dirname of this file, up one level from scripts/) * 3. XDG path (~/.config/claude/plugins/marketplaces/thedotmack) * 4. Legacy path (~/.claude/plugins/marketplaces/thedotmack) */ function resolveRoot() { // CLAUDE_PLUGIN_ROOT is the authoritative location set by Claude Code if (process.env.CLAUDE_PLUGIN_ROOT) { const root = process.env.CLAUDE_PLUGIN_ROOT; if (existsSync(join(root, 'package.json'))) return root; } // Derive from script location (this file is in /scripts/) try { const scriptDir = dirname(fileURLToPath(import.meta.url)); const candidate = dirname(scriptDir); if (existsSync(join(candidate, 'package.json'))) return candidate; } catch { // import.meta.url not available } // Probe XDG path, then legacy const marketplaceRel = join('plugins', 'marketplaces', 'thedotmack'); const xdg = join(homedir(), '.config', 'claude', marketplaceRel); if (existsSync(join(xdg, 'package.json'))) return xdg; return join(homedir(), '.claude', marketplaceRel); } const ROOT = resolveRoot(); const MARKER = join(ROOT, '.install-version'); /** * Check if Bun is installed and accessible */ function isBunInstalled() { try { const result = spawnSync('bun', ['--version'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: IS_WINDOWS }); if (result.status === 0) return true; } catch { // PATH check failed, try common installation paths } // Check common installation paths (handles fresh installs before PATH reload) const bunPaths = IS_WINDOWS ? [join(homedir(), '.bun', 'bin', 'bun.exe')] : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun']; return bunPaths.some(existsSync); } /** * Get the Bun executable path (from PATH or common install locations) */ function getBunPath() { // Try PATH first try { const result = spawnSync('bun', ['--version'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: IS_WINDOWS }); if (result.status === 0) return 'bun'; } catch { // Not in PATH } // Check common installation paths const bunPaths = IS_WINDOWS ? [join(homedir(), '.bun', 'bin', 'bun.exe')] : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun']; for (const bunPath of bunPaths) { if (existsSync(bunPath)) return bunPath; } return null; } /** * Minimum required bun version * v1.1.14+ required for .changes property and multi-statement SQL support */ const MIN_BUN_VERSION = '1.1.14'; /** * Compare semver versions */ function compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < 3; i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return 1; if (p1 < p2) return -1; } return 0; } /** * Check if bun version meets minimum requirements */ function isBunVersionSufficient() { const version = getBunVersion(); if (!version) return false; return compareVersions(version, MIN_BUN_VERSION) >= 0; } /** * Get Bun version if installed */ function getBunVersion() { const bunPath = getBunPath(); if (!bunPath) return null; try { const result = spawnSync(bunPath, ['--version'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: IS_WINDOWS }); return result.status === 0 ? result.stdout.trim() : null; } catch { return null; } } /** * Check if uv is installed and accessible */ function isUvInstalled() { try { const result = spawnSync('uv', ['--version'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: IS_WINDOWS }); if (result.status === 0) return true; } catch { // PATH check failed, try common installation paths } // Check common installation paths (handles fresh installs before PATH reload) const uvPaths = IS_WINDOWS ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')] : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv']; return uvPaths.some(existsSync); } /** * Get uv version if installed */ function getUvVersion() { try { const result = spawnSync('uv', ['--version'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: IS_WINDOWS }); return result.status === 0 ? result.stdout.trim() : null; } catch { return null; } } /** * Install Bun automatically based on platform */ function installBun() { console.error('🔧 Bun not found. Installing Bun runtime...'); try { if (IS_WINDOWS) { // Windows: Use PowerShell installer console.error(' Installing via PowerShell...'); execSync('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: ['pipe', 'pipe', 'inherit'], shell: true }); } else { // Unix/macOS: Use curl installer console.error(' Installing via curl...'); execSync('curl -fsSL https://bun.sh/install | bash', { stdio: ['pipe', 'pipe', 'inherit'], shell: true }); } // Verify installation if (isBunInstalled()) { const version = getBunVersion(); console.error(`✅ Bun ${version} installed successfully`); return true; } else { // Bun may be installed but not in PATH yet for this session // Try common installation paths const bunPaths = IS_WINDOWS ? [join(homedir(), '.bun', 'bin', 'bun.exe')] : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun']; for (const bunPath of bunPaths) { if (existsSync(bunPath)) { console.error(`✅ Bun installed at ${bunPath}`); console.error('⚠️ Please restart your terminal or add Bun to PATH:'); if (IS_WINDOWS) { console.error(` $env:Path += ";${join(homedir(), '.bun', 'bin')}"`); } else { console.error(` export PATH="$HOME/.bun/bin:$PATH"`); } return true; } } throw new Error('Bun installation completed but binary not found'); } } catch (error) { console.error('❌ Failed to install Bun automatically'); console.error(' Please install manually:'); if (IS_WINDOWS) { console.error(' - winget install Oven-sh.Bun'); console.error(' - Or: powershell -c "irm bun.sh/install.ps1 | iex"'); } else { console.error(' - curl -fsSL https://bun.sh/install | bash'); console.error(' - Or: brew install oven-sh/bun/bun'); } console.error(' Then restart your terminal and try again.'); throw error; } } /** * Install uv automatically based on platform */ function installUv() { console.error('🐍 Installing uv for Python/Chroma support...'); try { if (IS_WINDOWS) { // Windows: Use PowerShell installer console.error(' Installing via PowerShell...'); execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: ['pipe', 'pipe', 'inherit'], shell: true }); } else { // Unix/macOS: Use curl installer console.error(' Installing via curl...'); execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: ['pipe', 'pipe', 'inherit'], shell: true }); } // Verify installation if (isUvInstalled()) { const version = getUvVersion(); console.error(`✅ uv ${version} installed successfully`); return true; } else { // uv may be installed but not in PATH yet for this session // Try common installation paths const uvPaths = IS_WINDOWS ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')] : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv']; for (const uvPath of uvPaths) { if (existsSync(uvPath)) { console.error(`✅ uv installed at ${uvPath}`); console.error('⚠️ Please restart your terminal or add uv to PATH:'); if (IS_WINDOWS) { console.error(` $env:Path += ";${join(homedir(), '.local', 'bin')}"`); } else { console.error(` export PATH="$HOME/.local/bin:$PATH"`); } return true; } } throw new Error('uv installation completed but binary not found'); } } catch (error) { console.error('❌ Failed to install uv automatically'); console.error(' Please install manually:'); if (IS_WINDOWS) { console.error(' - winget install astral-sh.uv'); console.error(' - Or: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"'); } else { console.error(' - curl -LsSf https://astral.sh/uv/install.sh | sh'); console.error(' - Or: brew install uv (macOS)'); } console.error(' Then restart your terminal and try again.'); throw error; } } /** * Add shell alias for claude-mem command */ function installCLI() { const WORKER_CLI = join(ROOT, 'scripts', 'worker-service.cjs'); const bunPath = getBunPath() || 'bun'; const aliasLine = `alias claude-mem='${bunPath} "${WORKER_CLI}"'`; const markerPath = join(ROOT, '.cli-installed'); // Skip if already installed if (existsSync(markerPath)) return; try { if (IS_WINDOWS) { // Windows: Add to PATH via PowerShell profile const profilePath = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'); const profileDir = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell'); const functionDef = `function claude-mem { & "${bunPath}" "${WORKER_CLI}" $args }\n`; if (!existsSync(profileDir)) { execSync(`mkdir "${profileDir}"`, { stdio: 'ignore', shell: true }); } const existingContent = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''; if (!existingContent.includes('function claude-mem')) { writeFileSync(profilePath, existingContent + '\n' + functionDef); console.error(`✅ PowerShell function added to profile`); console.error(' Restart your terminal to use: claude-mem '); } } else { // Unix: Add alias to shell configs const shellConfigs = [ join(homedir(), '.bashrc'), join(homedir(), '.zshrc') ]; for (const config of shellConfigs) { if (existsSync(config)) { const content = readFileSync(config, 'utf-8'); if (!content.includes('alias claude-mem=')) { writeFileSync(config, content + '\n' + aliasLine + '\n'); console.error(`✅ Alias added to ${config}`); } } } console.error(' Restart your terminal to use: claude-mem '); } writeFileSync(markerPath, new Date().toISOString()); } catch (error) { console.error(`⚠️ Could not add shell alias: ${error.message}`); console.error(` Use directly: ${bunPath} "${WORKER_CLI}" `); } } /** * Check if dependencies need to be installed */ function needsInstall() { if (!existsSync(join(ROOT, 'node_modules'))) return true; try { const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); const marker = JSON.parse(readFileSync(MARKER, 'utf-8')); return pkg.version !== marker.version || getBunVersion() !== marker.bun; } catch { return true; } } /** * Install dependencies using Bun with npm fallback * * Bun has issues with npm alias packages (e.g., string-width-cjs, strip-ansi-cjs) * that are defined in package-lock.json. When bun fails with 404 errors for these * packages, we fall back to npm which handles aliases correctly. */ function installDeps() { const bunPath = getBunPath(); if (!bunPath) { throw new Error('Bun executable not found'); } console.error('📦 Installing dependencies with Bun...'); // Quote path for Windows paths with spaces const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath; // Use pipe for stdout to prevent non-JSON output leaking to Claude Code hooks. // stderr is inherited so progress/errors are still visible to the user. const installStdio = ['pipe', 'pipe', 'inherit']; let bunSucceeded = false; try { execSync(`${bunCmd} install`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS }); bunSucceeded = true; } catch { // First attempt failed, try with force flag try { execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS }); bunSucceeded = true; } catch { // Bun failed completely, will try npm fallback } } // Fallback to npm if bun failed (handles npm alias packages correctly) if (!bunSucceeded) { console.error('⚠️ Bun install failed, falling back to npm...'); console.error(' (This can happen with npm alias packages like *-cjs)'); try { execSync('npm install --legacy-peer-deps', { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS }); } catch (npmError) { throw new Error('Both bun and npm install failed: ' + npmError.message); } } // Write version marker const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); writeFileSync(MARKER, JSON.stringify({ version: pkg.version, bun: getBunVersion(), uv: getUvVersion(), installedAt: new Date().toISOString() })); } /** * Verify that critical runtime modules are resolvable from the install directory. * Returns true if all critical modules exist, false otherwise. */ function verifyCriticalModules() { const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); const dependencies = Object.keys(pkg.dependencies || {}); const missing = []; for (const dep of dependencies) { // Check that the module directory exists in node_modules const modulePath = join(ROOT, 'node_modules', ...dep.split('/')); if (!existsSync(modulePath)) { missing.push(dep); } } if (missing.length > 0) { console.error(`❌ Post-install check failed: missing modules: ${missing.join(', ')}`); return false; } return true; } // Mach-O 64-bit magic values as seen when reading the first 4 file bytes with readUInt32LE. // Native arm64/x86_64 Mach-O files start with bytes [CF FA ED FE]; readUInt32LE gives 0xFEEDFACF. // Byte-swapped (big-endian) Mach-O files start with bytes [FE ED FA CF]; readUInt32LE gives 0xCFFAEDFE. const MACHO_MAGIC_NATIVE = 0xFEEDFACF; // native 64-bit (arm64/x86_64) — file bytes CF FA ED FE const MACHO_MAGIC_SWAPPED = 0xCFFAEDFE; // byte-swapped 64-bit — file bytes FE ED FA CF /** * Warn when the bundled claude-mem binary cannot run on the current platform. * * The committed binary (plugin/scripts/claude-mem) is compiled for macOS arm64. * On Linux or Windows it produces "Exec format error" and silently fails. * This check surfaces the incompatibility at install time so users know why * the binary path doesn't work, and confirms the JS fallback (bun-runner.js → * worker-service.cjs) is active and covers all functionality. * * Fixes #1547 — Plugin silently fails on Linux ARM64. */ export function checkBinaryPlatformCompatibility(binaryPath = join(ROOT, 'scripts', 'claude-mem')) { if (!existsSync(binaryPath)) { return; // Binary absent — nothing to check (e.g. after npm install which excludes it) } // The binary only matters on non-macOS platforms; on macOS it works correctly. if (process.platform === 'darwin') { return; } // Read the first 4 bytes to identify the binary format. let fd; try { const buf = Buffer.alloc(4); fd = openSync(binaryPath, 'r'); readSync(fd, buf, 0, 4, 0); const magic = buf.readUInt32LE(0); if (magic === MACHO_MAGIC_NATIVE || magic === MACHO_MAGIC_SWAPPED) { console.error('⚠️ Platform notice: The bundled claude-mem binary is macOS-only.'); console.error(` Current platform: ${process.platform} ${process.arch}`); console.error(' The binary will not execute on this platform.'); console.error(' Plugin functionality is provided by the JS fallback'); console.error(' (bun-runner.js → worker-service.cjs) which works on all platforms.'); } } catch { // Unreadable binary — not critical, skip silently } finally { if (fd !== undefined) closeSync(fd); } } // Main execution try { // Step 1: Ensure Bun is installed and meets minimum version (REQUIRED) if (!isBunInstalled()) { installBun(); // Re-check after installation if (!isBunInstalled()) { console.error('❌ Bun is required but not available in PATH'); console.error(' Please restart your terminal after installation'); process.exit(1); } } // Step 1.5: Ensure Bun version is sufficient if (!isBunVersionSufficient()) { const currentVersion = getBunVersion(); console.error(`⚠️ Bun ${currentVersion} is outdated. Minimum required: ${MIN_BUN_VERSION}`); console.error(' Upgrading bun...'); try { execSync('bun upgrade', { stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS }); if (!isBunVersionSufficient()) { console.error(`❌ Bun upgrade failed. Please manually upgrade: bun upgrade`); process.exit(1); } console.error(`✅ Bun upgraded to ${getBunVersion()}`); } catch (error) { console.error(`❌ Failed to upgrade bun: ${error.message}`); console.error(' Please manually upgrade: bun upgrade'); process.exit(1); } } // Step 2: Ensure uv is installed (REQUIRED for vector search) if (!isUvInstalled()) { installUv(); // Re-check after installation if (!isUvInstalled()) { console.error('❌ uv is required but not available in PATH'); console.error(' Please restart your terminal after installation'); process.exit(1); } } // Step 3: Install dependencies if needed if (needsInstall()) { const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); const newVersion = pkg.version; installDeps(); // Verify critical modules are resolvable if (!verifyCriticalModules()) { console.error('⚠️ Retrying install with npm...'); try { execSync('npm install --production --legacy-peer-deps', { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS }); } catch { // npm also failed } if (!verifyCriticalModules()) { console.error('❌ Dependencies could not be installed. Plugin may not work correctly.'); process.exit(1); } } console.error('✅ Dependencies installed'); // Auto-restart worker to pick up new code const port = process.env.CLAUDE_MEM_WORKER_PORT || 37777; console.error(`[claude-mem] Plugin updated to v${newVersion} - restarting worker...`); try { // Graceful shutdown via HTTP (curl is cross-platform enough) execSync(`curl -s -X POST http://127.0.0.1:${port}/api/admin/shutdown`, { stdio: 'ignore', shell: IS_WINDOWS, timeout: 5000 }); // Brief wait for port to free execSync(IS_WINDOWS ? 'timeout /t 1 /nobreak >nul' : 'sleep 0.5', { stdio: 'ignore', shell: true }); } catch { // Worker wasn't running or already stopped - that's fine } // Worker will be started fresh by next hook in chain (worker-service.cjs start) } // Step 4: Install CLI to PATH installCLI(); // Step 5: Warn if the bundled native binary is incompatible with this platform checkBinaryPlatformCompatibility(); // Output valid JSON for Claude Code hook contract console.log(JSON.stringify({ continue: true, suppressOutput: true })); } catch (e) { console.error('❌ Installation failed:', e.message); // Still output valid JSON so Claude Code doesn't show a confusing error console.log(JSON.stringify({ continue: true, suppressOutput: true })); process.exit(1); }