@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.
393 lines (391 loc) • 14.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 { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { existsSync, readFileSync, unlinkSync } from "fs";
import { spawn } from "child_process";
import { join } from "path";
import {
loadDaemonConfig,
saveDaemonConfig,
readDaemonStatus,
getDaemonPaths,
DEFAULT_DAEMON_CONFIG
} from "../../daemon/daemon-config.js";
function createDaemonCommand() {
const cmd = new Command("daemon").description("Manage StackMemory unified daemon for background services").addHelpText(
"after",
`
Examples:
stackmemory daemon start Start the daemon
stackmemory daemon stop Stop the daemon
stackmemory daemon status Check daemon status
stackmemory daemon logs View daemon logs
stackmemory daemon config Show/edit configuration
The daemon provides:
- Context auto-save (default: every 15 minutes)
- Linear sync (optional, if configured)
- File watch (optional, for change detection)
`
);
cmd.command("start").description("Start the unified daemon").option("--foreground", "Run in foreground (for debugging)").option("--save-interval <minutes>", "Context save interval in minutes").option("--linear-interval <minutes>", "Linear sync interval in minutes").option("--no-linear", "Disable Linear sync").option("--log-level <level>", "Log level (debug|info|warn|error)").action(async (options) => {
const status = readDaemonStatus();
if (status.running) {
console.log(
chalk.yellow("Daemon already running"),
chalk.gray(`(pid: ${status.pid})`)
);
return;
}
const spinner = ora("Starting unified daemon...").start();
try {
const args = ["daemon-run"];
if (options.saveInterval) {
args.push("--save-interval", options.saveInterval);
}
if (options.linearInterval) {
args.push("--linear-interval", options.linearInterval);
}
if (options.linear === false) {
args.push("--no-linear");
}
if (options.logLevel) {
args.push("--log-level", options.logLevel);
}
if (options.foreground) {
spinner.stop();
console.log(chalk.cyan("Running in foreground (Ctrl+C to stop)"));
const { UnifiedDaemon } = await import("../../daemon/unified-daemon.js");
const config = {};
if (options.saveInterval) {
config.context = {
enabled: true,
interval: parseInt(options.saveInterval, 10)
};
}
if (options.linearInterval) {
config.linear = {
enabled: true,
interval: parseInt(options.linearInterval, 10),
retryAttempts: 3,
retryDelay: 3e4
};
}
if (options.linear === false) {
config.linear = {
enabled: false,
interval: 60,
retryAttempts: 3,
retryDelay: 3e4
};
}
const daemon = new UnifiedDaemon(config);
await daemon.start();
return;
}
const daemonScript = getDaemonScriptPath();
if (!daemonScript) {
spinner.fail(chalk.red("Daemon script not found"));
return;
}
const daemonProcess = spawn("node", [daemonScript, ...args.slice(1)], {
detached: true,
stdio: "ignore",
env: { ...process.env }
});
daemonProcess.unref();
await new Promise((r) => setTimeout(r, 1e3));
const newStatus = readDaemonStatus();
if (newStatus.running) {
spinner.succeed(chalk.green("Daemon started"));
console.log(chalk.gray(`PID: ${newStatus.pid}`));
const services = [];
if (newStatus.services.context.enabled) services.push("context");
if (newStatus.services.linear.enabled) services.push("linear");
if (newStatus.services.fileWatch.enabled) services.push("file-watch");
if (services.length > 0) {
console.log(chalk.gray(`Services: ${services.join(", ")}`));
}
} else {
spinner.fail(chalk.red("Failed to start daemon"));
console.log(chalk.gray("Check logs: stackmemory daemon logs"));
}
} catch (error) {
spinner.fail(chalk.red("Failed to start daemon"));
console.log(chalk.gray(error.message));
}
});
cmd.command("stop").description("Stop the unified daemon").action(() => {
const status = readDaemonStatus();
if (!status.running || !status.pid) {
console.log(chalk.yellow("Daemon not running"));
return;
}
try {
process.kill(status.pid, "SIGTERM");
console.log(chalk.green("Daemon stopped"));
} catch (err) {
console.log(chalk.red("Failed to stop daemon"));
console.log(chalk.gray(err.message));
const { pidFile } = getDaemonPaths();
if (existsSync(pidFile)) {
unlinkSync(pidFile);
console.log(chalk.gray("Cleaned up stale PID file"));
}
}
});
cmd.command("restart").description("Restart the unified daemon").action(async () => {
const status = readDaemonStatus();
if (status.running && status.pid) {
try {
process.kill(status.pid, "SIGTERM");
await new Promise((r) => setTimeout(r, 1e3));
} catch {
}
}
const config = loadDaemonConfig();
const daemonScript = getDaemonScriptPath();
if (!daemonScript) {
console.log(chalk.red("Daemon script not found"));
return;
}
const args = [];
if (config.context.interval !== 15) {
args.push("--save-interval", String(config.context.interval));
}
if (!config.linear.enabled) {
args.push("--no-linear");
} else if (config.linear.interval !== 60) {
args.push("--linear-interval", String(config.linear.interval));
}
const daemonProcess = spawn("node", [daemonScript, ...args], {
detached: true,
stdio: "ignore"
});
daemonProcess.unref();
await new Promise((r) => setTimeout(r, 1e3));
const newStatus = readDaemonStatus();
if (newStatus.running) {
console.log(
chalk.green("Daemon restarted"),
chalk.gray(`(pid: ${newStatus.pid})`)
);
} else {
console.log(chalk.red("Failed to restart daemon"));
}
});
cmd.command("status").description("Check daemon status").action(() => {
const status = readDaemonStatus();
const config = loadDaemonConfig();
console.log(chalk.bold("\nStackMemory Unified Daemon\n"));
console.log(
`Status: ${status.running ? chalk.green("Running") : chalk.yellow("Stopped")}`
);
if (status.running) {
console.log(chalk.gray(` PID: ${status.pid}`));
if (status.uptime) {
const uptime = Math.round(status.uptime / 1e3);
const hours = Math.floor(uptime / 3600);
const mins = Math.floor(uptime % 3600 / 60);
const secs = uptime % 60;
console.log(chalk.gray(` Uptime: ${hours}h ${mins}m ${secs}s`));
}
}
console.log("");
console.log(chalk.bold("Services:"));
const ctx = status.services.context;
console.log(
` Context: ${ctx.enabled ? chalk.green("Enabled") : chalk.gray("Disabled")}`
);
if (ctx.enabled) {
console.log(chalk.gray(` Interval: ${config.context.interval} min`));
if (ctx.saveCount) {
console.log(chalk.gray(` Saves: ${ctx.saveCount}`));
}
if (ctx.lastRun) {
const ago = Math.round((Date.now() - ctx.lastRun) / 1e3 / 60);
console.log(chalk.gray(` Last save: ${ago} min ago`));
}
}
const lin = status.services.linear;
console.log(
` Linear: ${lin.enabled ? chalk.green("Enabled") : chalk.gray("Disabled")}`
);
if (lin.enabled) {
console.log(chalk.gray(` Interval: ${config.linear.interval} min`));
if (config.linear.quietHours) {
console.log(
chalk.gray(
` Quiet hours: ${config.linear.quietHours.start}:00 - ${config.linear.quietHours.end}:00`
)
);
}
if (lin.syncCount) {
console.log(chalk.gray(` Syncs: ${lin.syncCount}`));
}
}
const fw = status.services.fileWatch;
console.log(
` FileWatch: ${fw.enabled ? chalk.green("Enabled") : chalk.gray("Disabled")}`
);
if (status.errors && status.errors.length > 0) {
console.log("");
console.log(chalk.bold("Recent Errors:"));
status.errors.slice(-3).forEach((err) => {
console.log(chalk.red(` - ${err.slice(0, 80)}`));
});
}
if (!status.running) {
console.log("");
console.log(chalk.bold("To start: stackmemory daemon start"));
}
});
cmd.command("logs").description("View daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").option("-f, --follow", "Follow log output").option("--level <level>", "Filter by log level").action((options) => {
const { logFile } = getDaemonPaths();
if (!existsSync(logFile)) {
console.log(chalk.yellow("No log file found"));
console.log(
chalk.gray("Start the daemon first: stackmemory daemon start")
);
return;
}
if (options.follow) {
const tail = spawn("tail", ["-f", logFile], { stdio: "inherit" });
tail.on("error", () => {
console.log(chalk.red("Could not follow logs"));
});
return;
}
const content = readFileSync(logFile, "utf8");
const lines = content.trim().split("\n");
const count = parseInt(options.lines, 10);
let recent = lines.slice(-count);
if (options.level) {
const level = options.level.toUpperCase();
recent = recent.filter((line) => {
try {
const entry = JSON.parse(line);
return entry.level === level;
} catch {
return false;
}
});
}
console.log(chalk.bold(`
Daemon logs (${recent.length} lines):
`));
for (const line of recent) {
try {
const entry = JSON.parse(line);
const time = entry.timestamp.split("T")[1].split(".")[0];
const levelColor = entry.level === "ERROR" ? chalk.red : entry.level === "WARN" ? chalk.yellow : entry.level === "DEBUG" ? chalk.gray : chalk.white;
console.log(
`${chalk.gray(time)} ${levelColor(`[${entry.level}]`)} ${chalk.cyan(`[${entry.service}]`)} ${entry.message}`
);
} catch {
console.log(line);
}
}
});
cmd.command("config").description("Show or edit daemon configuration").option("--edit", "Open config in editor").option("--reset", "Reset to default configuration").option("--set <key=value>", "Set a config value").action((options) => {
const { configFile } = getDaemonPaths();
if (options.reset) {
saveDaemonConfig(DEFAULT_DAEMON_CONFIG);
console.log(chalk.green("Configuration reset to defaults"));
return;
}
if (options.edit) {
const editor = process.env["EDITOR"] || "vim";
spawn(editor, [configFile], { stdio: "inherit" });
return;
}
if (options.set) {
const [key, value] = options.set.split("=");
const config2 = loadDaemonConfig();
const parts = key.split(".");
let target = config2;
for (let i = 0; i < parts.length - 1; i++) {
if (target[parts[i]] && typeof target[parts[i]] === "object") {
target = target[parts[i]];
} else {
console.log(chalk.red(`Invalid config key: ${key}`));
return;
}
}
const lastKey = parts[parts.length - 1];
const parsed = value === "true" ? true : value === "false" ? false : isNaN(Number(value)) ? value : Number(value);
target[lastKey] = parsed;
saveDaemonConfig(config2);
console.log(chalk.green(`Set ${key} = ${value}`));
return;
}
const config = loadDaemonConfig();
console.log(chalk.bold("\nDaemon Configuration\n"));
console.log(chalk.gray(`File: ${configFile}`));
console.log("");
console.log(chalk.bold("Context Service:"));
console.log(` Enabled: ${config.context.enabled}`);
console.log(` Interval: ${config.context.interval} minutes`);
console.log("");
console.log(chalk.bold("Linear Service:"));
console.log(` Enabled: ${config.linear.enabled}`);
console.log(` Interval: ${config.linear.interval} minutes`);
if (config.linear.quietHours) {
console.log(
` Quiet hours: ${config.linear.quietHours.start}:00 - ${config.linear.quietHours.end}:00`
);
}
console.log("");
console.log(chalk.bold("File Watch:"));
console.log(` Enabled: ${config.fileWatch.enabled}`);
console.log(` Extensions: ${config.fileWatch.extensions.join(", ")}`);
console.log("");
console.log(chalk.bold("General:"));
console.log(` Heartbeat: ${config.heartbeatInterval} seconds`);
console.log(` Log level: ${config.logLevel}`);
});
cmd.action(() => {
const status = readDaemonStatus();
console.log(chalk.bold("\nStackMemory Daemon\n"));
console.log(
`Status: ${status.running ? chalk.green("Running") : chalk.yellow("Stopped")}`
);
if (!status.running) {
console.log("");
console.log(chalk.bold("Quick start:"));
console.log(" stackmemory daemon start Start background services");
} else {
console.log("");
console.log(chalk.bold("Commands:"));
console.log(" stackmemory daemon status View detailed status");
console.log(" stackmemory daemon logs View daemon logs");
console.log(" stackmemory daemon stop Stop the daemon");
}
});
return cmd;
}
function getDaemonScriptPath() {
const candidates = [
join(__dirname, "../../daemon/unified-daemon.js"),
join(process.cwd(), "dist/daemon/unified-daemon.js"),
join(
process.cwd(),
"node_modules/@stackmemoryai/stackmemory/dist/daemon/unified-daemon.js"
)
];
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}
return candidates[0];
}
var daemon_default = createDaemonCommand();
export {
createDaemonCommand,
daemon_default as default
};
//# sourceMappingURL=daemon.js.map