bugger-mcp
Version:
MCP Server for managing bugs, feature requests, and improvements
414 lines (356 loc) • 14.5 kB
text/typescript
// Workflow automation operations
import { TokenUsageTracker } from './token-usage-tracker.js';
import { BugManager } from './bugs.js';
import { ImprovementManager } from './improvements.js';
import { ContextManager } from './context.js';
import sqlite3 from 'sqlite3';
export class WorkflowManager {
private tokenTracker: TokenUsageTracker;
private bugManager: BugManager;
private improvementManager: ImprovementManager;
private contextManager: ContextManager;
constructor() {
this.tokenTracker = TokenUsageTracker.getInstance();
this.bugManager = new BugManager();
this.improvementManager = new ImprovementManager();
this.contextManager = new ContextManager();
}
/**
* Execute predefined workflows
*/
async executeWorkflow(db: sqlite3.Database, args: any): Promise<string> {
this.tokenTracker.startOperation('execute_workflow');
const { workflow } = args;
switch (workflow) {
case 'create_and_link':
return this.executeCreateAndLinkWorkflow(db, args);
case 'batch_context_collection':
return this.executeBatchContextWorkflow(db, args);
case 'status_transition':
return this.executeStatusTransitionWorkflow(db, args);
default:
throw new Error(`Unknown workflow: ${workflow}`);
}
}
/**
* Create and link workflow
*/
private async executeCreateAndLinkWorkflow(db: sqlite3.Database, args: any): Promise<string> {
const { items } = args;
if (!items || !Array.isArray(items)) {
throw new Error('Items array is required for create_and_link workflow');
}
const results: any[] = [];
const createdItems: string[] = [];
try {
// Create all items first
for (const item of items) {
let createResult: string;
let itemId: string;
switch (item.type) {
case 'bug':
createResult = await this.bugManager.createBug(db, item.data);
itemId = this.extractIdFromCreateResponse(createResult, 'bug');
break;
case 'feature':
throw new Error('Feature requests are no longer supported');
case 'improvement':
createResult = await this.improvementManager.createImprovement(db, item.data);
itemId = this.extractIdFromCreateResponse(createResult, 'improvement');
break;
default:
throw new Error(`Unsupported item type: ${item.type}`);
}
results.push({
status: 'success',
type: item.type,
id: itemId,
message: `Created ${item.type} ${itemId}`
});
createdItems.push(itemId);
}
// Create links between items
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.linkTo && item.relationshipType) {
try {
const linkResult = await this.linkItems(db, {
fromItem: createdItems[i],
toItem: item.linkTo,
relationshipType: item.relationshipType
});
results.push({
status: 'success',
type: 'link',
message: `Linked ${createdItems[i]} to ${item.linkTo} (${item.relationshipType})`
});
} catch (error) {
results.push({
status: 'error',
type: 'link',
message: `Failed to link ${createdItems[i]} to ${item.linkTo}: ${error instanceof Error ? error.message : 'Unknown error'}`
});
}
}
}
// Record token usage
const inputText = JSON.stringify(args);
const outputText = this.formatWorkflowResults('create_and_link', results);
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'create_and_link_workflow');
return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`;
} catch (error) {
throw new Error(`Create and link workflow failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Batch context collection workflow
*/
private async executeBatchContextWorkflow(db: sqlite3.Database, args: any): Promise<string> {
const { tasks } = args;
if (!tasks || !Array.isArray(tasks)) {
throw new Error('Tasks array is required for batch_context_collection workflow');
}
const results: any[] = [];
try {
for (const task of tasks) {
try {
const contextResult = await this.contextManager.manageContexts(db, {
operation: 'collect',
taskId: task.taskId,
taskType: task.taskType,
title: task.title,
description: task.description
});
results.push({
status: 'success',
taskId: task.taskId,
message: 'Context collected successfully'
});
} catch (error) {
results.push({
status: 'error',
taskId: task.taskId,
message: `Failed to collect context: ${error instanceof Error ? error.message : 'Unknown error'}`
});
}
}
// Record token usage
const inputText = JSON.stringify(args);
const outputText = this.formatWorkflowResults('batch_context_collection', results);
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'batch_context_workflow');
return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`;
} catch (error) {
throw new Error(`Batch context collection workflow failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Status transition workflow
*/
private async executeStatusTransitionWorkflow(db: sqlite3.Database, args: any): Promise<string> {
const { transitions } = args;
if (!transitions || !Array.isArray(transitions)) {
throw new Error('Transitions array is required for status_transition workflow');
}
const results: any[] = [];
try {
for (const transition of transitions) {
try {
// Verify transition is valid if requested
if (transition.verifyTransition) {
const isValid = await this.verifyStatusTransition(transition.itemId, transition.fromStatus, transition.toStatus);
if (!isValid) {
results.push({
status: 'error',
itemId: transition.itemId,
message: `Invalid status transition from ${transition.fromStatus} to ${transition.toStatus}`
});
continue;
}
}
// Determine item type and update status
let updateResult: string;
if (transition.itemId.startsWith('Bug')) {
updateResult = await this.bugManager.updateBugStatus(db, {
itemId: transition.itemId,
status: transition.toStatus
});
} else if (transition.itemId.startsWith('FR-')) {
throw new Error('Feature requests are no longer supported');
} else if (transition.itemId.startsWith('IMP-')) {
updateResult = await this.improvementManager.updateImprovementStatus(db, {
itemId: transition.itemId,
status: transition.toStatus
});
} else {
throw new Error(`Unknown item type for ID: ${transition.itemId}`);
}
results.push({
status: 'success',
itemId: transition.itemId,
message: `Updated to ${transition.toStatus}`
});
} catch (error) {
results.push({
status: 'error',
itemId: transition.itemId,
message: `Failed to update status: ${error instanceof Error ? error.message : 'Unknown error'}`
});
}
}
// Record token usage
const inputText = JSON.stringify(args);
const outputText = this.formatWorkflowResults('status_transition', results);
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'status_transition_workflow');
return `${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`;
} catch (error) {
throw new Error(`Status transition workflow failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Link items together
*/
async linkItems(db: sqlite3.Database, args: any): Promise<string> {
this.tokenTracker.startOperation('link_items');
const { fromItem, toItem, relationshipType } = args;
if (!fromItem || !toItem || !relationshipType) {
throw new Error('fromItem, toItem, and relationshipType are required');
}
const validRelationships = ['blocks', 'relates_to', 'duplicate_of'];
if (!validRelationships.includes(relationshipType)) {
throw new Error(`Invalid relationship type: ${relationshipType}. Must be one of: ${validRelationships.join(', ')}`);
}
// Verify both items exist
const fromExists = await this.itemExists(db, fromItem);
const toExists = await this.itemExists(db, toItem);
if (!fromExists) {
throw new Error(`Source item ${fromItem} not found`);
}
if (!toExists) {
throw new Error(`Target item ${toItem} not found`);
}
return new Promise((resolve, reject) => {
const insertQuery = `
INSERT INTO item_relationships (fromItem, toItem, relationshipType, dateCreated)
VALUES (?, ?, ?, ?)
`;
db.run(insertQuery, [fromItem, toItem, relationshipType, new Date().toISOString()], (err) => {
if (err) {
reject(new Error(`Failed to create relationship: ${err.message}`));
} else {
// Record token usage
const inputText = JSON.stringify(args);
const outputText = `Relationship created: ${fromItem} ${relationshipType} ${toItem}`;
const tokenUsage = this.tokenTracker.recordUsage(inputText, outputText, 'link_items');
resolve(`${outputText}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`);
}
});
});
}
/**
* Get related items
*/
async getRelatedItems(db: sqlite3.Database, args: any): Promise<string> {
this.tokenTracker.startOperation('get_related_items');
const { itemId } = args;
if (!itemId) {
throw new Error('itemId is required');
}
return new Promise((resolve, reject) => {
const query = `
SELECT fromItem, toItem, relationshipType, dateCreated
FROM item_relationships
WHERE fromItem = ? OR toItem = ?
`;
db.all(query, [itemId, itemId], (err, rows: any[]) => {
if (err) {
reject(new Error(`Failed to get related items: ${err.message}`));
} else {
let output = `Related items for ${itemId}:\n\n`;
if (rows.length === 0) {
output += 'No related items found.';
} else {
rows.forEach(row => {
if (row.fromItem === itemId) {
output += `${itemId} ${row.relationshipType} ${row.toItem} (created: ${row.dateCreated})\n`;
} else {
output += `${row.fromItem} ${row.relationshipType} ${itemId} (created: ${row.dateCreated})\n`;
}
});
}
// Record token usage
const inputText = JSON.stringify(args);
const tokenUsage = this.tokenTracker.recordUsage(inputText, output, 'get_related_items');
resolve(`${output}\n\nToken usage: ${tokenUsage.total} tokens (${tokenUsage.input} input, ${tokenUsage.output} output)`);
}
});
});
}
/**
* Check if item exists
*/
private async itemExists(db: sqlite3.Database, itemId: string): Promise<boolean> {
return new Promise((resolve, reject) => {
let query: string;
if (itemId.startsWith('Bug')) {
query = 'SELECT 1 FROM bugs WHERE id = ?';
} else if (itemId.startsWith('FR-')) {
resolve(false);
return;
} else if (itemId.startsWith('IMP-')) {
query = 'SELECT 1 FROM improvements WHERE id = ?';
} else {
resolve(false);
return;
}
db.get(query, [itemId], (err, row) => {
if (err) {
reject(new Error(`Failed to check item existence: ${err.message}`));
} else {
resolve(row !== undefined);
}
});
});
}
/**
* Extract ID from create response
*/
private extractIdFromCreateResponse(response: string, _type: string): string {
const match = response.match(/([A-Z]+-\d+|Bug #\d+)/);
if (match) {
return match[1];
}
throw new Error(`Could not extract ID from response: ${response}`);
}
/**
* Verify status transition is valid
*/
private async verifyStatusTransition(_itemId: string, _fromStatus: string, _toStatus: string): Promise<boolean> {
// Simple validation - could be enhanced with more complex business rules
return true;
}
/**
* Format workflow results
*/
private formatWorkflowResults(workflowType: string, results: any[]): string {
const successCount = results.filter(r => r.status === 'success').length;
const errorCount = results.filter(r => r.status === 'error').length;
let output = `${workflowType} workflow completed:\n`;
output += `- Successful operations: ${successCount}\n`;
output += `- Failed operations: ${errorCount}\n`;
output += `- Total operations: ${results.length}\n\n`;
if (successCount > 0) {
output += 'Successful operations:\n';
results.filter(r => r.status === 'success').forEach(result => {
output += ` ✓ ${result.message}\n`;
});
output += '\n';
}
if (errorCount > 0) {
output += 'Failed operations:\n';
results.filter(r => r.status === 'error').forEach(result => {
output += ` ✗ ${result.message}\n`;
});
}
return output;
}
}