mirror of
https://github.com/obra/episodic-memory.git
synced 2026-09-14 13:43:14 +08:00
32c8382c2d
cli/mcp-server-wrapper.js previously checked only `existsSync(node_modules)` to decide whether to run `npm install`. A partial extraction — the directory exists, but a package is missing its package.json and lib/ — slips past that check, hands off to dist/mcp-server.js, and crashes with a confusing `ERR_MODULE_NOT_FOUND` after the wrapper has already declared deps healthy. The reporter on Windows 11 saw exactly this for better-sqlite3 (folder contained only `deps/` and `LICENSE`). New module cli/install-check.js exports findMissingDeps(pluginRoot) which returns the list of runtime-required packages whose package.json is missing under node_modules. The wrapper now logs the missing packages before re-running `npm install`, giving the user a useful diagnostic when the partial-extract case strikes again. Probing each package's manifest — not just the directory — catches the specific failure shape that originally motivated the issue. Optional and OS-specific externals (sharp, fsevents) are deliberately excluded from the check. Tests in test/install-check.test.ts cover the no-node_modules case, the empty-node_modules case, the all-present happy path, the partial-extraction case (better-sqlite3 dir present but manifest gone), multi-missing reporting, and the optional-deps-excluded behavior. Addresses Bug 1 of #95; Bug 2 (onnxruntime-common hoisting) is deferred pending a Windows reproduction.
125 lines
4.0 KiB
JavaScript
Executable File
125 lines
4.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Cross-platform wrapper script for MCP server that ensures dependencies are installed
|
|
* This runs before the MCP server starts and works on Windows, macOS, and Linux
|
|
*/
|
|
|
|
import { spawn } from 'child_process';
|
|
import { existsSync } from 'fs';
|
|
import { dirname, join } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { findMissingDeps } from './install-check.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Determine plugin root directory
|
|
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(__dirname, '..');
|
|
|
|
// Helper function to run npm install
|
|
function runNpmInstall() {
|
|
return new Promise((resolve, reject) => {
|
|
const isWindows = process.platform === 'win32';
|
|
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
|
|
|
|
console.error('Installing episodic-memory dependencies (first run only)...');
|
|
console.error('This may take 30-60 seconds...');
|
|
|
|
// Install dependencies - npm will auto-install optionalDependencies for current platform
|
|
const child = spawn(npmCommand, ['install', '--no-audit', '--no-fund'], {
|
|
cwd: PLUGIN_ROOT,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
shell: isWindows // On Windows, we need shell: true to find npm.cmd
|
|
});
|
|
|
|
child.stdout.on('data', (data) => {
|
|
// Suppress npm install output to stderr to avoid cluttering MCP logs
|
|
process.stderr.write(data);
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
process.stderr.write(data);
|
|
});
|
|
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
console.error('Dependencies installed successfully.');
|
|
resolve();
|
|
} else {
|
|
console.error('ERROR: Failed to install dependencies.');
|
|
console.error(`Please run manually: cd "${PLUGIN_ROOT}" && npm install`);
|
|
reject(new Error(`npm install failed with exit code ${code}`));
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
console.error(`ERROR: Failed to run npm install: ${err.message}`);
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
// Probe each required runtime dependency's package.json — not just the
|
|
// node_modules directory. A partial extraction (folder exists but the
|
|
// package is missing its manifest and lib/) would slip past an existsSync
|
|
// check on node_modules alone and crash the server with ERR_MODULE_NOT_FOUND
|
|
// *after* the wrapper has handed off to dist/mcp-server.js (#95 Bug 1).
|
|
const missing = findMissingDeps(PLUGIN_ROOT);
|
|
if (missing.length > 0) {
|
|
console.error(`Missing dependencies under node_modules: ${missing.join(', ')}`);
|
|
await runNpmInstall();
|
|
}
|
|
|
|
// Start the MCP server
|
|
const mcpServerPath = join(PLUGIN_ROOT, 'dist', 'mcp-server.js');
|
|
|
|
if (!existsSync(mcpServerPath)) {
|
|
console.error(`ERROR: MCP server not found at ${mcpServerPath}`);
|
|
console.error('Please run: npm run build');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Use spawn with shell: false for better cross-platform compatibility
|
|
const child = spawn(process.execPath, [mcpServerPath], {
|
|
stdio: 'inherit',
|
|
shell: false
|
|
});
|
|
|
|
// Forward signals to the child process
|
|
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
process.on('SIGHUP', () => child.kill('SIGHUP'));
|
|
|
|
// Detect parent process death via stdin close
|
|
// When Claude exits (normally or abnormally), stdin will close
|
|
process.stdin.on('end', () => {
|
|
child.kill();
|
|
process.exit(0);
|
|
});
|
|
|
|
child.on('exit', (code, signal) => {
|
|
if (signal) {
|
|
process.kill(process.pid, signal);
|
|
} else {
|
|
process.exit(code || 0);
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
console.error(`ERROR: Failed to start MCP server: ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`ERROR: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`Unexpected error: ${error.message}`);
|
|
process.exit(1);
|
|
});
|