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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user