UNPKG

@wiber/ccs

Version:

Turn any codebase into an AI-aware environment. Claude launches with full context, asks smart questions, and gets better with every interaction.

498 lines (422 loc) 14.2 kB
const fs = require('fs').promises; const path = require('path'); const { glob } = require('glob'); /** * Context Database - Unified context management * * Eliminates the need to hunt for logs, docs, and configuration * Provides schema-driven context loading with intelligent caching */ class ContextDatabase { constructor(projectPath, config = {}) { this.projectPath = projectPath; this.config = { cacheTimeout: config.cacheTimeout || 300000, // 5 minutes maxLogLines: config.maxLogLines || 1000, includeGitInfo: config.includeGitInfo !== false, ...config }; this.cache = new Map(); this.schema = null; this.projectInfo = null; } async initialize() { // Create context directory await this.ensureDirectory(path.join(this.projectPath, '.ccs')); // Load configuration await this.loadConfiguration(); // Load project schema and basic info this.schema = await this.loadContextSchema(); this.projectInfo = await this.loadProjectInfo(); console.log(`📚 Context database initialized for: ${this.projectInfo.name}`); } async loadConfiguration() { try { const configPath = path.join(this.projectPath, '.ccs-config.json'); const configContent = await fs.readFile(configPath, 'utf-8'); const fileConfig = JSON.parse(configContent); // Merge with defaults this.config = { ...this.config, ...fileConfig, context: { logPaths: ['logs/**/*.log'], documentationPaths: ['docs/', 'README.md', 'CLAUDE.md'], ...fileConfig.context } }; } catch (error) { // Use defaults if no config file this.config.context = { logPaths: ['logs/**/*.log'], documentationPaths: ['docs/', 'README.md', 'CLAUDE.md'] }; } } /** * Main context loading method - returns unified context for operation type */ async loadContext(operationType = 'general') { const cacheKey = `${operationType}-${await this.getContextHash()}`; if (this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (Date.now() - cached.timestamp < this.config.cacheTimeout) { console.log(`📋 Using cached context for ${operationType}`); return cached.data; } } console.log(`🔍 Loading fresh context for ${operationType}...`); const context = { // Core project information (always included) project: this.projectInfo, timestamp: new Date().toISOString(), operationType, // Schema-driven context loading ...await this.loadSchemaContext(operationType), // Dynamic context based on operation type ...await this.loadOperationSpecificContext(operationType), // Recent activity context recentActivity: await this.loadRecentActivity(), // Available tools and capabilities capabilities: await this.getAvailableCapabilities() }; // Cache the result this.cache.set(cacheKey, { data: context, timestamp: Date.now() }); return context; } /** * Load project information from CLAUDE.md and package.json */ async loadProjectInfo() { if (this.projectInfo) return this.projectInfo; const info = { name: 'Unknown Project', type: 'generic', version: '1.0.0', architecture: {}, commands: {}, criticalPaths: [], logLocations: [], documentationFolders: [] }; try { // Load from package.json const packageJson = await this.readProjectFile('package.json'); if (packageJson) { const pkg = JSON.parse(packageJson); info.name = pkg.name || info.name; info.version = pkg.version || info.version; info.commands = pkg.scripts || {}; } // Load from CLAUDE.md for enhanced context const claudeMd = await this.readProjectFile('CLAUDE.md'); if (claudeMd) { info.type = this.detectProjectType(claudeMd); info.architecture = this.extractArchitecture(claudeMd); info.criticalPaths = this.extractCriticalPaths(claudeMd); info.logLocations = this.extractLogLocations(claudeMd); info.documentationFolders = this.extractDocumentationFolders(claudeMd); } // Auto-detect common patterns if not specified if (info.logLocations.length === 0) { info.logLocations = await this.autoDetectLogLocations(); } if (info.documentationFolders.length === 0) { info.documentationFolders = await this.autoDetectDocumentationFolders(); } } catch (error) { console.warn('⚠️ Could not load complete project info:', error.message); } this.projectInfo = info; return info; } /** * Auto-detect log file locations */ async autoDetectLogLocations() { const patterns = [ 'logs/**/*.log', 'logs/**/*.txt', '*.log', 'var/log/**/*.log', '.vercel/output/**/*.log' ]; const locations = []; for (const pattern of patterns) { try { const files = await glob(pattern, { cwd: this.projectPath }); locations.push(...files); } catch (error) { // Ignore glob errors } } return [...new Set(locations)]; // Remove duplicates } /** * Auto-detect documentation folders */ async autoDetectDocumentationFolders() { const commonFolders = [ 'docs', 'documentation', 'doc', 'README.md', 'wiki', 'guides' ]; const folders = []; for (const folder of commonFolders) { try { const fullPath = path.join(this.projectPath, folder); const stat = await fs.stat(fullPath); if (stat.isDirectory()) { folders.push(folder); } else if (stat.isFile() && folder.endsWith('.md')) { folders.push(folder); } } catch (error) { // Folder doesn't exist, skip } } return folders; } /** * Load operation-specific context */ async loadOperationSpecificContext(operationType) { switch (operationType) { case 'monitor': case 'analyze-logs': return await this.loadMonitoringContext(); case 'plan': case 'business-planning': return await this.loadPlanningContext(); default: return await this.loadGeneralContext(); } } async loadMonitoringContext() { return { logs: await this.loadRecentLogs(), apiEndpoints: await this.extractApiEndpoints(), performanceMetrics: await this.loadPerformanceHistory(), errorPatterns: await this.loadErrorPatterns() }; } async loadPlanningContext() { return { businessDocuments: await this.loadBusinessDocuments(), strategicPlans: await this.loadStrategicDocuments(), marketAnalysis: await this.loadMarketDocuments(), previousPlans: await this.loadPreviousPlanningResults() }; } async loadGeneralContext() { return { recentChanges: await this.loadRecentChanges(), systemHealth: await this.getSystemHealthSnapshot() }; } /** * Load recent logs from detected log locations */ async loadRecentLogs() { const logs = {}; const logLocations = this.projectInfo?.logLocations || []; for (const location of logLocations) { try { const fullPath = path.join(this.projectPath, location); const content = await fs.readFile(fullPath, 'utf-8'); // Get last N lines const lines = content.split('\n'); const recentLines = lines.slice(-this.config.maxLogLines); logs[location] = { path: location, lines: recentLines.length, content: recentLines.join('\n'), lastModified: (await fs.stat(fullPath)).mtime }; } catch (error) { // Log file might not exist or be readable console.warn(`⚠️ Could not read log file: ${location}`); } } return logs; } /** * Load business documents from documentation folders */ async loadBusinessDocuments() { const docs = {}; const docFolders = this.projectInfo?.documentationFolders || []; for (const folder of docFolders) { try { const pattern = path.join(this.projectPath, folder, '**/*.md'); const files = await glob(pattern); for (const file of files) { const relativePath = path.relative(this.projectPath, file); const content = await fs.readFile(file, 'utf-8'); docs[relativePath] = { path: relativePath, size: content.length, content: content.slice(0, 2000), // First 2000 chars for context lastModified: (await fs.stat(file)).mtime }; } } catch (error) { console.warn(`⚠️ Could not load documents from: ${folder}`); } } return docs; } /** * Store execution results for future context */ async storeExecutionResult(operationType, result) { const resultsPath = path.join(this.projectPath, '.claude-context', 'results'); await this.ensureDirectory(resultsPath); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${operationType}-${timestamp}.json`; const filepath = path.join(resultsPath, filename); await fs.writeFile(filepath, JSON.stringify({ operationType, timestamp: new Date().toISOString(), result, context: { projectName: this.projectInfo?.name, version: this.projectInfo?.version } }, null, 2)); // Clean up old results (keep last 10) await this.cleanupOldResults(resultsPath, operationType); } // Utility methods async readProjectFile(filename) { try { const filepath = path.join(this.projectPath, filename); return await fs.readFile(filepath, 'utf-8'); } catch (error) { return null; } } async ensureDirectory(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); } catch (error) { // Directory might already exist } } async getContextHash() { // Simple hash based on file modification times const files = ['CLAUDE.md', 'package.json']; let hash = ''; for (const file of files) { try { const filepath = path.join(this.projectPath, file); const stat = await fs.stat(filepath); hash += stat.mtime.getTime(); } catch (error) { // File doesn't exist } } return hash || 'no-context'; } // Parsing methods for CLAUDE.md detectProjectType(claudeMd) { if (claudeMd.includes('Next.js') || claudeMd.includes('React')) return 'nextjs'; if (claudeMd.includes('Express') || claudeMd.includes('Node.js')) return 'nodejs'; if (claudeMd.includes('Python') || claudeMd.includes('Django')) return 'python'; return 'generic'; } extractLogLocations(claudeMd) { const matches = claudeMd.match(/logs?[:/]\s*([^\n]+)/gi) || []; return matches.map(match => match.split(/[:\s]+/).pop().trim()); } extractDocumentationFolders(claudeMd) { const matches = claudeMd.match(/docs?[:/]\s*([^\n]+)/gi) || []; return matches.map(match => match.split(/[:\s]+/).pop().trim()); } extractCriticalPaths(claudeMd) { // Look for sections about critical paths, API routes, etc. const sections = claudeMd.match(/## Critical.*/gi) || []; return sections; } extractArchitecture(claudeMd) { // Extract architecture information return { framework: this.detectProjectType(claudeMd), database: claudeMd.includes('Supabase') ? 'supabase' : 'unknown', deployment: claudeMd.includes('Vercel') ? 'vercel' : 'unknown' }; } // Additional helper methods async loadContextSchema() { return {}; } async loadSchemaContext() { return {}; } async loadRecentActivity() { return {}; } async getAvailableCapabilities() { return []; } async extractApiEndpoints() { return []; } async loadPerformanceHistory() { return {}; } async loadErrorPatterns() { return []; } async loadStrategicDocuments() { return {}; } async loadMarketDocuments() { return {}; } async loadPreviousPlanningResults() { return []; } async loadRecentChanges() { return []; } async getSystemHealthSnapshot() { return {}; } async cleanupOldResults() { return; } // Test helper methods async discoverLogs() { return await this.autoDetectLogLocations(); } async loadDocumentation() { const docs = []; const docFolders = this.config.context?.documentationPaths || []; for (const folder of docFolders) { try { const fullPath = path.join(this.projectPath, folder); const stat = await fs.stat(fullPath); if (stat.isFile()) { docs.push({ path: folder, type: 'file' }); } else if (stat.isDirectory()) { const files = await glob(path.join(folder, '**/*.md'), { cwd: this.projectPath }); files.forEach(file => docs.push({ path: file, type: 'file' })); } } catch (error) { // Skip missing files/directories } } return docs; } async getRecentActivity(hours = 24) { const activity = []; const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000); try { // Get recent file modifications const files = await glob('**/*', { cwd: this.projectPath, ignore: ['node_modules/**', '.git/**', '.ccs/**'] }); for (const file of files.slice(0, 50)) { // Limit for performance try { const fullPath = path.join(this.projectPath, file); const stat = await fs.stat(fullPath); if (stat.mtime > cutoff) { activity.push({ file, modified: stat.mtime, type: 'file_change' }); } } catch (error) { // Skip inaccessible files } } } catch (error) { // Return empty activity if scan fails } return activity.sort((a, b) => b.modified - a.modified); } } module.exports = ContextDatabase;