Skip to main content
Glama

MCP Prompt Enhancer

by soniankur948
ProjectContextManager.ts16.7 kB
/** * ProjectContextManager * * Manages project-level context including: * - Project structure analysis * - Framework and dependency detection * - Git history and recent changes * - Context caching */ import * as fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; import { simpleGit, SimpleGit } from 'simple-git'; import * as chokidar from 'chokidar'; import { debounce } from 'lodash'; import logger from '../utils/Logger'; // Configuration type definitions export interface ProjectContextConfig { projectPath: string; ignorePatterns: string[]; cacheTTL: number; // Time-to-live for context cache in seconds maxContextSize: number; // Maximum tokens for context } // Framework detection patterns const FRAMEWORK_PATTERNS = { react: ['react', 'jsx', 'tsx', 'createContext', 'useState', 'useEffect'], vue: ['vue', 'createApp', 'defineComponent', '<template>', '<script setup>'], angular: ['@angular', 'NgModule', 'Component', 'Injectable'], nextjs: ['next.config', 'getStaticProps', 'getServerSideProps', '_app.tsx', 'pages/'], express: ['express', 'app.use', 'app.get', 'app.post', 'Router()'], nestjs: ['@nestjs', '@Controller', '@Injectable', '@Module'], }; // Project structure context export interface ProjectContext { timestamp: number; projectName: string; frameworks: string[]; dependencies: Record<string, string>; devDependencies: Record<string, string>; fileStructure: FileStructure; recentChanges: GitChange[]; branchInfo: GitBranchInfo; patterns: CodePattern[]; } // File structure representation export interface FileStructure { directories: Record<string, FileStructure>; files: string[]; } // Git change information export interface GitChange { file: string; status: string; date: string; author: string; message: string; } // Git branch information export interface GitBranchInfo { currentBranch: string; branches: string[]; remotes: string[]; } // Code pattern information export interface CodePattern { type: string; // e.g., 'component', 'route', 'api', 'model' pattern: string; examples: string[]; } export class ProjectContextManager { private config: ProjectContextConfig; private context: ProjectContext | null = null; private lastUpdateTime: number = 0; private fileWatcher: chokidar.FSWatcher | null = null; private git: SimpleGit | null = null; private log = logger.createChildLogger('ProjectContextManager'); constructor(config: Partial<ProjectContextConfig> = {}) { this.config = { projectPath: config.projectPath || process.cwd(), ignorePatterns: config.ignorePatterns || [ 'node_modules', 'dist', '.git', 'build', 'coverage', '*.log', ], cacheTTL: config.cacheTTL || 900, // 15 minutes by default maxContextSize: config.maxContextSize || 10000, }; try { this.git = simpleGit(this.config.projectPath); } catch (error) { this.log.warn('Git integration not available', error); } this.setupFileWatcher(); } /** * Get the project context, refreshing if needed */ public async getContext(forceRefresh = false): Promise<ProjectContext> { const now = Date.now(); const cacheExpired = now - this.lastUpdateTime > this.config.cacheTTL * 1000; if (!this.context || forceRefresh || cacheExpired) { this.log.info('Refreshing project context...'); this.context = await this.analyzeProject(); this.lastUpdateTime = now; } return this.context; } /** * Analyze the entire project and build context */ private async analyzeProject(): Promise<ProjectContext> { try { const projectPath = this.config.projectPath; // Get project name from package.json or directory name let projectName = path.basename(projectPath); const packageJsonPath = path.join(projectPath, 'package.json'); // Initialize context structure const context: ProjectContext = { timestamp: Date.now(), projectName, frameworks: [], dependencies: {}, devDependencies: {}, fileStructure: { directories: {}, files: [] }, recentChanges: [], branchInfo: { currentBranch: '', branches: [], remotes: [] }, patterns: [], }; // Read package.json if exists if (await fs.pathExists(packageJsonPath)) { try { const packageJson = await fs.readJson(packageJsonPath); context.projectName = packageJson.name || projectName; context.dependencies = packageJson.dependencies || {}; context.devDependencies = packageJson.devDependencies || {}; // Infer frameworks from dependencies context.frameworks = this.detectFrameworks(packageJson); } catch (error) { this.log.error('Error parsing package.json', error as Error); } } // Build file structure context.fileStructure = await this.buildFileStructure(projectPath); // Get git information if available if (this.git) { try { // Get recent changes const gitLog = await this.git.log({ maxCount: 10 }); context.recentChanges = gitLog.all.map(commit => { return { file: '', // Will be filled in with diff info in a production version status: 'committed', date: new Date(commit.date).toISOString(), author: commit.author_name, message: commit.message, }; }); // Get branch info const branches = await this.git.branchLocal(); const remotes = await this.git.getRemotes(true); context.branchInfo = { currentBranch: branches.current, branches: branches.all, remotes: remotes.map(remote => `${remote.name}: ${remote.refs.fetch}`), }; } catch (error) { this.log.warn('Error getting git information', error as Error); } } // Detect code patterns context.patterns = await this.detectCodePatterns(projectPath, context.frameworks); this.log.info(`Project context refreshed for ${context.projectName}`); return context; } catch (error) { this.log.error('Error analyzing project', error as Error); throw new Error(`Failed to analyze project: ${(error as Error).message}`); } } /** * Detect frameworks used in the project */ private detectFrameworks(packageJson: any): string[] { const frameworks: string[] = []; const allDependencies = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) }; // Check dependencies for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) { const hasFramework = patterns.some(pattern => Object.keys(allDependencies).some(dep => dep.includes(pattern)) ); if (hasFramework) { frameworks.push(framework); } } // Check for custom configuration files const configFiles = Object.keys(packageJson.scripts || {}).join(' '); if (!frameworks.includes('nextjs') && configFiles.includes('next')) { frameworks.push('nextjs'); } if (!frameworks.includes('react') && (configFiles.includes('react-scripts') || configFiles.includes('vite'))) { frameworks.push('react'); } if (!frameworks.includes('vue') && configFiles.includes('vue-cli-service')) { frameworks.push('vue'); } return frameworks; } /** * Build file structure recursively */ private async buildFileStructure(dirPath: string, isRoot = true): Promise<FileStructure> { const structure: FileStructure = { directories: {}, files: [] }; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path.join(dirPath, entry.name); // Skip ignored patterns if (isRoot && this.shouldIgnore(entry.name)) { continue; } if (entry.isDirectory()) { // Process subdirectory structure.directories[entry.name] = await this.buildFileStructure(entryPath, false); } else { // Add file structure.files.push(entry.name); } } } catch (error) { this.log.warn(`Error reading directory ${dirPath}`, error as Error); } return structure; } /** * Check if a path should be ignored */ private shouldIgnore(pathName: string): boolean { return this.config.ignorePatterns.some(pattern => { // Exact match if (pathName === pattern) { return true; } // Directory match if (pathName.startsWith(pattern + '/')) { return true; } // Handle glob patterns safely try { // Escape special regex characters except * which we convert to .* const regexPattern = pattern .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars .replace(/\*/g, '.*'); // Convert * to .* return new RegExp(`^${regexPattern}$`).test(pathName); } catch (error) { this.log.warn(`Invalid pattern: ${pattern}`, error as Error); return false; } }); } /** * Detect code patterns in the project */ private async detectCodePatterns(projectPath: string, frameworks: string[]): Promise<CodePattern[]> { const patterns: CodePattern[] = []; try { // Add framework-specific pattern detection here if (frameworks.includes('react') || frameworks.includes('nextjs')) { // React component patterns patterns.push(await this.detectReactPatterns(projectPath)); } if (frameworks.includes('express')) { // Express route patterns patterns.push(await this.detectExpressPatterns(projectPath)); } // Generic patterns (functions, classes, etc.) patterns.push(...await this.detectGenericPatterns(projectPath)); } catch (error) { this.log.warn('Error detecting code patterns', error as Error); } return patterns.filter(Boolean) as CodePattern[]; } /** * Detect React component patterns */ private async detectReactPatterns(projectPath: string): Promise<CodePattern> { const pattern: CodePattern = { type: 'component', pattern: 'React component definition', examples: [] }; try { // Find React component files const files = await glob('**/*.{jsx,tsx}', { cwd: projectPath, ignore: this.config.ignorePatterns }); // Sample a few files const sampleFiles = files.slice(0, 3); for (const file of sampleFiles) { const filePath = path.join(projectPath, file); const content = await fs.readFile(filePath, 'utf-8'); // Extract component definition const componentMatch = content.match(/function\s+(\w+)\s*\([^)]*\)\s*{|const\s+(\w+)\s*=\s*\([^)]*\)\s*=>/); if (componentMatch) { const snippet = this.extractCodeSnippet(content, componentMatch.index || 0); if (snippet) { pattern.examples.push(snippet); } } } } catch (error) { this.log.warn('Error detecting React patterns', error as Error); } return pattern; } /** * Detect Express route patterns */ private async detectExpressPatterns(projectPath: string): Promise<CodePattern> { const pattern: CodePattern = { type: 'route', pattern: 'Express route definition', examples: [] }; try { // Find Express route files const files = await glob('**/*.{js,ts}', { cwd: projectPath, ignore: this.config.ignorePatterns }); // Sample a few files const sampleFiles = files.slice(0, 3); for (const file of sampleFiles) { const filePath = path.join(projectPath, file); const content = await fs.readFile(filePath, 'utf-8'); // Extract route definition const routeMatch = content.match(/app\.(get|post|put|delete)\s*\(['"]\//); if (routeMatch) { const snippet = this.extractCodeSnippet(content, routeMatch.index || 0); if (snippet) { pattern.examples.push(snippet); } } } } catch (error) { this.log.warn('Error detecting Express patterns', error as Error); } return pattern; } /** * Detect generic code patterns */ private async detectGenericPatterns(projectPath: string): Promise<CodePattern[]> { const patterns: CodePattern[] = []; try { // Function pattern const functionPattern: CodePattern = { type: 'function', pattern: 'Function definition', examples: [] }; // Class pattern const classPattern: CodePattern = { type: 'class', pattern: 'Class definition', examples: [] }; // Find all code files const files = await glob('**/*.{js,ts,jsx,tsx}', { cwd: projectPath, ignore: this.config.ignorePatterns }); // Sample a few files const sampleFiles = files.slice(0, 3); for (const file of sampleFiles) { const filePath = path.join(projectPath, file); const content = await fs.readFile(filePath, 'utf-8'); // Find function definitions const functionMatches = content.matchAll(/function\s+(\w+)\s*\([^)]*\)\s*{/g); for (const match of functionMatches) { const snippet = this.extractCodeSnippet(content, match.index || 0); if (snippet && functionPattern.examples.length < 3) { functionPattern.examples.push(snippet); } } // Find class definitions const classMatches = content.matchAll(/class\s+(\w+)(\s+extends\s+\w+)?\s*{/g); for (const match of classMatches) { const snippet = this.extractCodeSnippet(content, match.index || 0); if (snippet && classPattern.examples.length < 3) { classPattern.examples.push(snippet); } } } if (functionPattern.examples.length > 0) { patterns.push(functionPattern); } if (classPattern.examples.length > 0) { patterns.push(classPattern); } } catch (error) { this.log.warn('Error detecting generic patterns', error as Error); } return patterns; } /** * Extract a code snippet from the content starting at the given index */ private extractCodeSnippet(content: string, startIndex: number): string | null { try { // Get up to 10 lines const endOfContent = content.indexOf('\n', startIndex + 200) || content.length; const snippet = content.substring(startIndex, endOfContent); // Get a few lines const lines = snippet.split('\n').slice(0, 10); return lines.join('\n').trim(); } catch (error) { return null; } } /** * Set up file watcher to trigger context updates */ private setupFileWatcher(): void { try { // Create debounced update function const debouncedUpdate = debounce(() => { this.log.debug('Files changed, invalidating context cache'); this.lastUpdateTime = 0; // Invalidate cache }, 5000); // Debounce for 5 seconds // Set up file watcher const ignored = this.config.ignorePatterns.map(pattern => new RegExp(`(^|/)${pattern.replace(/\*/g, '.*')}(/|$)`) ); this.fileWatcher = chokidar.watch(this.config.projectPath, { ignored, persistent: true, ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 100 } }); this.fileWatcher .on('add', debouncedUpdate) .on('change', debouncedUpdate) .on('unlink', debouncedUpdate) .on('ready', () => { this.log.debug('File watcher initialized'); }) .on('error', (error) => { this.log.error('File watcher error', error); }); } catch (error) { this.log.warn('Error setting up file watcher', error as Error); } } /** * Clean up resources */ public dispose(): void { if (this.fileWatcher) { this.fileWatcher.close().catch(error => { this.log.warn('Error closing file watcher', error); }); } } } export default ProjectContextManager;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/soniankur948/prompt-enhancer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server