#!/usr/bin/env node /** * Build script for claude-mem hooks * Bundles TypeScript hooks into individual standalone executables using esbuild */ import { build } from 'esbuild'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const HOOKS = [ { name: 'context-hook', source: 'src/hooks/context-hook.ts' }, { name: 'new-hook', source: 'src/hooks/new-hook.ts' }, { name: 'save-hook', source: 'src/hooks/save-hook.ts' }, { name: 'summary-hook', source: 'src/hooks/summary-hook.ts' }, { name: 'cleanup-hook', source: 'src/hooks/cleanup-hook.ts' }, { name: 'user-message-hook', source: 'src/hooks/user-message-hook.ts' } ]; const WORKER_SERVICE = { name: 'worker-service', source: 'src/services/worker-service.ts' }; const WORKER_WRAPPER = { name: 'worker-wrapper', source: 'src/services/worker-wrapper.ts' }; const MCP_SERVER = { name: 'mcp-server', source: 'src/servers/mcp-server.ts' }; const CONTEXT_GENERATOR = { name: 'context-generator', source: 'src/services/context-generator.ts' }; const WORKER_CLI = { name: 'worker-cli', source: 'src/cli/worker-cli.ts' }; async function buildHooks() { console.log('šŸ”Ø Building claude-mem hooks and worker service...\n'); try { // Read version from package.json const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const version = packageJson.version; console.log(`šŸ“Œ Version: ${version}`); // Create output directories console.log('\nšŸ“¦ Preparing output directories...'); const hooksDir = 'plugin/scripts'; const uiDir = 'plugin/ui'; if (!fs.existsSync(hooksDir)) { fs.mkdirSync(hooksDir, { recursive: true }); } if (!fs.existsSync(uiDir)) { fs.mkdirSync(uiDir, { recursive: true }); } console.log('āœ“ Output directories ready'); // Generate plugin/package.json for cache directory dependency installation // Note: bun:sqlite is a Bun built-in, no external dependencies needed for SQLite console.log('\nšŸ“¦ Generating plugin package.json...'); const pluginPackageJson = { name: 'claude-mem-plugin', version: version, private: true, description: 'Runtime dependencies for claude-mem bundled hooks', type: 'module', dependencies: {}, engines: { node: '>=18.0.0', bun: '>=1.0.0' } }; fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n'); console.log('āœ“ plugin/package.json generated'); // Build React viewer console.log('\nšŸ“‹ Building React viewer...'); const { spawn } = await import('child_process'); const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' }); await new Promise((resolve, reject) => { viewerBuild.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Viewer build failed with exit code ${code}`)); } }); }); // Build worker service console.log(`\nšŸ”§ Building worker service...`); await build({ entryPoints: [WORKER_SERVICE.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`, minify: true, logLevel: 'error', // Suppress warnings (import.meta warning is benign) external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env bun' } }); // Make worker service executable fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755); const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`); console.log(`āœ“ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`); // Build worker wrapper (Windows zombie port fix) console.log(`\nšŸ”§ Building worker wrapper...`); await build({ entryPoints: [WORKER_WRAPPER.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${WORKER_WRAPPER.name}.cjs`, minify: true, logLevel: 'error', external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env bun' } }); // Make worker wrapper executable fs.chmodSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`, 0o755); const wrapperStats = fs.statSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`); console.log(`āœ“ worker-wrapper built (${(wrapperStats.size / 1024).toFixed(2)} KB)`); // Build MCP server console.log(`\nšŸ”§ Building MCP server...`); await build({ entryPoints: [MCP_SERVER.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`, minify: true, logLevel: 'error', external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env node' } }); // Make MCP server executable fs.chmodSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 0o755); const mcpServerStats = fs.statSync(`${hooksDir}/${MCP_SERVER.name}.cjs`); console.log(`āœ“ mcp-server built (${(mcpServerStats.size / 1024).toFixed(2)} KB)`); // Build context generator console.log(`\nšŸ”§ Building context generator...`); await build({ entryPoints: [CONTEXT_GENERATOR.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`, minify: true, logLevel: 'error', external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` } }); const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`); console.log(`āœ“ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`); // Build worker CLI console.log(`\nšŸ”§ Building worker CLI...`); await build({ entryPoints: [WORKER_CLI.source], bundle: true, platform: 'node', target: 'node18', format: 'esm', outfile: `${hooksDir}/${WORKER_CLI.name}.js`, minify: true, logLevel: 'error', external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env bun' } }); // Make worker CLI executable fs.chmodSync(`${hooksDir}/${WORKER_CLI.name}.js`, 0o755); const workerCliStats = fs.statSync(`${hooksDir}/${WORKER_CLI.name}.js`); console.log(`āœ“ worker-cli built (${(workerCliStats.size / 1024).toFixed(2)} KB)`); // Build each hook for (const hook of HOOKS) { console.log(`\nšŸ”§ Building ${hook.name}...`); const outfile = `${hooksDir}/${hook.name}.js`; await build({ entryPoints: [hook.source], bundle: true, platform: 'node', target: 'node18', format: 'esm', outfile, minify: true, external: ['bun:sqlite'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env bun' } }); // Make executable fs.chmodSync(outfile, 0o755); // Check file size const stats = fs.statSync(outfile); const sizeInKB = (stats.size / 1024).toFixed(2); console.log(`āœ“ ${hook.name} built (${sizeInKB} KB)`); } // Build mem-search skill zip for Claude Desktop console.log('\nšŸ“¦ Building mem-search skill zip for Claude Desktop...'); const { execSync } = await import('child_process'); const zipOutput = 'plugin/skills/mem-search.zip'; // Remove old zip if exists if (fs.existsSync(zipOutput)) { fs.unlinkSync(zipOutput); } // Create zip from mem-search skill directory execSync(`cd plugin/skills && zip -r mem-search.zip mem-search/`, { stdio: 'pipe' }); const zipStats = fs.statSync(zipOutput); console.log(`āœ“ mem-search.zip built (${(zipStats.size / 1024).toFixed(2)} KB)`); console.log('\nāœ… All hooks, worker service, and MCP server built successfully!'); console.log(` Output: ${hooksDir}/`); console.log(` - Hooks: *-hook.js`); console.log(` - Worker: worker-service.cjs`); console.log(` - MCP Server: mcp-server.cjs`); console.log(` - Skills: plugin/skills/`); console.log(` - Desktop Skill: plugin/skills/mem-search.zip`); console.log('\nšŸ’” Note: Dependencies will be auto-installed on first hook execution'); } catch (error) { console.error('\nāŒ Build failed:', error.message); if (error.errors) { console.error('\nBuild errors:'); error.errors.forEach(err => console.error(` - ${err.text}`)); } process.exit(1); } } buildHooks();