UNPKG

claude-buddy

Version:

Your friendly AI development companion for Claude Code - supercharge Claude Code with intelligent workflows and safety features

705 lines (600 loc) 21.6 kB
/** * Auto-Activation Context Detection Engine * * Intelligent persona selection based on multi-factor analysis: * - Keyword matching (30%) * - Context analysis (40%) * - File patterns (20%) * - User history (10%) */ import { promises as fs } from 'fs'; import path from 'path'; import type { DetectionResults, PersonaRecommendation } from '../types/personas.js'; import type { InputContext } from '../types/context.js'; interface PersonaConfig { auto_activation?: { keywords?: string[]; confidence_weight?: number; file_patterns?: string[]; }; } interface Config { personas: Record<string, PersonaConfig>; auto_activation?: { global_confidence_threshold?: number; multi_persona_limit?: number; }; } interface ProjectAnalysis { type: string; frameworks: string[]; features: string[]; complexity: number; } interface FilePatterns { frontend: number; backend: number; config: number; test: number; docs: number; } interface SessionContext { activePersonas: string[]; filePatterns: FilePatterns; projectType: string | null; recentCommands: RecentCommand[]; frameworks?: string[]; features?: string[]; } interface RecentCommand { input: string; personas: string[]; timestamp: number; } interface Analysis { userInput: string; keywords: string[]; context: unknown; files: string[]; command: string; } interface PersonaScoreBreakdown { keyword_matching: number; context_analysis: number; file_patterns: number; user_history: number; } interface PersonaScore { total: number; breakdown: PersonaScoreBreakdown; } interface UserHistoryData { usageCount: number; successfulUsage: number; lastUsed: number; contexts: unknown[]; } interface Weights { keyword_matching: number; context_analysis: number; file_patterns: number; user_history: number; } /** * Persona Auto-Activation Engine * * Intelligent persona selection engine that analyzes context to automatically * activate the most appropriate personas for a given task. Uses multi-factor * scoring algorithm with keyword matching, context analysis, file patterns, * and user history. * * @category Auto-Activation * @public */ class PersonaActivationEngine { private config: Config | null = null; private userHistory: Map<string, UserHistoryData> = new Map(); private sessionContext: SessionContext; private configPath: string; private weights: Weights; constructor(configPath?: string) { this.sessionContext = { activePersonas: [], filePatterns: { frontend: 0, backend: 0, config: 0, test: 0, docs: 0 }, projectType: null, recentCommands: [], }; this.configPath = configPath || path.join(__dirname, 'config', 'personas-config.json'); this.weights = { keyword_matching: 0.3, context_analysis: 0.4, file_patterns: 0.2, user_history: 0.1 }; } /** * Initialize the persona system with configuration */ async initialize(): Promise<boolean> { try { const configContent = await fs.readFile(this.configPath, 'utf8'); this.config = JSON.parse(configContent); // Load user history if exists await this.loadUserHistory(); // Analyze project context await this.analyzeProjectContext(); console.log('Persona activation engine initialized successfully'); return true; } catch (error) { console.error('Failed to initialize persona activation engine:', error); return false; } } /** * Analyze project context to understand technology stack and structure */ private async analyzeProjectContext(): Promise<void> { try { const projectRoot = process.cwd(); const packageJsonPath = path.join(projectRoot, 'package.json'); const pyprojectPath = path.join(projectRoot, 'pyproject.toml'); const cargoPath = path.join(projectRoot, 'Cargo.toml'); const gemfilePath = path.join(projectRoot, 'Gemfile'); // Detect project type and frameworks const projectAnalysis: ProjectAnalysis = { type: 'unknown', frameworks: [], features: [], complexity: 0.5 }; // Analyze package.json for JavaScript/TypeScript projects if (await this.fileExists(packageJsonPath)) { const packageContent = await fs.readFile(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageContent); projectAnalysis.type = 'javascript'; // Detect frontend frameworks const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (deps.react) projectAnalysis.frameworks.push('react'); if (deps.vue) projectAnalysis.frameworks.push('vue'); if (deps['@angular/core']) projectAnalysis.frameworks.push('angular'); if (deps.express) projectAnalysis.frameworks.push('express'); if (deps.next) projectAnalysis.frameworks.push('next'); // Detect features if (deps.typescript) projectAnalysis.features.push('typescript'); if (deps.jest || deps.mocha) projectAnalysis.features.push('testing'); if (deps.docker || deps.dockerfile) projectAnalysis.features.push('containerization'); } // Analyze Python projects if (await this.fileExists(pyprojectPath)) { projectAnalysis.type = 'python'; // Could parse pyproject.toml for dependencies } // Analyze Rust projects if (await this.fileExists(cargoPath)) { projectAnalysis.type = 'rust'; } // Analyze Ruby projects if (await this.fileExists(gemfilePath)) { projectAnalysis.type = 'ruby'; } // Detect file patterns await this.detectFilePatterns(); this.sessionContext.projectType = projectAnalysis.type; this.sessionContext.frameworks = projectAnalysis.frameworks; this.sessionContext.features = projectAnalysis.features; } catch (error) { console.error('Error analyzing project context:', error); } } /** * Detect file patterns in the current project */ private async detectFilePatterns(): Promise<void> { try { const patterns: FilePatterns = { frontend: 0, backend: 0, config: 0, test: 0, docs: 0 }; // Sample some files to understand project structure const files = await this.getProjectFiles(); for (const file of files.slice(0, 100)) { // Limit to avoid performance issues const ext = path.extname(file).toLowerCase(); const basename = path.basename(file).toLowerCase(); // Frontend patterns if (['.jsx', '.tsx', '.vue', '.css', '.scss', '.sass', '.html'].includes(ext)) { patterns.frontend++; } // Backend patterns if (['.py', '.js', '.ts', '.go', '.java', '.rb', '.php'].includes(ext) && (file.includes('/api/') || file.includes('/server/') || file.includes('/backend/'))) { patterns.backend++; } // Config patterns if (basename.includes('config') || basename.includes('.env') || ['.yml', '.yaml', '.json', '.toml'].includes(ext)) { patterns.config++; } // Test patterns if (basename.includes('test') || basename.includes('spec') || file.includes('/tests/') || file.includes('/__tests__/')) { patterns.test++; } // Documentation patterns if (['.md', '.rst', '.txt'].includes(ext) || file.includes('/docs/')) { patterns.docs++; } } this.sessionContext.filePatterns = patterns; } catch (error) { console.error('Error detecting file patterns:', error); } } /** * Get project files recursively (with reasonable limits) */ private async getProjectFiles(): Promise<string[]> { const files: string[] = []; const maxFiles = 500; const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv']; const walkDir = async (dir: string, depth = 0): Promise<void> => { if (depth > 5 || files.length >= maxFiles) return; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (files.length >= maxFiles) break; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!ignoreDirs.some(ignore => entry.name.includes(ignore))) { await walkDir(fullPath, depth + 1); } } else { files.push(fullPath); } } } catch (error) { // Ignore permission errors, etc. } }; await walkDir(process.cwd()); return files; } /** * Detect and recommend appropriate personas based on multi-factor analysis. * * Uses a sophisticated scoring algorithm that analyzes: * - Keyword matching (30% weight) - Direct matches in user input * - Context analysis (40% weight) - Project structure and technical indicators * - File patterns (20% weight) - File types and project organization * - User history (10% weight) - Previous successful activations * * @param userInput - The user's input string to analyze * @param options - Additional context including files, command type, project info * @returns Promise resolving to detection results with persona recommendations * * @example * ```typescript * const results = await activationEngine.detectPersonas( * "Review this authentication code for vulnerabilities", * { * files: ["auth.ts", "middleware/security.ts"], * command: "review", * projectType: "web-app" * } * ); * * console.log('Recommended personas:'); * results.recommendations.forEach(rec => { * console.log(`${rec.persona}: ${rec.confidence.toFixed(2)} confidence`); * console.log(`Reasoning: ${rec.reasoning}`); * }); * ``` * * @category Auto-Activation * @public */ async detectPersonas(userInput: string, options: Partial<InputContext> = {}): Promise<DetectionResults> { if (!this.config) { await this.initialize(); } if (!this.config) { throw new Error('Failed to initialize configuration'); } const analysis: Analysis = { userInput: userInput.toLowerCase(), keywords: this.extractKeywords(userInput), context: options || {}, files: options.files || [], command: options.command || '' }; const personaScores: Record<string, PersonaScore> = {}; // Initialize all personas with base scores for (const [personaName] of Object.entries(this.config.personas)) { personaScores[personaName] = { total: 0, breakdown: { keyword_matching: 0, context_analysis: 0, file_patterns: 0, user_history: 0 } }; } // Calculate scores for each factor this.calculateKeywordScores(analysis, personaScores); this.calculateContextScores(analysis, personaScores); this.calculateFilePatternScores(analysis, personaScores); this.calculateUserHistoryScores(analysis, personaScores); // Calculate final weighted scores for (const personaName of Object.keys(personaScores)) { const breakdown = personaScores[personaName].breakdown; personaScores[personaName].total = breakdown.keyword_matching * this.weights.keyword_matching + breakdown.context_analysis * this.weights.context_analysis + breakdown.file_patterns * this.weights.file_patterns + breakdown.user_history * this.weights.user_history; } // Filter and rank personas above threshold const threshold = this.config.auto_activation?.global_confidence_threshold || 0.7; const recommendations: PersonaRecommendation[] = Object.entries(personaScores) .filter(([_, score]) => score.total >= threshold) .sort((a, b) => b[1].total - a[1].total) .slice(0, this.config.auto_activation?.multi_persona_limit || 3) .map(([name, score]) => ({ persona: name, confidence: Math.round(score.total * 100) / 100, breakdown: score.breakdown, reasoning: this.generateReasoning(name, score, analysis) })); // Update session context this.sessionContext.recentCommands.push({ input: userInput, personas: recommendations.map(r => r.persona), timestamp: Date.now() }); // Keep only recent commands if (this.sessionContext.recentCommands.length > 10) { this.sessionContext.recentCommands = this.sessionContext.recentCommands.slice(-10); } return { recommendations, sessionContext: this.sessionContext, analysisDetails: analysis }; } /** * Calculate keyword matching scores */ private calculateKeywordScores(analysis: Analysis, personaScores: Record<string, PersonaScore>): void { if (!this.config) return; for (const [personaName, personaConfig] of Object.entries(this.config.personas)) { const keywords = personaConfig.auto_activation?.keywords || []; let matches = 0; const totalKeywords = keywords.length; for (const keyword of keywords) { if (analysis.userInput.includes(keyword.toLowerCase())) { matches++; } } if (totalKeywords > 0) { const baseScore = matches / totalKeywords; const confidenceWeight = personaConfig.auto_activation?.confidence_weight || 1.0; personaScores[personaName].breakdown.keyword_matching = baseScore * confidenceWeight; } } } /** * Calculate context analysis scores */ private calculateContextScores(analysis: Analysis, personaScores: Record<string, PersonaScore>): void { if (!this.config) return; for (const [personaName] of Object.entries(this.config.personas)) { let contextScore = 0; // Command-specific scoring if (analysis.command) { const commandMappings: Record<string, string[]> = { 'commit': ['scribe', 'security'], 'review': ['security', 'analyzer', 'qa', 'performance'], 'docs': ['scribe', 'mentor'], 'brainstorm': ['architect', 'frontend', 'backend'], 'analyze': ['analyzer', 'security', 'performance'], 'improve': ['refactorer', 'performance', 'security'] }; if (commandMappings[analysis.command]?.includes(personaName)) { contextScore += 0.5; } } // Project type alignment if (this.sessionContext.projectType) { const typeAlignments: Record<string, string[]> = { 'javascript': ['frontend', 'backend'], 'python': ['backend', 'analyzer'], 'rust': ['performance', 'backend'], 'ruby': ['backend', 'refactorer'] }; if (typeAlignments[this.sessionContext.projectType]?.includes(personaName)) { contextScore += 0.3; } } // Framework alignment for (const framework of this.sessionContext.frameworks || []) { if (framework === 'react' && personaName === 'frontend') contextScore += 0.2; if (framework === 'express' && personaName === 'backend') contextScore += 0.2; } personaScores[personaName].breakdown.context_analysis = Math.min(contextScore, 1.0); } } /** * Calculate file pattern scores */ private calculateFilePatternScores(analysis: Analysis, personaScores: Record<string, PersonaScore>): void { if (!this.config) return; for (const [personaName, personaConfig] of Object.entries(this.config.personas)) { const filePatterns = personaConfig.auto_activation?.file_patterns || []; let patternScore = 0; // Check files in current context for (const file of analysis.files) { for (const pattern of filePatterns) { if (this.matchesPattern(file, pattern)) { patternScore += 0.2; } } } // Check overall project file patterns const patterns = this.sessionContext.filePatterns; if (patterns) { if (personaName === 'frontend' && patterns.frontend > 10) patternScore += 0.3; if (personaName === 'backend' && patterns.backend > 10) patternScore += 0.3; if (personaName === 'devops' && patterns.config > 5) patternScore += 0.3; if (personaName === 'qa' && patterns.test > 5) patternScore += 0.3; if (personaName === 'scribe' && patterns.docs > 3) patternScore += 0.3; } personaScores[personaName].breakdown.file_patterns = Math.min(patternScore, 1.0); } } /** * Calculate user history scores */ private calculateUserHistoryScores(_analysis: Analysis, personaScores: Record<string, PersonaScore>): void { if (!this.config) return; // Simple user history scoring based on recent successful activations const recentPersonas = this.sessionContext.recentCommands .slice(-5) .flatMap(cmd => cmd.personas); const personaCounts: Record<string, number> = {}; for (const persona of recentPersonas) { personaCounts[persona] = (personaCounts[persona] || 0) + 1; } const maxCount = Math.max(...Object.values(personaCounts), 1); for (const [personaName] of Object.entries(this.config.personas)) { const count = personaCounts[personaName] || 0; personaScores[personaName].breakdown.user_history = count / maxCount * 0.5; } } /** * Generate human-readable reasoning for persona recommendation */ private generateReasoning(personaName: string, score: PersonaScore, _analysis: Analysis): string { const reasons: string[] = []; if (score.breakdown.keyword_matching > 0.3) { reasons.push(`Strong keyword alignment with ${personaName} domain`); } if (score.breakdown.context_analysis > 0.3) { reasons.push(`Project context matches ${personaName} expertise`); } if (score.breakdown.file_patterns > 0.3) { reasons.push(`File patterns indicate ${personaName} relevance`); } if (score.breakdown.user_history > 0.2) { reasons.push(`Recent usage history favors ${personaName}`); } return reasons.length > 0 ? reasons.join('; ') : `General alignment with ${personaName} capabilities`; } /** * Extract keywords from user input */ private extractKeywords(input: string): string[] { // Simple keyword extraction - could be enhanced with NLP const words = input.toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2); return [...new Set(words)]; } /** * Check if file matches pattern */ private matchesPattern(file: string, pattern: string): boolean { // Simple pattern matching - could use glob library for more complex patterns if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(file); } return file.includes(pattern); } /** * Check if file exists */ private async fileExists(filePath: string): Promise<boolean> { try { await fs.access(filePath); return true; } catch { return false; } } /** * Load user history from storage */ private async loadUserHistory(): Promise<void> { try { const historyPath = path.join(__dirname, '..', '.claude-buddy', 'persona-history.json'); const historyContent = await fs.readFile(historyPath, 'utf8'); const history = JSON.parse(historyContent); for (const [persona, data] of Object.entries(history)) { this.userHistory.set(persona, data as UserHistoryData); } } catch (error) { // No history file exists yet, that's okay } } /** * Save user history to storage */ private async saveUserHistory(): Promise<void> { try { const historyPath = path.join(__dirname, '..', '.claude-buddy', 'persona-history.json'); const historyDir = path.dirname(historyPath); // Ensure directory exists await fs.mkdir(historyDir, { recursive: true }); const historyObj: Record<string, UserHistoryData> = {}; for (const [persona, data] of this.userHistory.entries()) { historyObj[persona] = data; } await fs.writeFile(historyPath, JSON.stringify(historyObj, null, 2)); } catch (error) { console.error('Failed to save persona history:', error); } } /** * Record successful persona usage for learning */ recordPersonaUsage(personas: string[], userFeedback: 'positive' | 'negative' | 'neutral' = 'positive'): void { const timestamp = Date.now(); for (const persona of personas) { if (!this.userHistory.has(persona)) { this.userHistory.set(persona, { usageCount: 0, successfulUsage: 0, lastUsed: timestamp, contexts: [] }); } const history = this.userHistory.get(persona)!; history.usageCount++; history.lastUsed = timestamp; if (userFeedback === 'positive') { history.successfulUsage++; } this.userHistory.set(persona, history); } // Save updated history void this.saveUserHistory(); } } // Export both named and default for flexibility export { PersonaActivationEngine }; export default PersonaActivationEngine;