#!/usr/bin/env node /** * patch-token-saver-6in1.js * Consolidate 6 token-saver hooks into a single dispatcher * Idempotent: sentinel file prevents re-execution */ 'use strict'; const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(process.env.USERPROFILE || process.env.HOME, '.claude'); const HOOKS_DIR = path.join(CLAUDE_ROOT, 'hooks'); const SETTINGS_PATH = path.join(CLAUDE_ROOT, 'settings.json'); const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state'); const SENTINEL = path.join(STATE_DIR, 'patch-token-saver-6in1.done'); const SOURCE = path.join(__dirname, 'token-saver-dispatcher-source.js'); const TARGET = path.join(HOOKS_DIR, 'token-saver-dispatcher.js'); const D = 'node C:/Users/leesu/.claude/hooks/token-saver-dispatcher.js'; if (fs.existsSync(SENTINEL)) { console.log('[SKIP] patch-token-saver-6in1 already applied'); process.exit(0); } const OLD_HOOKS = [ 'token-saver-model-advisor.js', 'token-saver-read-guard.js', 'token-saver-bash-limiter.js', 'token-saver-post-output-guard.js', 'token-saver-mcp-tracker.js', 'token-saver-session-report.js', ]; const REPLACEMENTS = [ ['node C:/Users/leesu/.claude/hooks/token-saver-model-advisor.js', D + ' --mode=model-advisor'], ['node C:/Users/leesu/.claude/hooks/token-saver-read-guard.js', D + ' --mode=read-guard'], ['node C:/Users/leesu/.claude/hooks/token-saver-bash-limiter.js', D + ' --mode=bash-limiter'], ['node C:/Users/leesu/.claude/hooks/token-saver-post-output-guard.js', D + ' --mode=post-output-guard'], ['node C:/Users/leesu/.claude/hooks/token-saver-mcp-tracker.js', D + ' --mode=mcp-tracker'], ['node C:/Users/leesu/.claude/hooks/token-saver-session-report.js', D + ' --mode=session-report'], ]; try { // Step 1: Copy dispatcher source to hooks/ if (!fs.existsSync(SOURCE)) { console.error('[FAIL] Source not found: ' + SOURCE); process.exit(1); } fs.copyFileSync(SOURCE, TARGET); console.log('[OK] Dispatcher copied to ' + TARGET); // Step 2: Backup and update settings.json const settingsBak = SETTINGS_PATH + '.bak-tse6in1-' + Date.now(); fs.copyFileSync(SETTINGS_PATH, settingsBak); console.log('[OK] Settings backup: ' + path.basename(settingsBak)); let settings = fs.readFileSync(SETTINGS_PATH, 'utf8'); let replaced = 0; for (const [oldCmd, newCmd] of REPLACEMENTS) { if (settings.includes(oldCmd)) { settings = settings.replace(oldCmd, newCmd); replaced++; } } if (replaced === 0) { console.log('[WARN] No replacements made in settings.json - may already be updated'); } else { const tmp = SETTINGS_PATH + '.tmp.' + process.pid; fs.writeFileSync(tmp, settings, 'utf8'); fs.renameSync(tmp, SETTINGS_PATH); console.log('[OK] settings.json updated: ' + replaced + '/6 commands replaced'); } // Step 3: Rename old hooks to .bak let backed = 0; for (const name of OLD_HOOKS) { const p = path.join(HOOKS_DIR, name); if (fs.existsSync(p)) { fs.renameSync(p, p + '.bak-6in1'); backed++; } } console.log('[OK] Old hooks renamed: ' + backed + '/6 → .bak-6in1'); // Step 4: Write sentinel if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); fs.writeFileSync(SENTINEL, JSON.stringify({ applied: new Date().toISOString(), replaced: replaced, backed: backed, settingsBackup: path.basename(settingsBak) }, null, 2), 'utf8'); console.log('[DONE] patch-token-saver-6in1 applied successfully'); } catch (err) { console.error('[FAIL] ' + err.message); process.exit(1); }