@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
702 lines (689 loc) • 24.6 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import { Command } from "commander";
import chalk from "chalk";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { execSync } from "child_process";
const CLAUDE_DIR = join(homedir(), ".claude");
const CLAUDE_CONFIG_FILE = join(CLAUDE_DIR, "config.json");
const MCP_CONFIG_FILE = join(CLAUDE_DIR, "stackmemory-mcp.json");
const HOOKS_JSON = join(CLAUDE_DIR, "hooks.json");
function createSetupMCPCommand() {
return new Command("setup-mcp").description("Auto-configure Claude Code MCP integration").option("--dry-run", "Show what would be configured without making changes").option("--reset", "Reset MCP configuration to defaults").action(async (options) => {
console.log(chalk.cyan("\nStackMemory MCP Setup\n"));
if (options.dryRun) {
console.log(chalk.yellow("[DRY RUN] No changes will be made.\n"));
}
if (!existsSync(CLAUDE_DIR)) {
if (options.dryRun) {
console.log(chalk.gray(`Would create: ${CLAUDE_DIR}`));
} else {
mkdirSync(CLAUDE_DIR, { recursive: true });
console.log(chalk.green("[OK]") + " Created ~/.claude directory");
}
}
const mcpConfig = {
mcpServers: {
stackmemory: {
command: "stackmemory",
args: ["mcp-server"],
env: {
NODE_ENV: "production"
}
}
}
};
if (options.dryRun) {
console.log(
chalk.gray(`Would write MCP config to: ${MCP_CONFIG_FILE}`)
);
console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)));
} else {
writeFileSync(MCP_CONFIG_FILE, JSON.stringify(mcpConfig, null, 2));
console.log(chalk.green("[OK]") + " Created MCP server configuration");
}
let claudeConfig = {};
if (existsSync(CLAUDE_CONFIG_FILE)) {
try {
claudeConfig = JSON.parse(readFileSync(CLAUDE_CONFIG_FILE, "utf8"));
} catch {
console.log(
chalk.yellow("[WARN]") + " Could not parse existing config.json, creating new"
);
}
}
if (!claudeConfig.mcp) {
claudeConfig.mcp = {};
}
const mcp = claudeConfig.mcp;
if (!mcp.configFiles) {
mcp.configFiles = [];
}
const configFiles = mcp.configFiles;
if (!configFiles.includes(MCP_CONFIG_FILE)) {
configFiles.push(MCP_CONFIG_FILE);
}
if (options.dryRun) {
console.log(chalk.gray(`Would update: ${CLAUDE_CONFIG_FILE}`));
} else {
writeFileSync(
CLAUDE_CONFIG_FILE,
JSON.stringify(claudeConfig, null, 2)
);
console.log(chalk.green("[OK]") + " Updated Claude config.json");
}
console.log(chalk.cyan("\nValidating configuration..."));
try {
execSync("stackmemory --version", { stdio: "pipe" });
console.log(chalk.green("[OK]") + " stackmemory CLI is installed");
} catch {
console.log(chalk.yellow("[WARN]") + " stackmemory CLI not in PATH");
console.log(chalk.gray(" You may need to restart your terminal"));
}
try {
execSync("claude --version", { stdio: "pipe" });
console.log(chalk.green("[OK]") + " Claude Code is installed");
} catch {
console.log(chalk.yellow("[WARN]") + " Claude Code not found");
console.log(chalk.gray(" Install from: https://claude.ai/code"));
}
if (!options.dryRun) {
console.log(chalk.green("\nMCP setup complete!"));
console.log(chalk.cyan("\nNext steps:"));
console.log(chalk.white(" 1. Restart Claude Code"));
console.log(
chalk.white(
" 2. The StackMemory MCP tools will be available automatically"
)
);
console.log(
chalk.gray(
'\nTo verify: Run "stackmemory doctor" to check all integrations'
)
);
}
});
}
function createDoctorCommand() {
return new Command("doctor").description("Diagnose StackMemory configuration and common issues").option("--fix", "Attempt to automatically fix issues").action(async (options) => {
console.log(chalk.cyan("\nStackMemory Doctor\n"));
console.log(chalk.gray("Checking configuration and dependencies...\n"));
const results = [];
const projectDir = join(process.cwd(), ".stackmemory");
const dbPath = join(projectDir, "context.db");
if (existsSync(dbPath)) {
results.push({
name: "Project Initialization",
status: "ok",
message: "StackMemory is initialized in this project"
});
} else if (existsSync(projectDir)) {
results.push({
name: "Project Initialization",
status: "warn",
message: ".stackmemory directory exists but database not found",
fix: "Run: stackmemory init"
});
} else {
results.push({
name: "Project Initialization",
status: "error",
message: "StackMemory not initialized in this project",
fix: "Run: stackmemory init"
});
}
if (existsSync(dbPath)) {
try {
const Database = (await import("better-sqlite3")).default;
const db = new Database(dbPath, { readonly: true });
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
db.close();
const hasFrames = tables.some((t) => t.name === "frames");
if (hasFrames) {
results.push({
name: "Database Integrity",
status: "ok",
message: `Database has ${tables.length} tables`
});
} else {
results.push({
name: "Database Integrity",
status: "warn",
message: "Database exists but missing expected tables",
fix: "Run: stackmemory init --interactive"
});
}
} catch (error) {
results.push({
name: "Database Integrity",
status: "error",
message: `Database error: ${error.message}`,
fix: "Remove .stackmemory/context.db and run: stackmemory init"
});
}
}
if (existsSync(MCP_CONFIG_FILE)) {
try {
const config = JSON.parse(readFileSync(MCP_CONFIG_FILE, "utf8"));
if (config.mcpServers?.stackmemory) {
results.push({
name: "MCP Configuration",
status: "ok",
message: "MCP server configured"
});
} else {
results.push({
name: "MCP Configuration",
status: "warn",
message: "MCP config file exists but stackmemory server not configured",
fix: "Run: stackmemory setup-mcp"
});
}
} catch {
results.push({
name: "MCP Configuration",
status: "error",
message: "Invalid MCP configuration file",
fix: "Run: stackmemory setup-mcp --reset"
});
}
} else {
results.push({
name: "MCP Configuration",
status: "warn",
message: "MCP not configured for Claude Code",
fix: "Run: stackmemory setup-mcp"
});
}
if (existsSync(HOOKS_JSON)) {
try {
const hooks = JSON.parse(readFileSync(HOOKS_JSON, "utf8"));
const hasTraceHook = !!hooks["tool-use-approval"];
if (hasTraceHook) {
results.push({
name: "Claude Hooks",
status: "ok",
message: "Tool tracing hook installed"
});
} else {
results.push({
name: "Claude Hooks",
status: "warn",
message: "Hooks file exists but tracing not configured",
fix: "Run: stackmemory hooks install"
});
}
} catch {
results.push({
name: "Claude Hooks",
status: "warn",
message: "Could not read hooks.json"
});
}
} else {
results.push({
name: "Claude Hooks",
status: "warn",
message: "Claude hooks not installed (optional)",
fix: "Run: stackmemory hooks install"
});
}
const envChecks = [
{ key: "LINEAR_API_KEY", name: "Linear API Key", optional: true },
{ key: "TWILIO_ACCOUNT_SID", name: "Twilio Account", optional: true }
];
for (const check of envChecks) {
const value = process.env[check.key];
if (value) {
results.push({
name: check.name,
status: "ok",
message: "Environment variable set"
});
} else if (!check.optional) {
results.push({
name: check.name,
status: "error",
message: "Required environment variable not set",
fix: `Set ${check.key} in your .env file`
});
}
}
const homeStackmemory = join(homedir(), ".stackmemory");
if (existsSync(homeStackmemory)) {
try {
const testFile = join(homeStackmemory, ".write-test");
writeFileSync(testFile, "test");
const { unlinkSync } = await import("fs");
unlinkSync(testFile);
results.push({
name: "File Permissions",
status: "ok",
message: "~/.stackmemory is writable"
});
} catch {
results.push({
name: "File Permissions",
status: "error",
message: "~/.stackmemory is not writable",
fix: "Run: chmod 700 ~/.stackmemory"
});
}
}
let hasErrors = false;
let hasWarnings = false;
for (const result of results) {
const icon = result.status === "ok" ? chalk.green("[OK]") : result.status === "warn" ? chalk.yellow("[WARN]") : chalk.red("[ERROR]");
console.log(`${icon} ${result.name}`);
console.log(chalk.gray(` ${result.message}`));
if (result.fix) {
console.log(chalk.cyan(` Fix: ${result.fix}`));
if (options.fix && result.status !== "ok") {
if (result.fix.includes("stackmemory setup-mcp")) {
console.log(chalk.gray(" Attempting auto-fix..."));
try {
execSync("stackmemory setup-mcp", { stdio: "inherit" });
} catch {
console.log(chalk.red(" Auto-fix failed"));
}
}
}
}
if (result.status === "error") hasErrors = true;
if (result.status === "warn") hasWarnings = true;
}
console.log("");
if (hasErrors) {
console.log(
chalk.red("Some issues need attention. Run suggested fixes above.")
);
process.exit(1);
} else if (hasWarnings) {
console.log(
chalk.yellow(
"StackMemory is working but some optional features are not configured."
)
);
} else {
console.log(
chalk.green("All checks passed! StackMemory is properly configured.")
);
}
});
}
function createSetupPluginsCommand() {
const cmd = new Command("setup-plugins");
cmd.description("Install StackMemory plugins for Claude Code").option("--force", "Overwrite existing plugins").action(async (options) => {
console.log(
chalk.cyan("Installing StackMemory plugins for Claude Code...\n")
);
const pluginsDir = join(CLAUDE_DIR, "plugins");
if (!existsSync(pluginsDir)) {
mkdirSync(pluginsDir, { recursive: true });
console.log(chalk.gray(`Created: ${pluginsDir}`));
}
const possiblePaths = [
join(process.cwd(), "plugins"),
join(__dirname, "..", "..", "..", "plugins"),
join(homedir(), ".stackmemory", "plugins")
];
try {
const globalRoot = execSync("npm root -g", {
encoding: "utf-8"
}).trim();
possiblePaths.push(
join(globalRoot, "@stackmemoryai", "stackmemory", "plugins")
);
} catch {
}
let sourcePluginsDir;
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(join(p, "stackmemory"))) {
sourcePluginsDir = p;
break;
}
}
if (!sourcePluginsDir) {
console.log(chalk.red("Could not find StackMemory plugins directory"));
console.log(chalk.gray("Searched:"));
possiblePaths.forEach((p) => console.log(chalk.gray(` - ${p}`)));
process.exit(1);
}
console.log(chalk.gray(`Source: ${sourcePluginsDir}
`));
const plugins = ["stackmemory", "ralph-wiggum"];
let installed = 0;
for (const plugin of plugins) {
const sourcePath = join(sourcePluginsDir, plugin);
const targetPath = join(pluginsDir, plugin);
if (!existsSync(sourcePath)) {
console.log(chalk.yellow(` [SKIP] ${plugin} - not found in source`));
continue;
}
if (existsSync(targetPath)) {
if (options.force) {
try {
execSync(`rm -rf "${targetPath}"`, { encoding: "utf-8" });
} catch {
console.log(
chalk.red(` [ERROR] ${plugin} - could not remove existing`)
);
continue;
}
} else {
console.log(
chalk.gray(` [EXISTS] ${plugin} - use --force to overwrite`)
);
continue;
}
}
try {
execSync(`ln -s "${sourcePath}" "${targetPath}"`, {
encoding: "utf-8"
});
console.log(chalk.green(` [OK] ${plugin}`));
installed++;
} catch (err) {
console.log(
chalk.red(` [ERROR] ${plugin} - ${err.message}`)
);
}
}
console.log("");
if (installed > 0) {
console.log(chalk.green(`Installed ${installed} plugin(s)`));
console.log(chalk.gray("\nAvailable commands in Claude Code:"));
console.log(
chalk.white(" /sm-status ") + chalk.gray("Show StackMemory status")
);
console.log(
chalk.white(" /sm-capture ") + chalk.gray("Capture work for handoff")
);
console.log(
chalk.white(" /sm-restore ") + chalk.gray("Restore from last handoff")
);
console.log(
chalk.white(" /sm-decision ") + chalk.gray("Record a decision")
);
console.log(
chalk.white(" /sm-help ") + chalk.gray("Show all commands")
);
console.log(
chalk.white(" /ralph-loop ") + chalk.gray("Start Ralph iteration loop")
);
} else {
console.log(chalk.yellow("No plugins installed"));
}
});
return cmd;
}
function createSetupRemoteCommand() {
const cmd = new Command("setup-remote");
cmd.description("Configure remote MCP server to auto-start on boot").option("--port <number>", "Port for remote server", "3847").option("--project <path>", "Project root directory").option("--uninstall", "Remove the auto-start service").option("--status", "Check service status").action(async (options) => {
const home = homedir();
const platform = process.platform;
const serviceName = platform === "darwin" ? "com.stackmemory.remote-mcp" : "stackmemory-remote-mcp";
const serviceDir = platform === "darwin" ? join(home, "Library", "LaunchAgents") : join(home, ".config", "systemd", "user");
const serviceFile = platform === "darwin" ? join(serviceDir, `${serviceName}.plist`) : join(serviceDir, `${serviceName}.service`);
const logDir = join(home, ".stackmemory", "logs");
const pidFile = join(home, ".stackmemory", "remote-mcp.pid");
if (options.status) {
console.log(chalk.cyan("\nRemote MCP Server Status\n"));
if (platform === "darwin") {
try {
const result = execSync(
`launchctl list | grep ${serviceName} || true`,
{ encoding: "utf-8" }
);
if (result.includes(serviceName)) {
console.log(chalk.green("[RUNNING]") + " Service is active");
try {
const health = execSync(
`curl -s http://localhost:${options.port}/health 2>/dev/null`,
{ encoding: "utf-8" }
);
const data = JSON.parse(health);
console.log(chalk.gray(` Project: ${data.projectId}`));
console.log(
chalk.gray(` URL: http://localhost:${options.port}/sse`)
);
} catch {
console.log(
chalk.yellow(" Server not responding to health check")
);
}
} else {
console.log(chalk.yellow("[STOPPED]") + " Service not running");
}
} catch {
console.log(chalk.yellow("[UNKNOWN]") + " Could not check status");
}
} else if (platform === "linux") {
try {
execSync(`systemctl --user is-active ${serviceName}`, {
stdio: "pipe"
});
console.log(chalk.green("[RUNNING]") + " Service is active");
} catch {
console.log(chalk.yellow("[STOPPED]") + " Service not running");
}
}
console.log(chalk.gray(`
Service file: ${serviceFile}`));
console.log(chalk.gray(`Logs: ${logDir}/remote-mcp.log`));
return;
}
if (options.uninstall) {
console.log(chalk.cyan("\nUninstalling Remote MCP Server Service\n"));
if (platform === "darwin") {
try {
execSync(`launchctl unload "${serviceFile}"`, { stdio: "pipe" });
console.log(chalk.green("[OK]") + " Service unloaded");
} catch {
console.log(chalk.gray("[SKIP]") + " Service was not loaded");
}
if (existsSync(serviceFile)) {
const fs = await import("fs/promises");
await fs.unlink(serviceFile);
console.log(chalk.green("[OK]") + " Service file removed");
}
} else if (platform === "linux") {
try {
execSync(`systemctl --user stop ${serviceName}`, { stdio: "pipe" });
execSync(`systemctl --user disable ${serviceName}`, {
stdio: "pipe"
});
console.log(chalk.green("[OK]") + " Service stopped and disabled");
} catch {
console.log(chalk.gray("[SKIP]") + " Service was not running");
}
if (existsSync(serviceFile)) {
const fs = await import("fs/promises");
await fs.unlink(serviceFile);
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
console.log(chalk.green("[OK]") + " Service file removed");
}
}
console.log(chalk.green("\nRemote MCP service uninstalled"));
return;
}
console.log(chalk.cyan("\nSetting up Remote MCP Server Auto-Start\n"));
if (platform !== "darwin" && platform !== "linux") {
console.log(
chalk.red("Auto-start is only supported on macOS and Linux")
);
console.log(chalk.gray("\nManual start: stackmemory mcp-remote"));
return;
}
if (!existsSync(serviceDir)) {
mkdirSync(serviceDir, { recursive: true });
}
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
let nodePath;
try {
nodePath = execSync("which node", { encoding: "utf-8" }).trim();
} catch {
nodePath = "/usr/local/bin/node";
}
let stackmemoryPath;
try {
stackmemoryPath = execSync("which stackmemory", {
encoding: "utf-8"
}).trim();
} catch {
try {
const npmRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
stackmemoryPath = join(
npmRoot,
"@stackmemoryai",
"stackmemory",
"dist",
"cli",
"index.js"
);
} catch {
stackmemoryPath = "npx stackmemory";
}
}
const projectPath = options.project || home;
const port = options.port || "3847";
if (platform === "darwin") {
const plist = `xml version="1.0" encoding="UTF-8"
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${serviceName}</string>
<key>ProgramArguments</key>
<array>
<string>${stackmemoryPath.includes("npx") ? "npx" : nodePath}</string>
${stackmemoryPath.includes("npx") ? "<string>stackmemory</string>" : `<string>${stackmemoryPath}</string>`}
<string>mcp-remote</string>
<string>--port</string>
<string>${port}</string>
<string>--project</string>
<string>${projectPath}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>WorkingDirectory</key>
<string>${projectPath}</string>
<key>StandardOutPath</key>
<string>${logDir}/remote-mcp.log</string>
<key>StandardErrorPath</key>
<string>${logDir}/remote-mcp.error.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>${home}</string>
<key>PATH</key>
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
<key>NODE_ENV</key>
<string>production</string>
</dict>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>`;
writeFileSync(serviceFile, plist);
console.log(chalk.green("[OK]") + " Created launchd service file");
try {
execSync(`launchctl unload "${serviceFile}" 2>/dev/null`, {
stdio: "pipe"
});
} catch {
}
try {
execSync(`launchctl load -w "${serviceFile}"`, { stdio: "pipe" });
console.log(chalk.green("[OK]") + " Service loaded and started");
} catch (err) {
console.log(chalk.red("[ERROR]") + ` Failed to load service: ${err}`);
return;
}
} else if (platform === "linux") {
const service = `[Unit]
Description=StackMemory Remote MCP Server
Documentation=https://github.com/stackmemoryai/stackmemory
After=network.target
[Service]
Type=simple
ExecStart=${stackmemoryPath.includes("npx") ? "npx stackmemory" : `${nodePath} ${stackmemoryPath}`} mcp-remote --port ${port} --project ${projectPath}
Restart=on-failure
RestartSec=10
WorkingDirectory=${projectPath}
Environment=HOME=${home}
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=NODE_ENV=production
StandardOutput=append:${logDir}/remote-mcp.log
StandardError=append:${logDir}/remote-mcp.error.log
[Install]
WantedBy=default.target`;
writeFileSync(serviceFile, service);
console.log(chalk.green("[OK]") + " Created systemd service file");
try {
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
execSync(`systemctl --user enable ${serviceName}`, { stdio: "pipe" });
execSync(`systemctl --user start ${serviceName}`, { stdio: "pipe" });
console.log(chalk.green("[OK]") + " Service enabled and started");
} catch (err) {
console.log(
chalk.red("[ERROR]") + ` Failed to start service: ${err}`
);
return;
}
}
console.log(chalk.cyan("\nVerifying server...\n"));
await new Promise((resolve) => setTimeout(resolve, 2e3));
try {
const health = execSync(
`curl -s http://localhost:${port}/health 2>/dev/null`,
{ encoding: "utf-8" }
);
const data = JSON.parse(health);
console.log(chalk.green("[OK]") + " Server is running");
console.log(chalk.gray(` Project: ${data.projectId}`));
} catch {
console.log(
chalk.yellow("[WARN]") + " Server not responding yet (may still be starting)"
);
}
console.log(
chalk.green("\nRemote MCP Server configured for auto-start!")
);
console.log(chalk.cyan("\nConnection info:"));
console.log(chalk.white(` URL: http://localhost:${port}/sse`));
console.log(chalk.white(` Health: http://localhost:${port}/health`));
console.log(chalk.gray(`
Logs: ${logDir}/remote-mcp.log`));
console.log(chalk.gray(`Service: ${serviceFile}`));
console.log(chalk.cyan("\nFor external access (ngrok):"));
console.log(chalk.white(` ngrok http ${port}`));
console.log(chalk.gray(" Then use the ngrok URL + /sse in Claude.ai"));
});
return cmd;
}
function registerSetupCommands(program) {
program.addCommand(createSetupMCPCommand());
program.addCommand(createDoctorCommand());
program.addCommand(createSetupPluginsCommand());
program.addCommand(createSetupRemoteCommand());
}
export {
createDoctorCommand,
createSetupMCPCommand,
createSetupPluginsCommand,
createSetupRemoteCommand,
registerSetupCommands
};
//# sourceMappingURL=setup.js.map