UNPKG

@endlessblink/like-i-said-v2

Version:

Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.

331 lines (278 loc) 10.3 kB
/** * Data Integrity Protection System * Ensures real memories and tasks are never lost or corrupted */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); class DataIntegrity { constructor(baseDir = 'memories', taskDir = 'tasks') { this.baseDir = baseDir; this.taskDir = taskDir; this.integrityDir = path.join(process.cwd(), '.data-integrity'); this.checksumFile = path.join(this.integrityDir, 'checksums.json'); this.verificationLog = path.join(this.integrityDir, 'verification.log'); this.checksums = new Map(); this.init(); } init() { // Ensure integrity directory exists if (!fs.existsSync(this.integrityDir)) { fs.mkdirSync(this.integrityDir, { recursive: true }); } // Load existing checksums this.loadChecksums(); if (process.env.DEBUG_MCP) console.error('🔐 Data Integrity Protection initialized'); } loadChecksums() { try { if (fs.existsSync(this.checksumFile)) { const data = fs.readFileSync(this.checksumFile, 'utf8'); const checksumData = JSON.parse(data); this.checksums = new Map(Object.entries(checksumData)); } } catch (error) { console.error('Failed to load checksums:', error); this.checksums = new Map(); } } saveChecksums() { try { const checksumData = Object.fromEntries(this.checksums); fs.writeFileSync(this.checksumFile, JSON.stringify(checksumData, null, 2), 'utf8'); } catch (error) { console.error('Failed to save checksums:', error); } } calculateFileChecksum(filePath) { try { const content = fs.readFileSync(filePath); return crypto.createHash('sha256').update(content).digest('hex'); } catch (error) { console.error(`Failed to calculate checksum for ${filePath}:`, error); return null; } } calculateContentChecksum(content) { return crypto.createHash('sha256').update(content, 'utf8').digest('hex'); } verifyFileIntegrity(filePath) { const currentChecksum = this.calculateFileChecksum(filePath); const storedChecksum = this.checksums.get(filePath); if (!currentChecksum) { return { valid: false, reason: 'Unable to calculate checksum' }; } if (!storedChecksum) { // New file, store its checksum this.checksums.set(filePath, { checksum: currentChecksum, created: new Date().toISOString(), lastVerified: new Date().toISOString() }); this.saveChecksums(); return { valid: true, reason: 'New file registered' }; } const isValid = currentChecksum === storedChecksum.checksum; // Update last verified time storedChecksum.lastVerified = new Date().toISOString(); this.checksums.set(filePath, storedChecksum); this.saveChecksums(); return { valid: isValid, reason: isValid ? 'Checksum verified' : 'Checksum mismatch - file may be corrupted', storedChecksum: storedChecksum.checksum, currentChecksum }; } protectFile(filePath, content = null) { try { let checksum; if (content) { // Calculate checksum from content before writing checksum = this.calculateContentChecksum(content); } else if (fs.existsSync(filePath)) { // Calculate checksum from existing file checksum = this.calculateFileChecksum(filePath); } else { throw new Error('File does not exist and no content provided'); } this.checksums.set(filePath, { checksum, created: new Date().toISOString(), lastVerified: new Date().toISOString(), protected: true }); this.saveChecksums(); this.logVerification(`PROTECTED: ${filePath} (${checksum})`); return true; } catch (error) { console.error(`Failed to protect file ${filePath}:`, error); return false; } } verifyAllFiles() { const results = { valid: [], invalid: [], missing: [], total: 0 }; for (const [filePath, checksumData] of this.checksums) { results.total++; if (!fs.existsSync(filePath)) { results.missing.push({ path: filePath, reason: 'File missing', lastSeen: checksumData.lastVerified }); continue; } const verification = this.verifyFileIntegrity(filePath); if (verification.valid) { results.valid.push({ path: filePath, verified: verification.reason }); } else { results.invalid.push({ path: filePath, reason: verification.reason, stored: verification.storedChecksum, current: verification.currentChecksum }); } } this.logVerification(`VERIFICATION COMPLETE: ${results.valid.length} valid, ${results.invalid.length} invalid, ${results.missing.length} missing`); return results; } scanAndProtectNewFiles() { const protectedFiles = []; // Scan memory files if (fs.existsSync(this.baseDir)) { this.scanDirectory(this.baseDir, protectedFiles); } // Scan task files if (fs.existsSync(this.taskDir)) { this.scanDirectory(this.taskDir, protectedFiles); } // Protect important system files const systemFiles = [ 'task-index.json', 'package.json', 'server-markdown.js' ]; systemFiles.forEach(file => { const filePath = path.join(process.cwd(), file); if (fs.existsSync(filePath) && !this.checksums.has(filePath)) { if (this.protectFile(filePath)) { protectedFiles.push(filePath); } } }); if (protectedFiles.length > 0) { if (process.env.DEBUG_MCP) console.error(`🔐 Protected ${protectedFiles.length} new files`); } return protectedFiles; } scanDirectory(dirPath, protectedFiles) { try { const entries = fs.readdirSync(dirPath); entries.forEach(entry => { const fullPath = path.join(dirPath, entry); const stat = fs.lstatSync(fullPath); if (stat.isDirectory()) { this.scanDirectory(fullPath, protectedFiles); } else if (stat.isFile() && (entry.endsWith('.md') || entry.endsWith('.json'))) { if (!this.checksums.has(fullPath)) { if (this.protectFile(fullPath)) { protectedFiles.push(fullPath); } } } }); } catch (error) { console.error(`Failed to scan directory ${dirPath}:`, error); } } detectCorruption() { const verification = this.verifyAllFiles(); const corruption = { corrupted: verification.invalid, missing: verification.missing, hasIssues: verification.invalid.length > 0 || verification.missing.length > 0 }; if (corruption.hasIssues) { this.logVerification(`CORRUPTION DETECTED: ${corruption.corrupted.length} corrupted, ${corruption.missing.length} missing files`); console.error('🚨 Data corruption detected!', corruption); } return corruption; } repairCorruption(backupDir) { if (!backupDir || !fs.existsSync(backupDir)) { throw new Error('Valid backup directory required for corruption repair'); } const corruption = this.detectCorruption(); const repaired = []; // Attempt to repair corrupted files corruption.corrupted.forEach(corruptedFile => { try { const relativePath = path.relative(process.cwd(), corruptedFile.path); const backupPath = path.join(backupDir, relativePath); if (fs.existsSync(backupPath)) { // Verify backup file integrity const backupChecksum = this.calculateFileChecksum(backupPath); const expectedChecksum = this.checksums.get(corruptedFile.path)?.checksum; if (backupChecksum === expectedChecksum) { // Restore from backup fs.copyFileSync(backupPath, corruptedFile.path); repaired.push(corruptedFile.path); this.logVerification(`REPAIRED: ${corruptedFile.path} from backup`); } } } catch (error) { console.error(`Failed to repair ${corruptedFile.path}:`, error); } }); // Attempt to restore missing files corruption.missing.forEach(missingFile => { try { const relativePath = path.relative(process.cwd(), missingFile.path); const backupPath = path.join(backupDir, relativePath); if (fs.existsSync(backupPath)) { // Ensure parent directory exists const parentDir = path.dirname(missingFile.path); if (!fs.existsSync(parentDir)) { fs.mkdirSync(parentDir, { recursive: true }); } fs.copyFileSync(backupPath, missingFile.path); repaired.push(missingFile.path); this.logVerification(`RESTORED: ${missingFile.path} from backup`); } } catch (error) { console.error(`Failed to restore ${missingFile.path}:`, error); } }); return repaired; } logVerification(message) { try { const timestamp = new Date().toISOString(); const logEntry = `${timestamp}: ${message}\n`; fs.appendFileSync(this.verificationLog, logEntry, 'utf8'); } catch (error) { console.error('Failed to write verification log:', error); } } getIntegrityStatus() { const verification = this.verifyAllFiles(); return { totalFiles: verification.total, validFiles: verification.valid.length, corruptedFiles: verification.invalid.length, missingFiles: verification.missing.length, integrityScore: verification.total > 0 ? (verification.valid.length / verification.total) * 100 : 100, lastCheck: new Date().toISOString() }; } } module.exports = { DataIntegrity };