UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

487 lines (483 loc) 20.4 kB
import { performFormatAwareLlmCall } from '../../../utils/llmHelper.js'; import { getLLMModelForOperation } from '../utils/config-loader.js'; import logger from '../../../logger.js'; export class TagManagementService { static instance; config; tagCache = new Map(); tagHierarchy = new Map(); tagPatterns = new Map(); constructor(config) { this.config = config; this.initializeTagPatterns(); } static getInstance(config) { if (!TagManagementService.instance) { TagManagementService.instance = new TagManagementService(config); } return TagManagementService.instance; } initializeTagPatterns() { this.tagPatterns.set('functional', [ /\b(auth|authentication|login|register|user|session)\b/i, /\b(api|endpoint|route|service|backend)\b/i, /\b(ui|component|frontend|interface|view)\b/i, /\b(database|db|model|schema|migration)\b/i, /\b(security|permission|access|role)\b/i, /\b(video|media|stream|content)\b/i, /\b(notification|email|sms|push)\b/i, /\b(payment|billing|transaction|invoice)\b/i, /\b(search|filter|sort|pagination)\b/i, /\b(report|analytics|dashboard|metrics)\b/i ]); this.tagPatterns.set('technical', [ /\b(react|vue|angular|typescript|javascript)\b/i, /\b(node|express|fastify|koa)\b/i, /\b(postgresql|mysql|mongodb|redis)\b/i, /\b(docker|kubernetes|aws|azure|gcp)\b/i, /\b(jest|vitest|cypress|playwright)\b/i, /\b(webpack|vite|rollup|esbuild)\b/i, /\b(graphql|rest|grpc|websocket)\b/i, /\b(microservice|monolith|serverless)\b/i ]); this.tagPatterns.set('business', [ /\b(high-priority|low-priority|critical|urgent)\b/i, /\b(revenue|cost|profit|roi)\b/i, /\b(customer|user-experience|satisfaction)\b/i, /\b(compliance|regulation|audit|governance)\b/i, /\b(mvp|poc|prototype|pilot)\b/i, /\b(market|competition|strategy|growth)\b/i ]); this.tagPatterns.set('process', [ /\b(planning|development|testing|review)\b/i, /\b(deployment|release|rollback|hotfix)\b/i, /\b(agile|scrum|kanban|waterfall)\b/i, /\b(ci|cd|automation|pipeline)\b/i, /\b(documentation|training|knowledge)\b/i, /\b(maintenance|support|bugfix|enhancement)\b/i ]); this.tagPatterns.set('quality', [ /\b(performance|optimization|speed|efficiency)\b/i, /\b(security|vulnerability|encryption|ssl)\b/i, /\b(accessibility|a11y|wcag|usability)\b/i, /\b(reliability|stability|availability|uptime)\b/i, /\b(maintainability|refactor|cleanup|debt)\b/i, /\b(scalability|load|stress|capacity)\b/i ]); } async createTag(value, category, options = {}) { const tagId = this.generateTagId(value, category); const tag = { id: tagId, value: value.toLowerCase().trim(), category, confidence: options.confidence ?? 1.0, source: options.source ?? 'user', createdAt: new Date(), parentId: options.parentId, metadata: options.metadata }; const validation = await this.validateTag(tag); if (!validation.isValid) { throw new Error(`Invalid tag: ${validation.issues.map(i => i.description).join(', ')}`); } this.tagCache.set(tagId, tag); if (tag.parentId) { this.updateTagHierarchy(tag.parentId, tagId); } logger.debug({ tag }, 'Created new tag'); return tag; } async suggestTags(descriptionOrContent, taskOrOptions) { if (typeof descriptionOrContent === 'string' && taskOrOptions && 'id' in taskOrOptions) { const task = taskOrOptions; try { const content = { title: task.title, description: descriptionOrContent, type: task.type, existingTags: task.tags || [] }; const suggestions = await this.suggestTagsInternal(content, { useAI: false }); const tagCollection = { functional: [], technical: [], business: [], process: [], quality: [], custom: [], generated: [] }; let confidence = 0; let totalSuggestions = 0; for (const suggestion of suggestions) { if (suggestion.confidence > 0.5) { this.addTagToCollection(tagCollection, suggestion.tag); confidence += suggestion.confidence; totalSuggestions++; } } return { success: true, tags: tagCollection, source: 'pattern', confidence: totalSuggestions > 0 ? confidence / totalSuggestions : 0 }; } catch (error) { logger.error({ err: error, taskId: task.id }, 'Failed to suggest tags for task'); return { success: false, source: 'error', confidence: 0 }; } } const content = descriptionOrContent; const options = taskOrOptions; return this.suggestTagsInternal(content, options || {}); } async suggestTagsInternal(content, options = {}) { const suggestions = []; const maxSuggestions = options.maxSuggestions ?? 10; const useAI = options.useAI ?? true; try { const patternSuggestions = await this.getPatternBasedSuggestions(content); suggestions.push(...patternSuggestions); if (useAI) { const aiSuggestions = await this.getAIBasedSuggestions(content); suggestions.push(...aiSuggestions); } const similaritySuggestions = await this.getSimilarityBasedSuggestions(content); suggestions.push(...similaritySuggestions); const uniqueSuggestions = this.deduplicateSuggestions(suggestions); const filteredSuggestions = this.filterSuggestionsByCategory(uniqueSuggestions, options.categories); return filteredSuggestions .sort((a, b) => b.confidence - a.confidence) .slice(0, maxSuggestions); } catch (error) { logger.error({ err: error, content }, 'Failed to suggest tags'); return []; } } async validateTag(tag) { const issues = []; const suggestions = []; if (this.tagCache.has(tag.id)) { issues.push({ type: 'duplicate', description: `Tag '${tag.value}' already exists`, severity: 'error' }); } if (!/^[a-z0-9-_]+$/.test(tag.value)) { issues.push({ type: 'naming_convention', description: 'Tag must contain only lowercase letters, numbers, hyphens, and underscores', severity: 'error' }); suggestions.push(tag.value.toLowerCase().replace(/[^a-z0-9-_]/g, '-')); } if (tag.parentId && !this.tagCache.has(tag.parentId)) { issues.push({ type: 'hierarchy_conflict', description: `Parent tag '${tag.parentId}' does not exist`, severity: 'error' }); } const validCategories = ['functional', 'technical', 'business', 'process', 'quality', 'custom', 'generated']; if (!validCategories.includes(tag.category)) { issues.push({ type: 'invalid_category', description: `Invalid category '${tag.category}'`, severity: 'error' }); } return { isValid: issues.filter(i => i.severity === 'error').length === 0, issues, suggestions }; } async enhanceTagCollection(content, existingTags = []) { const collection = { functional: [], technical: [], business: [], process: [], quality: [], custom: [], generated: [] }; for (const tagValue of existingTags) { const category = await this.categorizeTag(tagValue); const tag = await this.createOrGetTag(tagValue, category, 'user'); this.addTagToCollection(collection, tag); } const suggestions = await this.suggestTags(content, { maxSuggestions: 15 }); for (const suggestion of suggestions) { if (suggestion.confidence > 0.7) { this.addTagToCollection(collection, suggestion.tag); } } return collection; } async getTagAnalytics(filters = {}) { const period = filters.dateRange ?? { start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), end: new Date() }; return { popular: await this.getPopularTags(filters), trends: await this.getTagTrends(filters), distribution: await this.getTagDistribution(filters), relationships: await this.getTagRelationships(filters), orphaned: await this.getOrphanedTags(filters), period }; } async searchTags(filters) { let tags = Array.from(this.tagCache.values()); if (filters.query) { const query = filters.query.toLowerCase(); tags = tags.filter(tag => tag.value.includes(query) || (tag.metadata && JSON.stringify(tag.metadata).toLowerCase().includes(query))); } if (filters.categories) { tags = tags.filter(tag => filters.categories.includes(tag.category)); } if (filters.sources) { tags = tags.filter(tag => filters.sources.includes(tag.source)); } if (filters.minConfidence) { tags = tags.filter(tag => tag.confidence >= filters.minConfidence); } if (filters.createdAfter) { tags = tags.filter(tag => tag.createdAt >= filters.createdAfter); } if (filters.createdBefore) { tags = tags.filter(tag => tag.createdAt <= filters.createdBefore); } return tags.sort((a, b) => b.confidence - a.confidence); } generateTagId(value, category) { const normalizedValue = value.toLowerCase().replace(/[^a-z0-9]/g, ''); return `${category}-${normalizedValue}-${Date.now()}`; } async getPatternBasedSuggestions(content) { const suggestions = []; const text = `${content.title} ${content.description}`.toLowerCase(); const keywords = [ { words: ['auth', 'authentication', 'login', 'register', 'user', 'session'], category: 'functional' }, { words: ['api', 'endpoint', 'route', 'service', 'backend'], category: 'functional' }, { words: ['ui', 'component', 'frontend', 'interface', 'view'], category: 'functional' }, { words: ['react', 'vue', 'angular', 'typescript', 'javascript'], category: 'technical' }, { words: ['node', 'express', 'fastify', 'koa'], category: 'technical' }, { words: ['high-priority', 'low-priority', 'critical', 'urgent'], category: 'business' }, { words: ['development', 'testing', 'review', 'deployment', 'documentation'], category: 'process' }, { words: ['performance', 'security', 'accessibility', 'reliability'], category: 'quality' } ]; for (const { words, category } of keywords) { for (const word of words) { if (text.includes(word)) { try { const tag = await this.createOrGetTag(word, category, 'system'); suggestions.push({ tag, confidence: 0.8, reasoning: `Keyword match for ${category} category`, source: 'pattern' }); } catch (error) { logger.debug({ error, word }, 'Failed to create tag from keyword'); } } } } return suggestions; } async getAIBasedSuggestions(content) { try { await getLLMModelForOperation('tag_suggestion'); const prompt = `Analyze the following task and suggest relevant tags: Title: ${content.title} Description: ${content.description} Type: ${content.type || 'unknown'} Please suggest 5-8 relevant tags categorized as: - functional (features, domains, capabilities) - technical (technologies, patterns, architecture) - business (priority, impact, value) - process (workflow, methodology, stage) - quality (performance, security, usability) Respond with JSON format: { "suggestions": [ { "tag": "tag-name", "category": "functional|technical|business|process|quality", "confidence": 0.9, "reasoning": "why this tag is relevant" } ] }`; const response = await performFormatAwareLlmCall(prompt, 'You are a helpful AI assistant that suggests relevant tags for tasks.', this.config, 'tag_suggestion', 'json'); const parsed = JSON.parse(response); const suggestions = []; for (const suggestion of parsed.suggestions || []) { const tag = await this.createOrGetTag(suggestion.tag, suggestion.category, 'ai'); suggestions.push({ tag, confidence: suggestion.confidence || 0.7, reasoning: suggestion.reasoning || 'AI suggestion', source: 'ai' }); } return suggestions; } catch (error) { logger.error({ err: error }, 'Failed to get AI-based tag suggestions'); return []; } } async getSimilarityBasedSuggestions(content) { const suggestions = []; const keywords = this.extractKeywords(`${content.title} ${content.description}`); for (const keyword of keywords) { const existingTag = Array.from(this.tagCache.values()) .find(tag => tag.value.includes(keyword) || keyword.includes(tag.value)); if (existingTag) { suggestions.push({ tag: existingTag, confidence: 0.6, reasoning: `Similar to existing tag: ${existingTag.value}`, source: 'similarity' }); } } return suggestions; } extractKeywords(text) { const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by']); return text .toLowerCase() .replace(/[^a-z0-9\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2 && !stopWords.has(word)) .slice(0, 20); } async categorizeTag(value) { const lowerValue = value.toLowerCase(); const functionalKeywords = ['auth', 'authentication', 'login', 'register', 'user', 'session', 'api', 'endpoint', 'route', 'service', 'backend', 'ui', 'component', 'frontend', 'interface', 'view']; if (functionalKeywords.some(keyword => lowerValue.includes(keyword))) { return 'functional'; } const technicalKeywords = ['react', 'vue', 'angular', 'typescript', 'javascript', 'node', 'express', 'fastify', 'koa']; if (technicalKeywords.some(keyword => lowerValue.includes(keyword))) { return 'technical'; } const businessKeywords = ['high-priority', 'low-priority', 'critical', 'urgent', 'priority']; if (businessKeywords.some(keyword => lowerValue.includes(keyword))) { return 'business'; } const processKeywords = ['development', 'testing', 'review', 'deployment', 'documentation']; if (processKeywords.some(keyword => lowerValue.includes(keyword))) { return 'process'; } const qualityKeywords = ['performance', 'security', 'accessibility', 'reliability']; if (qualityKeywords.some(keyword => lowerValue.includes(keyword))) { return 'quality'; } return 'custom'; } async createOrGetTag(value, category, source) { const normalizedValue = value.toLowerCase().trim(); const existingTag = Array.from(this.tagCache.values()) .find(tag => tag.value === normalizedValue && tag.category === category); if (existingTag) { return existingTag; } return this.createTag(normalizedValue, category, { source }); } addTagToCollection(collection, tag) { switch (tag.category) { case 'functional': collection.functional.push(tag); break; case 'technical': collection.technical.push(tag); break; case 'business': collection.business.push(tag); break; case 'process': collection.process.push(tag); break; case 'quality': collection.quality.push(tag); break; case 'custom': collection.custom.push(tag); break; case 'generated': collection.generated.push(tag); break; } } deduplicateSuggestions(suggestions) { const seen = new Set(); return suggestions.filter(suggestion => { const key = `${suggestion.tag.value}-${suggestion.tag.category}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } filterSuggestionsByCategory(suggestions, categories) { if (!categories || categories.length === 0) { return suggestions; } return suggestions.filter(suggestion => categories.includes(suggestion.tag.category)); } updateTagHierarchy(parentId, childId) { if (!this.tagHierarchy.has(parentId)) { this.tagHierarchy.set(parentId, []); } this.tagHierarchy.get(parentId).push(childId); } async getPopularTags(_filters) { return [ { tag: 'auth', count: 45, percentage: 15.2, category: 'functional', trend: 'increasing' }, { tag: 'api', count: 38, percentage: 12.8, category: 'functional', trend: 'stable' }, { tag: 'ui', count: 32, percentage: 10.8, category: 'functional', trend: 'increasing' }, { tag: 'react', count: 28, percentage: 9.4, category: 'technical', trend: 'stable' }, { tag: 'database', count: 25, percentage: 8.4, category: 'functional', trend: 'decreasing' } ]; } async getTagTrends(_filters) { return []; } async getTagDistribution(_filters) { return [ { category: 'functional', count: 120, percentage: 40.0, averageUsage: 8.5 }, { category: 'technical', count: 85, percentage: 28.3, averageUsage: 6.2 }, { category: 'business', count: 45, percentage: 15.0, averageUsage: 4.1 }, { category: 'process', count: 30, percentage: 10.0, averageUsage: 3.8 }, { category: 'quality', count: 20, percentage: 6.7, averageUsage: 2.9 } ]; } async getTagRelationships(_filters) { return []; } async getOrphanedTags(_filters) { return []; } async cleanup() { this.tagCache.clear(); this.tagHierarchy.clear(); this.tagPatterns.clear(); } }