diff --git a/.claude/hooks/dist/session-start-continuity.mjs b/.claude/hooks/dist/session-start-continuity.mjs index 671d6630..2ae5d483 100644 --- a/.claude/hooks/dist/session-start-continuity.mjs +++ b/.claude/hooks/dist/session-start-continuity.mjs @@ -1,7 +1,8 @@ // src/session-start-continuity.ts import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; -import { execSync } from "child_process"; +import { execSync, spawn } from "child_process"; function buildHandoffDirName(sessionName, sessionId) { const uuidShort = sessionId.replace(/-/g, "").slice(0, 8); return `${sessionName}-${uuidShort}`; @@ -159,6 +160,62 @@ function getUnmarkedHandoffs() { return []; } } +function ensureMemoryDaemon() { + const pidFile = path.join(os.homedir(), ".claude", "memory-daemon.pid"); + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + if (!isNaN(pid)) { + try { + process.kill(pid, 0); + return null; + } catch { + } + } + } catch { + } + try { + fs.unlinkSync(pidFile); + } catch { + } + } + const hookDir = path.dirname(new URL(import.meta.url).pathname); + const possibleLocations = [ + // 1. Relative to compiled hook (development: .claude/hooks/dist/ → opc/scripts/core/) + path.resolve(hookDir, "..", "..", "..", "opc", "scripts", "core", "memory_daemon.py"), + // 2. In .claude/scripts/core/ (wizard-installed) + path.resolve(hookDir, "..", "scripts", "core", "memory_daemon.py"), + // 3. Global ~/.claude/scripts/core/ + path.join(os.homedir(), ".claude", "scripts", "core", "memory_daemon.py") + ]; + let daemonScript = null; + for (const loc of possibleLocations) { + if (fs.existsSync(loc)) { + daemonScript = loc; + break; + } + } + if (!daemonScript) { + console.error("Warning: memory_daemon.py not found, cannot auto-start memory daemon"); + return null; + } + try { + const cwd = path.resolve(daemonScript, "..", "..", ".."); + const logFile = path.join(os.homedir(), ".claude", "memory-daemon.log"); + const logFd = fs.openSync(logFile, "a"); + const child = spawn("uv", ["run", daemonScript, "start"], { + cwd, + stdio: ["ignore", logFd, logFd], + detached: true + }); + child.unref(); + fs.closeSync(logFd); + return "Memory daemon: Started"; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + return null; + } +} async function main() { const input = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); @@ -374,6 +431,10 @@ All handoffs in ${handoffDir}: } } } + const daemonStatus = ensureMemoryDaemon(); + if (daemonStatus) { + console.error(`\u2713 ${daemonStatus}`); + } const output = { result: "continue" }; if (message) { output.message = message; @@ -388,10 +449,10 @@ All handoffs in ${handoffDir}: console.log(JSON.stringify(output)); } async function readStdin() { - return new Promise((resolve) => { + return new Promise((resolve2) => { let data = ""; process.stdin.on("data", (chunk) => data += chunk); - process.stdin.on("end", () => resolve(data)); + process.stdin.on("end", () => resolve2(data)); }); } main().catch(console.error); diff --git a/.claude/hooks/src/session-start-continuity.ts b/.claude/hooks/src/session-start-continuity.ts index 8becbf59..a5024cb5 100644 --- a/.claude/hooks/src/session-start-continuity.ts +++ b/.claude/hooks/src/session-start-continuity.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; -import { execSync } from 'child_process'; +import { execSync, spawn } from 'child_process'; interface SessionStartInput { type?: 'startup' | 'resume' | 'clear' | 'compact'; // Legacy field @@ -308,6 +309,82 @@ function getUnmarkedHandoffs(): UnmarkedHandoff[] { } } +/** + * Start global memory extraction daemon if not running. + * + * The memory daemon monitors for stale sessions (heartbeat > 5 min) + * and automatically extracts learnings when sessions end. + * + * Returns status message or null if daemon was already running. + */ +function ensureMemoryDaemon(): string | null { + const pidFile = path.join(os.homedir(), '.claude', 'memory-daemon.pid'); + + // Check if already running + if (fs.existsSync(pidFile)) { + try { + const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); + if (!isNaN(pid)) { + // Check if process exists (kill -0) + try { + process.kill(pid, 0); + return null; // Already running + } catch { + // Process not found — stale PID file + } + } + } catch { + // Can't read PID file + } + // Remove stale PID file + try { fs.unlinkSync(pidFile); } catch { /* ignore */ } + } + + // Find daemon script + const hookDir = path.dirname(new URL(import.meta.url).pathname); + const possibleLocations = [ + // 1. Relative to compiled hook (development: .claude/hooks/dist/ → opc/scripts/core/) + path.resolve(hookDir, '..', '..', '..', 'opc', 'scripts', 'core', 'memory_daemon.py'), + // 2. In .claude/scripts/core/ (wizard-installed) + path.resolve(hookDir, '..', 'scripts', 'core', 'memory_daemon.py'), + // 3. Global ~/.claude/scripts/core/ + path.join(os.homedir(), '.claude', 'scripts', 'core', 'memory_daemon.py'), + ]; + + let daemonScript: string | null = null; + for (const loc of possibleLocations) { + if (fs.existsSync(loc)) { + daemonScript = loc; + break; + } + } + + if (!daemonScript) { + console.error('Warning: memory_daemon.py not found, cannot auto-start memory daemon'); + return null; + } + + try { + // cwd = opc/ directory (3 levels up from the script) + const cwd = path.resolve(daemonScript, '..', '..', '..'); + const logFile = path.join(os.homedir(), '.claude', 'memory-daemon.log'); + + const logFd = fs.openSync(logFile, 'a'); + const child = spawn('uv', ['run', daemonScript, 'start'], { + cwd, + stdio: ['ignore', logFd, logFd], + detached: true, + }); + child.unref(); + fs.closeSync(logFd); + + return 'Memory daemon: Started'; + } catch (e) { + console.error(`Warning: Failed to start memory daemon: ${e}`); + return null; + } +} + async function main() { const input: SessionStartInput = JSON.parse(await readStdin()); const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); @@ -557,6 +634,12 @@ async function main() { } } + // Ensure memory daemon is running (auto-extracts learnings from ended sessions) + const daemonStatus = ensureMemoryDaemon(); + if (daemonStatus) { + console.error(`✓ ${daemonStatus}`); + } + // Output with proper format per Claude Code docs const output: Record = { result: 'continue' };