automation-tools.ts•26.6 kB
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { SemanticEngine } from '../../engines/semantic-engine.js';
import { PatternEngine } from '../../engines/pattern-engine.js';
import { SQLiteDatabase } from '../../storage/sqlite-db.js';
import { ProgressTracker } from '../../utils/progress-tracker.js';
import { ConsoleProgressRenderer } from '../../utils/console-progress.js';
import { glob } from 'glob';
import { statSync, existsSync } from 'fs';
import { join } from 'path';
export class AutomationTools {
  constructor(
    private semanticEngine: SemanticEngine,
    private patternEngine: PatternEngine,
    private database: SQLiteDatabase
  ) { }
  get tools(): Tool[] {
    return [
      {
        name: 'auto_learn_if_needed',
        description: 'Automatically learn from codebase if intelligence data is missing or stale. Includes project setup and verification. Perfect for seamless agent integration.',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Path to the codebase directory (defaults to current working directory)'
            },
            force: {
              type: 'boolean',
              description: 'Force re-learning even if data exists',
              default: false
            },
            includeProgress: {
              type: 'boolean',
              description: 'Include detailed progress information in response',
              default: true
            },
            skipLearning: {
              type: 'boolean',
              description: 'Skip the learning phase for faster setup (Phase 4: merged from quick_setup)',
              default: false
            },
            includeSetupSteps: {
              type: 'boolean',
              description: 'Include detailed setup verification steps (Phase 4: merged from quick_setup)',
              default: false
            }
          }
        }
      }
      // DEPRECATED (Phase 4): Merged into get_project_blueprint - returns learning status in blueprint
      // {
      //   name: 'get_learning_status',
      //   description: 'Get the current learning/intelligence status of the codebase',
      //   inputSchema: {
      //     type: 'object',
      //     properties: {
      //       path: {
      //         type: 'string',
      //         description: 'Path to check (defaults to current working directory)'
      //       }
      //     }
      //   }
      // },
      // DEPRECATED (Phase 4): Merged into auto_learn_if_needed - same functionality
      // {
      //   name: 'quick_setup',
      //   description: 'Perform quick setup and learning for immediate use by AI agents',
      //   inputSchema: {
      //     type: 'object',
      //     properties: {
      //       path: {
      //         type: 'string',
      //         description: 'Path to the project directory'
      //       },
      //       skipLearning: {
      //         type: 'boolean',
      //         description: 'Skip the learning phase for faster setup',
      //         default: false
      //       }
      //     }
      //   }
      // }
    ];
  }
  async autoLearnIfNeeded(args: {
    path?: string;
    force?: boolean;
    includeProgress?: boolean;
    skipLearning?: boolean;
    includeSetupSteps?: boolean;
  }): Promise<any> {
    const projectPath = args.path || process.cwd();
    const force = args.force || false;
    const includeProgress = args.includeProgress !== false;
    const skipLearning = args.skipLearning || false;
    const includeSetupSteps = args.includeSetupSteps || false;
    // Phase 4: If includeSetupSteps is true, include quick_setup functionality
    const setupSteps: Array<{
      step: string;
      status: string;
      message: string;
      details?: any;
      error?: string;
    }> | undefined = includeSetupSteps ? [] : undefined;
    if (includeSetupSteps) {
      console.error(`🚀 Quick setup for: ${projectPath}`);
      // Step 1: Project check
      const files = await this.countProjectFiles(projectPath);
      setupSteps!.push({
        step: 'project_check',
        status: 'completed',
        message: `Project detected at ${projectPath}`,
        details: files
      });
      // Step 2: Database initialization (automatic)
      setupSteps!.push({
        step: 'database_init',
        status: 'completed',
        message: 'Database initialized and migrations applied',
        details: {
          version: this.database.getMigrator().getCurrentVersion(),
          tablesReady: true
        }
      });
    } else {
      console.error(`\n🚀 Quick setup for: ${projectPath}`);
    }
    // Don't show progress bars yet - wait until we actually start learning
    // Check if learning is needed
    const status = await this.getLearningStatus({ path: projectPath });
    // Phase 4: Handle skipLearning from quick_setup
    if (skipLearning) {
      if (includeSetupSteps) {
        setupSteps!.push({
          step: 'learning',
          status: 'skipped',
          message: 'Learning phase skipped as requested'
        });
        // Verification step
        setupSteps!.push({
          step: 'verification',
          status: 'completed',
          message: status.message,
          details: status
        });
        return {
          success: true,
          action: 'setup_completed',
          projectPath,
          steps: setupSteps,
          message: '✅ Quick setup completed! In Memoria is ready for AI agent use.',
          readyForAgents: status.hasIntelligence,
          intelligenceStatus: status
        };
      }
      return {
        action: 'skipped',
        reason: 'Learning phase skipped',
        status,
        message: 'Setup completed without learning.'
      };
    }
    if (!force && status.hasIntelligence && !status.isStale) {
      if (includeSetupSteps) {
        setupSteps!.push({
          step: 'learning',
          status: 'skipped',
          message: 'Intelligence data is up-to-date'
        });
        setupSteps!.push({
          step: 'verification',
          status: 'completed',
          message: status.message,
          details: status
        });
        return {
          success: true,
          action: 'setup_completed',
          projectPath,
          steps: setupSteps,
          message: '✅ Setup completed! Intelligence data is current.',
          readyForAgents: true,
          intelligenceStatus: status
        };
      }
      return {
        action: 'skipped',
        reason: 'Intelligence data is up-to-date',
        status,
        message: 'Ready to use! Intelligence data is current.'
      };
    }
    // Perform learning with progress tracking
    const tracker = new ProgressTracker();
    const progressRenderer = new ConsoleProgressRenderer(tracker);
    try {
      // Setup progress phases
      const files = await this.countProjectFiles(projectPath);
      tracker.addPhase('discovery', files.total, 1);
      tracker.addPhase('semantic_analysis', files.codeFiles, 3);
      tracker.addPhase('pattern_learning', files.codeFiles, 2);
      tracker.addPhase('indexing', files.codeFiles, 1);
      console.error(`\n🧠 Starting intelligent learning...`);
      console.error('━'.repeat(60) + '\n');
      // Start the progress renderer which shows all phases
      if (includeProgress) {
        progressRenderer.start();
      }
      // Phase 1: Discovery (fast, completes immediately)
      tracker.startPhase('discovery');
      tracker.complete('discovery');
      // Phase 2: Semantic Analysis
      tracker.startPhase('semantic_analysis');
      const analysisStart = Date.now();
      let concepts: any[] = [];
      try {
        // Create timeout promise with proper cleanup
        let timeoutId: NodeJS.Timeout | null = null;
        const timeoutPromise = new Promise<never>((_, reject) => {
          timeoutId = setTimeout(() => {
            reject(new Error('Semantic analysis timed out after 5 minutes. This often happens with large projects.'));
          }, 300000); // 5 minutes
        });
        try {
          concepts = await Promise.race([
            this.semanticEngine.learnFromCodebase(
              projectPath,
              (current: number, total: number, message: string) => {
                // Update progress tracker with real-time updates from semantic engine
                tracker.updateProgress('semantic_analysis', current, message);
              }
            ),
            timeoutPromise
          ]);
        } finally {
          // CRITICAL: Clear timeout to prevent hanging
          if (timeoutId !== null) {
            clearTimeout(timeoutId);
          }
        }
        tracker.complete('semantic_analysis');
        const analysisTime = Date.now() - analysisStart;
        if (analysisTime > 120000) { // More than 2 minutes
          console.error(`\n⚠️  Semantic analysis took ${Math.round(analysisTime / 1000)}s. Consider excluding large generated files.`);
        }
      } catch (error) {
        tracker.complete('semantic_analysis');
        throw error;
      }
      // Phase 3: Pattern Learning
      const patternStart = Date.now();
      tracker.startPhase('pattern_learning');
      tracker.updateProgress('pattern_learning', 1, 'Analyzing code patterns...');
      const patterns = await this.patternEngine.learnFromCodebase(
        projectPath,
        (current: number, total: number, message: string) => {
          // Update progress tracker with real-time updates from pattern engine
          // Map the 0-100 range to the actual file count
          const mapped = Math.floor((current / 100) * files.codeFiles);
          tracker.updateProgress('pattern_learning', Math.max(1, mapped), message);
        }
      );
      const patternTime = Date.now() - patternStart;
      tracker.complete('pattern_learning');
      // Phase 4: Indexing
      const indexStart = Date.now();
      tracker.startPhase('indexing');
      tracker.updateProgress('indexing', 1, 'Indexing concepts and patterns...');
      // Indexing happens in-memory, mark progress
      tracker.updateProgress('indexing', Math.floor(files.codeFiles / 2), 'Building search structures...');
      const indexTime = Date.now() - indexStart;
      tracker.complete('indexing');
      progressRenderer.stop();
      // Print final summary (without duplicate completion message)
      const totalTime = Date.now() - tracker.getProgress()!.startTime;
      const separator = '━'.repeat(60);
      console.error(`${separator}`);
      console.error(`📊 Concepts:  ${concepts.length.toLocaleString()}`);
      console.error(`🔍 Patterns:  ${patterns.length.toLocaleString()}`);
      console.error(`📁 Files:     ${files.codeFiles.toLocaleString()}`);
      console.error(`⏱️  Time:      ${this.formatDuration(totalTime)}`);
      console.error(separator);
      // Phase 4: Handle setup steps from quick_setup
      if (includeSetupSteps) {
        setupSteps!.push({
          step: 'learning',
          status: 'completed',
          message: `Learned ${concepts.length} concepts and ${patterns.length} patterns`,
          details: {
            conceptsLearned: concepts.length,
            patternsLearned: patterns.length,
            filesAnalyzed: files.codeFiles
          }
        });
        const finalStatus = await this.getLearningStatus({ path: projectPath });
        setupSteps!.push({
          step: 'verification',
          status: 'completed',
          message: finalStatus.message,
          details: finalStatus
        });
        return {
          success: true,
          action: 'setup_completed',
          projectPath,
          steps: setupSteps,
          conceptsLearned: concepts.length,
          patternsLearned: patterns.length,
          filesAnalyzed: files.codeFiles,
          totalFiles: files.total,
          timeElapsed: Date.now() - tracker.getProgress()!.startTime,
          message: '✅ Quick setup completed! In Memoria is ready for AI agent use.',
          readyForAgents: true,
          intelligenceStatus: finalStatus
        };
      }
      const result = {
        action: 'learned',
        conceptsLearned: concepts.length,
        patternsLearned: patterns.length,
        filesAnalyzed: files.codeFiles,
        totalFiles: files.total,
        timeElapsed: Date.now() - tracker.getProgress()!.startTime,
        message: `✅ Learning completed! Analyzed ${files.codeFiles} code files and learned ${concepts.length} concepts and ${patterns.length} patterns.`,
        status: await this.getLearningStatus({ path: projectPath })
      };
      if (includeProgress) {
        (result as any).progressData = progressRenderer.getProgressData();
      }
      return result;
    } catch (error: unknown) {
      progressRenderer.stop();
      console.error('❌ Auto-learning failed:', error);
      // Phase 4: Handle setup steps failure
      if (includeSetupSteps) {
        setupSteps!.push({
          step: 'error',
          status: 'failed',
          message: `Setup failed: ${error instanceof Error ? error.message : String(error)}`,
          error: error instanceof Error ? error.message : String(error)
        });
        return {
          success: false,
          action: 'setup_failed',
          projectPath,
          steps: setupSteps,
          message: '❌ Quick setup failed. Manual intervention may be required.',
          readyForAgents: false,
          error: error instanceof Error ? error.message : String(error),
          status: await this.getLearningStatus({ path: projectPath })
        };
      }
      return {
        action: 'failed',
        error: error instanceof Error ? error.message : String(error),
        message: 'Learning failed. The system will continue with limited intelligence.',
        status: await this.getLearningStatus({ path: projectPath })
      };
    } finally {
      // CRITICAL: Ensure progress renderer is stopped even if errors occur
      if (progressRenderer) {
        progressRenderer.stop();
      }
      // CRITICAL: Clean up engine resources to prevent hanging
      // Note: We use the shared engines from the MCP server, so we don't close them here
      // But we should ensure no hanging timers or intervals remain
      if (this.semanticEngine) {
        try {
          // Don't call cleanup on shared engines - just ensure no hanging operations
          // The cleanup will be handled when the MCP server shuts down
        } catch (error) {
          console.warn('Warning: Issue during resource cleanup:', error);
        }
      }
    }
  }
  async getLearningStatus(args: { path?: string }): Promise<any> {
    const projectPath = args.path || process.cwd();
    try {
      // Check for existing intelligence
      const concepts = this.database.getSemanticConcepts();
      const patterns = this.database.getDeveloperPatterns();
      // Count project files
      const files = await this.countProjectFiles(projectPath);
      // Check if data is stale (based on file modification times)
      const hasIntelligence = concepts.length > 0 || patterns.length > 0;
      const isStale = hasIntelligence ? await this.detectStaleness(projectPath, concepts, patterns) : false;
      return {
        path: projectPath,
        hasIntelligence,
        isStale,
        conceptsStored: concepts.length,
        patternsStored: patterns.length,
        filesInProject: files.total,
        codeFilesInProject: files.codeFiles,
        lastLearningTime: hasIntelligence ? this.getLastLearningTime() : null,
        recommendation: hasIntelligence && !isStale
          ? 'ready'
          : 'learning_recommended',
        message: hasIntelligence && !isStale
          ? `Intelligence is ready! ${concepts.length} concepts and ${patterns.length} patterns available.`
          : `Learning recommended. Found ${files.codeFiles} code files to analyze.`
      };
    } catch (error: unknown) {
      return {
        path: projectPath,
        hasIntelligence: false,
        isStale: false,
        error: error instanceof Error ? error.message : String(error),
        recommendation: 'learning_needed',
        message: 'No intelligence data available. Learning needed for optimal functionality.'
      };
    }
  }
  async quickSetup(args: {
    path?: string;
    skipLearning?: boolean;
  }): Promise<any> {
    const projectPath = args.path || process.cwd();
    const skipLearning = args.skipLearning || false;
    console.error(`🚀 Quick setup for: ${projectPath}`);
    const steps = [];
    let success = true;
    try {
      // Step 1: Check project structure
      steps.push({
        step: 'project_check',
        status: 'completed',
        message: `Project detected at ${projectPath}`,
        details: await this.countProjectFiles(projectPath)
      });
      // Step 2: Database initialization (automatic)
      steps.push({
        step: 'database_init',
        status: 'completed',
        message: 'Database initialized and migrations applied',
        details: {
          version: this.database.getMigrator().getCurrentVersion(),
          tablesReady: true
        }
      });
      // Step 3: Learning (if not skipped)
      if (!skipLearning) {
        const learningResult = await this.autoLearnIfNeeded({
          path: projectPath,
          force: false,
          includeProgress: false
        });
        steps.push({
          step: 'learning',
          status: learningResult.action === 'failed' ? 'failed' : 'completed',
          message: learningResult.message,
          details: learningResult
        });
        if (learningResult.action === 'failed') {
          success = false;
        }
      } else {
        steps.push({
          step: 'learning',
          status: 'skipped',
          message: 'Learning phase skipped as requested'
        });
      }
      // Step 4: Verify ready state
      const status = await this.getLearningStatus({ path: projectPath });
      steps.push({
        step: 'verification',
        status: 'completed',
        message: status.message,
        details: status
      });
      return {
        success,
        projectPath,
        steps,
        message: success
          ? '✅ Quick setup completed! In Memoria is ready for AI agent use.'
          : '⚠️  Setup completed with warnings. Some features may have limited functionality.',
        readyForAgents: success,
        intelligenceStatus: status
      };
    } catch (error: unknown) {
      console.error('❌ Quick setup failed:', error);
      steps.push({
        step: 'error',
        status: 'failed',
        message: `Setup failed: ${error instanceof Error ? error.message : String(error)}`,
        error: error instanceof Error ? error.message : String(error)
      });
      return {
        success: false,
        projectPath,
        steps,
        message: '❌ Quick setup failed. Manual intervention may be required.',
        readyForAgents: false,
        error: error instanceof Error ? error.message : String(error)
      };
    }
  }
  private async countProjectFiles(path: string): Promise<{ total: number; codeFiles: number }> {
    try {
      const allFiles = await glob('**/*', {
        cwd: path,
        ignore: [
          // Package managers and dependencies
          '**/node_modules/**',
          '**/bower_components/**',
          '**/jspm_packages/**',
          '**/vendor/**',
          // Version control
          '**/.git/**',
          '**/.svn/**',
          '**/.hg/**',
          // Build outputs and artifacts
          '**/dist/**',
          '**/build/**',
          '**/out/**',
          '**/output/**',
          '**/target/**',
          '**/bin/**',
          '**/obj/**',
          '**/Debug/**',
          '**/Release/**',
          // Framework-specific build directories
          '**/.next/**',
          '**/.nuxt/**',
          '**/.svelte-kit/**',
          '**/.vitepress/**',
          '**/_site/**',
          // Static assets and public files
          '**/public/**',
          '**/static/**',
          '**/assets/**',
          // Testing and coverage
          '**/coverage/**',
          '**/.coverage/**',
          '**/htmlcov/**',
          '**/.pytest_cache/**',
          '**/.nyc_output/**',
          '**/nyc_output/**',
          '**/lib-cov/**',
          // Python environments and cache
          '**/__pycache__/**',
          '**/.venv/**',
          '**/venv/**',
          '**/env/**',
          '**/.env/**',
          // Temporary and cache directories
          '**/tmp/**',
          '**/temp/**',
          '**/.tmp/**',
          '**/cache/**',
          '**/.cache/**',
          '**/logs/**',
          '**/.logs/**',
          // Generated/minified files
          '**/*.min.js',
          '**/*.min.css',
          '**/*.bundle.js',
          '**/*.chunk.js',
          '**/*.map',
          // Lock files
          '**/package-lock.json',
          '**/yarn.lock',
          '**/Cargo.lock',
          '**/Gemfile.lock',
          '**/Pipfile.lock',
          '**/poetry.lock'
        ],
        nodir: true
      });
      const codeFiles = allFiles.filter(file =>
        /\.(ts|tsx|js|jsx|py|rs|go|java|c|cpp|h|hpp|svelte|vue)$/.test(file)
      );
      return {
        total: allFiles.length,
        codeFiles: codeFiles.length
      };
    } catch (error) {
      return { total: 0, codeFiles: 0 };
    }
  }
  private getLastLearningTime(): string | null {
    // Simple heuristic - get the latest created_at from semantic_concepts or developer_patterns
    try {
      const latestConcept = this.database.getSemanticConcepts()
        .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
      const latestPattern = this.database.getDeveloperPatterns()
        .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
      let latestTime = 0;
      if (latestConcept) {
        latestTime = Math.max(latestTime, new Date(latestConcept.createdAt).getTime());
      }
      if (latestPattern) {
        latestTime = Math.max(latestTime, new Date(latestPattern.createdAt).getTime());
      }
      return latestTime > 0 ? new Date(latestTime).toISOString() : null;
    } catch (error) {
      return null;
    }
  }
  private formatDuration(ms: number): string {
    const seconds = Math.floor(ms / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    if (hours > 0) {
      return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
    } else if (minutes > 0) {
      return `${minutes}m ${seconds % 60}s`;
    } else {
      return `${seconds}s`;
    }
  }
  /**
   * Detect if intelligence data is stale based on file modification times
   * @param projectPath Path to the project directory
   * @param concepts Stored semantic concepts
   * @param patterns Stored patterns
   * @returns True if data is stale, false otherwise
   */
  private async detectStaleness(
    projectPath: string,
    concepts: any[],
    patterns: any[]
  ): Promise<boolean> {
    try {
      // Get the most recent intelligence data timestamp
      let latestIntelligenceTime = 0;
      // Check concept timestamps (ensure UTC)
      for (const concept of concepts) {
        const conceptTime = concept.createdAt ? new Date(concept.createdAt).getTime() : 0;
        latestIntelligenceTime = Math.max(latestIntelligenceTime, conceptTime);
      }
      // Check pattern timestamps
      for (const pattern of patterns) {
        const patternTime = pattern.createdAt ? new Date(pattern.createdAt).getTime() : 0;
        latestIntelligenceTime = Math.max(latestIntelligenceTime, patternTime);
      }
      if (latestIntelligenceTime === 0) {
        return true; // No valid timestamps, consider stale
      }
      // Get the most recent file modification time in the project
      const mostRecentFileTime = await this.getMostRecentFileModificationTime(projectPath);
      if (mostRecentFileTime === null) {
        return false; // Can't determine file times, assume not stale
      }
      // Consider data stale if any source file has been modified after the intelligence data
      const timeDiff = mostRecentFileTime - latestIntelligenceTime;
      const rawIsStale = timeDiff > 0; // Files are newer than intelligence
      // Add a buffer to avoid false positives from minor timestamp differences
      const bufferMs = 5 * 60 * 1000; // 5 minutes
      // Data is considered stale only if files are significantly newer (beyond buffer)
      return rawIsStale && (timeDiff > bufferMs);
    } catch (error) {
      // If we can't determine staleness, err on the side of caution and assume not stale
      console.warn('Failed to detect staleness:', error);
      return false;
    }
  }
  /**
   * Get the most recent file modification time in the project
   * @param projectPath Path to the project directory
   * @returns Most recent modification timestamp in milliseconds, or null if error
   */
  private async getMostRecentFileModificationTime(projectPath: string): Promise<number | null> {
    try {
      // Use glob to find all source files
      const sourcePatterns = [
        '**/*.ts',
        '**/*.tsx',
        '**/*.js',
        '**/*.jsx',
        '**/*.py',
        '**/*.rs',
        '**/*.go',
        '**/*.java',
        '**/*.c',
        '**/*.cpp',
        '**/*.cs'
      ];
      let mostRecentTime = 0;
      for (const pattern of sourcePatterns) {
        const files = await glob(pattern, {
          cwd: projectPath,
          ignore: [
            'node_modules/**',
            '.git/**',
            'target/**',
            'dist/**',
            'build/**',
            '.next/**',
            '__pycache__/**',
            '**/*.min.js',
            '**/*.min.css'
          ],
          absolute: true
        });
        for (const file of files) {
          try {
            if (existsSync(file)) {
              const stats = statSync(file);
              const modTime = stats.mtime.getTime();
              mostRecentTime = Math.max(mostRecentTime, modTime);
            }
          } catch (fileError) {
            // Skip files that can't be accessed
            continue;
          }
        }
      }
      return mostRecentTime > 0 ? mostRecentTime : null;
    } catch (error) {
      console.warn('Failed to get file modification times:', error);
      return null;
    }
  }
}