85ed7c3d2f
Published from npm package build Source: https://github.com/thedotmack/claude-mem-source
287 lines
8.0 KiB
TypeScript
287 lines
8.0 KiB
TypeScript
import { Database } from 'better-sqlite3';
|
|
import { getDatabase } from './Database.js';
|
|
import { MemoryRow, MemoryInput, normalizeTimestamp } from './types.js';
|
|
|
|
/**
|
|
* Data Access Object for memory records
|
|
*/
|
|
export class MemoryStore {
|
|
private db: Database.Database;
|
|
|
|
constructor(db?: Database.Database) {
|
|
this.db = db || getDatabase();
|
|
}
|
|
|
|
/**
|
|
* Create a new memory record
|
|
*/
|
|
create(input: MemoryInput): MemoryRow {
|
|
const { isoString, epoch } = normalizeTimestamp(input.created_at);
|
|
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO memories (
|
|
session_id, text, document_id, keywords, created_at, created_at_epoch,
|
|
project, archive_basename, origin, title, subtitle, facts, concepts, files_touched
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
const info = stmt.run(
|
|
input.session_id,
|
|
input.text,
|
|
input.document_id || null,
|
|
input.keywords || null,
|
|
isoString,
|
|
epoch,
|
|
input.project,
|
|
input.archive_basename || null,
|
|
input.origin || 'transcript',
|
|
input.title || null,
|
|
input.subtitle || null,
|
|
input.facts || null,
|
|
input.concepts || null,
|
|
input.files_touched || null
|
|
);
|
|
|
|
return this.getById(info.lastInsertRowid as number)!;
|
|
}
|
|
|
|
/**
|
|
* Create multiple memory records in a transaction
|
|
*/
|
|
createMany(inputs: MemoryInput[]): MemoryRow[] {
|
|
const transaction = this.db.transaction((memories: MemoryInput[]) => {
|
|
const results: MemoryRow[] = [];
|
|
for (const memory of memories) {
|
|
results.push(this.create(memory));
|
|
}
|
|
return results;
|
|
});
|
|
|
|
return transaction(inputs);
|
|
}
|
|
|
|
/**
|
|
* Get memory by primary key
|
|
*/
|
|
getById(id: number): MemoryRow | null {
|
|
const stmt = this.db.prepare('SELECT * FROM memories WHERE id = ?');
|
|
return stmt.get(id) as MemoryRow || null;
|
|
}
|
|
|
|
/**
|
|
* Get memory by document_id
|
|
*/
|
|
getByDocumentId(documentId: string): MemoryRow | null {
|
|
const stmt = this.db.prepare('SELECT * FROM memories WHERE document_id = ?');
|
|
return stmt.get(documentId) as MemoryRow || null;
|
|
}
|
|
|
|
/**
|
|
* Check if a document_id already exists
|
|
*/
|
|
hasDocumentId(documentId: string): boolean {
|
|
const stmt = this.db.prepare('SELECT 1 FROM memories WHERE document_id = ? LIMIT 1');
|
|
return Boolean(stmt.get(documentId));
|
|
}
|
|
|
|
/**
|
|
* Get memories for a specific session
|
|
*/
|
|
getBySessionId(sessionId: string): MemoryRow[] {
|
|
const stmt = this.db.prepare(`
|
|
SELECT * FROM memories
|
|
WHERE session_id = ?
|
|
ORDER BY created_at_epoch DESC
|
|
`);
|
|
return stmt.all(sessionId) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Get recent memories for a project
|
|
*/
|
|
getRecentForProject(project: string, limit = 10): MemoryRow[] {
|
|
const stmt = this.db.prepare(`
|
|
SELECT * FROM memories
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`);
|
|
return stmt.all(project, limit) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Get recent memories across all projects
|
|
*/
|
|
getRecent(limit = 10): MemoryRow[] {
|
|
const stmt = this.db.prepare(`
|
|
SELECT * FROM memories
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`);
|
|
return stmt.all(limit) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Search memories by text content
|
|
*/
|
|
searchByText(query: string, project?: string, limit = 20): MemoryRow[] {
|
|
let sql = 'SELECT * FROM memories WHERE text LIKE ?';
|
|
const params: any[] = [`%${query}%`];
|
|
|
|
if (project) {
|
|
sql += ' AND project = ?';
|
|
params.push(project);
|
|
}
|
|
|
|
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
|
params.push(limit);
|
|
|
|
const stmt = this.db.prepare(sql);
|
|
return stmt.all(...params) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Search memories by keywords
|
|
*/
|
|
searchByKeywords(keywords: string, project?: string, limit = 20): MemoryRow[] {
|
|
let sql = 'SELECT * FROM memories WHERE keywords LIKE ?';
|
|
const params: any[] = [`%${keywords}%`];
|
|
|
|
if (project) {
|
|
sql += ' AND project = ?';
|
|
params.push(project);
|
|
}
|
|
|
|
sql += ' ORDER BY created_at_epoch DESC LIMIT ?';
|
|
params.push(limit);
|
|
|
|
const stmt = this.db.prepare(sql);
|
|
return stmt.all(...params) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Get memories by origin type
|
|
*/
|
|
getByOrigin(origin: string, limit?: number): MemoryRow[] {
|
|
const query = limit
|
|
? 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC LIMIT ?'
|
|
: 'SELECT * FROM memories WHERE origin = ? ORDER BY created_at_epoch DESC';
|
|
|
|
const stmt = this.db.prepare(query);
|
|
const params = limit ? [origin, limit] : [origin];
|
|
return stmt.all(...params) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Get recent memories for a project filtered by origin
|
|
*/
|
|
getRecentForProjectByOrigin(project: string, origin: string, limit = 10): MemoryRow[] {
|
|
const stmt = this.db.prepare(`
|
|
SELECT * FROM memories
|
|
WHERE project = ? AND origin = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`);
|
|
return stmt.all(project, origin, limit) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Get last N memories for a project, sorted oldest to newest
|
|
*/
|
|
getLastNForProject(project: string, limit = 10): MemoryRow[] {
|
|
const stmt = this.db.prepare(`
|
|
SELECT * FROM (
|
|
SELECT * FROM memories
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
)
|
|
ORDER BY created_at_epoch ASC
|
|
`);
|
|
return stmt.all(project, limit) as MemoryRow[];
|
|
}
|
|
|
|
/**
|
|
* Count total memories
|
|
*/
|
|
count(): number {
|
|
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories');
|
|
const result = stmt.get() as { count: number };
|
|
return result.count;
|
|
}
|
|
|
|
/**
|
|
* Count memories by project
|
|
*/
|
|
countByProject(project: string): number {
|
|
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE project = ?');
|
|
const result = stmt.get(project) as { count: number };
|
|
return result.count;
|
|
}
|
|
|
|
/**
|
|
* Update a memory record
|
|
*/
|
|
update(id: number, input: Partial<MemoryInput>): MemoryRow {
|
|
const existing = this.getById(id);
|
|
if (!existing) {
|
|
throw new Error(`Memory with id ${id} not found`);
|
|
}
|
|
|
|
const { isoString, epoch } = normalizeTimestamp(input.created_at || existing.created_at);
|
|
|
|
const stmt = this.db.prepare(`
|
|
UPDATE memories SET
|
|
text = ?, document_id = ?, keywords = ?, created_at = ?, created_at_epoch = ?,
|
|
project = ?, archive_basename = ?, origin = ?, title = ?, subtitle = ?, facts = ?,
|
|
concepts = ?, files_touched = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(
|
|
input.text || existing.text,
|
|
input.document_id !== undefined ? input.document_id : existing.document_id,
|
|
input.keywords !== undefined ? input.keywords : existing.keywords,
|
|
isoString,
|
|
epoch,
|
|
input.project || existing.project,
|
|
input.archive_basename !== undefined ? input.archive_basename : existing.archive_basename,
|
|
input.origin || existing.origin,
|
|
input.title !== undefined ? input.title : existing.title,
|
|
input.subtitle !== undefined ? input.subtitle : existing.subtitle,
|
|
input.facts !== undefined ? input.facts : existing.facts,
|
|
input.concepts !== undefined ? input.concepts : existing.concepts,
|
|
input.files_touched !== undefined ? input.files_touched : existing.files_touched,
|
|
id
|
|
);
|
|
|
|
return this.getById(id)!;
|
|
}
|
|
|
|
/**
|
|
* Delete a memory by ID
|
|
*/
|
|
deleteById(id: number): boolean {
|
|
const stmt = this.db.prepare('DELETE FROM memories WHERE id = ?');
|
|
const info = stmt.run(id);
|
|
return info.changes > 0;
|
|
}
|
|
|
|
/**
|
|
* Delete memories by session_id
|
|
*/
|
|
deleteBySessionId(sessionId: string): number {
|
|
const stmt = this.db.prepare('DELETE FROM memories WHERE session_id = ?');
|
|
const info = stmt.run(sessionId);
|
|
return info.changes;
|
|
}
|
|
|
|
/**
|
|
* Get unique projects from memories
|
|
*/
|
|
getUniqueProjects(): string[] {
|
|
const stmt = this.db.prepare('SELECT DISTINCT project FROM memories ORDER BY project');
|
|
const rows = stmt.all() as { project: string }[];
|
|
return rows.map(row => row.project);
|
|
}
|
|
} |