#!/usr/bin/env node /** * 技能使用报告 - 退役机制分析工具 * * 用法: * node scripts/skill-usage-report.js # CLI 报告 * node scripts/skill-usage-report.js --json # JSON 输出 * node scripts/skill-usage-report.js --days 90 # 指定分析天数 (默认 30) * * 分析内容: * - 每个技能的调用次数和最后使用时间 * - 标记 dormant (30天未调用) 和 candidate-for-archive (90天未调用) * - Agent 和 MCP 使用统计 * * 数据源: debug/activity-*.jsonl */ const fs = require('fs'); const path = require('path'); // 动态检测配置根目录 function detectClaudeRoot() { if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME; const selfDir = path.dirname(__filename); if (selfDir.includes('.claude')) return selfDir.replace(/[/\\]scripts$/, ''); try { return require('./paths.config.js').PATHS.root; } catch { return (process.env.USERPROFILE || process.env.HOME || '').replace(/\\/g, '/') + '/.claude'; } } const CLAUDE_ROOT = detectClaudeRoot(); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); const SKILLS_DIR = path.join(CLAUDE_ROOT, 'skills'); // === 参数解析 === const args = process.argv.slice(2); const jsonMode = args.includes('--json'); const daysIdx = args.indexOf('--days'); const analysisDays = (daysIdx >= 0 && parseInt(args[daysIdx + 1])) || 30; // === 数据加载 === function loadAllActivityLogs(days) { const entries = []; const now = new Date(); for (let i = 0; i < days; i++) { const d = new Date(now); d.setDate(d.getDate() - i); const dateStr = d.toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`); if (!fs.existsSync(logFile)) continue; const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n'); for (const line of lines) { if (!line) continue; try { entries.push(JSON.parse(line)); } catch {} } } return entries; } function getRegisteredSkills() { // 从 skills/ 目录读取所有已注册技能 const skills = new Set(); try { for (const dir of fs.readdirSync(SKILLS_DIR)) { const skillFile = path.join(SKILLS_DIR, dir, 'SKILL.md'); if (fs.existsSync(skillFile)) { skills.add(dir); } } } catch {} return skills; } // === 分析 === function analyze(entries) { const registeredSkills = getRegisteredSkills(); const today = new Date(); // 技能使用统计 const skillStats = {}; for (const skill of registeredSkills) { skillStats[skill] = { count: 0, lastUsed: null }; } // Agent 统计 const agentStats = {}; // MCP 统计 const mcpStats = {}; for (const entry of entries) { if (entry.event === 'skill' && entry.detail) { const name = entry.detail; if (!skillStats[name]) { skillStats[name] = { count: 0, lastUsed: null }; } skillStats[name].count++; const ts = entry.ts; if (!skillStats[name].lastUsed || ts > skillStats[name].lastUsed) { skillStats[name].lastUsed = ts; } } if (entry.event === 'agent' && entry.detail) { const name = entry.detail; agentStats[name] = (agentStats[name] || 0) + 1; } if (entry.event === 'mcp' && entry.detail) { const name = entry.detail; mcpStats[name] = (mcpStats[name] || 0) + 1; } } // 分类技能状态 const dormantThreshold = 30; const archiveThreshold = 90; const results = { period: `${analysisDays} days`, totalEvents: entries.length, skills: { active: [], dormant: [], candidateForArchive: [], neverUsed: [], }, agents: agentStats, mcps: mcpStats, }; for (const [name, stats] of Object.entries(skillStats)) { const item = { name, count: stats.count, lastUsed: stats.lastUsed }; if (stats.count === 0) { results.skills.neverUsed.push(item); } else if (stats.lastUsed) { const lastDate = new Date(stats.lastUsed); const daysSince = Math.floor((today - lastDate) / (1000 * 60 * 60 * 24)); item.daysSinceLastUse = daysSince; if (daysSince >= archiveThreshold) { results.skills.candidateForArchive.push(item); } else if (daysSince >= dormantThreshold) { results.skills.dormant.push(item); } else { results.skills.active.push(item); } } else { results.skills.active.push(item); } } // 按调用次数排序 results.skills.active.sort((a, b) => b.count - a.count); results.skills.dormant.sort((a, b) => (a.daysSinceLastUse || 0) - (b.daysSinceLastUse || 0)); results.skills.candidateForArchive.sort((a, b) => (a.daysSinceLastUse || 0) - (b.daysSinceLastUse || 0)); results.skills.neverUsed.sort((a, b) => a.name.localeCompare(b.name)); return results; } // === CLI 渲染 === function renderCli(results) { console.log(); console.log(`=== Skill Usage Report (${results.period}, ${results.totalEvents} events) ===`); console.log(); // Active const active = results.skills.active; console.log(`Active Skills (${active.length}):`); if (active.length > 0) { for (const s of active.slice(0, 15)) { const last = s.lastUsed ? s.lastUsed.slice(0, 10) : 'N/A'; console.log(` ${s.name.padEnd(35)} ${String(s.count).padStart(4)} calls last: ${last}`); } if (active.length > 15) console.log(` ... and ${active.length - 15} more`); } // Never used const neverUsed = results.skills.neverUsed; if (neverUsed.length > 0) { console.log(); console.log(`Never Used in ${results.period} (${neverUsed.length}):`); for (const s of neverUsed) { console.log(` ${s.name}`); } } // Dormant const dormant = results.skills.dormant; if (dormant.length > 0) { console.log(); console.log(`Dormant - 30+ days idle (${dormant.length}):`); for (const s of dormant) { console.log(` ${s.name.padEnd(35)} ${s.daysSinceLastUse}d idle (${s.count} total calls)`); } } // Archive candidates const archive = results.skills.candidateForArchive; if (archive.length > 0) { console.log(); console.log(`Archive Candidates - 90+ days idle (${archive.length}):`); for (const s of archive) { console.log(` ${s.name.padEnd(35)} ${s.daysSinceLastUse}d idle (${s.count} total calls)`); } } // Summary console.log(); console.log('Summary:'); console.log(` Registered: ${active.length + neverUsed.length + dormant.length + archive.length}`); console.log(` Active: ${active.length} | Never used: ${neverUsed.length} | Dormant: ${dormant.length} | Archive: ${archive.length}`); console.log(); } // === 主流程 === function main() { const entries = loadAllActivityLogs(analysisDays); const results = analyze(entries); if (jsonMode) { console.log(JSON.stringify(results, null, 2)); } else { renderCli(results); } } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { loadAllActivityLogs, getRegisteredSkills, analyze, main, }; } if (require.main === module) { main(); }