'use strict'; const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const fs = require('fs'); /** * AI Memory Database * Stores player-specific memories and bot directives * Singleton pattern - one instance per bot */ class AIMemoryDB { constructor() { this.db = null; this.botName = null; } /** * Initialize database connection and create tables * @param {string} dbPath - Path to sqlite database file * @param {string} botName - Bot name for namespacing */ async initialize(dbPath = './storage/ai-memory.db', botName = 'default') { if (this.db) { // DB already initialized — don't overwrite botName (singleton shared across bots) return; } const fullPath = path.resolve(dbPath); const dir = path.dirname(fullPath); // Ensure directory exists if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } console.log(`AI Memory: Initializing database at ${fullPath} for bot ${botName}`); this.db = new sqlite3.Database(fullPath); this.botName = botName; await this.createTables(); } /** * Create tables if they don't exist */ createTables() { return new Promise((resolve, reject) => { this.db.serialize(() => { // Player memories table this.db.run(` CREATE TABLE IF NOT EXISTS player_memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, bot_name TEXT NOT NULL, player_name TEXT NOT NULL, memory_key TEXT NOT NULL, memory_value TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(bot_name, player_name, memory_key) ) `); // Bot directives table - persistent instructions per bot this.db.run(` CREATE TABLE IF NOT EXISTS bot_directives ( id INTEGER PRIMARY KEY AUTOINCREMENT, bot_name TEXT NOT NULL, directive_key TEXT NOT NULL, directive_value TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(bot_name, directive_key) ) `); // General memories table - for non-player-specific info this.db.run(` CREATE TABLE IF NOT EXISTS general_memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, bot_name TEXT NOT NULL, memory_key TEXT NOT NULL, memory_value TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(bot_name, memory_key) ) `); // Create indexes for faster lookups this.db.run(`CREATE INDEX IF NOT EXISTS idx_player_memories_lookup ON player_memories(bot_name, player_name)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_bot_directives_lookup ON bot_directives(bot_name)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_general_memories_lookup ON general_memories(bot_name)`); resolve(); }); }); } // ======================================== // Player Memories (Shared across all bots) // ======================================== /** * Set a memory about a specific player (shared across all bots) */ async setPlayerMemory(playerName, key, value) { return new Promise((resolve, reject) => { this.db.run(` INSERT OR REPLACE INTO player_memories (bot_name, player_name, memory_key, memory_value, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) `, ['global', playerName, key, value], (err) => { if (err) reject(err); else resolve(); }); }); } /** * Get a specific memory about a player (shared across all bots) */ async getPlayerMemory(playerName, key) { return new Promise((resolve, reject) => { this.db.get(` SELECT memory_value FROM player_memories WHERE bot_name = 'global' AND player_name = ? COLLATE NOCASE AND memory_key = ? `, [playerName, key], (err, row) => { if (err) reject(err); else resolve(row ? row.memory_value : null); }); }); } /** * Get all memories about a player (shared across all bots) */ async getAllPlayerMemories(playerName) { return new Promise((resolve, reject) => { this.db.all(` SELECT memory_key, memory_value FROM player_memories WHERE bot_name = 'global' AND player_name = ? COLLATE NOCASE `, [playerName], (err, rows) => { if (err) reject(err); else { const memories = {}; for (const row of rows) { memories[row.memory_key] = row.memory_value; } resolve(memories); } }); }); } /** * Delete a specific player memory (shared across all bots) */ async deletePlayerMemory(playerName, key) { return new Promise((resolve, reject) => { this.db.run(` DELETE FROM player_memories WHERE bot_name = 'global' AND player_name = ? COLLATE NOCASE AND memory_key = ? `, [playerName, key], (err) => { if (err) reject(err); else resolve(); }); }); } /** * List all players with stored memories (shared across all bots) */ async getAllKnownPlayers() { return new Promise((resolve, reject) => { this.db.all(` SELECT DISTINCT player_name FROM player_memories WHERE bot_name = 'global' `, (err, rows) => { if (err) reject(err); else resolve(rows.map(r => r.player_name)); }); }); } // ======================================== // Bot Directives // ======================================== /** * Set a directive for this bot */ async setDirective(botName, key, value) { return new Promise((resolve, reject) => { this.db.run(` INSERT OR REPLACE INTO bot_directives (bot_name, directive_key, directive_value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) `, [botName, key, value], (err) => { if (err) reject(err); else resolve(); }); }); } /** * Get a specific directive */ async getDirective(botName, key) { return new Promise((resolve, reject) => { this.db.get(` SELECT directive_value FROM bot_directives WHERE bot_name = ? AND directive_key = ? `, [botName, key], (err, row) => { if (err) reject(err); else resolve(row ? row.directive_value : null); }); }); } /** * Get all directives for this bot */ async getAllDirectives(botName) { return new Promise((resolve, reject) => { this.db.all(` SELECT directive_key, directive_value FROM bot_directives WHERE bot_name = ? `, [botName], (err, rows) => { if (err) reject(err); else { const directives = {}; for (const row of rows) { directives[row.directive_key] = row.directive_value; } resolve(directives); } }); }); } // ======================================== // General Memories // ======================================== /** * Set a general memory (not player-specific) */ async setGeneralMemory(botName, key, value) { return new Promise((resolve, reject) => { this.db.run(` INSERT OR REPLACE INTO general_memories (bot_name, memory_key, memory_value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) `, [botName, key, value], (err) => { if (err) reject(err); else resolve(); }); }); } /** * Get a general memory */ async getGeneralMemory(botName, key) { return new Promise((resolve, reject) => { this.db.get(` SELECT memory_value FROM general_memories WHERE bot_name = ? AND memory_key = ? `, [botName, key], (err, row) => { if (err) reject(err); else resolve(row ? row.memory_value : null); }); }); } /** * Get all general memories */ async getAllGeneralMemories(botName) { return new Promise((resolve, reject) => { this.db.all(` SELECT memory_key, memory_value FROM general_memories WHERE bot_name = ? `, [botName], (err, rows) => { if (err) reject(err); else { const memories = {}; for (const row of rows) { memories[row.memory_key] = row.memory_value; } resolve(memories); } }); }); } /** * Get all general memories with timestamps */ async getAllGeneralMemoriesWithTimestamps(botName) { return new Promise((resolve, reject) => { this.db.all(` SELECT memory_key, memory_value, updated_at FROM general_memories WHERE bot_name = ? ORDER BY updated_at DESC `, [botName], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); } // ======================================== // Summary Methods (for AI context) // ======================================== /** * Get all directives as formatted string with timestamps */ async getDirectivesSummary(botName) { return new Promise((resolve, reject) => { this.db.all(` SELECT directive_key, directive_value, updated_at FROM bot_directives WHERE bot_name = ? ORDER BY updated_at DESC `, [botName], (err, rows) => { if (err) reject(err); else { if (rows.length === 0) { resolve(null); return; } let summary = 'Active Directives:\n'; for (const row of rows) { const timeAgo = this.formatTimeAgo(row.updated_at); summary += `- ${row.directive_key}: ${row.directive_value} (set ${timeAgo})\n`; } resolve(summary.trim()); } }); }); } /** * Format timestamp as relative time string */ formatTimeAgo(timestamp) { if (!timestamp) return 'unknown'; const now = new Date(); const past = new Date(timestamp); const diffMs = now - past; const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffSecs < 60) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return past.toLocaleDateString(); } /** * Get full memory context for prompt injection */ async getMemoryContext(botName) { const directives = await this.getDirectivesSummary(botName); const generalMemories = await this.getAllGeneralMemoriesWithTimestamps(botName); let context = ''; if (directives) { context += directives + '\n\n'; } if (generalMemories && generalMemories.length > 0) { context += 'General Memories:\n'; for (const mem of generalMemories) { const timeAgo = this.formatTimeAgo(mem.updated_at); context += `- ${mem.memory_key}: ${mem.memory_value} (stored ${timeAgo})\n`; } context += '\n'; } return context || null; } /** * Get memories for specific players (for prompt injection when they're online) * @param {string[]} playerNames - Array of player names to get memories for */ async getPlayerMemoriesForPrompt(playerNames) { if (!playerNames || playerNames.length === 0) return null; let context = ''; for (const playerName of playerNames) { const memories = await this.getAllPlayerMemories(playerName); if (Object.keys(memories).length > 0) { context += `Memories about ${playerName}:\n`; for (const [key, value] of Object.entries(memories)) { context += `- ${key}: ${value}\n`; } context += '\n'; } } return context.trim() || null; } // ======================================== // Utility Methods // ======================================== /** * Close database connection */ async close() { return new Promise((resolve, reject) => { if (this.db) { this.db.close((err) => { if (err) reject(err); else { this.db = null; this.botName = null; resolve(); } }); } else { resolve(); } }); } } // Singleton instance module.exports = new AIMemoryDB();