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.

157 lines (156 loc) 6.33 kB
import logger from '../../logger.js'; import path from 'path'; import fs from 'fs/promises'; import { writeFileSecure } from './fsUtils.js'; import { getOutputDirectory } from './directoryUtils.js'; function sanitizeMermaidText(text) { return text.replace(/["`]/g, "'").replace(/\n/g, ' ').substring(0, 80); } function generateSafeMermaidId(id) { const safeId = id.replace(/[^a-zA-Z0-9]/g, '_'); if (safeId.length > 30) { return `${safeId.substring(0, 20)}_${Math.abs(hashString(id) % 10000)}`; } return safeId; } function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash; } export function extractMethodCallSequences(nodes, edges, maxCalls = 0) { if (maxCalls === 0) { return []; } const functionNodes = nodes.filter(n => n.type === 'function' || n.type === 'method'); const nodeMap = new Map(); functionNodes.forEach(node => nodeMap.set(node.id, node)); const callEdges = edges.filter(e => nodeMap.has(e.from) && nodeMap.has(e.to) && e.label?.includes('call')); const sortedEdges = [...callEdges].sort((a, b) => { const nodeA = nodeMap.get(a.from); const nodeB = nodeMap.get(b.from); if (!nodeA || !nodeB) return 0; if (nodeA.filePath !== nodeB.filePath) { return (nodeA.filePath || '').localeCompare(nodeB.filePath || ''); } return 0; }); const limitedEdges = sortedEdges.slice(0, maxCalls); const methodCalls = limitedEdges.map((edge, index) => { const fromNode = nodeMap.get(edge.from); const toNode = nodeMap.get(edge.to); const isAsync = fromNode?.comment?.includes('async') || edge.from.includes('Async') || edge.to.includes('Async'); const fromName = fromNode?.label?.split(' — ')[0] || edge.from.split('::').pop() || edge.from; const toName = toNode?.label?.split(' — ')[0] || edge.to.split('::').pop() || edge.to; return { from: edge.from, to: edge.to, message: `${fromName} calls ${toName}`, isAsync, order: index }; }); return methodCalls; } export function extractParticipants(methodCalls, nodes) { const participantIds = new Set(); methodCalls.forEach(call => { participantIds.add(call.from); participantIds.add(call.to); }); const nodeMap = new Map(); nodes.forEach(node => nodeMap.set(node.id, node)); const participants = Array.from(participantIds).map(id => { const node = nodeMap.get(id); const label = node?.label?.split(' — ')[0] || id.split('::').pop() || id; const type = node?.type || 'function'; return { id: generateSafeMermaidId(id), label: sanitizeMermaidText(label), type, filePath: node?.filePath }; }); return participants; } export function generateSequenceDiagram(methodCalls, participants) { if (methodCalls.length === 0 || participants.length === 0) { return 'sequenceDiagram\n Note over System: No method calls detected'; } let mermaidString = 'sequenceDiagram\n'; participants.forEach(participant => { if (participant.type === 'class') { mermaidString += ` participant ${participant.id} as "${participant.label}"\n`; } else { mermaidString += ` participant ${participant.id} as "${participant.label}"\n`; } }); methodCalls.forEach(call => { const fromId = generateSafeMermaidId(call.from); const toId = generateSafeMermaidId(call.to); const arrow = call.isAsync ? '-)' : '->>'; const cleanMessage = sanitizeMermaidText(call.message); mermaidString += ` ${fromId}${arrow}${toId}: ${cleanMessage}\n`; }); return mermaidString; } export function optimizeSequenceDiagram(diagram, maxParticipants = 10) { if (!diagram.includes('participant')) { return diagram; } const lines = diagram.split('\n'); const participantLines = lines.filter(line => line.trim().startsWith('participant')); if (participantLines.length > maxParticipants) { const keptParticipants = participantLines.slice(0, maxParticipants); const keptIds = keptParticipants.map(line => { const match = line.match(/participant\s+([^\s]+)/); return match ? match[1] : ''; }).filter(id => id !== ''); const filteredLines = lines.filter(line => { if (line.trim() === 'sequenceDiagram') return true; if (line.trim().startsWith('participant')) { return keptIds.some(id => line.includes(`participant ${id}`)); } if (line.trim().includes('->') || line.trim().includes('-x') || line.trim().includes('-)')) { return keptIds.some(id => line.trim().startsWith(id)) && keptIds.some(id => line.includes(`${id}:`)); } return true; }); filteredLines.push(` Note over ${keptIds[0]}: Diagram limited to ${maxParticipants} participants for readability`); return filteredLines.join('\n'); } return diagram; } export async function processAndStoreSequenceDiagram(methodCalls, participants, config, jobId) { try { const diagram = generateSequenceDiagram(methodCalls, participants); const optimizedDiagram = optimizeSequenceDiagram(diagram); const outputDir = config.output?.outputDir || getOutputDirectory(config); const diagramsDir = path.join(outputDir, 'diagrams'); await fs.mkdir(diagramsDir, { recursive: true }); const diagramPath = path.join(diagramsDir, `sequence-diagram-${jobId}.md`); await writeFileSecure(diagramPath, optimizedDiagram, config.allowedMappingDirectory, 'utf-8', outputDir); logger.debug(`Sequence diagram saved to ${diagramPath}`); return diagramPath; } catch (error) { logger.error({ err: error }, 'Failed to process and store sequence diagram'); return ''; } }