Skip to main content
Glama
npc-memory.repo.ts9.09 kB
import Database from 'better-sqlite3'; /** * HIGH-004: NPC Memory Repository * Tracks relationships and conversation history between PCs and NPCs */ export type Familiarity = 'stranger' | 'acquaintance' | 'friend' | 'close_friend' | 'rival' | 'enemy'; export type Disposition = 'hostile' | 'unfriendly' | 'neutral' | 'friendly' | 'helpful'; export type Importance = 'low' | 'medium' | 'high' | 'critical'; export interface NpcRelationship { characterId: string; npcId: string; familiarity: Familiarity; disposition: Disposition; notes: string | null; firstMetAt: string; lastInteractionAt: string; interactionCount: number; } export interface ConversationMemory { id: number; characterId: string; npcId: string; summary: string; importance: Importance; topics: string[]; createdAt: string; } interface RelationshipRow { character_id: string; npc_id: string; familiarity: string; disposition: string; notes: string | null; first_met_at: string; last_interaction_at: string; interaction_count: number; } interface MemoryRow { id: number; character_id: string; npc_id: string; summary: string; importance: string; topics: string; created_at: string; } export class NpcMemoryRepository { constructor(private db: Database.Database) {} // ============================================================ // RELATIONSHIP METHODS // ============================================================ /** * Get relationship between PC and NPC * Returns null if no relationship exists (they're strangers) */ getRelationship(characterId: string, npcId: string): NpcRelationship | null { const stmt = this.db.prepare(` SELECT * FROM npc_relationships WHERE character_id = ? AND npc_id = ? `); const row = stmt.get(characterId, npcId) as RelationshipRow | undefined; if (!row) return null; return this.rowToRelationship(row); } /** * Create or update relationship between PC and NPC */ upsertRelationship(relationship: Omit<NpcRelationship, 'firstMetAt' | 'lastInteractionAt' | 'interactionCount'> & { firstMetAt?: string; lastInteractionAt?: string; interactionCount?: number; }): NpcRelationship { const now = new Date().toISOString(); const existing = this.getRelationship(relationship.characterId, relationship.npcId); if (existing) { // Update existing const stmt = this.db.prepare(` UPDATE npc_relationships SET familiarity = ?, disposition = ?, notes = ?, last_interaction_at = ?, interaction_count = interaction_count + 1 WHERE character_id = ? AND npc_id = ? `); stmt.run( relationship.familiarity, relationship.disposition, relationship.notes ?? existing.notes, now, relationship.characterId, relationship.npcId ); } else { // Create new const stmt = this.db.prepare(` INSERT INTO npc_relationships (character_id, npc_id, familiarity, disposition, notes, first_met_at, last_interaction_at, interaction_count) VALUES (?, ?, ?, ?, ?, ?, ?, 1) `); stmt.run( relationship.characterId, relationship.npcId, relationship.familiarity, relationship.disposition, relationship.notes ?? null, now, now ); } return this.getRelationship(relationship.characterId, relationship.npcId)!; } /** * Get all NPCs a character has interacted with */ getCharacterRelationships(characterId: string): NpcRelationship[] { const stmt = this.db.prepare(` SELECT * FROM npc_relationships WHERE character_id = ? ORDER BY last_interaction_at DESC `); const rows = stmt.all(characterId) as RelationshipRow[]; return rows.map(row => this.rowToRelationship(row)); } /** * Get all characters who have interacted with an NPC */ getNpcRelationships(npcId: string): NpcRelationship[] { const stmt = this.db.prepare(` SELECT * FROM npc_relationships WHERE npc_id = ? ORDER BY last_interaction_at DESC `); const rows = stmt.all(npcId) as RelationshipRow[]; return rows.map(row => this.rowToRelationship(row)); } // ============================================================ // CONVERSATION MEMORY METHODS // ============================================================ /** * Record a conversation memory */ recordMemory(memory: Omit<ConversationMemory, 'id' | 'createdAt'>): ConversationMemory { const now = new Date().toISOString(); const stmt = this.db.prepare(` INSERT INTO conversation_memories (character_id, npc_id, summary, importance, topics, created_at) VALUES (?, ?, ?, ?, ?, ?) `); const result = stmt.run( memory.characterId, memory.npcId, memory.summary, memory.importance, JSON.stringify(memory.topics), now ); return { ...memory, id: result.lastInsertRowid as number, createdAt: now }; } /** * Get conversation history between PC and NPC */ getConversationHistory( characterId: string, npcId: string, options?: { minImportance?: Importance; limit?: number; } ): ConversationMemory[] { let query = ` SELECT * FROM conversation_memories WHERE character_id = ? AND npc_id = ? `; const params: (string | number)[] = [characterId, npcId]; if (options?.minImportance) { const importanceOrder: Record<Importance, number> = { 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 }; const minLevel = importanceOrder[options.minImportance]; // Filter by importance level using CASE query += ` AND CASE importance WHEN 'low' THEN 1 WHEN 'medium' THEN 2 WHEN 'high' THEN 3 WHEN 'critical' THEN 4 END >= ?`; params.push(minLevel); } query += ` ORDER BY id DESC`; if (options?.limit) { query += ` LIMIT ?`; params.push(options.limit); } const stmt = this.db.prepare(query); const rows = stmt.all(...params) as MemoryRow[]; return rows.map(row => this.rowToMemory(row)); } /** * Get recent interactions across all NPCs for a character */ getRecentInteractions(characterId: string, limit: number = 10): ConversationMemory[] { const stmt = this.db.prepare(` SELECT * FROM conversation_memories WHERE character_id = ? ORDER BY id DESC LIMIT ? `); const rows = stmt.all(characterId, limit) as MemoryRow[]; return rows.map(row => this.rowToMemory(row)); } /** * Search memories by topic */ searchByTopic(characterId: string, topic: string): ConversationMemory[] { // SQLite JSON search - topics is stored as JSON array const stmt = this.db.prepare(` SELECT * FROM conversation_memories WHERE character_id = ? AND topics LIKE ? ORDER BY id DESC `); const rows = stmt.all(characterId, `%"${topic}"%`) as MemoryRow[]; return rows.map(row => this.rowToMemory(row)); } // ============================================================ // HELPER METHODS // ============================================================ private rowToRelationship(row: RelationshipRow): NpcRelationship { return { characterId: row.character_id, npcId: row.npc_id, familiarity: row.familiarity as Familiarity, disposition: row.disposition as Disposition, notes: row.notes, firstMetAt: row.first_met_at, lastInteractionAt: row.last_interaction_at, interactionCount: row.interaction_count }; } private rowToMemory(row: MemoryRow): ConversationMemory { return { id: row.id, characterId: row.character_id, npcId: row.npc_id, summary: row.summary, importance: row.importance as Importance, topics: JSON.parse(row.topics), createdAt: row.created_at }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server