Skip to main content
Glama
smart-suggestions.js•20.5 kB
/** * Smart Suggestions Generator * Provides proactive development assistance based on context and patterns */ import { getSemanticSearch } from './semantic-search.js'; export class SmartSuggestionsGenerator { constructor() { this.semanticSearch = null; this.initialized = false; this.suggestionCache = new Map(); this.contextHistory = []; this.maxHistoryLength = 50; // Suggestion types and their generators this.suggestionTypes = { next_steps: this.generateNextStepSuggestions.bind(this), code_improvements: this.generateCodeImprovements.bind(this), bug_prevention: this.generateBugPreventionSuggestions.bind(this), performance: this.generatePerformanceSuggestions.bind(this), architecture: this.generateArchitectureSuggestions.bind(this), testing: this.generateTestingSuggestions.bind(this), documentation: this.generateDocumentationSuggestions.bind(this), refactoring: this.generateRefactoringSuggestions.bind(this), security: this.generateSecuritySuggestions.bind(this), best_practices: this.generateBestPracticeSuggestions.bind(this), }; // Pattern-based suggestion rules this.suggestionRules = { missing_tests: { pattern: /^(?!.*\.test\.|.*\.spec\.).*\.(js|ts|jsx|tsx)$/, suggestion: 'Consider adding unit tests for this module', priority: 'high', }, large_function: { pattern: /function.*\{[\s\S]{500,}\}/, suggestion: 'This function might be too large. Consider breaking it down', priority: 'medium', }, missing_error_handling: { pattern: /\.catch\s*\(\s*\)/, suggestion: 'Empty catch block detected. Add proper error handling', priority: 'high', }, console_logs: { pattern: /console\.(log|warn|error|debug)/, suggestion: 'Remove console statements before production', priority: 'low', }, todo_comments: { pattern: /\/\/\s*(TODO|FIXME|HACK|XXX)/i, suggestion: 'Address TODO comments before committing', priority: 'medium', }, hardcoded_values: { pattern: /(api_key|password|secret)\s*=\s*["'][\w]+["']/i, suggestion: 'Move sensitive values to environment variables', priority: 'critical', }, unused_imports: { pattern: /import\s+{\s*([^}]+)\s*}\s+from/, suggestion: 'Check for unused imports to reduce bundle size', priority: 'low', }, long_lines: { pattern: /^.{120,}$/m, suggestion: 'Consider breaking long lines for better readability', priority: 'low', }, }; } async initialize() { if (this.initialized) return; try { this.semanticSearch = getSemanticSearch(); await this.semanticSearch.initialize(); this.initialized = true; } catch (error) { console.warn('SmartSuggestionsGenerator: Semantic search initialization failed:', error.message); // Continue without semantic search this.initialized = true; } } /** * Generate suggestions based on context */ async generateSuggestions(options = {}) { const { context = {}, files = [], recentActions = [], currentState = 'idle', requestedTypes = ['next_steps', 'code_improvements'], maxSuggestions = 10, } = options; await this.initialize(); // Update context history this.updateContextHistory({ timestamp: Date.now(), context, files, recentActions, currentState, }); // Check cache const cacheKey = this.generateCacheKey(options); if (this.suggestionCache.has(cacheKey)) { const cached = this.suggestionCache.get(cacheKey); if (Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5 min cache return cached.suggestions; } } // Generate suggestions const suggestions = []; for (const type of requestedTypes) { if (this.suggestionTypes[type]) { const typeSuggestions = await this.suggestionTypes[type]({ context, files, recentActions, currentState, history: this.contextHistory, }); suggestions.push(...typeSuggestions); } } // Sort and limit suggestions const sortedSuggestions = this.prioritizeSuggestions(suggestions) .slice(0, maxSuggestions); // Cache results this.suggestionCache.set(cacheKey, { suggestions: sortedSuggestions, timestamp: Date.now(), }); return sortedSuggestions; } /** * Generate next step suggestions */ async generateNextStepSuggestions({ context, recentActions, currentState }) { const suggestions = []; // Based on current state if (currentState === 'bug_fixing') { suggestions.push({ type: 'next_step', action: 'run_tests', description: 'Run tests to verify the bug fix', priority: 'high', confidence: 0.9, context: 'After fixing a bug, always verify with tests', }); } // Based on recent actions if (recentActions.some(a => a.tool === 'create_file')) { suggestions.push({ type: 'next_step', action: 'create_tests', description: 'Create unit tests for the new file', priority: 'high', confidence: 0.8, context: 'New files should have corresponding tests', }); } if (recentActions.some(a => a.tool === 'update_file' && a.significant)) { suggestions.push({ type: 'next_step', action: 'check_impacts', description: 'Check for impacts on dependent modules', priority: 'medium', confidence: 0.7, context: 'Significant changes may affect other parts', }); } // Based on project state if (context.uncommittedChanges > 10) { suggestions.push({ type: 'next_step', action: 'commit_changes', description: 'Consider committing your changes', priority: 'medium', confidence: 0.8, context: 'Multiple uncommitted changes detected', }); } return suggestions; } /** * Generate code improvement suggestions */ async generateCodeImprovements({ files, context }) { const suggestions = []; for (const file of files) { // Apply pattern-based rules for (const [ruleName, rule] of Object.entries(this.suggestionRules)) { if (file.content && rule.pattern.test(file.content)) { suggestions.push({ type: 'code_improvement', file: file.path, rule: ruleName, description: rule.suggestion, priority: rule.priority, confidence: 0.8, line: this.findLineNumber(file.content, rule.pattern), }); } } // File-specific suggestions if (file.path.endsWith('.js') || file.path.endsWith('.ts')) { if (!file.content?.includes('use strict') && !file.path.includes('.test.')) { suggestions.push({ type: 'code_improvement', file: file.path, description: 'Consider adding "use strict" directive', priority: 'low', confidence: 0.6, }); } } // Complexity-based suggestions const complexity = this.estimateComplexity(file.content); if (complexity > 20) { suggestions.push({ type: 'code_improvement', file: file.path, description: 'High complexity detected. Consider refactoring', priority: 'medium', confidence: 0.7, metric: { complexity }, }); } } return suggestions; } /** * Generate bug prevention suggestions */ async generateBugPreventionSuggestions({ files, recentActions }) { const suggestions = []; // Check for common bug patterns for (const file of files) { if (!file.content) continue; // Null/undefined checks if (file.content.match(/\.\w+\s*\(/g) && !file.content.includes('?.')) { suggestions.push({ type: 'bug_prevention', file: file.path, description: 'Consider using optional chaining (?.) for safer property access', priority: 'medium', confidence: 0.7, }); } // Array bounds if (file.content.includes('[') && !file.content.includes('length')) { suggestions.push({ type: 'bug_prevention', file: file.path, description: 'Ensure array bounds are checked before access', priority: 'medium', confidence: 0.6, }); } // Type safety if (file.path.endsWith('.js') && file.content.includes('function')) { suggestions.push({ type: 'bug_prevention', file: file.path, description: 'Consider migrating to TypeScript for better type safety', priority: 'low', confidence: 0.5, }); } } return suggestions; } /** * Generate performance suggestions */ async generatePerformanceSuggestions({ files }) { const suggestions = []; for (const file of files) { if (!file.content) continue; // Nested loops if (file.content.match(/for.*\{[\s\S]*?for.*\{/)) { suggestions.push({ type: 'performance', file: file.path, description: 'Nested loops detected. Consider optimization', priority: 'medium', confidence: 0.7, }); } // Large arrays if (file.content.includes('.map(') && file.content.includes('.filter(')) { suggestions.push({ type: 'performance', file: file.path, description: 'Multiple array operations. Consider combining for efficiency', priority: 'low', confidence: 0.6, }); } // Repeated calculations if (file.content.match(/Math\.\w+\([^)]+\)/g)?.length > 5) { suggestions.push({ type: 'performance', file: file.path, description: 'Consider caching repeated calculations', priority: 'low', confidence: 0.5, }); } } return suggestions; } /** * Generate architecture suggestions */ async generateArchitectureSuggestions({ context, files }) { const suggestions = []; // Check for architectural patterns const hasLargeFiles = files.some(f => f.size > 10000); if (hasLargeFiles) { suggestions.push({ type: 'architecture', description: 'Large files detected. Consider splitting into smaller modules', priority: 'medium', confidence: 0.8, files: files.filter(f => f.size > 10000).map(f => f.path), }); } // Check for proper separation of concerns const mixedConcerns = files.filter(f => f.content?.includes('database') && f.content?.includes('render') ); if (mixedConcerns.length > 0) { suggestions.push({ type: 'architecture', description: 'Consider separating data access from presentation logic', priority: 'medium', confidence: 0.7, files: mixedConcerns.map(f => f.path), }); } return suggestions; } /** * Generate testing suggestions */ async generateTestingSuggestions({ files, context }) { const suggestions = []; // Find untested files const sourceFiles = files.filter(f => (f.path.endsWith('.js') || f.path.endsWith('.ts')) && !f.path.includes('.test.') && !f.path.includes('.spec.') && !f.path.includes('test/') ); const testFiles = files.filter(f => f.path.includes('.test.') || f.path.includes('.spec.') ); for (const sourceFile of sourceFiles) { const hasTest = testFiles.some(t => t.path.includes(sourceFile.path.replace(/\.(js|ts)$/, '')) ); if (!hasTest && sourceFile.content?.includes('export')) { suggestions.push({ type: 'testing', file: sourceFile.path, description: 'No test file found for this module', priority: 'high', confidence: 0.9, action: 'create_test_file', }); } } // Check test coverage for (const testFile of testFiles) { if (testFile.content && !testFile.content.includes('describe')) { suggestions.push({ type: 'testing', file: testFile.path, description: 'Test file missing test suites', priority: 'high', confidence: 0.8, }); } } return suggestions; } /** * Generate documentation suggestions */ async generateDocumentationSuggestions({ files }) { const suggestions = []; for (const file of files) { if (!file.content) continue; // Check for JSDoc comments const functions = file.content.match(/function\s+\w+\s*\([^)]*\)/g) || []; const jsdocs = file.content.match(/\/\*\*[\s\S]*?\*\//g) || []; if (functions.length > jsdocs.length && functions.length > 2) { suggestions.push({ type: 'documentation', file: file.path, description: 'Missing JSDoc comments for functions', priority: 'low', confidence: 0.7, metric: { functions: functions.length, documented: jsdocs.length, }, }); } // Check for README if (file.path === 'README.md' && file.content.length < 100) { suggestions.push({ type: 'documentation', file: file.path, description: 'README file seems incomplete', priority: 'medium', confidence: 0.8, }); } } return suggestions; } /** * Generate refactoring suggestions */ async generateRefactoringSuggestions({ files }) { const suggestions = []; for (const file of files) { if (!file.content) continue; // Duplicate code detection (simple) const lines = file.content.split('\n'); const duplicates = this.findDuplicateBlocks(lines); if (duplicates.length > 0) { suggestions.push({ type: 'refactoring', file: file.path, description: 'Duplicate code blocks detected', priority: 'medium', confidence: 0.7, duplicates: duplicates.length, }); } // Long parameter lists const longParams = file.content.match(/function.*\([^)]{50,}\)/g); if (longParams) { suggestions.push({ type: 'refactoring', file: file.path, description: 'Consider using object parameters for functions with many arguments', priority: 'low', confidence: 0.6, }); } } return suggestions; } /** * Generate security suggestions */ async generateSecuritySuggestions({ files }) { const suggestions = []; for (const file of files) { if (!file.content) continue; // SQL injection risks if (file.content.includes('query') && file.content.includes('${')) { suggestions.push({ type: 'security', file: file.path, description: 'Potential SQL injection risk. Use parameterized queries', priority: 'critical', confidence: 0.8, }); } // XSS risks if (file.content.includes('innerHTML') || file.content.includes('dangerouslySetInnerHTML')) { suggestions.push({ type: 'security', file: file.path, description: 'Potential XSS risk. Sanitize user input', priority: 'high', confidence: 0.8, }); } // Hardcoded secrets const secretPatterns = [ /api[_-]?key\s*[:=]\s*["'][^"']{20,}/i, /password\s*[:=]\s*["'][^"']+/i, /secret\s*[:=]\s*["'][^"']{10,}/i, ]; for (const pattern of secretPatterns) { if (pattern.test(file.content)) { suggestions.push({ type: 'security', file: file.path, description: 'Hardcoded secrets detected. Use environment variables', priority: 'critical', confidence: 0.9, }); break; } } } return suggestions; } /** * Generate best practice suggestions */ async generateBestPracticeSuggestions({ files, context }) { const suggestions = []; // Check for common best practices for (const file of files) { if (!file.content) continue; // Error handling if (file.content.includes('async') && !file.content.includes('try')) { suggestions.push({ type: 'best_practice', file: file.path, description: 'Async functions should have error handling', priority: 'medium', confidence: 0.7, }); } // Consistent naming if (file.content.match(/[a-z]+_[a-z]+/) && file.content.match(/[a-z]+[A-Z]/)) { suggestions.push({ type: 'best_practice', file: file.path, description: 'Inconsistent naming conventions detected', priority: 'low', confidence: 0.6, }); } } return suggestions; } /** * Prioritize suggestions based on various factors */ prioritizeSuggestions(suggestions) { const priorityWeights = { critical: 10, high: 8, medium: 5, low: 2, }; return suggestions.sort((a, b) => { const scoreA = (priorityWeights[a.priority] || 0) * (a.confidence || 0.5); const scoreB = (priorityWeights[b.priority] || 0) * (b.confidence || 0.5); return scoreB - scoreA; }); } /** * Update context history */ updateContextHistory(entry) { this.contextHistory.push(entry); if (this.contextHistory.length > this.maxHistoryLength) { this.contextHistory.shift(); } } /** * Generate cache key for suggestions */ generateCacheKey(options) { const { context, files, currentState, requestedTypes } = options; const fileKeys = files.map(f => `${f.path}:${f.size}`).sort().join(','); return `${currentState}:${requestedTypes.join(',')}:${fileKeys}:${context.projectType || 'unknown'}`; } /** * Find line number for pattern match */ findLineNumber(content, pattern) { const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { if (pattern.test(lines[i])) { return i + 1; } } return null; } /** * Estimate code complexity */ estimateComplexity(content) { if (!content) return 0; let complexity = 1; // Count decision points const decisionPatterns = [ /if\s*\(/g, /else\s+if\s*\(/g, /switch\s*\(/g, /case\s+/g, /while\s*\(/g, /for\s*\(/g, /\?\s*[^:]+\s*:/g, // ternary /&&/g, /\|\|/g, ]; for (const pattern of decisionPatterns) { const matches = content.match(pattern); if (matches) { complexity += matches.length; } } return complexity; } /** * Find duplicate code blocks */ findDuplicateBlocks(lines, minBlockSize = 5) { const duplicates = []; const blockMap = new Map(); for (let i = 0; i < lines.length - minBlockSize; i++) { const block = lines.slice(i, i + minBlockSize).join('\n').trim(); if (block.length < 50) continue; // Skip small blocks if (blockMap.has(block)) { duplicates.push({ lines: [blockMap.get(block), i], size: minBlockSize, }); } else { blockMap.set(block, i); } } return duplicates; } /** * Get suggestion history for learning */ getSuggestionHistory() { return { totalSuggestions: this.suggestionCache.size, recentContext: this.contextHistory.slice(-10), cacheSize: this.suggestionCache.size, }; } /** * Clear caches */ clearCaches() { this.suggestionCache.clear(); this.contextHistory = []; } }

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/moikas-code/moidvk'

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