const fs = require('fs'); const path = require('path'); const Database = require('better-sqlite3'); /** * Database Migration Script: Migrate existing sessions to projects * * This script: * 1. Scans existing session files from Claude Sessions directory * 2. Extracts unique project names from metadata.project field * 3. Creates projects with random icons and colors * 4. Updates sessions table with projectId references */ const VAULT_PATH = '/home/uroma/obsidian-vault'; const CLAUDE_SESSIONS_DIR = path.join(VAULT_PATH, 'Claude Sessions'); const DB_PATH = path.join(__dirname, '..', 'database.sqlite'); const ICONS = ['šŸš€', 'šŸ“±', 'šŸŽØ', 'šŸ’»', 'šŸ”§', 'šŸ“Š', 'šŸŽÆ', 'šŸ”®', '⚔', '🌟']; const COLORS = ['#4a9eff', '#ff6b6b', '#51cf66', '#ffd43b', '#cc5de8', '#ff922b', '#20c997', '#339af0']; /** * Get random item from array */ function getRandomItem(array) { return array[Math.floor(Math.random() * array.length)]; } /** * Parse frontmatter from markdown content */ function parseFrontmatter(content) { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!frontmatterMatch) { return null; } const frontmatter = {}; frontmatterMatch[1].split('\n').forEach(line => { const [key, ...valueParts] = line.split(':'); if (key && valueParts.length > 0) { frontmatter[key.trim()] = valueParts.join(':').trim(); } }); return frontmatter; } /** * Scan session files and extract project information */ function scanSessionFiles() { console.log('šŸ“‚ Scanning session files...'); console.log(` Directory: ${CLAUDE_SESSIONS_DIR}`); if (!fs.existsSync(CLAUDE_SESSIONS_DIR)) { console.log('āš ļø Claude Sessions directory does not exist'); return []; } const files = fs.readdirSync(CLAUDE_SESSIONS_DIR); const sessionFiles = files.filter(f => f.endsWith('.md') && f.includes('session-')); console.log(` Found ${sessionFiles.length} session files`); const sessions = []; const projectMap = new Map(); // projectName -> { sessions: [], path: null } sessionFiles.forEach(file => { const filepath = path.join(CLAUDE_SESSIONS_DIR, file); const content = fs.readFileSync(filepath, 'utf-8'); const frontmatter = parseFrontmatter(content); if (!frontmatter) { console.log(` āš ļø Skipping ${file}: No frontmatter found`); return; } const sessionId = frontmatter.session_id; const projectName = frontmatter.project; const workingDir = frontmatter.working_dir; const createdAt = frontmatter.created_at; if (!sessionId) { console.log(` āš ļø Skipping ${file}: No session_id in frontmatter`); return; } // Add session to list sessions.push({ id: sessionId, projectName, workingDir, createdAt, file }); // Group by project if (projectName) { if (!projectMap.has(projectName)) { projectMap.set(projectName, { sessions: [], path: workingDir, earliestDate: createdAt }); } projectMap.get(projectName).sessions.push(sessionId); // Update earliest date if this session is older const currentDate = new Date(createdAt); const existingDate = new Date(projectMap.get(projectName).earliestDate); if (currentDate < existingDate) { projectMap.get(projectName).earliestDate = createdAt; } } }); console.log(` āœ… Processed ${sessions.length} sessions`); console.log(` šŸ“Š Found ${projectMap.size} unique projects`); return { sessions, projectMap }; } /** * Create projects in database */ function createProjects(db, projectMap) { console.log('\nšŸŽØ Creating projects in database...'); const insertProject = db.prepare(` INSERT INTO projects (name, description, icon, color, path, createdAt, lastActivity) VALUES (?, ?, ?, ?, ?, ?, ?) `); const projects = {}; let createdCount = 0; for (const [projectName, data] of projectMap.entries()) { const icon = getRandomItem(ICONS); const color = getRandomItem(COLORS); const now = new Date().toISOString(); try { const result = insertProject.run( projectName, `Sessions for ${projectName}`, icon, color, data.path || VAULT_PATH, data.earliestDate, now ); projects[projectName] = { id: result.lastInsertRowid, name: projectName, icon, color, sessionCount: data.sessions.length }; createdCount++; console.log(` āœ… Created project: ${projectName} (${icon})`); console.log(` - ID: ${result.lastInsertRowid}`); console.log(` - Sessions: ${data.sessions.length}`); console.log(` - Path: ${data.path || VAULT_PATH}`); } catch (error) { console.error(` āŒ Error creating project ${projectName}:`, error.message); } } console.log(`\n āœ… Created ${createdCount} projects`); return projects; } /** * Update sessions with projectId */ function updateSessions(db, sessions, projects) { console.log('\nšŸ”— Linking sessions to projects...'); const insertSession = db.prepare(` INSERT OR REPLACE INTO sessions (id, projectId) VALUES (?, ?) `); let updatedCount = 0; let unlinkedCount = 0; sessions.forEach(session => { const projectName = session.projectName; if (projectName && projects[projectName]) { const projectId = projects[projectName].id; try { insertSession.run(session.id, projectId); updatedCount++; console.log(` āœ… Linked ${session.id} -> ${projectName} (ID: ${projectId})`); } catch (error) { console.error(` āŒ Error linking session ${session.id}:`, error.message); } } else { // Session without a project - add to sessions table without projectId try { insertSession.run(session.id, null); unlinkedCount++; console.log(` āš ļø Unlinked session: ${session.id} (no project)`); } catch (error) { console.error(` āŒ Error adding session ${session.id}:`, error.message); } } }); console.log(`\n āœ… Linked ${updatedCount} sessions to projects`); console.log(` ā„¹ļø ${unlinkedCount} sessions without projects`); return { updatedCount, unlinkedCount }; } /** * Display migration summary */ function displaySummary(sessions, projects, stats) { console.log('\n' + '='.repeat(60)); console.log('šŸ“Š MIGRATION SUMMARY'); console.log('='.repeat(60)); console.log(`\nSessions Processed: ${sessions.length}`); console.log(`Projects Created: ${Object.keys(projects).length}`); console.log(`Sessions Linked: ${stats.updatedCount}`); console.log(`Sessions Unlinked: ${stats.unlinkedCount}`); console.log('\nšŸ“ Projects Created:'); Object.values(projects).forEach(project => { console.log(` ${project.icon} ${project.name} (${project.color})`); console.log(` - ID: ${project.id}`); console.log(` - Sessions: ${project.sessionCount}`); }); console.log('\n' + '='.repeat(60)); console.log('āœ… Migration completed successfully!'); console.log('='.repeat(60) + '\n'); } /** * Main migration function */ function migrate() { console.log('šŸš€ Starting database migration for projects...\n'); // Check if database exists if (!fs.existsSync(DB_PATH)) { console.error('āŒ Database file not found:', DB_PATH); console.error('Please run the application first to initialize the database.'); process.exit(1); } // Open database console.log('šŸ“¦ Opening database...'); const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); try { // Step 1: Scan session files const { sessions, projectMap } = scanSessionFiles(); if (sessions.length === 0) { console.log('\nāš ļø No sessions found to migrate'); db.close(); return; } // Step 2: Create projects const projects = createProjects(db, projectMap); if (Object.keys(projects).length === 0) { console.log('\nāš ļø No projects found to create'); db.close(); return; } // Step 3: Update sessions const stats = updateSessions(db, sessions, projects); // Step 4: Display summary displaySummary(sessions, projects, stats); } catch (error) { console.error('\nāŒ Migration failed:', error); throw error; } finally { db.close(); console.log('šŸ“¦ Database connection closed\n'); } } // Run migration if executed directly if (require.main === module) { migrate(); } module.exports = { migrate };