#!/usr/bin/env node /** * patch-token-saver-engine-v2.js · TSE v2.0 · 2026-04-27 * * P0. token-saver-post-output-guard.js — PostToolUse Read|Bash 超长输出提示 * P1. token-saver-mcp-tracker.js — PostToolUse mcp__ MCP 使用率追踪 * P2. token-saver-session-report.js — Stop 会话效率报告 * P3. pre-compact-handoff.js 增强 — PreCompact 智能保留指令 * * 幂等: sentinel 检查, 重复运行安全 * 用法: node scripts/patches/patch-token-saver-engine-v2.js */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(process.env.HOME || process.env.USERPROFILE, '.claude'); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const LIB_DIR = path.join(HOOKS_DIR, 'lib'); const SETTINGS_PATH = path.join(CLAUDE_ROOT, 'settings.json'); // ═══════════════════════════════════════════════════════════════════════ // P0: PostToolUse (Read|Bash) — 超长输出注入聚焦提示 // ═══════════════════════════════════════════════════════════════════════ const P0_CODE = `#!/usr/bin/env node /** * token-saver-post-output-guard.js · TSE Layer 4 · 2026-04-27 * PostToolUse (Read|Bash) · 超长输出检测, 注入聚焦提示 * 行为: fail-open, 不修改输出, 仅 additionalContext */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = require('./lib/root.js'); const readStdin = require('./lib/read-stdin.js'); const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); const STATE_PATH = path.join(STATE_DIR, 'tse-post-output-guard.json'); const THRESHOLD = 5000; const CRIT = 15000; const THROTTLE_MS = 60 * 1000; function loadState() { try { return fs.existsSync(STATE_PATH) ? JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) : {}; } catch { return {}; } } function saveState(s) { try { if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); const tmp = STATE_PATH + '.tmp.' + process.pid; fs.writeFileSync(tmp, JSON.stringify(s), 'utf8'); fs.renameSync(tmp, STATE_PATH); } catch {} } (async () => { try { const hd = await readStdin({ maxSize: 2 * 1024 * 1024 }); const tn = hd.tool_name; if (tn !== 'Read' && tn !== 'Bash') process.exit(0); const out = hd.tool_output; if (!out || typeof out !== 'string' || out.length < THRESHOLD) process.exit(0); const state = loadState(); const now = Date.now(); if (state[tn] && (now - state[tn]) < THROTTLE_MS) process.exit(0); state[tn] = now; for (const k of Object.keys(state)) { if (state[k] < now - 600000) delete state[k]; } saveState(state); const len = out.length; const tokens = Math.round(len / 3.5); const cr = len >= CRIT; var msg; if (tn === 'Read') { msg = cr ? '[TSE\\xb7POST_GUARD] Read ' + len + ' chars (' + tokens + ' tokens). \\u4ec5\\u63d0\\u53d6\\u4e0e\\u5f53\\u524d\\u4efb\\u52a1\\u76f4\\u63a5\\u76f8\\u5173\\u7684\\u4fe1\\u606f\\u3002\\u4e0d\\u8981\\u5728\\u56de\\u590d\\u4e2d\\u91cd\\u590d\\u5b8c\\u6574\\u6587\\u4ef6\\u5185\\u5bb9\\u3002\\u5982\\u9700\\u591a\\u6b21\\u5f15\\u7528\\uff0c\\u8bb0\\u4e0b\\u884c\\u53f7\\u7528 offset+limit \\u7cbe\\u786e\\u8bfb\\u53d6\\u3002' : '[TSE\\xb7POST_GUARD] Read ' + len + ' chars. \\u805a\\u7126\\u76f8\\u5173\\u6bb5\\u843d\\uff0c\\u907f\\u514d\\u5f15\\u7528\\u5927\\u6bb5\\u539f\\u6587\\u3002'; } else { msg = cr ? '[TSE\\xb7POST_GUARD] Bash ' + len + ' chars (' + tokens + ' tokens). \\u805a\\u7126\\u9519\\u8bef/\\u8b66\\u544a\\u884c\\u548c\\u6700\\u7ec8\\u72b6\\u6001\\uff0c\\u5ffd\\u7565\\u5197\\u4f59\\u65e5\\u5fd7\\u3002\\u5982\\u9700\\u5b8c\\u6574\\u5206\\u6790\\uff0c\\u5199\\u5165\\u6587\\u4ef6\\u540e\\u5206\\u6bb5\\u8bfb\\u53d6\\u3002' : '[TSE\\xb7POST_GUARD] Bash ' + len + ' chars. \\u805a\\u7126\\u5173\\u952e\\u8f93\\u51fa\\u884c\\uff0c\\u8df3\\u8fc7\\u5197\\u4f59\\u4fe1\\u606f\\u3002'; } process.stdout.write(JSON.stringify({ continue: true, hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: msg } })); process.exit(0); } catch { process.exit(0); } })(); `; // ═══════════════════════════════════════════════════════════════════════ // P1: PostToolUse (mcp__) — MCP 使用率追踪 // ═══════════════════════════════════════════════════════════════════════ const P1_CODE = `#!/usr/bin/env node /** * token-saver-mcp-tracker.js · TSE Layer 4 · 2026-04-27 * PostToolUse (mcp__) · MCP 工具使用率追踪 * 每 20 次 MCP 调用检查一次, 建议 /mcp-prune * 状态: session-state/tse-mcp-usage.json (跨会话累积) * 行为: fail-open */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = require('./lib/root.js'); const readStdin = require('./lib/read-stdin.js'); const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); const STATE_PATH = path.join(STATE_DIR, 'tse-mcp-usage.json'); const CHECK_INTERVAL = 20; function loadState() { try { if (fs.existsSync(STATE_PATH)) return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')); } catch {} return { version: 1, servers: {}, totalCalls: 0, trackingSince: new Date().toISOString() }; } function saveState(s) { try { if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); const tmp = STATE_PATH + '.tmp.' + process.pid; fs.writeFileSync(tmp, JSON.stringify(s, null, 2), 'utf8'); fs.renameSync(tmp, STATE_PATH); } catch {} } (async () => { try { const hd = await readStdin(); const tn = hd.tool_name || ''; if (!tn.startsWith('mcp__')) process.exit(0); var parts = tn.split('__'); if (parts.length < 3) process.exit(0); var server = parts[1]; var method = parts.slice(2).join('__'); var state = loadState(); state.totalCalls = (state.totalCalls || 0) + 1; state.lastCall = new Date().toISOString(); if (!state.servers[server]) { state.servers[server] = { count: 0, tools: {}, firstSeen: new Date().toISOString() }; } state.servers[server].count++; state.servers[server].lastUsed = new Date().toISOString(); state.servers[server].tools[method] = (state.servers[server].tools[method] || 0) + 1; saveState(state); if (state.totalCalls % CHECK_INTERVAL === 0) { var active = Object.keys(state.servers); var summary = active.map(function(k) { return k + '(' + state.servers[k].count + ')'; }).join(', '); var msg = '[TSE\\xb7MCP_TRACKER] MCP \\u8c03\\u7528\\u7edf\\u8ba1 (\\u7d2f\\u8ba1 ' + state.totalCalls + ' \\u6b21, ' + active.length + ' \\u4e2a\\u6d3b\\u8dc3\\u670d\\u52a1\\u5668)' + '\\n\\u6d3b\\u8dc3: ' + summary + '\\n\\u5efa\\u8bae: \\u8fd0\\u884c /mcp-prune \\u68c0\\u67e5\\u96f6\\u8c03\\u7528 MCP \\u670d\\u52a1\\u5668\\u5e76\\u8003\\u8651\\u7981\\u7528\\u4ee5\\u51cf\\u5c11\\u542f\\u52a8\\u5ef6\\u8fdf\\u3002'; process.stdout.write(JSON.stringify({ continue: true, hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: msg } })); } process.exit(0); } catch { process.exit(0); } })(); `; // ═══════════════════════════════════════════════════════════════════════ // P2: Stop — 会话效率报告 // ═══════════════════════════════════════════════════════════════════════ const P2_CODE = `#!/usr/bin/env node /** * token-saver-session-report.js · TSE Layer 5 · 2026-04-27 * Stop · 会话效率报告 * 解析 transcript, 统计效率指标, 写入 tse-efficiency-log.jsonl * 行为: fail-open */ 'use strict'; var fs = require('fs'); var path = require('path'); var CLAUDE_ROOT = require('./lib/root.js'); var readStdin = require('./lib/read-stdin.js'); var STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); var LOG_PATH = path.join(STATE_DIR, 'tse-efficiency-log.jsonl'); var MAX_TRANSCRIPT = 20 * 1024 * 1024; (async () => { try { var hookData = {}; try { hookData = await readStdin({ maxSize: 128 * 1024 }); } catch {} var tp = hookData.transcript_path; if (!tp || !fs.existsSync(tp)) process.exit(0); var stat = fs.statSync(tp); if (stat.size > MAX_TRANSCRIPT || stat.size < 100) process.exit(0); var raw = fs.readFileSync(tp, 'utf8'); var lines = raw.split('\\n').filter(Boolean); var m = { rounds: 0, compacts: 0, toolCalls: 0, mcpCalls: 0, agentCalls: 0, largeOutputs: 0, tseReadGuard: 0, tseBashLimiter: 0, tsePostGuard: 0, reads: 0, edits: 0, bashes: 0, models: {} }; for (var i = 0; i < lines.length; i++) { var obj; try { obj = JSON.parse(lines[i]); } catch { continue; } var content = (obj && obj.message && obj.message.content) || (obj && obj.content); var role = obj && obj.message && obj.message.role; var model = obj && obj.model; if (role === 'assistant') m.rounds++; if (model) m.models[model] = (m.models[model] || 0) + 1; if (!Array.isArray(content)) { if (typeof content === 'string') { if (content.indexOf('[TSE') !== -1) countTse(content, m); if (content.indexOf('PreCompact') !== -1) m.compacts++; } continue; } for (var j = 0; j < content.length; j++) { var part = content[j]; if (!part) continue; if (part.type === 'tool_use') { m.toolCalls++; var nm = part.name || ''; if (nm.startsWith('mcp__')) m.mcpCalls++; if (nm === 'Agent') m.agentCalls++; if (nm === 'Read') m.reads++; if (nm === 'Edit' || nm === 'Write') m.edits++; if (nm === 'Bash') m.bashes++; } if (part.type === 'tool_result') { var txt = typeof part.content === 'string' ? part.content : ''; if (txt.length > 5000) m.largeOutputs++; } var t = part.text || (typeof part === 'string' ? part : ''); if (typeof t === 'string' && t.indexOf('[TSE') !== -1) countTse(t, m); if (typeof t === 'string' && t.indexOf('PreCompact') !== -1) m.compacts++; } } var score = 80; score -= Math.min(m.compacts * 8, 24); score -= Math.min(m.largeOutputs * 2, 20); score -= Math.max(0, Math.floor((m.rounds - 40) * 0.5)); if (m.agentCalls > 0) score += 10; if (m.tseReadGuard + m.tseBashLimiter > 0 && m.tseReadGuard + m.tseBashLimiter < 8) score += 5; score = Math.max(0, Math.min(100, score)); var grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F'; var report = { ts: new Date().toISOString(), sid: hookData.session_id || 'unknown', score: score, grade: grade, metrics: m }; if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); fs.appendFileSync(LOG_PATH, JSON.stringify(report) + '\\n', 'utf8'); process.exit(0); } catch { process.exit(0); } })(); function countTse(text, m) { if (text.indexOf('READ_GUARD') !== -1) m.tseReadGuard++; if (text.indexOf('BASH_LIMITER') !== -1) m.tseBashLimiter++; if (text.indexOf('POST_GUARD') !== -1) m.tsePostGuard++; } `; // ═══════════════════════════════════════════════════════════════════════ // P3: lib/tse-retention-extractor.js — PreCompact 智能保留指令 // ═══════════════════════════════════════════════════════════════════════ const P3_LIB_CODE = `/** * tse-retention-extractor.js · TSE v2.0 · 2026-04-27 * 从 transcript 提取文件路径/函数名/TODO/决策点 * 用于 PreCompact 生成保留指令, 必须 <2s 完成 */ 'use strict'; var fs = require('fs'); function extract(transcriptPath) { if (!transcriptPath || !fs.existsSync(transcriptPath)) return null; var deadline = Date.now() + 2000; try { var stat = fs.statSync(transcriptPath); if (stat.size > 30 * 1024 * 1024) return null; var raw = fs.readFileSync(transcriptPath, 'utf8'); var lines = raw.split('\\n').filter(Boolean); var recent = lines.slice(-200); var filePaths = {}; var functions = {}; var todos = []; var decisions = []; for (var i = 0; i < recent.length; i++) { if (Date.now() > deadline) break; var obj; try { obj = JSON.parse(recent[i]); } catch { continue; } var content = (obj && obj.message && obj.message.content) || (obj && obj.content); if (!Array.isArray(content)) continue; for (var j = 0; j < content.length; j++) { var part = content[j]; if (!part) continue; if (part.type === 'tool_use') { var inp = part.input || {}; var fp = inp.file_path; if (fp && typeof fp === 'string') { filePaths[fp] = (filePaths[fp] || 0) + 1; } if (part.name === 'Grep' && inp.pattern) { var match = inp.pattern.match(/[a-zA-Z_][a-zA-Z0-9_]{3,40}/); if (match) functions[match[0]] = (functions[match[0]] || 0) + 1; } if (part.name === 'Edit' && inp.file_path) { filePaths[inp.file_path] = (filePaths[inp.file_path] || 0) + 2; } } var text = part.text || (typeof part === 'string' ? part : ''); if (typeof text === 'string' && text.length > 10) { var todoRe = /(?:TODO|FIXME|HACK)[:\\s](.{5,80})/gi; var tm; while ((tm = todoRe.exec(text)) !== null && todos.length < 5) { todos.push(tm[1].trim()); } var decRe = /(?:\\u51b3\\u5b9a|\\u9009\\u62e9|\\u65b9\\u6848|\\u786e\\u8ba4|decided|agreed|choose)[:\\s](.{5,80})/gi; var dm; while ((dm = decRe.exec(text)) !== null && decisions.length < 5) { decisions.push(dm[1].trim()); } } } } var out = ['[TSE\\xb7RETENTION] Compact \\u4fdd\\u7559\\u4ee5\\u4e0b\\u5173\\u952e\\u4e0a\\u4e0b\\u6587:']; var sorted = Object.entries(filePaths).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 10); if (sorted.length > 0) { out.push('## \\u6d3b\\u8dc3\\u6587\\u4ef6'); for (var si = 0; si < sorted.length; si++) { var bn = sorted[si][0].split(/[\\\\/]/).pop(); out.push('- ' + bn + ' (' + sorted[si][1] + 'x) ' + sorted[si][0]); } } var fnArr = Object.keys(functions).slice(0, 15); if (fnArr.length > 0) { out.push('## \\u5173\\u952e\\u6807\\u8bc6\\u7b26'); out.push(fnArr.join(', ')); } if (todos.length > 0) { out.push('## \\u5f85\\u529e'); for (var ti = 0; ti < todos.length; ti++) out.push('- ' + todos[ti]); } if (decisions.length > 0) { out.push('## \\u5173\\u952e\\u51b3\\u7b56'); for (var di = 0; di < decisions.length; di++) out.push('- ' + decisions[di]); } return out.length > 1 ? out.join('\\n') : null; } catch { return null; } } module.exports = { extract: extract }; `; // ═══════════════════════════════════════════════════════════════════════ // 安装逻辑 // ═══════════════════════════════════════════════════════════════════════ function writeHook(name, code) { const p = path.join(HOOKS_DIR, name); if (fs.existsSync(p)) { const existing = fs.readFileSync(p, 'utf8'); if (existing.includes('TSE Layer')) { console.log(' [skip] ' + name + ' (already installed)'); return; } const bak = p + '.bak.' + Date.now(); fs.copyFileSync(p, bak); console.log(' [backup] ' + name + ' -> ' + path.basename(bak)); } fs.writeFileSync(p, code, 'utf8'); console.log(' [write] ' + name); } function writeLib(name, code) { if (!fs.existsSync(LIB_DIR)) fs.mkdirSync(LIB_DIR, { recursive: true }); const p = path.join(LIB_DIR, name); if (fs.existsSync(p)) { const existing = fs.readFileSync(p, 'utf8'); if (existing.includes('TSE v2.0')) { console.log(' [skip] lib/' + name + ' (already installed)'); return; } fs.copyFileSync(p, p + '.bak.' + Date.now()); } fs.writeFileSync(p, code, 'utf8'); console.log(' [write] lib/' + name); } function patchPreCompact() { const p = path.join(HOOKS_DIR, 'pre-compact-handoff.js'); if (!fs.existsSync(p)) { console.log(' [skip] P3: pre-compact-handoff.js not found'); return; } let code = fs.readFileSync(p, 'utf8'); if (code.includes('TSE_V2_RETENTION')) { console.log(' [skip] P3: pre-compact-handoff.js (already patched)'); return; } // Backup const bak = p + '.bak.' + Date.now(); fs.copyFileSync(p, bak); console.log(' [backup] pre-compact-handoff.js -> ' + path.basename(bak)); // Normalize line endings for matching code = code.replace(/\r\n/g, '\n'); // Find the systemMessage anchor const OLD_MSG = "systemMessage: '[PRE_COMPACT] 上下文即将压缩。handoff.json 已写入。请在压缩前将当前任务的关键决策和待完成步骤总结到 handoff 记录中。'"; if (!code.includes(OLD_MSG)) { console.log(' [warn] P3: systemMessage anchor not found, skipping'); return; } // Replace the console.log block with retention-aware version const OLD_BLOCK = [ " console.log(JSON.stringify({", " continue: true,", " suppressOutput: false,", " systemMessage: '[PRE_COMPACT] 上下文即将压缩。handoff.json 已写入。请在压缩前将当前任务的关键决策和待完成步骤总结到 handoff 记录中。'", " }));" ].join('\n'); const NEW_BLOCK = [ " // TSE_V2_RETENTION: 智能保留指令", " let _retMsg = '[PRE_COMPACT] 上下文即将压缩。handoff.json 已写入。';", " try {", " const _ret = require('./lib/tse-retention-extractor.js').extract(hookData.transcript_path);", " if (_ret) _retMsg += '\\n' + _ret;", " } catch {}", " _retMsg += '\\n请保留关键上下文: 当前任务目标、活跃文件路径、关键决策和待完成步骤。';", " console.log(JSON.stringify({", " continue: true,", " suppressOutput: false,", " systemMessage: _retMsg", " }));" ].join('\n'); if (!code.includes(OLD_BLOCK)) { // Fallback: just replace the systemMessage value code = code.replace( OLD_MSG, "(function() { var r = ''; try { r = require('./lib/tse-retention-extractor.js').extract(hookData.transcript_path) || ''; } catch {} return '[PRE_COMPACT] 上下文即将压缩。handoff.json 已写入。' + (r ? '\\n' + r + '\\n' : '\\n') + '请保留关键上下文: 当前任务目标、活跃文件路径、关键决策和待完成步骤。'; })() // TSE_V2_RETENTION" ); console.log(' [patch] P3: pre-compact-handoff.js (fallback mode)'); } else { code = code.replace(OLD_BLOCK, NEW_BLOCK); console.log(' [patch] P3: pre-compact-handoff.js (+TSE_V2_RETENTION)'); } const tmp = p + '.tmp.' + process.pid; fs.writeFileSync(tmp, code, 'utf8'); fs.renameSync(tmp, p); } function patchSettings() { const raw = fs.readFileSync(SETTINGS_PATH, 'utf8'); const settings = JSON.parse(raw); if (raw.includes('token-saver-post-output-guard')) { console.log(' [skip] settings.json (TSE v2 hooks already registered)'); return; } // Backup const bak = SETTINGS_PATH + '.bak.' + Date.now(); fs.copyFileSync(SETTINGS_PATH, bak); console.log(' [backup] settings.json -> ' + path.basename(bak)); // P0: PostToolUse Read|Bash if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = []; settings.hooks.PostToolUse.push({ matcher: 'Read|Bash', hooks: [{ type: 'command', command: 'node C:/Users/leesu/.claude/hooks/token-saver-post-output-guard.js', timeout: 3000 }] }); // P1: PostToolUse mcp__ settings.hooks.PostToolUse.push({ matcher: 'mcp__', hooks: [{ type: 'command', command: 'node C:/Users/leesu/.claude/hooks/token-saver-mcp-tracker.js', timeout: 2000 }] }); // P2: Stop if (!settings.hooks.Stop) settings.hooks.Stop = []; settings.hooks.Stop.push({ hooks: [{ type: 'command', command: 'node C:/Users/leesu/.claude/hooks/token-saver-session-report.js', timeout: 5000 }] }); const tmp = SETTINGS_PATH + '.tmp.' + process.pid; fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8'); fs.renameSync(tmp, SETTINGS_PATH); console.log(' [patch] settings.json (+3 TSE v2 hooks: PostToolUse x2, Stop x1)'); } // ═══════════════════════════════════════════════════════════════════════ // Main // ═══════════════════════════════════════════════════════════════════════ console.log('\n=== Token Saver Engine (TSE) v2.0 Installation ===\n'); console.log('[Step 1/4] Writing P0-P2 hook files...'); writeHook('token-saver-post-output-guard.js', P0_CODE); writeHook('token-saver-mcp-tracker.js', P1_CODE); writeHook('token-saver-session-report.js', P2_CODE); console.log('\n[Step 2/4] Writing P3 lib module...'); writeLib('tse-retention-extractor.js', P3_LIB_CODE); console.log('\n[Step 3/4] Patching pre-compact-handoff.js (P3)...'); patchPreCompact(); console.log('\n[Step 4/4] Registering hooks in settings.json...'); patchSettings(); console.log('\n=== TSE v2.0 Installation Complete ==='); console.log('New hooks: 3 (post-output-guard + mcp-tracker + session-report)'); console.log('New lib: 1 (tse-retention-extractor.js)'); console.log('Patched: 1 (pre-compact-handoff.js +retention)'); console.log('Settings: +3 entries (PostToolUse x2, Stop x1)'); console.log('\nRestart Claude Code to activate.\n');