codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
744 lines (644 loc) • 21 kB
text/typescript
import * as fs from 'fs/promises';
import * as path from 'path';
import { homedir } from 'os';
import { logger } from '../logger.js';
import { SynthesisResult } from '../types.js';
export interface ProjectContext {
guidance: string;
preferences: VoicePreferences;
constraints: string[];
patterns: CodePattern[];
history: InteractionSummary[];
metadata: ProjectMetadata;
}
export interface VoicePreferences {
primary: string[];
secondary: string[];
disabled: string[];
customSettings: Record<string, any>;
}
export interface CodePattern {
name: string;
description: string;
pattern: string;
examples: string[];
category: 'architectural' | 'stylistic' | 'security' | 'performance';
}
export interface InteractionSummary {
timestamp: number;
prompt: string;
response: string;
voicesUsed: string[];
outcome: 'successful' | 'partial' | 'failed';
userFeedback?: 'positive' | 'negative' | 'neutral';
topics: string[];
}
export interface ProjectMetadata {
name: string;
type: 'web' | 'mobile' | 'desktop' | 'library' | 'tool' | 'other';
languages: string[];
frameworks: string[];
lastUpdated: number;
totalInteractions: number;
averageComplexity: number;
}
export interface ContextLayer {
level: 'global' | 'repo' | 'subfolder';
path: string;
content: ProjectContext;
priority: number;
}
export interface ContextHierarchy {
layers: ContextLayer[];
merged: ProjectContext;
conflicts: ContextConflict[];
}
export interface ContextConflict {
property: string;
layers: string[];
resolution: 'merge' | 'override' | 'manual';
value: any;
}
/**
* Project Memory System implementing hierarchical context management
* Inspired by Codex CLI's AGENTS.md pattern with intelligent merging
*/
export class ProjectMemorySystem {
private contextPath: string;
private globalContextPath: string;
private cache: Map<string, ContextHierarchy>;
private watchedPaths: Set<string>;
constructor(workspaceRoot: string) {
this.contextPath = path.join(workspaceRoot, '.codecrucible');
this.globalContextPath = path.join(homedir(), '.codecrucible');
this.cache = new Map();
this.watchedPaths = new Set();
logger.info('Project memory system initialized', {
contextPath: this.contextPath,
globalPath: this.globalContextPath,
});
}
/**
* Load and merge project context from hierarchy
*/
async loadProjectContext(currentPath?: string): Promise<ProjectContext> {
const hierarchy = await this.loadContextHierarchy(currentPath);
this.cache.set(currentPath || 'default', hierarchy);
return hierarchy.merged;
}
/**
* Save project context at appropriate level
*/
async saveProjectContext(
context: Partial<ProjectContext>,
level: 'global' | 'repo' | 'subfolder' = 'repo',
subfolderPath?: string
): Promise<void> {
let targetPath: string;
switch (level) {
case 'global':
targetPath = this.globalContextPath;
break;
case 'repo':
targetPath = this.contextPath;
break;
case 'subfolder':
if (!subfolderPath) {
throw new Error('Subfolder path required for subfolder context');
}
targetPath = path.join(subfolderPath, '.codecrucible');
break;
}
await this.ensureDirectoryExists(targetPath);
// Load existing context to merge
const existing = await this.loadContextFromPath(targetPath).catch(() =>
this.createDefaultContext()
);
const merged = this.mergeContexts([existing, context]);
// Save individual files
await this.saveContextFiles(targetPath, merged);
// Invalidate cache
this.cache.clear();
logger.info(`Saved project context at ${level} level`, { targetPath });
}
/**
* Store interaction in project history
*/
async storeInteraction(
prompt: string,
response: SynthesisResult,
context: ProjectContext,
userFeedback?: 'positive' | 'negative' | 'neutral'
): Promise<void> {
const interaction: InteractionSummary = {
timestamp: Date.now(),
prompt: prompt.substring(0, 200), // Truncate for storage
response: (response.synthesis || '').substring(0, 500), // Truncate for storage
voicesUsed: response.voicesUsed,
outcome:
(response.confidence || 0) > 0.7
? 'successful'
: (response.confidence || 0) > 0.4
? 'partial'
: 'failed',
userFeedback,
topics: this.extractTopics(prompt, response),
};
// Add to current context
context.history.unshift(interaction);
// Keep only last 50 interactions
context.history = context.history.slice(0, 50);
// Update metadata
context.metadata.totalInteractions++;
context.metadata.lastUpdated = Date.now();
context.metadata.averageComplexity = this.calculateAverageComplexity(context.history);
// Save updated context
await this.saveProjectContext(context, 'repo');
logger.debug('Stored interaction in project memory', {
outcome: interaction.outcome,
voicesUsed: interaction.voicesUsed.length,
topics: interaction.topics.length,
});
}
/**
* Search interaction history
*/
async searchHistory(
query: string,
options: {
limit?: number;
timeRange?: { start: number; end: number };
outcome?: 'successful' | 'partial' | 'failed';
voices?: string[];
} = {}
): Promise<InteractionSummary[]> {
const context = await this.loadProjectContext();
const { limit = 10, timeRange, outcome, voices } = options;
let results = context.history;
// Filter by time range
if (timeRange) {
results = results.filter(
interaction =>
interaction.timestamp >= timeRange.start && interaction.timestamp <= timeRange.end
);
}
// Filter by outcome
if (outcome) {
results = results.filter(interaction => interaction.outcome === outcome);
}
// Filter by voices
if (voices && voices.length > 0) {
results = results.filter(interaction =>
voices.some(voice => interaction.voicesUsed.includes(voice))
);
}
// Search in content
if (query.trim()) {
const searchTerms = query.toLowerCase().split(' ');
results = results.filter(interaction => {
const searchContent =
`${interaction.prompt} ${interaction.response} ${interaction.topics.join(' ')}`.toLowerCase();
return searchTerms.some(term => searchContent.includes(term));
});
}
// Sort by relevance (timestamp for now, could be enhanced with scoring)
results.sort((a, b) => b.timestamp - a.timestamp);
return results.slice(0, limit);
}
/**
* Get context recommendations for current task
*/
async getContextRecommendations(currentPrompt: string): Promise<{
relevantPatterns: CodePattern[];
similarInteractions: InteractionSummary[];
suggestedVoices: string[];
constraints: string[];
}> {
const context = await this.loadProjectContext();
const currentTopics = this.extractTopicsFromText(currentPrompt);
// Find relevant patterns
const relevantPatterns = context.patterns.filter(pattern => {
const patternTerms = `${pattern.name} ${pattern.description}`.toLowerCase();
return currentTopics.some(topic => patternTerms.includes(topic.toLowerCase()));
});
// Find similar interactions
const similarInteractions = context.history
.filter(interaction => {
const commonTopics = interaction.topics.filter(topic =>
currentTopics.some(current => current.toLowerCase().includes(topic.toLowerCase()))
);
return commonTopics.length > 0;
})
.slice(0, 5);
// Suggest voices based on successful interactions
const voiceUsageMap = new Map<string, number>();
similarInteractions
.filter(interaction => interaction.outcome === 'successful')
.forEach(interaction => {
interaction.voicesUsed.forEach(voice => {
voiceUsageMap.set(voice, (voiceUsageMap.get(voice) || 0) + 1);
});
});
const suggestedVoices = Array.from(voiceUsageMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([voice]) => voice);
return {
relevantPatterns,
similarInteractions,
suggestedVoices,
constraints: context.constraints,
};
}
/**
* Load context hierarchy from global -> repo -> subfolder
*/
private async loadContextHierarchy(currentPath?: string): Promise<ContextHierarchy> {
const layers: ContextLayer[] = [];
// Global context
try {
const globalContext = await this.loadContextFromPath(this.globalContextPath);
layers.push({
level: 'global',
path: this.globalContextPath,
content: globalContext,
priority: 1,
});
} catch (error) {
logger.debug('No global context found, using defaults');
}
// Repository context
try {
const repoContext = await this.loadContextFromPath(this.contextPath);
layers.push({
level: 'repo',
path: this.contextPath,
content: repoContext,
priority: 2,
});
} catch (error) {
logger.debug('No repository context found, using defaults');
}
// Subfolder context (if in subdirectory)
if (currentPath && currentPath !== path.dirname(this.contextPath)) {
const subfolderContextPath = path.join(currentPath, '.codecrucible');
try {
const subfolderContext = await this.loadContextFromPath(subfolderContextPath);
layers.push({
level: 'subfolder',
path: subfolderContextPath,
content: subfolderContext,
priority: 3,
});
} catch (error) {
logger.debug('No subfolder context found');
}
}
// If no layers found, create default
if (layers.length === 0) {
const defaultContext = this.createDefaultContext();
layers.push({
level: 'repo',
path: this.contextPath,
content: defaultContext,
priority: 2,
});
}
// Merge contexts
const merged = this.mergeContextLayers(layers);
const conflicts = this.detectConflicts(layers);
return {
layers,
merged,
conflicts,
};
}
/**
* Load context from a specific path
*/
private async loadContextFromPath(contextPath: string): Promise<ProjectContext> {
const contextFile = path.join(contextPath, 'context.md');
const voicesFile = path.join(contextPath, 'voices.yaml');
const historyFile = path.join(contextPath, 'history.json');
const patternsFile = path.join(contextPath, 'patterns.json');
let guidance = '';
let preferences: VoicePreferences = {
primary: [],
secondary: [],
disabled: [],
customSettings: {},
};
let history: InteractionSummary[] = [];
let patterns: CodePattern[] = [];
// Load guidance
try {
guidance = await fs.readFile(contextFile, 'utf-8');
} catch (error) {
logger.debug(`No context file found at ${contextFile}`);
}
// Load voice preferences
try {
const voicesContent = await fs.readFile(voicesFile, 'utf-8');
const yaml = await import('js-yaml');
preferences = yaml.load(voicesContent) as VoicePreferences;
} catch (error) {
logger.debug(`No voices file found at ${voicesFile}`);
}
// Load history
try {
const historyContent = await fs.readFile(historyFile, 'utf-8');
history = JSON.parse(historyContent);
} catch (error) {
logger.debug(`No history file found at ${historyFile}`);
}
// Load patterns
try {
const patternsContent = await fs.readFile(patternsFile, 'utf-8');
patterns = JSON.parse(patternsContent);
} catch (error) {
logger.debug(`No patterns file found at ${patternsFile}`);
}
return {
guidance,
preferences,
constraints: [],
patterns,
history,
metadata: {
name: path.basename(path.dirname(contextPath)),
type: 'other',
languages: [],
frameworks: [],
lastUpdated: Date.now(),
totalInteractions: history.length,
averageComplexity: this.calculateAverageComplexity(history),
},
};
}
/**
* Save context files to disk
*/
private async saveContextFiles(contextPath: string, context: ProjectContext): Promise<void> {
// Save guidance
const contextFile = path.join(contextPath, 'context.md');
await fs.writeFile(
contextFile,
context.guidance || '# Project Context\n\nAdd project-specific guidance here.\n'
);
// Save voice preferences
const voicesFile = path.join(contextPath, 'voices.yaml');
const yaml = await import('js-yaml');
await fs.writeFile(voicesFile, yaml.dump(context.preferences));
// Save history (last 50 interactions only)
const historyFile = path.join(contextPath, 'history.json');
const recentHistory = context.history.slice(0, 50);
await fs.writeFile(historyFile, JSON.stringify(recentHistory, null, 2));
// Save patterns
const patternsFile = path.join(contextPath, 'patterns.json');
await fs.writeFile(patternsFile, JSON.stringify(context.patterns, null, 2));
// Save metadata
const metadataFile = path.join(contextPath, 'metadata.json');
await fs.writeFile(metadataFile, JSON.stringify(context.metadata, null, 2));
}
/**
* Merge multiple contexts with priority-based resolution
*/
private mergeContextLayers(layers: ContextLayer[]): ProjectContext {
const contexts = layers
.sort((a, b) => a.priority - b.priority) // Lower priority first
.map(layer => layer.content);
return this.mergeContexts(contexts);
}
/**
* Merge contexts with intelligent conflict resolution
*/
private mergeContexts(contexts: Partial<ProjectContext>[]): ProjectContext {
const merged: ProjectContext = this.createDefaultContext();
for (const context of contexts) {
if (!context) continue;
// Merge guidance (concatenate with separators)
if (context.guidance) {
merged.guidance = merged.guidance
? `${merged.guidance}\n\n---\n\n${context.guidance}`
: context.guidance;
}
// Merge voice preferences (later preferences override)
if (context.preferences) {
merged.preferences = {
primary: context.preferences.primary || merged.preferences.primary,
secondary: context.preferences.secondary || merged.preferences.secondary,
disabled: [...merged.preferences.disabled, ...(context.preferences.disabled || [])],
customSettings: {
...merged.preferences.customSettings,
...context.preferences.customSettings,
},
};
}
// Merge constraints (combine unique)
if (context.constraints) {
merged.constraints = [...new Set([...merged.constraints, ...context.constraints])];
}
// Merge patterns (combine unique by name)
if (context.patterns) {
const existingNames = new Set(merged.patterns.map(p => p.name));
const newPatterns = context.patterns.filter(p => !existingNames.has(p.name));
merged.patterns = [...merged.patterns, ...newPatterns];
}
// Merge history (combine and sort by timestamp)
if (context.history) {
merged.history = [...merged.history, ...context.history]
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 100); // Keep only most recent 100
}
// Merge metadata (later values override)
if (context.metadata) {
merged.metadata = { ...merged.metadata, ...context.metadata };
}
}
return merged;
}
/**
* Detect conflicts between context layers
*/
private detectConflicts(layers: ContextLayer[]): ContextConflict[] {
const conflicts: ContextConflict[] = [];
// Check for voice preference conflicts
const voiceConflicts = this.detectVoiceConflicts(layers);
conflicts.push(...voiceConflicts);
return conflicts;
}
/**
* Detect voice preference conflicts
*/
private detectVoiceConflicts(layers: ContextLayer[]): ContextConflict[] {
const conflicts: ContextConflict[] = [];
// Check if same voice is in both primary and disabled
const allPrimary = new Set<string>();
const allDisabled = new Set<string>();
layers.forEach(layer => {
if (layer.content.preferences) {
layer.content.preferences.primary?.forEach(voice => allPrimary.add(voice));
layer.content.preferences.disabled?.forEach(voice => allDisabled.add(voice));
}
});
const conflictingVoices = [...allPrimary].filter(voice => allDisabled.has(voice));
if (conflictingVoices.length > 0) {
conflicts.push({
property: 'voice_preferences',
layers: layers.map(l => l.level),
resolution: 'override',
value: conflictingVoices,
});
}
return conflicts;
}
/**
* Create default project context
*/
private createDefaultContext(): ProjectContext {
return {
guidance: '# Project Context\n\nAdd project-specific guidance here.',
preferences: {
primary: ['explorer', 'maintainer'],
secondary: ['analyzer', 'developer'],
disabled: [],
customSettings: {},
},
constraints: [],
patterns: [],
history: [],
metadata: {
name: 'Unknown',
type: 'other',
languages: [],
frameworks: [],
lastUpdated: Date.now(),
totalInteractions: 0,
averageComplexity: 0,
},
};
}
/**
* Extract topics from prompt and response
*/
private extractTopics(prompt: string, response: SynthesisResult): string[] {
const text = `${prompt} ${response.synthesis}`;
return this.extractTopicsFromText(text);
}
/**
* Extract topics from text using simple keyword matching
*/
private extractTopicsFromText(text: string): string[] {
const topics = new Set<string>();
const lowercaseText = text.toLowerCase();
// Programming languages
const languages = [
'javascript',
'typescript',
'python',
'java',
'rust',
'go',
'cpp',
'c#',
'php',
'ruby',
];
languages.forEach(lang => {
if (lowercaseText.includes(lang)) topics.add(lang);
});
// Frameworks
const frameworks = [
'react',
'vue',
'angular',
'express',
'fastapi',
'django',
'spring',
'flutter',
'nextjs',
];
frameworks.forEach(framework => {
if (lowercaseText.includes(framework)) topics.add(framework);
});
// Technologies
const technologies = [
'database',
'api',
'rest',
'graphql',
'docker',
'kubernetes',
'aws',
'git',
'test',
'security',
];
technologies.forEach(tech => {
if (lowercaseText.includes(tech)) topics.add(tech);
});
// Activity types
const activities = [
'debug',
'refactor',
'optimize',
'implement',
'design',
'review',
'fix',
'create',
'update',
];
activities.forEach(activity => {
if (lowercaseText.includes(activity)) topics.add(activity);
});
return Array.from(topics);
}
/**
* Calculate average complexity from interaction history
*/
private calculateAverageComplexity(history: InteractionSummary[]): number {
if (history.length === 0) return 0;
const complexitySum = history.reduce((sum, interaction) => {
// Simple complexity scoring based on various factors
let complexity = 0;
// Prompt length factor
complexity += Math.min(interaction.prompt.length / 100, 5);
// Response length factor
complexity += Math.min(interaction.response.length / 200, 5);
// Number of voices used
complexity += interaction.voicesUsed.length;
// Topic count
complexity += interaction.topics.length;
return sum + Math.min(complexity, 10); // Cap at 10
}, 0);
return complexitySum / history.length;
}
/**
* Ensure directory exists
*/
private async ensureDirectoryExists(dirPath: string): Promise<void> {
try {
await fs.access(dirPath);
} catch (error) {
await fs.mkdir(dirPath, { recursive: true });
}
}
/**
* Cleanup resources and save pending data
*/
async dispose(): Promise<void> {
// Save any cached contexts
for (const [key, hierarchy] of this.cache.entries()) {
try {
await this.saveProjectContext(hierarchy.merged, 'repo');
} catch (error) {
logger.warn(`Failed to save cached context ${key}:`, error);
}
}
this.cache.clear();
this.watchedPaths.clear();
logger.info('Project memory system disposed');
}
}