Merge feature/import-export: Add memory export/import scripts with duplicate prevention

This commit is contained in:
Alex Newman
2025-12-13 17:53:18 -05:00
8 changed files with 920 additions and 98 deletions
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
/**
* Export memories matching a search query to a portable JSON format
* Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]
* Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem
*/
import Database from 'better-sqlite3';
import { existsSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
interface ObservationRecord {
id: number;
sdk_session_id: string;
project: string;
text: string | null;
type: string;
title: string;
subtitle: string | null;
facts: string | null;
narrative: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface SdkSessionRecord {
id: number;
claude_session_id: string;
sdk_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}
interface SessionSummaryRecord {
id: number;
sdk_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface UserPromptRecord {
id: number;
claude_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}
interface ExportData {
exportedAt: string;
exportedAtEpoch: number;
query: string;
project?: string;
totalObservations: number;
totalSessions: number;
totalSummaries: number;
totalPrompts: number;
observations: ObservationRecord[];
sessions: SdkSessionRecord[];
summaries: SessionSummaryRecord[];
prompts: UserPromptRecord[];
}
async function exportMemories(query: string, outputFile: string, project?: string) {
try {
// Read port from settings
const settings = SettingsDefaultsManager.loadFromFile(join(homedir(), '.claude-mem', 'settings.json'));
const port = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
const baseUrl = `http://localhost:${port}`;
console.log(`🔍 Searching for: "${query}"${project ? ` (project: ${project})` : ' (all projects)'}`);
// Build query params - use format=json for raw data
const params = new URLSearchParams({
query,
format: 'json',
limit: '999999'
});
if (project) params.set('project', project);
// Unified search - gets all result types using hybrid search
console.log('📡 Fetching all memories via hybrid search...');
const searchResponse = await fetch(`${baseUrl}/api/search?${params.toString()}`);
if (!searchResponse.ok) {
throw new Error(`Failed to search: ${searchResponse.status} ${searchResponse.statusText}`);
}
const searchData = await searchResponse.json();
const observations: ObservationRecord[] = searchData.observations || [];
const summaries: SessionSummaryRecord[] = searchData.sessions || [];
const prompts: UserPromptRecord[] = searchData.prompts || [];
console.log(`✅ Found ${observations.length} observations`);
console.log(`✅ Found ${summaries.length} session summaries`);
console.log(`✅ Found ${prompts.length} user prompts`);
// Get unique SDK session IDs from observations and summaries
const sdkSessionIds = new Set<string>();
observations.forEach((o) => {
if (o.sdk_session_id) sdkSessionIds.add(o.sdk_session_id);
});
summaries.forEach((s) => {
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
});
// Get SDK sessions metadata from database
// (We need this because the API doesn't expose sdk_sessions table directly)
console.log('📡 Fetching SDK sessions metadata...');
const sessions: SdkSessionRecord[] = [];
if (sdkSessionIds.size > 0) {
// Read directly from database for sdk_sessions table
const Database = (await import('better-sqlite3')).default;
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
console.error('💡 Has claude-mem been initialized? Try running a session first.');
process.exit(1);
}
const db = new Database(dbPath, { readonly: true });
try {
const placeholders = Array.from(sdkSessionIds).map(() => '?').join(',');
const sessionQuery = `
SELECT * FROM sdk_sessions
WHERE sdk_session_id IN (${placeholders})
ORDER BY started_at_epoch DESC
`;
sessions.push(...db.prepare(sessionQuery).all(...Array.from(sdkSessionIds)));
} finally {
db.close();
}
}
console.log(`✅ Found ${sessions.length} SDK sessions`);
// Create export data
const exportData: ExportData = {
exportedAt: new Date().toISOString(),
exportedAtEpoch: Date.now(),
query,
project,
totalObservations: observations.length,
totalSessions: sessions.length,
totalSummaries: summaries.length,
totalPrompts: prompts.length,
observations,
sessions,
summaries,
prompts
};
// Write to file
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
console.log(`\n📦 Export complete!`);
console.log(`📄 Output: ${outputFile}`);
console.log(`📊 Stats:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
} catch (error) {
console.error('❌ Export failed:', error);
process.exit(1);
}
}
// CLI interface
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: npx tsx scripts/export-memories.ts <query> <output-file> [--project=name]');
console.error('Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem');
console.error(' npx tsx scripts/export-memories.ts "authentication" auth.json');
process.exit(1);
}
// Parse arguments
const [query, outputFile, ...flags] = args;
const project = flags.find(f => f.startsWith('--project='))?.split('=')[1];
exportMemories(query, outputFile, project);
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env node
/**
* Import memories from a JSON export file with duplicate prevention
* Usage: npx tsx scripts/import-memories.ts <input-file>
* Example: npx tsx scripts/import-memories.ts windows-memories.json
*/
import Database from 'better-sqlite3';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
interface ImportStats {
sessionsImported: number;
sessionsSkipped: number;
summariesImported: number;
summariesSkipped: number;
observationsImported: number;
observationsSkipped: number;
promptsImported: number;
promptsSkipped: number;
}
function importMemories(inputFile: string) {
if (!existsSync(inputFile)) {
console.error(`❌ Input file not found: ${inputFile}`);
process.exit(1);
}
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
process.exit(1);
}
// Read and parse export file
const exportData = JSON.parse(readFileSync(inputFile, 'utf-8'));
console.log(`📦 Import file: ${inputFile}`);
console.log(`📅 Exported: ${exportData.exportedAt}`);
console.log(`🔍 Query: "${exportData.query}"`);
console.log(`📊 Contains:`);
console.log(`${exportData.totalObservations} observations`);
console.log(`${exportData.totalSessions} sessions`);
console.log(`${exportData.totalSummaries} summaries`);
console.log(`${exportData.totalPrompts} prompts`);
console.log('');
const db = new Database(dbPath);
const stats: ImportStats = {
sessionsImported: 0,
sessionsSkipped: 0,
summariesImported: 0,
summariesSkipped: 0,
observationsImported: 0,
observationsSkipped: 0,
promptsImported: 0,
promptsSkipped: 0
};
try {
// Prepare statements for duplicate checking
const checkSession = db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?');
const checkSummary = db.prepare('SELECT id FROM session_summaries WHERE sdk_session_id = ?');
const checkObservation = db.prepare(`
SELECT id FROM observations
WHERE sdk_session_id = ?
AND title = ?
AND created_at_epoch = ?
`);
const checkPrompt = db.prepare(`
SELECT id FROM user_prompts
WHERE claude_session_id = ?
AND prompt_number = ?
`);
// Prepare insert statements
const insertSession = db.prepare(`
INSERT INTO sdk_sessions (
claude_session_id, sdk_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertSummary = db.prepare(`
INSERT INTO session_summaries (
sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertObservation = db.prepare(`
INSERT INTO observations (
sdk_session_id, project, text, type, title, subtitle,
facts, narrative, concepts, files_read, files_modified,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertPrompt = db.prepare(`
INSERT INTO user_prompts (
claude_session_id, prompt_number, prompt_text,
created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?)
`);
// Import in transaction
db.transaction(() => {
// 1. Import sessions first (dependency for everything else)
console.log('🔄 Importing sessions...');
for (const session of exportData.sessions) {
const exists = checkSession.get(session.claude_session_id);
if (exists) {
stats.sessionsSkipped++;
continue;
}
insertSession.run(
session.claude_session_id,
session.sdk_session_id,
session.project,
session.user_prompt,
session.started_at,
session.started_at_epoch,
session.completed_at,
session.completed_at_epoch,
session.status
);
stats.sessionsImported++;
}
console.log(` ✅ Imported: ${stats.sessionsImported}, Skipped: ${stats.sessionsSkipped}`);
// 2. Import summaries (depends on sessions)
console.log('🔄 Importing summaries...');
for (const summary of exportData.summaries) {
const exists = checkSummary.get(summary.sdk_session_id);
if (exists) {
stats.summariesSkipped++;
continue;
}
insertSummary.run(
summary.sdk_session_id,
summary.project,
summary.request,
summary.investigated,
summary.learned,
summary.completed,
summary.next_steps,
summary.files_read,
summary.files_edited,
summary.notes,
summary.prompt_number,
summary.discovery_tokens || 0,
summary.created_at,
summary.created_at_epoch
);
stats.summariesImported++;
}
console.log(` ✅ Imported: ${stats.summariesImported}, Skipped: ${stats.summariesSkipped}`);
// 3. Import observations (depends on sessions)
console.log('🔄 Importing observations...');
for (const obs of exportData.observations) {
const exists = checkObservation.get(
obs.sdk_session_id,
obs.title,
obs.created_at_epoch
);
if (exists) {
stats.observationsSkipped++;
continue;
}
insertObservation.run(
obs.sdk_session_id,
obs.project,
obs.text,
obs.type,
obs.title,
obs.subtitle,
obs.facts,
obs.narrative,
obs.concepts,
obs.files_read,
obs.files_modified,
obs.prompt_number,
obs.discovery_tokens || 0,
obs.created_at,
obs.created_at_epoch
);
stats.observationsImported++;
}
console.log(` ✅ Imported: ${stats.observationsImported}, Skipped: ${stats.observationsSkipped}`);
// 4. Import prompts (depends on sessions)
console.log('🔄 Importing prompts...');
for (const prompt of exportData.prompts) {
const exists = checkPrompt.get(
prompt.claude_session_id,
prompt.prompt_number
);
if (exists) {
stats.promptsSkipped++;
continue;
}
insertPrompt.run(
prompt.claude_session_id,
prompt.prompt_number,
prompt.prompt_text,
prompt.created_at,
prompt.created_at_epoch
);
stats.promptsImported++;
}
console.log(` ✅ Imported: ${stats.promptsImported}, Skipped: ${stats.promptsSkipped}`);
})();
console.log('\n✅ Import complete!');
console.log('📊 Summary:');
console.log(` Sessions: ${stats.sessionsImported} imported, ${stats.sessionsSkipped} skipped`);
console.log(` Summaries: ${stats.summariesImported} imported, ${stats.summariesSkipped} skipped`);
console.log(` Observations: ${stats.observationsImported} imported, ${stats.observationsSkipped} skipped`);
console.log(` Prompts: ${stats.promptsImported} imported, ${stats.promptsSkipped} skipped`);
} finally {
db.close();
}
}
// CLI interface
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('Usage: npx tsx scripts/import-memories.ts <input-file>');
console.error('Example: npx tsx scripts/import-memories.ts windows-memories.json');
process.exit(1);
}
const [inputFile] = args;
importMemories(inputFile);