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