UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

428 lines (378 loc) 13.1 kB
import { z } from 'zod'; import { BaseTool } from './base-tool.js'; import { join, relative, isAbsolute } from 'path'; // Cache for import results to prevent circular dependency issues const importCache = new Map<string, any>(); // Function to dynamically import eslint with enhanced error handling async function tryImportESLint(): Promise<any> { const cacheKey = 'eslint'; if (importCache.has(cacheKey)) { const cached = importCache.get(cacheKey); return cached === 'loading' ? null : cached; } try { // Set loading state to prevent circular imports importCache.set(cacheKey, 'loading'); const eslintModule = await import('eslint'); // Validate that ESLint was imported correctly if (!eslintModule || !eslintModule.ESLint) { throw new Error('ESLint module imported but ESLint class not found'); } const ESLint = eslintModule.ESLint; // Cache the successful result importCache.set(cacheKey, ESLint); return ESLint; } catch (error) { // Cache the failure to prevent repeated attempts importCache.set(cacheKey, null); // Provide specific error messages for common issues if (error instanceof Error) { if (error.message.includes('Cannot find module')) { console.warn('ESLint is not installed. Install with: npm install eslint'); } else if (error.message.includes('circular dependency')) { console.warn('Circular dependency detected when importing ESLint'); } else if (error.message.includes('dynamic import')) { console.warn('Dynamic import not supported in this environment for ESLint'); } else { console.warn(`Failed to import ESLint: ${error.message}`); } } else { console.warn('Failed to import ESLint: Unknown error type'); } return null; } } // Function to dynamically import typescript with enhanced error handling async function tryImportTypeScript(): Promise<any> { const cacheKey = 'typescript'; if (importCache.has(cacheKey)) { const cached = importCache.get(cacheKey); return cached === 'loading' ? null : cached; } try { // Set loading state to prevent circular imports importCache.set(cacheKey, 'loading'); const tsModule = await import('typescript'); // Validate that TypeScript was imported correctly if (!tsModule || !tsModule.createProgram) { throw new Error('TypeScript module imported but createProgram function not found'); } // Cache the successful result importCache.set(cacheKey, tsModule); return tsModule; } catch (error) { // Cache the failure to prevent repeated attempts importCache.set(cacheKey, null); // Provide specific error messages for common issues if (error instanceof Error) { if (error.message.includes('Cannot find module')) { console.warn('TypeScript is not installed. Install with: npm install typescript'); } else if (error.message.includes('circular dependency')) { console.warn('Circular dependency detected when importing TypeScript'); } else if (error.message.includes('dynamic import')) { console.warn('Dynamic import not supported in this environment for TypeScript'); } else if (error.message.includes('createProgram function not found')) { console.warn('Invalid TypeScript module: missing required API functions'); } else { console.warn(`Failed to import TypeScript: ${error.message}`); } } else { console.warn('Failed to import TypeScript: Unknown error type'); } return null; } } const LintCodeSchema = z.object({ path: z.string().describe('The path to the file to lint.'), }); export class LintCodeTool extends BaseTool { private eslint: any; private eslintAvailable: boolean; constructor(private agentContext: { workingDirectory: string }) { super({ name: 'lintCode', description: 'Lints a code file and returns the linting errors.', category: 'Code Analysis', parameters: LintCodeSchema, }); this.eslintAvailable = false; this.eslint = null; // Initialize eslint asynchronously this.initializeESLint(); } private async initializeESLint(): Promise<void> { const ESLint = await tryImportESLint(); if (ESLint) { this.eslintAvailable = true; this.eslint = new ESLint({ cwd: this.agentContext.workingDirectory }); } } async execute(args: z.infer<typeof LintCodeSchema>): Promise<any[]> { // Log the received arguments for debugging console.log('LintCodeTool received args:', JSON.stringify(args, null, 2)); if (!args || !args.path || args.path.trim() === '') { return [ { filePath: args?.path || 'undefined', messages: [ { ruleId: null, severity: 2, message: 'Path parameter is required for lintCode tool. Received args: ' + JSON.stringify(args), line: 1, column: 1, }, ], errorCount: 1, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, source: '', usedDeprecatedRules: [], }, ]; } if (!this.eslintAvailable) { return [ { filePath: args.path, messages: [ { ruleId: null, severity: 1, message: 'ESLint not available in this environment. Install eslint package for linting functionality.', line: 1, column: 1, }, ], errorCount: 0, warningCount: 1, fixableErrorCount: 0, fixableWarningCount: 0, source: '', usedDeprecatedRules: [], }, ]; } const fullPath = this.resolvePath(args.path); // Check if file exists before trying to lint try { const { access } = await import('fs/promises'); await access(fullPath); } catch (error) { return [ { filePath: args.path, messages: [ { ruleId: null, severity: 2, message: `File not found at path '${args.path}' (resolved to '${fullPath}'). Please verify the file exists.`, line: 1, column: 1, }, ], errorCount: 1, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, source: '', usedDeprecatedRules: [], }, ]; } try { return await this.eslint.lintFiles([fullPath]); } catch (error) { return [ { filePath: args.path, messages: [ { ruleId: null, severity: 2, message: `Error linting file '${args.path}': ${error instanceof Error ? error.message : 'Unknown error'}`, line: 1, column: 1, }, ], errorCount: 1, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, source: '', usedDeprecatedRules: [], }, ]; } } private resolvePath(path: string): string { // Convert to relative path to comply with MCP workspace restrictions let resolvedPath = path; // If path is absolute, convert to relative to working directory if (isAbsolute(path)) { try { resolvedPath = relative(this.agentContext.workingDirectory, path); // If relative path starts with '..' it's outside working directory if (resolvedPath.startsWith('..')) { throw new Error(`Path ${path} is outside working directory`); } } catch (error) { // Fallback to using the path as-is but log the issue console.warn( `⚠️ Path conversion warning: ${error instanceof Error ? error.message : 'Unknown error'}` ); resolvedPath = path; } } // Join with working directory to ensure proper resolution return join(this.agentContext.workingDirectory, resolvedPath); } } const GetAstSchema = z.object({ path: z.string().describe('The path to the file to get the AST for.'), }); export class GetAstTool extends BaseTool { private ts: any; private tsAvailable: boolean; constructor(private agentContext: { workingDirectory: string }) { super({ name: 'getAst', description: 'Gets the Abstract Syntax Tree (AST) of a TypeScript file.', category: 'Code Analysis', parameters: GetAstSchema, }); this.tsAvailable = false; this.ts = null; // Initialize typescript asynchronously this.initializeTypeScript(); } private async initializeTypeScript(): Promise<void> { const ts = await tryImportTypeScript(); if (ts) { this.tsAvailable = true; this.ts = ts; } } async execute(args: z.infer<typeof GetAstSchema>): Promise<any> { // Log the received arguments for debugging console.log('GetAstTool received args:', JSON.stringify(args, null, 2)); if (!args || !args.path || args.path.trim() === '') { return { error: 'Path parameter is required for getAst tool. Received args: ' + JSON.stringify(args), fileName: args?.path || 'undefined', kind: 'InvalidInput', text: 'Invalid or missing path parameter', statements: [], childCount: 0, fullStart: 0, start: 0, end: 0, }; } if (!this.tsAvailable) { return { error: 'TypeScript not available in this environment. Install typescript package for AST functionality.', fileName: args.path, kind: 'NotAvailable', text: 'TypeScript compiler not available', statements: [], childCount: 0, fullStart: 0, start: 0, end: 0, }; } const fullPath = this.resolvePath(args.path); // Check if file exists before trying to analyze try { const { access } = await import('fs/promises'); await access(fullPath); } catch (error) { return { error: `File not found at path '${args.path}' (resolved to '${fullPath}'). Please verify the file exists.`, fileName: args.path, kind: 'FileNotFound', text: 'File does not exist', statements: [], childCount: 0, fullStart: 0, start: 0, end: 0, }; } try { const program = this.ts.createProgram([fullPath], { allowJs: true }); const sourceFile = program.getSourceFile(fullPath); if (!sourceFile) { return { error: `TypeScript could not parse source file: ${fullPath}`, fileName: args.path, kind: 'ParseError', text: 'Unable to parse file as TypeScript/JavaScript', statements: [], childCount: 0, fullStart: 0, start: 0, end: 0, }; } // Convert AST to serializable format const astSummary = { fileName: sourceFile.fileName, kind: this.ts.SyntaxKind[sourceFile.kind], text: sourceFile.text.slice(0, 500) + (sourceFile.text.length > 500 ? '...' : ''), statements: sourceFile.statements .map((stmt: any) => ({ kind: this.ts.SyntaxKind[stmt.kind], start: stmt.getStart(), end: stmt.getEnd(), })) .slice(0, 10), // Limit to first 10 statements childCount: sourceFile.getChildCount(), fullStart: sourceFile.getFullStart(), start: sourceFile.getStart(), end: sourceFile.getEnd(), }; return astSummary; } catch (error) { return { error: `Error analyzing file '${args.path}': ${error instanceof Error ? error.message : 'Unknown error'}`, fileName: args.path, kind: 'AnalysisError', text: 'Error during TypeScript analysis', statements: [], childCount: 0, fullStart: 0, start: 0, end: 0, }; } } private resolvePath(path: string): string { // Convert to relative path to comply with MCP workspace restrictions let resolvedPath = path; // If path is absolute, convert to relative to working directory if (isAbsolute(path)) { try { resolvedPath = relative(this.agentContext.workingDirectory, path); // If relative path starts with '..' it's outside working directory if (resolvedPath.startsWith('..')) { throw new Error(`Path ${path} is outside working directory`); } } catch (error) { // Fallback to using the path as-is but log the issue console.warn( `⚠️ Path conversion warning: ${error instanceof Error ? error.message : 'Unknown error'}` ); resolvedPath = path; } } // Join with working directory to ensure proper resolution return join(this.agentContext.workingDirectory, resolvedPath); } }