UNPKG

mcp-booster

Version:

Servidor MCP com CoConuT (Continuous Chain of Thought) para uso com Cursor IDE - Pacote Global NPM

620 lines (617 loc) 24.9 kB
"use strict"; /** * Storage and conclusion module for CoConuT * Generates conclusions and explanations of changes made in the chain of thoughts */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CoConuT_Storage = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const logger_1 = require("./logger"); /** * Classe responsável por gerar conclusões e salvar o histórico final de pensamentos */ class CoConuT_Storage { constructor(storageProvider, config) { this.locale = 'en'; // Default locale this.templates = new Map(); this.searchIndex = new Map(); /** * Mensagens internacionalizadas do sistema */ this.i18n = { 'error.no.path': { 'en': 'No path provided to save files', 'pt': 'Nenhum caminho foi fornecido para salvar os arquivos' }, 'info.adding.content': { 'en': 'Adding new entry to existing conclusion file', 'pt': 'Adicionando nova entrada ao arquivo de conclusão existente' }, 'info.creating.file': { 'en': 'Creating new conclusion file', 'pt': 'Criando novo arquivo de conclusão' }, 'error.saving.conclusion': { 'en': 'Error saving conclusion', 'pt': 'Erro ao salvar conclusão' }, 'error.adding.conclusion': { 'en': 'Error adding text to conclusion file', 'pt': 'Erro ao adicionar texto ao arquivo de conclusão' }, 'error.processing.conclusion': { 'en': 'Failed to process conclusion', 'pt': 'Falha ao processar conclusão' } }; this.storageProvider = storageProvider; this.config = config; this.logger = logger_1.Logger.getInstance(); // Verificar se o caminho do projeto está presente if (!config.projectPath) { this.logger.warn('Booster_Storage: No project path configured. It will need to be provided in processConclusion'); } // Registrar o template padrão this.registerDefaultTemplates(); } /** * Método para traduzir mensagens * @param key Chave da mensagem no dicionário * @param replacements Substituições a serem feitas na mensagem * @returns A mensagem traduzida */ t(key, replacements = {}) { const message = this.i18n[key]?.[this.locale] || key; return message.replace(/\{(\w+)\}/g, (_, k) => replacements[k] || `{${k}}`); } /** * Define o idioma a ser usado pelo sistema * @param locale Código do idioma (en, pt, etc) */ setLocale(locale) { if (Object.keys(this.i18n).some(key => this.i18n[key][locale])) { this.locale = locale; this.logger.info(`Locale set to ${locale}`); } else { this.logger.warn(`Locale ${locale} not supported, using ${this.locale}`); } } /** * Registra templates padrão */ registerDefaultTemplates() { // Criar mapa de emojis para os diferentes tipos de categoria const emojiMap = { 'feature': '✨', 'docs': '📝', 'test': '🧪', 'refactor': '♻️', 'fix': '🐛', 'chore': '🔧', 'style': '💄', 'perf': '⚡' }; // Template único semântico como template default this.registerTemplate('default', `## {emoji} {category} | {impactLevel} [ID:{id}] **Why:** {whyChange} **What:** {whatChange} **Files:** {affectedFilesInline} <!-- metadata --> `); } /** * Registra um novo template * @param name Nome do template * @param template Template em formato de string com placeholders */ registerTemplate(name, template) { this.templates.set(name, { name, template }); this.logger.info(`Template ${name} registered`); } /** * Renderiza um template com os dados fornecidos * @param templateName Nome do template a ser renderizado * @param data Dados a serem inseridos no template * @returns Template renderizado */ renderTemplate(templateName, data) { const template = this.templates.get(templateName); if (!template) { this.logger.warn(`Template ${templateName} not found, using default`); return this.renderTemplate('default', data); } // Renderização simples com substituição de placeholders return template.template.replace(/\{(\w+)\}/g, (_, key) => { if (data[key] === undefined) return `{${key}}`; if (Array.isArray(data[key])) { if (data[key].length === 0) return ''; return data[key].map((item) => `- ${item}`).join('\n'); } return data[key].toString(); }); } /** * Adiciona conteúdo ao índice de busca * @param id ID único da conclusão * @param content Conteúdo a ser indexado */ indexContent(id, content) { try { // Extrair palavras do conteúdo const words = content.toLowerCase() .replace(/[^\p{L}\s]/gu, ' ') // Remove caracteres não-alfabéticos .split(/\s+/) .filter(word => word.length > 3); // Ignorar palavras muito curtas // Adicionar ao índice words.forEach(word => { if (!this.searchIndex.has(word)) { this.searchIndex.set(word, new Set()); } this.searchIndex.get(word)?.add(id); }); this.logger.debug(`Indexed ${words.length} unique words for conclusion ${id}`); } catch (error) { this.logger.error('Error indexing content', { error }); } } /** * Busca conclusões no índice * @param query Query de busca * @returns Array de IDs de conclusões ordenados por relevância */ searchConclusions(query) { try { const searchTerms = query.toLowerCase() .replace(/[^\p{L}\s]/gu, ' ') .split(/\s+/) .filter(word => word.length > 3); const results = new Map(); // Buscar cada termo searchTerms.forEach(term => { const ids = this.searchIndex.get(term); if (ids) { ids.forEach(id => { results.set(id, (results.get(id) || 0) + 1); }); } }); // Ordenar por relevância e retornar IDs return Array.from(results.entries()) .sort((a, b) => b[1] - a[1]) .map(([id]) => id); } catch (error) { this.logger.error('Error searching conclusions', { error }); return []; } } /** * Gera uma conclusão e salva o histórico de pensamentos * @param thoughts Histórico de pensamentos para processar * @param projectPath Caminho do projeto onde os arquivos serão salvos * @param whyChange Motivo da mudança * @param whatChange Descrição da mudança * @param params Parâmetros adicionais opcionais * @returns Array com informações dos arquivos salvos */ async processConclusion(thoughts, projectPath, whyChange, whatChange, params) { try { // Verificar e configurar o caminho do projeto if (!projectPath) { throw new Error(this.t('error.no.path')); } // Atualizar o caminho do projeto na configuração this.config.projectPath = projectPath; // Mesclar parâmetros recebidos const fullParams = { ...params, projectPath, WhyChange: whyChange, WhatChange: whatChange }; // Gerar conclusão baseada nos parâmetros fornecidos const conclusion = this.generateCustomConclusion(whyChange, whatChange, fullParams); // Adicionar a conclusão como um metadado ao último pensamento const lastThought = thoughts[thoughts.length - 1]; if (lastThought) { lastThought.metadata = lastThought.metadata || {}; lastThought.metadata.conclusion = conclusion; lastThought.metadata.whyChange = whyChange; lastThought.metadata.whatChange = whatChange; // Adicionar metadados enriquecidos if (params) { lastThought.metadata.category = params.category; lastThought.metadata.tags = params.tags; lastThought.metadata.impactLevel = params.impactLevel; } } // Salvar todos os pensamentos const savedFiles = []; for (const thought of thoughts) { try { const fileInfo = await this.storageProvider.saveThought(thought); if (fileInfo) { savedFiles.push(fileInfo); } } catch (thoughtError) { this.logger.error('Error saving individual thought', { thoughtError, thoughtNumber: thought.thoughtNumber }); // Continuar tentando salvar os outros pensamentos } } // Registrar a conclusão em um arquivo separado const conclusionFileInfo = await this.saveConclusion(conclusion, projectPath); if (conclusionFileInfo) { savedFiles.push(conclusionFileInfo); } return savedFiles; } catch (error) { this.logger.error(this.t('error.processing.conclusion'), { error }); throw new Error(`${this.t('error.processing.conclusion')}: ${error?.message || 'Unknown error'}`); } } /** * Automatically records a summary of the current interaction in the conclusion.md file * @param projectPath Project path where the files will be saved * @param interactionSummary An object containing information about the current interaction * @returns Information about the saved file or null in case of error */ async appendInteractionSummary(projectPath, interactionSummary) { try { // Verify and configure the project path if (!projectPath) { throw new Error("A path must be provided to save the interaction summary"); } // Create interaction summary text const summary = `## Interaction Summary ${interactionSummary.thoughtNumber}/${interactionSummary.totalThoughts} ### Why it was done ${interactionSummary.why} ### What was done ${interactionSummary.what}`; // Save the summary to the conclusion.md file return await this.appendToConclusion(summary, projectPath); } catch (error) { this.logger.error('Error recording interaction summary', { error }); return null; } } /** * Gera metadados para a conclusão a partir dos parâmetros fornecidos * @param thoughts Array de pensamentos (opcional) * @param params Parâmetros adicionais * @returns Objeto de metadados estruturado */ generateMetadata(thoughts, params) { try { const now = new Date(); const metadata = { id: `conclusion-${now.getTime()}`, timestamp: now.toISOString(), category: params?.category || 'docs', subCategories: params?.subCategories || [], tags: params?.tags || [], impactLevel: (params?.impactLevel || 'medium'), affectedFiles: params?.affectedFiles || [], relatedConclusions: params?.relatedConclusions || [], ticketReference: params?.ticketReference || '', businessContext: params?.businessContext || '', technicalContext: params?.technicalContext || '' }; // Adicionar números dos pensamentos se disponíveis if (thoughts && thoughts.length > 0) { metadata.thoughtNumbers = thoughts.map(t => t.thoughtNumber); } return metadata; } catch (error) { this.logger.error('Error generating metadata', { error }); // Retornar metadados mínimos em caso de erro return { id: `conclusion-${Date.now()}`, timestamp: new Date().toISOString(), category: 'docs', subCategories: [], tags: [], impactLevel: 'medium', affectedFiles: [], relatedConclusions: [] }; } } /** * Formata a lista de arquivos afetados */ formatAffectedFiles(files) { try { if (!files || files.length === 0) { return 'No affected files specified.'; } return files.map(file => `- \`${file}\``).join('\n'); } catch (error) { return 'Error formatting affected files.'; } } /** * Formata a lista de arquivos afetados em uma única linha */ formatAffectedFilesInline(files) { try { if (!files || files.length === 0) { return 'No affected files specified.'; } return files.map(file => `\`${file}\``).join(', '); } catch (error) { return 'Error formatting affected files.'; } } /** * Formata as alternativas consideradas */ formatAlternatives(alternatives) { try { if (!alternatives || alternatives.length === 0) { return 'No alternatives considered were specified.'; } return alternatives.map((alt, i) => `${i + 1}. ${alt}`).join('\n'); } catch (error) { return 'Error formatting considered alternatives.'; } } /** * Formata as conclusões relacionadas */ formatRelatedConclusions(conclusions) { try { if (!conclusions || conclusions.length === 0) { return 'No related conclusions.'; } return conclusions.map(ref => `- ${ref}`).join('\n'); } catch (error) { return 'Error formatting related conclusions.'; } } /** * Formata os snippets de código */ formatCodeSnippets(snippets) { try { if (!snippets || snippets.length === 0) return ''; let result = `\n### 💻 Code Changes\n`; snippets.forEach((snippet, index) => { result += `#### Change ${index + 1} in \`${snippet.file}\`\n`; result += `**Before:**\n\`\`\`\n${snippet.before}\n\`\`\`\n\n`; result += `**After:**\n\`\`\`\n${snippet.after}\n\`\`\`\n\n`; }); return result; } catch (error) { return '\n### 💻 Error formatting code snippets\n'; } } /** * Salva a conclusão em um arquivo separado * @param conclusion Texto da conclusão a ser salva * @param projectPath Caminho do projeto onde o arquivo será salvo */ async saveConclusion(conclusion, projectPath) { try { // Verificar caminho if (!projectPath) { throw new Error(this.t('error.no.path')); } // Usar o método comum para preparar e salvar o conteúdo return await this.prepareAndSaveContent(conclusion, projectPath, true); } catch (error) { this.logger.error(this.t('error.saving.conclusion'), { error }); return null; } } /** * Adiciona texto ao arquivo de conclusão sem substituir o conteúdo existente * @param text Texto a ser adicionado ao arquivo de conclusão * @param projectPath Caminho do projeto onde o arquivo será salvo */ async appendToConclusion(text, projectPath) { try { // Verificar caminho if (!projectPath) { throw new Error(this.t('error.no.path')); } // Usar o método comum para preparar e salvar o conteúdo return await this.prepareAndSaveContent(text, projectPath, true); } catch (error) { this.logger.error(this.t('error.adding.conclusion'), { error }); return null; } } /** * Prepara e salva conteúdo no arquivo de conclusão * @param text Texto a ser salvo * @param projectPath Caminho do projeto onde salvar * @param isNewEntry Se é uma entrada nova (adiciona timestamp) * @returns Informações sobre o arquivo salvo ou null em caso de erro */ async prepareAndSaveContent(text, projectPath, isNewEntry = true) { try { // Verificar caminho do projeto if (!projectPath) { throw new Error(this.t('error.no.path')); } // Criar caminhos const storageDir = path.resolve(projectPath, 'booster-data'); const conclusionPath = path.resolve(storageDir, 'conclusion.md'); // Garantir que o diretório existe if (!fs.existsSync(storageDir)) { fs.mkdirSync(storageDir, { recursive: true }); } // Verificar se o texto já possui um cabeçalho semântico com emoji const hasSemanticHeader = /^## [✨📝🧪♻️🐛🔧💄⚡]/i.test(text); // Preparar o conteúdo com timestamp se for uma nova entrada let processedText = text; if (isNewEntry && !hasSemanticHeader) { const now = new Date(); const dateStr = now.toLocaleDateString(); const timeStr = now.toLocaleTimeString(); const timestamp = `## Entrada em ${dateStr} às ${timeStr}`; processedText = `${timestamp}\n\n${text}`; this.logger.debug('Adding timestamp to regular conclusion'); } else if (isNewEntry && hasSemanticHeader) { // Para templates semânticos, manter o formato original this.logger.debug('Preserving semantic header format'); } let finalContent = ""; const separator = "\n\n---\n\n"; // Verificar e adicionar conteúdo existente if (fs.existsSync(conclusionPath)) { const existingContent = await fs.promises.readFile(conclusionPath, 'utf-8'); finalContent = existingContent + separator + processedText; this.logger.info(this.t('info.adding.content')); } else { finalContent = processedText; this.logger.info(this.t('info.creating.file')); } // Salvar o conteúdo await fs.promises.writeFile(conclusionPath, finalContent); // Se for uma conclusão completa, indexar o conteúdo para busca const conclusionId = `conclusion-${Date.now()}`; if (text.includes('## Conclusão da Cadeia de Pensamentos') || hasSemanticHeader) { this.indexContent(conclusionId, text); } return { filePath: conclusionPath, type: 'conclusion', timestamp: Date.now() }; } catch (error) { const errorType = isNewEntry ? 'error.saving.conclusion' : 'error.adding.conclusion'; this.logger.error(this.t(errorType), { error }); return null; } } /** * Generates a custom conclusion based on the provided parameters * @param whyChange Reason for the change * @param whatChange Description of the change * @param params Additional parameters * @returns Formatted conclusion string */ generateCustomConclusion(whyChange, whatChange, params) { try { // Gerar metadados const metadata = this.generateMetadata(undefined, params); // Serializar metadados para armazenamento const metadataJson = JSON.stringify(metadata, null, 2); // Mapa de emojis para categorias const emojiMap = { 'feature': '✨', 'docs': '📝', 'test': '🧪', 'refactor': '♻️', 'fix': '🐛', 'chore': '🔧', 'style': '💄', 'perf': '⚡' }; // Obter a categoria e seu emoji correspondente const category = params?.category || 'feature'; const emoji = emojiMap[category] || '✨'; // Preparar dados para o template const templateData = { ...metadata, emoji, whyChange, whatChange, context: this.formatContext(metadata), affectedFiles: this.formatAffectedFiles(metadata.affectedFiles), affectedFilesInline: this.formatAffectedFilesInline(metadata.affectedFiles), alternatives: this.formatAlternatives(params?.alternativesConsidered || []), testing: params?.testingPerformed || '', relatedConclusions: this.formatRelatedConclusions(metadata.relatedConclusions) }; // Sempre usar o template default let templateName = 'default'; // Renderizar com o template selecionado let markdown = this.renderTemplate(templateName, templateData); // Armazenar metadados JSON em comentário HTML para facilitar extração programática // Para templates compactos, apenas adicionar o ID nos metadados markdown = markdown.replace('<!-- metadata -->', `<!-- metadata:${metadata.id} -->`); // Registrar qual template foi usado this.logger.info(`Using template: ${templateName} for category: ${category}`); return markdown; } catch (error) { this.logger.error('Error generating custom conclusion', { error }); // Fallback simples em caso de erro return `## Conclusão\n**Por que:** ${whyChange}\n**O que:** ${whatChange}\n`; } } /** * Formata a seção de contexto */ formatContext(metadata) { try { let context = ''; if (metadata.businessContext) { context += `#### Business Context\n${metadata.businessContext}\n\n`; } if (metadata.technicalContext) { context += `#### Technical Context\n${metadata.technicalContext}\n\n`; } return context || 'No additional context provided.'; } catch (error) { return 'Error formatting context.'; } } } exports.CoConuT_Storage = CoConuT_Storage;