diff --git a/CLAUDE.md b/CLAUDE.md index 142dcb23..d6007ade 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,8 +76,8 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created ## Requirements -- **Bun** >= 1.0 (Mac/Linux runtime) -- Node.js >= 18 (build tools) +- **Bun** >= 1.0 (all platforms - auto-installed if missing) +- Node.js >= 18 (build tools only) ## Quick Reference diff --git a/README.md b/README.md index 21ca8e24..9e68ded3 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ npx mintlify dev - **[Architecture Evolution](https://docs.claude-mem.ai/architecture-evolution)** - The journey from v3 to v5 - **[Hooks Architecture](https://docs.claude-mem.ai/hooks-architecture)** - How Claude-Mem uses lifecycle hooks - **[Hooks Reference](https://docs.claude-mem.ai/architecture/hooks)** - 7 hook scripts explained -- **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & PM2 management +- **[Worker Service](https://docs.claude-mem.ai/architecture/worker-service)** - HTTP API & Bun process management - **[Database](https://docs.claude-mem.ai/architecture/database)** - SQLite schema & FTS5 search - **[Search Architecture](https://docs.claude-mem.ai/architecture/search-architecture)** - Hybrid search with Chroma vector database @@ -148,7 +148,7 @@ npx mintlify dev 1. **5 Lifecycle Hooks** - SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd (6 hook scripts) 2. **Smart Install** - Cached dependency checker (pre-hook script, not a lifecycle hook) -3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by PM2 +3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by Bun 4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search 5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP) 6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval @@ -260,10 +260,10 @@ See [CHANGELOG.md](CHANGELOG.md) for complete version history. ## System Requirements -- **Node.js**: 18.0.0 or higher +- **Bun**: 1.0 or higher (auto-installed on first run if missing) +- **Node.js**: 18.0.0 or higher (for build tools) - **Claude Code**: Latest version with plugin support -- **PM2**: Process manager (bundled - no global install required) -- **SQLite 3**: For persistent storage (bundled) +- **SQLite 3**: For persistent storage (via bun:sqlite - zero native dependencies) --- diff --git a/docs/public/architecture/worker-service.mdx b/docs/public/architecture/worker-service.mdx index 1f87c2b4..e06953ea 100644 --- a/docs/public/architecture/worker-service.mdx +++ b/docs/public/architecture/worker-service.mdx @@ -1,16 +1,17 @@ --- title: "Worker Service" -description: "HTTP API and PM2 process management" +description: "HTTP API and Bun process management" --- # Worker Service -The worker service is a long-running HTTP API built with Express.js and managed by PM2. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues. +The worker service is a long-running HTTP API built with Express.js and managed natively by Bun. It processes observations through the Claude Agent SDK separately from hook execution to prevent timeout issues. ## Overview - **Technology**: Express.js HTTP server -- **Process Manager**: PM2 +- **Runtime**: Bun (auto-installed if missing) +- **Process Manager**: Native Bun process management via ProcessManager - **Port**: Fixed port 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`) - **Location**: `src/services/worker-service.ts` - **Built Output**: `plugin/scripts/worker-service.cjs` @@ -322,28 +323,15 @@ DELETE /sessions/:sessionDbId **Note**: As of v4.1.0, the cleanup hook no longer calls this endpoint. Sessions are marked complete instead of deleted to allow graceful worker shutdown. -## PM2 Management +## Bun Process Management -### Configuration +### Overview -The worker is configured via `ecosystem.config.cjs`: - -```javascript -module.exports = { - apps: [{ - name: 'claude-mem-worker', - script: './plugin/scripts/worker-service.cjs', - instances: 1, - autorestart: true, - watch: false, - max_memory_restart: '1G', - env: { - NODE_ENV: 'production', - FORCE_COLOR: '1' - } - }] -}; -``` +The worker is managed by the native `ProcessManager` class which handles: +- Process spawning with Bun runtime +- PID file tracking at `~/.claude-mem/worker.pid` +- Health checks with automatic retry +- Graceful shutdown with SIGTERM/SIGKILL fallback ### Commands @@ -366,7 +354,18 @@ npm run worker:status ### Auto-Start Behavior -As of v4.0.0, the worker service auto-starts when the SessionStart hook fires. Manual start is optional. +The worker service auto-starts when the SessionStart hook fires. Manual start is optional. + +### Bun Requirement + +Bun is required to run the worker service. If Bun is not installed, the smart-install script will automatically install it on first run: + +- **Windows**: `powershell -c "irm bun.sh/install.ps1 | iex"` +- **macOS/Linux**: `curl -fsSL https://bun.sh/install | bash` + +You can also install manually via: +- `winget install Oven-sh.Bun` (Windows) +- `brew install oven-sh/bun/bun` (macOS) ## Claude Agent SDK Integration @@ -410,15 +409,15 @@ If port 37777 is in use, the worker will fail to start. Set a custom port via en ## Data Storage -The worker service stores data in the plugin data directory: +The worker service stores data in the user data directory: ``` -${CLAUDE_PLUGIN_ROOT}/data/ -├── claude-mem.db # SQLite database -├── worker.port # Current worker port file +~/.claude-mem/ +├── claude-mem.db # SQLite database (bun:sqlite) +├── worker.pid # PID file for process tracking +├── settings.json # User settings └── logs/ - ├── worker-out.log # Worker stdout logs - └── worker-error.log # Worker stderr logs + └── worker-YYYY-MM-DD.log # Daily rotating logs ``` ## Error Handling diff --git a/scripts/smart-install.js b/scripts/smart-install.js index b5f93441..9b88ab32 100644 --- a/scripts/smart-install.js +++ b/scripts/smart-install.js @@ -1,40 +1,171 @@ #!/usr/bin/env node +/** + * Smart Install Script for claude-mem + * + * Ensures Bun runtime is installed (auto-installs if missing) + * and handles dependency installation when needed. + */ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { execSync } from 'child_process'; +import { execSync, spawnSync } from 'child_process'; import { join } from 'path'; import { homedir } from 'os'; const ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); const MARKER = join(ROOT, '.install-version'); +const IS_WINDOWS = process.platform === 'win32'; +/** + * 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 + }); + return result.status === 0; + } catch { + return false; + } +} + +/** + * Get Bun version if installed + */ +function getBunVersion() { + try { + const result = spawnSync('bun', ['--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: 'inherit', + shell: true + }); + } else { + // Unix/macOS: Use curl installer + console.error(' Installing via curl...'); + execSync('curl -fsSL https://bun.sh/install | bash', { + stdio: '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']; + + 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; + } +} + +/** + * 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 || process.version !== marker.node; + return pkg.version !== marker.version || getBunVersion() !== marker.bun; } catch { return true; } } -function install() { - console.error('Installing dependencies...'); +/** + * Install dependencies using Bun + */ +function installDeps() { + console.error('📦 Installing dependencies with Bun...'); try { - execSync('npm install', { cwd: ROOT, stdio: 'inherit' }); + execSync('bun install', { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS }); } catch { - execSync('npm install --force', { cwd: ROOT, stdio: 'inherit' }); + // Retry with force flag + execSync('bun install --force', { cwd: ROOT, stdio: 'inherit', shell: IS_WINDOWS }); } + + // Write version marker const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); - writeFileSync(MARKER, JSON.stringify({ version: pkg.version, node: process.version })); + writeFileSync(MARKER, JSON.stringify({ + version: pkg.version, + bun: getBunVersion(), + installedAt: new Date().toISOString() + })); } -if (needsInstall()) { - try { - install(); - console.error('✅ Dependencies installed'); - } catch (e) { - console.error('❌ npm install failed:', e.message); - process.exit(1); +// Main execution +try { + // Step 1: Ensure Bun is installed + 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 2: Install dependencies if needed + if (needsInstall()) { + installDeps(); + console.error('✅ Dependencies installed'); + } +} catch (e) { + console.error('❌ Installation failed:', e.message); + process.exit(1); } diff --git a/src/services/process/BinaryManager.ts b/src/services/process/BinaryManager.ts deleted file mode 100644 index 95b819b9..00000000 --- a/src/services/process/BinaryManager.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { DATA_DIR } from '../../shared/paths.js'; - -const BIN_DIR = join(DATA_DIR, 'bin'); -const VERSION_FILE = join(BIN_DIR, 'version.txt'); -const GITHUB_RELEASES = 'https://github.com/thedotmack/claude-mem/releases/download'; - -export class BinaryManager { - static async getExecutablePath(): Promise { - if (process.platform !== 'win32') { - throw new Error('BinaryManager only used on Windows'); - } - - const version = this.getCurrentVersion(); - const binaryPath = join(BIN_DIR, 'worker-service.exe'); - - // Check if we have correct version - if (existsSync(binaryPath)) { - const installed = this.getInstalledVersion(); - if (installed === version) return binaryPath; - } - - // Download - await this.downloadBinary(version); - return binaryPath; - } - - private static async downloadBinary(version: string): Promise { - const url = `${GITHUB_RELEASES}/v${version}/worker-service-v${version}-win-x64.exe`; - console.log(`Downloading worker binary v${version}...`); - - const response = await fetch(url); - if (!response.ok) { - throw new Error( - `Download failed: ${response.status}\n` + - `URL: ${url}\n` + - `Make sure the release exists with a Windows binary attached.` - ); - } - - const buffer = await response.arrayBuffer(); - mkdirSync(BIN_DIR, { recursive: true }); - - const binaryPath = join(BIN_DIR, 'worker-service.exe'); - writeFileSync(binaryPath, Buffer.from(buffer)); - - // Write version file - writeFileSync(VERSION_FILE, version); - - console.log('Download complete'); - } - - private static getCurrentVersion(): string { - // Read from package.json in the installed plugin location - // This ensures we get the correct version even when running from marketplace - try { - const packageJson = JSON.parse( - readFileSync(join(DATA_DIR, '..', '.claude', 'plugins', 'marketplaces', 'thedotmack', 'package.json'), 'utf-8') - ); - return packageJson.version; - } catch { - // Fallback to environment variable - return process.env.npm_package_version || 'unknown'; - } - } - - private static getInstalledVersion(): string | null { - try { - if (!existsSync(VERSION_FILE)) return null; - return readFileSync(VERSION_FILE, 'utf-8').trim(); - } catch { - return null; - } - } -} diff --git a/src/services/process/ProcessManager.ts b/src/services/process/ProcessManager.ts index 68c43935..9df4392f 100644 --- a/src/services/process/ProcessManager.ts +++ b/src/services/process/ProcessManager.ts @@ -43,21 +43,21 @@ export class ProcessManager { const logFile = this.getLogFilePath(); - // Platform-specific spawn - if (process.platform === 'win32') { - return this.startWindows(port); - } else { - return this.startUnix(workerScript, logFile, port); - } + // Use Bun on all platforms + return this.startWithBun(workerScript, logFile, port); } - private static async startUnix(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> { + private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> { try { + const isWindows = process.platform === 'win32'; + const child = spawn('bun', [script], { detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) }, - cwd: MARKETPLACE_ROOT + cwd: MARKETPLACE_ROOT, + // Hide console window on Windows + ...(isWindows && { windowsHide: true }) }); // Write logs @@ -89,48 +89,6 @@ export class ProcessManager { } } - private static async startWindows(port: number): Promise<{ success: boolean; pid?: number; error?: string }> { - // Windows: Use compiled exe from ~/.claude-mem/bin/ - // Import dynamically to avoid loading on non-Windows platforms - const { BinaryManager } = await import('./BinaryManager.js'); - - try { - const exePath = await BinaryManager.getExecutablePath(); - - const child = spawn(exePath, [], { - detached: true, - stdio: ['ignore', 'pipe', 'pipe'], - env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) }, - windowsHide: true - }); - - const logFile = this.getLogFilePath(); - const logStream = createWriteStream(logFile, { flags: 'a' }); - child.stdout?.pipe(logStream); - child.stderr?.pipe(logStream); - - child.unref(); - - if (!child.pid) { - return { success: false, error: 'Failed to get PID from spawned process' }; - } - - this.writePidFile({ - pid: child.pid, - port, - startedAt: new Date().toISOString(), - version: process.env.npm_package_version || 'unknown' - }); - - return this.waitForHealth(child.pid, port); - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error) - }; - } - } - static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise { const info = this.getPidInfo(); if (!info) return true;