@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
177 lines (176 loc) • 4.9 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, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { isProcessAlive } from "../utils/process-cleanup.js";
const DEFAULT_DAEMON_CONFIG = {
version: "1.0.0",
context: {
enabled: true,
interval: 15,
// 15 minutes
checkpointMessage: "Auto-checkpoint"
},
linear: {
enabled: false,
// Disabled by default, requires setup
interval: 60,
// 60 minutes
quietHours: { start: 22, end: 7 },
retryAttempts: 3,
retryDelay: 3e4
},
maintenance: {
enabled: true,
interval: 360,
// 6 hours
staleFrameThresholdDays: 30,
ftsRebuildInterval: 24,
// hours
embeddingBatchSize: 50,
vacuumInterval: 168,
// weekly
gcIntervalSeconds: 60
},
memory: {
enabled: true,
interval: 0.5,
// 30 seconds
ramThreshold: 0.9,
heapThreshold: 0.9,
cooldownMinutes: 10
},
fileWatch: {
enabled: false,
// Disabled by default
interval: 0,
// Not interval-based
paths: ["."],
extensions: [".ts", ".js", ".tsx", ".jsx", ".py", ".go", ".rs"],
ignore: ["node_modules", ".git", "dist", "build", ".stackmemory"],
debounceMs: 2e3
},
heartbeatInterval: 60,
// 1 minute
inactivityTimeout: 0,
// Disabled by default
logLevel: "info"
};
function getDaemonDir() {
const dir = join(homedir(), ".stackmemory", "daemon");
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
return dir;
}
function getLogsDir() {
const dir = join(homedir(), ".stackmemory", "logs");
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
return dir;
}
function getDaemonPaths() {
const daemonDir = getDaemonDir();
const logsDir = getLogsDir();
return {
pidFile: join(daemonDir, "daemon.pid"),
statusFile: join(daemonDir, "daemon.status"),
configFile: join(daemonDir, "config.json"),
logFile: join(logsDir, "daemon.log")
};
}
function loadDaemonConfig() {
const { configFile } = getDaemonPaths();
if (!existsSync(configFile)) {
return { ...DEFAULT_DAEMON_CONFIG };
}
try {
const content = readFileSync(configFile, "utf8");
const config = JSON.parse(content);
return {
...DEFAULT_DAEMON_CONFIG,
...config,
context: { ...DEFAULT_DAEMON_CONFIG.context, ...config.context },
linear: { ...DEFAULT_DAEMON_CONFIG.linear, ...config.linear },
maintenance: {
...DEFAULT_DAEMON_CONFIG.maintenance,
...config.maintenance
},
memory: { ...DEFAULT_DAEMON_CONFIG.memory, ...config.memory },
fileWatch: { ...DEFAULT_DAEMON_CONFIG.fileWatch, ...config.fileWatch }
};
} catch {
return { ...DEFAULT_DAEMON_CONFIG };
}
}
function saveDaemonConfig(config) {
const { configFile } = getDaemonPaths();
const currentConfig = loadDaemonConfig();
const newConfig = {
...currentConfig,
...config,
context: { ...currentConfig.context, ...config.context },
linear: { ...currentConfig.linear, ...config.linear },
maintenance: { ...currentConfig.maintenance, ...config.maintenance },
memory: { ...currentConfig.memory, ...config.memory },
fileWatch: { ...currentConfig.fileWatch, ...config.fileWatch }
};
writeFileSync(configFile, JSON.stringify(newConfig, null, 2));
}
function readDaemonStatus() {
const { statusFile, pidFile } = getDaemonPaths();
const defaultStatus = {
running: false,
services: {
context: { enabled: false },
linear: { enabled: false },
maintenance: { enabled: false },
memory: { enabled: false },
fileWatch: { enabled: false }
},
errors: []
};
if (!existsSync(pidFile)) {
return defaultStatus;
}
try {
const pidContent = readFileSync(pidFile, "utf8").trim();
const pid = parseInt(pidContent, 10);
if (!isProcessAlive(pid)) {
return defaultStatus;
}
if (!existsSync(statusFile)) {
return { ...defaultStatus, running: true, pid };
}
const content = readFileSync(statusFile, "utf8");
const status = JSON.parse(content);
return {
...status,
running: true,
pid,
uptime: status.startTime ? Date.now() - status.startTime : void 0
};
} catch {
return defaultStatus;
}
}
function writeDaemonStatus(status) {
const { statusFile } = getDaemonPaths();
const currentStatus = readDaemonStatus();
const newStatus = { ...currentStatus, ...status };
writeFileSync(statusFile, JSON.stringify(newStatus, null, 2));
}
export {
DEFAULT_DAEMON_CONFIG,
getDaemonDir,
getDaemonPaths,
getLogsDir,
loadDaemonConfig,
readDaemonStatus,
saveDaemonConfig,
writeDaemonStatus
};