Skip to main content
Glama

MCP Prompt Enhancer

by soniankur948
ProjectContextManager.js18.9 kB
"use strict"; /** * ProjectContextManager * * Manages project-level context including: * - Project structure analysis * - Framework and dependency detection * - Git history and recent changes * - Context caching */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProjectContextManager = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const glob_1 = require("glob"); const simple_git_1 = require("simple-git"); const chokidar = __importStar(require("chokidar")); const lodash_1 = require("lodash"); const Logger_1 = __importDefault(require("../utils/Logger")); // 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'], }; class ProjectContextManager { constructor(config = {}) { this.context = null; this.lastUpdateTime = 0; this.fileWatcher = null; this.git = null; this.log = Logger_1.default.createChildLogger('ProjectContextManager'); 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 = (0, simple_git_1.simpleGit)(this.config.projectPath); } catch (error) { this.log.warn('Git integration not available', error); } this.setupFileWatcher(); } /** * Get the project context, refreshing if needed */ async getContext(forceRefresh = false) { 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 */ async analyzeProject() { 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 = { 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); } } // 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); } } // 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); throw new Error(`Failed to analyze project: ${error.message}`); } } /** * Detect frameworks used in the project */ detectFrameworks(packageJson) { const frameworks = []; 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 */ async buildFileStructure(dirPath, isRoot = true) { const structure = { 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); } return structure; } /** * Check if a path should be ignored */ shouldIgnore(pathName) { 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); return false; } }); } /** * Detect code patterns in the project */ async detectCodePatterns(projectPath, frameworks) { const patterns = []; 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); } return patterns.filter(Boolean); } /** * Detect React component patterns */ async detectReactPatterns(projectPath) { const pattern = { type: 'component', pattern: 'React component definition', examples: [] }; try { // Find React component files const files = await (0, glob_1.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); } return pattern; } /** * Detect Express route patterns */ async detectExpressPatterns(projectPath) { const pattern = { type: 'route', pattern: 'Express route definition', examples: [] }; try { // Find Express route files const files = await (0, glob_1.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); } return pattern; } /** * Detect generic code patterns */ async detectGenericPatterns(projectPath) { const patterns = []; try { // Function pattern const functionPattern = { type: 'function', pattern: 'Function definition', examples: [] }; // Class pattern const classPattern = { type: 'class', pattern: 'Class definition', examples: [] }; // Find all code files const files = await (0, glob_1.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); } return patterns; } /** * Extract a code snippet from the content starting at the given index */ extractCodeSnippet(content, startIndex) { 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 */ setupFileWatcher() { try { // Create debounced update function const debouncedUpdate = (0, lodash_1.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); } } /** * Clean up resources */ dispose() { if (this.fileWatcher) { this.fileWatcher.close().catch(error => { this.log.warn('Error closing file watcher', error); }); } } } exports.ProjectContextManager = ProjectContextManager; exports.default = ProjectContextManager; //# sourceMappingURL=ProjectContextManager.js.map

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