#!/usr/bin/env node /** * Automated batch migration of project-specific documentation to task-mcp database * * Migrates: * - CLAUDE_HISTORY.md → project_archives (archive_type: session) * - planning.md → project_archives (archive_type: completed) * - PRD.md → project_archives (archive_type: research) * * Excludes: * - .framework-backup directories * - Archived projects * - ClaudeFramework (already migrated) */ import { readFileSync, readdirSync, renameSync, mkdirSync, existsSync, statSync } from 'fs'; import { join, basename, dirname, relative } from 'path'; import { homedir } from 'os'; import dotenv from 'dotenv'; import { archiveAdd } from './dist/tools/archives.js'; // Load environment dotenv.config(); // Configuration const DEV_DIR = join(homedir(), 'Development'); const EXCLUDED_PATTERNS = [ '.framework-backup', '/Archived/', '/ClaudeFramework/' ]; /** * Extract project key from directory path * Examples: * /Apps/RealEstate → RE * /Apps/eToroGridbot → GB * /Apps/ZorkiOS → ZK * /Infrastructure/mcp-servers/cloudmemorymcp → CM */ function getProjectKey(filePath) { const parts = filePath.split('/'); // Find the most specific directory name let projectName = ''; if (filePath.includes('/Apps/')) { const idx = parts.indexOf('Apps'); projectName = parts[idx + 1] || ''; } else if (filePath.includes('/Infrastructure/')) { // Use the deepest directory name projectName = parts[parts.length - 2] || ''; } // Generate 2-letter key from project name const normalized = projectName .replace(/([A-Z])/g, ' $1') // Split camelCase .trim() .toUpperCase(); const words = normalized.split(/\s+/); if (words.length >= 2) { return words[0][0] + words[1][0]; // First letter of first two words } else if (words[0].length >= 2) { return words[0].substring(0, 2); // First two letters } else { return words[0][0] + 'X'; // Fallback } } /** * Determine archive type based on filename */ function getArchiveType(filename) { if (filename === 'CLAUDE_HISTORY.md') { return 'session'; } else if (filename === 'planning.md') { return 'completed'; } else if (filename === 'PRD.md') { return 'research'; } return 'research'; } /** * Extract title from markdown content (first H1) */ function extractTitle(content, filename, projectKey) { const lines = content.split('\n'); for (const line of lines) { if (line.startsWith('# ')) { return line.slice(2).trim().substring(0, 500); } } // Fallback to project key + filename const baseName = filename.replace('.md', '').replace(/_/g, ' '); return `${projectKey} - ${baseName}`.substring(0, 500); } /** * Check if file should be excluded */ function shouldExclude(filePath) { return EXCLUDED_PATTERNS.some(pattern => filePath.includes(pattern)); } /** * Migrate a single file */ async function migrateFile(filePath) { const filename = basename(filePath); const projectKey = getProjectKey(filePath); try { const content = readFileSync(filePath, 'utf-8'); const title = extractTitle(content, filename, projectKey); const fileSize = statSync(filePath).size; const archiveType = getArchiveType(filename); console.log(`\n[${new Date().toISOString().substring(11, 19)}] Migrating: ${relative(DEV_DIR, filePath)}`); console.log(` Project: ${projectKey}`); console.log(` Title: ${title}`); console.log(` Size: ${Math.round(fileSize / 1024)}KB`); console.log(` Type: ${archiveType}`); // Call archive_add const result = await archiveAdd({ project: projectKey, archive_type: archiveType, title, content, original_path: filePath, file_size: fileSize }); console.log(` ✓ Migrated successfully`); // Move to backup const backupDir = join(dirname(filePath), '.migrated-to-mcp'); if (!existsSync(backupDir)) { mkdirSync(backupDir, { recursive: true }); } const backupPath = join(backupDir, filename); renameSync(filePath, backupPath); console.log(` ✓ Moved to backup`); return { success: true, filename, projectKey, title, fileSize, archiveType, filePath }; } catch (error) { console.error(` ✗ Error: ${error.message}`); return { success: false, filename, projectKey, error: error.message, filePath }; } } /** * Find all documentation files to migrate */ function findDocFiles() { const files = []; const targetFiles = ['CLAUDE_HISTORY.md', 'planning.md', 'PRD.md']; function scanDir(dir) { if (!existsSync(dir)) return; try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); // Skip excluded paths if (shouldExclude(fullPath)) continue; if (entry.isDirectory()) { // Skip node_modules, .git, etc. if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { scanDir(fullPath); } } else if (entry.isFile() && targetFiles.includes(entry.name)) { files.push(fullPath); } } } catch (error) { // Skip directories we can't read } } scanDir(DEV_DIR); return files.sort(); } /** * Main migration function */ async function main() { console.log('================================================================================'); console.log('Batch Migration: Project Documentation → task-mcp database'); console.log('================================================================================\n'); // Find all files const filesToMigrate = findDocFiles(); console.log(`Found ${filesToMigrate.length} documentation files to migrate\n`); if (filesToMigrate.length === 0) { console.log('✓ No files to migrate!'); return; } // Show sample paths console.log('Sample files:'); filesToMigrate.slice(0, 10).forEach(f => { console.log(` - ${relative(DEV_DIR, f)}`); }); if (filesToMigrate.length > 10) { console.log(` ... and ${filesToMigrate.length - 10} more`); } console.log('\nStarting migration...\n'); const results = { success: [], failed: [] }; // Migrate each file for (let i = 0; i < filesToMigrate.length; i++) { const filePath = filesToMigrate[i]; const result = await migrateFile(filePath); if (result.success) { results.success.push(result); } else { results.failed.push(result); } // Progress indicator const progress = Math.round(((i + 1) / filesToMigrate.length) * 100); console.log(` Progress: ${i + 1}/${filesToMigrate.length} (${progress}%)`); // Small delay to avoid overwhelming the database await new Promise(resolve => setTimeout(resolve, 50)); } // Summary console.log('\n================================================================================'); console.log('Migration Complete'); console.log('================================================================================\n'); console.log(`✓ Successfully migrated: ${results.success.length} files`); console.log(`✗ Failed: ${results.failed.length} files`); if (results.failed.length > 0) { console.log('\nFailed files:'); results.failed.forEach(f => { console.log(` - ${relative(DEV_DIR, f.filePath)}: ${f.error}`); }); } // Group by type const byType = {}; results.success.forEach(r => { byType[r.archiveType] = (byType[r.archiveType] || 0) + 1; }); console.log('\nBy archive type:'); Object.entries(byType).forEach(([type, count]) => { console.log(` - ${type}: ${count} files`); }); // Group by project const byProject = {}; results.success.forEach(r => { byProject[r.projectKey] = (byProject[r.projectKey] || 0) + 1; }); console.log('\nBy project:'); Object.entries(byProject) .sort((a, b) => b[1] - a[1]) .forEach(([project, count]) => { console.log(` - ${project}: ${count} files`); }); console.log('\nBackup location: /.migrated-to-mcp/'); console.log('Original files can be restored if needed.'); // Calculate total size const totalSize = results.success.reduce((sum, r) => sum + (r.fileSize || 0), 0); console.log(`\nTotal data migrated: ${Math.round(totalSize / 1024 / 1024 * 100) / 100}MB`); } // Run migration main() .then(() => { console.log('\n✓ Migration script completed successfully'); process.exit(0); }) .catch(error => { console.error('\n✗ Migration script failed:', error); console.error(error.stack); process.exit(1); });