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
JavaScript
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();
}
}