@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
JavaScript
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