#!/usr/bin/env node /** * Memory Service - NocoDB + Critic Integration * * Provides: * - Structured memory storage (preferences, episodes, decisions) * - Correction store (self-correcting patterns) * - Validation logging */ const axios = require('axios'); class MemoryService { constructor(config = {}) { this.nocodbUrl = config.nocodbUrl || 'http://192.168.25.5:8080'; this.nocodbToken = config.nocodbToken || process.env.NOCODB_TOKEN; this.projectId = config.projectId || 'default'; // Table mappings (NocoDB table IDs) this.tables = { corrections: config.correctionsTable || 'corrections', preferences: config.preferencesTable || 'memory_preferences', episodes: config.episodesTable || 'memory_episodes', decisions: config.decisionsTable || 'memory_decisions', validation: config.validationTable || 'validation_runs' }; } /** * Get HTTP client with auth */ getClient() { return axios.create({ baseURL: `${this.nocodbUrl}/api/v2/tables`, headers: { 'xc-token': this.nocodbToken, 'Content-Type': 'application/json' } }); } // ============================================ // CORRECTIONS API // ============================================ /** * Load all active corrections */ async loadCorrections() { const client = this.getClient(); const response = await client.get(`/${this.tables.corrections}/records`, { params: { where: `(blocking,eq,true)`, limit: 1000 } }); return response.data.list; } /** * Store new correction */ async storeCorrection(correction) { const client = this.getClient(); const payload = { id: correction.id, pattern: correction.pattern, exclude_pattern: correction.excludePattern || null, severity: correction.severity, message: correction.message, suggestion: correction.suggestion, blocking: correction.blocking !== false, auto_detected: correction.autoDetected || true, manual_entry: correction.manualEntry || false, hit_count: 0, context: correction.context || 'unknown', created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; const response = await client.post( `/${this.tables.corrections}/records`, payload ); return response.data; } /** * Increment hit count for correction */ async incrementCorrectionHit(correctionId) { const client = this.getClient(); // Get current hit count const current = await client.get( `/${this.tables.corrections}/records/${correctionId}` ); const newCount = (current.data.hit_count || 0) + 1; await client.patch( `/${this.tables.corrections}/records/${correctionId}`, { hit_count: newCount, last_triggered: new Date().toISOString(), updated_at: new Date().toISOString() } ); return newCount; } // ============================================ // PREFERENCES API // ============================================ /** * Get preferences by category */ async getPreferences(category, minImportance = 5) { const client = this.getClient(); const response = await client.get(`/${this.tables.preferences}/records`, { params: { where: `(category,eq,${category})~and(importance,gte,${minImportance})`, sort: '-importance,-access_count' } }); return response.data.list; } /** * Store preference */ async storePreference(pref) { const client = this.getClient(); const payload = { id: pref.id, category: pref.category, key: pref.key, value: pref.value, importance: pref.importance || 5, confidence: pref.confidence || 1.0, tags: JSON.stringify(pref.tags || []), confirmed_count: pref.confirmedCount || 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; const response = await client.post( `/${this.tables.preferences}/records`, payload ); return response.data; } // ============================================ // EPISODES API // ============================================ /** * Store episodic memory */ async storeEpisode(episode) { const client = this.getClient(); const payload = { id: episode.id, date: episode.date || new Date().toISOString().split('T')[0], summary: episode.summary, details: episode.details, project: episode.project, outcomes: JSON.stringify(episode.outcomes || []), corrections_triggered: JSON.stringify(episode.correctionsTriggered || []), created_at: new Date().toISOString() }; const response = await client.post( `/${this.tables.episodes}/records`, payload ); return response.data; } // ============================================ // DECISIONS API // ============================================ /** * Store decision */ async storeDecision(decision) { const client = this.getClient(); const payload = { id: decision.id, date: decision.date || new Date().toISOString().split('T')[0], project: decision.project, decision: decision.decision, alternatives: JSON.stringify(decision.alternatives || []), rationale: decision.rationale, status: decision.status || 'active', created_at: new Date().toISOString() }; const response = await client.post( `/${this.tables.decisions}/records`, payload ); return response.data; } // ============================================ // VALIDATION LOGGING // ============================================ /** * Log validation run */ async logValidation(run) { const client = this.getClient(); const payload = { id: run.id, session_id: run.sessionId, timestamp: new Date().toISOString(), input_length: run.inputLength, output_length: run.outputLength, violations_found: run.violationsFound || 0, new_patterns_detected: run.newPatternsDetected || 0, corrections_auto_stored: run.correctionsAutoStored || 0, processing_time_ms: run.processingTimeMs, blocked: run.blocked || false, workflow: run.workflow }; const response = await client.post( `/${this.tables.validation}/records`, payload ); return response.data; } /** * Get validation statistics */ async getValidationStats(days = 7) { const client = this.getClient(); const response = await client.get(`/${this.tables.validation}/records`, { params: { where: `(timestamp,gte,${days} days ago})`, fields: 'blocked,violations_found,corrections_auto_stored' } }); const runs = response.data.list; return { totalRuns: runs.length, blockedCount: runs.filter(r => r.blocked).length, totalViolations: runs.reduce((sum, r) => sum + (r.violations_found || 0), 0), totalAutoCorrections: runs.reduce((sum, r) => sum + (r.corrections_auto_stored || 0), 0), blockRate: runs.length > 0 ? (runs.filter(r => r.blocked).length / runs.length * 100).toFixed(1) : 0 }; } } // Export module.exports = MemoryService;