UNPKG

@sofianedjerbi/knowledge-tree-mcp

Version:

MCP server for hierarchical project knowledge management

168 lines 6.57 kB
/** * Stats knowledge tool implementation * Provides comprehensive statistics about the knowledge base */ import { join } from 'path'; import { STATS_DEFAULTS, PRIORITY_LEVELS } from '../constants/index.js'; import { readFile, getFileStats } from '../utils/index.js'; /** * Handler for the stats_knowledge tool */ export const statsKnowledgeHandler = async (args, context) => { const { include = STATS_DEFAULTS.INCLUDE } = args; const allEntries = await context.scanKnowledgeTree(); const entries = []; // Load all entries with file stats for (const path of allEntries) { const fullPath = join(context.knowledgeRoot, path); try { const content = await readFile(fullPath); const entry = JSON.parse(content); const stats = await getFileStats(fullPath); entries.push({ path, entry, stats }); } catch (error) { // Skip invalid entries } } const result = { generated_at: new Date().toISOString(), total_entries: entries.length }; // Summary statistics if (include.includes("summary")) { result.summary = { total_entries: entries.length, total_size_bytes: entries.reduce((sum, e) => sum + e.stats.size, 0), with_code_examples: entries.filter(e => (e.entry.code && e.entry.code.trim()) || (e.entry.examples && e.entry.examples.length > 0)).length, with_relationships: entries.filter(e => e.entry.related_to && e.entry.related_to.length > 0).length, total_relationships: entries.reduce((sum, e) => sum + (e.entry.related_to?.length || 0), 0) }; } // Priority breakdown if (include.includes("priorities")) { const priorityCounts = entries.reduce((acc, e) => { acc[e.entry.priority] = (acc[e.entry.priority] || 0) + 1; return acc; }, {}); // Ensure all priorities are represented for (const priority of PRIORITY_LEVELS) { if (!priorityCounts[priority]) { priorityCounts[priority] = 0; } } result.priorities = { counts: priorityCounts, percentages: Object.entries(priorityCounts).reduce((acc, [priority, count]) => { acc[priority] = Math.round((count / entries.length) * 100); return acc; }, {}) }; } // Category breakdown if (include.includes("categories")) { const categoryStats = entries.reduce((acc, e) => { const parts = e.path.split('/'); const category = parts.length > 1 ? parts[0] : 'root'; if (!acc[category]) { acc[category] = { count: 0, priorities: {}, subcategories: new Set() }; } acc[category].count++; acc[category].priorities[e.entry.priority] = (acc[category].priorities[e.entry.priority] || 0) + 1; if (parts.length > 2) { acc[category].subcategories.add(parts[1]); } return acc; }, {}); // Convert Sets to arrays for JSON serialization Object.values(categoryStats).forEach((cat) => { cat.subcategories = Array.from(cat.subcategories); }); result.categories = categoryStats; } // Orphaned entries (no relationships) if (include.includes("orphaned")) { const orphaned = entries.filter(e => !e.entry.related_to || e.entry.related_to.length === 0); result.orphaned = { count: orphaned.length, percentage: Math.round((orphaned.length / entries.length) * 100), entries: orphaned.slice(0, 10).map(e => ({ path: e.path, priority: e.entry.priority, problem: e.entry.problem.substring(0, 60) + (e.entry.problem.length > 60 ? '...' : '') })) }; } // Popular entries (most linked to) if (include.includes("popular")) { const linkCounts = new Map(); entries.forEach(e => { if (e.entry.related_to) { e.entry.related_to.forEach(link => { const count = linkCounts.get(link.path) || 0; linkCounts.set(link.path, count + 1); }); } }); const popular = Array.from(linkCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([path, count]) => { const entry = entries.find(e => e.path === path); return { path, incoming_links: count, priority: entry?.entry.priority || 'Unknown', problem: entry ? entry.entry.problem.substring(0, 60) + (entry.entry.problem.length > 60 ? '...' : '') : 'Entry not found' }; }); result.popular = { most_linked: popular, average_links: entries.length > 0 ? Math.round(Array.from(linkCounts.values()).reduce((a, b) => a + b, 0) / entries.length * 10) / 10 : 0 }; } // Coverage analysis if (include.includes("coverage")) { const now = new Date(); const oldEntries = entries.filter(e => { const daysSinceModified = (now.getTime() - e.stats.mtime.getTime()) / (1000 * 60 * 60 * 24); return daysSinceModified > 30; }); result.coverage = { stale_entries: { count: oldEntries.length, percentage: Math.round((oldEntries.length / entries.length) * 100), threshold_days: 30 }, recent_activity: { last_7_days: entries.filter(e => { const daysSinceModified = (now.getTime() - e.stats.mtime.getTime()) / (1000 * 60 * 60 * 24); return daysSinceModified <= 7; }).length, last_30_days: entries.filter(e => { const daysSinceModified = (now.getTime() - e.stats.mtime.getTime()) / (1000 * 60 * 60 * 24); return daysSinceModified <= 30; }).length } }; } return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; }; //# sourceMappingURL=stats.js.map