/** * File Modification Detector * * Detects when user has modified generated context files. * Uses content hashing for efficient change detection. */ import fs from 'fs/promises'; import path from 'path'; import type { DatabaseClient } from '../../db/client.js'; /** * File modification status */ export interface FileModificationStatus { /** Tool name */ tool: string; /** Full file path */ filePath: string; /** Whether file exists on disk */ exists: boolean; /** Whether file was generated by k0ntext */ wasGenerated: boolean; /** Current content hash */ currentHash: string | null; /** Hash stored in database when generated */ storedHash: string | null; /** Whether file has been modified since generation */ isModified: boolean; /** Last time file was generated */ generatedAt?: string; /** Last time file was verified */ lastVerifiedAt?: string; } /** * File modification detector options */ export interface ModificationDetectorOptions { /** Include files that don't exist in results */ includeNonExistent?: boolean; } /** * File modification detector * * Checks if user has modified generated context files. */ export class FileModificationDetector { private db: DatabaseClient; private projectRoot: string; constructor(db: DatabaseClient, projectRoot: string = process.cwd()) { this.db = db; this.projectRoot = projectRoot; } /** * Convert absolute path to relative path for database lookup */ private toRelativePath(filePath: string): string { if (path.isAbsolute(filePath)) { return path.relative(this.projectRoot, filePath); } return filePath; } /** * Check a single file for modifications * * @param tool - Tool name * @param filePath - Full path to file * @returns File modification status */ async checkFile(tool: string, filePath: string): Promise { // Check if file exists let exists = false; let currentContent = ''; try { currentContent = await fs.readFile(filePath, 'utf-8'); exists = true; } catch { // File doesn't exist } // Get current hash const currentHash = exists ? this.db.hashContent(currentContent) : null; // Get stored record - convert absolute path to relative for lookup const relativePath = this.toRelativePath(filePath); const stored = this.db.getGeneratedFileInfo(tool, relativePath); const wasGenerated = stored !== null; const storedHash = stored?.contentHash || null; const isModified = storedHash !== null && currentHash !== null && currentHash !== storedHash; return { tool, filePath, exists, wasGenerated, currentHash, storedHash, isModified, generatedAt: stored?.generatedAt, lastVerifiedAt: stored?.lastVerifiedAt }; } /** * Check all generated files for modifications * * @param options - Detector options * @returns Array of file modification statuses */ async checkAll(options: ModificationDetectorOptions = {}): Promise { const results: FileModificationStatus[] = []; // Get all tracked tools const tools = ['claude', 'copilot', 'cline', 'antigravity', 'windsurf', 'aider', 'continue', 'cursor', 'gemini']; for (const tool of tools) { const generatedFiles = this.db.getGeneratedFiles(tool); for (const file of generatedFiles) { // Convert relative path to absolute for file operations const absolutePath = path.isAbsolute(file.filePath) ? file.filePath : path.join(this.projectRoot, file.filePath); const status = await this.checkFile(file.tool, absolutePath); if (options.includeNonExistent || status.exists) { results.push(status); } } } return results; } /** * Get only modified files * * @returns Array of modified file statuses */ async getModifiedFiles(): Promise { const all = await this.checkAll(); return all.filter(f => f.isModified); } /** * Mark a file as verified (update last_verified_at) * * @param tool - Tool name * @param filePath - Full path to file */ markAsVerified(tool: string, filePath: string): void { const relativePath = this.toRelativePath(filePath); const record = this.db.getGeneratedFileInfo(tool, relativePath); if (record) { this.db.upsertGeneratedFile({ tool, filePath: relativePath, contentHash: record.contentHash, backupPath: record.backupPath }); } } /** * Mark a file as user-modified * * @param tool - Tool name * @param filePath - Full path to file */ markAsModified(tool: string, filePath: string): void { const relativePath = this.toRelativePath(filePath); this.db.markUserModified(tool, relativePath); } }