@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
126 lines (111 loc) • 3.02 kB
JavaScript
/**
* Daemon Auto-Start Hook (PostToolUse)
*
* Checks if the unified daemon is running on each tool call.
* If not, spawns it in the background. Runs at most once per session
* (tracks via env var to avoid repeated checks).
*
* Must complete in <50ms — PID file check + optional spawn.
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const HOME = process.env.HOME || '/tmp';
const DAEMON_DIR = path.join(HOME, '.stackmemory', 'daemon');
const PID_FILE = path.join(DAEMON_DIR, 'daemon.pid');
// Track whether we already checked this session (avoid repeated spawns)
const STATE_KEY = 'STACKMEMORY_DAEMON_CHECKED';
const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
const STATE_FILE = path.join(
HOME,
'.stackmemory',
`daemon-check-${SESSION_ID}`
);
function isDaemonRunning() {
try {
if (!fs.existsSync(PID_FILE)) return false;
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
if (isNaN(pid)) return false;
process.kill(pid, 0); // signal 0 = check existence
return true;
} catch {
return false;
}
}
function findDaemonScript() {
// Check common locations for the daemon script
const candidates = [
// Global npm install
path.join(
__dirname,
'..',
'..',
'node_modules',
'@stackmemoryai',
'stackmemory',
'dist',
'daemon',
'unified-daemon.js'
),
// Homebrew global
path.join(
'/opt/homebrew/lib/node_modules/@stackmemoryai/stackmemory/dist/daemon/unified-daemon.js'
),
// ~/.stackmemory/bin (installed by postinstall)
path.join(HOME, '.stackmemory', 'bin', 'session-daemon.js'),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function alreadyChecked() {
try {
if (fs.existsSync(STATE_FILE)) {
const age = Date.now() - fs.statSync(STATE_FILE).mtimeMs;
// Re-check every 30 minutes
return age < 30 * 60 * 1000;
}
} catch {
// ignore
}
return false;
}
function markChecked() {
try {
const dir = path.dirname(STATE_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(STATE_FILE, String(Date.now()));
} catch {
// best effort
}
}
async function main() {
try {
// Skip if recently checked
if (alreadyChecked()) return;
markChecked();
// Already running? Done.
if (isDaemonRunning()) return;
// Try to spawn daemon
const script = findDaemonScript();
if (!script) return;
const child = spawn('node', [script], {
detached: true,
stdio: 'ignore',
env: { ...process.env },
});
child.unref();
} catch {
// Silent fail -- never block the agent
}
}
// Read stdin (required by hook protocol) then run
let input = '';
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
main();
});