vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
181 lines (180 loc) • 7.71 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import logger from '../../../logger.js';
import { UnifiedSecurityConfigManager } from '../../vibe-task-manager/security/unified-security-config.js';
const execAsync = promisify(exec);
export class ClangdAdapter {
allowedDir;
outputDir;
securityValidator;
cache = new Map();
tempFiles = [];
constructor(allowedDir, outputDir) {
this.allowedDir = allowedDir;
this.outputDir = outputDir;
this.securityValidator = UnifiedSecurityConfigManager.getInstance();
}
async analyzeImports(filePath, options) {
try {
const cacheKey = `${filePath}:${JSON.stringify(options)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
if (!this.securityValidator.validatePathSecurity(filePath, { operation: 'read' })) {
logger.warn({ filePath }, 'File path is outside allowed directory');
return [];
}
const clangdPath = options.clangdPath || await this.findClangdPath();
if (!clangdPath) {
logger.warn('Clangd not found. Please install Clangd and set the path in options.');
return [];
}
const compileCommandsPath = await this.createCompilationDatabase(filePath, options.compileFlags || [], options.includePaths || []);
this.tempFiles.push(compileCommandsPath);
const includes = await this.extractIncludesWithClangd(filePath, clangdPath, compileCommandsPath);
const imports = this.convertToImportInfo(includes, filePath);
await fs.promises.unlink(compileCommandsPath);
const fileIndex = this.tempFiles.indexOf(compileCommandsPath);
if (fileIndex !== -1) {
this.tempFiles.splice(fileIndex, 1);
}
this.cache.set(cacheKey, imports);
return imports;
}
catch (error) {
logger.error({ err: error, filePath }, 'Error analyzing imports with Clangd');
return [];
}
}
async findClangdPath() {
try {
const { stdout } = await execAsync('which clangd || which clangd-15 || which clangd-14 || which clangd-13');
return stdout.trim();
}
catch (error) {
logger.warn({ err: error }, 'Clangd not found in PATH');
return null;
}
}
async createCompilationDatabase(filePath, compileFlags, includePaths) {
const includeFlags = includePaths.map(p => `-I${p}`);
const compileCommandsPath = path.join(this.outputDir, `compile_commands_${Date.now()}.json`);
const compileCommands = [
{
directory: path.dirname(filePath),
command: `clang++ -std=c++17 ${compileFlags.join(' ')} ${includeFlags.join(' ')} -c ${filePath}`,
file: filePath
}
];
await fs.promises.writeFile(compileCommandsPath, JSON.stringify(compileCommands, null, 2));
return compileCommandsPath;
}
async extractIncludesWithClangd(filePath, clangdPath, compileCommandsPath) {
try {
const outputFile = path.join(this.outputDir, `clangd_output_${Date.now()}.json`);
this.tempFiles.push(outputFile);
const command = `${clangdPath} --compile-commands-dir=${path.dirname(compileCommandsPath)} --path-mappings=. --background-index=false --log=verbose --input-style=protocol --pretty --enable-config --query-driver=** --pch-storage=memory ${filePath} > ${outputFile} 2>&1`;
await execAsync(command);
const output = await fs.promises.readFile(outputFile, 'utf8');
const includes = this.parseClangdOutput(output, filePath);
await fs.promises.unlink(outputFile);
const fileIndex = this.tempFiles.indexOf(outputFile);
if (fileIndex !== -1) {
this.tempFiles.splice(fileIndex, 1);
}
return includes;
}
catch (error) {
logger.error({ err: error, filePath }, 'Error extracting includes with Clangd');
return [];
}
}
parseClangdOutput(output, filePath) {
const includes = [];
try {
const includeRegex = /#include\s+[<"]([^>"]+)[>"]/g;
const fileContent = fs.readFileSync(filePath, 'utf8');
let match;
while ((match = includeRegex.exec(fileContent)) !== null) {
const includePath = match[1];
const isSystemInclude = match[0].includes('<');
includes.push({
path: includePath,
isSystemInclude,
});
}
const resolvedPathRegex = /Resolved include\s+([^:]+):\s+([^\s]+)/g;
let resolvedMatch;
while ((resolvedMatch = resolvedPathRegex.exec(output)) !== null) {
const includePath = resolvedMatch[1];
const resolvedPath = resolvedMatch[2];
const include = includes.find(inc => inc.path === includePath);
if (include) {
include.resolvedPath = resolvedPath;
}
}
}
catch (error) {
logger.error({ err: error }, 'Error parsing Clangd output');
}
return includes;
}
convertToImportInfo(includes, _filePath) {
const imports = [];
for (const include of includes) {
const importedItems = [];
importedItems.push({
name: this.extractNameFromPath(include.path),
path: include.resolvedPath || include.path,
isDefault: false,
isNamespace: false,
nodeText: include.isSystemInclude ? `#include <${include.path}>` : `#include "${include.path}"`
});
const importInfo = {
path: include.resolvedPath || include.path,
importedItems,
isCore: include.isSystemInclude,
isExternalPackage: include.isSystemInclude,
moduleSystem: 'c++',
metadata: {
isSystemInclude: include.isSystemInclude,
originalPath: include.path
}
};
imports.push(importInfo);
}
return imports;
}
extractNameFromPath(includePath) {
if (includePath.includes('/')) {
const parts = includePath.split('/');
const filename = parts[parts.length - 1];
return this.removeExtension(filename);
}
return this.removeExtension(includePath);
}
removeExtension(filename) {
const extIndex = filename.lastIndexOf('.');
return extIndex !== -1 ? filename.substring(0, extIndex) : filename;
}
dispose() {
this.cache.clear();
if (this.tempFiles && this.tempFiles.length > 0) {
this.tempFiles.forEach(file => {
try {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
logger.debug({ file }, 'Deleted temporary file during ClangdAdapter disposal');
}
}
catch (error) {
logger.warn({ file, error }, 'Failed to delete temporary file during ClangdAdapter disposal');
}
});
this.tempFiles = [];
}
logger.debug('ClangdAdapter disposed');
}
}