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