bookworm-smart-assistant/scripts/archive/skill-usage-report.js

242 lines
7.2 KiB
JavaScript
Raw Permalink Normal View History

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