UNPKG

@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.

952 lines (951 loc) 36.8 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); process.env["STACKMEMORY_CLI"] = "true"; import { config as loadDotenv } from "dotenv"; loadDotenv({ quiet: true }); import { initializeTracing, trace } from "../core/trace/index.js"; initializeTracing(); import { program } from "commander"; import { logger } from "../core/monitoring/logger.js"; import { FrameManager } from "../core/context/index.js"; import { sessionManager, FrameQueryMode } from "../core/session/index.js"; import { sharedContextLayer } from "../core/context/shared-context-layer.js"; import { UpdateChecker } from "../core/utils/update-checker.js"; import { ProgressTracker } from "../core/monitoring/progress-tracker.js"; import { registerProjectCommands } from "./commands/projects.js"; import { createSessionCommands } from "./commands/session.js"; import { isFeatureEnabled, isLocalOnly } from "../core/config/feature-flags.js"; import { registerWorktreeCommands } from "./commands/worktree.js"; import { registerOnboardingCommand } from "./commands/onboard.js"; import { createTaskCommands } from "./commands/tasks.js"; import { createSearchCommand } from "./commands/search.js"; import { createLogCommand } from "./commands/log.js"; import { createContextCommands } from "./commands/context.js"; import { createConfigCommand } from "./commands/config.js"; import { createCaptureCommand, createRestoreCommand, createAutoCaptureCommand } from "./commands/handoff.js"; import { createDecisionCommand, createMemoryCommand } from "./commands/decision.js"; import clearCommand from "./commands/clear.js"; import serviceCommand from "./commands/service.js"; import { registerLoginCommand } from "./commands/login.js"; import { registerSignupCommand } from "./commands/signup.js"; import { registerLogoutCommand, registerDbCommands } from "./commands/db.js"; import { createHooksCommand } from "./commands/hooks.js"; import { createDaemonCommand } from "./commands/daemon.js"; import { createSweepCommand } from "./commands/sweep.js"; import { createShellCommand } from "./commands/shell.js"; import { createAPICommand } from "./commands/api.js"; import { createCleanupProcessesCommand } from "./commands/cleanup-processes.js"; import { createAutoBackgroundCommand } from "./commands/auto-background.js"; import { createSettingsCommand } from "./commands/settings.js"; import { createRetrievalCommands } from "./commands/retrieval.js"; import { createDiscoveryCommands } from "./commands/discovery.js"; import { createModelCommand } from "./commands/model.js"; import { registerSetupCommands } from "./commands/setup.js"; import { createPingCommand } from "./commands/ping.js"; import chalk from "chalk"; import * as fs from "fs"; import * as path from "path"; import { filterPending } from "../integrations/mcp/pending-utils.js"; import { ProjectManager } from "../core/projects/project-manager.js"; import { join } from "path"; import { existsSync, mkdirSync } from "fs"; import inquirer from "inquirer"; import { enableChromaDB } from "../core/config/storage-config.js"; import { spawn } from "child_process"; import { homedir } from "os"; import { createRequire } from "module"; import { fileURLToPath } from "url"; import * as pathModule from "path"; const localRequire = createRequire(import.meta.url); const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = pathModule.dirname(currentFilePath); function findPackageJson() { let dir = currentDirPath; for (let i = 0; i < 5; i++) { const pkgPath = pathModule.join(dir, "package.json"); try { return localRequire(pkgPath); } catch { dir = pathModule.dirname(dir); } } return { version: "0.0.0" }; } const VERSION = findPackageJson().version; async function openDatabase(dbPath) { const { default: Database } = await import("better-sqlite3"); return new Database(dbPath); } function isTestEnv() { return process.env["VITEST"] === "true" || process.env["NODE_ENV"] === "test" || process.env["STACKMEMORY_TEST_SKIP_DB"] === "1"; } UpdateChecker.checkForUpdates(VERSION, true).catch(() => { }); async function startNotificationServices() { if (isLocalOnly() || !isFeatureEnabled("whatsapp")) return; try { const { loadSMSConfig } = await import("../hooks/sms-notify.js"); const config = loadSMSConfig(); if (!config.enabled) return; const WEBHOOK_PORT = 3456; let webhookStarted = false; let ngrokStarted = false; const webhookRunning = await fetch( `http://localhost:${WEBHOOK_PORT}/health` ).then((r) => r.ok).catch(() => false); if (!webhookRunning) { const webhookPath = join(__dirname, "../hooks/sms-webhook.js"); const webhookProcess = spawn("node", [webhookPath], { detached: true, stdio: "ignore", env: { ...process.env, SMS_WEBHOOK_PORT: String(WEBHOOK_PORT) } }); webhookProcess.unref(); webhookStarted = true; } const ngrokRunning = await fetch("http://localhost:4040/api/tunnels").then((r) => r.ok).catch(() => false); if (!ngrokRunning) { const ngrokProcess = spawn("ngrok", ["http", String(WEBHOOK_PORT)], { detached: true, stdio: "ignore" }); ngrokProcess.unref(); ngrokStarted = true; } if (webhookStarted || ngrokStarted) { setTimeout(async () => { try { const tunnels = await fetch("http://localhost:4040/api/tunnels").then( (r) => r.json() ); const publicUrl = tunnels?.tunnels?.[0]?.public_url; if (publicUrl) { const configDir = join(homedir(), ".stackmemory"); const configPath = join(configDir, "ngrok-url.txt"); const { writeFileSync, mkdirSync: mkdirSync2, existsSync: existsSync2 } = await import("fs"); if (!existsSync2(configDir)) { mkdirSync2(configDir, { recursive: true }); } writeFileSync(configPath, publicUrl); console.log( chalk.gray(`[notify] Webhook: ${publicUrl}/sms/incoming`) ); } } catch { } }, 4e3); } } catch { } } startNotificationServices(); program.name("stackmemory").description( "Lossless memory runtime for AI coding tools - organizes context as a call stack instead of linear chat logs, with team collaboration and infinite retention" ).version(VERSION); program.command("init").description( "Initialize StackMemory in current project (zero-config by default)" ).option("-i, --interactive", "Interactive mode with configuration prompts").option( "--chromadb", "Enable ChromaDB for semantic search (prompts for API key)" ).option("--daemon", "Start the background daemon after initialization").action(async (options) => { try { const projectRoot = process.cwd(); const dbDir = join(projectRoot, ".stackmemory"); const alreadyInit = existsSync(join(dbDir, "context.db")); if (alreadyInit && !options.interactive) { console.log(chalk.yellow("StackMemory already initialized.")); console.log(chalk.gray("Run with --interactive to reconfigure.")); return; } if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } if (options.chromadb) { await promptAndEnableChromaDB(); } else if (options.interactive && process.stdin.isTTY) { console.log(chalk.cyan("\nStorage Configuration")); console.log( chalk.gray("SQLite (default) is fast and requires no setup.") ); console.log( chalk.gray("ChromaDB adds semantic search but requires an API key.\n") ); const { enableChroma } = await inquirer.prompt([ { type: "confirm", name: "enableChroma", message: "Enable ChromaDB for semantic search?", default: false } ]); if (enableChroma) { await promptAndEnableChromaDB(); } } const dbPath = join(dbDir, "context.db"); if (!isTestEnv()) { const db = await openDatabase(dbPath); new FrameManager(db, "cli-project"); db.close(); } logger.info("StackMemory initialized successfully", { projectRoot }); console.log(chalk.green("\n[OK] StackMemory initialized")); console.log(chalk.gray(` Project: ${projectRoot}`)); console.log(chalk.gray(` Storage: SQLite (local)`)); if (options.daemon) { console.log(chalk.cyan("\nInstalling background service...")); try { const { installServiceSilent } = await import("./commands/service.js"); const success = await installServiceSilent(); if (success) { console.log(chalk.green("[OK] Guardian service installed")); console.log(chalk.gray(" Auto-starts on login")); console.log( chalk.gray(" Check status: stackmemory service status") ); } else { console.log(chalk.yellow("[WARN] Could not install service")); console.log(chalk.gray(" Run: stackmemory service install")); } } catch { console.log(chalk.yellow("[WARN] Could not install service")); console.log(chalk.gray(" Run: stackmemory service install")); } } console.log(chalk.cyan("\nNext steps:")); console.log( chalk.white(" 1. stackmemory setup-mcp") + chalk.gray(" # Configure Claude Code integration") ); console.log( chalk.white(" 2. stackmemory status") + chalk.gray(" # Check status") ); console.log( chalk.white(" 3. stackmemory doctor") + chalk.gray(" # Diagnose issues") ); } catch (error) { logger.error("Failed to initialize StackMemory", error); console.error(chalk.red("\n[ERROR] Initialization failed")); console.error(chalk.gray(` Reason: ${error.message}`)); console.error( chalk.gray( " Fix: Ensure you have write permissions to the current directory" ) ); console.error(chalk.gray(" Run: stackmemory doctor")); process.exit(1); } }); async function promptAndEnableChromaDB() { const answers = await inquirer.prompt([ { type: "password", name: "apiKey", message: "Enter your ChromaDB API key:", validate: (input) => { if (!input || input.trim().length === 0) { return "API key is required for ChromaDB"; } return true; } }, { type: "input", name: "apiUrl", message: "ChromaDB API URL (press Enter for default):", default: "https://api.trychroma.com" } ]); enableChromaDB({ apiKey: answers.apiKey, apiUrl: answers.apiUrl }); console.log(chalk.green("[OK] ChromaDB enabled for semantic search.")); console.log( chalk.gray("API key saved to ~/.stackmemory/storage-config.json") ); } program.command("status").description("Show current StackMemory status").option("--all", "Show all active frames across sessions").option("--project", "Show all active frames in current project").option("--session <id>", "Show frames for specific session").action(async (options) => { return trace.command("stackmemory-status", options, async () => { try { const projectRoot = process.cwd(); const dbPath = join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.log( '\u274C StackMemory not initialized. Run "stackmemory init" first.' ); return; } if (isTestEnv()) { console.log("\u{1F4CA} StackMemory Status (test mode):"); console.log(" Frames: n/a"); console.log(" Events: n/a"); console.log(" Sessions: n/a"); return; } await UpdateChecker.checkForUpdates(VERSION); await sessionManager.initialize(); await sharedContextLayer.initialize(); const session = await sessionManager.getOrCreateSession({ projectPath: projectRoot, sessionId: options.session }); const contextDiscovery = await sharedContextLayer.autoDiscoverContext(); if (contextDiscovery.hasSharedContext && contextDiscovery.sessionCount > 1) { console.log(` \u{1F4A1} Shared Context Available:`); console.log( ` ${contextDiscovery.sessionCount} sessions with shared context` ); if (contextDiscovery.recentPatterns.length > 0) { console.log(` Recent patterns:`); contextDiscovery.recentPatterns.slice(0, 3).forEach((p) => { console.log( ` \u2022 ${p.type}: ${p.pattern.slice(0, 50)} (${p.frequency}x)` ); }); } if (contextDiscovery.lastDecisions.length > 0) { console.log( ` Last decision: ${contextDiscovery.lastDecisions[0].decision.slice(0, 60)}` ); } } const db = await openDatabase(dbPath); const frameManager = new FrameManager(db, session.projectId); if (options.all) { frameManager.setQueryMode(FrameQueryMode.ALL_ACTIVE); } else if (options.project) { frameManager.setQueryMode(FrameQueryMode.PROJECT_ACTIVE); } const activeFrames = frameManager.getActiveFramePath(); const stackDepth = frameManager.getStackDepth(); const totalStats = db.prepare( ` SELECT COUNT(*) as total_frames, SUM(CASE WHEN state = 'active' THEN 1 ELSE 0 END) as active_frames, SUM(CASE WHEN state = 'closed' THEN 1 ELSE 0 END) as closed_frames, COUNT(DISTINCT run_id) as total_sessions FROM frames WHERE project_id = ? ` ).get(session.projectId); const contextCount = db.prepare( ` SELECT COUNT(*) as count FROM contexts ` ).get(); const eventCount = db.prepare( ` SELECT COUNT(*) as count FROM events e JOIN frames f ON e.frame_id = f.frame_id WHERE f.project_id = ? ` ).get(session.projectId); console.log("\u{1F4CA} StackMemory Status:"); console.log( ` Session: ${session.sessionId.slice(0, 8)} (${session.state}, ${Math.round((Date.now() - session.startedAt) / 1e3 / 60)}min old)` ); console.log(` Project: ${session.projectId}`); if (session.branch) { console.log(` Branch: ${session.branch}`); } console.log(` Database Statistics (this project):`); console.log( ` Frames: ${totalStats.total_frames || 0} (${totalStats.active_frames || 0} active, ${totalStats.closed_frames || 0} closed)` ); console.log(` Events: ${eventCount.count || 0}`); console.log(` Sessions: ${totalStats.total_sessions || 0}`); console.log( ` Cached contexts: ${contextCount.count || 0} (global)` ); const recentFrames = db.prepare( ` SELECT name, type, state, datetime(created_at, 'unixepoch') as created FROM frames WHERE project_id = ? ORDER BY created_at DESC LIMIT 3 ` ).all(session.projectId); if (recentFrames.length > 0) { console.log(` Recent Activity:`); recentFrames.forEach((f) => { const stateIcon = f.state === "active" ? "\u{1F7E2}" : "\u26AB"; console.log( ` ${stateIcon} ${f.name} [${f.type}] - ${f.created}` ); }); } console.log(` Current Session:`); console.log(` Stack depth: ${stackDepth}`); console.log(` Active frames: ${activeFrames.length}`); if (activeFrames.length > 0) { activeFrames.forEach((frame, i) => { const indent = " " + " ".repeat(frame.depth || i); const prefix = i === 0 ? "\u2514\u2500" : " \u2514\u2500"; console.log(`${indent}${prefix} ${frame.name} [${frame.type}]`); }); } if (!options.all && !options.project) { const otherSessions = await sessionManager.listSessions({ projectId: session.projectId, state: "active" }); const otherActive = otherSessions.filter( (s) => s.sessionId !== session.sessionId ); if (otherActive.length > 0) { console.log(` Other Active Sessions (same project):`); otherActive.forEach((s) => { const age = Math.round( (Date.now() - s.lastActiveAt) / 1e3 / 60 / 60 ); console.log( ` - ${s.sessionId.slice(0, 8)}: ${s.branch || "main"}, ${age}h old` ); }); console.log(` Tip: Use --all to see frames across sessions`); } } db.close(); } catch (error) { logger.error("Failed to get status", error); console.error("\u274C Status check failed:", error.message); process.exit(1); } }); }); program.command("update-check").description("Check for StackMemory updates").action(async () => { try { console.log("\u{1F50D} Checking for updates..."); await UpdateChecker.forceCheck(VERSION); } catch (error) { logger.error("Update check failed", error); console.error("\u274C Update check failed:", error.message); process.exit(1); } }); program.command("progress").description("Show current progress and recent changes").action(async () => { try { const projectRoot = process.cwd(); const dbPath = join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.log( '\u274C StackMemory not initialized. Run "stackmemory init" first.' ); return; } const progress = new ProgressTracker(projectRoot); console.log(progress.getSummary()); } catch (error) { logger.error("Failed to show progress", error); console.error("\u274C Failed to show progress:", error.message); process.exit(1); } }); program.command("mcp-server").description("Start StackMemory MCP server for Claude Desktop").option("-p, --project <path>", "Project root directory", process.cwd()).action(async (options) => { try { const { runMCPServer } = await import("../integrations/mcp/server.js"); process.env["PROJECT_ROOT"] = options.project; console.log("\u{1F680} Starting StackMemory MCP Server..."); console.log(` Project: ${options.project}`); console.log(` Version: ${VERSION}`); UpdateChecker.checkForUpdates(VERSION, true).catch(() => { }); await runMCPServer(); } catch (error) { logger.error("Failed to start MCP server", error); console.error("\u274C MCP server failed:", error.message); process.exit(1); } }); program.command("mcp-remote").description( "Start StackMemory Remote MCP server (HTTP/SSE) for Claude.ai web" ).option("-p, --port <number>", "Port to listen on", "3847").option("-d, --project <path>", "Project root directory", process.cwd()).action(async (options) => { try { const { runRemoteMCPServer } = await import("../integrations/mcp/remote-server.js"); const port = parseInt(options.port, 10); console.log("Starting StackMemory Remote MCP Server..."); console.log(` Project: ${options.project}`); console.log(` Version: ${VERSION}`); console.log(""); await runRemoteMCPServer(port, options.project); console.log(""); console.log("For Claude.ai web connector:"); console.log(` URL: http://localhost:${port}/sse`); console.log(""); console.log("For external access (ngrok):"); console.log(` ngrok http ${port}`); console.log(" Then use the ngrok URL + /sse in Claude.ai"); } catch (error) { logger.error("Failed to start remote MCP server", error); console.error("Remote MCP server failed:", error.message); process.exit(1); } }); program.command("context:test").description("Test context persistence by creating sample frames").action(async () => { try { const projectRoot = process.cwd(); const dbPath = join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.log( '\u274C StackMemory not initialized. Run "stackmemory init" first.' ); return; } if (isTestEnv()) { console.log("\u{1F4DD} [test] Skipping DB write in context:test"); return; } const db = await openDatabase(dbPath); const frameManager = new FrameManager(db, "cli-project"); console.log("\u{1F4DD} Creating test context frames..."); const rootFrame = frameManager.createFrame({ type: "task", name: "Test Session", inputs: { test: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() } }); const taskFrame = frameManager.createFrame({ type: "subtask", name: "Sample Task", inputs: { description: "Testing context persistence" }, parentFrameId: rootFrame }); const commandFrame = frameManager.createFrame({ type: "tool_scope", name: "test-command", inputs: { args: ["--test"] }, parentFrameId: taskFrame }); frameManager.addEvent( "observation", { message: "Test event recorded" }, commandFrame ); console.log("\u2705 Test frames created!"); console.log(`\u{1F4CA} Stack depth: ${frameManager.getStackDepth()}`); console.log( `\u{1F504} Active frames: ${frameManager.getActiveFramePath().length}` ); frameManager.closeFrame(commandFrame); console.log( `\u{1F4CA} After closing command frame: depth = ${frameManager.getStackDepth()}` ); db.close(); } catch (error) { logger.error("Test context failed", error); console.error("\u274C Test failed:", error.message); process.exit(1); } }); registerOnboardingCommand(program); registerSignupCommand(program); registerLoginCommand(program); registerLogoutCommand(program); registerDbCommands(program); registerProjectCommands(program); registerWorktreeCommands(program); if (isFeatureEnabled("linear")) { import("./commands/linear.js").then(({ registerLinearCommands }) => registerLinearCommands(program)).catch(() => { }); } program.addCommand(createSessionCommands()); program.addCommand(createTaskCommands()); program.addCommand(createSearchCommand()); program.addCommand(createLogCommand()); program.addCommand(createContextCommands()); program.addCommand(createConfigCommand()); program.addCommand(createCaptureCommand()); program.addCommand(createRestoreCommand()); program.addCommand(createAutoCaptureCommand()); program.addCommand(createDecisionCommand()); program.addCommand(createMemoryCommand()); program.addCommand(clearCommand); program.addCommand(serviceCommand); program.addCommand(createHooksCommand()); const lazyCommands = []; if (isFeatureEnabled("skills")) { lazyCommands.push( import("./commands/skills.js").then( ({ createSkillsCommand }) => program.addCommand(createSkillsCommand()) ).catch(() => { }) ); } if (isFeatureEnabled("ralph")) { lazyCommands.push( import("./commands/ralph.js").then( ({ default: createRalphCommand }) => program.addCommand(createRalphCommand()) ).catch(() => { }) ); } program.addCommand(createDaemonCommand()); program.addCommand(createSweepCommand()); program.addCommand(createShellCommand()); program.addCommand(createAPICommand()); program.addCommand(createCleanupProcessesCommand()); program.addCommand(createAutoBackgroundCommand()); program.addCommand(createSettingsCommand()); program.addCommand(createPingCommand()); if (isFeatureEnabled("whatsapp")) { lazyCommands.push( import("./commands/sms-notify.js").then( ({ createSMSNotifyCommand }) => program.addCommand(createSMSNotifyCommand()) ).catch(() => { }) ); } program.addCommand(createRetrievalCommands()); program.addCommand(createDiscoveryCommands()); program.addCommand(createModelCommand()); registerSetupCommands(program); program.command("mm-spike").description( "Run multi-agent planning/implementation spike (planner/implementer/critic)" ).option( "-t, --task <desc>", "Task description", "Add multi-agent spike harness" ).option( "--planner-model <name>", "Claude model for planning", "claude-sonnet-4-20250514" ).option( "--reviewer-model <name>", "Claude model for review", "claude-sonnet-4-20250514" ).option( "--execute", "Execute implementer (codex-sm) instead of dry-run", false ).option("--implementer <name>", "codex|claude", "codex").option("--max-iters <n>", "Retry loop iterations", "2").option("--audit-dir <path>", "Persist spike results to directory").option("--record-frame", "Record as real frame with anchors", false).option("--record", "Record plan & critique into StackMemory context", false).option("--json", "Emit single JSON result (UI-friendly)", false).option("--quiet", "Minimal output (default)", true).option("--verbose", "Verbose sectioned output", false).option( "--log", "Pretty print interaction log (planner/implementer/critic)", false ).action(async (opts) => { try { const { runSpike } = await import("../orchestrators/multimodal/harness.js"); const result = await runSpike( { task: opts.task, repoPath: process.cwd() }, { plannerModel: opts.plannerModel, reviewerModel: opts.reviewerModel, implementer: opts.implementer, maxIters: parseInt(opts.maxIters), dryRun: !opts.execute, auditDir: opts.auditDir, recordFrame: Boolean(opts.recordFrame), record: Boolean(opts.record) } ); if (opts.log) { printInteractionLog( { task: opts.task, plannerModel: opts.plannerModel, reviewerModel: opts.reviewerModel, implementer: opts.implementer, execute: Boolean(opts.execute) }, result ); return; } if (opts.json) { console.log(JSON.stringify(result)); return; } if (opts.verbose) { console.log("\n=== Plan ==="); console.log(JSON.stringify(result.plan, null, 2)); console.log("\n=== Iterations ==="); (result.iterations || []).forEach((it, i) => { console.log(` [Attempt ${i + 1}] ${it.command}`); console.log("OK:", it.ok); console.log("Critique:", JSON.stringify(it.critique)); }); console.log("\n=== Implementation ==="); console.log(JSON.stringify(result.implementation, null, 2)); console.log("\n=== Critique ==="); console.log(JSON.stringify(result.critique, null, 2)); } else if (!opts.quiet) { console.log( `Plan steps: ${result.plan.steps.length}, Approved: ${result.critique.approved}` ); } } catch (error) { console.error("mm-spike failed:", error.message); process.exit(1); } }); program.command("build").description( "Plan + code: planner (Claude), implementer (Codex/Claude), critic (Claude) with optional log/json output" ).argument("[task]", "Task description (positional)").option( "-t, --task <desc>", "Task description (required if no positional arg)" ).option( "--planner-model <name>", "Claude model for planning", "claude-sonnet-4-20250514" ).option( "--reviewer-model <name>", "Claude model for review", "claude-sonnet-4-20250514" ).option("--execute", "Execute implementer (default: true)", true).option("--dry-run", "Skip execution, show commands only").option("--implementer <name>", "codex|claude", "codex").option("--max-iters <n>", "Retry loop iterations", "2").option("--audit-dir <path>", "Persist spike results to directory").option("--record-frame", "Record as real frame with anchors").option("--record", "Record plan & critique into StackMemory context").option("--json", "Emit single JSON result (UI-friendly)").option("--quiet", "Minimal output").option("--verbose", "Verbose sectioned output").option("--log", "Pretty print interaction log (default: true)", true).option("-C, --cwd <path>", "Working directory for implementation").action(async (taskArg, opts) => { try { const task = typeof taskArg === "string" && taskArg.length > 0 ? taskArg : opts.task; if (!task) { console.error( chalk.red( "Error: Task description required. Provide as argument or --task option." ) ); console.error( chalk.gray(' Example: stackmemory build "Add user authentication"') ); process.exit(1); } const { runSpike } = await import("../orchestrators/multimodal/harness.js"); const dryRun = opts.dryRun === true || opts.execute === false; const findGitRoot = (startDir) => { let dir = startDir; while (dir !== "/") { if (existsSync(join(dir, ".git"))) { return dir; } dir = path.dirname(dir); } return startDir; }; const repoPath = opts.cwd ? path.resolve(opts.cwd) : findGitRoot(process.cwd()); const result = await runSpike( { task, repoPath }, { plannerModel: opts.plannerModel, reviewerModel: opts.reviewerModel, implementer: opts.implementer, maxIters: parseInt(opts.maxIters), dryRun, auditDir: opts.auditDir, recordFrame: Boolean(opts.recordFrame), record: Boolean(opts.record) } ); if (opts.log) { printInteractionLog( { task, plannerModel: opts.plannerModel, reviewerModel: opts.reviewerModel, implementer: opts.implementer, execute: !dryRun }, result ); return; } if (opts.json) { console.log(JSON.stringify(result)); return; } if (opts.verbose) { console.log("\n=== Plan ==="); console.log(JSON.stringify(result.plan, null, 2)); console.log("\n=== Iterations ==="); (result.iterations || []).forEach((it, i) => { console.log(` [Attempt ${i + 1}] ${it.command}`); console.log("OK:", it.ok); console.log("Critique:", JSON.stringify(it.critique)); }); console.log("\n=== Implementation ==="); console.log(JSON.stringify(result.implementation, null, 2)); console.log("\n=== Critique ==="); console.log(JSON.stringify(result.critique, null, 2)); } else if (!opts.quiet) { console.log( `Plan steps: ${result.plan.steps.length}, Approved: ${result.critique.approved}` ); } } catch (error) { console.error("build failed:", error.message); process.exit(1); } }); function printInteractionLog(meta, result) { const divider = chalk.gray( "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" ); console.log(chalk.cyan.bold("\nPlan & Code Session")); console.log(`${chalk.gray("Task:")} ${meta.task}`); console.log(`${chalk.gray("Planner:")} ${meta.plannerModel}`); console.log(`${chalk.gray("Reviewer:")} ${meta.reviewerModel}`); console.log( `${chalk.gray("Implementer:")} ${meta.implementer} ${meta.execute ? chalk.green("(execute)") : chalk.yellow("(dry-run)")}` ); console.log(divider); if (result.plan) { console.log( chalk.bold("Plan Summary: "), result.plan.summary || "(no summary)" ); const steps = result.plan.steps.slice(0, 6); if (steps.length) { console.log(chalk.bold("\nSteps:")); steps.forEach((s, idx) => { console.log(`${chalk.gray(String(idx + 1) + ".")} ${s.title || s.id}`); const ac = s.acceptanceCriteria || []; if (ac.length) { ac.slice(0, 3).forEach((c) => console.log(chalk.gray(` - ${c}`))); if (ac.length > 3) console.log(chalk.gray(" - ...")); } }); } if (result.plan.risks?.length) { console.log(chalk.bold("\nRisks:")); result.plan.risks.slice(0, 5).forEach((r) => console.log(chalk.gray(` - ${r}`))); } } console.log(` ${divider}`); const iters = result.iterations || []; if (iters.length) { iters.forEach((it, i) => { console.log(chalk.magenta(`Attempt ${i + 1}`)); console.log(`${chalk.gray("Command:")} ${it.command}`); console.log( `${chalk.gray("OK:")} ${it.ok ? chalk.green("true") : chalk.red("false")}` ); const issues = it.critique?.issues || []; const sugg = it.critique?.suggestions || []; if (issues.length) { console.log(chalk.bold("Issues:")); issues.slice(0, 5).forEach((x) => console.log(chalk.red(` - ${x}`))); } if (sugg.length) { console.log(chalk.bold("Suggestions:")); sugg.slice(0, 5).forEach((x) => console.log(chalk.yellow(` - ${x}`))); } console.log(divider); }); } const approved = result.critique?.approved ?? false; console.log( `${chalk.bold("Final:")} ${approved ? chalk.green("Approved") : chalk.yellow("Needs changes")}` ); console.log(""); } program.command("pending:list").description( "List pending approval-gated plans (from .stackmemory/build/pending.json)" ).option("--task-contains <substr>", "Filter tasks containing this substring").option("--older-than-ms <number>", "Only items older than this age (ms)").option("--newer-than-ms <number>", "Only items newer than this age (ms)").option("--sort <asc|desc>", "Sort by createdAt", "desc").option("--limit <number>", "Max items to return", "20").option("--pretty", "Pretty-print JSON output", false).action(async (opts) => { try { const storePath = path.join( process.cwd(), ".stackmemory", "build", "pending.json" ); let pending = {}; if (fs.existsSync(storePath)) { try { pending = JSON.parse(fs.readFileSync(storePath, "utf-8")); } catch { } } const items = Object.entries(pending).map(([approvalId, data]) => ({ approvalId, task: data?.task ?? "", createdAt: Number(data?.createdAt || 0) || null })); const filters = { taskContains: opts.taskContains, olderThanMs: opts.olderThanMs ? Number(opts.olderThanMs) : void 0, newerThanMs: opts.newerThanMs ? Number(opts.newerThanMs) : void 0, sort: opts.sort || void 0, limit: opts.limit ? Number(opts.limit) : void 0 }; const out = filterPending(items, filters); console.log( JSON.stringify({ ok: true, pending: out }, null, opts.pretty ? 2 : 0) ); } catch (error) { console.error( JSON.stringify({ ok: false, error: error.message }) ); process.exit(1); } }); program.command("plan").description("Generate an implementation plan (no code execution)").option("-t, --task <desc>", "Task description", "Plan a small change").option( "--planner-model <name>", "Claude model for planning", "claude-sonnet-4-20250514" ).option("--json", "Emit JSON (default)", true).option("--pretty", "Pretty-print JSON", false).option( "--compact", "Compact output (summary + step titles + criteria)", false ).action(async (opts) => { try { const { runPlanOnly } = await import("../orchestrators/multimodal/harness.js"); const plan = await runPlanOnly( { task: opts.task, repoPath: process.cwd() }, { plannerModel: opts.plannerModel } ); const typedPlan = plan; const compacted = opts.compact ? { summary: typedPlan?.summary, steps: Array.isArray(typedPlan?.steps) ? typedPlan.steps.map((s) => ({ id: s.id, title: s.title, acceptanceCriteria: s.acceptanceCriteria })) : [], risks: typedPlan?.risks } : plan; const payload = JSON.stringify(compacted, null, opts.pretty ? 2 : 0); console.log(payload); } catch (error) { console.error("plan failed:", error.message); process.exit(1); } }); program.command("dashboard").description("Display monitoring dashboard in terminal").option("-w, --watch", "Auto-refresh dashboard").option("-i, --interval <seconds>", "Refresh interval in seconds", "5").action(async (options) => { const { dashboardCommand } = await import("./commands/dashboard.js"); await dashboardCommand.handler(options); }); if (process.argv.length > 2) { try { const manager = ProjectManager.getInstance(); manager.detectProject().catch(() => { }); } catch { } } const isMainModule = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/stackmemory") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.includes("tsx"); if (isMainModule) { Promise.all(lazyCommands).then(() => program.parse()); } export { program }; //# sourceMappingURL=index.js.map