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
JavaScript
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 '';
}
}