UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

336 lines 13.7 kB
// Bug management operations import { formatBugs, formatBulkUpdateResults, formatTokenUsage } from './format.js'; import { TokenUsageTracker } from './token-usage-tracker.js'; export class BugManager { tokenTracker; constructor() { this.tokenTracker = TokenUsageTracker.getInstance(); } /** * Create a new bug */ async createBug(db, args) { const startTime = Date.now(); this.tokenTracker.startOperation('create_bug'); return this.createBugWithRetry(db, args, 0); } /** * Create bug with retry mechanism to handle race conditions */ async createBugWithRetry(db, args, retryCount) { const maxRetries = 3; try { return await this.createBugAttempt(db, args); } catch (error) { if (retryCount < maxRetries && error instanceof Error && error.message.includes('UNIQUE constraint failed')) { // Wait a bit and retry with a new ID await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 50)); return this.createBugWithRetry(db, args, retryCount + 1); } throw error; } } /** * Single attempt to create a bug */ async createBugAttempt(db, args) { return new Promise(async (resolve, reject) => { try { // Generate ID const bugId = args.bugId || await this.generateNextId(db, 'bug'); const bug = { id: bugId, status: 'Open', priority: args.priority || 'Medium', dateReported: new Date().toISOString().split('T')[0], component: args.component || 'General', title: args.title, description: args.description, expectedBehavior: args.expectedBehavior, actualBehavior: args.actualBehavior, potentialRootCause: args.potentialRootCause, filesLikelyInvolved: args.filesLikelyInvolved || [], stepsToReproduce: args.stepsToReproduce || [], verification: [], humanVerified: false }; const insertQuery = ` INSERT INTO bugs ( id, status, priority, dateReported, component, title, description, expectedBehavior, actualBehavior, potentialRootCause, filesLikelyInvolved, stepsToReproduce, verification, humanVerified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; db.run(insertQuery, [ bug.id, bug.status, bug.priority, bug.dateReported, bug.component, bug.title, bug.description, bug.expectedBehavior, bug.actualBehavior, bug.potentialRootCause, JSON.stringify(bug.filesLikelyInvolved), JSON.stringify(bug.stepsToReproduce), JSON.stringify(bug.verification), bug.humanVerified ? 1 : 0 ], async (insertErr) => { if (insertErr) { reject(new Error(`Failed to create bug: ${insertErr.message}`)); return; } // Record token usage const inputText = JSON.stringify(args); const outputText = `Bug ${bug.id} created successfully.`; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'create_bug'); resolve(`${outputText}${formatTokenUsage(tokenUsage)}`); }); } catch (err) { reject(new Error(`Bug creation failed: ${err instanceof Error ? err.message : 'Unknown error'}`)); } }); } /** * List bugs with filtering options */ async listBugs(db, args) { this.tokenTracker.startOperation('list_bugs'); let query = 'SELECT * FROM bugs'; const params = []; const conditions = []; if (args.status) { conditions.push('status = ?'); params.push(args.status); } if (args.priority) { conditions.push('priority = ?'); params.push(args.priority); } if (args.component) { conditions.push('component = ?'); params.push(args.component); } if (args.humanVerified !== undefined) { conditions.push('humanVerified = ?'); params.push(args.humanVerified ? 1 : 0); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY dateReported DESC'; return new Promise((resolve, reject) => { db.all(query, params, (err, rows) => { if (err) { reject(new Error(`Failed to list 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 })); // Record token usage const inputText = JSON.stringify(args); const outputText = formatBugs(bugs); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'list_bugs'); resolve(`${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`); } }); }); } /** * Update bug status */ async updateBugStatus(db, args) { this.tokenTracker.startOperation('update_bug_status'); // Added comment to test context change detection const { itemId, status, humanVerified } = args; const validStatuses = ['Open', 'In Progress', 'Fixed', 'Closed', 'Temporarily Resolved']; if (!validStatuses.includes(status)) { throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`); } return new Promise((resolve, reject) => { let updateQuery = 'UPDATE bugs SET status = ?'; const params = [status]; if (humanVerified !== undefined) { updateQuery += ', humanVerified = ?'; params.push(humanVerified ? 1 : 0); } updateQuery += ' WHERE id = ?'; params.push(itemId); db.run(updateQuery, params, (err, result) => { if (err) { reject(new Error(`Failed to update bug status: ${err.message}`)); } else if (result && result.changes === 0) { reject(new Error(`Bug ${itemId} not found`)); } else { // Record token usage const inputText = JSON.stringify(args); const outputText = `Updated bug ${itemId} to ${status}`; const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'update_bug_status'); resolve(`Bug ${itemId} updated to ${status}.\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`); } }); }); } /** * Search bugs */ async searchBugs(db, query, args) { this.tokenTracker.startOperation('search_bugs'); const searchFields = args.searchFields || ['title', 'description', 'component']; const limit = args.limit || 50; const offset = args.offset || 0; let sql = 'SELECT * FROM bugs WHERE '; const conditions = []; const params = []; // Add search conditions if (query) { const searchConditions = searchFields.map((field) => `${field} LIKE ?`); conditions.push(`(${searchConditions.join(' OR ')})`); searchFields.forEach(() => params.push(`%${query}%`)); } // Add filters if (args.status) { if (Array.isArray(args.status)) { conditions.push(`status IN (${args.status.map(() => '?').join(', ')})`); params.push(...args.status); } else { conditions.push('status = ?'); params.push(args.status); } } if (args.priority) { if (Array.isArray(args.priority)) { conditions.push(`priority IN (${args.priority.map(() => '?').join(', ')})`); params.push(...args.priority); } else { conditions.push('priority = ?'); params.push(args.priority); } } if (args.component) { conditions.push('component LIKE ?'); params.push(`%${args.component}%`); } if (args.humanVerified !== undefined) { conditions.push('humanVerified = ?'); params.push(args.humanVerified ? 1 : 0); } // Date range filter if (args.dateFrom) { conditions.push('dateReported >= ?'); params.push(args.dateFrom); } if (args.dateTo) { conditions.push('dateReported <= ?'); params.push(args.dateTo); } if (conditions.length === 0) { sql = 'SELECT * FROM bugs'; } else { sql += conditions.join(' AND '); } // Add sorting const sortBy = args.sortBy || 'dateReported'; const sortOrder = args.sortOrder || 'desc'; sql += ` ORDER BY ${sortBy} ${sortOrder}`; // Add pagination sql += ' LIMIT ? OFFSET ?'; params.push(limit, offset); return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) { reject(new Error(`Failed to search bugs: ${err.message}`)); } else { const bugs = rows.map(row => ({ ...row, type: 'bug', filesLikelyInvolved: JSON.parse(row.filesLikelyInvolved || '[]'), stepsToReproduce: JSON.parse(row.stepsToReproduce || '[]'), verification: JSON.parse(row.verification || '[]'), humanVerified: row.humanVerified === 1 })); resolve(bugs); } }); }); } /** * Bulk update bug status */ async bulkUpdateBugStatus(db, args) { this.tokenTracker.startOperation('bulk_update_bug_status'); const { updates } = args; const results = []; for (const update of updates) { try { const result = await this.updateBugStatus(db, update); results.push({ status: 'success', bugId: update.itemId, message: `Updated to ${update.status}`, humanVerified: update.humanVerified }); } catch (error) { results.push({ status: 'error', bugId: update.itemId, message: error instanceof Error ? error.message : 'Unknown error' }); } } // Record token usage const inputText = JSON.stringify(args); const outputText = formatBulkUpdateResults(results, 'bugs'); const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'bulk_update_bug_status'); return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`; } /** * Generate next ID for bug with retry mechanism for race conditions */ async generateNextIdWithRetry(db, retryCount = 0) { const maxRetries = 3; try { return await this.generateNextId(db, 'bug'); } catch (error) { if (retryCount < maxRetries && error instanceof Error && error.message.includes('UNIQUE constraint')) { // Wait a bit and retry await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 50)); return this.generateNextIdWithRetry(db, retryCount + 1); } throw error; } } /** * Generate next ID for bug */ async generateNextId(db, type) { return new Promise((resolve, reject) => { db.get('SELECT MAX(CAST(SUBSTR(id, 6) AS INTEGER)) as maxId FROM bugs', [], (err, row) => { if (err) { reject(new Error(`Failed to generate ID: ${err.message}`)); } else { const nextNum = (row?.maxId || 0) + 1; resolve(`Bug #${nextNum.toString().padStart(3, '0')}`); } }); }); } } //# sourceMappingURL=bugs.js.map