/**
* 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(),
};
}
}