#!/usr/bin/env node 'use strict'; /** * 补丁 — SessionStart Memory Audit Hook * * 目的: * 1. 创建 hooks/session-start-memory-audit.js (轻量日级记忆体检) * 2. 注册到 settings.json 的 UserPromptSubmit (Bookworm 无 SessionStart 键) * * 设计: * - 日级守卫: 今日已跑则 <5ms 快速返回 * - 静默默认: orphan<3 && ghost==0 && score>=80 完全不输出 * - 告警触发: 任一条件命中才 console.log 到 UserPromptSubmit additionalContext * - Fail-open: 任何异常 exit 0 * - Feature flag: .bookworm-features.json.memory_audit=false 关闭 * - Timeout: 3s (比 memory-audit --json 实测 <500ms 充分) * * 幂等: * - sentinel: SESSION_START_MEMORY_AUDIT_2026_04_25 * - hook 文件内容一致则跳过 * - settings.json 按 command 字符串检测 * * 原子性: * - hook 文件: tmp + rename * - settings.json: .bak.memory-audit-hook + tmp + JSON 解析自检 + rename * * 回滚: * 方式 1 (feature flag): echo '{"memory_audit":false}' > ~/.claude/.bookworm-features.json * 方式 2 (完整): cp settings.json.bak.memory-audit-hook settings.json * rm hooks/session-start-memory-audit.js */ const fs = require('fs'); const path = require('path'); const os = require('os'); const HOME = process.env.USERPROFILE || process.env.HOME || os.homedir(); const CLAUDE_ROOT = process.env.CLAUDE_HOME || (fs.existsSync(path.join(HOME, '.claude')) ? path.join(HOME, '.claude') : HOME); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const HOOK_TARGET = path.join(HOOKS_DIR, 'session-start-memory-audit.js'); const SETTINGS_FILE = path.join(CLAUDE_ROOT, 'settings.json'); const SETTINGS_BAK = SETTINGS_FILE + '.bak.memory-audit-hook'; const SENTINEL = 'SESSION_START_MEMORY_AUDIT_2026_04_25'; const HOOK_CMD = 'node C:/Users/leesu/.claude/hooks/session-start-memory-audit.js'; const HOOK_CONTENT = `#!/usr/bin/env node 'use strict'; /** * SessionStart Memory Audit Hook * sentinel: ${SENTINEL} * * 事件: UserPromptSubmit (Bookworm 无 SessionStart, 用 UserPromptSubmit + 日守卫) * 目的: 每日首次会话自动体检记忆文件健康度, 异常时作为 additionalContext 提醒 * * 预算: * - 今日已跑: <5ms (stamp 快速返回) * - 首次: <1000ms (memory-audit.js --json, execFileSync 3s timeout) * * 告警门槛 (静默默认, 命中才输出): * - orphan >= 3 目录有文件未索引 * - ghost >= 1 索引指向已删文件 * - health.score < 80 * * Feature flag: .bookworm-features.json.memory_audit=false 可关闭 * Fail-open: 任何异常 exit 0, 永不阻断用户输入 */ const fs = require('fs'); const path = require('path'); const os = require('os'); const HOME = process.env.USERPROFILE || process.env.HOME || os.homedir(); const CLAUDE_ROOT = process.env.CLAUDE_HOME || (fs.existsSync(path.join(HOME, '.claude')) ? path.join(HOME, '.claude') : HOME); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); const STAMP_FILE = path.join(DEBUG_DIR, '.last-memory-audit'); const FEATURE_FLAGS_FILE = path.join(CLAUDE_ROOT, '.bookworm-features.json'); const AUDIT_TOOL = path.join( CLAUDE_ROOT, 'projects', 'C--Users-leesu', 'memory', '_tools', 'memory-audit.js' ); const TODAY = new Date().toISOString().slice(0, 10); function safeExit() { process.exit(0); } function main() { try { // Feature flag try { if (fs.existsSync(FEATURE_FLAGS_FILE)) { const flags = JSON.parse(fs.readFileSync(FEATURE_FLAGS_FILE, 'utf8')); if (flags && flags.memory_audit === false) return safeExit(); } } catch {} // 日级守卫 if (fs.existsSync(STAMP_FILE)) { try { const last = fs.readFileSync(STAMP_FILE, 'utf8').trim(); if (last === TODAY) return safeExit(); } catch {} } // 工具存在性 if (!fs.existsSync(AUDIT_TOOL)) return safeExit(); // 运行审计 const { execFileSync } = require('child_process'); let report; try { const result = execFileSync(process.execPath, [AUDIT_TOOL, '--json'], { timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); report = JSON.parse(result); } catch { return safeExit(); } // 更新 stamp (不管结果, 避免失败时每次会话都重试) try { fs.mkdirSync(DEBUG_DIR, { recursive: true }); fs.writeFileSync(STAMP_FILE, TODAY); } catch {} // 判断是否需要告警 const h = (report && report.health) || {}; const orphan = h.orphanCount || 0; const ghost = h.ghostCount || 0; const oversize = h.oversizeCount || 0; const score = typeof h.score === 'number' ? h.score : 100; const needs = (orphan >= 3) || (ghost >= 1) || (score < 80); if (!needs) return safeExit(); // 输出为 UserPromptSubmit 的 additionalContext const lines = [ '[memory-audit] 记忆系统需要关注:', ' score=' + score + '/100 | orphan=' + orphan + ' ghost=' + ghost + ' oversize=' + oversize, ' 运行: node projects/C--Users-leesu/memory/_tools/memory-audit.js', ' 一键归档 orphan: ... --fix', ]; console.log(lines.join('\\n')); } catch { // fail-open } safeExit(); } if (require.main === module) main(); module.exports = { main }; `; function patchHookFile() { if (!fs.existsSync(HOOKS_DIR)) { console.error('[patch-memory-audit-hook] hooks 目录不存在:', HOOKS_DIR); process.exit(1); } if (fs.existsSync(HOOK_TARGET)) { const current = fs.readFileSync(HOOK_TARGET, 'utf8'); if (current.includes(SENTINEL) && current === HOOK_CONTENT) { console.log('[patch-memory-audit-hook] hook 已落地且内容一致,跳过'); return 'skipped'; } const bak = HOOK_TARGET + '.bak.memory-audit-hook'; fs.copyFileSync(HOOK_TARGET, bak); console.log('[patch-memory-audit-hook] 已备份旧 hook:', bak); } const tmp = HOOK_TARGET + '.tmp'; fs.writeFileSync(tmp, HOOK_CONTENT, 'utf8'); fs.renameSync(tmp, HOOK_TARGET); console.log('[patch-memory-audit-hook] 已写入 hook:', HOOK_TARGET); // 语法自检 try { const { execFileSync } = require('child_process'); execFileSync(process.execPath, ['--check', HOOK_TARGET], { stdio: 'pipe' }); console.log('[patch-memory-audit-hook] hook 语法自检 PASS'); } catch (e) { console.error('[patch-memory-audit-hook] 语法自检失败:', (e.stderr || e.message || '').toString().slice(0, 500)); process.exit(3); } return 'written'; } function patchSettings() { if (!fs.existsSync(SETTINGS_FILE)) { console.error('[patch-memory-audit-hook] settings.json 不存在'); process.exit(4); } const before = fs.readFileSync(SETTINGS_FILE, 'utf8'); const settings = JSON.parse(before); if (!settings.hooks) settings.hooks = {}; if (!Array.isArray(settings.hooks.UserPromptSubmit)) settings.hooks.UserPromptSubmit = []; const alreadyRegistered = settings.hooks.UserPromptSubmit.some(group => Array.isArray(group.hooks) && group.hooks.some(h => h.command === HOOK_CMD) ); if (alreadyRegistered) { console.log('[patch-memory-audit-hook] settings.json 已注册,跳过'); return 'skipped'; } if (!fs.existsSync(SETTINGS_BAK)) { fs.copyFileSync(SETTINGS_FILE, SETTINGS_BAK); console.log('[patch-memory-audit-hook] 已备份 settings.json:', SETTINGS_BAK); } settings.hooks.UserPromptSubmit.push({ hooks: [ { type: 'command', command: HOOK_CMD, timeout: 3000, }, ], }); const tmp = SETTINGS_FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8'); // JSON 解析自检 try { JSON.parse(fs.readFileSync(tmp, 'utf8')); } catch (e) { fs.unlinkSync(tmp); console.error('[patch-memory-audit-hook] 写出的 settings.json 无法解析,中止'); process.exit(5); } fs.renameSync(tmp, SETTINGS_FILE); console.log('[patch-memory-audit-hook] settings.json 已更新'); return 'updated'; } function main() { const hookResult = patchHookFile(); const settingsResult = patchSettings(); console.log(''); console.log('[patch-memory-audit-hook] sentinel:', SENTINEL); console.log('[patch-memory-audit-hook] hook:', hookResult); console.log('[patch-memory-audit-hook] settings:', settingsResult); console.log('[patch-memory-audit-hook] 完成。'); console.log(''); console.log('验证:'); console.log(' 1. 下次会话启动 (UserPromptSubmit 触发), 今日首次会跑记忆体检'); console.log(' 2. 健康时静默 (无输出)'); console.log(' 3. 异常时在 additionalContext 输出告警'); console.log(' 4. 查看 stamp: cat ~/.claude/debug/.last-memory-audit'); console.log(''); console.log('回滚:'); console.log(' 方式 1 (feature flag): echo \'{"memory_audit":false}\' > ~/.claude/.bookworm-features.json'); console.log(' 方式 2 (完整): cp ' + SETTINGS_BAK + ' ' + SETTINGS_FILE); console.log(' rm ' + HOOK_TARGET); process.exit(0); } main();