- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
370 lines
13 KiB
JavaScript
370 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* patch-token-saver-engine.js · TSE v1.0 · 2026-04-27
|
||
*
|
||
* 安装 Token Saver Engine (TSE) 三个 Hook:
|
||
* 1. token-saver-read-guard.js — PreToolUse Read 大文件拦截
|
||
* 2. token-saver-bash-limiter.js — PreToolUse Bash 输出截断
|
||
* 3. token-saver-model-advisor.js — UserPromptSubmit 模型建议
|
||
*
|
||
* 同时增强 context-pressure-monitor.js 添加 60% compact 指令阈值
|
||
*
|
||
* 幂等: sentinel 检查, 重复运行安全
|
||
* 用法: node scripts/patches/patch-token-saver-engine.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 SETTINGS_PATH = path.join(CLAUDE_ROOT, 'settings.json');
|
||
const SENTINEL = 'TSE_V1_INSTALLED';
|
||
|
||
// ─── Hook 1: Read Guard ───
|
||
const READ_GUARD_CODE = `#!/usr/bin/env node
|
||
/**
|
||
* token-saver-read-guard.js · TSE Layer 3 · 2026-04-27
|
||
* PreToolUse (Read) · 大文件读取拦截, 引导 offset/limit 分段
|
||
* 行为: 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-read-guard.json');
|
||
const WARN_BYTES = 10000;
|
||
const CRIT_BYTES = 35000;
|
||
const THROTTLE_MS = 3 * 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, null, 2), 'utf8');
|
||
fs.renameSync(tmp, STATE_PATH);
|
||
} catch {}
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
const hookData = await readStdin();
|
||
if (hookData.tool_name !== 'Read') process.exit(0);
|
||
const input = hookData.tool_input || {};
|
||
if (!input.file_path) process.exit(0);
|
||
if (input.offset !== undefined || input.limit !== undefined || input.pages !== undefined) process.exit(0);
|
||
|
||
let fileSize = 0;
|
||
try { fileSize = fs.statSync(input.file_path).size; } catch { process.exit(0); }
|
||
if (fileSize < WARN_BYTES) process.exit(0);
|
||
|
||
const state = loadState();
|
||
const now = Date.now();
|
||
const key = input.file_path.replace(/[\\\\\\/\\:]/g, '_');
|
||
if (state[key] && (now - state[key]) < THROTTLE_MS) process.exit(0);
|
||
state[key] = now;
|
||
for (const k of Object.keys(state)) { if (state[k] < now - 600000) delete state[k]; }
|
||
saveState(state);
|
||
|
||
const estLines = Math.round(fileSize * 30 / 1000);
|
||
const estTokens = Math.round(fileSize / 3.5);
|
||
const bn = path.basename(input.file_path);
|
||
|
||
let msg;
|
||
if (fileSize >= CRIT_BYTES) {
|
||
msg = '[TSE·READ_GUARD] ⚠️ 大文件: ' + bn + ' ≈' + estLines + '行 (' + estTokens + ' tokens)\\n' +
|
||
'你必须使用 offset+limit 分段读取。如需全文分析, 委托 Agent 子进程。';
|
||
} else {
|
||
msg = '[TSE·READ_GUARD] 提示: ' + bn + ' ≈' + estLines + '行 (' + estTokens + ' tokens). 建议用 offset+limit 分段。';
|
||
}
|
||
|
||
process.stdout.write(JSON.stringify({
|
||
continue: true,
|
||
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg }
|
||
}));
|
||
process.exit(0);
|
||
} catch { process.exit(0); }
|
||
})();
|
||
`;
|
||
|
||
// ─── Hook 2: Bash Output Limiter ───
|
||
const BASH_LIMITER_CODE = `#!/usr/bin/env node
|
||
/**
|
||
* token-saver-bash-limiter.js · TSE Layer 3 · 2026-04-27
|
||
* PreToolUse (Bash) · 查看型命令自动截断输出
|
||
* 仅截断 cat/find/tree/journalctl 等查看命令, 不动 build/git/ssh 执行命令
|
||
* 行为: fail-open, 通过 updatedInput 追加 | head -N
|
||
*/
|
||
'use strict';
|
||
|
||
const readStdin = require('./lib/read-stdin.js');
|
||
|
||
const VIEW_RE = [
|
||
/\\bcat\\s+\\S+/, /\\bdocker\\s+logs\\b/, /\\bjournalctl\\b/,
|
||
/\\bdmesg\\b/, /\\bps\\s+aux/, /\\bfind\\s+\\//,
|
||
/\\bls\\s+-[^\\s]*R/, /\\btree\\b/, /\\bsqlite3\\b.*\\.dump/,
|
||
/\\bnpm\\s+ls\\b/, /\\bpip\\s+(list|freeze)\\b/,
|
||
/\\bdpkg\\s+-l/, /\\bsystemctl\\s+list/,
|
||
];
|
||
const SKIP_RE = [
|
||
/\\|\\s*head\\b/, /\\|\\s*tail\\b/, /\\|\\s*grep\\b/,
|
||
/\\|\\s*awk\\b/, /\\|\\s*sed\\b/, /\\|\\s*wc\\b/,
|
||
/[>]/, /\\bgit\\s+(push|pull|fetch|clone|rebase|merge|commit)/,
|
||
/\\bnpm\\s+(install|run|build|test)/, /\\bpnpm\\s/,
|
||
/\\bdocker\\s+(build|run|push|compose)/, /\\bssh\\b/, /\\bscp\\b/,
|
||
/\\bcurl\\b/, /\\bwget\\b/, /\\bmake\\b/, /\\bcargo\\b/, /\\bgo\\s+(build|run|test)/,
|
||
];
|
||
|
||
(async () => {
|
||
try {
|
||
const hookData = await readStdin();
|
||
if (hookData.tool_name !== 'Bash') process.exit(0);
|
||
const cmd = (hookData.tool_input || {}).command || '';
|
||
if (!cmd || cmd.length < 5) process.exit(0);
|
||
|
||
for (const re of SKIP_RE) { if (re.test(cmd)) process.exit(0); }
|
||
|
||
let match = false;
|
||
for (const re of VIEW_RE) { if (re.test(cmd)) { match = true; break; } }
|
||
if (!match) process.exit(0);
|
||
|
||
const limit = /\\bfind\\s+\\/|journalctl|tree\\s+\\/|\\.dump/.test(cmd) ? 80 : 150;
|
||
const newCmd = cmd + ' 2>&1 | head -' + limit;
|
||
|
||
process.stdout.write(JSON.stringify({
|
||
continue: true,
|
||
hookSpecificOutput: {
|
||
hookEventName: 'PreToolUse',
|
||
updatedInput: { command: newCmd, description: (hookData.tool_input || {}).description },
|
||
additionalContext: '[TSE·BASH_LIMITER] 输出已截断至 ' + limit + ' 行。需完整输出请 > file 重定向。'
|
||
}
|
||
}));
|
||
process.exit(0);
|
||
} catch { process.exit(0); }
|
||
})();
|
||
`;
|
||
|
||
// ─── Hook 3: Model Advisor ──<E29480><E29480>
|
||
const MODEL_ADVISOR_CODE = `#!/usr/bin/env node
|
||
/**
|
||
* token-saver-model-advisor.js · TSE Layer 1 · 2026-04-27
|
||
* UserPromptSubmit · 分析 prompt 复杂度, 建议模型选择
|
||
* 节流: 每会话每复杂度级别仅提醒 1 次
|
||
* 行为: 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-model-advisor.json');
|
||
|
||
const SIMPLE = [
|
||
/^(查找|搜索|找到?|在哪|哪个文件)/,
|
||
/^(翻译|translate)\\b/i, /^(格式化|format)\\b/i,
|
||
/^(解释|explain|what is|what does)\\b/i,
|
||
/^(列出|list|show)\\b/i, /^(帮我看|看一下|看看)/,
|
||
/^(改个?名|rename)\\b/i, /^(运行|run|execute)\\s+(test|build|lint)\\b/i,
|
||
];
|
||
const COMPLEX = [
|
||
/(架构|architecture|设计方案|system design)/i,
|
||
/(全面审计|comprehensive audit|全栈审计)/i,
|
||
/(从零开始|from scratch|end.to.end)/i,
|
||
/(重构整个|refactor the entire|重新设计)/i,
|
||
/(安全审查|security review|红队|red.team)/i,
|
||
/(性能优化方案|performance optimization plan)/i,
|
||
/(对比分析|comparative analysis|trade.off)/i,
|
||
];
|
||
|
||
function classify(p) {
|
||
if (!p || p.length < 3) return null;
|
||
for (const re of SIMPLE) { if (re.test(p.trim())) return 'simple'; }
|
||
for (const re of COMPLEX) { if (re.test(p.trim())) return 'complex'; }
|
||
if (p.trim().length < 25) return 'simple';
|
||
return null;
|
||
}
|
||
|
||
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, null, 2), 'utf8');
|
||
fs.renameSync(tmp, STATE_PATH);
|
||
} catch {}
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
const hookData = await readStdin();
|
||
const prompt = hookData.prompt || '';
|
||
const sid = hookData.session_id || 'u';
|
||
const level = classify(prompt);
|
||
if (!level) process.exit(0);
|
||
|
||
const state = loadState();
|
||
const ss = state[sid] || { a: {}, ts: Date.now() };
|
||
if (ss.a[level]) process.exit(0);
|
||
ss.a[level] = true; ss.ts = Date.now();
|
||
state[sid] = ss;
|
||
const cutoff = Date.now() - 86400000;
|
||
for (const k of Object.keys(state)) { if (state[k].ts < cutoff) delete state[k]; }
|
||
saveState(state);
|
||
|
||
const msg = level === 'simple'
|
||
? '[TSE·MODEL_ADVISOR] 简单任务检测。如当前是 Opus, 建议 /model sonnet 或 /model haiku 以节省 5 倍额度。子 Agent 请指定 model: "haiku"<22><>'
|
||
: '[TSE·MODEL_ADVISOR] 复杂任务检测。Opus 适合规划阶段。建议: 方案确定后切回 Sonnet 执行, 子 Agent 用 Sonnet/Haiku。';
|
||
|
||
process.stdout.write(JSON.stringify({
|
||
continue: true, suppressOutput: true,
|
||
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: msg }
|
||
}));
|
||
process.exit(0);
|
||
} catch { process.exit(0); }
|
||
})();
|
||
`;
|
||
|
||
// ─── 安装逻辑 ───
|
||
|
||
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 patchSettings() {
|
||
const raw = fs.readFileSync(SETTINGS_PATH, 'utf8');
|
||
const settings = JSON.parse(raw);
|
||
|
||
if (raw.includes('token-saver-read-guard')) {
|
||
console.log(' [skip] settings.json (TSE 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));
|
||
|
||
// Add Read guard to PreToolUse
|
||
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
||
settings.hooks.PreToolUse.push({
|
||
matcher: 'Read',
|
||
hooks: [{
|
||
type: 'command',
|
||
command: 'node C:/Users/leesu/.claude/hooks/token-saver-read-guard.js',
|
||
timeout: 2000
|
||
}]
|
||
});
|
||
|
||
// Add Bash limiter to PreToolUse (separate from existing bash-precheck)
|
||
settings.hooks.PreToolUse.push({
|
||
matcher: 'Bash',
|
||
hooks: [{
|
||
type: 'command',
|
||
command: 'node C:/Users/leesu/.claude/hooks/token-saver-bash-limiter.js',
|
||
timeout: 1500
|
||
}]
|
||
});
|
||
|
||
// Add Model advisor to UserPromptSubmit
|
||
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
||
settings.hooks.UserPromptSubmit.push({
|
||
hooks: [{
|
||
type: 'command',
|
||
command: 'node C:/Users/leesu/.claude/hooks/token-saver-model-advisor.js',
|
||
timeout: 1500
|
||
}]
|
||
});
|
||
|
||
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 hooks registered)');
|
||
}
|
||
|
||
function patchPressureMonitor() {
|
||
const p = path.join(HOOKS_DIR, 'context-pressure-monitor.js');
|
||
if (!fs.existsSync(p)) {
|
||
console.log(' [skip] context-pressure-monitor.js not found');
|
||
return;
|
||
}
|
||
const code = fs.readFileSync(p, 'utf8');
|
||
|
||
if (code.includes('THRESHOLD_COMPACT') || code.includes('TSE_COMPACT')) {
|
||
console.log(' [skip] context-pressure-monitor.js (already enhanced)');
|
||
return;
|
||
}
|
||
|
||
const bak = p + '.bak.' + Date.now();
|
||
fs.copyFileSync(p, bak);
|
||
console.log(' [backup] context-pressure-monitor.js → ' + path.basename(bak));
|
||
|
||
// Insert COMPACT threshold between INFO and WARN
|
||
let patched = code.replace(
|
||
"const THRESHOLD_INFO = 0.50;",
|
||
"const THRESHOLD_INFO = 0.50;\nconst THRESHOLD_COMPACT = 0.60; // TSE_COMPACT: auto-compact directive"
|
||
);
|
||
|
||
// Enhance levelFor to include COMPACT
|
||
patched = patched.replace(
|
||
"if (ratio >= THRESHOLD_WARN) return 'WARN';",
|
||
"if (ratio >= THRESHOLD_WARN) return 'WARN';\n if (ratio >= THRESHOLD_COMPACT) return 'COMPACT';"
|
||
);
|
||
|
||
// Add COMPACT case to buildMessage
|
||
patched = patched.replace(
|
||
"case 'INFO':",
|
||
"case 'COMPACT':\n" +
|
||
" return head + '\\n[TSE·AUTO_COMPACT] 建议立即执行 /compact 以释放 context 空间。' +\n" +
|
||
" '\\n推荐 compact 指令: /compact 保留: 当前任务目标、已确定的方案、关键文件路径和行号。' +\n" +
|
||
" '\\n越早 compact 摘要质量越高 (60% 优于 83% 被动触发)。';\n" +
|
||
" case 'INFO':"
|
||
);
|
||
|
||
fs.writeFileSync(p, patched, 'utf8');
|
||
console.log(' [patch] context-pressure-monitor.js (+COMPACT @60%)');
|
||
}
|
||
|
||
// ─── Main ───
|
||
console.log('\\n=== Token Saver Engine (TSE) v1.0 Installation ===\\n');
|
||
|
||
console.log('[Step 1] Writing hook files...');
|
||
writeHook('token-saver-read-guard.js', READ_GUARD_CODE);
|
||
writeHook('token-saver-bash-limiter.js', BASH_LIMITER_CODE);
|
||
writeHook('token-saver-model-advisor.js', MODEL_ADVISOR_CODE);
|
||
|
||
console.log('\\n[Step 2] Enhancing context-pressure-monitor.js...');
|
||
patchPressureMonitor();
|
||
|
||
console.log('\\n[Step 3] Registering hooks in settings.json...');
|
||
patchSettings();
|
||
|
||
console.log('\\n=== TSE v1.0 Installation Complete ===');
|
||
console.log('New hooks: 3 (read-guard + bash-limiter + model-advisor)');
|
||
console.log('Enhanced: 1 (context-pressure-monitor +COMPACT@60%)');
|
||
console.log('\\nRestart Claude Code to activate.\\n');
|