UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

350 lines (349 loc) 12.3 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); 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"; class CodexSM { config; stackmemoryPath; constructor() { this.config = { instanceId: this.generateInstanceId(), useWorktree: false, contextEnabled: true, tracingEnabled: true, verboseTracing: false }; this.stackmemoryPath = this.findStackMemory(); } 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" ]; 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; } } resolveCodexBin() { if (this.config.codexBin && this.config.codexBin.trim()) { return this.config.codexBin.trim(); } const envBin = process.env["CODEX_BIN"]; if (envBin && envBin.trim()) { return envBin.trim(); } try { execSync("which codex", { stdio: "ignore" }); return "codex"; } catch { } try { execSync("which codex-cli", { stdio: "ignore" }); return "codex-cli"; } catch { } return null; } 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 || `codex-${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 cmd = `git worktree add -b "${branch}" "${worktreePath}"`; execSync(cmd, { stdio: "inherit" }); console.log(chalk.green(`Worktree created: ${worktreePath}`)); console.log(chalk.gray(` Branch: ${branch}`)); const configPath = path.join(worktreePath, ".codex-instance.json"); const configData = { instanceId: this.config.instanceId, worktreePath, branch, task: this.config.task, 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("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 list --limit 5 --format json`; const output = execSync(cmd, { encoding: "utf8" }); const contexts = JSON.parse(output); if (Array.isArray(contexts) && contexts.length > 0) { console.log(chalk.gray("Recent context loaded:")); contexts.forEach( (ctx) => { console.log( chalk.gray(` - ${ctx.message} (${ctx.metadata?.timestamp})`) ); } ); } } catch { } } suggestWorktreeMode() { if (this.hasUncommittedChanges()) { console.log(chalk.yellow("WARNING: Uncommitted changes detected")); console.log( chalk.gray(" Consider using --worktree to work in isolation") ); } } async run(args) { const codexArgs = []; let i = 0; while (i < args.length) { const arg = args[i]; switch (arg) { case "--worktree": case "-w": this.config.useWorktree = true; 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 "--codex-bin": i++; this.config.codexBin = args[i]; process.env["CODEX_BIN"] = this.config.codexBin; break; case "--auto": case "-a": if (this.isGitRepo()) { this.config.useWorktree = this.hasUncommittedChanges(); } break; default: codexArgs.push(arg); } i++; } 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( "codex-sm", { instanceId: this.config.instanceId, worktree: this.config.useWorktree, 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 Codex + 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 Codex instance", { action: "worktree_created", path: worktreePath, branch: this.config.branch }); } } this.loadContext(); process.env["CODEX_INSTANCE_ID"] = this.config.instanceId; if (this.config.worktreePath) process.env["CODEX_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()}`)); console.log(); console.log(chalk.gray("Starting Codex...")); console.log(chalk.gray("\u2500".repeat(42))); const codexBin = this.resolveCodexBin(); if (!codexBin) { console.error(chalk.red("\u274C Codex CLI not found.")); console.log( chalk.gray( " Install codex/codex-cli or set an override:\n export CODEX_BIN=/path/to/codex\n codex-sm --help\n\n Ensure PATH includes npm global bin (npm bin -g)." ) ); process.exit(1); return; } const child = spawn(codexBin, codexArgs, { stdio: "inherit", env: process.env }); child.on("error", (err) => { console.error(chalk.red("\u274C Failed to launch Codex CLI.")); if (err.code === "ENOENT") { console.error( chalk.gray( " Not found. Set CODEX_BIN or install codex/codex-cli on PATH." ) ); } else if (err.code === "EPERM" || err.code === "EACCES") { console.error( chalk.gray( " Permission/sandbox issue. Try running outside a sandbox or set CODEX_BIN." ) ); } else { console.error(chalk.gray(` ${err.message}`)); } process.exit(1); }); child.on("exit", (code) => { this.saveContext("Codex 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.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}`)); } process.exit(code || 0); }); process.on("SIGINT", () => { this.saveContext("Codex session interrupted", { action: "session_interrupt" }); child.kill("SIGINT"); }); process.on("SIGTERM", () => { this.saveContext("Codex session terminated", { action: "session_terminate" }); child.kill("SIGTERM"); }); } } program.name("codex-sm").description("Codex with StackMemory context and optional worktree isolation").version("1.0.0").option("-w, --worktree", "Create isolated worktree for this instance").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("--codex-bin <path>", "Path to codex/codex-cli (or use CODEX_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").helpOption("-h, --help", "Display help").allowUnknownOption(true).action(async (_options) => { const codexSM = new CodexSM(); const args = process.argv.slice(2); await codexSM.run(args); }); program.parse(process.argv); //# sourceMappingURL=codex-sm.js.map