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.

277 lines (276 loc) 8.31 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 { existsSync, writeFileSync, unlinkSync, appendFileSync, readFileSync } from "fs"; import { loadDaemonConfig, getDaemonPaths, writeDaemonStatus } from "./daemon-config.js"; import { DaemonContextService } from "./services/context-service.js"; import { DaemonLinearService } from "./services/linear-service.js"; class UnifiedDaemon { config; paths; contextService; linearService; heartbeatInterval; isShuttingDown = false; startTime = 0; constructor(config) { this.paths = getDaemonPaths(); this.config = { ...loadDaemonConfig(), ...config }; this.contextService = new DaemonContextService( this.config.context, (level, msg, data) => this.log(level, "context", msg, data) ); this.linearService = new DaemonLinearService( this.config.linear, (level, msg, data) => this.log(level, "linear", msg, data) ); } log(level, service, message, data) { const logLevel = level.toUpperCase(); const levels = ["DEBUG", "INFO", "WARN", "ERROR"]; const configLevel = this.config.logLevel.toUpperCase(); if (levels.indexOf(logLevel) < levels.indexOf(configLevel)) { return; } const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: logLevel, service, message, data }; const logLine = JSON.stringify(entry) + "\n"; try { appendFileSync(this.paths.logFile, logLine); } catch { console.error(`[${entry.timestamp}] ${level} [${service}]: ${message}`); } } checkIdempotency() { if (existsSync(this.paths.pidFile)) { try { const existingPid = readFileSync(this.paths.pidFile, "utf8").trim(); const pid = parseInt(existingPid, 10); try { process.kill(pid, 0); this.log("WARN", "daemon", "Daemon already running", { pid }); return false; } catch { this.log("INFO", "daemon", "Cleaning stale PID file", { pid }); unlinkSync(this.paths.pidFile); } } catch { try { unlinkSync(this.paths.pidFile); } catch { } } } return true; } writePidFile() { writeFileSync(this.paths.pidFile, process.pid.toString()); this.log("INFO", "daemon", "PID file created", { pid: process.pid, file: this.paths.pidFile }); } updateStatus() { const status = { running: true, pid: process.pid, startTime: this.startTime, uptime: Date.now() - this.startTime, services: { context: { enabled: this.config.context.enabled, lastRun: this.contextService.getState().lastSaveTime || void 0, saveCount: this.contextService.getState().saveCount }, linear: { enabled: this.config.linear.enabled, lastRun: this.linearService.getState().lastSyncTime || void 0, syncCount: this.linearService.getState().syncCount }, fileWatch: { enabled: this.config.fileWatch.enabled } }, errors: [ ...this.contextService.getState().errors.slice(-5), ...this.linearService.getState().errors.slice(-5) ] }; writeDaemonStatus(status); } setupSignalHandlers() { const handleSignal = (signal) => { this.log("INFO", "daemon", `Received ${signal}, shutting down`); this.shutdown(signal.toLowerCase()); }; process.on("SIGTERM", () => handleSignal("SIGTERM")); process.on("SIGINT", () => handleSignal("SIGINT")); process.on("SIGHUP", () => handleSignal("SIGHUP")); process.on("uncaughtException", (err) => { this.log("ERROR", "daemon", "Uncaught exception", { error: err.message, stack: err.stack }); this.shutdown("uncaught_exception"); }); process.on("unhandledRejection", (reason) => { this.log("ERROR", "daemon", "Unhandled rejection", { reason: String(reason) }); }); } cleanup() { try { if (existsSync(this.paths.pidFile)) { unlinkSync(this.paths.pidFile); this.log("INFO", "daemon", "PID file removed"); } } catch (e) { this.log("WARN", "daemon", "Failed to remove PID file", { error: String(e) }); } const finalStatus = { running: false, startTime: this.startTime, uptime: Date.now() - this.startTime, services: { context: { enabled: false, saveCount: this.contextService.getState().saveCount }, linear: { enabled: false, syncCount: this.linearService.getState().syncCount }, fileWatch: { enabled: false } }, errors: [] }; writeDaemonStatus(finalStatus); } shutdown(reason) { if (this.isShuttingDown) return; this.isShuttingDown = true; this.log("INFO", "daemon", "Daemon shutting down", { reason, uptime: Date.now() - this.startTime, contextSaves: this.contextService.getState().saveCount, linearSyncs: this.linearService.getState().syncCount }); if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = void 0; } this.contextService.stop(); this.linearService.stop(); this.cleanup(); const exitCode = reason === "sigterm" || reason === "sigint" || reason === "sighup" ? 0 : 1; process.exit(exitCode); } async start() { if (!this.checkIdempotency()) { this.log("INFO", "daemon", "Exiting - daemon already running"); process.exit(0); } this.startTime = Date.now(); this.writePidFile(); this.setupSignalHandlers(); this.log("INFO", "daemon", "Unified daemon started", { pid: process.pid, config: { context: this.config.context.enabled, linear: this.config.linear.enabled, fileWatch: this.config.fileWatch.enabled } }); this.contextService.start(); await this.linearService.start(); this.heartbeatInterval = setInterval(() => { this.updateStatus(); }, this.config.heartbeatInterval * 1e3); this.updateStatus(); } getStatus() { return { running: !this.isShuttingDown, pid: process.pid, startTime: this.startTime, uptime: Date.now() - this.startTime, services: { context: { enabled: this.config.context.enabled, lastRun: this.contextService.getState().lastSaveTime || void 0, saveCount: this.contextService.getState().saveCount }, linear: { enabled: this.config.linear.enabled, lastRun: this.linearService.getState().lastSyncTime || void 0, syncCount: this.linearService.getState().syncCount }, fileWatch: { enabled: this.config.fileWatch.enabled } }, errors: [ ...this.contextService.getState().errors, ...this.linearService.getState().errors ] }; } } if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("unified-daemon.js")) { const args = process.argv.slice(2); const config = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--save-interval" && args[i + 1]) { config.context = { enabled: true, interval: parseInt(args[i + 1], 10) }; i++; } else if (arg === "--linear-interval" && args[i + 1]) { config.linear = { enabled: true, interval: parseInt(args[i + 1], 10), retryAttempts: 3, retryDelay: 3e4 }; i++; } else if (arg === "--no-linear") { config.linear = { enabled: false, interval: 60, retryAttempts: 3, retryDelay: 3e4 }; } else if (arg === "--log-level" && args[i + 1]) { config.logLevel = args[i + 1]; i++; } } const daemon = new UnifiedDaemon(config); daemon.start().catch((err) => { console.error("Failed to start daemon:", err); process.exit(1); }); } export { UnifiedDaemon }; //# sourceMappingURL=unified-daemon.js.map