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
JavaScript
"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;