@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
1,173 lines (1,171 loc) • 41.1 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 { config as loadDotenv } from "dotenv";
loadDotenv({ override: true, debug: false });
import { spawn, execSync, execFileSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { program } from "commander";
import { v4 as uuidv4 } from "uuid";
import chalk from "chalk";
import { initializeTracing, trace } from "../core/trace/index.js";
import {
getModelRouter,
loadModelRouterConfig
} from "../core/models/model-router.js";
import { launchWrapper } from "../features/sweep/pty-wrapper.js";
import { FallbackMonitor } from "../core/models/fallback-monitor.js";
import {
ensureWorkerStateDir,
saveRegistry,
loadRegistry,
clearRegistry
} from "../features/workers/worker-registry.js";
import {
isTmuxAvailable,
createTmuxSession,
sendToPane,
killTmuxSession,
attachToSession,
listPanes,
sendCtrlC,
sessionExists
} from "../features/workers/tmux-manager.js";
import {
CANONICAL_HOOKS,
mergeSettings,
hasDeadHooks,
readSettings,
writeSettingsAtomic,
getSettingsPath
} from "../utils/hook-installer.js";
const DEFAULT_SM_CONFIG = {
defaultWorktree: false,
defaultSandbox: false,
defaultChrome: false,
defaultTracing: true,
defaultNotifyOnDone: true,
defaultModelRouting: false,
defaultSweep: true,
defaultGEPA: false
};
function getConfigPath() {
return path.join(os.homedir(), ".stackmemory", "claude-sm.json");
}
function loadSMConfig() {
try {
const configPath = getConfigPath();
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf8");
return { ...DEFAULT_SM_CONFIG, ...JSON.parse(content) };
}
} catch {
}
return { ...DEFAULT_SM_CONFIG };
}
function saveSMConfig(config) {
const configPath = getConfigPath();
const dir = path.dirname(configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
class ClaudeSM {
config;
stackmemoryPath;
worktreeScriptPath;
claudeConfigDir;
smConfig;
constructor() {
this.smConfig = loadSMConfig();
this.config = {
instanceId: this.generateInstanceId(),
useSandbox: this.smConfig.defaultSandbox,
useChrome: this.smConfig.defaultChrome,
useWorktree: this.smConfig.defaultWorktree,
notifyOnDone: this.smConfig.defaultNotifyOnDone,
contextEnabled: true,
tracingEnabled: this.smConfig.defaultTracing,
verboseTracing: false,
sessionStartTime: Date.now(),
useModelRouting: this.smConfig.defaultModelRouting,
useThinkingMode: false,
useSweep: this.smConfig.defaultSweep,
useGEPA: this.smConfig.defaultGEPA
};
this.stackmemoryPath = this.findStackMemory();
this.worktreeScriptPath = path.join(
__dirname,
"../../scripts/claude-worktree-manager.sh"
);
this.claudeConfigDir = path.join(os.homedir(), ".claude");
if (!fs.existsSync(this.claudeConfigDir)) {
fs.mkdirSync(this.claudeConfigDir, { recursive: true });
}
}
getRepoRoot() {
try {
const root = execSync("git rev-parse --show-toplevel", {
encoding: "utf8"
}).trim();
return root || null;
} catch {
return null;
}
}
ensureInitialized() {
try {
const root = this.getRepoRoot();
const dir = root || process.cwd();
const dbPath = path.join(dir, ".stackmemory", "context.db");
if (!fs.existsSync(dbPath)) {
console.log(
chalk.blue("\u{1F4E6} Initializing StackMemory for this project...")
);
execSync(`${this.stackmemoryPath} init`, {
cwd: dir,
stdio: ["ignore", "ignore", "ignore"]
});
}
} catch {
}
}
generateInstanceId() {
return uuidv4().substring(0, 8);
}
findStackMemory() {
const possiblePaths = [
path.join(os.homedir(), ".stackmemory", "bin", "stackmemory"),
"/usr/local/bin/stackmemory",
"/opt/homebrew/bin/stackmemory",
"stackmemory"
// Rely on PATH
];
for (const smPath of possiblePaths) {
try {
execFileSync("which", [smPath], { stdio: "ignore" });
return smPath;
} catch {
}
}
return "stackmemory";
}
isGitRepo() {
try {
execSync("git rev-parse --git-dir", { stdio: "ignore" });
return true;
} catch {
return false;
}
}
getCurrentBranch() {
try {
return execSync("git rev-parse --abbrev-ref HEAD", {
encoding: "utf8"
}).trim();
} catch {
return "main";
}
}
hasUncommittedChanges() {
try {
const status = execSync("git status --porcelain", { encoding: "utf8" });
return status.length > 0;
} catch {
return false;
}
}
resolveClaudeBin() {
if (this.config.claudeBin && this.config.claudeBin.trim()) {
return this.config.claudeBin.trim();
}
const envBin = process.env["CLAUDE_BIN"];
if (envBin && envBin.trim()) return envBin.trim();
try {
execSync("which claude", { stdio: "ignore" });
return "claude";
} catch {
}
return null;
}
gepaProcesses = [];
startGEPAWatcher() {
const watchFiles = ["CLAUDE.md", "AGENT.md", "AGENTS.md"].map((f) => path.join(process.cwd(), f)).filter((p) => fs.existsSync(p));
if (watchFiles.length === 0) {
console.log(
chalk.gray(
" Prompt Forge: disabled (no CLAUDE.md, AGENT.md, or AGENTS.md found)"
)
);
return;
}
const gepaPaths = [
// From dist/src/cli -> scripts/gepa (3 levels up)
path.join(__dirname, "../../../scripts/gepa/hooks/auto-optimize.js"),
// From src/cli -> scripts/gepa (2 levels up, for dev mode)
path.join(__dirname, "../../scripts/gepa/hooks/auto-optimize.js"),
// Global install location
path.join(
os.homedir(),
".stackmemory",
"scripts",
"gepa",
"hooks",
"auto-optimize.js"
),
// npm global install
path.join(
__dirname,
"..",
"..",
"scripts",
"gepa",
"hooks",
"auto-optimize.js"
)
];
const gepaScript = gepaPaths.find((p) => fs.existsSync(p));
if (!gepaScript) {
console.log(chalk.gray(" Prompt Forge: disabled (scripts not found)"));
return;
}
for (const filePath of watchFiles) {
const gepaProcess = spawn("node", [gepaScript, "watch", filePath], {
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, GEPA_SILENT: "1" }
});
gepaProcess.unref();
this.gepaProcesses.push(gepaProcess);
gepaProcess.stdout?.on("data", (data) => {
const output = data.toString().trim();
if (output && !output.includes("Watching")) {
console.log(chalk.magenta(`[GEPA] ${output}`));
}
});
}
const fileNames = watchFiles.map((f) => path.basename(f)).join(", ");
console.log(
chalk.cyan(` Prompt Forge: watching ${fileNames} for optimization`)
);
}
stopGEPAWatcher() {
for (const proc of this.gepaProcesses) {
proc.kill("SIGTERM");
}
this.gepaProcesses = [];
}
setupWorktree() {
if (!this.config.useWorktree || !this.isGitRepo()) {
return null;
}
console.log(chalk.blue("\u{1F333} Setting up isolated worktree..."));
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
const branch = this.config.branch || `claude-${this.config.task || "work"}-${timestamp}-${this.config.instanceId}`;
const repoName = path.basename(process.cwd());
const worktreePath = path.join(
path.dirname(process.cwd()),
`${repoName}--${branch}`
);
try {
const flags = [];
if (this.config.useSandbox) flags.push("--sandbox");
if (this.config.useChrome) flags.push("--chrome");
const cmd = `git worktree add -b "${branch}" "${worktreePath}"`;
execSync(cmd, { stdio: "inherit" });
console.log(chalk.green(`\u2705 Worktree created: ${worktreePath}`));
console.log(chalk.gray(` Branch: ${branch}`));
const configPath = path.join(worktreePath, ".claude-instance.json");
const configData = {
instanceId: this.config.instanceId,
worktreePath,
branch,
task: this.config.task,
sandboxEnabled: this.config.useSandbox,
chromeEnabled: this.config.useChrome,
created: (/* @__PURE__ */ new Date()).toISOString(),
parentRepo: process.cwd()
};
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2));
const envFiles = [".env", ".env.local", ".mise.toml", ".tool-versions"];
for (const file of envFiles) {
const srcPath = path.join(process.cwd(), file);
if (fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, path.join(worktreePath, file));
}
}
return worktreePath;
} catch (err) {
console.error(chalk.red("\u274C Failed to create worktree:"), err);
return null;
}
}
saveContext(message, metadata = {}) {
if (!this.config.contextEnabled) return;
try {
const contextData = {
message,
metadata: {
...metadata,
instanceId: this.config.instanceId,
worktree: this.config.worktreePath,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
const cmd = `${this.stackmemoryPath} context save --json '${JSON.stringify(contextData)}'`;
execSync(cmd, { stdio: "ignore" });
} catch {
}
}
getTheoryContent() {
try {
let root;
try {
root = execSync("git rev-parse --show-toplevel", {
encoding: "utf-8",
timeout: 5e3
}).trim();
} catch {
root = process.cwd();
}
const theoryPath = path.join(root, "THEORY.MD");
if (fs.existsSync(theoryPath)) {
const content = fs.readFileSync(theoryPath, "utf8").trim();
if (content.length > 0) {
return content.length > 4e3 ? content.substring(0, 4e3) + "\n\n[...truncated]" : content;
}
}
} catch {
}
return null;
}
getHandoffContent() {
if (!this.config.contextEnabled) return null;
try {
const handoffPath = path.join(
process.cwd(),
".stackmemory",
"last-handoff.md"
);
if (fs.existsSync(handoffPath)) {
const content = fs.readFileSync(handoffPath, "utf8").trim();
if (content.length > 0) {
return content.length > 8e3 ? content.substring(0, 8e3) + "\n\n[...truncated]" : content;
}
}
} catch {
}
return null;
}
/**
* Ensure Claude Code hooks are installed. Copies missing scripts from
* templates and updates settings.json. Non-fatal on any error.
*/
ensureHooks() {
try {
const hooksDir = path.join(os.homedir(), ".claude", "hooks");
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, { recursive: true });
}
const candidateDirs = [
path.join(__dirname, "../../templates/claude-hooks"),
path.join(__dirname, "../../../templates/claude-hooks"),
path.join(
__dirname,
"..",
"..",
"..",
"..",
"templates",
"claude-hooks"
)
];
const templatesDir = candidateDirs.find(
(d) => fs.existsSync(d) && fs.readdirSync(d).length > 0
);
if (!templatesDir) return;
let copiedCount = 0;
for (const entry of CANONICAL_HOOKS) {
const dest = path.join(hooksDir, entry.scriptName);
if (!fs.existsSync(dest)) {
const src = path.join(templatesDir, entry.scriptName);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
try {
fs.chmodSync(dest, 493);
} catch {
}
copiedCount++;
}
}
}
const settingsPath = getSettingsPath();
const current = readSettings(settingsPath);
if (copiedCount > 0 || hasDeadHooks(current)) {
const merged = mergeSettings(current, hooksDir);
writeSettingsAtomic(merged, settingsPath);
}
if (copiedCount > 0) {
console.log(
chalk.gray(` Hooks: installed ${copiedCount} missing hook(s)`)
);
}
} catch {
}
}
loadContext() {
if (!this.config.contextEnabled) return;
try {
const cmd = `${this.stackmemoryPath} context show`;
const output = execSync(cmd, {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
});
const lines = output.trim().split("\n").filter((l) => l.trim());
if (lines.length > 3) {
console.log(chalk.gray(" Context stack loaded"));
}
} catch {
}
}
detectMultipleInstances() {
try {
const lockDir = path.join(process.cwd(), ".claude-worktree-locks");
if (!fs.existsSync(lockDir)) return false;
const locks = fs.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
const activeLocks = locks.filter((lockFile) => {
const lockPath = path.join(lockDir, lockFile);
const lockData = JSON.parse(fs.readFileSync(lockPath, "utf8"));
const lockAge = Date.now() - new Date(lockData.created).getTime();
return lockAge < 24 * 60 * 60 * 1e3;
});
return activeLocks.length > 0;
} catch {
return false;
}
}
suggestWorktreeMode() {
if (this.hasUncommittedChanges()) {
console.log(chalk.yellow("\u26A0\uFE0F Uncommitted changes detected"));
console.log(
chalk.gray(" Consider using --worktree to work in isolation")
);
}
if (this.detectMultipleInstances()) {
console.log(chalk.yellow("\u26A0\uFE0F Other Claude instances detected"));
console.log(
chalk.gray(" Using --worktree is recommended to avoid conflicts")
);
}
}
notifyDone(exitCode) {
process.stdout.write("\x07");
console.log(chalk.gray(`
Session ended (exit ${exitCode ?? 0})`));
}
async run(args) {
const claudeArgs = [];
let i = 0;
while (i < args.length) {
const arg = args[i];
switch (arg) {
case "--worktree":
case "-w":
this.config.useWorktree = true;
break;
case "--no-worktree":
case "-W":
this.config.useWorktree = false;
break;
case "--notify-done":
case "-n":
this.config.notifyOnDone = true;
break;
case "--no-notify-done":
this.config.notifyOnDone = false;
break;
case "--sandbox":
case "-s":
this.config.useSandbox = true;
claudeArgs.push("--sandbox");
break;
case "--chrome":
case "-c":
this.config.useChrome = true;
claudeArgs.push("--chrome");
break;
case "--no-context":
this.config.contextEnabled = false;
break;
case "--no-trace":
this.config.tracingEnabled = false;
break;
case "--verbose-trace":
this.config.verboseTracing = true;
break;
case "--branch":
case "-b":
i++;
this.config.branch = args[i];
break;
case "--task":
case "-t":
i++;
this.config.task = args[i];
break;
case "--claude-bin":
i++;
this.config.claudeBin = args[i];
process.env["CLAUDE_BIN"] = this.config.claudeBin;
break;
case "--auto":
case "-a":
if (this.isGitRepo()) {
this.config.useWorktree = this.hasUncommittedChanges() || this.detectMultipleInstances();
}
break;
case "--think":
case "--think-hard":
case "--ultrathink":
this.config.useThinkingMode = true;
this.config.useModelRouting = true;
this.config.forceProvider = "qwen";
break;
case "--qwen":
this.config.useModelRouting = true;
this.config.forceProvider = "qwen";
break;
case "--openai":
this.config.useModelRouting = true;
this.config.forceProvider = "openai";
break;
case "--ollama":
this.config.useModelRouting = true;
this.config.forceProvider = "ollama";
break;
case "--model-routing":
this.config.useModelRouting = true;
break;
case "--no-model-routing":
this.config.useModelRouting = false;
break;
case "--sweep":
this.config.useSweep = true;
break;
case "--no-sweep":
this.config.useSweep = false;
break;
case "--gepa":
this.config.useGEPA = true;
break;
case "--no-gepa":
this.config.useGEPA = false;
break;
default:
claudeArgs.push(arg);
}
i++;
}
const printIndex = claudeArgs.findIndex(
(a) => a === "-p" || a === "--print"
);
if (printIndex !== -1) {
const nextArg = claudeArgs[printIndex + 1];
const hasStdin = !process.stdin.isTTY;
const hasPromptArg = nextArg && !nextArg.startsWith("-");
if (!hasStdin && !hasPromptArg) {
console.error(
chalk.red("Error: --print/-p requires a prompt argument.")
);
console.log(chalk.gray('Usage: claude-smd -p "your prompt here"'));
console.log(chalk.gray(' echo "prompt" | claude-smd -p'));
process.exit(1);
}
}
if (this.config.tracingEnabled) {
process.env["DEBUG_TRACE"] = "true";
process.env["STACKMEMORY_DEBUG"] = "true";
process.env["TRACE_OUTPUT"] = "file";
process.env["TRACE_MASK_SENSITIVE"] = "true";
if (this.config.verboseTracing) {
process.env["TRACE_VERBOSITY"] = "full";
process.env["TRACE_PARAMS"] = "true";
process.env["TRACE_RESULTS"] = "true";
process.env["TRACE_MEMORY"] = "true";
} else {
process.env["TRACE_VERBOSITY"] = "summary";
process.env["TRACE_PARAMS"] = "true";
process.env["TRACE_RESULTS"] = "false";
}
initializeTracing();
trace.command(
"claude-sm",
{
instanceId: this.config.instanceId,
worktree: this.config.useWorktree,
sandbox: this.config.useSandbox,
task: this.config.task
},
async () => {
}
);
}
console.log(chalk.blue("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
console.log(chalk.blue("\u2551 Claude + StackMemory + Worktree \u2551"));
console.log(chalk.blue("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
console.log();
this.ensureInitialized();
if (this.isGitRepo()) {
const branch = this.getCurrentBranch();
console.log(chalk.gray(`\u{1F4CD} Current branch: ${branch}`));
if (!this.config.useWorktree) {
this.suggestWorktreeMode();
}
}
if (this.config.useWorktree) {
const worktreePath = this.setupWorktree();
if (worktreePath) {
this.config.worktreePath = worktreePath;
process.chdir(worktreePath);
this.saveContext("Created worktree for Claude instance", {
action: "worktree_created",
path: worktreePath,
branch: this.config.branch
});
}
}
this.loadContext();
this.ensureHooks();
process.env["CLAUDE_INSTANCE_ID"] = this.config.instanceId;
if (this.config.worktreePath) {
process.env["CLAUDE_WORKTREE_PATH"] = this.config.worktreePath;
}
console.log(chalk.gray(`\u{1F916} Instance ID: ${this.config.instanceId}`));
console.log(chalk.gray(`\u{1F4C1} Working in: ${process.cwd()}`));
if (this.config.useSandbox) {
console.log(chalk.yellow("\u{1F512} Sandbox mode enabled"));
}
if (this.config.useChrome) {
console.log(chalk.yellow("\u{1F310} Chrome automation enabled"));
}
if (this.config.tracingEnabled) {
console.log(
chalk.gray(`\u{1F50D} Debug tracing enabled (logs to ~/.stackmemory/traces/)`)
);
if (this.config.verboseTracing) {
console.log(
chalk.gray(` Verbose mode: capturing all execution details`)
);
}
}
if (this.config.useModelRouting) {
const routerConfig = loadModelRouterConfig();
if (routerConfig.enabled || this.config.forceProvider) {
const router = getModelRouter();
let routeResult;
if (this.config.forceProvider) {
const env = router.switchTo(this.config.forceProvider);
Object.assign(process.env, env);
console.log(
chalk.magenta(`\u{1F500} Model: ${this.config.forceProvider} (forced)`)
);
if (this.config.forceProvider === "qwen" && this.config.useThinkingMode) {
const qwenConfig = routerConfig.providers.qwen;
if (qwenConfig?.params?.enable_thinking) {
console.log(
chalk.gray(
` Thinking mode: budget ${qwenConfig.params.thinking_budget || 1e4} tokens`
)
);
}
}
} else {
const taskType = this.config.useThinkingMode ? "think" : "default";
routeResult = router.route(taskType, this.config.task);
Object.assign(process.env, routeResult.env);
if (routeResult.switched) {
console.log(
chalk.magenta(`\u{1F500} Model routed to: ${routeResult.provider}`)
);
}
}
} else {
console.log(
chalk.gray(
" Model routing: disabled (run: stackmemory model enable)"
)
);
}
}
if (this.config.useGEPA) {
this.startGEPAWatcher();
}
let initialInput = "";
const hasResume = claudeArgs.includes("--continue") || claudeArgs.some((a) => a === "--resume");
if (!hasResume) {
const handoffContent = this.getHandoffContent();
if (handoffContent) {
initialInput = handoffContent;
console.log(chalk.gray(" Handoff context ready"));
}
const theoryContent = this.getTheoryContent();
if (theoryContent) {
initialInput += (initialInput ? "\n\n---\n\n" : "") + `## Operating Theory (THEORY.MD)
${theoryContent}`;
console.log(chalk.gray(" Theory context loaded"));
}
}
console.log();
if (this.config.useSweep) {
const claudeBin2 = this.resolveClaudeBin();
if (!claudeBin2) {
console.error(chalk.red("Claude CLI not found."));
process.exit(1);
return;
}
console.log(
chalk.cyan("[Sweep] Launching Claude with prediction bar...")
);
console.log(chalk.gray("\u2500".repeat(42)));
try {
await launchWrapper({
claudeBin: claudeBin2,
claudeArgs,
initialInput: initialInput || void 0
});
return;
} catch (error) {
const msg = error.message || "Unknown PTY error";
console.error(chalk.yellow(`[Sweep disabled] ${msg}`));
console.log(
chalk.gray(
"Falling back to direct Claude launch (no prediction bar)..."
)
);
this.config.useSweep = false;
}
}
if (initialInput) {
claudeArgs.push("--append-system-prompt", initialInput);
}
console.log(chalk.gray("Starting Claude..."));
console.log(chalk.gray("\u2500".repeat(42)));
const claudeBin = this.resolveClaudeBin();
if (!claudeBin) {
console.error(chalk.red("\u274C Claude CLI not found."));
console.log(
chalk.gray(
" Install Claude CLI or set an override:\n export CLAUDE_BIN=/path/to/claude\n claude-sm --help\n\n Ensure PATH includes npm global bin (npm bin -g)."
)
);
process.exit(1);
return;
}
const fallbackMonitor = new FallbackMonitor({
enabled: true,
maxRestarts: 2,
restartDelayMs: 1500,
onFallback: (provider, reason) => {
console.log(chalk.yellow(`
[auto-fallback] Switching to ${provider}`));
console.log(chalk.gray(` Reason: ${reason}`));
console.log(chalk.gray(` Session will continue on ${provider}...`));
}
});
const fallbackAvailable = fallbackMonitor.isFallbackAvailable();
if (fallbackAvailable) {
console.log(
chalk.gray(` Auto-fallback: Qwen ready (on rate limit/error)`)
);
}
const wrapper = fallbackMonitor.wrapProcess(claudeBin, claudeArgs, {
env: process.env,
cwd: process.cwd()
});
const claude = wrapper.start();
claude.on("error", (err) => {
console.error(chalk.red("\u274C Failed to launch Claude CLI."));
if (err.code === "ENOENT") {
console.error(
chalk.gray(" Not found. Set CLAUDE_BIN or install claude on PATH.")
);
} else if (err.code === "EPERM" || err.code === "EACCES") {
console.error(
chalk.gray(
" Permission/sandbox issue. Try outside a sandbox or set CLAUDE_BIN."
)
);
} else {
console.error(chalk.gray(` ${err.message}`));
}
process.exit(1);
});
claude.on("exit", async (code) => {
this.stopGEPAWatcher();
const status = fallbackMonitor.getStatus();
if (status.inFallback) {
console.log(
chalk.yellow(
`
Session completed on fallback provider: ${status.currentProvider}`
)
);
}
this.saveContext("Claude session ended", {
action: "session_end",
exitCode: code
});
if (process.env["LINEAR_API_KEY"]) {
try {
execSync("stackmemory linear sync", {
stdio: "ignore",
timeout: 1e4
});
} catch {
}
}
if (this.config.tracingEnabled) {
const summary = trace.getExecutionSummary();
console.log();
console.log(chalk.gray("\u2500".repeat(42)));
console.log(chalk.blue("Debug Trace Summary:"));
console.log(chalk.gray(summary));
}
if (this.config.notifyOnDone) {
this.notifyDone(code);
}
if (this.config.worktreePath) {
console.log();
console.log(chalk.gray("\u2500".repeat(42)));
console.log(chalk.blue("Session ended in worktree:"));
console.log(chalk.gray(` ${this.config.worktreePath}`));
console.log();
console.log(chalk.gray("To remove worktree: gd_claude"));
console.log(chalk.gray("To merge to main: cwm"));
}
process.exit(code || 0);
});
process.on("SIGINT", () => {
this.saveContext("Claude session interrupted", {
action: "session_interrupt"
});
claude.kill("SIGINT");
});
process.on("SIGTERM", () => {
this.saveContext("Claude session terminated", {
action: "session_terminate"
});
claude.kill("SIGTERM");
});
}
}
program.name("claude-sm").description("Claude with StackMemory context and worktree isolation").version("1.0.0");
const configCmd = program.command("config").description("Manage claude-sm defaults");
configCmd.command("show").description("Show current default settings").action(() => {
const config = loadSMConfig();
console.log(chalk.blue("claude-sm defaults:"));
const on = chalk.green("ON ");
const off = chalk.gray("OFF");
console.log(chalk.cyan("\n Feature Status"));
console.log(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"));
console.log(` Predictive Edit ${config.defaultSweep ? on : off}`);
console.log(` Prompt Forge ${config.defaultGEPA ? on : off}`);
console.log(` Model Switcher ${config.defaultModelRouting ? on : off}`);
console.log(` Safe Branch ${config.defaultWorktree ? on : off}`);
console.log(` Session Insights ${config.defaultTracing ? on : off}`);
console.log(` Task Alert ${config.defaultNotifyOnDone ? on : off}`);
console.log(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"));
console.log(` Sandbox ${config.defaultSandbox ? on : off}`);
console.log(` Chrome ${config.defaultChrome ? on : off}`);
console.log(` Remote ${config.defaultRemote ? on : off}`);
console.log(chalk.gray(`
Config: ${getConfigPath()}`));
});
configCmd.command("set <key> <value>").description("Set a default (e.g., set worktree true)").action((key, value) => {
const config = loadSMConfig();
const boolValue = value === "true" || value === "1" || value === "on";
const keyMap = {
worktree: "defaultWorktree",
sandbox: "defaultSandbox",
chrome: "defaultChrome",
tracing: "defaultTracing",
"notify-done": "defaultNotifyOnDone",
notifyondone: "defaultNotifyOnDone",
"model-routing": "defaultModelRouting",
modelrouting: "defaultModelRouting",
sweep: "defaultSweep",
gepa: "defaultGEPA"
};
const configKey = keyMap[key];
if (!configKey) {
console.log(chalk.red(`Unknown key: ${key}`));
console.log(
chalk.gray(
"Valid keys: worktree, sandbox, chrome, tracing, notify-done, model-routing, sweep, gepa"
)
);
process.exit(1);
}
config[configKey] = boolValue;
saveSMConfig(config);
console.log(chalk.green(`Set ${key} = ${boolValue}`));
});
configCmd.command("worktree-on").description("Enable worktree mode by default").action(() => {
const config = loadSMConfig();
config.defaultWorktree = true;
saveSMConfig(config);
console.log(chalk.green("Worktree mode enabled by default"));
});
configCmd.command("worktree-off").description("Disable worktree mode by default").action(() => {
const config = loadSMConfig();
config.defaultWorktree = false;
saveSMConfig(config);
console.log(chalk.green("Worktree mode disabled by default"));
});
configCmd.command("notify-done-on").description("Enable bell notification when session ends (default)").action(() => {
const config = loadSMConfig();
config.defaultNotifyOnDone = true;
saveSMConfig(config);
console.log(chalk.green("Notify-on-done enabled by default"));
});
configCmd.command("notify-done-off").description("Disable notification when session ends").action(() => {
const config = loadSMConfig();
config.defaultNotifyOnDone = false;
saveSMConfig(config);
console.log(chalk.green("Notify-on-done disabled by default"));
});
configCmd.command("model-routing-on").description(
"Enable model routing by default (route tasks to Qwen/other models)"
).action(() => {
const config = loadSMConfig();
config.defaultModelRouting = true;
saveSMConfig(config);
console.log(chalk.green("Model routing enabled by default"));
console.log(chalk.gray("Configure with: stackmemory model setup-qwen"));
});
configCmd.command("model-routing-off").description("Disable model routing by default (use Claude only)").action(() => {
const config = loadSMConfig();
config.defaultModelRouting = false;
saveSMConfig(config);
console.log(chalk.green("Model routing disabled by default"));
});
configCmd.command("gepa-on").description("Enable GEPA auto-optimization of CLAUDE.md on changes").action(() => {
const config = loadSMConfig();
config.defaultGEPA = true;
saveSMConfig(config);
console.log(chalk.green("GEPA auto-optimization enabled by default"));
console.log(
chalk.gray("CLAUDE.md changes will trigger evolutionary optimization")
);
});
configCmd.command("gepa-off").description("Disable GEPA auto-optimization").action(() => {
const config = loadSMConfig();
config.defaultGEPA = false;
saveSMConfig(config);
console.log(chalk.green("GEPA auto-optimization disabled by default"));
});
configCmd.command("setup").description("Interactive feature setup wizard").action(async () => {
const inquirer = await import("inquirer");
const ora = (await import("ora")).default;
const config = loadSMConfig();
console.log(chalk.cyan("\nClaude-SM Feature Setup\n"));
const features = [
{
key: "defaultSweep",
name: "Predictive Edit",
desc: "AI-powered next-edit suggestions in real-time"
},
{
key: "defaultGEPA",
name: "Prompt Forge",
desc: "Evolutionary optimization of system prompts"
},
{
key: "defaultModelRouting",
name: "Model Switcher",
desc: "Smart routing across Claude, Qwen, and other models"
},
{
key: "defaultWorktree",
name: "Safe Branch",
desc: "Isolated git worktrees for conflict-free parallel work"
},
{
key: "defaultTracing",
name: "Session Insights",
desc: "Deep execution tracing and performance analytics"
},
{
key: "defaultNotifyOnDone",
name: "Task Alert",
desc: "Instant notification when sessions complete"
}
];
const choices = features.map((f) => ({
name: `${f.name} - ${f.desc}`,
value: f.key,
checked: config[f.key]
}));
const { selected } = await inquirer.default.prompt([
{
type: "checkbox",
name: "selected",
message: "Select features to enable:",
choices
}
]);
const selectedKeys = selected;
for (const f of features) {
config[f.key] = selectedKeys.includes(f.key);
}
saveSMConfig(config);
if (config.defaultSweep) {
let hasPty = false;
try {
await import("node-pty");
hasPty = true;
} catch {
}
if (!hasPty) {
const spinner = ora("Installing node-pty...").start();
try {
execSync("npm install node-pty", {
stdio: "ignore",
cwd: process.cwd()
});
spinner.succeed("node-pty installed");
} catch {
spinner.fail("Failed to install node-pty");
console.log(chalk.gray(" Install manually: npm install node-pty"));
}
}
}
console.log(chalk.cyan("\nFeature summary:"));
for (const f of features) {
const on = config[f.key];
const mark = on ? chalk.green("ON") : chalk.gray("OFF");
console.log(` ${mark} ${f.name}`);
}
console.log(chalk.gray(`
Saved to ${getConfigPath()}`));
});
program.command("spawn <count>").description("Spawn N parallel Claude workers in tmux panes").option("-t, --task <desc>", "Task description for each worker").option("--worktree", "Create isolated git worktrees per worker").option("--no-sweep", "Disable Sweep predictions").option("--no-attach", "Do not attach to tmux session after creation").action(async (countStr, opts) => {
const count = parseInt(countStr, 10);
if (isNaN(count) || count < 1 || count > 8) {
console.error(chalk.red("Worker count must be between 1 and 8"));
process.exit(1);
}
if (!isTmuxAvailable()) {
console.error(chalk.red("tmux is required for parallel workers."));
console.log(chalk.gray("Install with: brew install tmux"));
process.exit(1);
}
const sessionId = uuidv4().substring(0, 8);
const sessionName = `claude-sm-${sessionId}`;
console.log(
chalk.blue(`Spawning ${count} workers in tmux session: ${sessionName}`)
);
createTmuxSession(sessionName, count);
const workers = [];
const panes = listPanes(sessionName);
for (let i = 0; i < count; i++) {
const workerId = `w${i}-${uuidv4().substring(0, 6)}`;
const stateDir = ensureWorkerStateDir(workerId);
const pane = panes[i] || String(i);
const parts = ["claude-sm"];
if (opts["sweep"] === false) parts.push("--no-sweep");
if (opts["worktree"]) parts.push("--worktree");
if (opts["task"]) parts.push("--task", `"${opts["task"]}"`);
const cmd = parts.join(" ");
sendToPane(
sessionName,
pane,
`export SWEEP_INSTANCE_ID=${workerId} SWEEP_STATE_DIR=${stateDir} && ${cmd}`
);
workers.push({
id: workerId,
pane,
cwd: process.cwd(),
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
stateDir,
task: opts["task"]
});
console.log(
chalk.gray(
` Worker ${i}: ${workerId} (pane ${pane}, state: ${stateDir})`
)
);
}
const session = {
sessionName,
workers,
createdAt: (/* @__PURE__ */ new Date()).toISOString()
};
saveRegistry(session);
console.log(chalk.green(`
Registry saved (${workers.length} workers)`));
if (opts["attach"] !== false) {
console.log(chalk.gray("Attaching to tmux session..."));
attachToSession(sessionName);
} else {
console.log(
chalk.gray(`Attach later with: tmux attach -t ${sessionName}`)
);
}
});
const workersCmd = program.command("workers").description("List active workers (default) or manage them");
workersCmd.command("list", { isDefault: true }).description("List active workers").action(() => {
const session = loadRegistry();
if (!session) {
console.log(chalk.gray("No active worker session."));
return;
}
const alive = sessionExists(session.sessionName);
const status = alive ? chalk.green("ACTIVE") : chalk.red("DEAD");
console.log(chalk.blue(`Session: ${session.sessionName} [${status}]`));
console.log(chalk.gray(`Created: ${session.createdAt}`));
console.log();
for (const w of session.workers) {
const taskLabel = w.task ? ` task="${w.task}"` : "";
console.log(` ${chalk.cyan(w.id)} pane=${w.pane}${taskLabel}`);
console.log(chalk.gray(` state: ${w.stateDir}`));
}
});
workersCmd.command("kill [id]").description("Kill entire session or send Ctrl-C to a specific worker").action((id) => {
const session = loadRegistry();
if (!session) {
console.log(chalk.gray("No active worker session."));
return;
}
if (id) {
const worker = session.workers.find((w) => w.id === id);
if (!worker) {
console.error(chalk.red(`Worker ${id} not found.`));
process.exit(1);
}
sendCtrlC(session.sessionName, worker.pane);
console.log(
chalk.yellow(`Sent Ctrl-C to worker ${id} (pane ${worker.pane})`)
);
} else {
if (sessionExists(session.sessionName)) {
killTmuxSession(session.sessionName);
console.log(
chalk.yellow(`Killed tmux session: ${session.sessionName}`)
);
}
clearRegistry();
console.log(chalk.gray("Registry cleared."));
}
});
program.option("-w, --worktree", "Create isolated worktree for this instance").option("-W, --no-worktree", "Disable worktree (override default)").option("-n, --notify-done", "Bell notification when session ends").option("--no-notify-done", "Disable notification when session ends").option("-s, --sandbox", "Enable sandbox mode (file/network restrictions)").option("-c, --chrome", "Enable Chrome automation").option("-a, --auto", "Automatically detect and apply best settings").option("-b, --branch <name>", "Specify branch name for worktree").option("-t, --task <desc>", "Task description for context").option("--claude-bin <path>", "Path to claude CLI (or use CLAUDE_BIN)").option("--no-context", "Disable StackMemory context integration").option("--no-trace", "Disable debug tracing (enabled by default)").option("--verbose-trace", "Enable verbose debug tracing with full details").option("--think", "Enable thinking mode with Qwen (deep reasoning)").option("--think-hard", "Alias for --think").option("--ultrathink", "Alias for --think").option("--qwen", "Force Qwen provider for this session").option("--openai", "Force OpenAI provider for this session").option("--ollama", "Force Ollama provider for this session").option("--model-routing", "Enable model routing").option("--no-model-routing", "Disable model routing").option("--sweep", "Enable Sweep next-edit predictions (PTY wrapper)").option("--no-sweep", "Disable Sweep predictions").option("--gepa", "Enable GEPA auto-optimization of CLAUDE.md").option("--no-gepa", "Disable GEPA auto-optimization").helpOption("-h, --help", "Display help").allowUnknownOption(true).action(async (_options) => {
const claudeSM = new ClaudeSM();
const args = process.argv.slice(2);
await claudeSM.run(args);
});
program.parse(process.argv);