├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── scripts └── update-version.js ├── src ├── index.ts ├── prompts │ └── index.ts ├── server.ts ├── tools │ ├── base-servers-installer │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ ├── code-analyzer │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ ├── code-collector │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ ├── github-issues │ │ ├── handler.ts │ │ ├── index.ts │ │ └── types.ts │ └── index.ts ├── types │ ├── index.ts │ └── tool.ts ├── utils │ ├── fs.ts │ ├── paths.ts │ ├── progress.ts │ ├── project-files.ts │ └── safe-fs.ts └── validators │ └── index.ts ├── test-project └── test_code.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Environment mode (local/production) 2 | MCP_ENV=local 3 | 4 | # OpenAI API key 5 | OPENAI_API_KEY=your_api_key_here 6 | 7 | # Storage configuration 8 | STORAGE_PATH=data 9 | 10 | # Analysis settings 11 | MAX_TOKENS=190000 12 | TIMEOUT_MS=300000 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # Build 7 | build/ 8 | dist/ 9 | *.tsbuildinfo 10 | 11 | # IDE 12 | .idea/ 13 | .vscode/ 14 | *.swp 15 | *.swo 16 | 17 | # Logs 18 | logs/ 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Environment 25 | .env 26 | .env.local 27 | .env.*.local 28 | 29 | # Testing 30 | coverage/ 31 | tests/ 32 | 33 | # OS 34 | .DS_Store 35 | Thumbs.db 36 | 37 | # Temporary files 38 | .tmp/ 39 | FULL_CODE_*.md 40 | 41 | # Internal documentation 42 | PROJECT_SUMMARY.md 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AindreyWay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Neurolora 2 | 3 | ![MCP Server](https://img.shields.io/badge/MCP-Server-blue) 4 | ![Version](https://img.shields.io/badge/version-1.4.0-green) 5 | ![License](https://img.shields.io/badge/license-MIT-blue) 6 | 7 | An intelligent MCP server that provides tools for code analysis using OpenAI API, code collection, and documentation generation. 8 | 9 | ## 🚀 Installation Guide 10 | 11 | Don't worry if you don't have anything installed yet! Just follow these steps or ask your assistant to help you with the installation. 12 | 13 | ### Step 1: Install Node.js 14 | 15 | #### macOS 16 | 17 | 1. Install Homebrew if not installed: 18 | ```bash 19 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 20 | ``` 21 | 2. Install Node.js 18: 22 | ```bash 23 | brew install node@18 24 | echo 'export PATH="/opt/homebrew/opt/node@18/bin:$PATH"' >> ~/.zshrc 25 | source ~/.zshrc 26 | ``` 27 | 28 | #### Windows 29 | 30 | 1. Download Node.js 18 LTS from [nodejs.org](https://nodejs.org/) 31 | 2. Run the installer 32 | 3. Open a new terminal to apply changes 33 | 34 | #### Linux (Ubuntu/Debian) 35 | 36 | ```bash 37 | curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - 38 | sudo apt-get install -y nodejs 39 | ``` 40 | 41 | ### Step 2: Install uv and uvx 42 | 43 | #### All Operating Systems 44 | 45 | 1. Install uv: 46 | 47 | ```bash 48 | curl -LsSf https://astral.sh/uv/install.sh | sh 49 | ``` 50 | 51 | 2. Install uvx: 52 | ```bash 53 | uv pip install uvx 54 | ``` 55 | 56 | ### Step 3: Verify Installation 57 | 58 | Run these commands to verify everything is installed: 59 | 60 | ```bash 61 | node --version # Should show v18.x.x 62 | npm --version # Should show 9.x.x or higher 63 | uv --version # Should show uv installed 64 | uvx --version # Should show uvx installed 65 | ``` 66 | 67 | ### Step 4: Configure MCP Server 68 | 69 | Your assistant will help you: 70 | 71 | 1. Find your Cline settings file: 72 | 73 | - VSCode: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 74 | - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` 75 | - Windows VSCode: `%APPDATA%/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 76 | - Windows Claude: `%APPDATA%/Claude/claude_desktop_config.json` 77 | 78 | 2. Add this configuration: 79 | ```json 80 | { 81 | "mcpServers": { 82 | "aindreyway-mcp-neurolora": { 83 | "command": "npx", 84 | "args": ["-y", "@aindreyway/mcp-neurolora@latest"], 85 | "env": { 86 | "NODE_OPTIONS": "--max-old-space-size=256", 87 | "OPENAI_API_KEY": "your_api_key_here" 88 | } 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ### Step 5: Install Base Servers 95 | 96 | Simply ask your assistant: 97 | "Please install the base MCP servers for my environment" 98 | 99 | Your assistant will: 100 | 101 | 1. Find your settings file 102 | 2. Run the install_base_servers tool 103 | 3. Configure all necessary servers automatically 104 | 105 | After the installation is complete: 106 | 107 | 1. Close VSCode completely (Cmd+Q on macOS, Alt+F4 on Windows) 108 | 2. Reopen VSCode 109 | 3. The new servers will be ready to use 110 | 111 | > **Important:** A complete restart of VSCode is required after installing the base servers for them to be properly initialized. 112 | 113 | > **Note:** This server uses `npx` for direct npm package execution, which is optimal for Node.js/TypeScript MCP servers, providing seamless integration with the npm ecosystem and TypeScript tooling. 114 | 115 | ## Base MCP Servers 116 | 117 | The following base servers will be automatically installed and configured: 118 | 119 | - fetch: Basic HTTP request functionality for accessing web resources 120 | - puppeteer: Browser automation capabilities for web interaction and testing 121 | - sequential-thinking: Advanced problem-solving tools for complex tasks 122 | - github: GitHub integration features for repository management 123 | - git: Git operations support for version control 124 | - shell: Basic shell command execution with common commands: 125 | - ls: List directory contents 126 | - cat: Display file contents 127 | - pwd: Print working directory 128 | - grep: Search text patterns 129 | - wc: Count words, lines, characters 130 | - touch: Create empty files 131 | - find: Search for files 132 | 133 | ## 🎯 What Your Assistant Can Do 134 | 135 | Ask your assistant to: 136 | 137 | - "Analyze my code and suggest improvements" 138 | - "Install base MCP servers for my environment" 139 | - "Collect code from my project directory" 140 | - "Create documentation for my codebase" 141 | - "Generate a markdown file with all my code" 142 | 143 | ## 🛠 Available Tools 144 | 145 | ### analyze_code 146 | 147 | Analyzes code using OpenAI API and generates detailed feedback with improvement suggestions. 148 | 149 | Parameters: 150 | 151 | - `codePath` (required): Path to the code file or directory to analyze 152 | 153 | Example usage: 154 | 155 | ```json 156 | { 157 | "codePath": "/path/to/your/code.ts" 158 | } 159 | ``` 160 | 161 | The tool will: 162 | 163 | 1. Analyze your code using OpenAI API 164 | 2. Generate detailed feedback with: 165 | - Issues and recommendations 166 | - Best practices violations 167 | - Impact analysis 168 | - Steps to fix 169 | 3. Create two output files in your project: 170 | - LAST_RESPONSE_OPENAI.txt - Human-readable analysis 171 | - LAST_RESPONSE_OPENAI_GITHUB_FORMAT.json - Structured data for GitHub issues 172 | 173 | > Note: Requires OpenAI API key in environment configuration 174 | 175 | ### collect_code 176 | 177 | Collects all code from a directory into a single markdown file with syntax highlighting and navigation. 178 | 179 | Parameters: 180 | 181 | - `directory` (required): Directory path to collect code from 182 | - `outputPath` (optional): Path where to save the output markdown file 183 | - `ignorePatterns` (optional): Array of patterns to ignore (similar to .gitignore) 184 | 185 | Example usage: 186 | 187 | ```json 188 | { 189 | "directory": "/path/to/project/src", 190 | "outputPath": "/path/to/project/src/FULL_CODE_SRC_2024-12-20.md", 191 | "ignorePatterns": ["*.log", "temp/", "__pycache__", "*.pyc", ".git"] 192 | } 193 | ``` 194 | 195 | ### install_base_servers 196 | 197 | Installs base MCP servers to your configuration file. 198 | 199 | Parameters: 200 | 201 | - `configPath` (required): Path to the MCP settings configuration file 202 | 203 | Example usage: 204 | 205 | ```json 206 | { 207 | "configPath": "/path/to/cline_mcp_settings.json" 208 | } 209 | ``` 210 | 211 | ## 🔧 Features 212 | 213 | The server provides: 214 | 215 | - Code Analysis: 216 | 217 | - OpenAI API integration 218 | - Structured feedback 219 | - Best practices recommendations 220 | - GitHub issues generation 221 | 222 | - Code Collection: 223 | 224 | - Directory traversal 225 | - Syntax highlighting 226 | - Navigation generation 227 | - Pattern-based filtering 228 | 229 | - Base Server Management: 230 | - Automatic installation 231 | - Configuration handling 232 | - Version management 233 | 234 | ## 📄 License 235 | 236 | MIT License - feel free to use this in your projects! 237 | 238 | ## 👤 Author 239 | 240 | **Aindreyway** 241 | 242 | - GitHub: [@aindreyway](https://github.com/aindreyway) 243 | 244 | ## ⭐️ Support 245 | 246 | Give a ⭐️ if this project helped you! 247 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aindreyway/mcp-neurolora", 3 | "version": "1.4.0", 4 | "description": "An MCP server for collecting and documenting code from directories", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "bin": { 8 | "mcp-neurolora": "build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 12 | "start": "node build/index.js", 13 | "dev": "ts-node-esm src/index.ts", 14 | "test": "jest", 15 | "lint": "eslint src/**/*.ts", 16 | "format": "prettier --write src/**/*.ts", 17 | "local": "cross-env MCP_ENV=local npm run dev", 18 | "local:build": "cross-env MCP_ENV=local npm run build", 19 | "local:start": "cross-env MCP_ENV=local npm run start", 20 | "update-version": "node scripts/update-version.js", 21 | "version": "npm run update-version && git add -A", 22 | "prepublishOnly": "npm run build" 23 | }, 24 | "keywords": [ 25 | "mcp", 26 | "documentation", 27 | "code-collection" 28 | ], 29 | "author": "aindreyway", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@modelcontextprotocol/sdk": "^0.4.0", 33 | "@octokit/rest": "21.0.2", 34 | "openai": "4.77.0", 35 | "tiktoken": "1.0.18" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20.8.2", 39 | "@typescript-eslint/eslint-plugin": "^6.7.4", 40 | "@typescript-eslint/parser": "^6.7.4", 41 | "cross-env": "7.0.3", 42 | "eslint": "^8.50.0", 43 | "prettier": "^3.0.3", 44 | "ts-node": "^10.9.1", 45 | "typescript": "^5.2.2" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scripts/update-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs/promises'; 4 | import path from 'path'; 5 | 6 | // Получаем версию из package.json 7 | const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')); 8 | const version = packageJson.version; 9 | 10 | // Список файлов для обновления 11 | const filesToUpdate = [ 12 | { 13 | path: 'README.md', 14 | pattern: /!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-[\d\.]+/g, 15 | replace: `![Version](https://img.shields.io/badge/version-${version}`, 16 | }, 17 | { 18 | path: 'src/server.ts', 19 | pattern: /version: '[\d\.]+'/g, 20 | replace: `version: '${version}'`, 21 | }, 22 | ]; 23 | 24 | // Обновляем версию в каждом файле 25 | for (const file of filesToUpdate) { 26 | try { 27 | const content = await fs.readFile(file.path, 'utf8'); 28 | const updatedContent = content.replace(file.pattern, file.replace); 29 | await fs.writeFile(file.path, updatedContent, 'utf8'); 30 | console.log(`✅ Updated version in ${file.path}`); 31 | } catch (error) { 32 | console.error(`❌ Failed to update ${file.path}:`, error); 33 | } 34 | } 35 | 36 | console.log(`\n🎉 Version ${version} updated in all files`); 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { NeuroloraServer } from './server.js'; 5 | 6 | /** 7 | * Main entry point for the Neurolora MCP server 8 | */ 9 | async function main() { 10 | try { 11 | const server = new NeuroloraServer(); 12 | const transport = new StdioServerTransport(); 13 | await server.run(transport); 14 | } catch (error) { 15 | console.error('Failed to start Neurolora MCP server:', error); 16 | process.exit(1); 17 | } 18 | } 19 | 20 | main().catch(error => { 21 | console.error('Unhandled error:', error); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /src/prompts/index.ts: -------------------------------------------------------------------------------- 1 | // Промпт для анализа кода 2 | export const CODE_ANALYSIS_PROMPT = `You are a strict senior software architect performing a thorough code review. Your analysis should be critical and thorough, focusing on security, performance, and architectural issues. 3 | 4 | Categorize each finding by severity: 5 | - CRITICAL: Security vulnerabilities, data loss risks, major performance issues 6 | - ERROR: Bugs, memory leaks, incorrect implementations 7 | - WARNING: Code smells, maintainability issues, unclear patterns 8 | - IMPROVE: Optimization opportunities, architectural enhancements 9 | 10 | For each issue found, use this exact format with all fields required: 11 | 12 | {number}. [ ] ISSUE {SEVERITY}: {short title} 13 | 14 | Title: {clear and concise issue title} 15 | 16 | Description: {detailed description of the problem} 17 | 18 | Best Practice Violation: {what standards or practices are being violated} 19 | 20 | Impact: 21 | {bullet points listing specific impacts} 22 | 23 | Steps to Fix: 24 | {numbered list of specific steps to resolve the issue} 25 | 26 | Labels: {comma-separated list of labels} 27 | 28 | --- 29 | 30 | Example: 31 | 1. [ ] ISSUE CRITICAL: SQL Injection Risk in Query Builder 32 | 33 | Title: Unescaped User Input Used Directly in SQL Query 34 | 35 | Description: The query builder concatenates user input directly into SQL queries without proper escaping or parameterization, creating a severe security vulnerability. 36 | 37 | Best Practice Violation: All user input must be properly escaped or use parameterized queries to prevent SQL injection attacks. 38 | 39 | Impact: 40 | - Potential database compromise through SQL injection 41 | - Unauthorized data access 42 | - Possible data loss or corruption 43 | - Security breach vulnerability 44 | 45 | Steps to Fix: 46 | 1. Replace string concatenation with parameterized queries 47 | 2. Add input validation layer 48 | 3. Implement proper escaping for special characters 49 | 4. Add SQL injection tests 50 | 51 | Labels: security, priority-critical, effort-small 52 | 53 | --- 54 | 55 | Analysis criteria (be thorough and strict): 56 | 1. Security: 57 | - SQL injection risks 58 | - XSS vulnerabilities 59 | - Unsafe data handling 60 | - Exposed secrets 61 | - Insecure dependencies 62 | 63 | 2. Performance: 64 | - Inefficient algorithms (O(n²) or worse) 65 | - Memory leaks 66 | - Unnecessary computations 67 | - Resource management issues 68 | - Unoptimized database queries 69 | 70 | 3. Architecture: 71 | - SOLID principles violations 72 | - Tight coupling 73 | - Global state usage 74 | - Unclear boundaries 75 | - Mixed responsibilities 76 | 77 | 4. Code Quality: 78 | - Missing error handling 79 | - Untestable code 80 | - Code duplication 81 | - Complex conditionals 82 | - Deep nesting 83 | 84 | Label types: 85 | - security: Security vulnerabilities and risks 86 | - performance: Performance issues and bottlenecks 87 | - architecture: Design and structural problems 88 | - reliability: Error handling and stability issues 89 | - maintainability: Code organization and clarity 90 | - scalability: Growth and scaling concerns 91 | - testing: Test coverage and testability 92 | 93 | Priority levels: 94 | - priority-critical: Fix immediately (security risks, data loss) 95 | - priority-high: Fix in next release (bugs, performance) 96 | - priority-medium: Plan to fix soon (code quality) 97 | - priority-low: Consider fixing (improvements) 98 | 99 | Effort estimates: 100 | - effort-small: simple changes, up to 1 day 101 | - effort-medium: moderate changes, 2-3 days 102 | - effort-large: complex changes, more than 3 days 103 | 104 | Code to analyze: 105 | --- 106 | `; 107 | 108 | // Формат для сохранения диалога с OpenAI 109 | export const CONVERSATION_FORMAT = `=== OpenAI Code Analysis Details === 110 | 111 | File Information: 112 | ---------------- 113 | Path: {filePath} 114 | Token Count: {tokenCount} 115 | 116 | === Request to OpenAI === 117 | ------------------------ 118 | {prompt} 119 | 120 | === Source Code === 121 | ------------------ 122 | {code} 123 | 124 | === Analysis Results === 125 | ----------------------- 126 | {response}`; 127 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 2 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; 3 | import path from 'path'; 4 | import { tools } from './tools/index.js'; 5 | 6 | type ToolHandler = (args: Record) => Promise<{ 7 | content: Array<{ 8 | type: string; 9 | text: string; 10 | }>; 11 | isError?: boolean; 12 | }>; 13 | 14 | /** 15 | * Main MCP server class for Neurolora functionality 16 | */ 17 | const ENV = { 18 | isLocal: process.env.MCP_ENV === 'local', 19 | storagePath: process.env.STORAGE_PATH || 'data', 20 | maxTokens: parseInt(process.env.MAX_TOKENS || '190000'), 21 | timeoutMs: parseInt(process.env.TIMEOUT_MS || '300000'), 22 | }; 23 | 24 | /** 25 | * Server configuration and state management 26 | */ 27 | class ServerConfig { 28 | public readonly isLocal: boolean; 29 | public readonly name: string; 30 | public readonly baseDir: string; 31 | 32 | constructor() { 33 | this.isLocal = ENV.isLocal; 34 | this.name = this.isLocal ? 'local-mcp-neurolora' : '@aindreyway/mcp-neurolora'; 35 | this.baseDir = process.argv[1] ? path.dirname(process.argv[1]) : process.cwd(); 36 | } 37 | } 38 | 39 | /** 40 | * Error handler for MCP server 41 | */ 42 | class ErrorHandler { 43 | constructor(private readonly config: ServerConfig) {} 44 | 45 | public formatError(error: Error): string { 46 | return this.config.isLocal 47 | ? `${error.message}\n${error.stack}` 48 | : 'An error occurred while processing the request'; 49 | } 50 | 51 | public logError(error: unknown, context?: string): void { 52 | const prefix = this.config.isLocal ? '[LOCAL VERSION][MCP Error]' : '[MCP Error]'; 53 | console.error(prefix, context ? `[${context}]` : '', error); 54 | } 55 | } 56 | 57 | /** 58 | * Connection manager for MCP server 59 | */ 60 | class ConnectionManager { 61 | private currentTransport: any; 62 | 63 | constructor( 64 | private readonly server: Server, 65 | private readonly errorHandler: ErrorHandler 66 | ) {} 67 | 68 | public async connect(transport: any): Promise { 69 | this.currentTransport = transport; 70 | await this.reconnect(); 71 | } 72 | 73 | private async reconnect(retryCount = 0, maxRetries = 3): Promise { 74 | try { 75 | await this.server.connect(this.currentTransport); 76 | console.error('✅ Neurolora MCP server connected successfully'); 77 | } catch (error) { 78 | this.errorHandler.logError(error, 'reconnect'); 79 | if (retryCount < maxRetries) { 80 | console.error(`Retrying connection (${retryCount + 1}/${maxRetries})...`); 81 | await new Promise(resolve => setTimeout(resolve, 1000)); 82 | await this.reconnect(retryCount + 1, maxRetries); 83 | } else { 84 | throw error; 85 | } 86 | } 87 | } 88 | 89 | public async disconnect(): Promise { 90 | await this.server.close(); 91 | console.error('Server closed successfully'); 92 | } 93 | } 94 | 95 | export class NeuroloraServer { 96 | private readonly server: Server; 97 | private readonly config: ServerConfig; 98 | private readonly errorHandler: ErrorHandler; 99 | private readonly connectionManager: ConnectionManager; 100 | 101 | constructor() { 102 | this.config = new ServerConfig(); 103 | this.errorHandler = new ErrorHandler(this.config); 104 | 105 | // Initialize server 106 | this.server = new Server({ 107 | name: this.config.name, 108 | version: '1.4.0', 109 | capabilities: { 110 | tools: {}, 111 | }, 112 | timeout: ENV.timeoutMs, 113 | }); 114 | 115 | this.connectionManager = new ConnectionManager(this.server, this.errorHandler); 116 | 117 | // Show debug info in local mode 118 | if (this.config.isLocal) { 119 | console.error('[LOCAL VERSION] Running in development mode'); 120 | console.error(`[LOCAL VERSION] Storage path: ${ENV.storagePath}`); 121 | console.error(`[LOCAL VERSION] Max tokens: ${ENV.maxTokens}`); 122 | console.error(`[LOCAL VERSION] Timeout: ${ENV.timeoutMs}ms`); 123 | } 124 | 125 | this.initializeHandlers(); 126 | } 127 | 128 | private initializeHandlers(): void { 129 | // Register tools 130 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); 131 | 132 | // Handle tool calls 133 | this.server.setRequestHandler(CallToolRequestSchema, async request => { 134 | const tool = tools.find(t => t.name === request.params.name); 135 | if (!tool) { 136 | return { 137 | content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], 138 | isError: true, 139 | }; 140 | } 141 | 142 | try { 143 | const handler = tool?.handler as ToolHandler; 144 | return handler(request.params.arguments || {}); 145 | } catch (error) { 146 | this.errorHandler.logError(error, 'tool execution'); 147 | return { 148 | content: [{ type: 'text', text: this.errorHandler.formatError(error as Error) }], 149 | isError: true, 150 | }; 151 | } 152 | }); 153 | 154 | // Error handling 155 | this.server.onerror = async error => { 156 | this.errorHandler.logError(error); 157 | if (error instanceof Error && error.message.includes('connection')) { 158 | await this.handleConnectionError(); 159 | } 160 | }; 161 | } 162 | 163 | private async handleConnectionError(): Promise { 164 | console.error('Connection error detected, attempting to reconnect...'); 165 | try { 166 | await this.server.close(); 167 | await new Promise(resolve => setTimeout(resolve, 1000)); 168 | await this.connectionManager.connect(this.connectionManager['currentTransport']); 169 | } catch (error) { 170 | this.errorHandler.logError(error, 'reconnection'); 171 | } 172 | } 173 | 174 | private async handleShutdown(signal: string): Promise { 175 | console.error(`\nReceived ${signal}, shutting down...`); 176 | try { 177 | await this.connectionManager.disconnect(); 178 | // Allow async operations to complete before exiting 179 | setTimeout(() => process.exit(0), 1000); 180 | } catch (error) { 181 | this.errorHandler.logError(error, 'shutdown'); 182 | process.exit(1); 183 | } 184 | } 185 | 186 | async run(transport: any): Promise { 187 | console.error('Neurolora MCP server running from:', this.config.baseDir); 188 | 189 | try { 190 | await this.connectionManager.connect(transport); 191 | 192 | // Setup signal handlers 193 | const signals = ['SIGINT', 'SIGTERM', 'SIGHUP']; 194 | signals.forEach(signal => { 195 | process.on(signal, () => this.handleShutdown(signal)); 196 | }); 197 | } catch (error) { 198 | this.errorHandler.logError(error, 'startup'); 199 | process.exit(1); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/tools/base-servers-installer/handler.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs/promises'; 2 | import { BASE_SERVERS, McpSettings } from './types.js'; 3 | 4 | /** 5 | * Handle installation of base MCP servers 6 | */ 7 | export async function handleInstallBaseServers(configPath: string): Promise { 8 | try { 9 | console.error('Reading config from:', configPath); 10 | // Read existing configuration 11 | const configContent = await readFile(configPath, 'utf8'); 12 | console.error('Config content:', configContent); 13 | const config: McpSettings = JSON.parse(configContent); 14 | console.error('Parsed config:', JSON.stringify(config, null, 2)); 15 | 16 | let serversAdded = 0; 17 | 18 | console.error('Available base servers:', Object.keys(BASE_SERVERS).join(', ')); 19 | // Add base servers if they don't exist 20 | for (const [name, serverConfig] of Object.entries(BASE_SERVERS)) { 21 | console.error('Checking server:', name); 22 | if (!config.mcpServers[name]) { 23 | console.error('Adding server:', name); 24 | config.mcpServers[name] = { ...serverConfig }; 25 | serversAdded++; 26 | } else { 27 | console.error('Server already exists:', name); 28 | } 29 | } 30 | 31 | if (serversAdded > 0) { 32 | console.error('Writing updated config with', serversAdded, 'new servers'); 33 | // Проверяем, что конфиг валидный перед сохранением 34 | const configStr = JSON.stringify(config, null, 2); 35 | // Пробуем распарсить для проверки 36 | JSON.parse(configStr); 37 | const updatedConfig = configStr; 38 | console.error('Updated config:', updatedConfig); 39 | // Write updated configuration 40 | await writeFile(configPath, updatedConfig); 41 | const result = `Successfully added ${serversAdded} base servers to the configuration`; 42 | console.error('Result:', result); 43 | return result; 44 | } 45 | 46 | const result = 'All base servers are already installed in the configuration'; 47 | console.error('Result:', result); 48 | return result; 49 | } catch (error) { 50 | let errorMessage = 'Failed to install base servers'; 51 | if (error instanceof Error) { 52 | errorMessage += ': ' + error.message; 53 | console.error('Error stack:', error.stack); 54 | } else { 55 | errorMessage += ': ' + String(error); 56 | } 57 | console.error('ERROR:', errorMessage); 58 | throw new Error(errorMessage); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/tools/base-servers-installer/index.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { handleInstallBaseServers } from './handler.js'; 3 | 4 | export const installBaseServersTool: Tool = { 5 | name: 'install_base_servers', 6 | description: 'Install base MCP servers to the configuration', 7 | inputSchema: { 8 | type: 'object', 9 | properties: { 10 | configPath: { 11 | type: 'string', 12 | description: 'Path to the MCP settings configuration file', 13 | }, 14 | }, 15 | required: ['configPath'], 16 | }, 17 | handler: async (args: Record) => { 18 | const configPath = String(args.configPath); 19 | const result = await handleInstallBaseServers(configPath); 20 | 21 | return { 22 | content: [ 23 | { 24 | type: 'text', 25 | text: result, 26 | }, 27 | ], 28 | }; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/tools/base-servers-installer/types.ts: -------------------------------------------------------------------------------- 1 | export interface McpServerConfig { 2 | command: string; 3 | args: string[]; 4 | disabled?: boolean; 5 | alwaysAllow?: string[]; 6 | env?: Record; 7 | } 8 | 9 | export interface McpSettings { 10 | mcpServers: Record; 11 | } 12 | 13 | export const BASE_SERVERS: Record = { 14 | fetch: { 15 | command: 'uvx', 16 | args: ['mcp-server-fetch'], 17 | disabled: false, 18 | alwaysAllow: [], 19 | }, 20 | puppeteer: { 21 | command: 'npx', 22 | args: ['-y', '@modelcontextprotocol/server-puppeteer@latest'], 23 | disabled: false, 24 | alwaysAllow: [], 25 | }, 26 | 'sequential-thinking': { 27 | command: 'npx', 28 | args: ['-y', '@modelcontextprotocol/server-sequential-thinking@latest'], 29 | disabled: false, 30 | alwaysAllow: [], 31 | }, 32 | github: { 33 | command: 'npx', 34 | args: ['-y', '@modelcontextprotocol/server-github@latest'], 35 | disabled: false, 36 | alwaysAllow: [], 37 | }, 38 | git: { 39 | command: 'uvx', 40 | args: ['mcp-server-git'], 41 | disabled: false, 42 | alwaysAllow: [], 43 | }, 44 | shell: { 45 | command: 'uvx', 46 | args: ['mcp-shell-server'], 47 | env: { 48 | ALLOW_COMMANDS: 'ls,cat,pwd,grep,wc,touch,find', // Команды уже разделены запятыми 49 | }, 50 | disabled: false, 51 | alwaysAllow: [], 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/tools/code-analyzer/handler.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; 3 | import path from 'path'; 4 | import { encoding_for_model } from 'tiktoken'; 5 | import { CODE_ANALYSIS_PROMPT } from '../../prompts/index.js'; 6 | import { clearProgress, showProgress } from '../../utils/progress.js'; 7 | import { createAnalysisFile, createCodeCollectionFile } from '../../utils/project-files.js'; 8 | import { safeReadFile } from '../../utils/safe-fs.js'; 9 | import { handleCollectCode } from '../code-collector/handler.js'; 10 | import { AnalyzeCodeOptions, AnalyzeResult, OpenAIError } from './types.js'; 11 | 12 | const MAX_TOKENS = 190000; 13 | 14 | // Точный подсчет токенов с помощью tiktoken 15 | function estimateTokenCount(text: string): number { 16 | try { 17 | // Используем тот же энкодер, что и модель o1-preview 18 | const enc = encoding_for_model('gpt-4'); 19 | const tokens = enc.encode(text); 20 | enc.free(); // Освобождаем ресурсы 21 | return tokens.length; 22 | } catch (error) { 23 | console.warn('Failed to use tiktoken, falling back to approximate count:', error); 24 | // Запасной вариант: примерно 4 символа на токен 25 | return Math.ceil(text.length / 4); 26 | } 27 | } 28 | 29 | // Проверка размера кода 30 | function checkCodeSize(codeContent: string): { isValid: boolean; tokenCount: number } { 31 | const tokenCount = estimateTokenCount(codeContent); 32 | return { 33 | isValid: tokenCount <= MAX_TOKENS, 34 | tokenCount, 35 | }; 36 | } 37 | 38 | // Анализ кода с помощью OpenAI 39 | async function analyzeWithOpenAI( 40 | openai: OpenAI, 41 | codeContent: string, 42 | codePath: string, 43 | tokenCount: number 44 | ): Promise { 45 | try { 46 | if (tokenCount > MAX_TOKENS) { 47 | throw new OpenAIError( 48 | `Code is too large: ${tokenCount} tokens (maximum is ${MAX_TOKENS} tokens). ` + 49 | 'Please analyze a smaller codebase or split the analysis into multiple parts.' 50 | ); 51 | } 52 | 53 | const fullPrompt = CODE_ANALYSIS_PROMPT.replace( 54 | 'Code to analyze:\n---', 55 | `Code to analyze:\n---\n${codeContent}` 56 | ); 57 | const messages: ChatCompletionMessageParam[] = [{ role: 'user', content: fullPrompt }]; 58 | 59 | // Создаем файлы анализа 60 | const lastResponseJsonPath = await createAnalysisFile( 61 | 'LAST_RESPONSE_OPENAI_GITHUB_FORMAT.json', 62 | '' 63 | ); 64 | console.log('\nAnalyzing code...\n'); 65 | 66 | // Показываем прогресс анализа 67 | const totalSteps = 20; // 20 шагов за 2 минуты 68 | const startTime = Date.now(); 69 | const progressInterval = setInterval(async () => { 70 | const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000); 71 | const step = Math.min(totalSteps, Math.floor((elapsedSeconds / 120) * totalSteps)); 72 | const percentage = Math.round((step / totalSteps) * 100); 73 | await showProgress(percentage, { width: 20, prefix: '[OpenAI Analysis]' }); 74 | }, 1000); 75 | 76 | try { 77 | const response = await openai.chat.completions.create( 78 | { 79 | model: 'o1-preview', 80 | messages, 81 | }, 82 | { 83 | timeout: 5 * 60 * 1000, // 5 минут 84 | } 85 | ); 86 | clearInterval(progressInterval); 87 | 88 | const totalTime = Math.floor((Date.now() - startTime) / 1000); 89 | const content = response.choices[0]?.message?.content; 90 | if (!content) { 91 | throw new OpenAIError('No response from OpenAI'); 92 | } 93 | 94 | // Сохраняем результат анализа 95 | let analysisText = `Recommended Fixes and Improvements\n\n`; 96 | analysisText += content; 97 | await clearProgress(analysisText); 98 | 99 | // Сохраняем JSON для GitHub issues 100 | console.log('\nWriting JSON response...'); 101 | // Парсим ответ от OpenAI 102 | const blocks = content.split('\n---\n\n'); 103 | interface ParsedIssue { 104 | number: number; 105 | title: string; 106 | body: string; 107 | labels: string[]; 108 | } 109 | 110 | const issues: ParsedIssue[] = blocks 111 | .filter(block => { 112 | const lines = block.trim().split('\n'); 113 | return lines[0].match(/^\d+\.\s*\[\s*\]\s*ISSUE\s+[A-Z]+:/i); 114 | }) 115 | .map(block => { 116 | const lines = block.trim().split('\n'); 117 | 118 | // Парсим первую строку (номер, severity и краткий заголовок) 119 | const firstLine = lines[0]; 120 | const headerMatch = firstLine.match(/^(\d+)\.\s*\[\s*\]\s*ISSUE\s+([A-Z]+):\s*(.+)$/i); 121 | if (!headerMatch) return null; 122 | const [, number, severity, shortTitle] = headerMatch; 123 | 124 | // Находим основные секции 125 | const titleLine = lines.find(line => line.startsWith('Title:')); 126 | const labelsLine = lines.find(line => line.startsWith('Labels:')); 127 | 128 | // Извлекаем значения 129 | const title = titleLine ? titleLine.replace('Title:', '').trim() : shortTitle; 130 | const labels = labelsLine 131 | ? labelsLine 132 | .replace('Labels:', '') 133 | .trim() 134 | .split(',') 135 | .map(l => l.trim()) 136 | : []; 137 | 138 | // Добавляем severity как метку 139 | labels.unshift(severity.toLowerCase()); 140 | 141 | return { 142 | number: parseInt(number), 143 | title, 144 | body: block.trim(), 145 | labels, 146 | }; 147 | }) 148 | .filter((issue): issue is ParsedIssue => issue !== null); 149 | 150 | const issuesData = { 151 | filePath: codePath, 152 | tokenCount, 153 | issues, 154 | }; 155 | 156 | // Сохраняем результат анализа в текстовом формате 157 | await createAnalysisFile('LAST_RESPONSE_OPENAI.txt', analysisText); 158 | console.log('✅ Analysis response written'); 159 | 160 | // Сохраняем данные для GitHub 161 | await createAnalysisFile( 162 | 'LAST_RESPONSE_OPENAI_GITHUB_FORMAT.json', 163 | JSON.stringify(issuesData, null, 2) 164 | ); 165 | console.log('✅ GitHub issues data written'); 166 | 167 | // Формируем команду для открытия директории в зависимости от ОС 168 | const analysisDir = process.cwd(); 169 | let openCommand = ''; 170 | if (process.platform === 'darwin') { 171 | openCommand = `open "${analysisDir}"`; 172 | } else if (process.platform === 'win32') { 173 | openCommand = `explorer "${analysisDir}"`; 174 | } else { 175 | openCommand = `xdg-open "${analysisDir}"`; 176 | } 177 | 178 | // Показываем пути к файлам 179 | console.log('\nFiles created:'); 180 | console.log('Analysis files:'); 181 | console.log(`- Analysis: ${path.join(analysisDir, 'LAST_RESPONSE_OPENAI.txt')}`); 182 | console.log(`- GitHub Issues: ${lastResponseJsonPath}`); 183 | console.log('\nCollected code:'); 184 | console.log(`- Prompt: ${codePath}`); 185 | 186 | // Возвращаем результат в формате AnalyzeResult 187 | const analysisResult: AnalyzeResult = { 188 | type: 'analyze', 189 | mdFilePath: codePath, 190 | tokenCount, 191 | issues: issuesData.issues.map(issue => ({ 192 | title: `[#${issue.number}] ${issue.title}`, 193 | body: `Issue #${issue.number}\n\n${issue.body}`, 194 | labels: issue.labels, 195 | })), 196 | files: { 197 | analysis: path.join(analysisDir, 'LAST_RESPONSE_OPENAI.txt'), 198 | json: lastResponseJsonPath, 199 | prompt: codePath, 200 | openCommand, 201 | }, 202 | }; 203 | 204 | // Выводим в MCP только если есть issues 205 | if (issuesData.issues.length > 0) { 206 | console.log('\nIssues found:'); 207 | issuesData.issues.forEach(issue => { 208 | console.log(`[#${issue.number}] ${issue.title}`); 209 | }); 210 | } 211 | 212 | return analysisResult; 213 | } catch (error) { 214 | clearInterval(progressInterval); 215 | throw error; 216 | } 217 | } catch (error) { 218 | if (error instanceof OpenAIError) { 219 | throw error; 220 | } 221 | throw new OpenAIError(`Failed to analyze code: ${(error as Error).message}`); 222 | } 223 | } 224 | 225 | async function getCodeContent(codePath: string): Promise<{ content: string; mdPath: string }> { 226 | try { 227 | // Если это уже markdown файл, используем его напрямую 228 | if (codePath.endsWith('.md')) { 229 | const content = await safeReadFile(codePath); 230 | return { content, mdPath: codePath }; 231 | } 232 | 233 | // Иначе запускаем code_collector с сохранением в корень проекта 234 | const outputPath = await createCodeCollectionFile(codePath, ''); 235 | const result = await handleCollectCode({ 236 | input: codePath, 237 | outputPath, 238 | }); 239 | 240 | // Получаем путь к созданному файлу из результата 241 | const match = result.match(/Output saved to: (.+)$/); 242 | if (!match) { 243 | throw new Error('Failed to get output file path from collect_code result'); 244 | } 245 | 246 | const fullCodePath = match[1]; 247 | const content = await safeReadFile(fullCodePath); 248 | return { content, mdPath: fullCodePath }; 249 | } catch (error) { 250 | throw new Error(`Failed to read code: ${(error as Error).message}`); 251 | } 252 | } 253 | 254 | export async function handleAnalyzeCode(options: AnalyzeCodeOptions): Promise { 255 | try { 256 | // Проверяем, что путь абсолютный 257 | if (!path.isAbsolute(options.codePath)) { 258 | throw new Error(`Code path must be absolute. Got: ${options.codePath}`); 259 | } 260 | 261 | const { content: codeContent, mdPath } = await getCodeContent(options.codePath); 262 | const { tokenCount } = checkCodeSize(codeContent); 263 | 264 | // Проверяем наличие OpenAI API ключа в настройках 265 | if (!process.env.OPENAI_API_KEY) { 266 | throw new OpenAIError( 267 | 'OpenAI API key is not found in MCP settings. Analysis cannot be performed.' 268 | ); 269 | } 270 | 271 | const openai = new OpenAI({ 272 | apiKey: process.env.OPENAI_API_KEY, 273 | }); 274 | 275 | const analysis = await analyzeWithOpenAI(openai, codeContent, mdPath, tokenCount); 276 | 277 | return analysis; 278 | } catch (error) { 279 | if (error instanceof OpenAIError) { 280 | throw error; 281 | } 282 | throw new Error(`Failed to process code: ${(error as Error).message}`); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/tools/code-analyzer/index.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { handleAnalyzeCode } from './handler.js'; 3 | import { codeAnalyzerSchema } from './types.js'; 4 | 5 | export const codeAnalyzerTool: Tool = { 6 | name: 'analyze_code', 7 | description: 8 | 'Analyze code using OpenAI API (requires your API key). The analysis may take a few minutes. So, wait please.', 9 | inputSchema: codeAnalyzerSchema, 10 | handler: async (args: Record) => { 11 | try { 12 | const codePath = String(args.codePath); 13 | const openaiKey = process.env.OPENAI_API_KEY || process.env.OPENAI_KEY; 14 | 15 | if (!openaiKey) { 16 | return { 17 | content: [ 18 | { 19 | type: 'text', 20 | text: 'OpenAI API key is required for code analysis. Please add OPENAI_API_KEY to your MCP server configuration.', 21 | }, 22 | ], 23 | isError: true, 24 | }; 25 | } 26 | 27 | const analysis = await handleAnalyzeCode({ 28 | mode: 'analyze', 29 | codePath, 30 | }); 31 | 32 | return { 33 | content: [ 34 | { 35 | type: 'text', 36 | text: 37 | `Code analyzed successfully:\n` + 38 | `- MD File: ${analysis.mdFilePath}\n` + 39 | `- Token Count: ${analysis.tokenCount}\n` + 40 | `- Issues Found: ${analysis.issues.length}\n\n` + 41 | `${analysis.issues.map((issue: { body: string }) => issue.body).join('\n\n')}`, 42 | }, 43 | ], 44 | }; 45 | } catch (error: unknown) { 46 | return { 47 | content: [ 48 | { 49 | type: 'text', 50 | text: `Error analyzing code: ${error instanceof Error ? error.message : String(error)}`, 51 | }, 52 | ], 53 | isError: true, 54 | }; 55 | } 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/tools/code-analyzer/types.ts: -------------------------------------------------------------------------------- 1 | // Base interfaces 2 | export interface CodeIssue { 3 | title: string; 4 | body: string; 5 | labels: string[]; 6 | milestone?: string; 7 | project?: string; 8 | } 9 | 10 | // Options for code analyzer 11 | export interface AnalyzeCodeOptions { 12 | mode: 'analyze'; 13 | /** 14 | * Path to the code file to analyze. 15 | * Must be an absolute path, e.g. '/Users/username/project/src/code.ts' 16 | */ 17 | codePath: string; 18 | } 19 | 20 | // Results 21 | export interface AnalyzeResult { 22 | type: 'analyze'; 23 | mdFilePath: string; 24 | tokenCount: number; 25 | issues: CodeIssue[]; 26 | files: { 27 | analysis: string; // Путь к файлу с результатами анализа 28 | json: string; // Путь к JSON файлу 29 | prompt: string; // Путь к файлу с собранным кодом 30 | openCommand: string; // Команда для открытия директории с результатами 31 | }; 32 | } 33 | 34 | // Schema for code analyzer 35 | export const codeAnalyzerSchema = { 36 | type: 'object', 37 | properties: { 38 | codePath: { 39 | type: 'string', 40 | description: 41 | 'Absolute path to the code file to analyze (e.g. /Users/username/project/src/code.ts)', 42 | }, 43 | }, 44 | required: ['codePath'], 45 | additionalProperties: false, 46 | } as const; 47 | 48 | // Error types 49 | export class OpenAIError extends Error { 50 | constructor(message: string) { 51 | super(message); 52 | this.name = 'OpenAIError'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/tools/code-collector/handler.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { CODE_ANALYSIS_PROMPT } from '../../prompts/index.js'; 3 | import { CodeCollectorOptions } from '../../types/index.js'; 4 | import { collectFiles } from '../../utils/fs.js'; 5 | import { createCodeCollectionFile } from '../../utils/project-files.js'; 6 | import { validateOptions } from '../../validators/index.js'; 7 | 8 | /** 9 | * Handle code collection tool execution 10 | */ 11 | export async function handleCollectCode(options: CodeCollectorOptions): Promise { 12 | // Проверяем, что все входные пути абсолютные 13 | const inputs = Array.isArray(options.input) ? options.input : [options.input]; 14 | for (const input of inputs) { 15 | if (!path.isAbsolute(input)) { 16 | throw new Error(`Input path must be absolute. Got: ${input}`); 17 | } 18 | } 19 | 20 | // Проверяем, что выходной путь абсолютный 21 | if (!path.isAbsolute(options.outputPath)) { 22 | throw new Error(`Output path must be absolute. Got: ${options.outputPath}`); 23 | } 24 | 25 | // Формируем имя выходного файла 26 | const date = new Date().toISOString().split('T')[0]; 27 | const inputName = Array.isArray(options.input) 28 | ? 'MULTIPLE_FILES' 29 | : path.basename(options.input).toUpperCase(); 30 | 31 | // Используем предоставленный выходной путь 32 | const outputPath = options.outputPath; 33 | 34 | // Собираем файлы 35 | const files = await collectFiles(options.input, options.ignorePatterns || []); 36 | 37 | if (files.length === 0) { 38 | return 'No files found matching the criteria'; 39 | } 40 | 41 | // Generate markdown content 42 | const title = Array.isArray(options.input) ? 'Selected Files' : path.basename(options.input); 43 | 44 | let markdown = CODE_ANALYSIS_PROMPT; 45 | markdown += `\n# Code Collection: ${title}\n\n`; 46 | markdown += `Source: ${ 47 | Array.isArray(options.input) ? options.input.join(', ') : options.input 48 | }\n\n`; 49 | 50 | // Table of contents 51 | markdown += '## Table of Contents\n\n'; 52 | for (const file of files) { 53 | const anchor = file.relativePath.toLowerCase().replace(/[^a-z0-9]+/g, '-'); 54 | markdown += `- [${file.relativePath}](#${anchor})\n`; 55 | } 56 | 57 | // File contents 58 | markdown += '\n## Files\n\n'; 59 | for (const file of files) { 60 | const anchor = file.relativePath.toLowerCase().replace(/[^a-z0-9]+/g, '-'); 61 | markdown += `### ${file.relativePath} {#${anchor}}\n`; 62 | markdown += '```' + file.language + '\n'; 63 | markdown += file.content; 64 | markdown += '\n```\n\n'; 65 | } 66 | 67 | // Write output file 68 | const input = Array.isArray(options.input) ? options.input[0] : options.input; 69 | await createCodeCollectionFile(input, markdown); 70 | 71 | return `Successfully collected ${files.length} files. Output saved to: ${outputPath}`; 72 | } 73 | -------------------------------------------------------------------------------- /src/tools/code-collector/index.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import path from 'path'; 3 | import { CodeCollectorOptions } from './types.js'; 4 | import { handleCollectCode } from './handler.js'; 5 | 6 | export const codeCollectorTool: Tool = { 7 | name: 'collect_code', 8 | description: 'Collect all code from a directory into a single markdown file', 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | input: { 13 | oneOf: [ 14 | { 15 | type: 'string', 16 | description: 'Path to directory or file to collect code from', 17 | }, 18 | { 19 | type: 'array', 20 | items: { 21 | type: 'string', 22 | }, 23 | description: 'List of file paths to collect code from', 24 | }, 25 | ], 26 | }, 27 | outputPath: { 28 | type: 'string', 29 | description: 'Path where to save the output markdown file', 30 | pattern: '^/.*', 31 | examples: ['/path/to/project/src/FULL_CODE_SRC_2024-12-20.md'], 32 | }, 33 | ignorePatterns: { 34 | type: 'array', 35 | items: { 36 | type: 'string', 37 | }, 38 | description: 'Patterns to ignore (similar to .gitignore)', 39 | optional: true, 40 | }, 41 | }, 42 | required: ['input', 'outputPath'], 43 | }, 44 | handler: async (args: Record) => { 45 | try { 46 | const input = Array.isArray(args.input) ? args.input.map(String) : String(args.input); 47 | const outputPath = path.resolve(String(args.outputPath)); 48 | 49 | const options: CodeCollectorOptions = { 50 | input, 51 | outputPath, 52 | ignorePatterns: args.ignorePatterns as string[] | undefined, 53 | }; 54 | 55 | const result = await handleCollectCode(options); 56 | return { 57 | content: [ 58 | { 59 | type: 'text', 60 | text: result, 61 | }, 62 | ], 63 | }; 64 | } catch (error) { 65 | return { 66 | content: [ 67 | { 68 | type: 'text', 69 | text: `Error collecting code: ${error instanceof Error ? error.message : String(error)}`, 70 | }, 71 | ], 72 | isError: true, 73 | }; 74 | } 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/tools/code-collector/types.ts: -------------------------------------------------------------------------------- 1 | // Options for code collector 2 | export interface CodeCollectorOptions { 3 | // Путь к директории или файлу, или массив путей к файлам 4 | input: string | string[]; 5 | outputPath: string; 6 | ignorePatterns?: string[]; 7 | } 8 | 9 | // Schema for code collector 10 | export const codeCollectorSchema = { 11 | type: 'object', 12 | properties: { 13 | input: { 14 | oneOf: [ 15 | { 16 | type: 'string', 17 | description: 'Path to directory or file to collect code from', 18 | }, 19 | { 20 | type: 'array', 21 | items: { 22 | type: 'string', 23 | }, 24 | description: 'List of file paths to collect code from', 25 | }, 26 | ], 27 | }, 28 | outputPath: { 29 | type: 'string', 30 | description: 'Path where to save the output markdown file', 31 | }, 32 | ignorePatterns: { 33 | type: 'array', 34 | items: { 35 | type: 'string', 36 | }, 37 | description: 'Patterns to ignore (similar to .gitignore)', 38 | }, 39 | }, 40 | required: ['input', 'outputPath'], 41 | additionalProperties: false, 42 | } as const; 43 | -------------------------------------------------------------------------------- /src/tools/github-issues/handler.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | import path from 'path'; 3 | import { safeReadFile } from '../../utils/safe-fs.js'; 4 | import { GitHubError, GitHubIssuesOptions } from './types.js'; 5 | 6 | interface ParsedIssue { 7 | number: number; 8 | title: string; 9 | body: string; 10 | labels: string[]; 11 | } 12 | 13 | async function parseResponseFile(filePath: string): Promise { 14 | try { 15 | const content = await safeReadFile(filePath); 16 | const data = JSON.parse(content); 17 | return data.issues; 18 | } catch (error) { 19 | if (error instanceof GitHubError) { 20 | throw error; 21 | } 22 | throw new GitHubError(`Failed to parse conversation file: ${(error as Error).message}`); 23 | } 24 | } 25 | 26 | export async function handleGitHubIssues(options: GitHubIssuesOptions): Promise { 27 | try { 28 | if (!options.githubToken) { 29 | throw new GitHubError( 30 | 'GitHub token is required. Please add your GitHub token to the MCP server configuration.' 31 | ); 32 | } 33 | 34 | const octokit = new Octokit({ 35 | auth: options.githubToken, 36 | }); 37 | 38 | // Парсим файл с результатами анализа 39 | const allIssues = await parseResponseFile( 40 | path.join(process.cwd(), 'LAST_RESPONSE_OPENAI.json') 41 | ); 42 | if (allIssues.length === 0) { 43 | return 'No issues found in the conversation file'; 44 | } 45 | 46 | // Фильтруем issues по номерам, если они указаны 47 | const issuesToCreate = options.issueNumbers 48 | ? allIssues.filter(issue => options.issueNumbers?.includes(issue.number)) 49 | : allIssues; 50 | 51 | if (issuesToCreate.length === 0) { 52 | return 'No matching issues found with the specified numbers'; 53 | } 54 | 55 | // Создаем issues 56 | const results = await Promise.all( 57 | issuesToCreate.map(async issue => { 58 | try { 59 | await octokit.issues.create({ 60 | owner: options.owner, 61 | repo: options.repo, 62 | title: `[#${issue.number}] ${issue.title}`, 63 | body: issue.body, 64 | labels: issue.labels, 65 | }); 66 | return `Created issue #${issue.number}: ${issue.title}`; 67 | } catch (error) { 68 | return `Failed to create issue #${issue.number}: ${ 69 | error instanceof Error ? error.message : String(error) 70 | }`; 71 | } 72 | }) 73 | ); 74 | 75 | return results.join('\n'); 76 | } catch (error) { 77 | if (error instanceof GitHubError) { 78 | throw error; 79 | } 80 | throw new GitHubError(`Failed to create GitHub issues: ${(error as Error).message}`); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tools/github-issues/index.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { handleGitHubIssues } from './handler.js'; 3 | import { githubIssuesSchema } from './types.js'; 4 | 5 | export const githubIssuesTool: Tool = { 6 | name: 'create_github_issues', 7 | description: 'Create GitHub issues from analysis results. Requires GitHub token.', 8 | inputSchema: githubIssuesSchema, 9 | handler: async (args: Record) => { 10 | try { 11 | const githubToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; 12 | if (!githubToken) { 13 | return { 14 | content: [ 15 | { 16 | type: 'text', 17 | text: 'GitHub token is required. Please add GITHUB_PERSONAL_ACCESS_TOKEN to your MCP server configuration.', 18 | }, 19 | ], 20 | isError: true, 21 | }; 22 | } 23 | 24 | const options = { 25 | githubToken, 26 | owner: String(args.owner), 27 | repo: String(args.repo), 28 | issueNumbers: args.issueNumbers as number[] | undefined, 29 | }; 30 | 31 | const result = await handleGitHubIssues(options); 32 | return { 33 | content: [ 34 | { 35 | type: 'text', 36 | text: result, 37 | }, 38 | ], 39 | }; 40 | } catch (error) { 41 | return { 42 | content: [ 43 | { 44 | type: 'text', 45 | text: `Error creating GitHub issues: ${error instanceof Error ? error.message : String(error)}`, 46 | }, 47 | ], 48 | isError: true, 49 | }; 50 | } 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/tools/github-issues/types.ts: -------------------------------------------------------------------------------- 1 | // Options for GitHub issues creator 2 | export interface GitHubIssuesOptions { 3 | githubToken: string; 4 | owner: string; 5 | repo: string; 6 | issueNumbers?: number[]; // номера issues для создания (если не указаны, создаются все) 7 | } 8 | 9 | // Schema for GitHub issues creator 10 | export const githubIssuesSchema = { 11 | type: 'object', 12 | properties: { 13 | owner: { 14 | type: 'string', 15 | description: 'GitHub repository owner', 16 | }, 17 | repo: { 18 | type: 'string', 19 | description: 'GitHub repository name', 20 | }, 21 | issueNumbers: { 22 | type: 'array', 23 | items: { 24 | type: 'number', 25 | }, 26 | description: 'Issue numbers to create (optional, creates all issues if not specified)', 27 | }, 28 | }, 29 | required: ['owner', 'repo'], 30 | additionalProperties: false, 31 | } as const; 32 | 33 | // Error types 34 | export class GitHubError extends Error { 35 | constructor(message: string) { 36 | super(message); 37 | this.name = 'GitHubError'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { installBaseServersTool } from './base-servers-installer/index.js'; 2 | import { codeAnalyzerTool } from './code-analyzer/index.js'; 3 | import { codeCollectorTool } from './code-collector/index.js'; 4 | import { githubIssuesTool } from './github-issues/index.js'; 5 | 6 | export const tools = [ 7 | codeCollectorTool, 8 | installBaseServersTool, 9 | codeAnalyzerTool, 10 | githubIssuesTool, 11 | ]; 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Types for code collection functionality 2 | 3 | /** 4 | * Options for code collection 5 | */ 6 | export interface CodeCollectorOptions { 7 | /** 8 | * Input path(s) to collect code from. 9 | * Must be absolute paths, e.g. '/Users/username/project/src' 10 | */ 11 | input: string | string[]; 12 | 13 | /** 14 | * Path where to save the output file. 15 | * Must be an absolute path, e.g. '/Users/username/project/output.md' 16 | */ 17 | outputPath: string; 18 | 19 | /** 20 | * Optional patterns to ignore when collecting files 21 | */ 22 | ignorePatterns?: string[]; 23 | } 24 | 25 | export interface FileInfo { 26 | relativePath: string; 27 | content: string; 28 | language: string; 29 | } 30 | 31 | export interface CollectionResult { 32 | success: boolean; 33 | message: string; 34 | error?: string; 35 | } 36 | 37 | export interface LanguageMapping { 38 | [key: string]: string; 39 | } 40 | 41 | export interface CollectorConfig { 42 | maxFileSize: number; 43 | defaultIgnorePatterns: string[]; 44 | encoding: BufferEncoding; 45 | } 46 | -------------------------------------------------------------------------------- /src/types/tool.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | 3 | export type ToolHandler = Tool['handler']; 4 | 5 | export interface ToolResult { 6 | content: Array<{ 7 | type: string; 8 | text: string; 9 | }>; 10 | isError?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs, Stats } from 'fs'; 2 | import path from 'path'; 3 | import { FileInfo, LanguageMapping } from '../types/index.js'; 4 | 5 | /** 6 | * Map of file extensions to markdown code block languages 7 | */ 8 | export const LANGUAGE_MAP: LanguageMapping = { 9 | '.py': 'python', 10 | '.js': 'javascript', 11 | '.ts': 'typescript', 12 | '.jsx': 'jsx', 13 | '.tsx': 'tsx', 14 | '.html': 'html', 15 | '.css': 'css', 16 | '.scss': 'scss', 17 | '.sass': 'sass', 18 | '.less': 'less', 19 | '.md': 'markdown', 20 | '.json': 'json', 21 | '.yml': 'yaml', 22 | '.yaml': 'yaml', 23 | '.sh': 'bash', 24 | '.bash': 'bash', 25 | '.zsh': 'bash', 26 | '.bat': 'batch', 27 | '.ps1': 'powershell', 28 | '.sql': 'sql', 29 | '.java': 'java', 30 | '.cpp': 'cpp', 31 | '.hpp': 'cpp', 32 | '.c': 'c', 33 | '.h': 'c', 34 | '.rs': 'rust', 35 | '.go': 'go', 36 | '.rb': 'ruby', 37 | '.php': 'php', 38 | '.swift': 'swift', 39 | '.kt': 'kotlin', 40 | '.kts': 'kotlin', 41 | '.r': 'r', 42 | '.lua': 'lua', 43 | '.m': 'matlab', 44 | '.pl': 'perl', 45 | '.xml': 'xml', 46 | '.toml': 'toml', 47 | '.ini': 'ini', 48 | '.conf': 'conf', 49 | }; 50 | 51 | /** 52 | * Default patterns to ignore when collecting files 53 | */ 54 | export const DEFAULT_IGNORE_PATTERNS = [ 55 | 'node_modules', 56 | '.git', 57 | '__pycache__', 58 | '*.pyc', 59 | 'venv', 60 | '.env', 61 | 'dist', 62 | 'build', 63 | '.idea', 64 | '.vscode', 65 | 'coverage', 66 | '.next', 67 | '.nuxt', 68 | '.cache', 69 | '*.log', 70 | '*.lock', 71 | 'package-lock.json', 72 | 'yarn.lock', 73 | 'pnpm-lock.yaml', 74 | '.DS_Store', 75 | 'Thumbs.db', 76 | '*.swp', 77 | '*.swo', 78 | '*.bak', 79 | '*.tmp', 80 | '*.temp', 81 | '*.o', 82 | '*.obj', 83 | '*.class', 84 | '*.exe', 85 | '*.dll', 86 | '*.so', 87 | '*.dylib', 88 | '*.min.js', 89 | '*.min.css', 90 | '*.map', 91 | ]; 92 | 93 | /** 94 | * Maximum file size in bytes (1MB) 95 | */ 96 | export const MAX_FILE_SIZE = 1024 * 1024; 97 | 98 | /** 99 | * Get language identifier for a file extension 100 | */ 101 | export function getLanguage(filePath: string): string { 102 | const ext = path.extname(filePath).toLowerCase(); 103 | return LANGUAGE_MAP[ext] || ''; 104 | } 105 | 106 | /** 107 | * Create a valid markdown anchor from a path 108 | */ 109 | export function makeAnchor(filePath: string): string { 110 | return filePath 111 | .toLowerCase() 112 | .replace(/[^a-z0-9]+/g, '-') 113 | .replace(/^-|-$/g, ''); 114 | } 115 | 116 | /** 117 | * Check if a file should be ignored based on patterns 118 | */ 119 | export async function shouldIgnoreFile( 120 | filePath: string, 121 | ignorePatterns: string[], 122 | stats?: Stats 123 | ): Promise { 124 | const fileName = path.basename(filePath); 125 | 126 | // Get file stats if not provided 127 | if (!stats) { 128 | try { 129 | stats = await fs.stat(filePath); 130 | } catch (error) { 131 | console.error(`Error getting stats for ${filePath}:`, error); 132 | return true; 133 | } 134 | } 135 | 136 | // Skip files larger than MAX_FILE_SIZE 137 | if (stats.size > MAX_FILE_SIZE) { 138 | return true; 139 | } 140 | 141 | // Check against ignore patterns 142 | return ignorePatterns.some(pattern => { 143 | if (pattern.endsWith('/')) { 144 | return filePath.includes(pattern.slice(0, -1)); 145 | } 146 | return ( 147 | fileName === pattern || 148 | filePath.includes(pattern) || 149 | (pattern.includes('*') && new RegExp('^' + pattern.replace(/\*/g, '.*') + '$').test(fileName)) 150 | ); 151 | }); 152 | } 153 | 154 | /** 155 | * Read file content with proper encoding handling 156 | */ 157 | export async function readFileContent(filePath: string): Promise { 158 | try { 159 | return await fs.readFile(filePath, 'utf-8'); 160 | } catch (error) { 161 | if (error instanceof Error) { 162 | console.error(`Error reading ${filePath}:`, error.message); 163 | return `[Error reading file: ${error.message}]`; 164 | } 165 | return '[Unknown error reading file]'; 166 | } 167 | } 168 | 169 | /** 170 | * Process a single file and return its info 171 | */ 172 | async function processFile(filePath: string, baseDir: string): Promise { 173 | // Используем path.join для объединения путей 174 | const fullPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); 175 | const relativePath = path.relative(baseDir, fullPath); 176 | 177 | try { 178 | const stats = await fs.stat(fullPath); 179 | if (!stats.isFile()) { 180 | return null; 181 | } 182 | 183 | const content = await readFileContent(fullPath); 184 | return { 185 | relativePath, 186 | content, 187 | language: getLanguage(fullPath), 188 | }; 189 | } catch (error) { 190 | console.error(`Error processing file ${filePath}:`, error); 191 | return null; 192 | } 193 | } 194 | 195 | /** 196 | * Process a directory recursively 197 | */ 198 | async function processDirectory( 199 | dirPath: string, 200 | baseDir: string, 201 | ignorePatterns: string[] 202 | ): Promise { 203 | const files: FileInfo[] = []; 204 | // Убеждаемся, что dirPath абсолютный 205 | const absoluteDirPath = path.isAbsolute(dirPath) ? dirPath : path.join(process.cwd(), dirPath); 206 | const entries = await fs.readdir(absoluteDirPath, { withFileTypes: true }); 207 | 208 | for (const entry of entries) { 209 | const fullPath = path.join(absoluteDirPath, entry.name); 210 | 211 | if (await shouldIgnoreFile(fullPath, ignorePatterns)) { 212 | continue; 213 | } 214 | 215 | if (entry.isDirectory()) { 216 | const subDirFiles = await processDirectory(fullPath, baseDir, ignorePatterns); 217 | files.push(...subDirFiles); 218 | } else if (entry.isFile()) { 219 | const fileInfo = await processFile(fullPath, baseDir); 220 | if (fileInfo) { 221 | files.push(fileInfo); 222 | } 223 | } 224 | } 225 | 226 | return files; 227 | } 228 | 229 | /** 230 | * Collect files from input (directory, file, or array of files) 231 | */ 232 | export async function collectFiles( 233 | input: string | string[], 234 | ignorePatterns: string[] 235 | ): Promise { 236 | const files: FileInfo[] = []; 237 | const inputs = Array.isArray(input) ? input : [input]; 238 | const projectRoot = process.cwd(); 239 | 240 | console.log('Project root:', projectRoot); 241 | console.log('Input paths:', inputs); 242 | 243 | // Преобразуем все пути в абсолютные относительно текущей директории 244 | const resolvedInputs = inputs.map(item => { 245 | const resolvedPath = path.isAbsolute(item) ? item : path.join(projectRoot, item); 246 | console.log(`Resolving path: ${item} -> ${resolvedPath}`); 247 | return resolvedPath; 248 | }); 249 | 250 | // Всегда используем корень проекта как базовую директорию 251 | const baseDir = projectRoot; 252 | console.log('Base directory:', baseDir); 253 | 254 | for (const fullPath of resolvedInputs) { 255 | try { 256 | const stats = await fs.stat(fullPath); 257 | 258 | if (stats.isDirectory()) { 259 | const dirFiles = await processDirectory(fullPath, fullPath, ignorePatterns); 260 | files.push(...dirFiles); 261 | } else if (stats.isFile()) { 262 | const fileInfo = await processFile(fullPath, baseDir); 263 | if (fileInfo) { 264 | files.push(fileInfo); 265 | } 266 | } 267 | } catch (error) { 268 | console.error(`Error processing ${fullPath}:`, error); 269 | } 270 | } 271 | 272 | return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); 273 | } 274 | -------------------------------------------------------------------------------- /src/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | 5 | const APP_NAME = 'aindreyway-mcp-neurolora'; 6 | 7 | /** 8 | * Get standard paths for file storage following OS conventions 9 | */ 10 | export function getAppPaths() { 11 | // Base paths 12 | const homeDir = os.homedir(); 13 | const tempDir = os.tmpdir(); 14 | 15 | // OS-specific data directory 16 | let dataDir: string; 17 | switch (process.platform) { 18 | case 'darwin': 19 | // macOS: ~/Library/Application Support/aindreyway-mcp-neurolora 20 | dataDir = path.join(homeDir, 'Library', 'Application Support', APP_NAME); 21 | break; 22 | case 'win32': 23 | // Windows: %APPDATA%/aindreyway-mcp-neurolora 24 | dataDir = path.join( 25 | process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 26 | APP_NAME 27 | ); 28 | break; 29 | default: 30 | // Linux/Unix: ~/.local/share/aindreyway-mcp-neurolora 31 | dataDir = path.join(homeDir, '.local', 'share', APP_NAME); 32 | } 33 | 34 | // OS-specific log directory 35 | let logDir: string; 36 | switch (process.platform) { 37 | case 'darwin': 38 | // macOS: ~/Library/Logs/aindreyway-mcp-neurolora 39 | logDir = path.join(homeDir, 'Library', 'Logs', APP_NAME); 40 | break; 41 | case 'win32': 42 | // Windows: %LOCALAPPDATA%/aindreyway-mcp-neurolora/Logs 43 | logDir = path.join( 44 | process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 45 | APP_NAME, 46 | 'Logs' 47 | ); 48 | break; 49 | default: 50 | // Linux/Unix: ~/.local/share/aindreyway-mcp-neurolora/logs 51 | logDir = path.join(dataDir, 'logs'); 52 | } 53 | 54 | // Temporary files directory (same for all OS) 55 | const tempFilesDir = path.join(tempDir, APP_NAME, 'prompts'); 56 | 57 | // Analysis results directory (in data directory) 58 | const analysisDir = path.join(dataDir, 'analysis'); 59 | 60 | return { 61 | // Base directories 62 | homeDir, // User's home directory 63 | tempDir, // System temp directory 64 | dataDir, // App data directory 65 | logDir, // App logs directory 66 | 67 | // Specific directories 68 | tempFilesDir, // Temporary files (PROMPT_FULL_CODE_*.md) 69 | analysisDir, // Analysis results (LAST_RESPONSE_*) 70 | 71 | // Helper functions 72 | getPromptPath: (filename: string) => path.join(tempFilesDir, filename), 73 | getAnalysisPath: (filename: string) => path.join(analysisDir, filename), 74 | getLogPath: (filename: string) => path.join(logDir, filename), 75 | }; 76 | } 77 | 78 | /** 79 | * Ensure all required directories exist 80 | */ 81 | export async function ensureAppDirectories() { 82 | const paths = getAppPaths(); 83 | const dirsToCreate = [paths.tempFilesDir, paths.analysisDir, paths.logDir]; 84 | 85 | for (const dir of dirsToCreate) { 86 | try { 87 | await fs.mkdir(dir, { recursive: true }); 88 | } catch (error) { 89 | throw new Error(`Failed to create directory ${dir}: ${(error as Error).message}`); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/progress.ts: -------------------------------------------------------------------------------- 1 | import { createAnalysisFile } from './project-files.js'; 2 | 3 | /** 4 | * Show progress bar in a single line 5 | */ 6 | export async function showProgress( 7 | percentage: number, 8 | options = { width: 20, prefix: 'Processing' } 9 | ): Promise { 10 | const filled = Math.floor((options.width * percentage) / 100); 11 | const empty = options.width - filled; 12 | const bar = `[${'='.repeat(filled)}>${'.'.repeat(empty)}]`; 13 | 14 | // Move cursor up and clear line 15 | process.stdout.write('\x1b[1A\x1b[2K'); 16 | process.stdout.write(`${options.prefix}... ${bar} ${percentage}% complete\n`); 17 | 18 | // Update file without newlines to keep single line 19 | await createAnalysisFile( 20 | 'LAST_RESPONSE_OPENAI.txt', 21 | `${options.prefix}... ${bar} ${percentage}% complete`, 22 | { append: false } 23 | ); 24 | } 25 | 26 | /** 27 | * Clear progress and show final content 28 | */ 29 | export async function clearProgress(content: string): Promise { 30 | // Clear progress line 31 | process.stdout.write('\x1b[1A\x1b[2K'); 32 | 33 | // Write final content 34 | await createAnalysisFile('LAST_RESPONSE_OPENAI.txt', content, { append: false }); 35 | console.log('Analysis completed. Recommended fixes and improvements:\n'); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/project-files.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { safeWriteFile } from './safe-fs.js'; 3 | 4 | /** 5 | * Create a file in the project root directory 6 | */ 7 | export async function createProjectFile( 8 | filename: string, 9 | content: string, 10 | options = { append: false } 11 | ): Promise { 12 | // Используем абсолютный путь к корню проекта 13 | const projectRoot = path.resolve(process.cwd(), '..'); 14 | const filePath = path.join(projectRoot, filename); 15 | 16 | // Записываем файл 17 | await safeWriteFile(filePath, content, options.append); 18 | 19 | return filePath; 20 | } 21 | 22 | /** 23 | * Create a code collection file in the project root directory 24 | */ 25 | export async function createCodeCollectionFile( 26 | inputPath: string, 27 | content: string 28 | ): Promise { 29 | const date = new Date().toISOString().split('T')[0]; 30 | const inputName = path.basename(inputPath).toUpperCase(); 31 | const filename = `PROMPT_FULL_CODE_${inputName}_${date}.md`; 32 | 33 | return createProjectFile(filename, content); 34 | } 35 | 36 | /** 37 | * Create an analysis file in the project root directory 38 | */ 39 | export async function createAnalysisFile( 40 | filename: string, 41 | content: string, 42 | options = { append: false } 43 | ): Promise { 44 | return createProjectFile(filename, content, options); 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/safe-fs.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | 4 | /** 5 | * Read file from the filesystem 6 | */ 7 | export async function safeReadFile(filePath: string): Promise { 8 | try { 9 | const absolutePath = path.resolve(filePath); 10 | return await fs.readFile(absolutePath, 'utf-8'); 11 | } catch (error) { 12 | if (error instanceof Error) { 13 | throw new Error(`Failed to read file: ${error.message}`); 14 | } 15 | throw error; 16 | } 17 | } 18 | 19 | /** 20 | * Write file to a directory 21 | */ 22 | export async function safeWriteFile( 23 | filePath: string, 24 | content: string, 25 | append: boolean = false 26 | ): Promise { 27 | // Always use absolute paths 28 | const absolutePath = path.resolve(filePath); 29 | const dir = path.dirname(absolutePath); 30 | 31 | try { 32 | // Create directory if it doesn't exist 33 | await fs.mkdir(dir, { recursive: true }); 34 | 35 | // Write or append to the file 36 | if (append) { 37 | await fs.appendFile(absolutePath, content, 'utf-8'); 38 | } else { 39 | await fs.writeFile(absolutePath, content, 'utf-8'); 40 | } 41 | } catch (error) { 42 | if (error instanceof Error) { 43 | throw new Error(`Failed to write file: ${error.message}`); 44 | } 45 | throw error; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | import { CodeCollectorOptions } from '../types/index.js'; 4 | import { DEFAULT_IGNORE_PATTERNS } from '../utils/fs.js'; 5 | 6 | /** 7 | * Validate input path exists and is accessible 8 | */ 9 | export async function validateInput(input: string | string[]): Promise { 10 | if (Array.isArray(input)) { 11 | // Проверяем каждый путь в массиве 12 | const validatedPaths = await Promise.all( 13 | input.map(async filePath => { 14 | try { 15 | const stats = await fs.stat(filePath); 16 | if (!stats.isFile() && !stats.isDirectory()) { 17 | throw new Error(`Invalid path: ${filePath} is neither a file nor a directory`); 18 | } 19 | return filePath; 20 | } catch (error) { 21 | throw new Error(`Invalid path: ${(error as Error).message}`); 22 | } 23 | }) 24 | ); 25 | return validatedPaths; 26 | } else { 27 | // Проверяем одиночный путь 28 | try { 29 | const stats = await fs.stat(input); 30 | if (!stats.isFile() && !stats.isDirectory()) { 31 | throw new Error('Path is neither a file nor a directory'); 32 | } 33 | return input; 34 | } catch (error) { 35 | throw new Error(`Invalid path: ${(error as Error).message}`); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Validate output path format 42 | */ 43 | export function validateOutputPath(outputPath?: string): string { 44 | if (!outputPath) { 45 | throw new Error('Output path is required'); 46 | } 47 | return path.resolve(outputPath); 48 | } 49 | 50 | /** 51 | * Validate ignore patterns are valid 52 | */ 53 | export function validateIgnorePatterns(patterns?: string[]): string[] { 54 | if (!patterns) { 55 | return []; 56 | } 57 | 58 | return patterns.filter(pattern => { 59 | if (typeof pattern !== 'string') { 60 | console.warn(`Invalid ignore pattern: ${pattern}, must be string`); 61 | return false; 62 | } 63 | if (pattern.trim().length === 0) { 64 | console.warn('Empty ignore pattern will be skipped'); 65 | return false; 66 | } 67 | return true; 68 | }); 69 | } 70 | 71 | /** 72 | * Validate all code collector options 73 | */ 74 | export async function validateOptions( 75 | options: CodeCollectorOptions 76 | ): Promise { 77 | const validatedInput = await validateInput(options.input); 78 | 79 | // Определяем имя для выходного файла 80 | const inputName = Array.isArray(options.input) 81 | ? 'MULTIPLE_FILES' 82 | : path.basename(options.input).toUpperCase(); 83 | 84 | // Get current date in YYYY-MM-DD format 85 | const date = new Date().toISOString().split('T')[0]; 86 | 87 | // Always save in project root directory 88 | let outputPath = options.outputPath || ''; 89 | if (!outputPath) { 90 | const projectRoot = process.cwd(); 91 | outputPath = path.join(projectRoot, `PROMPT_FULL_CODE_${inputName}_${date}.md`); 92 | } 93 | 94 | const validatedOutputPath = await validateOutputPath(outputPath); 95 | const validatedIgnorePatterns = validateIgnorePatterns( 96 | options.ignorePatterns || DEFAULT_IGNORE_PATTERNS 97 | ); 98 | 99 | return { 100 | input: validatedInput, 101 | outputPath: validatedOutputPath, 102 | ignorePatterns: validatedIgnorePatterns, 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /test-project/test_code.js: -------------------------------------------------------------------------------- 1 | // Global configuration object 2 | var globalConfig = { 3 | taxRate: 0.2, 4 | currency: 'USD', 5 | apiKey: 'secret_key_123', 6 | debug: true, 7 | }; 8 | 9 | // Process order without proper validation 10 | function processOrder(items) { 11 | var total = 0; 12 | for (var i = 0; i < items.length; i++) { 13 | total += items[i].price; 14 | } 15 | 16 | var tax = total * globalConfig.taxRate; 17 | return total + tax; 18 | } 19 | 20 | // Apply discount without checks 21 | function applyDiscount(total, discount) { 22 | return total - total * discount; 23 | } 24 | 25 | // Calculate total with nested loops 26 | function calculateTotal(orders) { 27 | var grandTotal = 0; 28 | 29 | for (var i = 0; i < orders.length; i++) { 30 | var orderTotal = 0; 31 | for (var j = 0; j < orders[i].items.length; j++) { 32 | orderTotal += orders[i].items[j].price; 33 | } 34 | grandTotal += orderTotal; 35 | } 36 | 37 | return grandTotal; 38 | } 39 | 40 | // Export without proper module system 41 | window.processOrder = processOrder; 42 | window.applyDiscount = applyDiscount; 43 | window.calculateTotal = calculateTotal; 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "build"] 17 | } 18 | --------------------------------------------------------------------------------