Release v3.9.9

Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Alex Newman
2025-10-03 18:20:47 -04:00
parent 4d5b307a74
commit 85ed7c3d2f
85 changed files with 11156 additions and 7458 deletions
+49 -9
View File
@@ -17,12 +17,12 @@ export class MemoryStore {
*/
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
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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(
@@ -34,7 +34,12 @@ export class MemoryStore {
epoch,
input.project,
input.archive_basename || null,
input.origin || 'transcript'
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)!;
@@ -158,15 +163,44 @@ export class MemoryStore {
* Get memories by origin type
*/
getByOrigin(origin: string, limit?: number): MemoryRow[] {
const query = limit
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
*/
@@ -195,11 +229,12 @@ export class MemoryStore {
}
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 = ?
project = ?, archive_basename = ?, origin = ?, title = ?, subtitle = ?, facts = ?,
concepts = ?, files_touched = ?
WHERE id = ?
`);
@@ -212,6 +247,11 @@ export class MemoryStore {
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
);
+47 -2
View File
@@ -68,14 +68,26 @@ export class OverviewStore {
*/
getRecentForProject(project: string, limit = 5): OverviewRow[] {
const stmt = this.db.prepare(`
SELECT * FROM overviews
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch DESC
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as OverviewRow[];
}
/**
* Get all overviews for a project (oldest to newest)
*/
getAllForProject(project: string): OverviewRow[] {
const stmt = this.db.prepare(`
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch ASC
`);
return stmt.all(project) as OverviewRow[];
}
/**
* Get recent overviews across all projects
*/
@@ -193,4 +205,37 @@ export class OverviewStore {
const rows = stmt.all() as { project: string }[];
return rows.map(row => row.project);
}
/**
* Get most recent overview for a specific project
*/
getByProject(project: string): OverviewRow | null {
const stmt = this.db.prepare(`
SELECT * FROM overviews
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT 1
`);
return stmt.get(project) as OverviewRow || null;
}
/**
* Create or update overview for a project (keeps only most recent)
*/
upsertByProject(input: OverviewInput): OverviewRow {
const existing = this.getByProject(input.project);
if (existing) {
return this.update(existing.id, input);
}
return this.create(input);
}
/**
* Delete overview by project name
*/
deleteByProject(project: string): boolean {
const stmt = this.db.prepare('DELETE FROM overviews WHERE project = ?');
const info = stmt.run(project);
return info.changes > 0;
}
}
+107
View File
@@ -0,0 +1,107 @@
import { Database } from 'better-sqlite3';
import { getDatabase } from './Database.js';
import {
TranscriptEventInput,
TranscriptEventRow,
normalizeTimestamp
} from './types.js';
/**
* Data access for transcript_events table
*/
export class TranscriptEventStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Insert or update a transcript event
*/
upsert(event: TranscriptEventInput): TranscriptEventRow {
const { isoString, epoch } = normalizeTimestamp(event.captured_at);
const stmt = this.db.prepare(`
INSERT INTO transcript_events (
session_id,
project,
event_index,
event_type,
raw_json,
captured_at,
captured_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id, event_index) DO UPDATE SET
project = excluded.project,
event_type = excluded.event_type,
raw_json = excluded.raw_json,
captured_at = excluded.captured_at,
captured_at_epoch = excluded.captured_at_epoch
`);
stmt.run(
event.session_id,
event.project || null,
event.event_index,
event.event_type || null,
event.raw_json,
isoString,
epoch
);
return this.getBySessionAndIndex(event.session_id, event.event_index)!;
}
/**
* Bulk upsert events in a single transaction
*/
upsertMany(events: TranscriptEventInput[]): TranscriptEventRow[] {
const transaction = this.db.transaction((rows: TranscriptEventInput[]) => {
const results: TranscriptEventRow[] = [];
for (const row of rows) {
results.push(this.upsert(row));
}
return results;
});
return transaction(events);
}
/**
* Get event by session and index
*/
getBySessionAndIndex(sessionId: string, eventIndex: number): TranscriptEventRow | null {
const stmt = this.db.prepare(`
SELECT * FROM transcript_events
WHERE session_id = ? AND event_index = ?
`);
return stmt.get(sessionId, eventIndex) as TranscriptEventRow | null;
}
/**
* Get highest event_index stored for a session
*/
getMaxEventIndex(sessionId: string): number {
const stmt = this.db.prepare(`
SELECT MAX(event_index) as max_event_index
FROM transcript_events
WHERE session_id = ?
`);
const row = stmt.get(sessionId) as { max_event_index: number | null } | undefined;
return row?.max_event_index ?? -1;
}
/**
* List recent events for a session
*/
listBySession(sessionId: string, limit = 200, offset = 0): TranscriptEventRow[] {
const stmt = this.db.prepare(`
SELECT * FROM transcript_events
WHERE session_id = ?
ORDER BY event_index ASC
LIMIT ? OFFSET ?
`);
return stmt.all(sessionId, limit, offset) as TranscriptEventRow[];
}
}
+17 -6
View File
@@ -1,6 +1,3 @@
// Import migrations to register them
import './migrations/index.js';
// Export main components
export { DatabaseManager, getDatabase, initializeDatabase } from './Database.js';
@@ -9,24 +6,38 @@ export { SessionStore } from './SessionStore.js';
export { MemoryStore } from './MemoryStore.js';
export { OverviewStore } from './OverviewStore.js';
export { DiagnosticsStore } from './DiagnosticsStore.js';
export { TranscriptEventStore } from './TranscriptEventStore.js';
// Export types
export * from './types.js';
// Export migrations
export { migrations } from './migrations.js';
// Convenience function to get all stores
export async function createStores() {
const { DatabaseManager } = await import('./Database.js');
const db = await DatabaseManager.getInstance().initialize();
const { migrations } = await import('./migrations.js');
// Register migrations before initialization
const manager = DatabaseManager.getInstance();
for (const migration of migrations) {
manager.registerMigration(migration);
}
const db = await manager.initialize();
const { SessionStore } = await import('./SessionStore.js');
const { MemoryStore } = await import('./MemoryStore.js');
const { OverviewStore } = await import('./OverviewStore.js');
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
return {
sessions: new SessionStore(db),
memories: new MemoryStore(db),
overviews: new OverviewStore(db),
diagnostics: new DiagnosticsStore(db)
diagnostics: new DiagnosticsStore(db),
transcriptEvents: new TranscriptEventStore(db)
};
}
+169
View File
@@ -0,0 +1,169 @@
import { Database } from 'better-sqlite3';
import { Migration } from './Database.js';
/**
* Initial schema migration - creates all core tables
*/
export const migration001: Migration = {
version: 1,
up: (db: Database.Database) => {
// Sessions table - core session tracking
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL,
project TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'compress',
archive_path TEXT,
archive_bytes INTEGER,
archive_checksum TEXT,
archived_at TEXT,
metadata_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at_epoch DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_project_created ON sessions(project, created_at_epoch DESC);
`);
// Memories table - compressed memory chunks
db.exec(`
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
text TEXT NOT NULL,
document_id TEXT UNIQUE,
keywords TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
archive_basename TEXT,
origin TEXT NOT NULL DEFAULT 'transcript',
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at_epoch DESC);
CREATE INDEX IF NOT EXISTS idx_memories_project_created ON memories(project, created_at_epoch DESC);
CREATE INDEX IF NOT EXISTS idx_memories_document_id ON memories(document_id);
CREATE INDEX IF NOT EXISTS idx_memories_origin ON memories(origin);
`);
// Overviews table - session summaries (one per project)
db.exec(`
CREATE TABLE IF NOT EXISTS overviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
origin TEXT NOT NULL DEFAULT 'claude',
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_overviews_session ON overviews(session_id);
CREATE INDEX IF NOT EXISTS idx_overviews_project ON overviews(project);
CREATE INDEX IF NOT EXISTS idx_overviews_created_at ON overviews(created_at_epoch DESC);
CREATE INDEX IF NOT EXISTS idx_overviews_project_created ON overviews(project, created_at_epoch DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_overviews_project_latest ON overviews(project, created_at_epoch DESC);
`);
// Diagnostics table - system health and debug info
db.exec(`
CREATE TABLE IF NOT EXISTS diagnostics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
message TEXT NOT NULL,
severity TEXT NOT NULL DEFAULT 'info',
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
origin TEXT NOT NULL DEFAULT 'system',
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_diagnostics_session ON diagnostics(session_id);
CREATE INDEX IF NOT EXISTS idx_diagnostics_project ON diagnostics(project);
CREATE INDEX IF NOT EXISTS idx_diagnostics_severity ON diagnostics(severity);
CREATE INDEX IF NOT EXISTS idx_diagnostics_created ON diagnostics(created_at_epoch DESC);
`);
// Transcript events table - raw conversation events
db.exec(`
CREATE TABLE IF NOT EXISTS transcript_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
project TEXT,
event_index INTEGER NOT NULL,
event_type TEXT,
raw_json TEXT NOT NULL,
captured_at TEXT NOT NULL,
captured_at_epoch INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
UNIQUE(session_id, event_index)
);
CREATE INDEX IF NOT EXISTS idx_transcript_events_session ON transcript_events(session_id, event_index);
CREATE INDEX IF NOT EXISTS idx_transcript_events_project ON transcript_events(project);
CREATE INDEX IF NOT EXISTS idx_transcript_events_type ON transcript_events(event_type);
CREATE INDEX IF NOT EXISTS idx_transcript_events_captured ON transcript_events(captured_at_epoch DESC);
`);
console.log('✅ Created all database tables successfully');
},
down: (db: Database.Database) => {
db.exec(`
DROP TABLE IF EXISTS transcript_events;
DROP TABLE IF EXISTS diagnostics;
DROP TABLE IF EXISTS overviews;
DROP TABLE IF EXISTS memories;
DROP TABLE IF EXISTS sessions;
`);
}
};
/**
* Migration 002 - Add hierarchical memory fields (v2 format)
*/
export const migration002: Migration = {
version: 2,
up: (db: Database.Database) => {
// Add new columns for hierarchical memory structure
db.exec(`
ALTER TABLE memories ADD COLUMN title TEXT;
ALTER TABLE memories ADD COLUMN subtitle TEXT;
ALTER TABLE memories ADD COLUMN facts TEXT;
ALTER TABLE memories ADD COLUMN concepts TEXT;
ALTER TABLE memories ADD COLUMN files_touched TEXT;
`);
// Create indexes for the new fields to improve search performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_memories_title ON memories(title);
CREATE INDEX IF NOT EXISTS idx_memories_concepts ON memories(concepts);
`);
console.log('✅ Added hierarchical memory fields to memories table');
},
down: (db: Database.Database) => {
// Note: SQLite doesn't support DROP COLUMN in all versions
// In production, we'd need to recreate the table without these columns
// For now, we'll just log a warning
console.log('⚠️ Warning: SQLite ALTER TABLE DROP COLUMN not fully supported');
console.log('⚠️ To rollback, manually recreate the memories table');
}
};
/**
* All migrations in order
*/
export const migrations: Migration[] = [
migration001,
migration002
];
@@ -1,133 +0,0 @@
import { Migration } from '../Database.js';
/**
* Initial migration: Create all core tables for claude-mem SQLite index
*/
export const migration001: Migration = {
version: 1,
up: (db) => {
// Create sessions table
db.exec(`
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL,
project TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
source TEXT DEFAULT 'compress',
archive_path TEXT,
archive_bytes INTEGER,
archive_checksum TEXT,
archived_at TEXT,
metadata_json TEXT
)
`);
// Create indexes for sessions
db.exec(`
CREATE INDEX sessions_project_created_at ON sessions (project, created_at_epoch DESC)
`);
db.exec(`
CREATE INDEX sessions_source_created ON sessions (source, created_at_epoch DESC)
`);
// Create overviews table
db.exec(`
CREATE TABLE overviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
origin TEXT DEFAULT 'claude'
)
`);
// Create index for overviews
db.exec(`
CREATE INDEX overviews_project_created_at ON overviews (project, created_at_epoch DESC)
`);
// Create memories table
db.exec(`
CREATE TABLE memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
text TEXT NOT NULL,
document_id TEXT,
keywords TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
archive_basename TEXT,
origin TEXT DEFAULT 'transcript'
)
`);
// Create indexes for memories
db.exec(`
CREATE INDEX memories_project_created_at ON memories (project, created_at_epoch DESC)
`);
db.exec(`
CREATE UNIQUE INDEX memories_document_id_unique ON memories (document_id) WHERE document_id IS NOT NULL
`);
// Create diagnostics table
db.exec(`
CREATE TABLE diagnostics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT REFERENCES sessions(session_id) ON DELETE SET NULL,
message TEXT NOT NULL,
severity TEXT DEFAULT 'warn',
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
project TEXT NOT NULL,
origin TEXT DEFAULT 'compressor'
)
`);
// Create index for diagnostics
db.exec(`
CREATE INDEX diagnostics_project_created_at ON diagnostics (project, created_at_epoch DESC)
`);
// Create archives table (for future archival workflows)
db.exec(`
CREATE TABLE archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
path TEXT NOT NULL,
bytes INTEGER,
checksum TEXT,
stored_at TEXT NOT NULL,
storage_status TEXT DEFAULT 'active'
)
`);
// Create titles table (ready for conversation-titles.jsonl migration)
db.exec(`
CREATE TABLE titles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
title TEXT NOT NULL,
created_at TEXT NOT NULL,
project TEXT NOT NULL
)
`);
console.log('✅ Created initial database schema with all tables and indexes');
},
down: (db) => {
// Drop tables in reverse order to respect foreign key constraints
const tables = ['titles', 'archives', 'diagnostics', 'memories', 'overviews', 'sessions'];
for (const table of tables) {
db.exec(`DROP TABLE IF EXISTS ${table}`);
}
console.log('🗑️ Dropped all tables from initial migration');
}
};
-15
View File
@@ -1,15 +0,0 @@
import { DatabaseManager } from '../Database.js';
import { migration001 } from './001_initial.js';
/**
* Register all migrations with the database manager
*/
export function registerMigrations(): void {
const manager = DatabaseManager.getInstance();
// Register migrations in order
manager.registerMigration(migration001);
}
// Auto-register migrations when this module is imported
registerMigrations();
+33 -1
View File
@@ -37,6 +37,12 @@ export interface MemoryRow {
project: string;
archive_basename?: string;
origin: string;
// Hierarchical memory fields (v2)
title?: string;
subtitle?: string;
facts?: string; // JSON array of fact strings
concepts?: string; // JSON array of concept strings
files_touched?: string; // JSON array of file paths
}
export interface DiagnosticRow {
@@ -50,6 +56,17 @@ export interface DiagnosticRow {
origin: string;
}
export interface TranscriptEventRow {
id: number;
session_id: string;
project?: string;
event_index: number;
event_type?: string;
raw_json: string;
captured_at: string;
captured_at_epoch: number;
}
export interface ArchiveRow {
id: number;
session_id: string;
@@ -100,6 +117,12 @@ export interface MemoryInput {
project: string;
archive_basename?: string;
origin?: string;
// Hierarchical memory fields (v2)
title?: string;
subtitle?: string;
facts?: string; // JSON array of fact strings
concepts?: string; // JSON array of concept strings
files_touched?: string; // JSON array of file paths
}
export interface DiagnosticInput {
@@ -111,6 +134,15 @@ export interface DiagnosticInput {
origin?: string;
}
export interface TranscriptEventInput {
session_id: string;
project?: string;
event_index: number;
event_type?: string;
raw_json: string;
captured_at?: string | Date | number;
}
/**
* Helper function to normalize timestamps from various formats
*/
@@ -149,4 +181,4 @@ export function normalizeTimestamp(timestamp: string | Date | number | undefined
isoString: date.toISOString(),
epoch: date.getTime()
};
}
}