@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.
1,374 lines (1,373 loc) • 49.5 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 {
generateSessionSummary,
formatSummaryMessage
} from "../hooks/session-summary.js";
import { sendNotification, loadSMSConfig } from "../hooks/sms-notify.js";
import { enableAutoSync } from "../hooks/whatsapp-sync.js";
import { enableCommands } from "../hooks/whatsapp-commands.js";
import { startScheduler, listSchedules } from "../hooks/whatsapp-scheduler.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";
const DEFAULT_SM_CONFIG = {
defaultWorktree: false,
defaultSandbox: false,
defaultChrome: false,
defaultTracing: true,
defaultRemote: false,
defaultNotifyOnDone: true,
defaultWhatsApp: false,
defaultModelRouting: false,
defaultSweep: true,
defaultGreptile: 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,
useRemote: this.smConfig.defaultRemote,
notifyOnDone: this.smConfig.defaultNotifyOnDone,
useWhatsApp: this.smConfig.defaultWhatsApp,
contextEnabled: true,
tracingEnabled: this.smConfig.defaultTracing,
verboseTracing: false,
sessionStartTime: Date.now(),
useModelRouting: this.smConfig.defaultModelRouting,
useThinkingMode: false,
useSweep: this.smConfig.defaultSweep,
useGreptile: this.smConfig.defaultGreptile,
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 });
}
}
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 = [];
}
ensureGreptileMcp() {
const apiKey = process.env["GREPTILE_API_KEY"];
const ghToken = process.env["GITHUB_TOKEN"];
if (!apiKey) {
console.log(
chalk.gray(" Greptile: disabled (set GREPTILE_API_KEY in .env)")
);
return;
}
try {
const result = execSync("claude mcp list 2>/dev/null", {
encoding: "utf-8"
});
if (result.includes("greptile")) {
console.log(chalk.gray(" Greptile: MCP server registered"));
return;
}
} catch {
}
try {
const cmd = [
"claude mcp add",
"--transport http",
"greptile",
"https://api.greptile.com/mcp",
`--header "Authorization: Bearer ${apiKey}"`
];
if (ghToken) {
cmd.push(`--header "X-GitHub-Token: ${ghToken}"`);
}
execSync(cmd.join(" "), { stdio: "ignore" });
console.log(chalk.cyan(" Greptile: MCP server registered"));
} catch {
try {
const envArgs = [`GREPTILE_API_KEY=${apiKey}`];
if (ghToken) envArgs.push(`GITHUB_TOKEN=${ghToken}`);
execSync(
`claude mcp add greptile -- env ${envArgs.join(" ")} npx greptile-mcp-server`,
{ stdio: "ignore" }
);
console.log(chalk.cyan(" Greptile: MCP server registered (stdio)"));
} catch {
console.log(chalk.gray(" Greptile: failed to register MCP server"));
}
}
}
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 {
}
}
loadContext() {
if (!this.config.contextEnabled) return;
try {
console.log(chalk.blue("\u{1F4DA} Loading previous context..."));
const cmd = `${this.stackmemoryPath} context show`;
const output = execSync(cmd, {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
// Capture stderr to suppress errors
});
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")
);
}
}
async startWhatsAppServices() {
const WEBHOOK_PORT = 3456;
console.log(chalk.cyan("\n\u{1F4F1} WhatsApp Mode - Full Integration"));
console.log(chalk.gray("\u2500".repeat(42)));
const smsConfig = loadSMSConfig();
if (!smsConfig.enabled) {
console.log(
chalk.yellow(" Notifications disabled. Run: stackmemory notify enable")
);
}
try {
enableAutoSync();
console.log(
chalk.green(" \u2713 Auto-sync enabled (digests on frame close)")
);
} catch {
console.log(chalk.gray(" Auto-sync: skipped"));
}
try {
enableCommands();
console.log(chalk.green(" \u2713 Command processing enabled"));
console.log(
chalk.gray(" Reply: status, tasks, context, help, build, test, lint")
);
} catch {
console.log(chalk.gray(" Command processing: skipped"));
}
try {
const schedules = listSchedules();
if (schedules.length > 0) {
startScheduler();
console.log(
chalk.green(` \u2713 Scheduler started (${schedules.length} schedules)`)
);
}
} catch {
}
const webhookRunning = await fetch(
`http://localhost:${WEBHOOK_PORT}/health`
).then((r) => r.ok).catch(() => false);
if (!webhookRunning) {
const webhookPath = path.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();
console.log(
chalk.green(` \u2713 Webhook server started (port ${WEBHOOK_PORT})`)
);
} else {
console.log(
chalk.green(` \u2713 Webhook already running (port ${WEBHOOK_PORT})`)
);
}
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();
console.log(chalk.gray(" Starting ngrok tunnel..."));
await new Promise((resolve) => setTimeout(resolve, 3e3));
}
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 = path.join(os.homedir(), ".stackmemory");
const configPath = path.join(configDir, "ngrok-url.txt");
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(configPath, publicUrl);
console.log(chalk.green(` \u2713 ngrok tunnel: ${publicUrl}/sms/incoming`));
}
} catch {
console.log(chalk.yellow(" ngrok: waiting for tunnel..."));
}
if (smsConfig.pendingPrompts.length > 0) {
console.log();
console.log(
chalk.yellow(
` \u23F3 ${smsConfig.pendingPrompts.length} pending prompt(s) awaiting response:`
)
);
smsConfig.pendingPrompts.slice(0, 3).forEach((p, i) => {
const msg = p.message.length > 40 ? p.message.slice(0, 40) + "..." : p.message;
console.log(chalk.gray(` ${i + 1}. [${p.id}] ${msg}`));
});
if (smsConfig.pendingPrompts.length > 3) {
console.log(
chalk.gray(` ... and ${smsConfig.pendingPrompts.length - 3} more`)
);
}
}
console.log();
console.log(chalk.gray(" Quick actions from WhatsApp:"));
console.log(chalk.gray(' "status" - session status'));
console.log(chalk.gray(' "context" - current frame digest'));
console.log(chalk.gray(' "1", "2" - respond to prompts'));
console.log(chalk.gray("\u2500".repeat(42)));
}
async sendDoneNotification(exitCode) {
try {
const context = {
instanceId: this.config.instanceId,
exitCode,
sessionStartTime: this.config.sessionStartTime,
worktreePath: this.config.worktreePath,
branch: this.config.branch,
task: this.config.task
};
const summary = await generateSessionSummary(context);
const message = formatSummaryMessage(summary, this.config.instanceId);
console.log(chalk.cyan("\nSending session summary via WhatsApp..."));
let options = summary.suggestions.slice(0, 4).map((s) => ({
key: s.key,
label: s.label,
action: s.action
}));
if (options.length < 2) {
const defaults = [
{ key: "1", label: "Start new session", action: "claude-sm" },
{
key: "2",
label: "View logs",
action: "tail -30 ~/.claude/logs/*.log"
}
];
options = defaults.slice(0, 2 - options.length).concat(options);
options.forEach((o, i) => o.key = String(i + 1));
}
const result = await sendNotification({
type: "task_complete",
title: `Claude Session ${this.config.instanceId}`,
message,
prompt: {
type: "options",
options
}
});
if (result.success) {
console.log(chalk.green("Notification sent successfully"));
} else {
console.log(
chalk.yellow(`Notification not sent: ${result.error || "unknown"}`)
);
}
} catch (error) {
console.log(
chalk.yellow(
`Could not send notification: ${error instanceof Error ? error.message : "unknown"}`
)
);
}
}
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 "--remote":
case "-r":
this.config.useRemote = true;
break;
case "--no-remote":
this.config.useRemote = false;
break;
case "--notify-done":
case "-n":
this.config.notifyOnDone = true;
break;
case "--no-notify-done":
this.config.notifyOnDone = false;
break;
case "--whatsapp":
this.config.useWhatsApp = true;
this.config.notifyOnDone = true;
break;
case "--no-whatsapp":
this.config.useWhatsApp = 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 "--greptile":
this.config.useGreptile = true;
break;
case "--no-greptile":
this.config.useGreptile = 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();
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();
process.env["CLAUDE_INSTANCE_ID"] = this.config.instanceId;
if (this.config.worktreePath) {
process.env["CLAUDE_WORKTREE_PATH"] = this.config.worktreePath;
}
if (this.config.useRemote) {
process.env["CLAUDE_REMOTE"] = "1";
}
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.useRemote) {
console.log(
chalk.cyan("\u{1F4F1} Remote mode: WhatsApp notifications for all questions")
);
}
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.useGreptile) {
this.ensureGreptileMcp();
}
if (this.config.useGEPA) {
this.startGEPAWatcher();
}
if (this.config.useWhatsApp) {
console.log(
chalk.cyan("\u{1F4F1} WhatsApp mode: notifications + webhook enabled")
);
await this.startWhatsAppServices();
}
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
});
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;
}
}
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}...`));
if (this.config.notifyOnDone || this.config.useWhatsApp) {
sendNotification({
type: "custom",
title: "Model Fallback",
message: `Claude unavailable (${reason}). Switched to ${provider}.`
}).catch(() => {
});
}
}
});
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 (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.config.useRemote) {
await this.sendDoneNotification(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(` Code Review ${config.defaultGreptile ? 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(` Mobile Sync ${config.defaultWhatsApp ? 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",
remote: "defaultRemote",
"notify-done": "defaultNotifyOnDone",
notifyondone: "defaultNotifyOnDone",
whatsapp: "defaultWhatsApp",
"model-routing": "defaultModelRouting",
modelrouting: "defaultModelRouting",
sweep: "defaultSweep",
greptile: "defaultGreptile",
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, remote, notify-done, whatsapp, sweep, greptile, 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("remote-on").description("Enable remote mode by default (WhatsApp for all questions)").action(() => {
const config = loadSMConfig();
config.defaultRemote = true;
saveSMConfig(config);
console.log(chalk.green("Remote mode enabled by default"));
});
configCmd.command("remote-off").description("Disable remote mode by default").action(() => {
const config = loadSMConfig();
config.defaultRemote = false;
saveSMConfig(config);
console.log(chalk.green("Remote mode disabled by default"));
});
configCmd.command("notify-done-on").description("Enable WhatsApp 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("whatsapp-on").description("Enable WhatsApp mode by default (auto-starts webhook + ngrok)").action(() => {
const config = loadSMConfig();
config.defaultWhatsApp = true;
config.defaultNotifyOnDone = true;
saveSMConfig(config);
console.log(chalk.green("WhatsApp mode enabled by default"));
console.log(chalk.gray("Sessions will auto-start webhook and ngrok"));
});
configCmd.command("whatsapp-off").description("Disable WhatsApp mode by default").action(() => {
const config = loadSMConfig();
config.defaultWhatsApp = false;
saveSMConfig(config);
console.log(chalk.green("WhatsApp mode 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("greptile-on").description(
"Enable Greptile AI code review by default (requires GREPTILE_API_KEY)"
).action(() => {
const config = loadSMConfig();
config.defaultGreptile = true;
saveSMConfig(config);
console.log(chalk.green("Greptile enabled by default"));
if (!process.env["GREPTILE_API_KEY"]) {
console.log(chalk.gray("Set GREPTILE_API_KEY in .env to activate"));
}
});
configCmd.command("greptile-off").description("Disable Greptile AI code review by default").action(() => {
const config = loadSMConfig();
config.defaultGreptile = false;
saveSMConfig(config);
console.log(chalk.green("Greptile 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: "defaultGreptile",
name: "Code Review",
desc: "Automated code review with deep codebase intelligence"
},
{
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: "defaultWhatsApp",
name: "Mobile Sync",
desc: "Remote control and notifications via WhatsApp"
},
{
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"));
}
}
}
if (config.defaultGreptile) {
const apiKey = process.env["GREPTILE_API_KEY"];
if (!apiKey) {
const { key } = await inquirer.default.prompt([
{
type: "password",
name: "key",
message: "Enter your Greptile API key (from app.greptile.com):",
mask: "*"
}
]);
if (key && key.trim()) {
const envPath = path.join(process.cwd(), ".env");
const line = `
GREPTILE_API_KEY=${key.trim()}
`;
fs.appendFileSync(envPath, line);
process.env["GREPTILE_API_KEY"] = key.trim();
console.log(chalk.green(" API key saved to .env"));
}
}
const currentKey = process.env["GREPTILE_API_KEY"];
if (currentKey) {
try {
const result = execSync("claude mcp list 2>/dev/null", {
encoding: "utf-8"
});
if (!result.includes("greptile")) {
execSync(
`claude mcp add --transport http greptile https://api.greptile.com/mcp --header "Authorization: Bearer ${currentKey}"`,
{ stdio: "ignore" }
);
console.log(chalk.green(" Greptile MCP server registered"));
}
} catch {
}
}
}
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("-r, --remote", "Enable remote mode (WhatsApp for all questions)").option("--no-remote", "Disable remote mode (override default)").option("-n, --notify-done", "Send WhatsApp notification when session ends").option("--no-notify-done", "Disable notification when session ends").option(
"--whatsapp",
"Enable WhatsApp mode (auto-start webhook + ngrok + notifications)"
).option("--no-whatsapp", "Disable WhatsApp mode (override default)").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("--greptile", "Enable Greptile AI code review (MCP server)").option("--no-greptile", "Disable Greptile integration").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);
//# sourceMappingURL=claude-sm.js.map