@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.
449 lines (448 loc) • 15.3 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";
const DEFAULT_SM_CONFIG = {
defaultWorktree: false,
defaultTracing: true
};
function getConfigPath() {
return path.join(os.homedir(), ".stackmemory", "opencode-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 OpencodeSM {
config;
stackmemoryPath;
smConfig;
constructor() {
this.smConfig = loadSMConfig();
this.config = {
instanceId: this.generateInstanceId(),
useWorktree: this.smConfig.defaultWorktree,
contextEnabled: true,
tracingEnabled: this.smConfig.defaultTracing,
verboseTracing: false,
sessionStartTime: Date.now()
};
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;
}
}
resolveOpencodeBin() {
if (this.config.opencodeBin && this.config.opencodeBin.trim()) {
return this.config.opencodeBin.trim();
}
const envBin = process.env["OPENCODE_BIN"];
if (envBin && envBin.trim()) return envBin.trim();
const possiblePaths = [
path.join(os.homedir(), ".opencode", "bin", "opencode"),
"/usr/local/bin/opencode",
"/opt/homebrew/bin/opencode"
];
for (const binPath of possiblePaths) {
if (fs.existsSync(binPath)) {
return binPath;
}
}
try {
execSync("which opencode", { stdio: "ignore" });
return "opencode";
} catch {
}
return null;
}
setupWorktree() {
if (!this.config.useWorktree || !this.isGitRepo()) {
return null;
}
console.log(chalk.blue("Setting up isolated worktree..."));
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
const branch = this.config.branch || `opencode-${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, ".opencode-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(),
tool: "opencode"
}
};
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("Loading previous context..."));
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(), ".opencode-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("Uncommitted changes detected"));
console.log(chalk.gray("Consider using --worktree to work in isolation"));
}
if (this.detectMultipleInstances()) {
console.log(chalk.yellow("Other OpenCode instances detected"));
console.log(
chalk.gray("Using --worktree is recommended to avoid conflicts")
);
}
}
async run(args) {
const opencodeArgs = [];
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 "--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 "--opencode-bin":
i++;
this.config.opencodeBin = args[i];
process.env["OPENCODE_BIN"] = this.config.opencodeBin;
break;
case "--auto":
case "-a":
if (this.isGitRepo()) {
this.config.useWorktree = this.hasUncommittedChanges() || this.detectMultipleInstances();
}
break;
default:
opencodeArgs.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(
"opencode-sm",
{
instanceId: this.config.instanceId,
worktree: this.config.useWorktree,
task: this.config.task
},
async () => {
}
);
}
console.log(chalk.magenta("OpenCode + StackMemory"));
console.log();
if (this.isGitRepo()) {
const branch = this.getCurrentBranch();
console.log(chalk.gray(`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 OpenCode instance", {
action: "worktree_created",
path: worktreePath,
branch: this.config.branch
});
}
}
this.loadContext();
process.env["OPENCODE_INSTANCE_ID"] = this.config.instanceId;
if (this.config.worktreePath) {
process.env["OPENCODE_WORKTREE_PATH"] = this.config.worktreePath;
}
console.log(chalk.gray(`Instance: ${this.config.instanceId}`));
console.log(chalk.gray(`Working in: ${process.cwd()}`));
if (this.config.tracingEnabled) {
console.log(
chalk.gray(`Tracing enabled (logs to ~/.stackmemory/traces/)`)
);
}
console.log();
console.log(chalk.gray("Starting OpenCode..."));
console.log(chalk.gray("-".repeat(40)));
const opencodeBin = this.resolveOpencodeBin();
if (!opencodeBin) {
console.error(chalk.red("OpenCode CLI not found."));
console.log(
chalk.gray(
"Install OpenCode or set an override:\n export OPENCODE_BIN=/path/to/opencode\n opencode-sm --help"
)
);
process.exit(1);
return;
}
const opencode = spawn(opencodeBin, opencodeArgs, {
stdio: "inherit",
env: process.env
});
opencode.on("error", (err) => {
console.error(chalk.red("Failed to launch OpenCode CLI."));
if (err.code === "ENOENT") {
console.error(
chalk.gray("Not found. Set OPENCODE_BIN or install opencode.")
);
} else {
console.error(chalk.gray(`${err.message}`));
}
process.exit(1);
});
opencode.on("exit", async (code) => {
this.saveContext("OpenCode session ended", {
action: "session_end",
exitCode: code
});
if (this.config.tracingEnabled) {
const summary = trace.getExecutionSummary();
console.log();
console.log(chalk.gray("-".repeat(40)));
console.log(chalk.magenta("Trace Summary:"));
console.log(chalk.gray(summary));
}
if (this.config.worktreePath) {
console.log();
console.log(chalk.gray("-".repeat(40)));
console.log(chalk.magenta("Session ended in worktree:"));
console.log(chalk.gray(` ${this.config.worktreePath}`));
}
process.exit(code || 0);
});
process.on("SIGINT", () => {
this.saveContext("OpenCode session interrupted", {
action: "session_interrupt"
});
opencode.kill("SIGINT");
});
process.on("SIGTERM", () => {
this.saveContext("OpenCode session terminated", {
action: "session_terminate"
});
opencode.kill("SIGTERM");
});
}
}
program.name("opencode-sm").description("OpenCode with StackMemory context and worktree isolation").version("1.0.0");
const configCmd = program.command("config").description("Manage opencode-sm defaults");
configCmd.command("show").description("Show current default settings").action(() => {
const config = loadSMConfig();
console.log(chalk.magenta("opencode-sm defaults:"));
console.log(
` defaultWorktree: ${config.defaultWorktree ? chalk.green("true") : chalk.gray("false")}`
);
console.log(
` defaultTracing: ${config.defaultTracing ? chalk.green("true") : chalk.gray("false")}`
);
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",
tracing: "defaultTracing"
};
const configKey = keyMap[key];
if (!configKey) {
console.log(chalk.red(`Unknown key: ${key}`));
console.log(chalk.gray("Valid keys: worktree, tracing"));
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"));
});
program.option("-w, --worktree", "Create isolated worktree for this instance").option("-W, --no-worktree", "Disable worktree (override default)").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("--opencode-bin <path>", "Path to opencode CLI (or use OPENCODE_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 opencodeSM = new OpencodeSM();
const args = process.argv.slice(2);
await opencodeSM.run(args);
});
program.parse(process.argv);
//# sourceMappingURL=opencode-sm.js.map