UNPKG

claude-buddy

Version:

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

425 lines (424 loc) 17.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PersonaActivationEngine = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); class PersonaActivationEngine { config = null; userHistory = new Map(); sessionContext; configPath; weights; constructor(configPath) { this.sessionContext = { activePersonas: [], filePatterns: { frontend: 0, backend: 0, config: 0, test: 0, docs: 0 }, projectType: null, recentCommands: [], }; this.configPath = configPath || path_1.default.join(__dirname, 'config', 'personas-config.json'); this.weights = { keyword_matching: 0.3, context_analysis: 0.4, file_patterns: 0.2, user_history: 0.1 }; } async initialize() { try { const configContent = await fs_1.promises.readFile(this.configPath, 'utf8'); this.config = JSON.parse(configContent); await this.loadUserHistory(); 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; } } async analyzeProjectContext() { try { const projectRoot = process.cwd(); const packageJsonPath = path_1.default.join(projectRoot, 'package.json'); const pyprojectPath = path_1.default.join(projectRoot, 'pyproject.toml'); const cargoPath = path_1.default.join(projectRoot, 'Cargo.toml'); const gemfilePath = path_1.default.join(projectRoot, 'Gemfile'); const projectAnalysis = { type: 'unknown', frameworks: [], features: [], complexity: 0.5 }; if (await this.fileExists(packageJsonPath)) { const packageContent = await fs_1.promises.readFile(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageContent); projectAnalysis.type = 'javascript'; 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'); 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'); } if (await this.fileExists(pyprojectPath)) { projectAnalysis.type = 'python'; } if (await this.fileExists(cargoPath)) { projectAnalysis.type = 'rust'; } if (await this.fileExists(gemfilePath)) { projectAnalysis.type = 'ruby'; } 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); } } async detectFilePatterns() { try { const patterns = { frontend: 0, backend: 0, config: 0, test: 0, docs: 0 }; const files = await this.getProjectFiles(); for (const file of files.slice(0, 100)) { const ext = path_1.default.extname(file).toLowerCase(); const basename = path_1.default.basename(file).toLowerCase(); if (['.jsx', '.tsx', '.vue', '.css', '.scss', '.sass', '.html'].includes(ext)) { patterns.frontend++; } if (['.py', '.js', '.ts', '.go', '.java', '.rb', '.php'].includes(ext) && (file.includes('/api/') || file.includes('/server/') || file.includes('/backend/'))) { patterns.backend++; } if (basename.includes('config') || basename.includes('.env') || ['.yml', '.yaml', '.json', '.toml'].includes(ext)) { patterns.config++; } if (basename.includes('test') || basename.includes('spec') || file.includes('/tests/') || file.includes('/__tests__/')) { patterns.test++; } if (['.md', '.rst', '.txt'].includes(ext) || file.includes('/docs/')) { patterns.docs++; } } this.sessionContext.filePatterns = patterns; } catch (error) { console.error('Error detecting file patterns:', error); } } async getProjectFiles() { const files = []; const maxFiles = 500; const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv']; const walkDir = async (dir, depth = 0) => { if (depth > 5 || files.length >= maxFiles) return; try { const entries = await fs_1.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { if (files.length >= maxFiles) break; const fullPath = path_1.default.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) { } }; await walkDir(process.cwd()); return files; } async detectPersonas(userInput, options = {}) { if (!this.config) { await this.initialize(); } if (!this.config) { throw new Error('Failed to initialize configuration'); } const analysis = { userInput: userInput.toLowerCase(), keywords: this.extractKeywords(userInput), context: options || {}, files: options.files || [], command: options.command || '' }; const personaScores = {}; 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 } }; } this.calculateKeywordScores(analysis, personaScores); this.calculateContextScores(analysis, personaScores); this.calculateFilePatternScores(analysis, personaScores); this.calculateUserHistoryScores(analysis, personaScores); 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; } const threshold = this.config.auto_activation?.global_confidence_threshold || 0.7; const recommendations = 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) })); this.sessionContext.recentCommands.push({ input: userInput, personas: recommendations.map(r => r.persona), timestamp: Date.now() }); if (this.sessionContext.recentCommands.length > 10) { this.sessionContext.recentCommands = this.sessionContext.recentCommands.slice(-10); } return { recommendations, sessionContext: this.sessionContext, analysisDetails: analysis }; } calculateKeywordScores(analysis, personaScores) { 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; } } } calculateContextScores(analysis, personaScores) { if (!this.config) return; for (const [personaName] of Object.entries(this.config.personas)) { let contextScore = 0; if (analysis.command) { const commandMappings = { '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; } } if (this.sessionContext.projectType) { const typeAlignments = { 'javascript': ['frontend', 'backend'], 'python': ['backend', 'analyzer'], 'rust': ['performance', 'backend'], 'ruby': ['backend', 'refactorer'] }; if (typeAlignments[this.sessionContext.projectType]?.includes(personaName)) { contextScore += 0.3; } } 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); } } calculateFilePatternScores(analysis, personaScores) { if (!this.config) return; for (const [personaName, personaConfig] of Object.entries(this.config.personas)) { const filePatterns = personaConfig.auto_activation?.file_patterns || []; let patternScore = 0; for (const file of analysis.files) { for (const pattern of filePatterns) { if (this.matchesPattern(file, pattern)) { patternScore += 0.2; } } } 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); } } calculateUserHistoryScores(_analysis, personaScores) { if (!this.config) return; const recentPersonas = this.sessionContext.recentCommands .slice(-5) .flatMap(cmd => cmd.personas); const personaCounts = {}; 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; } } generateReasoning(personaName, score, _analysis) { const reasons = []; 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`; } extractKeywords(input) { const words = input.toLowerCase() .replace(/[^\w\s]/g, ' ') .split(/\s+/) .filter(word => word.length > 2); return [...new Set(words)]; } matchesPattern(file, pattern) { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(file); } return file.includes(pattern); } async fileExists(filePath) { try { await fs_1.promises.access(filePath); return true; } catch { return false; } } async loadUserHistory() { try { const historyPath = path_1.default.join(__dirname, '..', '.claude-buddy', 'persona-history.json'); const historyContent = await fs_1.promises.readFile(historyPath, 'utf8'); const history = JSON.parse(historyContent); for (const [persona, data] of Object.entries(history)) { this.userHistory.set(persona, data); } } catch (error) { } } async saveUserHistory() { try { const historyPath = path_1.default.join(__dirname, '..', '.claude-buddy', 'persona-history.json'); const historyDir = path_1.default.dirname(historyPath); await fs_1.promises.mkdir(historyDir, { recursive: true }); const historyObj = {}; for (const [persona, data] of this.userHistory.entries()) { historyObj[persona] = data; } await fs_1.promises.writeFile(historyPath, JSON.stringify(historyObj, null, 2)); } catch (error) { console.error('Failed to save persona history:', error); } } recordPersonaUsage(personas, userFeedback = 'positive') { 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); } void this.saveUserHistory(); } } exports.PersonaActivationEngine = PersonaActivationEngine; exports.default = PersonaActivationEngine;