UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

391 lines (341 loc) 13.2 kB
// Search and semantic operations import { formatSearchResults, formatStatistics } from './format.js'; import { TokenUsageTracker } from './token-usage-tracker.js'; import { BugManager } from './bugs.js'; import { FeatureManager } from './features.js'; import { ImprovementManager } from './improvements.js'; import sqlite3 from 'sqlite3'; export class SearchManager { private tokenTracker: TokenUsageTracker; private bugManager: BugManager; private featureManager: FeatureManager; private improvementManager: ImprovementManager; constructor() { this.tokenTracker = TokenUsageTracker.getInstance(); this.bugManager = new BugManager(); this.featureManager = new FeatureManager(); this.improvementManager = new ImprovementManager(); } /** * Perform semantic search across all item types */ async performSemanticSearch(db: sqlite3.Database, args: any): Promise<string> { this.tokenTracker.startOperation('semantic_search'); const { query, limit = 10, minSimilarity = 0.3 } = args; if (!query) { throw new Error('Query is required for semantic search'); } const results: any[] = []; try { // Search bugs const bugs = await this.searchAllBugs(db, query); for (const bug of bugs) { const similarity = this.calculateSimilarity(query, `${bug.title} ${bug.description}`); if (similarity >= minSimilarity) { results.push({ ...bug, type: 'bug', similarity, matchedFields: this.getMatchedFields(query, bug, ['title', 'description', 'component']) }); } } // Search features const features = await this.searchAllFeatures(db, query); for (const feature of features) { const similarity = this.calculateSimilarity(query, `${feature.title} ${feature.description}`); if (similarity >= minSimilarity) { results.push({ ...feature, type: 'feature', similarity, matchedFields: this.getMatchedFields(query, feature, ['title', 'description', 'category']) }); } } // Search improvements const improvements = await this.searchAllImprovements(db, query); for (const improvement of improvements) { const similarity = this.calculateSimilarity(query, `${improvement.title} ${improvement.description}`); if (similarity >= minSimilarity) { results.push({ ...improvement, type: 'improvement', similarity, matchedFields: this.getMatchedFields(query, improvement, ['title', 'description', 'category']) }); } } // Sort by similarity and limit results results.sort((a, b) => b.similarity - a.similarity); const limitedResults = results.slice(0, limit); // Record token usage const inputText = JSON.stringify(args); const outputText = formatSearchResults(limitedResults, { total: results.length, showing: limitedResults.length, offset: 0 }); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'semantic_search'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Semantic search failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * General search across all item types */ async searchItems(db: sqlite3.Database, args: any): Promise<string> { this.tokenTracker.startOperation('search_items'); const { query, type, limit = 50, offset = 0 } = args; let results: any[] = []; try { if (type === 'bugs' || type === 'all') { const bugs = await this.bugManager.searchBugs(db, query, args); results.push(...bugs); } if (type === 'features' || type === 'all') { const features = await this.featureManager.searchFeatures(db, query, args); results.push(...features); } if (type === 'improvements' || type === 'all') { const improvements = await this.improvementManager.searchImprovements(db, query, args); results.push(...improvements); } // Apply global sorting if type is 'all' if (type === 'all') { const sortBy = args.sortBy || 'dateReported'; const sortOrder = args.sortOrder || 'desc'; results.sort((a, b) => { const aValue = a[sortBy] || a.dateRequested || ''; const bValue = b[sortBy] || b.dateRequested || ''; if (sortOrder === 'asc') { return aValue.localeCompare(bValue); } else { return bValue.localeCompare(aValue); } }); // Apply pagination for 'all' type results = results.slice(offset, offset + limit); } // Record token usage const inputText = JSON.stringify(args); const outputText = formatSearchResults(results, { total: results.length, showing: results.length, offset: offset }); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'search_items'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get project statistics */ async getStatistics(db: sqlite3.Database, args: any): Promise<string> { this.tokenTracker.startOperation('get_statistics'); const { type = 'all' } = args; const stats: any = {}; try { if (type === 'bugs' || type === 'all') { stats.bugs = await this.getBugStatistics(db); } if (type === 'features' || type === 'all') { stats.features = await this.getFeatureStatistics(db); } if (type === 'improvements' || type === 'all') { stats.improvements = await this.getImprovementStatistics(db); } // Record token usage const inputText = JSON.stringify(args); const outputText = formatStatistics(stats); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'get_statistics'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to get statistics: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Calculate similarity between two texts using Jaccard similarity */ private calculateSimilarity(text1: string, text2: string): number { const normalize = (text: string) => text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 0); const words1 = new Set(normalize(text1)); const words2 = new Set(normalize(text2)); const intersection = new Set([...words1].filter(word => words2.has(word))); const union = new Set([...words1, ...words2]); return union.size === 0 ? 0 : intersection.size / union.size; } /** * Get matched fields for search results */ private getMatchedFields(query: string, item: any, fields: string[]): string[] { const queryWords = query.toLowerCase().split(/\s+/); const matchedFields: string[] = []; for (const field of fields) { const fieldValue = item[field] || ''; const fieldWords = fieldValue.toLowerCase().split(/\s+/); for (const queryWord of queryWords) { if (fieldWords.some((word: string) => word.includes(queryWord))) { matchedFields.push(field); break; } } } return matchedFields; } /** * Search all bugs (helper method) */ private async searchAllBugs(db: sqlite3.Database, query: string): Promise<any[]> { return new Promise((resolve, reject) => { db.all('SELECT * FROM bugs', [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to search bugs: ${err.message}`)); } else { const bugs = rows.map(row => ({ ...row, filesLikelyInvolved: JSON.parse(row.filesLikelyInvolved || '[]'), stepsToReproduce: JSON.parse(row.stepsToReproduce || '[]'), verification: JSON.parse(row.verification || '[]'), humanVerified: row.humanVerified === 1 })); resolve(bugs); } }); }); } /** * Search all features (helper method) */ private async searchAllFeatures(db: sqlite3.Database, query: string): Promise<any[]> { return new Promise((resolve, reject) => { db.all('SELECT * FROM feature_requests', [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to search features: ${err.message}`)); } else { const features = rows.map(row => ({ ...row, acceptanceCriteria: JSON.parse(row.acceptanceCriteria || '[]'), dependencies: JSON.parse(row.dependencies || '[]') })); resolve(features); } }); }); } /** * Search all improvements (helper method) */ private async searchAllImprovements(db: sqlite3.Database, query: string): Promise<any[]> { return new Promise((resolve, reject) => { db.all('SELECT * FROM improvements', [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to search improvements: ${err.message}`)); } else { const improvements = rows.map(row => ({ ...row, acceptanceCriteria: JSON.parse(row.acceptanceCriteria || '[]'), filesLikelyInvolved: JSON.parse(row.filesLikelyInvolved || '[]'), dependencies: JSON.parse(row.dependencies || '[]'), benefits: JSON.parse(row.benefits || '[]') })); resolve(improvements); } }); }); } /** * Get bug statistics */ private async getBugStatistics(db: sqlite3.Database): Promise<any> { return new Promise((resolve, reject) => { db.all(` SELECT COUNT(*) as total, status, priority, COUNT(*) as count FROM bugs GROUP BY status, priority `, [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to get bug statistics: ${err.message}`)); } else { const byStatus: any = {}; const byPriority: any = {}; let total = 0; for (const row of rows) { byStatus[row.status] = (byStatus[row.status] || 0) + row.count; byPriority[row.priority] = (byPriority[row.priority] || 0) + row.count; total += row.count; } resolve({ total, byStatus, byPriority }); } }); }); } /** * Get feature statistics */ private async getFeatureStatistics(db: sqlite3.Database): Promise<any> { return new Promise((resolve, reject) => { db.all(` SELECT COUNT(*) as total, status, priority, COUNT(*) as count FROM feature_requests GROUP BY status, priority `, [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to get feature statistics: ${err.message}`)); } else { const byStatus: any = {}; const byPriority: any = {}; let total = 0; for (const row of rows) { byStatus[row.status] = (byStatus[row.status] || 0) + row.count; byPriority[row.priority] = (byPriority[row.priority] || 0) + row.count; total += row.count; } resolve({ total, byStatus, byPriority }); } }); }); } /** * Get improvement statistics */ private async getImprovementStatistics(db: sqlite3.Database): Promise<any> { return new Promise((resolve, reject) => { db.all(` SELECT COUNT(*) as total, status, priority, COUNT(*) as count FROM improvements GROUP BY status, priority `, [], (err, rows: any[]) => { if (err) { reject(new Error(`Failed to get improvement statistics: ${err.message}`)); } else { const byStatus: any = {}; const byPriority: any = {}; let total = 0; for (const row of rows) { byStatus[row.status] = (byStatus[row.status] || 0) + row.count; byPriority[row.priority] = (byPriority[row.priority] || 0) + row.count; total += row.count; } resolve({ total, byStatus, byPriority }); } }); }); } }