Skip to main content
Glama
project-memory.ts8.55 kB
/** * Project Memory System * * Provides persistent memory storage for project-specific knowledge. * Enables AI agents to retain context across sessions. */ import * as fs from 'fs'; import * as path from 'path'; import { getLogger } from '../utils/logger.js'; export interface Memory { name: string; content: string; createdAt: string; updatedAt: string; tags?: string[]; } export interface MemoryIndex { memories: Record<string, Memory>; lastUpdated: string; } /** * Project Memory Manager */ export class ProjectMemory { private memoryDir: string; private indexPath: string; private logger = getLogger(); private index: MemoryIndex | null = null; constructor(workspaceRoot: string) { this.memoryDir = path.join(workspaceRoot, '.partnercore', 'memories'); this.indexPath = path.join(this.memoryDir, 'index.json'); } /** * Initialize the memory directory */ private ensureMemoryDir(): void { if (!fs.existsSync(this.memoryDir)) { fs.mkdirSync(this.memoryDir, { recursive: true }); this.logger.debug(`Created memory directory: ${this.memoryDir}`); } } /** * Load the memory index */ private loadIndex(): MemoryIndex { if (this.index) { return this.index; } this.ensureMemoryDir(); if (fs.existsSync(this.indexPath)) { try { const content = fs.readFileSync(this.indexPath, 'utf-8'); this.index = JSON.parse(content) as MemoryIndex; return this.index; } catch { this.logger.warn('Failed to load memory index, creating new one'); } } this.index = { memories: {}, lastUpdated: new Date().toISOString(), }; return this.index; } /** * Save the memory index */ private saveIndex(): void { this.ensureMemoryDir(); const index = this.loadIndex(); index.lastUpdated = new Date().toISOString(); fs.writeFileSync(this.indexPath, JSON.stringify(index, null, 2), 'utf-8'); } /** * Write a memory */ writeMemory(name: string, content: string, tags?: string[]): Memory { const index = this.loadIndex(); const now = new Date().toISOString(); const existing = index.memories[name]; const memory: Memory = { name, content, createdAt: existing?.createdAt || now, updatedAt: now, tags, }; // Save the memory content to a file const memoryFile = path.join(this.memoryDir, `${this.sanitizeName(name)}.md`); const memoryContent = this.formatMemoryFile(memory); fs.writeFileSync(memoryFile, memoryContent, 'utf-8'); // Update index index.memories[name] = memory; this.saveIndex(); this.logger.debug(`Memory written: ${name}`); return memory; } /** * Read a memory */ readMemory(name: string): Memory | null { const index = this.loadIndex(); const memory = index.memories[name]; if (!memory) { return null; } // Read the actual content from file const memoryFile = path.join(this.memoryDir, `${this.sanitizeName(name)}.md`); if (fs.existsSync(memoryFile)) { const fileContent = fs.readFileSync(memoryFile, 'utf-8'); const parsed = this.parseMemoryFile(fileContent); return { ...memory, content: parsed.content }; } return memory; } /** * Delete a memory */ deleteMemory(name: string): boolean { const index = this.loadIndex(); if (!index.memories[name]) { return false; } // Delete the memory file const memoryFile = path.join(this.memoryDir, `${this.sanitizeName(name)}.md`); if (fs.existsSync(memoryFile)) { fs.unlinkSync(memoryFile); } // Update index delete index.memories[name]; this.saveIndex(); this.logger.debug(`Memory deleted: ${name}`); return true; } /** * List all memories */ listMemories(): Memory[] { const index = this.loadIndex(); return Object.values(index.memories); } /** * Search memories by tag or content */ searchMemories(query: string): Memory[] { const memories = this.listMemories(); const queryLower = query.toLowerCase(); return memories.filter(m => m.name.toLowerCase().includes(queryLower) || m.content.toLowerCase().includes(queryLower) || m.tags?.some(t => t.toLowerCase().includes(queryLower)) ); } /** * Edit a memory using search/replace (supports regex) */ editMemory( name: string, needle: string, replacement: string, options?: { mode?: 'literal' | 'regex'; allowMultiple?: boolean; } ): { success: boolean; replacements: number; message: string } { const memory = this.readMemory(name); if (!memory) { return { success: false, replacements: 0, message: `Memory '${name}' not found` }; } const mode = options?.mode || 'literal'; const allowMultiple = options?.allowMultiple || false; let updatedContent: string; let replacements = 0; if (mode === 'regex') { try { const regex = new RegExp(needle, 'gm'); const matches = memory.content.match(regex); replacements = matches ? matches.length : 0; if (replacements === 0) { return { success: false, replacements: 0, message: `Pattern '${needle}' not found in memory` }; } if (replacements > 1 && !allowMultiple) { return { success: false, replacements, message: `Pattern matches ${replacements} times. Set allowMultiple:true to replace all.` }; } updatedContent = memory.content.replace(regex, replacement); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, replacements: 0, message: `Invalid regex: ${errorMessage}` }; } } else { // Literal mode const occurrences = memory.content.split(needle).length - 1; replacements = occurrences; if (occurrences === 0) { return { success: false, replacements: 0, message: `Text '${needle}' not found in memory` }; } if (occurrences > 1 && !allowMultiple) { return { success: false, replacements: occurrences, message: `Text matches ${occurrences} times. Set allowMultiple:true to replace all.` }; } if (allowMultiple) { updatedContent = memory.content.split(needle).join(replacement); } else { updatedContent = memory.content.replace(needle, replacement); replacements = 1; } } // Save the updated memory this.writeMemory(name, updatedContent, memory.tags); return { success: true, replacements, message: `Successfully replaced ${replacements} occurrence(s)` }; } /** * Sanitize memory name for filesystem */ private sanitizeName(name: string): string { return name .replace(/[<>:"/\\|?*]/g, '-') .replace(/\s+/g, '_') .toLowerCase(); } /** * Format memory as a markdown file */ private formatMemoryFile(memory: Memory): string { const frontmatter = [ '---', `name: ${memory.name}`, `created: ${memory.createdAt}`, `updated: ${memory.updatedAt}`, ]; if (memory.tags && memory.tags.length > 0) { frontmatter.push(`tags: [${memory.tags.join(', ')}]`); } frontmatter.push('---', ''); return frontmatter.join('\n') + memory.content; } /** * Parse a memory file */ private parseMemoryFile(content: string): { metadata: Record<string, string>; content: string } { const lines = content.split('\n'); const metadata: Record<string, string> = {}; let inFrontmatter = false; let contentStart = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line === '---') { if (!inFrontmatter) { inFrontmatter = true; } else { contentStart = i + 1; break; } } else if (inFrontmatter) { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.slice(0, colonIndex).trim(); const value = line.slice(colonIndex + 1).trim(); metadata[key] = value; } } } return { metadata, content: lines.slice(contentStart).join('\n').trim(), }; } }

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/ciellosinc/partnercore-proxy'

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