@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
552 lines (540 loc) • 17.2 kB
JavaScript
#!/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 inquirer from "inquirer";
import chalk from "chalk";
import { homedir } from "os";
import { join } from "path";
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { execFileSync } from "child_process";
import { WorktreeManager } from "../../core/worktree/worktree-manager.js";
import { ProjectManager } from "../../core/projects/project-manager.js";
import { logger } from "../../core/monitoring/logger.js";
function registerOnboardingCommand(program) {
program.command("onboard").alias("setup").description("Interactive setup for StackMemory").option("--reset", "Reset all configurations and start fresh").action(async (options) => {
console.log(chalk.cyan("\n\u{1F680} Welcome to StackMemory Setup!\n"));
const configPath = join(homedir(), ".stackmemory");
if (existsSync(configPath) && !options.reset) {
const { proceed } = await inquirer.prompt([
{
type: "confirm",
name: "proceed",
message: "StackMemory is already configured. Do you want to reconfigure?",
default: false
}
]);
if (!proceed) {
console.log(chalk.yellow("\nSetup cancelled."));
return;
}
}
try {
const config = await runOnboarding();
await applyConfiguration(config);
console.log(
chalk.green("\n\u2705 StackMemory setup completed successfully!\n")
);
showNextSteps(config);
} catch (error) {
logger.error("Onboarding failed", error);
console.error(
chalk.red("\n\u274C Setup failed:"),
error.message
);
process.exit(1);
}
});
}
async function runOnboarding() {
const { setupType } = await inquirer.prompt([
{
type: "list",
name: "setupType",
message: "Choose your setup type:",
choices: [
{ name: "Basic (Recommended for most users)", value: "basic" },
{ name: "Advanced (Full customization)", value: "advanced" }
],
default: "basic"
}
]);
let config = {
setupType,
enableWorktrees: false,
worktreeIsolation: true,
enableProjects: true,
scanProjects: false,
enableLinear: false,
enableAnalytics: true,
defaultContextPath: join(homedir(), ".stackmemory"),
storageMode: "local"
};
const { storageMode } = await inquirer.prompt([
{
type: "list",
name: "storageMode",
message: "Where should StackMemory store data?",
choices: [
{ name: "Local (free, SQLite in ~/.stackmemory)", value: "local" },
{ name: "Hosted (paid, managed Postgres)", value: "hosted" }
],
default: "local"
}
]);
config.storageMode = storageMode;
if (storageMode === "hosted") {
const { hasAccount } = await inquirer.prompt([
{
type: "confirm",
name: "hasAccount",
message: "Do you already have a hosted account & connection string?",
default: false
}
]);
if (!hasAccount) {
try {
const signupUrl = "https://stackmemory.ai/hosted";
console.log(chalk.gray(`Opening signup page: ${signupUrl}`));
const mod = await import("open");
await mod.default(signupUrl);
} catch {
console.log(
chalk.yellow(
"Could not open browser automatically. Please sign up and obtain your DATABASE_URL."
)
);
}
}
const { databaseUrl } = await inquirer.prompt([
{
type: "password",
name: "databaseUrl",
message: "Paste your DATABASE_URL (postgres://...)",
validate: (input) => input.startsWith("postgres://") || input.startsWith("postgresql://") ? true : "Must start with postgres:// or postgresql://"
}
]);
config.databaseUrl = databaseUrl;
}
if (setupType === "basic") {
const basicAnswers = await inquirer.prompt([
{
type: "confirm",
name: "enableWorktrees",
message: "Enable Git worktree support? (Recommended for multi-branch workflows)",
default: false
},
{
type: "confirm",
name: "scanProjects",
message: "Scan and organize your existing projects?",
default: true
},
{
type: "confirm",
name: "enableLinear",
message: "Connect to Linear for task management?",
default: false
}
]);
config = { ...config, ...basicAnswers };
if (basicAnswers.enableLinear) {
const { linearApiKey } = await inquirer.prompt([
{
type: "password",
name: "linearApiKey",
message: "Enter your Linear API key:",
validate: (input) => input.length > 0 || "API key is required"
}
]);
config.linearApiKey = linearApiKey;
}
} else {
const advancedAnswers = await inquirer.prompt([
{
type: "confirm",
name: "enableWorktrees",
message: "Enable Git worktree support?",
default: false
}
]);
if (advancedAnswers.enableWorktrees) {
const worktreeAnswers = await inquirer.prompt([
{
type: "confirm",
name: "worktreeIsolation",
message: "Isolate contexts between worktrees? (Recommended)",
default: true
},
{
type: "confirm",
name: "autoDetect",
message: "Auto-detect worktrees when switching directories?",
default: true
},
{
type: "confirm",
name: "shareGlobal",
message: "Share global context across worktrees?",
default: false
},
{
type: "number",
name: "syncInterval",
message: "Context sync interval in minutes (0 to disable):",
default: 15,
validate: (input) => input >= 0 || "Must be 0 or positive"
}
]);
config = { ...config, ...advancedAnswers, ...worktreeAnswers };
}
const projectAnswers = await inquirer.prompt([
{
type: "confirm",
name: "enableProjects",
message: "Enable automatic project management?",
default: true
}
]);
if (projectAnswers.enableProjects) {
const projectDetailAnswers = await inquirer.prompt([
{
type: "confirm",
name: "scanProjects",
message: "Scan for existing projects now?",
default: true
},
{
type: "checkbox",
name: "scanPaths",
message: "Select directories to scan:",
choices: [
{ name: "~/Dev", value: join(homedir(), "Dev"), checked: true },
{ name: "~/dev", value: join(homedir(), "dev"), checked: true },
{
name: "~/Projects",
value: join(homedir(), "Projects"),
checked: true
},
{
name: "~/projects",
value: join(homedir(), "projects"),
checked: true
},
{ name: "~/Work", value: join(homedir(), "Work"), checked: false },
{ name: "~/code", value: join(homedir(), "code"), checked: true },
{
name: "~/Documents/GitHub",
value: join(homedir(), "Documents/GitHub"),
checked: false
}
],
when: () => projectDetailAnswers.scanProjects
}
]);
config = { ...config, ...projectAnswers, ...projectDetailAnswers };
}
const integrationAnswers = await inquirer.prompt([
{
type: "confirm",
name: "enableLinear",
message: "Enable Linear integration?",
default: false
},
{
type: "password",
name: "linearApiKey",
message: "Linear API key:",
when: (answers) => answers.enableLinear,
validate: (input) => input.length > 0 || "API key is required"
},
{
type: "confirm",
name: "enableAnalytics",
message: "Enable usage analytics? (Local only)",
default: true
},
{
type: "input",
name: "defaultContextPath",
message: "Default context storage path:",
default: join(homedir(), ".stackmemory"),
validate: (input) => input.length > 0 || "Path is required"
}
]);
config = { ...config, ...integrationAnswers };
}
return config;
}
async function applyConfiguration(config) {
const configPath = join(homedir(), ".stackmemory");
console.log(chalk.gray("\nCreating directory structure..."));
const dirs = [
configPath,
join(configPath, "contexts"),
join(configPath, "projects"),
join(configPath, "worktrees"),
join(configPath, "bin"),
join(configPath, "logs"),
join(configPath, "analytics")
];
for (const dir of dirs) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
if (config.enableWorktrees) {
console.log(chalk.gray("Configuring worktree support..."));
const worktreeManager = WorktreeManager.getInstance();
worktreeManager.saveConfig({
enabled: true,
autoDetect: true,
isolateContexts: config.worktreeIsolation,
shareGlobalContext: false,
syncInterval: 15
});
const worktrees = worktreeManager.detectWorktrees();
if (worktrees.length > 0) {
console.log(chalk.green(` \u2713 Found ${worktrees.length} worktree(s)`));
worktrees.forEach((wt) => {
console.log(chalk.gray(` - ${wt.branch} at ${wt.path}`));
});
}
}
if (config.enableProjects && config.scanProjects) {
console.log(chalk.gray("Scanning for projects..."));
const projectManager = ProjectManager.getInstance();
const scanPaths = config.scanPaths || [
join(homedir(), "Dev"),
join(homedir(), "dev"),
join(homedir(), "Projects"),
join(homedir(), "projects"),
join(homedir(), "code")
];
await projectManager.scanAndCategorizeAllProjects(
scanPaths.filter((p) => existsSync(p))
);
const projects = projectManager.getAllProjects();
console.log(chalk.green(` \u2713 Found ${projects.length} project(s)`));
const byType = {};
projects.forEach((p) => {
byType[p.accountType] = (byType[p.accountType] || 0) + 1;
});
Object.entries(byType).forEach(([type, count]) => {
console.log(chalk.gray(` - ${type}: ${count} project(s)`));
});
}
if (config.enableLinear && config.linearApiKey) {
console.log(chalk.gray("Configuring Linear integration..."));
const linearConfig = {
apiKey: config.linearApiKey,
autoSync: true,
syncInterval: 3e5
// 5 minutes
};
writeFileSync(
join(configPath, "linear-config.json"),
JSON.stringify(linearConfig, null, 2)
);
console.log(chalk.green(" \u2713 Linear configured"));
}
const mainConfig = {
version: "1.0.0",
setupCompleted: (/* @__PURE__ */ new Date()).toISOString(),
features: {
worktrees: config.enableWorktrees,
projects: config.enableProjects,
linear: config.enableLinear,
analytics: config.enableAnalytics
},
paths: {
default: config.defaultContextPath
},
database: {
mode: config.storageMode,
...config.databaseUrl ? { url: config.databaseUrl } : {}
}
};
writeFileSync(
join(configPath, "config.json"),
JSON.stringify(mainConfig, null, 2)
);
if (config.storageMode === "hosted" && config.databaseUrl) {
try {
const envFile = join(configPath, "railway.env");
writeFileSync(
envFile,
`# StackMemory hosted DB
DATABASE_URL=${config.databaseUrl}
`
);
console.log(
chalk.green(
" \u2713 Saved hosted DB settings to ~/.stackmemory/railway.env"
)
);
console.log(
chalk.gray(
" Tip: export DATABASE_URL from this file in your shell profile."
)
);
} catch {
console.log(chalk.yellow(" \u26A0 Could not write hosted DB env file"));
}
}
const binPath = "/usr/local/bin/claude-sm";
const sourcePath = join(configPath, "bin", "stackmemory");
try {
const wrapperScript = `#!/bin/bash
# StackMemory CLI wrapper with worktree support
CURRENT_DIR=$(pwd)
# Auto-detect worktree if enabled
if [ -f ~/.stackmemory/worktree-config.json ]; then
WORKTREE_ENABLED=$(grep '"enabled": true' ~/.stackmemory/worktree-config.json)
if [ ! -z "$WORKTREE_ENABLED" ]; then
# Check if we're in a git worktree
if git worktree list &>/dev/null; then
export SM_WORKTREE_PATH="$CURRENT_DIR"
fi
fi
fi
# Run StackMemory with context
exec stackmemory "$@"
`;
writeFileSync(sourcePath, wrapperScript);
execFileSync("chmod", ["+x", sourcePath]);
if (!existsSync(binPath)) {
execFileSync("ln", ["-s", sourcePath, binPath]);
console.log(chalk.green(" \u2713 Created claude-sm command"));
}
} catch {
console.log(
chalk.yellow(" \u26A0 Could not create claude-sm symlink (may need sudo)")
);
}
const codexBinPath = "/usr/local/bin/codex-sm";
const codexSourcePath = join(configPath, "bin", "codex-sm");
try {
const codexWrapper = `#!/bin/bash
# Codex CLI wrapper with StackMemory integration
# Usage: codex-sm [--auto-sync] [--sync-interval=MINUTES] [args...]
# Flags
AUTO_SYNC=false
SYNC_INTERVAL=5
for arg in "$@"; do
case $arg in
--auto-sync)
AUTO_SYNC=true
shift
;;
--sync-interval=*)
SYNC_INTERVAL="\${arg#*=}"
shift
;;
esac
done
# Auto-initialize StackMemory if in git repo without it
if [ -d ".git" ] && [ ! -d ".stackmemory" ]; then
echo "\u{1F4E6} Initializing StackMemory for this project..."
# Quiet init (no unsupported flags)
stackmemory init >/dev/null 2>&1 || true
fi
# Load existing context if available
if [ -d ".stackmemory" ]; then
echo "\u{1F9E0} Loading StackMemory context..."
stackmemory status --brief 2>/dev/null || true
fi
# Start Linear auto-sync in background if requested
SYNC_PID=""
if [ "$AUTO_SYNC" = true ] && [ -n "$LINEAR_API_KEY" ]; then
echo "\u{1F504} Starting Linear auto-sync (${SYNC_INTERVAL}min intervals)..."
(
while true; do
sleep $((SYNC_INTERVAL * 60))
if [ -d ".stackmemory" ]; then
stackmemory linear sync --quiet 2>/dev/null || true
fi
done
) &
SYNC_PID=$!
fi
cleanup() {
echo ""
echo "\u{1F4DD} Saving StackMemory context..."
# Kill auto-sync if running
if [ -n "$SYNC_PID" ] && kill -0 $SYNC_PID 2>/dev/null; then
echo "\u{1F6D1} Stopping auto-sync..."
kill $SYNC_PID 2>/dev/null || true
fi
# Save project status and final sync
if [ -d ".stackmemory" ]; then
stackmemory status 2>/dev/null
if [ -n "$LINEAR_API_KEY" ]; then
echo "\u{1F504} Final Linear sync..."
stackmemory linear sync 2>/dev/null
fi
echo "\u2705 StackMemory context saved"
fi
}
trap cleanup EXIT INT TERM
# Run Codex CLI
if command -v codex &> /dev/null; then
codex "$@"
elif command -v codex-cli &> /dev/null; then
codex-cli "$@"
else
echo "\u274C Codex CLI not found. Please install it first."
echo " See: https://github.com/openai/codex-cli"
exit 1
fi
`;
writeFileSync(codexSourcePath, codexWrapper);
execFileSync("chmod", ["+x", codexSourcePath]);
if (!existsSync(codexBinPath)) {
execFileSync("ln", ["-s", codexSourcePath, codexBinPath]);
console.log(chalk.green(" \u2713 Created codex-sm command"));
}
} catch {
console.log(
chalk.yellow(" \u26A0 Could not create codex-sm symlink (may need sudo)")
);
}
}
function showNextSteps(config) {
console.log(chalk.cyan("\u{1F389} Next Steps:\n"));
console.log("1. Initialize StackMemory in your project:");
console.log(chalk.gray(" cd your-project"));
console.log(chalk.gray(" stackmemory init\n"));
if (config.enableWorktrees) {
console.log("2. Create a new worktree:");
console.log(
chalk.gray(
" git worktree add -b feature/new-feature ../project-feature"
)
);
console.log(chalk.gray(" cd ../project-feature"));
console.log(
chalk.gray(
" stackmemory status # Isolated context for this worktree\n"
)
);
}
console.log("3. Use with Claude:");
console.log(chalk.gray(" claude-sm # Or use stackmemory directly\n"));
console.log("4. Use with Codex:");
console.log(chalk.gray(" codex-sm # Codex + StackMemory integration\n"));
if (config.enableLinear) {
console.log("5. Sync with Linear:");
console.log(chalk.gray(" stackmemory linear sync\n"));
}
console.log("For more help:");
console.log(chalk.gray(" stackmemory --help"));
console.log(chalk.gray(" stackmemory projects --help"));
if (config.enableWorktrees) {
console.log(chalk.gray(" stackmemory worktree --help"));
}
}
export {
registerOnboardingCommand
};