claude-mem
Version:
Memory compression system for Claude Code - persist context across sessions
154 lines (139 loc) • 4.83 kB
text/typescript
import { OptionValues } from 'commander';
import { spawnSync } from 'child_process';
import { createStores } from '../services/sqlite/index.js';
/**
* Store a memory to all three storage layers
* Called by SDK via bash during streaming memory capture
*/
export async function storeMemory(options: OptionValues): Promise<void> {
const { id, project, session, date, title, subtitle, facts, concepts, files } = options;
// Validate required fields
if (!id || !project || !session || !date) {
console.error('Error: All fields required: --id, --project, --session, --date');
process.exit(1);
}
// Validate hierarchical fields (required for v2 format)
if (!title || !subtitle || !facts) {
console.error('Error: Hierarchical format required: --title, --subtitle, --facts');
process.exit(1);
}
try {
const stores = await createStores();
const timestamp = new Date().toISOString();
// Ensure session exists
const sessionExists = await stores.sessions.has(session);
if (!sessionExists) {
await stores.sessions.create({
session_id: session,
project,
created_at: timestamp,
source: 'save'
});
}
// Parse JSON arrays if provided as strings
let factsArray: string | undefined;
let conceptsArray: string | undefined;
let filesArray: string | undefined;
try {
factsArray = facts ? JSON.stringify(JSON.parse(facts)) : undefined;
} catch (e) {
factsArray = facts; // Store as-is if not valid JSON
}
try {
conceptsArray = concepts ? JSON.stringify(JSON.parse(concepts)) : undefined;
} catch (e) {
conceptsArray = concepts; // Store as-is if not valid JSON
}
try {
filesArray = files ? JSON.stringify(JSON.parse(files)) : undefined;
} catch (e) {
filesArray = files; // Store as-is if not valid JSON
}
// Layer 1: SQLite Memory Index
const memoryExists = stores.memories.hasDocumentId(id);
if (!memoryExists) {
stores.memories.create({
document_id: id,
text: '', // Deprecated: hierarchical fields replace narrative text
keywords: '',
session_id: session,
project,
created_at: timestamp,
origin: 'streaming-sdk',
// Hierarchical fields (v2)
title: title || undefined,
subtitle: subtitle || undefined,
facts: factsArray,
concepts: conceptsArray,
files_touched: filesArray
});
}
// Layer 2: ChromaDB - Store hierarchical memory
if (factsArray) {
const factsJson = JSON.parse(factsArray);
const conceptsJson = conceptsArray ? JSON.parse(conceptsArray) : [];
const filesJson = filesArray ? JSON.parse(filesArray) : [];
// Store each atomic fact as a separate ChromaDB document
factsJson.forEach((fact: string, idx: number) => {
spawnSync('claude-mem', [
'chroma_add_documents',
'--collection_name', 'claude_memories',
'--documents', JSON.stringify([fact]),
'--ids', JSON.stringify([`${id}_fact_${String(idx).padStart(3, '0')}`]),
'--metadatas', JSON.stringify([{
type: 'fact',
parent_id: id,
fact_index: idx,
title,
subtitle,
project,
session_id: session,
created_at: timestamp,
created_at_epoch: Date.parse(timestamp),
keywords: '',
concepts: JSON.stringify(conceptsJson),
files_touched: JSON.stringify(filesJson),
origin: 'streaming-sdk'
}])
]);
});
// Store full narrative with hierarchical metadata
spawnSync('claude-mem', [
'chroma_add_documents',
'--collection_name', 'claude_memories',
'--documents', JSON.stringify([`${title}\n${subtitle}\n\n${factsJson.join('\n')}`]),
'--ids', JSON.stringify([id]),
'--metadatas', JSON.stringify([{
type: 'narrative',
title,
subtitle,
facts_count: factsJson.length,
project,
session_id: session,
created_at: timestamp,
created_at_epoch: Date.parse(timestamp),
keywords: '',
concepts: JSON.stringify(conceptsJson),
files_touched: JSON.stringify(filesJson),
origin: 'streaming-sdk'
}])
]);
}
// Success output (SDK will see this)
console.log(JSON.stringify({
success: true,
memory_id: id,
project,
session,
date,
timestamp,
hierarchical: !!(title && subtitle && facts)
}));
} catch (error: any) {
console.error(JSON.stringify({
success: false,
error: error.message || 'Unknown error storing memory'
}));
process.exit(1);
}
}