UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

459 lines 19.2 kB
// Context management operations import { ContextCollectionEngine } from './context-collection-engine.js'; import { TokenUsageTracker } from './token-usage-tracker.js'; export class ContextManager { tokenTracker; contextEngine; constructor() { this.tokenTracker = TokenUsageTracker.getInstance(); this.contextEngine = new ContextCollectionEngine(process.cwd(), { maxTokensPerTask: 2000, maxTokensPerContext: 200, enableIntelligentSummarization: true, enableContentDeduplication: true, taskTypeTokenLimits: { bug: 1500, feature: 2500, improvement: 2000, } }); } /** * Manage contexts with various operations */ async manageContexts(db, args) { this.tokenTracker.startOperation('manage_contexts'); const { operation, taskId } = args; switch (operation) { case 'collect': return this.collectContextForTask(db, args); case 'get': return this.getTaskContexts(db, args); case 'check_freshness': return this.checkContextFreshness(db, args); case 'add': return this.addManualContext(db, args); case 'update': return this.updateContext(db, args); case 'remove': return this.removeContext(db, args); default: throw new Error(`Unknown context operation: ${operation}`); } } /** * Collect context for a task */ async collectContextForTask(db, args) { const { taskId, taskType, title, description, currentState, desiredState, expectedBehavior, actualBehavior, filesLikelyInvolved, keywords, entities } = args; if (!taskId || !taskType || !title || !description) { throw new Error('taskId, taskType, title, and description are required for context collection'); } try { const input = { taskId, taskType, title, description, currentState, desiredState, expectedBehavior, actualBehavior, filesLikelyInvolved, keywords, entities }; const result = await this.contextEngine.collectContexts(input); // Store contexts in database await this.storeContextsInDatabase(db, result.contexts); // Record token usage const inputText = JSON.stringify(args); const outputText = this.formatContextCollectionResult(result); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'collect_context'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Context collection failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get task contexts */ async getTaskContexts(db, args) { const { taskId } = args; if (!taskId) { throw new Error('taskId is required'); } try { const contexts = await this.getContextsFromDatabase(db, taskId); // Record token usage const inputText = JSON.stringify(args); const outputText = this.formatContexts(contexts); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'get_contexts'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to get contexts: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Check context freshness */ async checkContextFreshness(db, args) { const { taskId } = args; if (!taskId) { throw new Error('taskId is required'); } try { const contexts = await this.getContextsFromDatabase(db, taskId); const now = Date.now(); const stalenessThreshold = 24 * 60 * 60 * 1000; // 24 hours let freshCount = 0; let staleCount = 0; let contentChangedCount = 0; const staleContexts = []; const changedContexts = []; for (const context of contexts) { const lastChecked = context.dateLastChecked ? new Date(context.dateLastChecked).getTime() : new Date(context.dateCollected).getTime(); const age = now - lastChecked; // Check if content has changed const contentChanged = await this.hasContentChanged(context); if (contentChanged) { contentChangedCount++; changedContexts.push(context); // Mark as stale in database await this.updateContextInDatabase(db, context.id, { isStale: true }); } else if (age > stalenessThreshold) { staleCount++; staleContexts.push(context); } else { freshCount++; } } const summary = `Context freshness check for task ${taskId}:\n` + `- Fresh contexts: ${freshCount}\n` + `- Stale contexts: ${staleCount}\n` + `- Content changed contexts: ${contentChangedCount}\n` + `- Total contexts: ${contexts.length}`; let details = ''; if (changedContexts.length > 0) { const changedDetails = changedContexts.map(ctx => ` - ${ctx.id}: ${ctx.description} (content changed since collection)`).join('\n'); details += `\nContent changed contexts:\n${changedDetails}`; } if (staleContexts.length > 0) { const staleDetails = staleContexts.map(ctx => ` - ${ctx.id}: ${ctx.description} (last checked: ${ctx.dateLastChecked || ctx.dateCollected})`).join('\n'); details += `\nTime-based stale contexts:\n${staleDetails}`; } if (details) { return `${summary}${details}`; } // Record token usage const inputText = JSON.stringify(args); const outputText = summary; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'check_freshness'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to check context freshness: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Add manual context */ async addManualContext(db, args) { const { taskId, taskType, contextType, filePath, startLine, endLine, content, description, relevanceScore, keywords } = args; if (!taskId || !taskType || !contextType || !filePath || !content || !description) { throw new Error('taskId, taskType, contextType, filePath, content, and description are required'); } try { const context = { id: `${taskId}_manual_${Date.now()}`, taskId, taskType, contextType, source: 'manual', filePath, startLine, endLine, content, description, relevanceScore: relevanceScore || 0.8, keywords: keywords || [], dateCollected: new Date().toISOString(), isStale: false }; await this.storeContextsInDatabase(db, [context]); // Record token usage const inputText = JSON.stringify(args); const outputText = `Manual context added successfully: ${context.id}`; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'add_manual_context'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to add manual context: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Update context */ async updateContext(db, args) { const { contextId, updates } = args; if (!contextId || !updates) { throw new Error('contextId and updates are required'); } try { await this.updateContextInDatabase(db, contextId, updates); // Record token usage const inputText = JSON.stringify(args); const outputText = `Context ${contextId} updated successfully`; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'update_context'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to update context: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Remove context */ async removeContext(db, args) { const { contextId } = args; if (!contextId) { throw new Error('contextId is required'); } try { await this.removeContextFromDatabase(db, contextId); // Record token usage const inputText = JSON.stringify(args); const outputText = `Context ${contextId} removed successfully`; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'remove_context'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } catch (error) { throw new Error(`Failed to remove context: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Store contexts in database */ async storeContextsInDatabase(db, contexts) { const insertQuery = ` INSERT OR REPLACE INTO code_contexts ( id, taskId, taskType, contextType, source, filePath, startLine, endLine, content, description, relevanceScore, keywords, dateCollected, dateLastChecked, isStale ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; for (const context of contexts) { await new Promise((resolve, reject) => { db.run(insertQuery, [ context.id, context.taskId, context.taskType, context.contextType, context.source, context.filePath, context.startLine, context.endLine, context.content, context.description, context.relevanceScore, JSON.stringify(context.keywords), context.dateCollected, context.dateLastChecked, context.isStale ? 1 : 0 ], function (err) { if (err) { reject(new Error(`Failed to store context: ${err.message}`)); } else { resolve(); } }); }); } } /** * Get contexts from database */ async getContextsFromDatabase(db, taskId) { return new Promise((resolve, reject) => { db.all('SELECT * FROM code_contexts WHERE taskId = ? ORDER BY relevanceScore DESC', [taskId], (err, rows) => { if (err) { reject(new Error(`Failed to get contexts: ${err.message}`)); } else { const contexts = rows.map(row => ({ ...row, keywords: JSON.parse(row.keywords || '[]'), isStale: row.isStale === 1 })); resolve(contexts); } }); }); } /** * Update context in database */ async updateContextInDatabase(db, contextId, updates) { const allowedUpdates = ['content', 'description', 'relevanceScore', 'keywords', 'isStale']; const updateFields = []; const updateValues = []; for (const [key, value] of Object.entries(updates)) { if (allowedUpdates.includes(key)) { updateFields.push(`${key} = ?`); if (key === 'keywords') { updateValues.push(JSON.stringify(value)); } else if (key === 'isStale') { updateValues.push(value ? 1 : 0); } else { updateValues.push(value); } } } if (updateFields.length === 0) { throw new Error('No valid update fields provided'); } updateFields.push('dateLastChecked = ?'); updateValues.push(new Date().toISOString()); updateValues.push(contextId); const updateQuery = `UPDATE code_contexts SET ${updateFields.join(', ')} WHERE id = ?`; return new Promise((resolve, reject) => { db.run(updateQuery, updateValues, function (err) { if (err) { reject(new Error(`Failed to update context: ${err.message}`)); } else if (this.changes === 0) { reject(new Error(`Context ${contextId} not found`)); } else { resolve(); } }); }); } /** * Remove context from database */ async removeContextFromDatabase(db, contextId) { return new Promise((resolve, reject) => { db.run('DELETE FROM code_contexts WHERE id = ?', [contextId], function (err) { if (err) { reject(new Error(`Failed to remove context: ${err.message}`)); } else if (this.changes === 0) { reject(new Error(`Context ${contextId} not found`)); } else { resolve(); } }); }); } /** * Format context collection result */ formatContextCollectionResult(result) { const { contexts, summary, recommendations, potentialIssues } = result; let output = `Context Collection Summary:\n`; output += `- Total contexts collected: ${summary.totalContexts}\n`; output += `- High relevance contexts: ${summary.highRelevanceContexts}\n`; output += `- Medium relevance contexts: ${summary.mediumRelevanceContexts}\n`; output += `- Low relevance contexts: ${summary.lowRelevanceContexts}\n`; output += `- Average relevance score: ${summary.averageRelevanceScore.toFixed(2)}\n`; output += `- Processing time: ${summary.processingTimeMs}ms\n`; output += `- Files analyzed: ${summary.filesAnalyzed}\n`; output += `- Patterns found: ${summary.patternsFound}\n`; output += `- Dependencies analyzed: ${summary.dependenciesAnalyzed}\n\n`; if (contexts.length > 0) { output += `Collected Contexts:\n`; output += this.formatContexts(contexts); output += '\n'; } if (recommendations.length > 0) { output += `Recommendations:\n`; recommendations.forEach(rec => { output += `- ${rec}\n`; }); output += '\n'; } if (potentialIssues.length > 0) { output += `Potential Issues:\n`; potentialIssues.forEach(issue => { output += `- ${issue}\n`; }); } return output; } /** * Check if context content has changed since collection */ async hasContentChanged(context) { try { const fs = await import('fs'); // Check if file still exists if (!fs.existsSync(context.filePath)) { return true; // File deleted = content changed } // Read current file content const currentContent = fs.readFileSync(context.filePath, 'utf8'); const lines = currentContent.split('\n'); // Extract the lines that the context refers to if (context.startLine && context.endLine) { const contextLines = lines.slice(context.startLine - 1, context.endLine); const currentContextContent = contextLines.join('\n').trim(); const originalContent = (context.content || '').trim(); // Compare content (ignoring minor whitespace differences) return this.normalizeContent(currentContextContent) !== this.normalizeContent(originalContent); } // For contexts without specific line ranges, we can't easily verify return false; } catch (error) { // If we can't read the file, assume it has changed return true; } } /** * Normalize content for comparison (remove extra whitespace, etc.) */ normalizeContent(content) { return content .replace(/\s+/g, ' ') // Replace multiple whitespace with single space .replace(/^\s+|\s+$/g, '') // Trim start and end .toLowerCase(); // Case insensitive comparison } /** * Format contexts for display */ formatContexts(contexts) { if (contexts.length === 0) { return 'No contexts found.'; } let output = ''; contexts.forEach((context, index) => { output += `${index + 1}. ${context.description}\n`; output += ` ID: ${context.id}\n`; output += ` Type: ${context.contextType}\n`; output += ` Source: ${context.source}\n`; output += ` File: ${context.filePath}\n`; if (context.startLine && context.endLine) { output += ` Lines: ${context.startLine}-${context.endLine}\n`; } output += ` Relevance: ${context.relevanceScore.toFixed(2)}\n`; output += ` Keywords: ${context.keywords.join(', ')}\n`; output += ` Collected: ${context.dateCollected}\n`; if (context.isStale) { output += ` Status: STALE\n`; } output += '\n'; }); return output; } } //# sourceMappingURL=context.js.map