/* eslint-disable no-console */ import * as fs from 'fs'; import * as path from 'path'; import { randomUUID } from 'crypto'; import type { Memory, MemoryInput } from '@rigstate/shared'; // ───────────────────────────────────────────────────────────────────────────── // Memory Store: Local file-based storage for Decentralized Intelligence. // Memories are stored as individual JSON files in .rigstate/memories/ // ───────────────────────────────────────────────────────────────────────────── const MEMORIES_DIR = '.rigstate/memories'; function getMemoriesDir(): string { const dir = path.resolve(process.cwd(), MEMORIES_DIR); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } return dir; } // ── Save ───────────────────────────────────────────────────────────────────── export function saveMemory(input: MemoryInput): Memory { const dir = getMemoriesDir(); const now = new Date().toISOString(); const memory: Memory = { id: randomUUID(), ...input, title: input.title, content: input.content, category: input.category || 'CONTEXT', source: input.source || 'USER', tags: input.tags || [], importance: input.importance || 5, confidence: input.confidence || 1.0, created_at: now, updated_at: now, expires_at: input.expires_at || null, }; const filename = `${memory.id}.json`; const filepath = path.join(dir, filename); fs.writeFileSync(filepath, JSON.stringify(memory, null, 2), 'utf-8'); return memory; } // ── Load All ───────────────────────────────────────────────────────────────── export function loadAllMemories(): Memory[] { const dir = getMemoriesDir(); const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); const memories: Memory[] = []; for (const file of files) { try { const raw = fs.readFileSync(path.join(dir, file), 'utf-8'); memories.push(JSON.parse(raw) as Memory); } catch { // Skip corrupted files silently } } return memories; } // ── Search (Keyword-based, <10ms) ──────────────────────────────────────────── export interface SearchResult { memory: Memory; score: number; matchedFields: string[]; } export function searchMemories(query: string, limit = 5): SearchResult[] { const memories = loadAllMemories(); const tokens = tokenize(query); if (tokens.length === 0) return []; const results: SearchResult[] = []; for (const memory of memories) { // Check expiry if (memory.expires_at && new Date(memory.expires_at) < new Date()) { continue; } let score = 0; const matchedFields: string[] = []; // Title match (high weight) const titleTokens = tokenize(memory.title); const titleMatches = tokens.filter(t => titleTokens.includes(t)).length; if (titleMatches > 0) { score += titleMatches * 3; matchedFields.push('title'); } // Content match (medium weight) const contentLower = memory.content.toLowerCase(); const contentMatches = tokens.filter(t => contentLower.includes(t)).length; if (contentMatches > 0) { score += contentMatches * 1; matchedFields.push('content'); } // Tag match (high weight) const tagLower = memory.tags.map(t => t.toLowerCase()); const tagMatches = tokens.filter(t => tagLower.includes(t)).length; if (tagMatches > 0) { score += tagMatches * 4; matchedFields.push('tags'); } // Category match (bonus) if (tokens.includes(memory.category.toLowerCase())) { score += 2; matchedFields.push('category'); } // Importance boost score *= (memory.importance / 5); // importance 10 = 2x multiplier if (score > 0) { results.push({ memory, score, matchedFields }); } } // Sort by score descending, return top N return results .sort((a, b) => b.score - a.score) .slice(0, limit); } // ── Stats ──────────────────────────────────────────────────────────────────── export function getMemoryStats(): { total: number; byCategory: Record } { const memories = loadAllMemories(); const byCategory: Record = {}; for (const m of memories) { byCategory[m.category] = (byCategory[m.category] || 0) + 1; } return { total: memories.length, byCategory }; } // ── Delete ─────────────────────────────────────────────────────────────────── export function deleteMemory(id: string): boolean { const dir = getMemoriesDir(); const filepath = path.join(dir, `${id}.json`); if (fs.existsSync(filepath)) { fs.unlinkSync(filepath); return true; } return false; } // ── Helpers ────────────────────────────────────────────────────────────────── const STOP_WORDS = new Set([ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'and', 'or', 'not', 'this', 'that', 'it', 'we', 'you', 'they', 'my', 'our', 'your', 'its', 'his', 'her', 'how', 'what', 'why', 'when', 'where', 'which', 'who', 'do', 'does', 'did', 'has', 'have', 'had', 'be', 'been', 'being', 'will', 'would', 'can', 'could', 'should', 'shall', 'may', 'might', 'must', 'vi', 'er', 'var', 'har', 'den', 'det', 'en', 'et', 'og', 'i', 'på', 'til', 'fra', 'med', 'som', 'om', 'for', 'av', 'ikke', 'hvorfor', 'hvordan', 'hva', 'når', 'hvor', ]); function tokenize(text: string): string[] { return text .toLowerCase() .replace(/[^a-zA-Z0-9æøåÆØÅ\s]/g, ' ') .split(/\s+/) .filter(t => t.length > 1 && !STOP_WORDS.has(t)); }