├── .gitignore ├── .llm-context ├── config.json ├── state.json └── templates │ ├── context.hbs │ ├── files.hbs │ ├── highlights.hbs │ └── prompt.hbs ├── Dockerfile ├── LICENSE ├── README.md ├── cache └── 4b648882430d8aad657e9165c33c42a605e7b408d335749f5f401b1eacb7e484.cache ├── docs └── technical-design.md ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── index.ts ├── services │ ├── CodeAnalysisService.ts │ ├── FileWatcherService.ts │ ├── ProfileService.ts │ └── TemplateService.ts ├── types.ts ├── types │ └── index.ts └── utils │ └── globAsync.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # Build output 7 | build/ 8 | dist/ 9 | out/ 10 | *.tsbuildinfo 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE and editor files 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | *~ 23 | 24 | # Logs 25 | logs/ 26 | *.log 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Testing 32 | coverage/ 33 | .nyc_output/ 34 | 35 | # Operating System 36 | .DS_Store 37 | Thumbs.db 38 | 39 | # Temporary files 40 | *.tmp 41 | *.temp 42 | .cache/ 43 | 44 | # Debug files 45 | .debug/ 46 | *.debug 47 | 48 | # TypeScript source maps 49 | *.map 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variable files 67 | .env 68 | .env.test 69 | .env.production 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | .parcel-cache 74 | 75 | # Next.js build output 76 | .next 77 | out 78 | 79 | # Nuxt.js build / generate output 80 | .nuxt 81 | dist 82 | 83 | # Gatsby files 84 | .cache/ 85 | # Comment in the public line in if your project uses Gatsby and not Next.js 86 | # public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | 103 | # Stores VSCode versions used for testing VSCode extensions 104 | .vscode-test 105 | 106 | # yarn v2 107 | .yarn/cache 108 | .yarn/unplugged 109 | .yarn/build-state.yml 110 | .yarn/install-state.gz 111 | .pnp.* 112 | 113 | # Local backup files 114 | *.bak 115 | 116 | rules/ -------------------------------------------------------------------------------- /.llm-context/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "context": "context.j2", 4 | "files": "files.j2", 5 | "highlights": "highlights.j2" 6 | }, 7 | "profiles": { 8 | "code": { 9 | "gitignores": { 10 | "full_files": [ 11 | ".git/", 12 | "node_modules/", 13 | "dist/", 14 | "build/", 15 | ".env", 16 | ".env.*", 17 | "*.min.*", 18 | "*.bundle.*" 19 | ], 20 | "outline_files": [ 21 | ".git/", 22 | "node_modules/", 23 | "dist/", 24 | "build/", 25 | ".env", 26 | ".env.*", 27 | "*.min.*", 28 | "*.bundle.*" 29 | ] 30 | }, 31 | "only_includes": { 32 | "full_files": [ 33 | "**/*" 34 | ], 35 | "outline_files": [ 36 | "**/*" 37 | ] 38 | }, 39 | "settings": { 40 | "no_media": true, 41 | "with_user_notes": false 42 | } 43 | }, 44 | "code-prompt": { 45 | "gitignores": { 46 | "full_files": [ 47 | ".git/", 48 | "node_modules/", 49 | "dist/", 50 | "build/", 51 | ".env", 52 | ".env.*", 53 | "*.min.*", 54 | "*.bundle.*" 55 | ], 56 | "outline_files": [ 57 | ".git/", 58 | "node_modules/", 59 | "dist/", 60 | "build/", 61 | ".env", 62 | ".env.*", 63 | "*.min.*", 64 | "*.bundle.*" 65 | ] 66 | }, 67 | "only_includes": { 68 | "full_files": [ 69 | "**/*" 70 | ], 71 | "outline_files": [ 72 | "**/*" 73 | ] 74 | }, 75 | "settings": { 76 | "no_media": true, 77 | "with_user_notes": false 78 | }, 79 | "base": "code", 80 | "prompt": "prompt.md" 81 | }, 82 | "code-file": { 83 | "gitignores": { 84 | "full_files": [ 85 | ".git/", 86 | "node_modules/", 87 | "dist/", 88 | "build/", 89 | ".env", 90 | ".env.*", 91 | "*.min.*", 92 | "*.bundle.*" 93 | ], 94 | "outline_files": [ 95 | ".git/", 96 | "node_modules/", 97 | "dist/", 98 | "build/", 99 | ".env", 100 | ".env.*", 101 | "*.min.*", 102 | "*.bundle.*" 103 | ] 104 | }, 105 | "only_includes": { 106 | "full_files": [ 107 | "**/*" 108 | ], 109 | "outline_files": [ 110 | "**/*" 111 | ] 112 | }, 113 | "settings": { 114 | "no_media": true, 115 | "with_user_notes": false, 116 | "context_file": "project-context.md.tmp" 117 | }, 118 | "base": "code" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /.llm-context/templates/context.hbs: -------------------------------------------------------------------------------- 1 | {{#if prompt}} 2 | {{{prompt}}} 3 | {{/if}} 4 | {{#if project_notes}} 5 | {{{project_notes}}} 6 | {{/if}} 7 | {{#if user_notes}} 8 | {{{user_notes}}} 9 | {{/if}} 10 | # Repository Content: **{{project_name}}** 11 | 12 | > Generation timestamp: {{timestamp}} 13 | > Use lc-list-modified-files tool to track changes since generation 14 | 15 | {{#if sample_requested_files}} 16 | ## File Access Instructions 17 | 18 | Missing/incomplete files (marked "✓" or "○" in the repository structure) can be retrieved using the lc-get-files tool. 19 | {{/if}} 20 | 21 | ## Repository Structure 22 | 23 | ``` 24 | {{{folder_structure_diagram}}} 25 | ``` 26 | 27 | {{#if files}} 28 | ## Current Files 29 | 30 | {{> files}} 31 | {{/if}} 32 | 33 | {{#if highlights}} 34 | ## Code Outlines 35 | 36 | {{> highlights}} 37 | {{/if}} -------------------------------------------------------------------------------- /.llm-context/templates/files.hbs: -------------------------------------------------------------------------------- 1 | {{#each files}} 2 | ### {{path}} 3 | 4 | ```{{language}} 5 | {{{content}}} 6 | ``` 7 | 8 | {{/each}} -------------------------------------------------------------------------------- /.llm-context/templates/highlights.hbs: -------------------------------------------------------------------------------- 1 | {{#each highlights}} 2 | ### {{path}} 3 | 4 | ``` 5 | {{{outline}}} 6 | ``` 7 | 8 | {{/each}} -------------------------------------------------------------------------------- /.llm-context/templates/prompt.hbs: -------------------------------------------------------------------------------- 1 | # LLM Instructions 2 | 3 | ## Role and Context 4 | You are a code-aware AI assistant analyzing the provided repository content. Your task is to help understand, modify, and improve the codebase while maintaining its integrity and following best practices. 5 | 6 | ## Guidelines 7 | 1. Always analyze the full context before making suggestions 8 | 2. Consider dependencies and potential side effects 9 | 3. Maintain consistent code style 10 | 4. Preserve existing functionality unless explicitly asked to change it 11 | 5. Document significant changes 12 | 6. Handle errors gracefully 13 | 14 | ## Response Structure 15 | 1. First, acknowledge the specific files or code sections you're working with 16 | 2. Explain your understanding of the current implementation 17 | 3. Present your suggestions or changes clearly 18 | 4. Include any necessary warnings about potential impacts 19 | 5. Provide context for your decisions 20 | 21 | ## Code Style 22 | - Follow the project's existing conventions 23 | - Maintain consistent indentation and formatting 24 | - Use clear, descriptive names 25 | - Add appropriate comments for complex logic 26 | 27 | ## Security Considerations 28 | - Never expose sensitive information 29 | - Validate inputs appropriately 30 | - Handle errors securely 31 | - Follow security best practices for the language/framework 32 | 33 | ## Performance 34 | - Consider efficiency in your suggestions 35 | - Highlight potential performance impacts 36 | - Suggest optimizations when relevant 37 | 38 | Remember to maintain a balance between ideal solutions and practical constraints within the existing codebase. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | WORKDIR /app 5 | 6 | # Copy package files 7 | COPY package*.json ./ 8 | 9 | # Install dependencies 10 | RUN npm install 11 | 12 | # Copy the rest of the project 13 | COPY . . 14 | 15 | # Build the project 16 | RUN npm run build 17 | 18 | # Set environment variables defaults 19 | ENV MAX_CACHE_SIZE=1000 20 | ENV CACHE_TTL=3600000 21 | ENV MAX_FILE_SIZE=1048576 22 | 23 | # Start the server 24 | CMD ["node", "build/index.js"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 bsmi021 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 | # File Context Server 2 | [![smithery badge](https://smithery.ai/badge/@bsmi021/mcp-file-context-server)](https://smithery.ai/server/@bsmi021/mcp-file-context-server) 3 | 4 | A Model Context Protocol (MCP) server that provides file system context to Large Language Models (LLMs). This server enables LLMs to read, search, and analyze code files with advanced caching and real-time file watching capabilities. 5 | 6 | ## Features 7 | 8 | - **File Operations** 9 | - Read file and directory contents 10 | - List files with detailed metadata 11 | - Real-time file watching and cache invalidation 12 | - Support for multiple file encodings 13 | - Recursive directory traversal 14 | - File type filtering 15 | 16 | - **Code Analysis** 17 | - Cyclomatic complexity calculation 18 | - Dependency extraction 19 | - Comment analysis 20 | - Quality metrics: 21 | - Duplicate lines detection 22 | - Long lines detection (>100 characters) 23 | - Complex function identification 24 | - Line counts (total, non-empty, comments) 25 | 26 | - **Smart Caching** 27 | - LRU (Least Recently Used) caching strategy 28 | - Automatic cache invalidation on file changes 29 | - Size-aware caching with configurable limits 30 | - Cache statistics and performance metrics 31 | - Last read result caching for efficient searches 32 | 33 | - **Advanced Search** 34 | - Regex pattern matching 35 | - Context-aware results with configurable surrounding lines 36 | - File type filtering 37 | - Multi-pattern search support 38 | - Cached result searching 39 | - Exclusion patterns 40 | 41 | ## Installation 42 | 43 | ### Installing via Smithery 44 | 45 | To install File Context Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@bsmi021/mcp-file-context-server): 46 | 47 | ```bash 48 | npx -y @smithery/cli install @bsmi021/mcp-file-context-server --client claude 49 | ``` 50 | 51 | ### Manual Installation 52 | ```bash 53 | npm install @modelcontextprotocol/file-context-server 54 | ``` 55 | 56 | ## Usage 57 | 58 | ### Starting the Server 59 | 60 | ```bash 61 | npx file-context-server 62 | ``` 63 | 64 | ### Available Tools 65 | 66 | 1. **list_context_files** 67 | - Lists files in a directory with detailed metadata 68 | 69 | ```json 70 | { 71 | "path": "./src", 72 | "recursive": true, 73 | "includeHidden": false 74 | } 75 | ``` 76 | 77 | 2. **read_context** 78 | - Reads file or directory contents with metadata 79 | 80 | ```json 81 | { 82 | "path": "./src/index.ts", 83 | "encoding": "utf8", 84 | "maxSize": 1000000, 85 | "recursive": true, 86 | "fileTypes": ["ts", "js"] 87 | } 88 | ``` 89 | 90 | 3. **search_context** 91 | - Searches for patterns in files with context 92 | 93 | ```json 94 | { 95 | "pattern": "function.*", 96 | "path": "./src", 97 | "options": { 98 | "recursive": true, 99 | "contextLines": 2, 100 | "fileTypes": ["ts"] 101 | } 102 | } 103 | ``` 104 | 105 | 4. **analyze_code** 106 | - Analyzes code files for quality metrics 107 | 108 | ```json 109 | { 110 | "path": "./src", 111 | "recursive": true, 112 | "metrics": ["complexity", "dependencies", "quality"] 113 | } 114 | ``` 115 | 116 | 5. **cache_stats** 117 | - Gets cache statistics and performance metrics 118 | 119 | ```json 120 | { 121 | "detailed": true 122 | } 123 | ``` 124 | 125 | ## Error Handling 126 | 127 | The server provides detailed error messages with specific error codes: 128 | 129 | - `FILE_NOT_FOUND`: File or directory does not exist 130 | - `PERMISSION_DENIED`: Access permission issues 131 | - `INVALID_PATH`: Invalid file path format 132 | - `FILE_TOO_LARGE`: File exceeds size limit 133 | - `ENCODING_ERROR`: File encoding issues 134 | - `UNKNOWN_ERROR`: Unexpected errors 135 | 136 | ## Configuration 137 | 138 | Environment variables for customization: 139 | 140 | - `MAX_CACHE_SIZE`: Maximum number of cached entries (default: 1000) 141 | - `CACHE_TTL`: Cache time-to-live in milliseconds (default: 1 hour) 142 | - `MAX_FILE_SIZE`: Maximum file size in bytes for reading 143 | 144 | ## Development 145 | 146 | ```bash 147 | # Install dependencies 148 | npm install 149 | 150 | # Build 151 | npm run build 152 | 153 | # Run tests 154 | npm test 155 | 156 | # Start in development mode 157 | npm run dev 158 | ``` 159 | 160 | ## License 161 | 162 | MIT 163 | 164 | ## Contributing 165 | 166 | Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. 167 | 168 | ## Cross-Platform Path Compatibility 169 | 170 | **Note:** As of April 2025, all file and directory path handling in File Context Server has been updated for improved cross-platform compatibility (Windows, macOS, Linux): 171 | 172 | - All glob patterns use POSIX-style paths (forward slashes) internally, ensuring consistent file matching regardless of OS. 173 | - All file system operations (reading, writing, stat, etc.) use normalized absolute paths for reliability. 174 | - If you are developing or extending the server, use `path.posix.join` for glob patterns and `path.normalize` for file system access. 175 | - This change prevents issues with path separators and file matching on different operating systems. 176 | 177 | No changes are required for end users, but developers should follow these conventions when contributing to the project. 178 | -------------------------------------------------------------------------------- /docs/technical-design.md: -------------------------------------------------------------------------------- 1 | # Technical Design: MCP File Context Server Initial Read Optimization 2 | 3 | ## 1. System Requirements 4 | 5 | ### 1.1 Hardware Requirements 6 | - CPU: Multi-core processor (recommended minimum 4 cores) 7 | - Memory: Minimum 8GB RAM, recommended 16GB for large codebases 8 | - Storage: SSD recommended for optimal I/O performance 9 | 10 | ### 1.2 Software Requirements 11 | - Node.js 18.0 or higher 12 | - TypeScript 4.5 or higher 13 | - Operating System: Cross-platform (Windows, Linux, macOS) 14 | 15 | ### 1.3 Dependencies 16 | ```typescript 17 | interface DependencyRequirements { 18 | required: { 19 | 'lru-cache': '^7.0.0', 20 | 'chokidar': '^3.5.0', 21 | '@modelcontextprotocol/sdk': '^1.0.0', 22 | 'mime-types': '^2.1.0' 23 | }, 24 | optional: { 25 | 'worker-threads': '^1.0.0', // For parallel processing 26 | 'node-worker-threads-pool': '^1.5.0' // For worker pool management 27 | } 28 | } 29 | ``` 30 | 31 | ## 2. Architecture Components 32 | 33 | ### 2.1 Memory Management System 34 | ```typescript 35 | interface MemoryConfig { 36 | maxCacheSize: number; // Maximum memory for cache in MB 37 | workerMemoryLimit: number; // Memory limit per worker in MB 38 | gcThreshold: number; // GC trigger threshold (0.8 = 80%) 39 | emergencyFreeThreshold: number; // Emergency memory release threshold 40 | } 41 | 42 | class MemoryManager { 43 | private memoryUsage: number; 44 | private totalAllocated: number; 45 | 46 | constructor(private config: MemoryConfig) { 47 | this.setupMemoryMonitoring(); 48 | } 49 | 50 | private setupMemoryMonitoring() { 51 | if (globalThis.gc) { 52 | // Register memory pressure handlers 53 | this.setupMemoryPressureHandlers(); 54 | } 55 | } 56 | 57 | private shouldTriggerGC(): boolean { 58 | const usage = process.memoryUsage(); 59 | return (usage.heapUsed / usage.heapTotal) > this.config.gcThreshold; 60 | } 61 | } 62 | ``` 63 | 64 | ### 2.2 Worker Pool Management 65 | ```typescript 66 | interface WorkerPoolConfig { 67 | minWorkers: number; 68 | maxWorkers: number; 69 | idleTimeout: number; // ms before releasing idle worker 70 | taskTimeout: number; // ms before task timeout 71 | } 72 | 73 | class WorkerPoolManager { 74 | private workerPool: Map; 75 | private taskQueue: Queue; 76 | private activeWorkers: number; 77 | 78 | constructor(private config: WorkerPoolConfig) { 79 | this.initializeWorkerPool(); 80 | } 81 | 82 | private async initializeWorkerPool() { 83 | const initialWorkers = Math.min( 84 | this.config.minWorkers, 85 | os.cpus().length 86 | ); 87 | 88 | for (let i = 0; i < initialWorkers; i++) { 89 | await this.addWorker(); 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ### 2.3 Enhanced Cache Service Configuration 96 | ```typescript 97 | interface CacheConfig { 98 | maxSize: number; 99 | parallelProcessing: { 100 | enabled: boolean; 101 | maxWorkers?: number; 102 | chunkSize?: number; 103 | }; 104 | preloadStrategy: { 105 | enabled: boolean; 106 | maxPreloadItems: number; 107 | preloadDepth: number; 108 | }; 109 | progressiveLoading: { 110 | enabled: boolean; 111 | priorityLevels: number; 112 | }; 113 | memoryManagement: { 114 | maxMemoryPercent: number; 115 | gcThreshold: number; 116 | emergencyThreshold: number; 117 | }; 118 | storage: { 119 | persistToDisk: boolean; 120 | compressionLevel?: number; 121 | storageLocation?: string; 122 | }; 123 | } 124 | ``` 125 | 126 | ## 3. Core Components 127 | 128 | ### 3.1 Parallel File Reader Service 129 | ```typescript 130 | class ParallelFileReader { 131 | private workerPool: WorkerPoolManager; 132 | private memoryManager: MemoryManager; 133 | 134 | constructor(config: FileReaderConfig) { 135 | this.workerPool = new WorkerPoolManager({ 136 | minWorkers: 2, 137 | maxWorkers: os.cpus().length, 138 | idleTimeout: 60000, 139 | taskTimeout: 30000 140 | }); 141 | 142 | this.memoryManager = new MemoryManager({ 143 | maxCacheSize: config.maxCacheSize, 144 | workerMemoryLimit: config.workerMemoryLimit, 145 | gcThreshold: 0.8, 146 | emergencyFreeThreshold: 0.95 147 | }); 148 | } 149 | 150 | async readFileChunked(filepath: string, options: ReadOptions): Promise { 151 | const stats = await fs.stat(filepath); 152 | const chunks: Buffer[] = []; 153 | const chunkSize = this.calculateOptimalChunkSize(stats.size); 154 | 155 | // Distribute chunks to worker pool 156 | const chunkTasks = this.createChunkTasks(filepath, stats.size, chunkSize); 157 | const results = await this.workerPool.executeBatch(chunkTasks); 158 | 159 | return this.assembleResults(results, stats); 160 | } 161 | } 162 | ``` 163 | 164 | ### 3.2 Messaging Architecture 165 | ```typescript 166 | interface Message { 167 | id: string; 168 | type: MessageType; 169 | payload: any; 170 | metadata: { 171 | timestamp: number; 172 | priority: number; 173 | timeout?: number; 174 | }; 175 | } 176 | 177 | class MessageBroker { 178 | private subscriptions: Map>; 179 | private priorityQueue: PriorityQueue; 180 | 181 | constructor() { 182 | this.subscriptions = new Map(); 183 | this.priorityQueue = new PriorityQueue(); 184 | } 185 | 186 | async publish(message: Message): Promise { 187 | const handlers = this.subscriptions.get(message.type); 188 | if (handlers) { 189 | await Promise.all( 190 | Array.from(handlers).map(handler => 191 | handler(message.payload) 192 | ) 193 | ); 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | ### 3.3 File Analysis Pipeline 200 | ```typescript 201 | interface AnalysisPipeline { 202 | stages: Array<{ 203 | name: string; 204 | processor: (chunk: Buffer) => Promise; 205 | priority: number; 206 | }>; 207 | } 208 | 209 | class FileAnalyzer { 210 | private pipeline: AnalysisPipeline; 211 | private messageBroker: MessageBroker; 212 | 213 | constructor() { 214 | this.pipeline = { 215 | stages: [ 216 | { 217 | name: 'header', 218 | processor: this.analyzeFileHeader, 219 | priority: 1 220 | }, 221 | { 222 | name: 'content', 223 | processor: this.analyzeContent, 224 | priority: 2 225 | }, 226 | { 227 | name: 'metadata', 228 | processor: this.extractMetadata, 229 | priority: 3 230 | } 231 | ] 232 | }; 233 | } 234 | } 235 | ``` 236 | 237 | ## 4. Performance Optimization Strategies 238 | 239 | ### 4.1 Memory Management Strategies 240 | ```typescript 241 | class OptimizedMemoryStrategy { 242 | private readonly BUFFER_POOL_SIZE = 8192; 243 | private bufferPool: Buffer[]; 244 | 245 | constructor() { 246 | this.bufferPool = Array(10).fill(null) 247 | .map(() => Buffer.allocUnsafe(this.BUFFER_POOL_SIZE)); 248 | } 249 | 250 | private acquireBuffer(): Buffer { 251 | return this.bufferPool.pop() || 252 | Buffer.allocUnsafe(this.BUFFER_POOL_SIZE); 253 | } 254 | 255 | private releaseBuffer(buffer: Buffer) { 256 | if (this.bufferPool.length < 10) { 257 | this.bufferPool.push(buffer); 258 | } 259 | } 260 | } 261 | ``` 262 | 263 | ### 4.2 Worker Thread Management 264 | ```typescript 265 | class WorkerThreadManager { 266 | private workers: Worker[]; 267 | private taskQueue: PriorityQueue; 268 | private activeWorkers: Set; 269 | 270 | constructor(private config: WorkerConfig) { 271 | this.workers = []; 272 | this.taskQueue = new PriorityQueue(); 273 | this.activeWorkers = new Set(); 274 | } 275 | 276 | async executeTask(task: Task): Promise { 277 | const worker = await this.getAvailableWorker(); 278 | this.activeWorkers.add(worker); 279 | 280 | try { 281 | return await this.runTaskInWorker(worker, task); 282 | } finally { 283 | this.activeWorkers.delete(worker); 284 | this.releaseWorker(worker); 285 | } 286 | } 287 | } 288 | ``` 289 | 290 | ### 4.3 I/O Optimization 291 | ```typescript 292 | class IOOptimizer { 293 | private readonly PAGE_SIZE = 4096; 294 | private readonly READ_AHEAD = 4; 295 | 296 | constructor(private config: IOConfig) { 297 | this.initializeIOBuffers(); 298 | } 299 | 300 | private async readWithReadAhead( 301 | fd: number, 302 | position: number, 303 | size: number 304 | ): Promise { 305 | // Implement read-ahead buffering 306 | const readAheadSize = this.PAGE_SIZE * this.READ_AHEAD; 307 | const buffer = Buffer.allocUnsafe(readAheadSize); 308 | 309 | await fs.read(fd, buffer, 0, readAheadSize, position); 310 | return buffer.slice(0, size); 311 | } 312 | } 313 | ``` 314 | 315 | ## 5. Integration Points 316 | 317 | ### 5.1 Cache Service Integration 318 | ```typescript 319 | class EnhancedCacheService extends CacheService { 320 | private fileReader: ParallelFileReader; 321 | private memoryManager: MemoryManager; 322 | private messageBroker: MessageBroker; 323 | 324 | constructor(config: CacheServiceConfig) { 325 | super(config); 326 | this.initializeServices(config); 327 | } 328 | 329 | private async initializeServices(config: CacheServiceConfig) { 330 | this.fileReader = new ParallelFileReader(config); 331 | this.memoryManager = new MemoryManager(config.memoryManagement); 332 | this.messageBroker = new MessageBroker(); 333 | 334 | await this.setupMessageHandlers(); 335 | } 336 | } 337 | ``` 338 | 339 | ### 5.2 Event System Integration 340 | ```typescript 341 | interface EventConfig { 342 | maxListeners: number; 343 | errorThreshold: number; 344 | debugMode: boolean; 345 | } 346 | 347 | class EventSystem { 348 | private eventEmitter: EventEmitter; 349 | private errorCount: Map; 350 | 351 | constructor(private config: EventConfig) { 352 | this.eventEmitter = new EventEmitter(); 353 | this.errorCount = new Map(); 354 | 355 | this.setupErrorHandling(); 356 | } 357 | 358 | private setupErrorHandling() { 359 | this.eventEmitter.on('error', (error: Error) => { 360 | this.handleError(error); 361 | }); 362 | } 363 | } 364 | ``` 365 | 366 | ## 6. Configuration Examples 367 | 368 | ### 6.1 Development Configuration 369 | ```typescript 370 | const devConfig: CacheServiceConfig = { 371 | maxSize: 1000, 372 | parallelProcessing: { 373 | enabled: true, 374 | maxWorkers: 4, 375 | chunkSize: 1024 * 1024 376 | }, 377 | memoryManagement: { 378 | maxMemoryPercent: 70, 379 | gcThreshold: 0.8, 380 | emergencyThreshold: 0.95 381 | }, 382 | storage: { 383 | persistToDisk: true, 384 | compressionLevel: 1, 385 | storageLocation: './cache' 386 | } 387 | }; 388 | ``` 389 | 390 | ### 6.2 Production Configuration 391 | ```typescript 392 | const prodConfig: CacheServiceConfig = { 393 | maxSize: 5000, 394 | parallelProcessing: { 395 | enabled: true, 396 | maxWorkers: 8, 397 | chunkSize: 2 * 1024 * 1024 398 | }, 399 | memoryManagement: { 400 | maxMemoryPercent: 85, 401 | gcThreshold: 0.75, 402 | emergencyThreshold: 0.9 403 | }, 404 | storage: { 405 | persistToDisk: true, 406 | compressionLevel: 4, 407 | storageLocation: '/var/cache/mcp' 408 | } 409 | }; 410 | ``` 411 | 412 | ### 6.3 Containerized Configuration 413 | ```typescript 414 | const containerConfig: CacheServiceConfig = { 415 | maxSize: 2000, 416 | parallelProcessing: { 417 | enabled: true, 418 | maxWorkers: 2, 419 | chunkSize: 512 * 1024 420 | }, 421 | memoryManagement: { 422 | maxMemoryPercent: 60, 423 | gcThreshold: 0.7, 424 | emergencyThreshold: 0.85 425 | }, 426 | storage: { 427 | persistToDisk: false 428 | } 429 | }; 430 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-context-server", 3 | "version": "1.0.0", 4 | "description": "File context server for Model Context Protocol", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "ts-node-esm src/index.ts", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@modelcontextprotocol/sdk": "^1.0.0", 15 | "@typescript-eslint/parser": "^6.0.0", 16 | "@typescript-eslint/types": "^6.0.0", 17 | "chokidar": "^4.0.3", 18 | "glob": "^10.0.0", 19 | "handlebars": "^4.7.8", 20 | "lru-cache": "^10.4.3", 21 | "mime-types": "^2.1.35", 22 | "typescript": "^5.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/glob": "^8.1.0", 26 | "@types/mime-types": "^2.1.1", 27 | "@types/node": "^20.0.0", 28 | "ts-node": "^10.9.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: {} 9 | commandFunction: 10 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 11 | |- 12 | (config) => ({ command: 'node', args: ['build/index.js'], env: { MAX_CACHE_SIZE: process.env.MAX_CACHE_SIZE || '1000', CACHE_TTL: process.env.CACHE_TTL || '3600000', MAX_FILE_SIZE: process.env.MAX_FILE_SIZE || '1048576' } }) 13 | exampleConfig: {} 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | import { promises as fs } from 'fs'; 11 | import { createReadStream, ReadStream } from 'fs'; 12 | import { createGzip, Gzip } from 'zlib'; 13 | import { pipeline, Transform } from 'stream'; 14 | import { glob as globAsync } from 'glob'; 15 | import * as path from 'path'; 16 | import * as mime from 'mime-types'; 17 | import { createHash } from 'crypto'; 18 | import { 19 | FileMetadata, 20 | FileContent, 21 | FileEntry, 22 | FileErrorCode, 23 | FileOperationError, 24 | SearchResult, 25 | FileOutline, 26 | } from './types.js'; 27 | import { FileWatcherService } from './services/FileWatcherService.js'; 28 | import { ProfileService } from './services/ProfileService.js'; 29 | import { TemplateService } from './services/TemplateService.js'; 30 | import { CodeAnalysisService } from './services/CodeAnalysisService.js'; 31 | import { LRUCache } from 'lru-cache'; 32 | 33 | interface StreamConfig { 34 | chunkSize: number; 35 | useCompression: boolean; 36 | compressionLevel?: number; 37 | } 38 | 39 | interface ChunkInfo { 40 | totalChunks: number; 41 | chunkSize: number; 42 | lastChunkSize: number; 43 | totalSize: number; 44 | } 45 | 46 | interface FileInfo { 47 | path: string; 48 | content: string; 49 | hash: string; 50 | size: number; 51 | lastModified: number; 52 | } 53 | 54 | interface FilesInfo { 55 | [path: string]: FileInfo; 56 | } 57 | 58 | const DEFAULT_IGNORE_PATTERNS = [ 59 | // Version Control 60 | '.git/', 61 | 62 | // Python 63 | '.venv/', 64 | 'venv/', 65 | '__pycache__/', 66 | '*.pyc', 67 | '.pytest_cache/', 68 | '.coverage', 69 | 'coverage/', 70 | '*.egg-info/', 71 | 72 | // JavaScript/Node.js 73 | 'node_modules/', 74 | 'bower_components/', 75 | '.npm/', 76 | '.yarn/', 77 | '.pnp.*', 78 | '.next/', 79 | '.nuxt/', 80 | '.output/', 81 | 'dist/', 82 | 'build/', 83 | '.cache/', 84 | '*.min.js', 85 | '*.bundle.js', 86 | '*.bundle.js.map', 87 | 88 | // IDE/Editor 89 | '.DS_Store', 90 | '.idea/', 91 | '.vscode/', 92 | '*.swp', 93 | '*.swo', 94 | '.env', 95 | '.env.local', 96 | '.env.*', 97 | ]; 98 | 99 | const DEFAULT_CONFIG: StreamConfig = { 100 | chunkSize: 64 * 1024, // 64KB chunks 101 | useCompression: false, 102 | compressionLevel: 6 103 | }; 104 | 105 | class FileContextServer { 106 | private server: Server; 107 | private fileWatcherService: FileWatcherService; 108 | private profileService: ProfileService; 109 | private templateService: TemplateService; 110 | private config: StreamConfig; 111 | private fileContentCache: LRUCache; 112 | private codeAnalysisService: CodeAnalysisService; 113 | 114 | /** 115 | * Create standardized file content object 116 | */ 117 | private async createFileContent(content: string, metadata: FileMetadata, filePath: string, encoding: string = 'utf8'): Promise { 118 | return { 119 | content, 120 | metadata, 121 | encoding, 122 | truncated: false, 123 | totalLines: content.split('\n').length, 124 | path: filePath 125 | }; 126 | } 127 | 128 | /** 129 | * Create standardized JSON response 130 | */ 131 | private createJsonResponse(data: any): { content: { type: string; text: string }[] } { 132 | return { 133 | content: [{ 134 | type: 'text', 135 | text: JSON.stringify(data, null, 2) 136 | }] 137 | }; 138 | } 139 | 140 | /** 141 | * Handle file operation errors consistently 142 | */ 143 | private handleFileOperationError(error: unknown, context: string, path: string): never { 144 | if (error instanceof FileOperationError) throw error; 145 | throw new FileOperationError( 146 | FileErrorCode.UNKNOWN_ERROR, 147 | `Failed to ${context}: ${error instanceof Error ? error.message : 'Unknown error'}`, 148 | path 149 | ); 150 | } 151 | 152 | /** 153 | * Process file content using streaming when appropriate 154 | */ 155 | private async processFile(filePath: string, metadata: FileMetadata, encoding: string = 'utf8'): Promise { 156 | try { 157 | const { content } = await this.readFileWithEncoding(filePath, encoding); 158 | 159 | return { 160 | content, 161 | metadata, 162 | encoding, 163 | truncated: false, 164 | totalLines: content.split('\n').length, 165 | path: filePath 166 | }; 167 | } catch (error) { 168 | throw new FileOperationError( 169 | FileErrorCode.UNKNOWN_ERROR, 170 | `Failed to process file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`, 171 | filePath 172 | ); 173 | } 174 | } 175 | 176 | /** 177 | * Creates a readable stream for file with optional compression 178 | */ 179 | private createFileStream(filePath: string, useCompression: boolean = false): ReadStream | Gzip { 180 | const fileStream = createReadStream(filePath, { 181 | highWaterMark: this.config.chunkSize 182 | }); 183 | 184 | if (useCompression) { 185 | const gzip = createGzip({ 186 | level: this.config.compressionLevel 187 | }); 188 | return fileStream.pipe(gzip); 189 | } 190 | 191 | return fileStream; 192 | } 193 | 194 | /** 195 | * Memory-efficient search implementation using streams 196 | */ 197 | private async streamSearch( 198 | filePath: string, 199 | searchPattern: string, 200 | options: { 201 | useCompression?: boolean; 202 | context?: number; 203 | } = {} 204 | ): Promise { 205 | const { 206 | useCompression = this.config.useCompression, 207 | context = 2 208 | } = options; 209 | 210 | const matches: SearchResult['matches'] = []; 211 | const contextLines: string[] = []; 212 | let lineNumber = 0; 213 | 214 | const processLine = (line: string) => { 215 | lineNumber++; 216 | contextLines.push(line); 217 | 218 | if (contextLines.length > context * 2 + 1) { 219 | contextLines.shift(); 220 | } 221 | 222 | const regex = new RegExp(searchPattern); 223 | if (regex.test(line)) { 224 | matches.push({ 225 | path: filePath, 226 | line: lineNumber, 227 | content: line, 228 | context: { 229 | before: contextLines.slice(0, -1), 230 | after: [] 231 | } 232 | }); 233 | } 234 | }; 235 | 236 | await this.processFileStream( 237 | filePath, 238 | async (chunk) => { 239 | const lines = chunk.toString().split(/\r?\n/); 240 | lines.forEach(processLine); 241 | }, 242 | useCompression 243 | ); 244 | 245 | return matches; 246 | } 247 | 248 | constructor(config: Partial = {}) { 249 | this.fileWatcherService = new FileWatcherService(); 250 | this.profileService = new ProfileService(process.cwd()); 251 | this.templateService = new TemplateService(process.cwd()); 252 | this.config = { ...DEFAULT_CONFIG, ...config }; 253 | this.fileContentCache = new LRUCache({ 254 | max: 500, // Maximum number of items to store 255 | ttl: 1000 * 60 * 5 // Time to live: 5 minutes 256 | }); 257 | this.codeAnalysisService = new CodeAnalysisService(); 258 | 259 | this.server = new Server( 260 | { 261 | name: 'file-context-server', 262 | version: '1.0.0', 263 | }, 264 | { 265 | capabilities: { 266 | tools: { 267 | 268 | read_context: { 269 | description: 'WARNING: Run get_chunk_count first to determine total chunks, then request specific chunks using chunkNumber parameter.\nRead and analyze code files with advanced filtering and chunking. The server automatically ignores common artifact directories and files:\n- Version Control: .git/\n- Python: .venv/, __pycache__/, *.pyc, etc.\n- JavaScript/Node.js: node_modules/, bower_components/, .next/, dist/, etc.\n- IDE/Editor: .idea/, .vscode/, .env, etc.\n\n**WARNING** use get_chunk_count first to determine total chunks, then request specific chunks using chunkNumber parameter.', 270 | inputSchema: { 271 | type: 'object', 272 | properties: { 273 | path: { 274 | type: 'string', 275 | description: 'Path to file or directory to read' 276 | }, 277 | maxSize: { 278 | type: 'number', 279 | description: 'Maximum file size in bytes. Files larger than this will be chunked.', 280 | default: 1048576 281 | }, 282 | encoding: { 283 | type: 'string', 284 | description: 'File encoding (e.g., utf8, ascii, latin1)', 285 | default: 'utf8' 286 | }, 287 | recursive: { 288 | type: 'boolean', 289 | description: 'Whether to read directories recursively (includes subdirectories)', 290 | default: true 291 | }, 292 | fileTypes: { 293 | type: ['array', 'string'], 294 | items: { type: 'string' }, 295 | description: 'File extension(s) to include WITHOUT dots (e.g. ["ts", "js", "py"] or just "ts"). Empty/undefined means all files.', 296 | default: [] 297 | }, 298 | chunkNumber: { 299 | type: 'number', 300 | description: 'Which chunk to return (0-based). Use with get_chunk_count to handle large files/directories.', 301 | default: 0 302 | } 303 | }, 304 | required: ['path'] 305 | } 306 | }, 307 | search_context: { 308 | description: 'Search for patterns in files with context', 309 | inputSchema: { 310 | type: 'object', 311 | properties: { 312 | pattern: { 313 | type: 'string', 314 | description: 'Search pattern (regex supported)' 315 | }, 316 | path: { 317 | type: 'string', 318 | description: 'Directory to search in' 319 | }, 320 | options: { 321 | type: 'object', 322 | properties: { 323 | recursive: { 324 | type: 'boolean', 325 | default: true 326 | }, 327 | includeHidden: { 328 | type: 'boolean', 329 | default: false 330 | }, 331 | fileTypes: { 332 | type: 'array', 333 | items: { type: 'string' } 334 | } 335 | } 336 | } 337 | }, 338 | required: ['pattern', 'path'] 339 | } 340 | }, 341 | get_chunk_count: { 342 | description: 'RUN ME ONE TIME BEFORE READING CONTENT\nGet the total number of chunks that will be returned for a read_context request.\nUse this tool FIRST before reading content to determine how many chunks you need to request.\nThe parameters should match what you\'ll use in read_context.', 343 | inputSchema: { 344 | type: 'object', 345 | properties: { 346 | path: { 347 | type: 'string', 348 | description: 'Path to file or directory' 349 | }, 350 | encoding: { 351 | type: 'string', 352 | description: 'File encoding (e.g., utf8, ascii, latin1)', 353 | default: 'utf8' 354 | }, 355 | maxSize: { 356 | type: 'number', 357 | description: 'Maximum file size in bytes. Files larger than this will be chunked.', 358 | default: 1048576 359 | }, 360 | recursive: { 361 | type: 'boolean', 362 | description: 'Whether to read directories recursively (includes subdirectories)', 363 | default: true 364 | }, 365 | fileTypes: { 366 | type: ['array', 'string'], 367 | items: { type: 'string' }, 368 | description: 'File extension(s) to include WITHOUT dots (e.g. ["ts", "js", "py"] or just "ts"). Empty/undefined means all files.', 369 | default: [] 370 | } 371 | }, 372 | required: ['path'] 373 | } 374 | }, 375 | set_profile: { 376 | description: 'Set the active profile for context generation. Available profiles: code (default), code-prompt (includes LLM instructions), code-file (saves to file)', 377 | inputSchema: { 378 | type: 'object', 379 | properties: { 380 | profile_name: { 381 | type: 'string', 382 | description: 'Name of the profile to activate' 383 | } 384 | }, 385 | required: ['profile_name'] 386 | } 387 | }, 388 | get_profile_context: { 389 | description: 'Get repository context based on current profile settings. Includes directory structure, file contents, and code outlines based on profile configuration.', 390 | inputSchema: { 391 | type: 'object', 392 | properties: { 393 | refresh: { 394 | type: 'boolean', 395 | description: 'Whether to refresh file selection before generating context', 396 | default: false 397 | } 398 | } 399 | } 400 | }, 401 | generate_outline: { 402 | description: 'Generate a code outline for a file, showing its structure (classes, functions, imports, etc). Supports TypeScript/JavaScript and Python files.', 403 | inputSchema: { 404 | type: 'object', 405 | properties: { 406 | path: { 407 | type: 'string', 408 | description: 'Path to the file to analyze' 409 | } 410 | }, 411 | required: ['path'] 412 | } 413 | } 414 | } 415 | } 416 | } 417 | ); 418 | 419 | // Error handling 420 | this.server.onerror = (error) => console.error('[MCP Error]', error); 421 | process.on('SIGINT', async () => { 422 | await this.cleanup(); 423 | process.exit(0); 424 | }); 425 | 426 | // Setup file watcher event handlers 427 | this.fileWatcherService.on('fileChanged', async (filePath) => { 428 | try { 429 | const stat = await fs.stat(filePath); 430 | const content = await fs.readFile(filePath, 'utf8'); 431 | this.fileContentCache.set(filePath, { 432 | content, 433 | lastModified: stat.mtimeMs 434 | }); 435 | } catch (error) { 436 | console.error(`Error processing file change for ${filePath}:`, error); 437 | } 438 | }); 439 | } 440 | 441 | /** 442 | * Cleanup resources before shutdown 443 | */ 444 | private async cleanup(): Promise { 445 | await this.fileWatcherService.close(); 446 | await this.server.close(); 447 | this.fileContentCache.clear(); 448 | } 449 | 450 | /** 451 | * Validate file access permissions and resolve path 452 | */ 453 | private async validateAccess(filePath: string): Promise { 454 | const resolvedPath = path.resolve(process.cwd(), filePath); 455 | try { 456 | await fs.access(resolvedPath); 457 | return resolvedPath; 458 | } catch (error) { 459 | console.error(`Access validation failed for path: ${resolvedPath}`, error); 460 | throw new FileOperationError( 461 | FileErrorCode.INVALID_PATH, 462 | `Path does not exist or is not accessible: ${resolvedPath}`, 463 | resolvedPath 464 | ); 465 | } 466 | } 467 | 468 | /** 469 | * Read file with encoding detection and streaming support 470 | */ 471 | private async readFileWithEncoding( 472 | filePath: string, 473 | encoding: string = 'utf8', 474 | options: { 475 | useStreaming?: boolean; 476 | useCompression?: boolean; 477 | } = {} 478 | ): Promise<{ content: string, encoding: string }> { 479 | const stats = await fs.stat(filePath); 480 | const useStreaming = options.useStreaming ?? (stats.size > 10 * 1024 * 1024); 481 | 482 | if (useStreaming) { 483 | let content = ''; 484 | await this.processFileStream( 485 | filePath, 486 | async (chunk) => { 487 | content += chunk.toString(encoding as BufferEncoding); 488 | }, 489 | options.useCompression 490 | ); 491 | return { content, encoding }; 492 | } 493 | 494 | // For smaller files, use regular file reading 495 | const buffer = await fs.readFile(filePath); 496 | 497 | // Try to detect UTF-16 BOM 498 | if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { 499 | return { 500 | content: buffer.toString('utf16le'), 501 | encoding: 'utf16le' 502 | }; 503 | } 504 | 505 | return { 506 | content: buffer.toString(encoding as BufferEncoding), 507 | encoding 508 | }; 509 | } 510 | 511 | private async getFileMetadata(filePath: string): Promise { 512 | try { 513 | const stats = await fs.stat(filePath); 514 | const ext = path.extname(filePath).slice(1); 515 | const language = this.getLanguageFromExtension(ext); 516 | 517 | let analysis = null; 518 | if (language) { 519 | const content = await fs.readFile(filePath, 'utf8'); 520 | analysis = await this.codeAnalysisService.analyzeCode(content, language); 521 | } 522 | 523 | return { 524 | size: stats.size, 525 | mimeType: mime.lookup(filePath) || 'application/octet-stream', 526 | modifiedTime: stats.mtime.toISOString(), 527 | createdTime: stats.birthtime.toISOString(), 528 | isDirectory: stats.isDirectory(), 529 | lastAnalyzed: new Date().toISOString(), 530 | ...(analysis && { 531 | metrics: { 532 | linesOfCode: analysis.complexity_metrics.linesOfCode, 533 | numberOfFunctions: analysis.complexity_metrics.numberOfFunctions, 534 | cyclomaticComplexity: analysis.complexity_metrics.cyclomaticComplexity, 535 | maintainabilityIndex: analysis.complexity_metrics.maintainabilityIndex 536 | } 537 | }) 538 | }; 539 | } catch (error) { 540 | throw new FileOperationError( 541 | FileErrorCode.FILE_NOT_FOUND, 542 | `Failed to get metadata for ${filePath}`, 543 | filePath 544 | ); 545 | } 546 | } 547 | 548 | private async globPromise(pattern: string, options: any): Promise { 549 | try { 550 | // Always resolve to absolute path and convert to POSIX for glob compatibility 551 | const absolutePath = path.resolve(pattern); 552 | const globPattern = absolutePath.split(path.sep).join(path.posix.sep); 553 | console.error(`Glob pattern: ${globPattern}`); 554 | 555 | // Add .meta to ignore list if not already present 556 | const ignore = [...(options.ignore || [])]; 557 | if (!ignore.includes('**/*.meta')) { 558 | ignore.push('**/*.meta'); 559 | } 560 | 561 | const result = await globAsync(globPattern, { 562 | ...options, 563 | ignore, 564 | withFileTypes: false, 565 | windowsPathsNoEscape: true, 566 | absolute: true 567 | }); 568 | 569 | const paths = Array.isArray(result) ? result : [result]; 570 | console.error(`Glob found ${paths.length} paths`); 571 | 572 | // Always return normalized absolute paths for file system operations 573 | return paths.map(entry => path.normalize(entry.toString())); 574 | } catch (error) { 575 | console.error('Glob error:', error); 576 | throw new FileOperationError( 577 | FileErrorCode.UNKNOWN_ERROR, 578 | `Glob operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 579 | pattern 580 | ); 581 | } 582 | } 583 | 584 | private async handleListFiles(args: any) { 585 | const { path: dirPath, recursive = false, includeHidden = false } = args; 586 | 587 | try { 588 | await this.validateAccess(dirPath); 589 | await this.fileWatcherService.watch(dirPath); 590 | 591 | const entries: FileEntry[] = []; 592 | // Always resolve and convert to POSIX for glob 593 | const normalizedDirPath = path.resolve(dirPath); 594 | const pattern = recursive ? '**/*' : '*'; 595 | const globPattern = path.posix.join(normalizedDirPath.split(path.sep).join(path.posix.sep), pattern); 596 | 597 | console.error(`Directory path: ${normalizedDirPath}`); 598 | console.error(`Glob pattern: ${globPattern}`); 599 | 600 | const files = await this.globPromise(globPattern, { 601 | ignore: includeHidden ? [] : ['.*', '**/.*'], 602 | nodir: false, 603 | dot: includeHidden 604 | }); 605 | console.error(`Found files: ${files.length}`); 606 | 607 | for (const file of files) { 608 | try { 609 | // file is already absolute and normalized 610 | const fullPath = file; 611 | const metadata = await this.getFileMetadata(fullPath); 612 | await this.processFile(fullPath, metadata, 'utf8'); 613 | 614 | entries.push({ 615 | path: fullPath, 616 | name: path.basename(fullPath), 617 | metadata, 618 | }); 619 | } catch (error) { 620 | console.error(`Error getting metadata for ${file}: ${error}`); 621 | } 622 | } 623 | 624 | return this.createJsonResponse({ 625 | entries, 626 | metadata: { 627 | totalFiles: entries.length, 628 | searchPath: dirPath, 629 | timestamp: new Date().toISOString() 630 | } 631 | }); 632 | } catch (error) { 633 | throw this.handleFileOperationError(error, 'list files', dirPath); 634 | } 635 | } 636 | 637 | /** 638 | * Calculate chunk information for a file 639 | */ 640 | private async calculateChunkInfo(filePath: string, chunkSize: number): Promise { 641 | const stats = await fs.stat(filePath); 642 | const totalSize = stats.size; 643 | const totalFullChunks = Math.floor(totalSize / chunkSize); 644 | const lastChunkSize = totalSize % chunkSize; 645 | const totalChunks = lastChunkSize > 0 ? totalFullChunks + 1 : totalFullChunks; 646 | 647 | return { 648 | totalChunks, 649 | chunkSize, 650 | lastChunkSize: lastChunkSize || chunkSize, 651 | totalSize 652 | }; 653 | } 654 | 655 | /** 656 | * Read a specific chunk from a file 657 | */ 658 | private async readFileChunk(filePath: string, chunkNumber: number, chunkSize: number): Promise { 659 | const { totalChunks, lastChunkSize } = await this.calculateChunkInfo(filePath, chunkSize); 660 | 661 | if (chunkNumber >= totalChunks) { 662 | throw new FileOperationError( 663 | FileErrorCode.INVALID_CHUNK, 664 | `Invalid chunk number ${chunkNumber}. Total chunks: ${totalChunks}`, 665 | filePath 666 | ); 667 | } 668 | 669 | const start = chunkNumber * chunkSize; 670 | const size = chunkNumber === totalChunks - 1 ? lastChunkSize : chunkSize; 671 | 672 | const fileHandle = await fs.open(filePath, 'r'); 673 | try { 674 | const buffer = Buffer.alloc(size); 675 | await fileHandle.read(buffer, 0, size, start); 676 | return buffer; 677 | } finally { 678 | await fileHandle.close(); 679 | } 680 | } 681 | 682 | private async handleReadFile(args: any) { 683 | const { 684 | path: filePath, 685 | encoding = 'utf8', 686 | maxSize, 687 | recursive = true, 688 | fileTypes, 689 | chunkNumber = 0 690 | } = args; 691 | 692 | try { 693 | const filesInfo = await this.readContent(filePath, encoding as BufferEncoding, maxSize, recursive, fileTypes); 694 | const { content, hasMore } = this.getContentChunk(filesInfo, chunkNumber * this.config.chunkSize); 695 | 696 | return this.createJsonResponse({ 697 | content, 698 | hasMore, 699 | nextChunk: hasMore ? chunkNumber + 1 : null 700 | }); 701 | } catch (error) { 702 | throw this.handleFileOperationError(error, 'read file', filePath); 703 | } 704 | } 705 | 706 | private async handleGetChunkCount(args: any) { 707 | const { 708 | path: filePath, 709 | encoding = 'utf8', 710 | maxSize, 711 | recursive = true, 712 | fileTypes 713 | } = args; 714 | 715 | try { 716 | const filesInfo = await this.readContent(filePath, encoding as BufferEncoding, maxSize, recursive, fileTypes); 717 | const totalChunks = this.getTotalChunks(filesInfo); 718 | 719 | return this.createJsonResponse({ 720 | totalChunks, 721 | chunkSize: this.config.chunkSize 722 | }); 723 | } catch (error) { 724 | throw this.handleFileOperationError(error, 'get chunk count', filePath); 725 | } 726 | } 727 | 728 | /** 729 | * Read content from files with filtering 730 | */ 731 | private async readContent( 732 | filePath: string, 733 | encoding: BufferEncoding = 'utf8', 734 | maxSize?: number, 735 | recursive: boolean = true, 736 | fileTypes?: string[] | string 737 | ): Promise { 738 | const filesInfo: FilesInfo = {}; 739 | const absolutePath = path.resolve(filePath); 740 | const cleanFileTypes = Array.isArray(fileTypes) 741 | ? fileTypes.map(ext => ext.toLowerCase().replace(/^\./, '')) 742 | : fileTypes 743 | ? [fileTypes.toLowerCase().replace(/^\./, '')] 744 | : undefined; 745 | 746 | console.error('[FileContextServer] Reading content with fileTypes:', cleanFileTypes); 747 | 748 | // Handle single file 749 | if ((await fs.stat(absolutePath)).isFile()) { 750 | if (cleanFileTypes && !cleanFileTypes.some(ext => absolutePath.toLowerCase().endsWith(`.${ext}`))) { 751 | return filesInfo; 752 | } 753 | 754 | const stat = await fs.stat(absolutePath); 755 | if (maxSize && stat.size > maxSize) { 756 | throw new FileOperationError( 757 | FileErrorCode.FILE_TOO_LARGE, 758 | `File ${absolutePath} exceeds maximum size limit of ${maxSize} bytes`, 759 | absolutePath 760 | ); 761 | } 762 | 763 | // Check cache first 764 | const cached = this.fileContentCache.get(absolutePath); 765 | let content: string; 766 | if (cached && cached.lastModified === stat.mtimeMs) { 767 | content = cached.content; 768 | } else { 769 | content = await fs.readFile(absolutePath, encoding); 770 | this.fileContentCache.set(absolutePath, { 771 | content, 772 | lastModified: stat.mtimeMs 773 | }); 774 | } 775 | 776 | const hash = createHash('md5').update(content).digest('hex'); 777 | filesInfo[absolutePath] = { 778 | path: absolutePath, 779 | content, 780 | hash, 781 | size: stat.size, 782 | lastModified: stat.mtimeMs 783 | }; 784 | 785 | return filesInfo; 786 | } 787 | 788 | // Handle directory: use POSIX join for glob 789 | const pattern = recursive ? '**/*' : '*'; 790 | const globPattern = path.posix.join(absolutePath.split(path.sep).join(path.posix.sep), pattern); 791 | 792 | const files = await this.globPromise(globPattern, { 793 | ignore: DEFAULT_IGNORE_PATTERNS, 794 | nodir: true, 795 | dot: false, 796 | cache: true, 797 | follow: false 798 | }); 799 | 800 | await Promise.all(files.map(async (file) => { 801 | if (cleanFileTypes && !cleanFileTypes.some(ext => file.toLowerCase().endsWith(`.${ext}`))) { 802 | return; 803 | } 804 | 805 | try { 806 | const stat = await fs.stat(file); 807 | if (maxSize && stat.size > maxSize) { 808 | return; 809 | } 810 | 811 | // Check cache first 812 | const cached = this.fileContentCache.get(file); 813 | let content: string; 814 | if (cached && cached.lastModified === stat.mtimeMs) { 815 | content = cached.content; 816 | } else { 817 | content = await fs.readFile(file, encoding); 818 | this.fileContentCache.set(file, { 819 | content, 820 | lastModified: stat.mtimeMs 821 | }); 822 | } 823 | 824 | const hash = createHash('md5').update(content).digest('hex'); 825 | filesInfo[file] = { 826 | path: file, 827 | content, 828 | hash, 829 | size: stat.size, 830 | lastModified: stat.mtimeMs 831 | }; 832 | } catch (error) { 833 | console.error(`Error reading ${file}:`, error); 834 | } 835 | })); 836 | 837 | return filesInfo; 838 | } 839 | 840 | /** 841 | * Get content chunk from files info 842 | */ 843 | private getContentChunk(filesInfo: FilesInfo, startChunk: number = 0): { content: string; hasMore: boolean } { 844 | const allContent: string[] = []; 845 | 846 | for (const fileInfo of Object.values(filesInfo)) { 847 | allContent.push(`File: ${fileInfo.path}\n${fileInfo.content}\n`); 848 | } 849 | 850 | const combinedContent = allContent.join(''); 851 | const chunk = combinedContent.slice(startChunk, startChunk + this.config.chunkSize); 852 | const hasMore = combinedContent.length > startChunk + this.config.chunkSize; 853 | 854 | return { content: chunk, hasMore }; 855 | } 856 | 857 | /** 858 | * Calculate total chunks for files 859 | */ 860 | private getTotalChunks(filesInfo: FilesInfo): number { 861 | let totalContentLength = 0; 862 | 863 | for (const fileInfo of Object.values(filesInfo)) { 864 | totalContentLength += `File: ${fileInfo.path}\n${fileInfo.content}\n`.length; 865 | } 866 | 867 | return Math.ceil(totalContentLength / this.config.chunkSize); 868 | } 869 | 870 | private async processFileStream( 871 | filePath: string, 872 | processChunk: (chunk: Buffer) => Promise, 873 | useCompression: boolean = false 874 | ): Promise { 875 | return new Promise((resolve, reject) => { 876 | const fileStream = this.createFileStream(filePath, useCompression); 877 | 878 | const processStream = new Transform({ 879 | transform: async function (chunk: Buffer, encoding: string, callback) { 880 | try { 881 | await processChunk(chunk); 882 | callback(); 883 | } catch (error) { 884 | callback(error as Error); 885 | } 886 | } 887 | }); 888 | 889 | pipeline(fileStream, processStream, (error) => { 890 | if (error) { 891 | reject(error); 892 | } else { 893 | resolve(); 894 | } 895 | }); 896 | }); 897 | } 898 | 899 | private async handleSearchFiles(request: any) { 900 | const { pattern: searchPattern, path: searchPath, options = {} } = request.params.arguments; 901 | const { recursive = true, includeHidden = false, fileTypes = [] } = options; 902 | 903 | try { 904 | await this.validateAccess(searchPath); 905 | 906 | const matches: SearchResult['matches'] = []; 907 | let totalMatches = 0; 908 | 909 | // Always resolve and convert to POSIX for glob 910 | const normalizedSearchPath = path.resolve(searchPath); 911 | const pattern = recursive ? '**/*' : '*'; 912 | const globPattern = path.posix.join(normalizedSearchPath.split(path.sep).join(path.posix.sep), pattern); 913 | 914 | const files = await this.globPromise(globPattern, { 915 | ignore: includeHidden ? [] : ['.*', '**/.*'], 916 | nodir: true, 917 | dot: includeHidden 918 | }); 919 | 920 | for (const file of files) { 921 | if (fileTypes.length > 0) { 922 | const ext = path.extname(file).slice(1); 923 | if (!fileTypes.includes(ext)) continue; 924 | } 925 | 926 | const fileMatches = await this.streamSearch(file, searchPattern, { 927 | useCompression: this.config.useCompression, 928 | context: 2 929 | }); 930 | 931 | matches.push(...fileMatches); 932 | totalMatches += fileMatches.length; 933 | } 934 | 935 | return this.createJsonResponse({ 936 | matches, 937 | totalMatches, 938 | metadata: { 939 | searchPath, 940 | pattern: searchPattern, 941 | timestamp: new Date().toISOString() 942 | } 943 | }); 944 | } catch (error) { 945 | throw this.handleFileOperationError(error, 'search files', searchPath); 946 | } 947 | } 948 | 949 | private async handleSetProfile(args: any) { 950 | const { profile_name } = args; 951 | console.error(`[FileContextServer] Setting profile: ${profile_name}`); 952 | try { 953 | await this.profileService.setProfile(profile_name); 954 | const response = { 955 | message: `Successfully switched to profile: ${profile_name}`, 956 | timestamp: Date.now() 957 | }; 958 | console.error('[FileContextServer] Profile set successfully:', response); 959 | return this.createJsonResponse(response); 960 | } catch (error) { 961 | console.error('[FileContextServer] Failed to set profile:', error); 962 | throw new McpError( 963 | ErrorCode.InvalidParams, 964 | `Failed to set profile: ${error instanceof Error ? error.message : 'Unknown error'}` 965 | ); 966 | } 967 | } 968 | 969 | private async handleGetProfileContext(args: any) { 970 | try { 971 | const { refresh = false } = args; 972 | const spec = await this.profileService.getActiveProfile(); 973 | const state = this.profileService.getState(); 974 | 975 | if (refresh || !state.full_files) { 976 | await this.profileService.selectFiles(); 977 | } 978 | 979 | // Read full content files 980 | const files = await Promise.all(state.full_files.map(async (path) => { 981 | try { 982 | const metadata = await this.getFileMetadata(path); 983 | const content = await this.processFile(path, metadata); 984 | const analysis = await this.codeAnalysisService.analyzeCode(content.content, path); 985 | return { 986 | ...content, 987 | analysis: { 988 | metrics: analysis.metrics, 989 | complexity: analysis.complexity_metrics.cyclomaticComplexity, 990 | maintainability: analysis.complexity_metrics.maintainabilityIndex, 991 | quality_issues: analysis.metrics.quality.longLines + analysis.metrics.quality.duplicateLines + analysis.metrics.quality.complexFunctions 992 | } 993 | }; 994 | } catch (error) { 995 | console.error(`Error processing file ${path}:`, error); 996 | return null; 997 | } 998 | })).then(results => results.filter((f): f is NonNullable => f !== null)); 999 | 1000 | // Generate outlines for selected files 1001 | const outlines = await Promise.all(state.outline_files.map(async (path) => { 1002 | try { 1003 | const metadata = await this.getFileMetadata(path); 1004 | const content = await this.processFile(path, metadata); 1005 | const analysis = await this.codeAnalysisService.analyzeCode(content.content, path); 1006 | return { 1007 | path, 1008 | outline: analysis.outline, 1009 | metadata, 1010 | analysis: { 1011 | metrics: analysis.metrics, 1012 | complexity: analysis.complexity_metrics.cyclomaticComplexity, 1013 | maintainability: analysis.complexity_metrics.maintainabilityIndex 1014 | } 1015 | }; 1016 | } catch (error) { 1017 | console.error(`Error generating outline for ${path}:`, error); 1018 | return null; 1019 | } 1020 | })).then(results => results.filter((o): o is NonNullable => o !== null)); 1021 | 1022 | const structure = await this.generateStructure(spec.profile.settings.no_media); 1023 | 1024 | // Get prompt if profile specifies it 1025 | let prompt = ''; 1026 | if (spec.profile.prompt) { 1027 | prompt = await this.templateService.getPrompt(); 1028 | } 1029 | 1030 | // Enhanced context with LLM-friendly metadata 1031 | const context = { 1032 | project_name: path.basename(process.cwd()), 1033 | project_root: process.cwd(), 1034 | timestamp: new Date(state.timestamp).toISOString(), 1035 | profile: { 1036 | name: spec.profile.name, 1037 | description: spec.profile.description || 'Default profile settings', 1038 | settings: spec.profile.settings 1039 | }, 1040 | stats: { 1041 | total_files: files.length + outlines.length, 1042 | full_content_files: files.length, 1043 | outline_files: outlines.length, 1044 | excluded_files: state.excluded_files?.length || 0, 1045 | code_metrics: { 1046 | total_lines: files.reduce((sum, f) => sum + f.analysis.metrics.lineCount.total, 0), 1047 | code_lines: files.reduce((sum, f) => sum + f.analysis.metrics.lineCount.code, 0), 1048 | comment_lines: files.reduce((sum, f) => sum + f.analysis.metrics.lineCount.comment, 0), 1049 | average_complexity: files.length > 0 ? files.reduce((sum, f) => sum + f.analysis.complexity, 0) / files.length : 0, 1050 | quality_issues: files.reduce((sum, f) => sum + f.analysis.quality_issues, 0) 1051 | } 1052 | }, 1053 | prompt, 1054 | files: files.map(f => ({ 1055 | ...f, 1056 | language: path.extname(f.path).slice(1) || 'text', 1057 | metadata: { 1058 | ...f.metadata, 1059 | relative_path: path.relative(process.cwd(), f.path), 1060 | file_type: this.getFileType(f.path), 1061 | last_modified_relative: this.getRelativeTime(new Date(f.metadata.modifiedTime)), 1062 | analysis: f.analysis 1063 | } 1064 | })), 1065 | highlights: outlines.map(o => ({ 1066 | ...o, 1067 | metadata: { 1068 | ...o.metadata, 1069 | relative_path: path.relative(process.cwd(), o.path), 1070 | file_type: this.getFileType(o.path), 1071 | last_modified_relative: this.getRelativeTime(new Date(o.metadata.modifiedTime)), 1072 | analysis: o.analysis 1073 | } 1074 | })), 1075 | folder_structure_diagram: structure, 1076 | tools: { 1077 | file_access: { 1078 | name: 'lc-get-files', 1079 | description: 'Retrieve specific file contents', 1080 | example: { path: process.cwd(), files: ['example/path/file.ts'] } 1081 | }, 1082 | search: { 1083 | name: 'search_context', 1084 | description: 'Search for patterns in files', 1085 | example: { pattern: 'searchTerm', path: process.cwd() } 1086 | }, 1087 | changes: { 1088 | name: 'lc-list-modified-files', 1089 | description: 'Track file changes since context generation', 1090 | example: { timestamp: state.timestamp } 1091 | } 1092 | } 1093 | }; 1094 | 1095 | return this.createJsonResponse(context); 1096 | } catch (error) { 1097 | throw new McpError( 1098 | ErrorCode.InternalError, 1099 | `Failed to get profile context: ${error instanceof Error ? error.message : 'Unknown error'}` 1100 | ); 1101 | } 1102 | } 1103 | 1104 | private getFileType(filePath: string): string { 1105 | const ext = path.extname(filePath).toLowerCase(); 1106 | const filename = path.basename(filePath).toLowerCase(); 1107 | 1108 | // Common file type mappings 1109 | const typeMap: Record = { 1110 | '.ts': 'TypeScript', 1111 | '.js': 'JavaScript', 1112 | '.py': 'Python', 1113 | '.json': 'JSON', 1114 | '.md': 'Markdown', 1115 | '.txt': 'Text', 1116 | '.html': 'HTML', 1117 | '.css': 'CSS', 1118 | '.scss': 'SCSS', 1119 | '.less': 'LESS', 1120 | '.xml': 'XML', 1121 | '.yaml': 'YAML', 1122 | '.yml': 'YAML', 1123 | '.sh': 'Shell', 1124 | '.bash': 'Shell', 1125 | '.zsh': 'Shell', 1126 | '.fish': 'Shell', 1127 | '.sql': 'SQL', 1128 | '.env': 'Environment', 1129 | 'dockerfile': 'Docker', 1130 | '.dockerignore': 'Docker', 1131 | '.gitignore': 'Git', 1132 | 'package.json': 'NPM', 1133 | 'tsconfig.json': 'TypeScript Config', 1134 | '.eslintrc': 'ESLint Config', 1135 | '.prettierrc': 'Prettier Config' 1136 | }; 1137 | 1138 | // Check for exact filename matches first 1139 | if (typeMap[filename]) { 1140 | return typeMap[filename]; 1141 | } 1142 | 1143 | // Then check extensions 1144 | return typeMap[ext] || 'Unknown'; 1145 | } 1146 | 1147 | private getRelativeTime(date: Date): string { 1148 | const now = new Date(); 1149 | const diffMs = now.getTime() - date.getTime(); 1150 | const diffSecs = Math.round(diffMs / 1000); 1151 | const diffMins = Math.round(diffSecs / 60); 1152 | const diffHours = Math.round(diffMins / 60); 1153 | const diffDays = Math.round(diffHours / 24); 1154 | 1155 | if (diffSecs < 60) return `${diffSecs} seconds ago`; 1156 | if (diffMins < 60) return `${diffMins} minutes ago`; 1157 | if (diffHours < 24) return `${diffHours} hours ago`; 1158 | if (diffDays < 30) return `${diffDays} days ago`; 1159 | 1160 | return date.toLocaleDateString(); 1161 | } 1162 | 1163 | private async getFilteredFiles(ignorePatterns: string[], includePatterns: string[]): Promise { 1164 | const allFiles: string[] = []; 1165 | for (const pattern of includePatterns) { 1166 | const files = await globAsync(pattern, { 1167 | ignore: ignorePatterns, 1168 | nodir: true, 1169 | dot: true 1170 | }); 1171 | allFiles.push(...files); 1172 | } 1173 | return [...new Set(allFiles)]; 1174 | } 1175 | 1176 | private async readFiles(paths: string[]): Promise { 1177 | const files: FileContent[] = []; 1178 | for (const path of paths) { 1179 | try { 1180 | const metadata = await this.getFileMetadata(path); 1181 | const content = await this.processFile(path, metadata); 1182 | files.push(content); 1183 | } catch (error) { 1184 | console.error(`Error reading file ${path}:`, error); 1185 | } 1186 | } 1187 | return files; 1188 | } 1189 | 1190 | private async generateOutlines(paths: string[]): Promise { 1191 | const outlines: FileOutline[] = []; 1192 | for (const path of paths) { 1193 | try { 1194 | const metadata = await this.getFileMetadata(path); 1195 | const content = await this.processFile(path, metadata); 1196 | const analysis = await this.codeAnalysisService.analyzeCode(content.content, path); 1197 | outlines.push({ 1198 | path, 1199 | outline: this.formatAnalysisOutline(path, analysis), 1200 | metadata 1201 | }); 1202 | } catch (error) { 1203 | console.error(`Error generating outline for ${path}:`, error); 1204 | } 1205 | } 1206 | return outlines; 1207 | } 1208 | 1209 | private formatAnalysisOutline(filePath: string, analysis: any): string { 1210 | const parts: string[] = []; 1211 | parts.push(`File: ${path.basename(filePath)}`); 1212 | 1213 | if (analysis.imports?.length) { 1214 | parts.push('\nImports:'); 1215 | parts.push(analysis.imports.map((imp: string) => ` - ${imp}`).join('\n')); 1216 | } 1217 | 1218 | if (analysis.definitions?.length) { 1219 | parts.push('\nDefinitions:'); 1220 | parts.push(analysis.definitions.map((def: string) => ` - ${def}`).join('\n')); 1221 | } 1222 | 1223 | if (analysis.complexity) { 1224 | parts.push(`\nComplexity: ${analysis.complexity}`); 1225 | } 1226 | 1227 | return parts.join('\n'); 1228 | } 1229 | 1230 | private getLanguageFromExtension(ext: string): string | null { 1231 | const extensionMap: Record = { 1232 | 'py': 'python', 1233 | 'ts': 'typescript', 1234 | 'tsx': 'typescript', 1235 | 'js': 'javascript', 1236 | 'jsx': 'javascript', 1237 | 'cs': 'csharp', 1238 | 'go': 'go', 1239 | 'sh': 'bash', 1240 | 'bash': 'bash' 1241 | }; 1242 | return extensionMap[ext] || null; 1243 | } 1244 | 1245 | private async generateStructure(noMedia: boolean = false): Promise { 1246 | const state = this.profileService.getState(); 1247 | const files = [...new Set([...state.full_files, ...state.outline_files])]; 1248 | 1249 | // Use noMedia parameter to filter out media files if needed 1250 | const filteredFiles = noMedia 1251 | ? files.filter(file => !this.isMediaFile(file)) 1252 | : files; 1253 | 1254 | return filteredFiles.map(file => { 1255 | const prefix = state.full_files.includes(file) ? '✓' : '○'; 1256 | return `${prefix} ${file}`; 1257 | }).join('\n'); 1258 | } 1259 | 1260 | private isMediaFile(filePath: string): boolean { 1261 | const mediaExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.mp4', '.avi', '.mov']; 1262 | return mediaExtensions.some(ext => filePath.toLowerCase().endsWith(ext)); 1263 | } 1264 | 1265 | private async handleGenerateOutline(args: any) { 1266 | const { path: filePath } = args; 1267 | console.error(`[FileContextServer] Generating outline for: ${filePath}`); 1268 | 1269 | try { 1270 | await this.validateAccess(filePath); 1271 | const outline = `File: ${path.basename(filePath)} 1272 | Type: ${path.extname(filePath) || 'unknown'} 1273 | Path: ${filePath}`; 1274 | 1275 | return this.createJsonResponse({ 1276 | path: filePath, 1277 | outline, 1278 | timestamp: new Date().toISOString() 1279 | }); 1280 | } catch (error) { 1281 | throw this.handleFileOperationError(error, 'generate outline', filePath); 1282 | } 1283 | } 1284 | 1285 | async run() { 1286 | console.error('[FileContextServer] Starting server'); 1287 | // Initialize services 1288 | await this.profileService.initialize(); 1289 | await this.templateService.initialize(); 1290 | console.error('[FileContextServer] Services initialized'); 1291 | 1292 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 1293 | tools: [ 1294 | 1295 | { 1296 | name: 'read_context', 1297 | description: 'Read and analyze code files with advanced filtering and chunking. The server automatically ignores common artifact directories and files:\n- Version Control: .git/\n- Python: .venv/, __pycache__/, *.pyc, etc.\n- JavaScript/Node.js: node_modules/, bower_components/, .next/, dist/, etc.\n- IDE/Editor: .idea/, .vscode/, .env, etc.\n\nFor large files or directories, use get_chunk_count first to determine total chunks, then request specific chunks using chunkNumber parameter.', 1298 | inputSchema: { 1299 | type: 'object', 1300 | properties: { 1301 | path: { 1302 | type: 'string', 1303 | description: 'Path to file or directory to read' 1304 | }, 1305 | maxSize: { 1306 | type: 'number', 1307 | description: 'Maximum file size in bytes. Files larger than this will be chunked.', 1308 | default: 1048576 1309 | }, 1310 | encoding: { 1311 | type: 'string', 1312 | description: 'File encoding (e.g., utf8, ascii, latin1)', 1313 | default: 'utf8' 1314 | }, 1315 | recursive: { 1316 | type: 'boolean', 1317 | description: 'Whether to read directories recursively (includes subdirectories)', 1318 | default: true 1319 | }, 1320 | fileTypes: { 1321 | type: ['array', 'string'], 1322 | items: { type: 'string' }, 1323 | description: 'File extension(s) to include WITHOUT dots (e.g. ["ts", "js", "py"] or just "ts"). Empty/undefined means all files.', 1324 | default: [] 1325 | }, 1326 | chunkNumber: { 1327 | type: 'number', 1328 | description: 'Which chunk to return (0-based). Use with get_chunk_count to handle large files/directories.', 1329 | default: 0 1330 | } 1331 | }, 1332 | required: ['path'] 1333 | } 1334 | }, 1335 | { 1336 | name: 'get_chunk_count', 1337 | description: 'Get the total number of chunks that will be returned for a read_context request.\nUse this tool FIRST before reading content to determine how many chunks you need to request.\nThe parameters should match what you\'ll use in read_context.', 1338 | inputSchema: { 1339 | type: 'object', 1340 | properties: { 1341 | path: { 1342 | type: 'string', 1343 | description: 'Path to file or directory' 1344 | }, 1345 | encoding: { 1346 | type: 'string', 1347 | description: 'File encoding (e.g., utf8, ascii, latin1)', 1348 | default: 'utf8' 1349 | }, 1350 | maxSize: { 1351 | type: 'number', 1352 | description: 'Maximum file size in bytes. Files larger than this will be chunked.', 1353 | default: 1048576 1354 | }, 1355 | recursive: { 1356 | type: 'boolean', 1357 | description: 'Whether to read directories recursively (includes subdirectories)', 1358 | default: true 1359 | }, 1360 | fileTypes: { 1361 | type: ['array', 'string'], 1362 | items: { type: 'string' }, 1363 | description: 'File extension(s) to include WITHOUT dots (e.g. ["ts", "js", "py"] or just "ts"). Empty/undefined means all files.', 1364 | default: [] 1365 | } 1366 | }, 1367 | required: ['path'] 1368 | } 1369 | }, 1370 | { 1371 | name: 'set_profile', 1372 | description: 'Set the active profile for context generation', 1373 | inputSchema: { 1374 | type: 'object', 1375 | properties: { 1376 | profile_name: { 1377 | type: 'string', 1378 | description: 'Name of the profile to activate' 1379 | } 1380 | }, 1381 | required: ['profile_name'] 1382 | } 1383 | }, 1384 | { 1385 | name: 'get_profile_context', 1386 | description: 'Get repository context based on current profile settings', 1387 | inputSchema: { 1388 | type: 'object', 1389 | properties: { 1390 | refresh: { 1391 | type: 'boolean', 1392 | description: 'Whether to refresh file selection before generating context', 1393 | default: false 1394 | } 1395 | } 1396 | } 1397 | }, 1398 | { 1399 | name: 'generate_outline', 1400 | description: 'Generate a code outline for a file, showing its structure (classes, functions, imports, etc). Supports TypeScript/JavaScript and Python files.', 1401 | inputSchema: { 1402 | type: 'object', 1403 | properties: { 1404 | path: { 1405 | type: 'string', 1406 | description: 'Path to the file to analyze' 1407 | } 1408 | }, 1409 | required: ['path'] 1410 | } 1411 | } 1412 | ] 1413 | })); 1414 | 1415 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 1416 | try { 1417 | if (!request.params.arguments) { 1418 | throw new McpError(ErrorCode.InvalidParams, 'Missing arguments'); 1419 | } 1420 | 1421 | switch (request.params.name) { 1422 | case 'list_context_files': 1423 | return await this.handleListFiles(request.params.arguments); 1424 | case 'read_context': 1425 | return await this.handleReadFile(request.params.arguments); 1426 | case 'search_context': 1427 | return await this.handleSearchFiles(request); 1428 | case 'get_chunk_count': 1429 | return await this.handleGetChunkCount(request.params.arguments); 1430 | case 'set_profile': 1431 | return await this.handleSetProfile(request.params.arguments); 1432 | case 'get_profile_context': 1433 | return await this.handleGetProfileContext(request.params.arguments); 1434 | case 'generate_outline': 1435 | return await this.handleGenerateOutline(request.params.arguments); 1436 | default: 1437 | throw new McpError( 1438 | ErrorCode.MethodNotFound, 1439 | `Unknown tool: ${request.params.name}` 1440 | ); 1441 | } 1442 | } catch (error) { 1443 | if (error instanceof FileOperationError) { 1444 | return { 1445 | content: [{ 1446 | type: 'text', 1447 | text: `File operation error: ${error.message} (${error.code})` 1448 | }], 1449 | isError: true 1450 | }; 1451 | } 1452 | throw error; 1453 | } 1454 | }); 1455 | 1456 | const transport = new StdioServerTransport(); 1457 | await this.server.connect(transport); 1458 | console.error('File Context MCP server running on stdio'); 1459 | } 1460 | } 1461 | 1462 | // Start the server 1463 | const server = new FileContextServer(); 1464 | server.run().catch(console.error); 1465 | 1466 | -------------------------------------------------------------------------------- /src/services/CodeAnalysisService.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | import { exec } from 'child_process'; 4 | import { promisify } from 'util'; 5 | import * as parser from '@typescript-eslint/parser'; 6 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types'; 7 | import { FileContent } from '../types.js'; 8 | 9 | const execAsync = promisify(exec); 10 | 11 | export interface SecurityIssue { 12 | type: string; 13 | severity: 'low' | 'medium' | 'high' | 'critical'; 14 | description: string; 15 | line?: number; 16 | column?: number; 17 | } 18 | 19 | export interface StyleViolation { 20 | rule: string; 21 | message: string; 22 | line: number; 23 | column: number; 24 | } 25 | 26 | export interface ComplexityMetrics { 27 | cyclomaticComplexity: number; 28 | maintainabilityIndex: number; 29 | linesOfCode: number; 30 | numberOfFunctions: number; 31 | branchCount: number; 32 | returnCount: number; 33 | maxNestingDepth: number; 34 | averageFunctionComplexity: number; 35 | functionMetrics: FunctionMetrics[]; 36 | } 37 | 38 | export interface FunctionMetrics { 39 | name: string; 40 | startLine: number; 41 | endLine: number; 42 | complexity: number; 43 | parameterCount: number; 44 | returnCount: number; 45 | localVariables: number; 46 | nestingDepth: number; 47 | } 48 | 49 | export interface CodeMetrics { 50 | complexity: number; 51 | lineCount: { 52 | total: number; 53 | code: number; 54 | comment: number; 55 | blank: number; 56 | }; 57 | quality: { 58 | longLines: number; 59 | duplicateLines: number; 60 | complexFunctions: number; 61 | }; 62 | dependencies: string[]; 63 | imports: string[]; 64 | definitions: { 65 | classes: string[]; 66 | functions: string[]; 67 | variables: string[]; 68 | }; 69 | } 70 | 71 | export interface CodeAnalysisResult { 72 | metrics: CodeMetrics; 73 | outline: string; 74 | language: string; 75 | security_issues: any[]; 76 | style_violations: any[]; 77 | complexity_metrics: any; 78 | } 79 | 80 | interface LanguageConfig { 81 | extensions: string[]; 82 | securityTool?: string; 83 | styleTool?: string; 84 | complexityTool?: string; 85 | parser?: (code: string) => TSESTree.Program; 86 | } 87 | 88 | export class CodeAnalysisService { 89 | private tempDir: string; 90 | private languageConfigs: Record; 91 | private readonly LONG_LINE_THRESHOLD = 100; 92 | private readonly COMPLEX_FUNCTION_THRESHOLD = 10; 93 | 94 | constructor() { 95 | this.tempDir = path.join(process.cwd(), '.temp'); 96 | this.languageConfigs = { 97 | python: { 98 | extensions: ['.py'], 99 | securityTool: 'bandit', 100 | styleTool: 'pylint', 101 | complexityTool: 'radon' 102 | }, 103 | typescript: { 104 | extensions: ['.ts', '.tsx'], 105 | securityTool: 'tsc --noEmit', 106 | styleTool: 'eslint', 107 | parser: (code: string) => parser.parse(code, { 108 | sourceType: 'module', 109 | ecmaFeatures: { jsx: true } 110 | }) 111 | }, 112 | javascript: { 113 | extensions: ['.js', '.jsx'], 114 | securityTool: 'eslint', 115 | styleTool: 'eslint', 116 | parser: (code: string) => parser.parse(code, { 117 | sourceType: 'module', 118 | ecmaFeatures: { jsx: true } 119 | }) 120 | }, 121 | csharp: { 122 | extensions: ['.cs'], 123 | securityTool: 'security-code-scan', 124 | styleTool: 'dotnet format', 125 | complexityTool: 'ndepend' 126 | }, 127 | go: { 128 | extensions: ['.go'], 129 | securityTool: 'gosec', 130 | styleTool: 'golint', 131 | complexityTool: 'gocyclo' 132 | }, 133 | bash: { 134 | extensions: ['.sh', '.bash'], 135 | securityTool: 'shellcheck', 136 | styleTool: 'shellcheck', 137 | complexityTool: 'shellcheck' 138 | } 139 | }; 140 | } 141 | 142 | public async initialize(): Promise { 143 | await fs.mkdir(this.tempDir, { recursive: true }); 144 | } 145 | 146 | public async analyzeCode(content: string, filePath: string): Promise { 147 | const ext = path.extname(filePath).toLowerCase(); 148 | const language = this.getLanguage(ext); 149 | 150 | const metrics = await this.calculateMetrics(content, language); 151 | const outline = await this.generateOutline(content, language); 152 | 153 | return { 154 | metrics, 155 | outline, 156 | language, 157 | security_issues: [], // TODO: Implement security analysis 158 | style_violations: [], // TODO: Implement style analysis 159 | complexity_metrics: { 160 | cyclomaticComplexity: metrics.complexity, 161 | linesOfCode: metrics.lineCount.code, 162 | maintainabilityIndex: 100 - (metrics.quality.longLines + metrics.quality.duplicateLines) / metrics.lineCount.total * 100 163 | } 164 | }; 165 | } 166 | 167 | private getLanguage(ext: string): string { 168 | const map: Record = { 169 | '.ts': 'typescript', 170 | '.tsx': 'typescript', 171 | '.js': 'javascript', 172 | '.jsx': 'javascript', 173 | '.py': 'python', 174 | '.go': 'go', 175 | '.java': 'java', 176 | '.cs': 'csharp', 177 | '.cpp': 'cpp', 178 | '.c': 'c', 179 | '.rb': 'ruby' 180 | }; 181 | return map[ext] || 'unknown'; 182 | } 183 | 184 | private async calculateMetrics(content: string, language: string): Promise { 185 | const lines = content.split('\n'); 186 | 187 | const lineCount = this.calculateLineCount(lines, language); 188 | const complexity = this.calculateComplexity(content, language); 189 | const quality = this.calculateQualityMetrics(lines); 190 | const { imports, dependencies } = this.extractDependencies(content, language); 191 | const definitions = this.extractDefinitions(content, language); 192 | 193 | return { 194 | complexity, 195 | lineCount, 196 | quality, 197 | dependencies, 198 | imports, 199 | definitions 200 | }; 201 | } 202 | 203 | private calculateLineCount(lines: string[], language: string): CodeMetrics['lineCount'] { 204 | let code = 0; 205 | let comment = 0; 206 | let blank = 0; 207 | let inMultilineComment = false; 208 | 209 | const commentStart = this.getCommentPatterns(language); 210 | 211 | for (const line of lines) { 212 | const trimmed = line.trim(); 213 | 214 | if (!trimmed) { 215 | blank++; 216 | continue; 217 | } 218 | 219 | if (inMultilineComment) { 220 | comment++; 221 | if (commentStart.multiEnd && trimmed.includes(commentStart.multiEnd)) { 222 | inMultilineComment = false; 223 | } 224 | continue; 225 | } 226 | 227 | if (commentStart.multi && trimmed.startsWith(commentStart.multi)) { 228 | comment++; 229 | inMultilineComment = true; 230 | continue; 231 | } 232 | 233 | if (commentStart.single.some(pattern => trimmed.startsWith(pattern))) { 234 | comment++; 235 | } else { 236 | code++; 237 | } 238 | } 239 | 240 | return { 241 | total: lines.length, 242 | code, 243 | comment, 244 | blank 245 | }; 246 | } 247 | 248 | private calculateComplexity(content: string, language: string): number { 249 | let complexity = 1; 250 | const patterns = [ 251 | /\bif\b/g, 252 | /\belse\b/g, 253 | /\bwhile\b/g, 254 | /\bfor\b/g, 255 | /\bforeach\b/g, 256 | /\bcase\b/g, 257 | /\bcatch\b/g, 258 | /\b\|\|\b/g, 259 | /\b&&\b/g, 260 | /\?/g 261 | ]; 262 | 263 | patterns.forEach(pattern => { 264 | const matches = content.match(pattern); 265 | if (matches) { 266 | complexity += matches.length; 267 | } 268 | }); 269 | 270 | return complexity; 271 | } 272 | 273 | private calculateQualityMetrics(lines: string[]): CodeMetrics['quality'] { 274 | const longLines = lines.filter(line => line.length > this.LONG_LINE_THRESHOLD).length; 275 | 276 | // Simple duplicate line detection 277 | const lineSet = new Set(); 278 | let duplicateLines = 0; 279 | lines.forEach(line => { 280 | const trimmed = line.trim(); 281 | if (trimmed && lineSet.has(trimmed)) { 282 | duplicateLines++; 283 | } else { 284 | lineSet.add(trimmed); 285 | } 286 | }); 287 | 288 | // Count complex functions based on line count and complexity 289 | const complexFunctions = this.countComplexFunctions(lines.join('\n')); 290 | 291 | return { 292 | longLines, 293 | duplicateLines, 294 | complexFunctions 295 | }; 296 | } 297 | 298 | private countComplexFunctions(content: string): number { 299 | const functionMatches = content.match(/\bfunction\s+\w+\s*\([^)]*\)\s*{[^}]*}/g) || []; 300 | return functionMatches.filter(func => { 301 | const complexity = this.calculateComplexity(func, 'unknown'); 302 | return complexity > this.COMPLEX_FUNCTION_THRESHOLD; 303 | }).length; 304 | } 305 | 306 | private extractDependencies(content: string, language: string): { imports: string[], dependencies: string[] } { 307 | const imports: string[] = []; 308 | const dependencies: string[] = []; 309 | 310 | switch (language) { 311 | case 'typescript': 312 | case 'javascript': 313 | const importMatches = content.match(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g) || []; 314 | const requireMatches = content.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g) || []; 315 | 316 | importMatches.forEach(match => { 317 | const [, path] = match.match(/from\s+['"]([^'"]+)['"]/) || []; 318 | if (path) imports.push(path); 319 | }); 320 | 321 | requireMatches.forEach(match => { 322 | const [, path] = match.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/) || []; 323 | if (path) dependencies.push(path); 324 | }); 325 | break; 326 | 327 | case 'python': 328 | const pythonImports = content.match(/(?:from\s+(\S+)\s+)?import\s+(\S+)(?:\s+as\s+\S+)?/g) || []; 329 | pythonImports.forEach(match => { 330 | const [, from, module] = match.match(/(?:from\s+(\S+)\s+)?import\s+(\S+)/) || []; 331 | if (from) imports.push(from); 332 | if (module) imports.push(module); 333 | }); 334 | break; 335 | } 336 | 337 | return { imports, dependencies }; 338 | } 339 | 340 | private extractDefinitions(content: string, language: string): CodeMetrics['definitions'] { 341 | const definitions: CodeMetrics['definitions'] = { 342 | classes: [], 343 | functions: [], 344 | variables: [] 345 | }; 346 | 347 | switch (language) { 348 | case 'typescript': 349 | case 'javascript': 350 | // Classes 351 | const classMatches = content.match(/class\s+(\w+)/g) || []; 352 | definitions.classes = classMatches.map(match => match.split(/\s+/)[1]); 353 | 354 | // Functions 355 | const functionMatches = content.match(/(?:function|const|let|var)\s+(\w+)\s*(?:=\s*(?:function|\([^)]*\)\s*=>)|\([^)]*\))/g) || []; 356 | definitions.functions = functionMatches.map(match => { 357 | const [, name] = match.match(/(?:function|const|let|var)\s+(\w+)/) || []; 358 | return name; 359 | }).filter(Boolean); 360 | 361 | // Variables 362 | const varMatches = content.match(/(?:const|let|var)\s+(\w+)\s*=/g) || []; 363 | definitions.variables = varMatches.map(match => { 364 | const [, name] = match.match(/(?:const|let|var)\s+(\w+)/) || []; 365 | return name; 366 | }).filter(Boolean); 367 | break; 368 | 369 | case 'python': 370 | // Classes 371 | const pyClassMatches = content.match(/class\s+(\w+)(?:\([^)]*\))?:/g) || []; 372 | definitions.classes = pyClassMatches.map(match => { 373 | const [, name] = match.match(/class\s+(\w+)/) || []; 374 | return name; 375 | }).filter(Boolean); 376 | 377 | // Functions 378 | const pyFuncMatches = content.match(/def\s+(\w+)\s*\([^)]*\):/g) || []; 379 | definitions.functions = pyFuncMatches.map(match => { 380 | const [, name] = match.match(/def\s+(\w+)/) || []; 381 | return name; 382 | }).filter(Boolean); 383 | 384 | // Variables 385 | const pyVarMatches = content.match(/(\w+)\s*=(?!=)/g) || []; 386 | definitions.variables = pyVarMatches.map(match => { 387 | const [, name] = match.match(/(\w+)\s*=/) || []; 388 | return name; 389 | }).filter(Boolean); 390 | break; 391 | } 392 | 393 | return definitions; 394 | } 395 | 396 | private getCommentPatterns(language: string): { single: string[], multi?: string, multiEnd?: string } { 397 | switch (language) { 398 | case 'typescript': 399 | case 'javascript': 400 | return { 401 | single: ['//'], 402 | multi: '/*', 403 | multiEnd: '*/' 404 | }; 405 | case 'python': 406 | return { 407 | single: ['#'] 408 | }; 409 | case 'ruby': 410 | return { 411 | single: ['#'] 412 | }; 413 | default: 414 | return { 415 | single: ['//'], 416 | multi: '/*', 417 | multiEnd: '*/' 418 | }; 419 | } 420 | } 421 | 422 | private async generateOutline(content: string, language: string): Promise { 423 | const metrics = await this.calculateMetrics(content, language); 424 | 425 | const sections: string[] = []; 426 | 427 | // Add imports section 428 | if (metrics.imports.length > 0) { 429 | sections.push('Imports:', ...metrics.imports.map(imp => ` - ${imp}`)); 430 | } 431 | 432 | // Add definitions section 433 | if (metrics.definitions.classes.length > 0) { 434 | sections.push('\nClasses:', ...metrics.definitions.classes.map(cls => ` - ${cls}`)); 435 | } 436 | 437 | if (metrics.definitions.functions.length > 0) { 438 | sections.push('\nFunctions:', ...metrics.definitions.functions.map(func => ` - ${func}`)); 439 | } 440 | 441 | // Add metrics section 442 | sections.push('\nMetrics:', 443 | ` Lines: ${metrics.lineCount.total} (${metrics.lineCount.code} code, ${metrics.lineCount.comment} comments, ${metrics.lineCount.blank} blank)`, 444 | ` Complexity: ${metrics.complexity}`, 445 | ` Quality Issues:`, 446 | ` - ${metrics.quality.longLines} long lines`, 447 | ` - ${metrics.quality.duplicateLines} duplicate lines`, 448 | ` - ${metrics.quality.complexFunctions} complex functions` 449 | ); 450 | 451 | return sections.join('\n'); 452 | } 453 | 454 | private analyzeAst(ast: TSESTree.Node): ComplexityMetrics { 455 | const functionMetrics: FunctionMetrics[] = []; 456 | let totalComplexity = 0; 457 | let maxNestingDepth = 0; 458 | let branchCount = 0; 459 | let returnCount = 0; 460 | 461 | const visitNode = (node: TSESTree.Node, depth: number = 0): void => { 462 | maxNestingDepth = Math.max(maxNestingDepth, depth); 463 | 464 | switch (node.type) { 465 | case AST_NODE_TYPES.FunctionDeclaration: 466 | case AST_NODE_TYPES.FunctionExpression: 467 | case AST_NODE_TYPES.ArrowFunctionExpression: 468 | case AST_NODE_TYPES.MethodDefinition: 469 | const metrics = this.analyzeFunctionNode(node, depth); 470 | functionMetrics.push(metrics); 471 | totalComplexity += metrics.complexity; 472 | break; 473 | 474 | case AST_NODE_TYPES.IfStatement: 475 | case AST_NODE_TYPES.SwitchCase: 476 | case AST_NODE_TYPES.ConditionalExpression: 477 | branchCount++; 478 | break; 479 | 480 | case AST_NODE_TYPES.ReturnStatement: 481 | returnCount++; 482 | break; 483 | } 484 | 485 | // Recursively visit children 486 | for (const key in node) { 487 | const child = (node as any)[key]; 488 | if (child && typeof child === 'object') { 489 | if (Array.isArray(child)) { 490 | child.forEach(item => { 491 | if (item && typeof item === 'object' && item.type) { 492 | visitNode(item as TSESTree.Node, depth + 1); 493 | } 494 | }); 495 | } else if (child.type) { 496 | visitNode(child as TSESTree.Node, depth + 1); 497 | } 498 | } 499 | } 500 | }; 501 | 502 | visitNode(ast); 503 | 504 | const averageFunctionComplexity = functionMetrics.length > 0 505 | ? totalComplexity / functionMetrics.length 506 | : 0; 507 | 508 | return { 509 | cyclomaticComplexity: totalComplexity, 510 | maintainabilityIndex: this.calculateMaintainabilityIndex(totalComplexity, ast.loc?.end.line || 0), 511 | linesOfCode: ast.loc?.end.line || 0, 512 | numberOfFunctions: functionMetrics.length, 513 | branchCount, 514 | returnCount, 515 | maxNestingDepth, 516 | averageFunctionComplexity, 517 | functionMetrics 518 | }; 519 | } 520 | 521 | private analyzeFunctionNode(node: TSESTree.Node, depth: number): FunctionMetrics { 522 | let complexity = 1; // Base complexity 523 | let returnCount = 0; 524 | let localVariables = 0; 525 | 526 | const visitFunctionNode = (node: TSESTree.Node): void => { 527 | switch (node.type) { 528 | case AST_NODE_TYPES.IfStatement: 529 | case AST_NODE_TYPES.SwitchCase: 530 | case AST_NODE_TYPES.ConditionalExpression: 531 | case AST_NODE_TYPES.LogicalExpression: 532 | complexity++; 533 | break; 534 | 535 | case AST_NODE_TYPES.ReturnStatement: 536 | returnCount++; 537 | break; 538 | 539 | case AST_NODE_TYPES.VariableDeclaration: 540 | localVariables += node.declarations.length; 541 | break; 542 | } 543 | 544 | // Recursively visit children 545 | for (const key in node) { 546 | const child = (node as any)[key]; 547 | if (child && typeof child === 'object') { 548 | if (Array.isArray(child)) { 549 | child.forEach(item => { 550 | if (item && typeof item === 'object' && item.type) { 551 | visitFunctionNode(item as TSESTree.Node); 552 | } 553 | }); 554 | } else if (child.type) { 555 | visitFunctionNode(child as TSESTree.Node); 556 | } 557 | } 558 | } 559 | }; 560 | 561 | visitFunctionNode(node); 562 | 563 | return { 564 | name: this.getFunctionName(node), 565 | startLine: node.loc?.start.line || 0, 566 | endLine: node.loc?.end.line || 0, 567 | complexity, 568 | parameterCount: this.getParameterCount(node), 569 | returnCount, 570 | localVariables, 571 | nestingDepth: depth 572 | }; 573 | } 574 | 575 | private getFunctionName(node: TSESTree.Node): string { 576 | switch (node.type) { 577 | case AST_NODE_TYPES.FunctionDeclaration: 578 | return node.id?.name || 'anonymous'; 579 | case AST_NODE_TYPES.MethodDefinition: 580 | return node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : 'computed'; 581 | default: 582 | return 'anonymous'; 583 | } 584 | } 585 | 586 | private getParameterCount(node: TSESTree.Node): number { 587 | switch (node.type) { 588 | case AST_NODE_TYPES.FunctionDeclaration: 589 | case AST_NODE_TYPES.FunctionExpression: 590 | case AST_NODE_TYPES.ArrowFunctionExpression: 591 | return node.params.length; 592 | case AST_NODE_TYPES.MethodDefinition: 593 | return node.value.params.length; 594 | default: 595 | return 0; 596 | } 597 | } 598 | 599 | private calculateMaintainabilityIndex(complexity: number, linesOfCode: number): number { 600 | // Maintainability Index formula: 601 | // 171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code) 602 | // We're using a simplified version since we don't calculate Halstead Volume 603 | const mi = 171 - (0.23 * complexity) - (16.2 * Math.log(linesOfCode)); 604 | return Math.max(0, Math.min(100, mi)); 605 | } 606 | 607 | private async runSecurityAnalysis(filePath: string, config: LanguageConfig): Promise { 608 | if (!config.securityTool) { 609 | return []; 610 | } 611 | 612 | try { 613 | const { stdout } = await execAsync(`${config.securityTool} ${filePath}`); 614 | return this.parseSecurityOutput(stdout, config.securityTool); 615 | } catch (error) { 616 | console.error('Security analysis failed:', error); 617 | return []; 618 | } 619 | } 620 | 621 | private async runStyleAnalysis(filePath: string, config: LanguageConfig): Promise { 622 | if (!config.styleTool) { 623 | return []; 624 | } 625 | 626 | try { 627 | const { stdout } = await execAsync(`${config.styleTool} ${filePath}`); 628 | return this.parseStyleOutput(stdout, config.styleTool); 629 | } catch (error) { 630 | console.error('Style analysis failed:', error); 631 | return []; 632 | } 633 | } 634 | 635 | private getDefaultComplexityMetrics(code: string): ComplexityMetrics { 636 | const lines = code.split('\n'); 637 | const functionMatches = code.match(/function|def|func|method/g); 638 | const branchMatches = code.match(/if|else|switch|case|while|for|catch/g); 639 | const returnMatches = code.match(/return/g); 640 | 641 | return { 642 | cyclomaticComplexity: (branchMatches?.length || 0) + 1, 643 | maintainabilityIndex: 100, 644 | linesOfCode: lines.length, 645 | numberOfFunctions: functionMatches?.length || 0, 646 | branchCount: branchMatches?.length || 0, 647 | returnCount: returnMatches?.length || 0, 648 | maxNestingDepth: 0, 649 | averageFunctionComplexity: 1, 650 | functionMetrics: [] 651 | }; 652 | } 653 | 654 | private parseSecurityOutput(output: string, tool: string): SecurityIssue[] { 655 | switch (tool) { 656 | case 'bandit': 657 | return this.parseBanditOutput(output); 658 | case 'eslint': 659 | return this.parseEslintOutput(output); 660 | default: 661 | return []; 662 | } 663 | } 664 | 665 | private parseStyleOutput(output: string, tool: string): StyleViolation[] { 666 | switch (tool) { 667 | case 'pylint': 668 | return this.parsePylintOutput(output); 669 | case 'eslint': 670 | return this.parseEslintOutput(output).map(issue => ({ 671 | rule: issue.type, 672 | message: issue.description, 673 | line: issue.line || 0, 674 | column: issue.column || 0 675 | })); 676 | default: 677 | return []; 678 | } 679 | } 680 | 681 | private parseComplexityOutput(output: string, tool: string): ComplexityMetrics { 682 | switch (tool) { 683 | case 'radon': 684 | try { 685 | const results = JSON.parse(output); 686 | const totalComplexity = Object.values(results).reduce((sum: number, file: any) => { 687 | return sum + file.complexity; 688 | }, 0); 689 | 690 | return { 691 | cyclomaticComplexity: totalComplexity, 692 | maintainabilityIndex: 100 - (totalComplexity * 5), 693 | linesOfCode: 0, 694 | numberOfFunctions: Object.keys(results).length, 695 | branchCount: 0, 696 | returnCount: 0, 697 | maxNestingDepth: 0, 698 | averageFunctionComplexity: totalComplexity / Object.keys(results).length, 699 | functionMetrics: [] 700 | }; 701 | } catch { 702 | return this.getDefaultComplexityMetrics(''); 703 | } 704 | case 'gocyclo': 705 | try { 706 | const lines = output.split('\n').filter(Boolean); 707 | const metrics = lines.map(line => { 708 | const [complexity, path, name] = line.split(' '); 709 | return { 710 | name, 711 | complexity: parseInt(complexity, 10), 712 | startLine: 0, 713 | endLine: 0, 714 | parameterCount: 0, 715 | returnCount: 0, 716 | localVariables: 0, 717 | nestingDepth: 0 718 | }; 719 | }); 720 | 721 | const totalComplexity = metrics.reduce((sum, m) => sum + m.complexity, 0); 722 | return { 723 | cyclomaticComplexity: totalComplexity, 724 | maintainabilityIndex: this.calculateMaintainabilityIndex(totalComplexity, 0), 725 | linesOfCode: 0, 726 | numberOfFunctions: metrics.length, 727 | branchCount: 0, 728 | returnCount: 0, 729 | maxNestingDepth: 0, 730 | averageFunctionComplexity: totalComplexity / metrics.length, 731 | functionMetrics: metrics 732 | }; 733 | } catch { 734 | return this.getDefaultComplexityMetrics(''); 735 | } 736 | default: 737 | return this.getDefaultComplexityMetrics(''); 738 | } 739 | } 740 | 741 | private parseBanditOutput(output: string): SecurityIssue[] { 742 | try { 743 | const results = JSON.parse(output); 744 | return results.results.map((result: any) => ({ 745 | type: result.test_id, 746 | severity: result.issue_severity, 747 | description: result.issue_text, 748 | line: result.line_number 749 | })); 750 | } catch { 751 | return []; 752 | } 753 | } 754 | 755 | private parsePylintOutput(output: string): StyleViolation[] { 756 | try { 757 | const results = JSON.parse(output); 758 | return results.map((result: any) => ({ 759 | rule: result.symbol, 760 | message: result.message, 761 | line: result.line, 762 | column: result.column 763 | })); 764 | } catch { 765 | return []; 766 | } 767 | } 768 | 769 | private parseEslintOutput(output: string): SecurityIssue[] { 770 | try { 771 | const results = JSON.parse(output); 772 | return results.map((result: { 773 | ruleId: string; 774 | severity: number; 775 | message: string; 776 | line: number; 777 | column: number; 778 | }) => ({ 779 | type: result.ruleId, 780 | severity: result.severity === 2 ? 'high' : result.severity === 1 ? 'medium' : 'low', 781 | description: result.message, 782 | line: result.line, 783 | column: result.column 784 | })); 785 | } catch { 786 | return []; 787 | } 788 | } 789 | } 790 | -------------------------------------------------------------------------------- /src/services/FileWatcherService.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { watch } from 'chokidar'; 3 | import * as path from 'path'; 4 | 5 | /** 6 | * Service for monitoring file system changes 7 | */ 8 | export class FileWatcherService extends EventEmitter { 9 | private watchers: Map = new Map(); 10 | 11 | constructor() { 12 | super(); 13 | } 14 | 15 | /** 16 | * Start watching a path for changes 17 | */ 18 | public async watch(targetPath: string): Promise { 19 | if (this.watchers.has(targetPath)) { 20 | return; 21 | } 22 | 23 | if (!this.watchers.has(targetPath)) { 24 | this.watchers.set(targetPath, watch(targetPath, { 25 | persistent: true, 26 | ignoreInitial: true, 27 | usePolling: true, 28 | interval: 100, 29 | binaryInterval: 300, 30 | awaitWriteFinish: { 31 | stabilityThreshold: 2000, 32 | pollInterval: 100 33 | } 34 | })); 35 | 36 | this.setupWatcherEvents(targetPath); 37 | } else { 38 | this.watchers.get(targetPath).add(targetPath); 39 | } 40 | 41 | console.error(`Started watching: ${targetPath}`); 42 | } 43 | 44 | /** 45 | * Stop watching a path 46 | */ 47 | public async unwatch(targetPath: string): Promise { 48 | if (this.watchers.has(targetPath)) { 49 | await this.watchers.get(targetPath).unwatch(targetPath); 50 | this.watchers.delete(targetPath); 51 | console.error(`Stopped watching: ${targetPath}`); 52 | } 53 | } 54 | 55 | /** 56 | * Close all watchers 57 | */ 58 | public async close(): Promise { 59 | if (this.watchers.size > 0) { 60 | for (const watcher of this.watchers.values()) { 61 | await watcher.close(); 62 | } 63 | this.watchers.clear(); 64 | console.error('File watcher closed'); 65 | } 66 | } 67 | 68 | /** 69 | * Get list of watched paths 70 | */ 71 | public getWatchedPaths(): string[] { 72 | return Array.from(this.watchers.keys()); 73 | } 74 | 75 | /** 76 | * Setup watcher event handlers 77 | */ 78 | private setupWatcherEvents(targetPath: string): void { 79 | if (!this.watchers.has(targetPath)) return; 80 | 81 | const watcher = this.watchers.get(targetPath); 82 | 83 | // File added 84 | watcher.on('add', (filePath: string) => { 85 | console.error(`File ${filePath} has been added`); 86 | this.emit('fileAdded', filePath); 87 | }); 88 | 89 | // File changed 90 | watcher.on('change', (filePath: string) => { 91 | console.error(`File ${filePath} has been changed`); 92 | this.emit('fileChanged', filePath); 93 | }); 94 | 95 | // File deleted 96 | watcher.on('unlink', (filePath: string) => { 97 | console.error(`File ${filePath} has been removed`); 98 | this.emit('fileDeleted', filePath); 99 | }); 100 | 101 | // Directory added 102 | watcher.on('addDir', (dirPath: string) => { 103 | console.error(`Directory ${dirPath} has been added`); 104 | this.emit('directoryAdded', dirPath); 105 | }); 106 | 107 | // Directory deleted 108 | watcher.on('unlinkDir', (dirPath: string) => { 109 | console.error(`Directory ${dirPath} has been removed`); 110 | this.emit('directoryDeleted', dirPath); 111 | }); 112 | 113 | // Error handling 114 | watcher.on('error', (error: Error) => { 115 | console.error(`Watcher error: ${error}`); 116 | this.emit('error', error); 117 | }); 118 | 119 | // Ready event 120 | watcher.on('ready', () => { 121 | console.error('Initial scan complete. Ready for changes'); 122 | this.emit('ready'); 123 | }); 124 | } 125 | 126 | /** 127 | * Check if a path is being watched 128 | */ 129 | public isWatching(targetPath: string): boolean { 130 | return this.watchers.has(targetPath); 131 | } 132 | 133 | /** 134 | * Get watcher status 135 | */ 136 | public getStatus(): { isWatching: boolean; watchedPaths: string[]; ready: boolean } { 137 | return { 138 | isWatching: this.watchers.size > 0, 139 | watchedPaths: Array.from(this.watchers.keys()), 140 | ready: this.watchers.size > 0 && this.watchers.get(Array.from(this.watchers.keys())[0])?.getWatched !== undefined 141 | }; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/services/ProfileService.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | import { Profile, ProfileConfig, ProfileState, ContextSpec } from '../types.js'; 4 | import { glob } from 'glob'; 5 | import { promisify } from 'util'; 6 | 7 | const globAsync = promisify(glob); 8 | 9 | const DEFAULT_IGNORE_PATTERNS = [ 10 | '.git/', 11 | 'node_modules/', 12 | 'dist/', 13 | 'build/', 14 | '.env', 15 | '.env.*', 16 | '*.min.*', 17 | '*.bundle.*', 18 | ]; 19 | 20 | const INCLUDE_ALL = ['**/*']; 21 | 22 | export class ProfileService { 23 | private config: ProfileConfig; 24 | private state: ProfileState; 25 | private projectRoot: string; 26 | private activeProfile: Profile | null; 27 | private readonly configPath: string; 28 | 29 | constructor(projectRoot: string) { 30 | console.error('[ProfileService] Initializing with root:', projectRoot); 31 | this.projectRoot = projectRoot; 32 | this.configPath = path.join(projectRoot, '.llm-context', 'config.toml'); 33 | this.config = this.createDefaultConfig(); 34 | this.state = { 35 | profile_name: 'code', 36 | full_files: [], 37 | outline_files: [], 38 | excluded_files: [], 39 | timestamp: Date.now() 40 | }; 41 | this.activeProfile = null; 42 | } 43 | 44 | private createDefaultConfig(): ProfileConfig { 45 | console.error('[ProfileService] Creating default config'); 46 | const defaultProfile = this.createDefaultProfile(); 47 | return { 48 | profiles: { 49 | code: defaultProfile, 50 | 'code-prompt': { 51 | ...defaultProfile, 52 | name: 'code-prompt', 53 | prompt: 'prompt.md' 54 | } 55 | }, 56 | default_profile: 'code' 57 | }; 58 | } 59 | 60 | private createDefaultProfile(): Profile { 61 | return { 62 | name: 'code', 63 | gitignores: { 64 | full_files: DEFAULT_IGNORE_PATTERNS, 65 | outline_files: DEFAULT_IGNORE_PATTERNS 66 | }, 67 | only_includes: { 68 | full_files: INCLUDE_ALL, 69 | outline_files: INCLUDE_ALL 70 | }, 71 | settings: { 72 | no_media: true, 73 | with_user_notes: false 74 | } 75 | }; 76 | } 77 | 78 | public async initialize(): Promise { 79 | console.error('[ProfileService] Starting initialization'); 80 | await this.loadConfig(); 81 | await this.loadState(); 82 | } 83 | 84 | private async loadConfig(): Promise { 85 | const configPath = path.join(this.projectRoot, '.llm-context'); 86 | try { 87 | await fs.mkdir(configPath, { recursive: true }); 88 | console.error('[ProfileService] Created config directory:', configPath); 89 | 90 | // Create default config if it doesn't exist 91 | const configFile = path.join(configPath, 'config.json'); 92 | if (!await this.fileExists(configFile)) { 93 | console.error('[ProfileService] Creating default config file'); 94 | const defaultConfig = this.createDefaultConfig(); 95 | await fs.writeFile(configFile, JSON.stringify(defaultConfig, null, 2)); 96 | this.config = defaultConfig; 97 | } else { 98 | console.error('[ProfileService] Loading existing config file'); 99 | const content = await fs.readFile(configFile, 'utf8'); 100 | this.config = JSON.parse(content); 101 | } 102 | 103 | // Log available profiles 104 | console.error('[ProfileService] Available profiles:', Object.keys(this.config.profiles)); 105 | console.error('[ProfileService] Current profile:', this.state.profile_name); 106 | } catch (error) { 107 | console.error('[ProfileService] Failed to initialize:', error); 108 | throw error; 109 | } 110 | } 111 | 112 | private async loadState(): Promise { 113 | const statePath = path.join(this.projectRoot, '.llm-context', 'state.json'); 114 | if (!await this.fileExists(statePath)) { 115 | console.error('[ProfileService] Creating default state file'); 116 | await fs.writeFile(statePath, JSON.stringify(this.state, null, 2)); 117 | } else { 118 | console.error('[ProfileService] Loading existing state file'); 119 | const content = await fs.readFile(statePath, 'utf8'); 120 | this.state = JSON.parse(content); 121 | } 122 | } 123 | 124 | private async fileExists(filePath: string): Promise { 125 | try { 126 | await fs.access(filePath); 127 | return true; 128 | } catch { 129 | return false; 130 | } 131 | } 132 | 133 | public async setProfile(profileName: string): Promise { 134 | console.error(`[ProfileService] Attempting to set profile: ${profileName}`); 135 | console.error('[ProfileService] Available profiles:', Object.keys(this.config.profiles)); 136 | 137 | if (!this.config.profiles[profileName]) { 138 | throw new Error(`Profile '${profileName}' does not exist. Available profiles: ${Object.keys(this.config.profiles).join(', ')}`); 139 | } 140 | 141 | this.state = { 142 | ...this.state, 143 | profile_name: profileName, 144 | timestamp: Date.now() 145 | }; 146 | 147 | await this.saveState(); 148 | console.error(`[ProfileService] Successfully set profile to: ${profileName}`); 149 | } 150 | 151 | public getContextSpec(): ContextSpec { 152 | const profile = this.resolveProfile(this.state.profile_name); 153 | return { 154 | profile, 155 | state: this.state 156 | }; 157 | } 158 | 159 | private resolveProfile(profileName: string): Profile { 160 | const profile = this.config.profiles[profileName]; 161 | if (!profile) { 162 | console.error(`[ProfileService] Profile ${profileName} not found, using default`); 163 | return this.config.profiles[this.config.default_profile]; 164 | } 165 | return profile; 166 | } 167 | 168 | private async saveState(): Promise { 169 | const statePath = path.join(this.projectRoot, '.llm-context', 'state.json'); 170 | await fs.writeFile(statePath, JSON.stringify(this.state, null, 2)); 171 | console.error('[ProfileService] Saved state:', this.state); 172 | } 173 | 174 | public async updateFileSelection(fullFiles: string[], outlineFiles: string[]): Promise { 175 | this.state = { 176 | ...this.state, 177 | full_files: fullFiles, 178 | outline_files: outlineFiles, 179 | timestamp: Date.now() 180 | }; 181 | 182 | await this.saveState(); 183 | } 184 | 185 | public getProfile(): Profile { 186 | return this.resolveProfile(this.state.profile_name); 187 | } 188 | 189 | public getState(): ProfileState { 190 | return this.state; 191 | } 192 | 193 | public async getActiveProfile(): Promise<{ profile: Profile }> { 194 | if (!this.activeProfile) { 195 | throw new Error('No active profile'); 196 | } 197 | return { profile: this.activeProfile }; 198 | } 199 | 200 | public async selectFiles(): Promise { 201 | if (!this.activeProfile) { 202 | throw new Error('No active profile'); 203 | } 204 | 205 | const fullFiles = await this.getFilteredFiles( 206 | this.activeProfile.gitignores.full_files, 207 | this.activeProfile.only_includes.full_files 208 | ); 209 | 210 | const outlineFiles = await this.getFilteredFiles( 211 | this.activeProfile.gitignores.outline_files, 212 | this.activeProfile.only_includes.outline_files 213 | ); 214 | 215 | this.state = { 216 | ...this.state, 217 | full_files: fullFiles, 218 | outline_files: outlineFiles, 219 | timestamp: Date.now() 220 | }; 221 | 222 | await this.saveState(); 223 | } 224 | 225 | private async getFilteredFiles(ignorePatterns: string[], includePatterns: string[]): Promise { 226 | const allFiles: string[] = []; 227 | for (const pattern of includePatterns) { 228 | const files = await globAsync(pattern, { 229 | ignore: ignorePatterns, 230 | nodir: true, 231 | dot: true 232 | }) as string[]; 233 | allFiles.push(...files); 234 | } 235 | return [...new Set(allFiles)]; 236 | } 237 | } -------------------------------------------------------------------------------- /src/services/TemplateService.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | import Handlebars from 'handlebars'; 4 | 5 | type CompiledTemplate = ReturnType; 6 | 7 | interface TemplateMap { 8 | context: string; 9 | files: string; 10 | highlights: string; 11 | prompt: string; 12 | } 13 | 14 | const DEFAULT_TEMPLATES: TemplateMap = { 15 | context: `{{#if prompt}} 16 | {{{prompt}}} 17 | {{/if}} 18 | {{#if project_notes}} 19 | {{{project_notes}}} 20 | {{/if}} 21 | {{#if user_notes}} 22 | {{{user_notes}}} 23 | {{/if}} 24 | # Repository Content: **{{project_name}}** 25 | 26 | > 🕒 Generation timestamp: {{timestamp}} 27 | > 📝 Use \`lc-list-modified-files\` to track changes since generation 28 | 29 | {{#if sample_requested_files}} 30 | ## 📂 File Access Guide 31 | 32 | Files in the repository structure are marked as: 33 | - ✓ Full content available 34 | - ○ Outline available 35 | - ✗ Excluded/not loaded 36 | 37 | To retrieve missing files, use the \`lc-get-files\` tool: 38 | \`\`\`json 39 | { 40 | "path": "{{project_root}}", 41 | "files": ["path/to/file"] 42 | } 43 | \`\`\` 44 | {{/if}} 45 | 46 | ## 📁 Repository Structure 47 | \`\`\` 48 | {{{folder_structure_diagram}}} 49 | \`\`\` 50 | 51 | {{#if files}} 52 | ## 📄 Current Files 53 | {{> files}} 54 | {{/if}} 55 | 56 | {{#if highlights}} 57 | ## 🔍 Code Outlines 58 | {{> highlights}} 59 | {{/if}} 60 | 61 | ## 🔄 Next Steps 62 | 1. Use \`lc-list-modified-files\` to check for changes 63 | 2. Request specific files with \`lc-get-files\` 64 | 3. Search code with \`search_context\``, 65 | 66 | files: `{{#each files}} 67 | ### 📄 {{path}} 68 | {{#if metadata.analysis}} 69 | > 📊 Complexity: {{metadata.analysis.complexity}} | 🔗 Dependencies: {{metadata.analysis.imports.length}} 70 | {{/if}} 71 | 72 | \`\`\`{{language}} 73 | {{{content}}} 74 | \`\`\` 75 | 76 | {{/each}}`, 77 | 78 | highlights: `{{#each highlights}} 79 | ### 🔍 {{path}} 80 | {{#if metadata.analysis}} 81 | > 📊 Complexity: {{metadata.analysis.complexity}} 82 | {{/if}} 83 | 84 | \`\`\` 85 | {{{outline}}} 86 | \`\`\` 87 | 88 | {{/each}}`, 89 | 90 | prompt: `# LLM Analysis Guide 91 | 92 | ## 🎯 Role 93 | Expert code analyst and developer focusing on understanding and improving the codebase. 94 | 95 | ## 📋 Guidelines 96 | 1. 🔍 Analyze context before suggesting changes 97 | 2. 🔗 Consider dependencies and side effects 98 | 3. 📝 Follow project's code style 99 | 4. ⚠️ Preserve existing functionality 100 | 5. 📚 Document significant changes 101 | 6. 🛡️ Handle errors gracefully 102 | 103 | ## 💡 Response Structure 104 | 1. Acknowledge files/code being analyzed 105 | 2. Explain current implementation 106 | 3. Present suggestions clearly 107 | 4. Highlight potential impacts 108 | 5. Provide rationale for decisions 109 | 110 | ## 🎨 Code Style 111 | - Match existing conventions 112 | - Use consistent formatting 113 | - Choose clear names 114 | - Add helpful comments 115 | 116 | ## 🔒 Security 117 | - Protect sensitive data 118 | - Validate inputs 119 | - Handle errors securely 120 | - Follow best practices 121 | 122 | ## ⚡ Performance 123 | - Consider efficiency 124 | - Note performance impacts 125 | - Suggest optimizations 126 | 127 | Remember to balance ideal solutions with practical constraints.` 128 | }; 129 | 130 | export class TemplateService { 131 | private templates: Map; 132 | private projectRoot: string; 133 | private templatesDir: string; 134 | 135 | constructor(projectRoot: string) { 136 | this.projectRoot = projectRoot; 137 | this.templatesDir = path.join(projectRoot, '.llm-context', 'templates'); 138 | this.templates = new Map(); 139 | } 140 | 141 | public async initialize(): Promise { 142 | // Create templates directory if it doesn't exist 143 | await fs.mkdir(this.templatesDir, { recursive: true }); 144 | 145 | // Initialize default templates if they don't exist 146 | for (const [name, content] of Object.entries(DEFAULT_TEMPLATES)) { 147 | const templatePath = path.join(this.templatesDir, `${name}.hbs`); 148 | if (!await this.fileExists(templatePath)) { 149 | await fs.writeFile(templatePath, content); 150 | } 151 | } 152 | 153 | // Load all templates 154 | await this.loadTemplates(); 155 | } 156 | 157 | private async fileExists(filePath: string): Promise { 158 | try { 159 | await fs.access(filePath); 160 | return true; 161 | } catch { 162 | return false; 163 | } 164 | } 165 | 166 | private async loadTemplates(): Promise { 167 | // Register partials first 168 | const filesContent = await this.readTemplate('files'); 169 | const highlightsContent = await this.readTemplate('highlights'); 170 | Handlebars.registerPartial('files', filesContent); 171 | Handlebars.registerPartial('highlights', highlightsContent); 172 | 173 | // Compile and cache templates 174 | for (const name of Object.keys(DEFAULT_TEMPLATES)) { 175 | const content = await this.readTemplate(name as keyof TemplateMap); 176 | this.templates.set(name, Handlebars.compile(content)); 177 | } 178 | } 179 | 180 | private async readTemplate(name: keyof TemplateMap): Promise { 181 | const templatePath = path.join(this.templatesDir, `${name}.hbs`); 182 | try { 183 | return await fs.readFile(templatePath, 'utf8'); 184 | } catch (error) { 185 | console.error(`Error reading template ${name}:`, error); 186 | return DEFAULT_TEMPLATES[name]; 187 | } 188 | } 189 | 190 | public async render(templateName: string, context: any): Promise { 191 | const template = this.templates.get(templateName); 192 | if (!template) { 193 | throw new Error(`Template '${templateName}' not found`); 194 | } 195 | 196 | try { 197 | return template(context); 198 | } catch (error) { 199 | console.error(`Error rendering template ${templateName}:`, error); 200 | throw error; 201 | } 202 | } 203 | 204 | public async getPrompt(): Promise { 205 | return this.render('prompt', {}); 206 | } 207 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface FileMetadata { 2 | size: number; 3 | mimeType: string; 4 | modifiedTime: string; 5 | createdTime: string; 6 | isDirectory: boolean; 7 | analysis?: CodeAnalysis; 8 | lastAnalyzed?: string; 9 | } 10 | 11 | export interface FileContent { 12 | content: string; 13 | metadata: FileMetadata; 14 | encoding: string; 15 | truncated: boolean; 16 | totalLines: number; 17 | path: string; 18 | } 19 | 20 | export interface CodeAnalysis { 21 | definitions?: string[]; 22 | imports?: string[]; 23 | complexity?: number; 24 | } 25 | 26 | export interface CacheEntry { 27 | content: FileContent; 28 | lastModified: number; 29 | lastAccessed: Date; 30 | } 31 | 32 | export interface EnhancedCacheEntry extends CacheEntry { 33 | size: number; 34 | hits: number; 35 | } 36 | 37 | export interface FileEntry { 38 | path: string; 39 | name: string; 40 | metadata: FileMetadata; 41 | } 42 | 43 | export enum FileErrorCode { 44 | INVALID_PATH = 'INVALID_PATH', 45 | FILE_NOT_FOUND = 'FILE_NOT_FOUND', 46 | FILE_TOO_LARGE = 'FILE_TOO_LARGE', 47 | UNKNOWN_ERROR = 'UNKNOWN_ERROR', 48 | INVALID_CHUNK = 'INVALID_CHUNK' 49 | } 50 | 51 | export class FileOperationError extends Error { 52 | constructor( 53 | public code: FileErrorCode, 54 | message: string, 55 | public path: string 56 | ) { 57 | super(message); 58 | this.name = 'FileOperationError'; 59 | } 60 | } 61 | 62 | export interface SearchOptions { 63 | recursive?: boolean; 64 | includeHidden?: boolean; 65 | fileTypes?: string[]; 66 | } 67 | 68 | export interface SearchResult { 69 | matches: Array<{ 70 | path: string; 71 | line: number; 72 | content: string; 73 | context: { 74 | before: string[]; 75 | after: string[]; 76 | }; 77 | }>; 78 | } 79 | 80 | export interface DirectoryContent { 81 | files: Record; 82 | totalSize: number; 83 | totalFiles: number; 84 | } 85 | 86 | export interface EnhancedSearchOptions extends SearchOptions { 87 | maxResults?: number; 88 | contextLines?: number; 89 | ignoreCase?: boolean; 90 | } 91 | 92 | export interface ChunkMetadata { 93 | id: string; 94 | path: string; 95 | startOffset: number; 96 | endOffset: number; 97 | type: 'code' | 'text' | 'markdown'; 98 | relevanceScore: number; 99 | semanticContext?: string; 100 | } 101 | 102 | export interface CompressedChunk { 103 | id: string; 104 | compressedData: Buffer; 105 | originalSize: number; 106 | compressionRatio: number; 107 | } 108 | 109 | export interface ContentChunk { 110 | metadata: ChunkMetadata; 111 | content: string | CompressedChunk; 112 | lastAccessed: number; 113 | accessCount: number; 114 | } 115 | 116 | export interface ChunkingStrategy { 117 | maxChunkSize: number; 118 | minChunkSize: number; 119 | preferredBoundaries: RegExp[]; 120 | compressionThreshold: number; 121 | } 122 | 123 | export interface MemoryPressureEvent { 124 | timestamp: number; 125 | currentUsage: number; 126 | threshold: number; 127 | availableMemory: number; 128 | } 129 | 130 | export interface CacheStats { 131 | totalSize: number; 132 | chunkCount: number; 133 | compressionRatio: number; 134 | hitRate: number; 135 | evictionCount: number; 136 | } 137 | 138 | export interface Profile { 139 | name: string; 140 | description?: string; 141 | settings: { 142 | no_media: boolean; 143 | with_user_notes: boolean; 144 | context_file?: string; 145 | }; 146 | gitignores: { 147 | full_files: string[]; 148 | outline_files: string[]; 149 | }; 150 | only_includes: { 151 | full_files: string[]; 152 | outline_files: string[]; 153 | }; 154 | prompt?: string; 155 | } 156 | 157 | export interface ProfileState { 158 | profile_name: string; 159 | full_files: string[]; 160 | outline_files: string[]; 161 | excluded_files: string[]; 162 | timestamp: number; 163 | } 164 | 165 | export interface ProfileConfig { 166 | profiles: Record; 167 | default_profile: string; 168 | } 169 | 170 | export interface ContextSpec { 171 | profile: Profile; 172 | state: ProfileState; 173 | } 174 | 175 | export interface FileOutline { 176 | path: string; 177 | outline: string; 178 | metadata: FileMetadata; 179 | } 180 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface FileMetadata { 2 | size: number; 3 | mimeType: string; 4 | modifiedTime: string; 5 | createdTime: string; 6 | isDirectory: boolean; 7 | } 8 | 9 | export interface FileContent { 10 | content: string; 11 | metadata: FileMetadata; 12 | encoding: string; 13 | truncated: boolean; 14 | totalLines: number; 15 | } 16 | 17 | export interface DirectoryContent { 18 | files: { [path: string]: FileContent }; 19 | metadata: { 20 | totalFiles: number; 21 | totalSize: number; 22 | truncated: boolean; 23 | searchPath: string; 24 | fileTypes?: string[]; 25 | timestamp: string; 26 | }; 27 | } 28 | 29 | export interface FileEntry { 30 | path: string; 31 | name: string; 32 | metadata: FileMetadata; 33 | } 34 | 35 | export interface SearchResult { 36 | matches: Array<{ 37 | path: string; 38 | line: number; 39 | content: string; 40 | context: { 41 | before: string[]; 42 | after: string[]; 43 | }; 44 | }>; 45 | totalMatches: number; 46 | } 47 | 48 | export interface SearchOptions { 49 | recursive?: boolean; 50 | includeHidden?: boolean; 51 | contextLines?: number; 52 | fileTypes?: string[]; 53 | excludePatterns?: string[]; 54 | } 55 | 56 | export enum FileErrorCode { 57 | FILE_NOT_FOUND = 'FILE_NOT_FOUND', 58 | INVALID_PATH = 'INVALID_PATH', 59 | FILE_TOO_LARGE = 'FILE_TOO_LARGE', 60 | PERMISSION_DENIED = 'PERMISSION_DENIED', 61 | UNKNOWN_ERROR = 'UNKNOWN_ERROR' 62 | } 63 | 64 | export class FileOperationError extends Error { 65 | constructor( 66 | public code: FileErrorCode, 67 | message: string, 68 | public path: string 69 | ) { 70 | super(message); 71 | this.name = 'FileOperationError'; 72 | } 73 | } 74 | 75 | export interface TaskResult { 76 | success: boolean; 77 | data?: T; 78 | error?: Error; 79 | duration: number; 80 | } 81 | 82 | export interface ProcessFileResult { 83 | lines: number; 84 | size: number; 85 | truncated: boolean; 86 | } 87 | 88 | export interface FileProcessingResult { 89 | content: FileContent; 90 | error?: Error; 91 | } -------------------------------------------------------------------------------- /src/utils/globAsync.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'glob'; 2 | import { promisify } from 'util'; 3 | 4 | export const globAsync = promisify(glob); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "target": "ES2022", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "esModuleInterop": true, 8 | "outDir": "./build", 9 | "rootDir": "./src", 10 | "strict": true, 11 | "declaration": true, 12 | "sourceMap": true, 13 | "resolveJsonModule": true, 14 | "allowJs": true, 15 | "paths": { 16 | "@typescript-eslint/*": [ 17 | "./node_modules/@typescript-eslint/*" 18 | ] 19 | } 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "build" 27 | ] 28 | } --------------------------------------------------------------------------------