bookworm-smart-assistant/scripts/patches/patch-phase1-t1.4-mcp-prune.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

337 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
/**
* Phase 1 · T1.4 补丁 — /mcp-prune 命令
*
* 产出:
* 1. scripts/mcp-prune.js — 剪枝分析 CLI (纯只读 + 生成 plan)
* 2. skills/mcp-prune/SKILL.md — 触发器 /mcp-prune
*
* 安全设计:
* - 默认只报告 (read-only)
* - --plan: 生成 mcp-prune-plan-<date>.json
* - --confirm: 输出用户手动 apply 的指令,不自动修改 .claude.json
* - 永远不修改用户核心配置
*
* 幂等:
* - sentinel: PHASE1_T1_4_MCP_PRUNE_2026_04_24
* - 已存在且一致则跳过
*
* 回滚:
* - 删除 scripts/mcp-prune.js + skills/mcp-prune/ 目录
*/
const fs = require('fs');
const path = require('path');
const CLAUDE_ROOT = path.join(__dirname, '..', '..');
const SENTINEL = 'PHASE1_T1_4_MCP_PRUNE_2026_04_24';
const SCRIPT_TARGET = path.join(CLAUDE_ROOT, 'scripts', 'mcp-prune.js');
const SKILL_DIR = path.join(CLAUDE_ROOT, 'skills', 'mcp-prune');
const SKILL_TARGET = path.join(SKILL_DIR, 'SKILL.md');
const SCRIPT_CONTENT = `#!/usr/bin/env node
'use strict';
/**
* /mcp-prune — MCP 剪枝分析工具 (Phase 1 · T1.4)
* sentinel: ${SENTINEL}
*
* 职责:
* - 基于 mcp-usage-tracker 的数据识别低频 MCP
* - 交叉 mcp-critical-allowlist.json 保护救命 MCP
* - 生成剪枝 plan 文件 (JSON)
* - 输出用户手动 apply 的指令
*
* 用法:
* node scripts/mcp-prune.js # 默认: 只报告
* node scripts/mcp-prune.js --days 30 # 用 30 天窗口
* node scripts/mcp-prune.js --plan # 写入 mcp-prune-plan-<date>.json
* node scripts/mcp-prune.js --confirm # 输出用户 apply 的完整步骤
*
* 安全:
* - 永不修改 ~/.claude.json (用户核心配置)
* - 永不自动删除 MCP
* - --confirm 只打印操作指南,用户自行执行
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const HOME = process.env.USERPROFILE || process.env.HOME || os.homedir();
const CLAUDE_ROOT = process.env.CLAUDE_HOME ||
(fs.existsSync(path.join(HOME, '.claude')) ? path.join(HOME, '.claude') : HOME);
const TRACKER_PATH = path.join(CLAUDE_ROOT, 'scripts', 'mcp-usage-tracker.js');
const ALLOWLIST_FILE = path.join(CLAUDE_ROOT, 'mcp-critical-allowlist.json');
const GLOBAL_CONFIG = path.join(HOME, '.claude.json');
function parseArgs(argv) {
const args = { days: 30, plan: false, confirm: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--days' && argv[i + 1]) { args.days = parseInt(argv[++i], 10) || 30; }
else if (a === '--plan') { args.plan = true; }
else if (a === '--confirm') { args.confirm = true; }
else if (a === '-h' || a === '--help') { args.help = true; }
}
return args;
}
function safeReadJson(file, fallback) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
catch { return fallback; }
}
function runAnalysis(days) {
const tracker = require(TRACKER_PATH);
const allowlist = safeReadJson(ALLOWLIST_FILE, { critical: [] });
const criticalSet = new Set((allowlist.critical || []).map(c => c.name));
const cfg = safeReadJson(GLOBAL_CONFIG, { mcpServers: {} });
const allServers = Object.keys(cfg.mcpServers || {});
const events = tracker.collectMcpEvents(days);
const stats = tracker.aggregate(events, allServers);
const candidates = tracker.identifyPruneCandidates(stats, criticalSet);
return { stats, candidates, criticalSet, allServers, cfg, days, events };
}
function report(result) {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' /mcp-prune — 剪枝分析 · ' + result.days + '天窗口');
console.log('═══════════════════════════════════════════════════════════');
console.log('');
console.log('总 MCP 数: ' + result.allServers.length);
console.log('★critical (永不剪枝): ' + result.criticalSet.size);
console.log('活跃 MCP (>0 调用): ' + Object.values(result.stats).filter(s => s.totalCalls > 0).length);
console.log('剪枝候选 (0 调用): ' + result.candidates.length);
console.log('');
if (result.candidates.length === 0) {
console.log('✅ 没有剪枝候选。所有 MCP 都在最近 ' + result.days + ' 天被使用或在 critical 清单。');
return;
}
console.log('剪枝候选清单:');
console.log('─'.repeat(60));
for (const c of result.candidates) {
console.log(' - ' + c.server.padEnd(24) + ' ' + c.reason);
}
console.log('');
const estTokens = result.candidates.length * 200;
console.log('预估节省 (tokens/cold start): ≈ ' + estTokens + ' tokens');
console.log(' (每 MCP schema 均值 200 tokens × ' + result.candidates.length + ')');
}
function writePlan(result, planFile) {
const plan = {
schema_version: 1,
generated: new Date().toISOString(),
tool: 'mcp-prune',
windowDays: result.days,
totalMcps: result.allServers.length,
critical: Array.from(result.criticalSet),
candidates: result.candidates.map(c => ({
server: c.server,
reason: c.reason,
recommendedAction: 'REMOVE from ~/.claude.json mcpServers (manual)',
backupHint: 'cp ~/.claude.json ~/.claude.json.bak-before-prune-' + new Date().toISOString().slice(0, 10)
})),
note: '此 plan 仅供参考mcp-prune 绝不自动修改 .claude.json。用户需人工 apply。'
};
const tmp = planFile + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(plan, null, 2), 'utf8');
fs.renameSync(tmp, planFile);
console.log('');
console.log('✅ Plan 写入: ' + planFile);
return plan;
}
function printApplyInstructions(plan) {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' 用户 Apply 步骤 (请人工执行)');
console.log('═══════════════════════════════════════════════════════════');
console.log('');
console.log('1. 备份原配置:');
console.log(' Copy-Item ~/.claude.json ~/.claude.json.bak-$(Get-Date -Format yyyy-MM-dd)');
console.log('');
console.log('2. 用编辑器打开 ~/.claude.json在 mcpServers 下删除以下键:');
for (const c of plan.candidates) {
console.log(' • ' + c.server);
}
console.log('');
console.log('3. 重启 Claude Code 使改动生效');
console.log('');
console.log('4. 若需恢复某个 MCP, 从备份文件找回对应 JSON 片段即可');
console.log('');
console.log('⚠ 本工具不会自动修改 .claude.json — 这是故意设计的安全边界。');
}
function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log('用法: node scripts/mcp-prune.js [--days N] [--plan] [--confirm]');
console.log(' 默认: 只报告');
console.log(' --plan: 生成 mcp-prune-plan-<date>.json');
console.log(' --confirm: 打印用户手动 apply 的完整步骤');
process.exit(0);
}
if (!fs.existsSync(TRACKER_PATH)) {
console.error('错误: mcp-usage-tracker.js 不存在,需先完成 Phase 1 T1.1');
process.exit(2);
}
const result = runAnalysis(args.days);
report(result);
if (result.candidates.length === 0) {
process.exit(0);
}
if (args.plan || args.confirm) {
const planFile = path.join(CLAUDE_ROOT, 'mcp-prune-plan-' + new Date().toISOString().slice(0, 10) + '.json');
const plan = writePlan(result, planFile);
if (args.confirm) {
printApplyInstructions(plan);
}
} else {
console.log('');
console.log('提示: 运行 \\'--plan\\' 生成 plan 文件, \\'--confirm\\' 查看完整 apply 步骤');
}
process.exit(0);
}
if (require.main === module) main();
`;
const SKILL_CONTENT = `---
name: mcp-prune
version: 1.0.0
description: |
MCP 剪枝分析工具 (Phase 1 · T1.4)。基于 mcp-usage-tracker 的使用率数据,
识别最近 N 天零调用且非 critical 的 MCP 候选,生成剪枝 plan 文件。
绝不自动修改 ~/.claude.json用户需人工 apply。
触发词: "mcp-prune", "剪枝 MCP", "MCP 剪枝", "清理 MCP", "精简 MCP",
"disable unused MCP", "prune MCP servers"。
maturity: stable
allowed-tools:
- Bash
- Read
---
# /mcp-prune — MCP 剪枝分析
基于 \`scripts/mcp-usage-tracker.js\` 产出的使用率数据,识别并报告低频 MCP
候选。**绝不自动修改** \`~/.claude.json\` — 用户必须人工 apply。
## 安全边界
| 能力 | 默认 | --plan | --confirm |
|------|------|--------|-----------|
| 只读分析 | ✅ | ✅ | ✅ |
| 生成 plan 文件 | — | ✅ | ✅ |
| 修改 .claude.json | ❌ | ❌ | ❌ (永远不自动改) |
| 打印 apply 指令 | — | — | ✅ |
## 执行
\`\`\`bash
# 报告模式 (默认 30 天窗口)
node ~/.claude/scripts/mcp-prune.js
# 7 天窗口 (更激进)
node ~/.claude/scripts/mcp-prune.js --days 7
# 写入 plan 文件
node ~/.claude/scripts/mcp-prune.js --plan
# 打印用户 apply 步骤
node ~/.claude/scripts/mcp-prune.js --confirm
\`\`\`
## 剪枝逻辑
- 候选条件: 窗口内 0 调用 **AND** 不在 \`~/.claude/mcp-critical-allowlist.json\`
- 豁免: critical 清单永远保留
- 数据源:
- 使用率: \`~/.claude/debug/activity-*.jsonl\` (event=='mcp')
- 白名单: \`~/.claude/mcp-critical-allowlist.json\`
- 配置: \`~/.claude.json\` (只读)
## 输出
- 报告到 stdout
- --plan 时写入 \`~/.claude/mcp-prune-plan-<date>.json\`
- --confirm 追加 apply 指令 (PowerShell + 编辑 .claude.json 指引)
## 关联
- 依赖: \`scripts/mcp-usage-tracker.js\` (Phase 1 · T1.1)
- 依赖: \`mcp-critical-allowlist.json\` (Phase 1 · T1.5)
- 消费方: 用户手动 apply plan
## sentinel
${SENTINEL}
`;
function writeIfDifferent(target, content, label) {
if (fs.existsSync(target)) {
const current = fs.readFileSync(target, 'utf8');
if (current === content) {
console.log('[patch-phase1-T1.4] ' + label + ' 已一致,跳过');
return 'skipped';
}
const bak = target + '.bak.phase1-t1.4';
fs.copyFileSync(target, bak);
console.log('[patch-phase1-T1.4] 已备份:', bak);
}
const tmp = target + '.tmp.js';
fs.writeFileSync(tmp, content, 'utf8');
if (target.endsWith('.js')) {
try {
const { execFileSync } = require('child_process');
execFileSync(process.execPath, ['--check', tmp], { stdio: 'pipe' });
} catch (e) {
try { fs.unlinkSync(tmp); } catch {}
console.error('[patch-phase1-T1.4] ' + label + ' 语法检查失败:',
(e.stderr || e.message || '').toString().slice(0, 500));
process.exit(3);
}
}
fs.renameSync(tmp, target);
console.log('[patch-phase1-T1.4] 已写入 ' + label + ':', target);
return 'written';
}
function main() {
// 确保 skill 目录存在
if (!fs.existsSync(SKILL_DIR)) {
fs.mkdirSync(SKILL_DIR, { recursive: true });
console.log('[patch-phase1-T1.4] 已创建 skill 目录:', SKILL_DIR);
}
const r1 = writeIfDifferent(SCRIPT_TARGET, SCRIPT_CONTENT, 'script');
const r2 = writeIfDifferent(SKILL_TARGET, SKILL_CONTENT, 'SKILL.md');
console.log('');
console.log('[patch-phase1-T1.4] sentinel:', SENTINEL);
console.log('[patch-phase1-T1.4] script:', r1);
console.log('[patch-phase1-T1.4] skill:', r2);
console.log('[patch-phase1-T1.4] 完成。');
console.log('');
console.log('验证:');
console.log(' node scripts/mcp-prune.js --days 30');
process.exit(0);
}
main();