bookworm-smart-assistant/scripts/patches/patch-token-saver-engine-v2.js

584 lines
23 KiB
JavaScript
Raw Permalink Normal View History

#!/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');