refactor: require Bun globally, add auto-install, remove Windows executable approach (#243)

- Delete BinaryManager.ts - no longer needed
- Simplify ProcessManager.ts - single Bun spawn path for all platforms
- Update smart-install.js - auto-install Bun if missing (Windows/Unix)
- Update documentation to reflect Bun requirement

This simplifies the codebase by:
- Using Bun consistently across all platforms (hooks + worker)
- Eliminating binary download/hosting complexity
- Zero native dependencies with bun:sqlite
- Auto-installing Bun on first run if not present

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-11 17:27:18 -05:00
committed by GitHub
parent 50c7603a37
commit d24d5dda04
6 changed files with 189 additions and 177 deletions
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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)
---
+29 -30
View File
@@ -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
+145 -14
View File
@@ -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);
}
-76
View File
@@ -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<string> {
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<void> {
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;
}
}
}
+8 -50
View File
@@ -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<boolean> {
const info = this.getPidInfo();
if (!info) return true;