bugger-mcp
Version:
MCP Server for managing bugs, feature requests, and improvements
312 lines • 12.2 kB
JavaScript
import chalk from 'chalk';
// Disable colors in MCP environment since they show as codes
chalk.level = 0;
// Color and formatting utilities for MCP output - simplified without colors
const colors = {
// Status colors - no colors, just return text
red: (text) => text,
green: (text) => text,
yellow: (text) => text,
blue: (text) => text,
orange: (text) => text,
purple: (text) => text,
// Priority colors - no colors, just return text
critical: (text) => text,
high: (text) => text,
medium: (text) => text,
low: (text) => text,
// General formatting - no colors, just return text
highlight: (text) => text,
success: (text) => text,
info: (text) => text,
warning: (text) => text,
error: (text) => text
};
/**
* Unified table formatter for consistent output across all tools
*/
export function formatTable(data, columns, options = {}) {
if (data.length === 0) {
return "No items found.";
}
const { includeHeaders = true, maxContentLength = 50 } = options;
// Calculate column widths
const columnWidths = columns.map(col => {
const headerWidth = col.header.length;
const contentWidth = Math.max(...data.map(item => {
const value = col.formatter ? col.formatter(item[col.key]) : String(item[col.key] || '');
return Math.min(value.length, maxContentLength);
}));
return Math.max(headerWidth, contentWidth, col.width || 0);
});
let output = '';
// Add headers
if (includeHeaders) {
const headerRow = columns.map((col, i) => {
const padding = columnWidths[i] - col.header.length;
return col.header + ' '.repeat(Math.max(0, padding));
}).join(' | ');
const separator = columns.map((_, i) => '-'.repeat(columnWidths[i])).join('--|--');
output += headerRow + '\n';
output += separator + '\n';
}
// Add data rows
data.forEach(item => {
const row = columns.map((col, i) => {
let value = col.formatter ? col.formatter(item[col.key]) : String(item[col.key] || '');
// Truncate long content
if (value.length > maxContentLength) {
value = value.substring(0, maxContentLength - 3) + '...';
}
const padding = columnWidths[i] - value.length;
const align = col.align || 'left';
switch (align) {
case 'right':
return ' '.repeat(Math.max(0, padding)) + value;
case 'center':
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return ' '.repeat(leftPad) + value + ' '.repeat(rightPad);
default: // left
return value + ' '.repeat(Math.max(0, padding));
}
}).join(' | ');
output += row + '\n';
});
return output;
}
/**
* Format error message consistently
*/
export function formatError(message, context) {
let output = colors.error(`Error: ${message}`);
if (context) {
output += `\n${colors.info(`Context: ${context}`)}`;
}
return output;
}
/**
* Format success message consistently
*/
export function formatSuccess(message, details) {
let output = colors.success(message);
if (details) {
output += `\n${colors.info(details)}`;
}
return output;
}
/**
* Format metadata consistently
*/
export function formatMetadata(metadata) {
let output = '';
if (metadata.total !== undefined) {
output += `Total: ${metadata.total}`;
if (metadata.showing !== undefined && metadata.showing !== metadata.total) {
output += ` (showing ${metadata.showing})`;
}
output += '\n';
}
if (metadata.offset !== undefined && metadata.offset > 0) {
output += `Results ${metadata.offset + 1}-${metadata.offset + (metadata.showing || metadata.total)}\n`;
}
return output;
}
/**
* Format token usage consistently
*/
export function formatTokenUsage(usage) {
return `\nToken usage: ${usage.total} tokens (${usage.input} input, ${usage.output} output)`;
}
/**
* Get priority formatter
*/
export function getPriorityFormatter() {
return (priority) => {
switch (priority?.toLowerCase()) {
case 'critical': return colors.critical(priority);
case 'high': return colors.high(priority);
case 'medium': return colors.medium(priority);
case 'low': return colors.low(priority);
default: return priority || '';
}
};
}
/**
* Get status formatter with colors
*/
export function getStatusFormatter() {
return (status) => {
const statusLower = status?.toLowerCase() || '';
if (statusLower.includes('open') || statusLower === 'proposed')
return chalk.red(status);
else if (statusLower.includes('progress') || statusLower.includes('development'))
return chalk.yellow(status);
else if (statusLower.includes('fixed') || statusLower.includes('completed') || statusLower === 'done')
return chalk.green(status);
else if (statusLower.includes('closed') || statusLower === 'rejected')
return chalk.gray(status);
else if (statusLower.includes('blocked'))
return chalk.red.bold(status);
else if (statusLower.includes('discussion') || statusLower.includes('research'))
return chalk.blue(status);
else if (statusLower.includes('approved'))
return chalk.green.bold(status);
return status;
};
}
export function formatBugs(bugs) {
const columns = [
{ key: 'id', header: 'ID', width: 8 },
{ key: 'status', header: 'Status', formatter: getStatusFormatter() },
{ key: 'priority', header: 'Priority', formatter: getPriorityFormatter() },
{ key: 'component', header: 'Component' },
{ key: 'dateReported', header: 'Date', width: 10 },
{ key: 'title', header: 'Title' }
];
return formatTable(bugs, columns);
}
export function formatFeatureRequests(features) {
const columns = [
{ key: 'id', header: 'ID', width: 8 },
{ key: 'status', header: 'Status', formatter: getStatusFormatter() },
{ key: 'priority', header: 'Priority', formatter: getPriorityFormatter() },
{ key: 'category', header: 'Category' },
{ key: 'dateRequested', header: 'Date', width: 10 },
{ key: 'title', header: 'Title' }
];
return formatTable(features, columns);
}
export function formatImprovements(improvements) {
const columns = [
{ key: 'id', header: 'ID', width: 8 },
{ key: 'status', header: 'Status', formatter: getStatusFormatter() },
{ key: 'priority', header: 'Priority', formatter: getPriorityFormatter() },
{ key: 'category', header: 'Category' },
{ key: 'dateRequested', header: 'Date', width: 10 },
{ key: 'title', header: 'Title' }
];
return formatTable(improvements, columns);
}
export function formatImprovementsWithContext(improvements) {
if (improvements.length === 0) {
return "No improvements found.";
}
let output = formatImprovements(improvements);
// Add code context for each improvement
improvements.forEach(improvement => {
if (improvement.codeContext && improvement.codeContext.length > 0) {
output += `\n${colors.highlight(`Code Context for ${improvement.id} - ${improvement.title}`)}\n`;
output += `${colors.info('Description:')} ${improvement.description}\n`;
output += `${colors.info('Current State:')} ${improvement.currentState}\n`;
output += `${colors.info('Desired State:')} ${improvement.desiredState}\n\n`;
improvement.codeContext.forEach((context) => {
output += `${colors.highlight(`File: ${context.file}`)}\n`;
if (context.error) {
output += `${colors.error(context.error)}\n\n`;
}
else {
// Show token-optimized content display
const contentLength = context.content?.length || 0;
const estimatedTokens = Math.ceil(contentLength / 4);
output += `${colors.info(`Content (${contentLength} chars, ~${estimatedTokens} tokens)`)}\n`;
output += '```\n';
output += context.content;
output += '\n```\n\n';
}
});
}
});
return output;
}
export function formatSearchResults(results, metadata) {
if (results.length === 0) {
return "No items found.";
}
let output = '';
// Add metadata header if provided
if (metadata) {
output += `Search Results (${metadata.showing} of ${metadata.total} total)\n`;
if (metadata.offset > 0) {
output += `Showing results ${metadata.offset + 1}-${metadata.offset + metadata.showing}\n`;
}
output += '\n';
}
const formattedResults = results.map(item => {
switch (item.type) {
case 'bug':
return formatBugs([item]);
case 'feature':
return formatFeatureRequests([item]);
case 'improvement':
return formatImprovements([item]);
default:
return JSON.stringify(item, null, 2);
}
}).join('\n\n');
return output + formattedResults;
}
export function formatBulkUpdateResults(results, type) {
const successCount = results.filter(r => r.status === 'success').length;
const errorCount = results.filter(r => r.status === 'error').length;
let output = '';
if (successCount > 0) {
const successfulResults = results.filter(r => r.status === 'success');
output += `${successfulResults.length} ${type} updated successfully:\n\n`;
// Create table-like format
const longestId = Math.max(...successfulResults.map(r => r.bugId?.length || r.featureId?.length || r.improvementId?.length || 0));
const longestStatus = Math.max(...successfulResults.map(r => r.message?.replace('Updated to ', '').length || 0));
successfulResults.forEach(r => {
const itemId = r.bugId || r.featureId || r.improvementId || '';
const status = r.message?.replace('Updated to ', '') || '';
const date = r.dateCompleted || new Date().toISOString().split('T')[0];
output += `${itemId.padEnd(longestId)} | ${status.padEnd(longestStatus)} | ${date}\n`;
});
if (errorCount > 0) {
output += '\n';
}
}
if (errorCount > 0) {
output += `${errorCount} ${type} failed to update:\n\n`;
results.filter(r => r.status === 'error').forEach(r => {
const itemId = r.bugId || r.featureId || r.improvementId || '';
output += `${itemId}: ${r.message}\n`;
});
}
return output;
}
export function formatStatistics(stats) {
let output = 'Project Statistics\n\n';
if (stats.bugs) {
output += `Bugs (${stats.bugs.total} total)\n`;
output += formatStatusAndPriority(stats.bugs);
output += '\n';
}
if (stats.features) {
output += `Feature Requests (${stats.features.total} total)\n`;
output += formatStatusAndPriority(stats.features);
output += '\n';
}
if (stats.improvements) {
output += `Improvements (${stats.improvements.total} total)\n`;
output += formatStatusAndPriority(stats.improvements);
}
return output;
}
function formatStatusAndPriority(category) {
let output = '';
if (category.byStatus) {
output += 'By Status:\n';
for (const [status, count] of Object.entries(category.byStatus)) {
output += ` ${status}: ${count}\n`;
}
}
if (category.byPriority) {
output += 'By Priority:\n';
for (const [priority, count] of Object.entries(category.byPriority)) {
output += ` ${priority}: ${count}\n`;
}
}
return output;
}
//# sourceMappingURL=format.js.map