UNPKG

ollama-code-qwen

Version:

Un assistant IA en ligne de commande utilisant Ollama et le modèle qwen2.5-coder pour aider au développement, avec des capacités MCP améliorées et détection d'intentions en français et anglais

1,363 lines (1,138 loc) 57.6 kB
/** * Mode interactif avec interface TUI */ import { UIManager } from './tui.js'; import { GitService } from '../services/git-service.js'; import { FileService } from '../services/file-service.js'; import { MCPFileService } from '../services/mcp-file-service.js'; import { MCPService } from '../services/mcp-service.js'; import { CodeAnalyzer } from '../services/code-analyzer.js'; import { CodeExecutor } from '../services/code-executor.js'; import { CacheService } from '../services/cache-service.js'; import { PromptService } from '../services/prompt-service.js'; import { Config } from '../utils/config.js'; import path from 'path'; export class InteractiveMode { /** * Initialise le mode interactif * @param {OllamaService} ollamaService - Service Ollama * @param {ContextManager} contextManager - Gestionnaire de contexte */ constructor(ollamaService, contextManager) { this.ollamaService = ollamaService; this.contextManager = contextManager; this.config = new Config(); // Initialiser les services this.gitService = new GitService(); this.fileService = new FileService(); // Initialiser le service MCP pour la gestion avancée des fichiers this.mcpFileService = new MCPFileService(process.cwd()); // Initialiser le service MCP pour les intentions de fichiers - avec debug actif pour plus de visibilité this.mcpService = new MCPService({ baseDirectory: process.cwd(), allowDelete: true, // Activer par défaut pour une meilleure expérience utilisateur allowExecute: true, // Activer par défaut pour une meilleure expérience utilisateur debug: true // Activer le debug pour voir ce qui se passe en cas de problème }); this.codeAnalyzer = new CodeAnalyzer(this.fileService); this.codeExecutor = new CodeExecutor({ timeout: this.config.get('codeExecutionTimeout') || 10000 }); // Initialiser le service de cache si activé if (this.config.get('enableCache')) { this.cacheService = new CacheService({ ttl: this.config.get('cacheTTL') || 3600000 }); this.cacheService.init().then(() => { // Passer le service de cache au gestionnaire de contexte this.contextManager.cacheService = this.cacheService; }); } // Initialiser le service de prompts this.promptService = new PromptService(this.config); // Initialiser l'interface utilisateur this.ui = new UIManager({ model: this.ollamaService.modelName, host: this.ollamaService.host, version: '0.3.1', onSubmit: this.handleUserInput.bind(this), onCommand: this.handleCommand.bind(this), onExit: this.handleExit.bind(this) }); // Initialiser l'historique de conversation this.conversation = []; this.projectContext = ''; // Répertoire de travail courant this.currentDirectory = process.cwd(); // Support du mode focus this.focusedPath = null; // Chemin actuellement en focus this.focusMode = false; // Mode focus activé/désactivé // Informations sur le projet détectées this.projectInfo = null; } /** * Démarre le mode interactif */ async start() { try { // Afficher l'interface this.ui.start(); // Vérifier si on est dans un dépôt Git const isGitRepo = await this.gitService.isGitRepository(); this.ui.updateStatus(`Git: ${isGitRepo ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`); // Charger le contexte du projet this.ui.startLoading('Loading project context and analyzing project structure...'); this.projectContext = await this.contextManager.getContext(); // Détecter le type de projet this.projectInfo = await this.contextManager.detectProjectType(); this.ui.stopLoading(); // Initialiser la conversation avec le contexte du projet const projectTypeInfo = this.projectInfo.language ? `Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` : ''; this.conversation = [ { role: 'system', content: `You are an AI coding assistant. You help with programming tasks and questions. ${projectTypeInfo} Current project context: ${this.projectContext} IMPORTANT: When the user asks you to create or modify files, you can do so directly. If they ask you to "create a file named X", you can write the content and it will be automatically created. You do not need to use commands like /write or /edit. Just clearly mention the file name and provide the code. Examples: - When asked to create a file, you can say "Here's the content for file.js:" and then provide the code - When asked to modify a file, you can say "Here's the updated version of file.js:" and provide the full content ` } ]; this.ui.addSystemMessage(`Project context loaded. ${projectTypeInfo ? 'Detected ' + projectTypeInfo + '.' : ''} Ready for your questions!`); this.ui.focusInput(); } catch (error) { this.ui.showError(`Failed to start interactive mode: ${error.message}`); } } /** * Gère la commande pwd pour afficher le répertoire courant */ handlePwdCommand() { const currentDir = this.mcpFileService.pwd(); this.ui.addSystemMessage(`Current directory: ${currentDir}`); } /** * Gère la commande find pour rechercher des fichiers * @param {string} command - Commande complète */ async handleFindCommand(command) { try { // Extraire le motif et le chemin const parts = command.slice(6).trim().split(' '); if (parts.length === 0 || !parts[0]) { throw new Error('Search pattern is required. Usage: /find <pattern> [path]'); } const pattern = parts[0]; const dirPath = parts.length > 1 ? parts.slice(1).join(' ') : '.'; this.ui.startLoading(`Searching for files matching '${pattern}'...`); // Rechercher les fichiers avec MCPFileService const files = await this.mcpFileService.findFiles(pattern, dirPath); this.ui.stopLoading(); if (files.length === 0) { this.ui.addSystemMessage(`No files matching '${pattern}' found in ${dirPath}`); return; } let result = `Found ${files.length} file(s) matching '${pattern}' in ${dirPath}:\n\n`; files.forEach(file => { result += `- ${file.relativePath}\n`; }); this.ui.addSystemMessage(result); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Gère la commande grep pour rechercher du texte dans les fichiers * @param {string} command - Commande complète */ async handleGrepCommand(command) { try { // Extraire le motif, le filtre de fichiers et le chemin const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g); if (!parts || parts.length === 0) { throw new Error('Search pattern is required. Usage: /grep <pattern> [file_pattern] [path]'); } const pattern = parts[0].replace(/^"|"$/g, ''); const filePattern = parts.length > 1 ? parts[1].replace(/^"|"$/g, '') : '*'; const dirPath = parts.length > 2 ? parts.slice(2).join(' ').replace(/^"|"$/g, '') : '.'; this.ui.startLoading(`Searching for text '${pattern}' in files...`); // Rechercher dans les fichiers avec MCPFileService const results = await this.mcpFileService.grepFiles(pattern, filePattern, dirPath); this.ui.stopLoading(); if (results.length === 0) { this.ui.addSystemMessage(`No matches found for '${pattern}' in ${filePattern} files under ${dirPath}`); return; } let output = `Found ${results.length} match(es) for '${pattern}' in ${filePattern} files under ${dirPath}:\n\n`; const uniqueFiles = new Set(); results.forEach(result => uniqueFiles.add(result.file)); output += `Matches in ${uniqueFiles.size} file(s):\n\n`; let currentFile = ''; results.forEach(result => { if (currentFile !== result.file) { currentFile = result.file; output += `\n## ${result.file}\n`; } output += `Line ${result.line}: ${result.content.trim()}\n`; }); this.ui.addSystemMessage(output); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Gère la commande copy pour copier un fichier * @param {string} command - Commande complète */ async handleCopyCommand(command) { try { // Extraire les chemins source et destination const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g); if (!parts || parts.length < 2) { throw new Error('Source and destination paths are required. Usage: /copy <source> <destination>'); } const sourcePath = parts[0].replace(/^"|"$/g, ''); const destPath = parts[1].replace(/^"|"$/g, ''); // Vérifier si l'opération est autorisée if (!this.mcpService.isOperationAllowed('updateFile', destPath)) { throw new Error(`Copy operation to '${destPath}' is not allowed by security policy.`); } // Copier le fichier avec MCPFileService const result = await this.mcpFileService.copyFile(sourcePath, destPath); this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Gère la commande move pour déplacer un fichier * @param {string} command - Commande complète */ async handleMoveCommand(command) { try { // Extraire les chemins source et destination const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g); if (!parts || parts.length < 2) { throw new Error('Source and destination paths are required. Usage: /move <source> <destination>'); } const sourcePath = parts[0].replace(/^"|"$/g, ''); const destPath = parts[1].replace(/^"|"$/g, ''); // Vérifier si l'opération est autorisée if (!this.mcpService.isOperationAllowed('updateFile', destPath) || !this.mcpService.isOperationAllowed('deleteFile', sourcePath)) { throw new Error(`Move operation from '${sourcePath}' to '${destPath}' is not allowed by security policy.`); } // Déplacer le fichier avec MCPFileService const result = await this.mcpFileService.moveFile(sourcePath, destPath); this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Gère la commande delete pour supprimer un fichier * @param {string} command - Commande complète */ async handleDeleteCommand(command) { try { // Extraire le chemin const filePath = command.slice(8).trim(); if (!filePath) { throw new Error('File path is required. Usage: /delete <path>'); } // Vérifier si l'opération est autorisée if (!this.mcpService.isOperationAllowed('deleteFile', filePath)) { throw new Error(`Deletion of '${filePath}' is not allowed by security policy.`); } // Vérifier si c'est un fichier ou un répertoire const info = await this.mcpFileService.getInfo(filePath); let result; if (info.type === 'directory') { // Demander confirmation pour un répertoire const confirmMessage = `Are you sure you want to delete the directory '${filePath}'? This cannot be undone. (y/n)`; this.ui.addSystemMessage(confirmMessage); // Pour une véritable implémentation, il faudrait attendre la réponse utilisateur // Ici, on considère que c'est confirmé pour l'exemple result = await this.mcpFileService.removeDirectory(filePath, true); } else { // Supprimer le fichier avec MCPFileService result = await this.mcpFileService.removeFile(filePath); } this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Gère la commande info pour afficher les informations sur un fichier * @param {string} command - Commande complète */ async handleInfoCommand(command) { try { // Extraire le chemin const filePath = command.slice(6).trim(); if (!filePath) { throw new Error('File path is required. Usage: /info <path>'); } // Obtenir les informations avec MCPFileService const info = await this.mcpFileService.getInfo(filePath); let result = `Information about ${filePath}:\n\n`; result += `- Type: ${info.type}\n`; result += `- Size: ${this._formatSize(info.size)}\n`; result += `- Created: ${new Date(info.created).toLocaleString()}\n`; result += `- Modified: ${new Date(info.modified).toLocaleString()}\n`; result += `- Accessed: ${new Date(info.accessed).toLocaleString()}\n`; result += `- Readable: ${info.isReadable ? 'Yes' : 'No'}\n`; result += `- Writable: ${info.isWritable ? 'Yes' : 'No'}\n`; if (info.type === 'file') { result += `- Extension: ${info.extension || 'None'}\n`; result += `- MIME Type: ${info.mime || 'Unknown'}\n`; } this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Gère l'entrée utilisateur * @param {string} input - Texte saisi par l'utilisateur */ async handleUserInput(input) { try { // Ajouter le message utilisateur à la conversation this.conversation.push({ role: 'user', content: input }); // Utiliser le PromptService pour générer un meilleur prompt const promptConfig = this.promptService.generateSystemPrompt( input, this.projectContext ); // Mettre à jour le premier message système si nécessaire if (this.conversation.length > 1 && this.conversation[0].role === 'system') { this.conversation[0].content = promptConfig.systemPrompt; } if (this.config.get('enableStreaming')) { // Mode streaming (affichage progressif de la réponse) this.ui.startResponseStream(); let streamedResponse = ''; const onToken = (token) => { streamedResponse += token; this.ui.updateResponseStream(streamedResponse); }; // Obtenir une réponse du modèle en streaming await this.ollamaService.streamChat( this.conversation, promptConfig.temperature, this.config.get('maxTokens') || 4096, onToken ); this.ui.stopResponseStream(); // Analyser la réponse pour les commandes spéciales const processedResponse = await this.processResponse(streamedResponse); // Ajouter la réponse à la conversation this.conversation.push({ role: 'assistant', content: processedResponse }); // Afficher la réponse complète si elle a été modifiée if (processedResponse !== streamedResponse) { this.ui.addAssistantMessage(processedResponse); } } else { // Mode classique (sans streaming) this.ui.startLoading('Réflexion en cours...'); const response = await this.ollamaService.chat( this.conversation, promptConfig.temperature, this.config.get('maxTokens') || 4096 ); this.ui.stopLoading(); // Analyser la réponse pour les commandes spéciales const processedResponse = await this.processResponse(response); // Ajouter la réponse à la conversation this.conversation.push({ role: 'assistant', content: processedResponse }); // Afficher la réponse this.ui.addAssistantMessage(processedResponse); } } catch (error) { if (this.config.get('enableStreaming')) { this.ui.stopResponseStream(); } else { this.ui.stopLoading(); } this.ui.showError(`Error: ${error.message}`); } finally { this.ui.focusInput(); } } /** * Traite la réponse pour détecter les commandes spéciales * @param {string} response - Réponse du modèle * @returns {Promise<string>} - Réponse traitée */ async processResponse(response) { // Traiter les blocs de fichiers avec CodeAnalyzer const fileResult = await this.codeAnalyzer.processFileBlocks(response, true); let processedResponse = fileResult.modifiedResponse; // Ajouter un résumé des fichiers sauvegardés if (fileResult.savedFiles.length > 0) { const successFiles = fileResult.savedFiles.filter(f => f.success); if (successFiles.length > 0) { processedResponse += `\n\n${successFiles.length} file${successFiles.length > 1 ? 's' : ''} saved successfully.`; // Si on est en mode focus, mettre à jour le contexte if (this.focusMode) { await this.refreshFocusedContext(); } } } // Traiter les commandes shell const shellResult = await this.codeAnalyzer.processShellCommands(processedResponse, false); processedResponse = shellResult.modifiedResponse; // Utiliser MCPService pour détecter les intentions d'actions sur les fichiers // Important: Analyser à la fois la requête de l'utilisateur et la réponse du modèle const userMessage = this.conversation[this.conversation.length - 2]?.content || ''; // Toujours activer le debug pour voir les intentions console.log("Analysant l'intention utilisateur: ", userMessage); // Détecter les intentions dans le message de l'utilisateur d'abord // Analyse directe de l'entrée utilisateur pour des patterns simples const userIntents = []; // Trouver des patterns très simples comme "crée un fichier X avec Y" const simplePattern = /(?:cr[ée]e|crée|créer|create)\s+(?:un\s+)?(?:fichier|file)\s+([^\s]+)(?:\s+avec|\s+with|\s+containing|\s+contenant)\s+(.+)/i; const simpleMatch = simplePattern.exec(userMessage); if (simpleMatch) { const fileName = simpleMatch[1].trim(); const content = simpleMatch[2].trim(); console.log(`Détection simple: "${fileName}" avec "${content}"`); userIntents.push({ type: 'createFile', path: fileName, content: content, confidence: 0.99, position: 0 }); } else { // Si pas de pattern super simple, utiliser le détecteur complet const detectedIntents = this.mcpService.detectFileOperationIntents(userMessage); userIntents.push(...detectedIntents); } // Puis dans la réponse du modèle const responseIntents = this.mcpService.detectFileOperationIntents(response); // Combiner les intentions (déduplication gérée par MCPService) const fileIntents = [...userIntents, ...responseIntents]; // Toujours afficher les détails des intentions trouvées console.log(`Détection d'intentions: ${fileIntents.length} intentions trouvées`); if (fileIntents.length > 0) { fileIntents.forEach((intent, i) => console.log(`Intent ${i+1}: ${intent.type} - ${intent.path} - Contenu: ${intent.contentHint ? 'Oui (hint)' : (intent.content ? 'Oui' : 'Non')}`)); } if (fileIntents.length > 0) { // Exécuter les opérations détectées this.ui.startLoading('Processing file operations...'); const fileResults = await this.mcpService.executeFileOperations(fileIntents, response); this.ui.stopLoading(); // Ajouter un résumé des opérations effectuées const successOps = fileResults.filter(r => r.success); if (successOps.length > 0) { let summary = `\n\n${successOps.length} file operation${successOps.length > 1 ? 's' : ''} completed:`; successOps.forEach(op => { summary += `\n- ${op.message}`; }); processedResponse += summary; // Si on est en mode focus, mettre à jour le contexte if (this.focusMode) { await this.refreshFocusedContext(); } } // Ajouter les erreurs éventuelles const failedOps = fileResults.filter(r => !r.success); if (failedOps.length > 0) { let errors = `\n\n${failedOps.length} file operation${failedOps.length > 1 ? 's' : ''} failed:`; failedOps.forEach(op => { errors += `\n- ${op.message}`; }); processedResponse += errors; } } } // Extraire et éventuellement exécuter les blocs de code si l'exécution est activée if (this.config.get('enableCodeExecution')) { const codeBlocks = this.codeExecutor.extractCodeBlocks(processedResponse); // Exécuter automatiquement les blocs de code JavaScript si marqués comme exécutables if (processedResponse.includes('```javascript:run') || processedResponse.includes('```js:run')) { const executableBlocks = codeBlocks.filter(([lang]) => lang === 'javascript:run' || lang === 'js:run' ); if (executableBlocks.length > 0) { this.ui.addSystemMessage('Executing JavaScript code...'); for (const [_, code] of executableBlocks) { try { const result = await this.codeExecutor.executeCode('javascript', code); if (result.success) { processedResponse += `\n\n**Execution result:**\n\`\`\`\n${result.stdout || 'No output'}\n\`\`\``; } else { processedResponse += `\n\n**Execution failed:**\n\`\`\`\n${result.stderr || result.error}\n\`\`\``; } } catch (error) { processedResponse += `\n\n**Execution error:**\n\`\`\`\n${error.message}\n\`\`\``; } } } } } return processedResponse; } /** * Gère les commandes spéciales * @param {string} command - Commande saisie par l'utilisateur */ async handleCommand(command) { const cmd = command.trim().toLowerCase(); try { if (cmd === '/help') { this.showHelp(); } else if (cmd === '/exit' || cmd === '/quit') { this.handleExit(); process.exit(0); } else if (cmd === '/clear') { this.ui.clearChat(); } else if (cmd === '/init') { this.resetConversation(); } else if (cmd === '/refresh') { await this.refreshContext(); } else if (cmd === '/context') { this.showContext(); } else if (cmd.startsWith('/git')) { await this.handleGitCommand(cmd); } else if (cmd.startsWith('/run-code')) { await this.handleRunCodeCommand(cmd); } else if (cmd.startsWith('/file')) { await this.handleFileCommand(cmd); } else if (cmd.startsWith('/cd')) { await this.handleCdCommand(cmd); } else if (cmd.startsWith('/ls')) { await this.handleLsCommand(cmd); } else if (cmd.startsWith('/tree')) { await this.handleTreeCommand(cmd); } else if (cmd.startsWith('/find')) { await this.handleFindCommand(cmd); } else if (cmd.startsWith('/grep')) { await this.handleGrepCommand(cmd); } else if (cmd.startsWith('/mkdir')) { await this.handleMkdirCommand(cmd); } else if (cmd.startsWith('/write')) { await this.handleWriteCommand(cmd); } else if (cmd.startsWith('/read')) { await this.handleReadCommand(cmd); } else if (cmd.startsWith('/edit')) { await this.handleEditCommand(cmd); } else if (cmd.startsWith('/copy')) { await this.handleCopyCommand(cmd); } else if (cmd.startsWith('/move')) { await this.handleMoveCommand(cmd); } else if (cmd.startsWith('/delete')) { await this.handleDeleteCommand(cmd); } else if (cmd.startsWith('/info')) { await this.handleInfoCommand(cmd); } else if (cmd.startsWith('/run')) { await this.handleRunCommand(cmd); } else if (cmd.startsWith('/focus')) { await this.handleFocusCommand(cmd); } else if (cmd === '/unfocus') { await this.disableFocusMode(); } else if (cmd === '/project-info') { this.showProjectInfo(); } else if (cmd === '/suggest-commit') { await this.handleSuggestCommitCommand(); } else if (cmd === '/analyze-git') { await this.handleAnalyzeGitCommand(); } else if (cmd.startsWith('/pwd')) { this.handlePwdCommand(); } else { this.ui.showError(`Unknown command: ${command}. Type /help for available commands.`); } } catch (error) { this.ui.showError(`Error executing command: ${error.message}`); } finally { this.ui.focusInput(); } } /** * Affiche l'aide des commandes disponibles */ showHelp() { const helpText = ` Available commands: Basic commands: /help - Show this help message /exit, /quit - Exit the application /clear - Clear the chat history /init - Reset the conversation but keep project context /refresh - Refresh the project context /context - Show the current project context /project-info - Display detected project information Focus mode: /focus [path] - Focus on a specific file or directory /unfocus - Disable focus mode File Management: /pwd - Show current working directory /cd [path] - Change current directory /ls [path] - List directory contents /tree [path] [max_depth] - Display directory tree /find [pattern] [path] - Find files by pattern /grep [pattern] [file_pattern] - Search in file contents /mkdir [path] - Create a new directory /read [path] - Read file contents /write [path] [text] - Write text to a file /edit [path] [old] [new] - Replace text in a file /copy [source] [dest] - Copy file or directory /move [source] [dest] - Move file or directory /delete [path] - Delete file or directory /info [path] - Display file information /run [command] - Execute a system command /run-code [file] - Execute code from a file Git Commands: /git status - Show Git repository status /git add [path] - Stage files for commit /git commit -m "message" - Create a new commit /git pull - Pull changes from remote /git push - Push changes to remote /git log - Show commit history /git branch - List all branches /git checkout [branch] - Switch to a branch /suggest-commit - Get AI-generated commit message based on changes /analyze-git - Analyze recent Git activity You can also press Ctrl+G to access the Git menu. Press Ctrl+L to clear the screen. Press Ctrl+R to refresh the context. Press Ctrl+C to exit. `; this.ui.addSystemMessage(helpText); } /** * Réinitialise la conversation mais conserve le contexte du projet */ resetConversation() { const projectTypeInfo = this.projectInfo && this.projectInfo.language ? `Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` : ''; this.conversation = [ { role: 'system', content: `You are an AI coding assistant. You help with programming tasks and questions.\n\n${projectTypeInfo}\n\nCurrent project context:\n${this.projectContext}` } ]; this.ui.addSystemMessage('Conversation reset. Project context is preserved.'); } /** * Actualise le contexte du projet */ async refreshContext() { try { this.ui.startLoading('Refreshing project context...'); this.projectContext = await this.contextManager.getContext(); // Mettre à jour le message système avec le nouveau contexte et instructions pour les fichiers const projectTypeInfo = this.projectInfo && this.projectInfo.language ? `Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` : ''; if (this.conversation.length > 0 && this.conversation[0].role === 'system') { this.conversation[0].content = `You are an AI coding assistant. You help with programming tasks and questions. ${projectTypeInfo} Current project context: ${this.projectContext} IMPORTANT: When the user asks you to create or modify files, you can do so directly. If they ask you to "create a file named X", you can write the content and it will be automatically created. You do not need to use commands like /write or /edit. Just clearly mention the file name and provide the code. Examples: - When asked to create a file, you can say "Here's the content for file.js:" and then provide the code - When asked to modify a file, you can say "Here's the updated version of file.js:" and provide the full content `; } this.ui.stopLoading(); this.ui.addSystemMessage('Project context refreshed.'); } catch (error) { throw new Error(`Failed to refresh context: ${error.message}`); } } /** * Affiche le contexte du projet actuel */ showContext() { if (this.focusMode) { this.ui.addSystemMessage(`Currently focusing on: ${this.focusedPath}`); } this.ui.addSystemMessage('Current Project Context:'); this.ui.addSystemMessage(this.projectContext); } /** * Active le mode focus sur un fichier ou dossier spécifique * @param {string} path - Chemin du fichier ou dossier */ async enableFocusMode(path) { try { const stats = await this.fileService.getFileStats(path); if (stats.isFile() || stats.isDirectory()) { this.focusedPath = path; this.focusMode = true; // Si c'est un fichier, le marquer comme consulté if (stats.isFile()) { this.contextManager.trackFileAccess(path); } // Mettre à jour le contexte avec focus await this.refreshFocusedContext(); const relPath = this.fileService.getRelativePath(path); this.ui.addSystemMessage(`Focus activé sur: ${relPath}`); this.ui.updateStatus(`Focus: ${relPath} | Git: ${await this.gitService.isGitRepository() ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName}`); } else { throw new Error(`Chemin invalide: ${path}`); } } catch (error) { throw new Error(`Impossible d'activer le mode focus: ${error.message}`); } } /** * Désactive le mode focus */ async disableFocusMode() { this.focusedPath = null; this.focusMode = false; // Restaurer le contexte complet await this.refreshContext(); this.ui.addSystemMessage('Mode focus désactivé.'); this.ui.updateStatus(`Git: ${await this.gitService.isGitRepository() ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`); } /** * Rafraîchit le contexte en mode focus */ async refreshFocusedContext() { try { this.ui.startLoading('Actualisation du contexte focalisé...'); let focusedContext = ''; const stats = await this.fileService.getFileStats(this.focusedPath); if (stats.isFile()) { // Focus sur un seul fichier const content = await this.fileService.readFile(this.focusedPath); const relPath = this.fileService.getRelativePath(this.focusedPath); focusedContext = `# Fichier en focus: ${relPath}\n\n\`\`\`\n${content}\n\`\`\`\n`; // Ajouter les imports/dépendances si possible const deps = await this.codeAnalyzer.findDependencies(this.focusedPath); if (deps.length > 0) { focusedContext += '\n# Dépendances et imports:\n'; for (const dep of deps) { focusedContext += `- ${dep}\n`; } } } else if (stats.isDirectory()) { // Focus sur un dossier focusedContext = await this.contextManager.getContextForDirectory(this.focusedPath); } // Mettre à jour le contexte du projet this.projectContext = focusedContext; // Mettre à jour le message système avec le nouveau contexte const projectTypeInfo = this.projectInfo && this.projectInfo.language ? `Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` : ''; if (this.conversation.length > 0 && this.conversation[0].role === 'system') { this.conversation[0].content = `You are an AI coding assistant. You help with programming tasks and questions.\n\n${projectTypeInfo}\n\nCurrent focused context:\n${this.projectContext}`; } this.ui.stopLoading(); } catch (error) { this.ui.stopLoading(); throw new Error(`Failed to refresh focused context: ${error.message}`); } } /** * Gère la commande focus * @param {string} command - Commande complète */ async handleFocusCommand(command) { try { // Extraire le chemin const focusPath = command.slice(7).trim(); if (!focusPath) { throw new Error('Path is required for focus mode. Usage: /focus <path>'); } // Activer le mode focus await this.enableFocusMode(focusPath); } catch (error) { throw error; } } /** * Affiche les informations détectées sur le projet */ showProjectInfo() { if (!this.projectInfo) { this.ui.addSystemMessage('Project information not available. Try refreshing the context.'); return; } let infoText = '# Project Information\n\n'; infoText += `- **Language**: ${this.projectInfo.language || 'Not detected'}\n`; infoText += `- **Framework**: ${this.projectInfo.framework || 'None detected'}\n`; infoText += `- **Type**: ${this.projectInfo.type || 'Not detected'}\n`; if (this.projectInfo.buildTools && this.projectInfo.buildTools.length > 0) { infoText += `- **Build Tools**: ${this.projectInfo.buildTools.join(', ')}\n`; } if (this.projectInfo.testing && this.projectInfo.testing.length > 0) { infoText += `- **Testing**: ${this.projectInfo.testing.join(', ')}\n`; } if (this.projectInfo.database) { infoText += `- **Database**: ${this.projectInfo.database}\n`; } this.ui.addSystemMessage(infoText); } /** * Gère la commande de suggestion de message de commit */ async handleSuggestCommitCommand() { try { const isGitRepo = await this.gitService.isGitRepository(); if (!isGitRepo) { throw new Error('Not a Git repository. Use "/git init" to initialize one.'); } this.ui.startLoading('Analyzing changes and suggesting commit message...'); const suggestedMessage = await this.gitService.suggestCommitMessage(); this.ui.stopLoading(); this.ui.addSystemMessage(`Suggested commit message:\n\n${suggestedMessage}\n\nTo use this message, run:\n/git commit -m "${suggestedMessage}"`); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Gère la commande d'analyse Git */ async handleAnalyzeGitCommand() { try { const isGitRepo = await this.gitService.isGitRepository(); if (!isGitRepo) { throw new Error('Not a Git repository. Use "/git init" to initialize one.'); } this.ui.startLoading('Analyzing Git repository...'); const days = 7; // Par défaut, analyser les 7 derniers jours const analysis = await this.gitService.analyzeRecentChanges(days); this.ui.stopLoading(); let result = `# Git Repository Analysis (Last ${days} days)\n\n`; result += `- **Period**: ${analysis.period}\n`; result += `- **Total commits**: ${analysis.commitCount}\n\n`; if (analysis.authors && analysis.authors.length > 0) { result += '## Contributors\n\n'; analysis.authors.forEach(([author, count]) => { result += `- **${author}**: ${count} commit${count > 1 ? 's' : ''}\n`; }); result += '\n'; } if (analysis.mostChangedFiles && analysis.mostChangedFiles.length > 0) { result += '## Most Active Files\n\n'; analysis.mostChangedFiles.forEach(([file, count]) => { result += `- **${file}**: ${count} change${count > 1 ? 's' : ''}\n`; }); } this.ui.addSystemMessage(result); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Gère les commandes Git * @param {string} command - Commande Git complète */ async handleGitCommand(command) { // Vérifier si on est dans un dépôt Git const isGitRepo = await this.gitService.isGitRepository(); // Extraire les arguments de la commande Git const gitArgs = command.slice(5).trim().split(' '); const gitCommand = gitArgs[0]; if (!isGitRepo && gitCommand !== 'init') { throw new Error('Not a Git repository. Use "/git init" to initialize one.'); } this.ui.startLoading(`Executing Git command: ${gitCommand}...`); try { let result = ''; switch (gitCommand) { case 'init': result = await this.gitService.initRepository(); break; case 'status': const status = await this.gitService.status(); result = `Branch: ${status.current}\n`; result += `Staged changes: ${status.staged}\n`; result += `Changed files: ${status.changed}\n`; if (status.files.length > 0) { result += '\nFiles:\n'; status.files.forEach(file => { const state = file.working_dir === 'M' ? 'Modified' : file.working_dir === 'D' ? 'Deleted' : file.working_dir === 'A' ? 'Added' : 'Changed'; result += ` ${state}: ${file.path} ${file.staged ? '(staged)' : ''}\n`; }); } break; case 'add': const path = gitArgs.length > 1 ? gitArgs.slice(1).join(' ') : '.'; result = await this.gitService.add(path); break; case 'commit': if (gitArgs.includes('-m')) { const messageIndex = gitArgs.indexOf('-m') + 1; if (messageIndex < gitArgs.length) { let message = gitArgs[messageIndex]; // Si le message est entre guillemets, prendre tout ce qui est entre if (message.startsWith('"') && !message.endsWith('"')) { const msgParts = []; for (let i = messageIndex; i < gitArgs.length; i++) { msgParts.push(gitArgs[i]); if (gitArgs[i].endsWith('"')) break; } message = msgParts.join(' ').replace(/^"|"$/g, ''); } else if (message.startsWith('"') && message.endsWith('"')) { message = message.slice(1, -1); } result = await this.gitService.commit(message); } else { throw new Error('No commit message provided.'); } } else { throw new Error('Please use -m flag to provide a commit message.'); } break; case 'pull': const pullRemote = gitArgs.length > 1 ? gitArgs[1] : 'origin'; const pullBranch = gitArgs.length > 2 ? gitArgs[2] : null; result = await this.gitService.pull(pullRemote, pullBranch); break; case 'push': const pushRemote = gitArgs.length > 1 ? gitArgs[1] : 'origin'; const pushBranch = gitArgs.length > 2 ? gitArgs[2] : null; result = await this.gitService.push(pushRemote, pushBranch); break; case 'log': const logs = await this.gitService.log(); result = 'Recent commits:\n\n'; logs.forEach(commit => { result += `${commit.hash.substring(0, 7)} - ${commit.date.substring(0, 10)} - ${commit.message}\n`; result += `Author: ${commit.author_name} <${commit.author_email}>\n\n`; }); break; case 'branch': if (gitArgs.length === 1) { // Lister les branches const branches = await this.gitService.listBranches(); result = `Current branch: ${branches.current}\n\nAll branches:\n`; branches.all.forEach(branch => { result += `${branch === branches.current ? '* ' : ' '}${branch}\n`; }); } else { // Créer une branche result = await this.gitService.createBranch(gitArgs[1]); } break; case 'checkout': if (gitArgs.length < 2) { throw new Error('Branch name is required for checkout.'); } if (gitArgs[1] === '-b' && gitArgs.length > 2) { // Créer et basculer vers une nouvelle branche result = await this.gitService.createBranch(gitArgs[2]); } else { // Basculer vers une branche existante result = await this.gitService.checkout(gitArgs[1]); } break; case 'conflicts': const conflicts = await this.gitService.handleMergeConflicts(); if (conflicts.hasConflicts) { result = conflicts.message + '\n\n'; conflicts.files.forEach(file => { result += `- ${file.file}: ${file.sections} conflict section(s)\n`; }); result += '\nUse "/git show-conflict <file>" to view a specific conflict.'; } else { result = conflicts.message; } break; case 'show-conflict': if (gitArgs.length < 2) { throw new Error('File path is required. Usage: /git show-conflict <path>'); } const conflictPath = gitArgs.slice(1).join(' '); const conflictInfo = await this.gitService.showConflictsInFile(conflictPath); if (conflictInfo.hasConflicts) { result = `Found ${conflictInfo.conflictCount} conflict(s) in ${conflictPath}:\n\n`; conflictInfo.conflicts.forEach((conflict, i) => { result += `## Conflict ${i+1}:\n\n`; result += `Our version:\n\`\`\`\n${conflict.ours}\`\`\`\n\n`; result += `Their version:\n\`\`\`\n${conflict.theirs}\`\`\`\n\n`; }); result += 'To resolve, use: /git resolve-conflict <path> <strategy>'; } else { result = conflictInfo.message; } break; case 'resolve-conflict': if (gitArgs.length < 3) { throw new Error('File path and strategy required. Usage: /git resolve-conflict <path> <strategy>'); } const resolvePath = gitArgs[1]; const strategy = gitArgs[2]; const resolution = await this.gitService.resolveConflict(resolvePath, strategy); result = resolution.message; break; default: // Exécuter une commande Git arbitraire result = await this.gitService.raw(gitArgs); } this.ui.stopLoading(); this.ui.addSystemMessage(`Git ${gitCommand} result:\n${result}`); // Mettre à jour le statut Git dans la barre d'état const isGitRepoNow = await this.gitService.isGitRepository(); const statusText = this.focusMode ? `Focus: ${this.fileService.getRelativePath(this.focusedPath)} | Git: ${isGitRepoNow ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName}` : `Git: ${isGitRepoNow ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`; this.ui.updateStatus(statusText); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Gère la commande cd pour changer de répertoire * @param {string} command - Commande complète */ async handleCdCommand(command) { try { // Extraire le chemin const dirPath = command.slice(4).trim(); if (!dirPath) { // Aucun chemin spécifié, aller au répertoire utilisateur await this.mcpFileService.changeDirectory(process.env.HOME || process.env.USERPROFILE); } else { // Utiliser MCPFileService pour changer de répertoire await this.mcpFileService.changeDirectory(dirPath); } // Mettre à jour le répertoire courant this.currentDirectory = this.mcpFileService.pwd(); // Mettre à jour les autres services this.contextManager.basePath = this.currentDirectory; this.fileService.setBasePath(this.currentDirectory); this.gitService = new GitService(this.currentDirectory); this.mcpService.setBaseDirectory(this.currentDirectory); // Si on était en mode focus, le désactiver if (this.focusMode) { await this.disableFocusMode(); } else { // Sinon, juste actualiser le contexte await this.refreshContext(); } // Mettre à jour la barre d'état const isGitRepo = await this.gitService.isGitRepository(); this.ui.updateStatus(`Git: ${isGitRepo ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`); this.ui.addSystemMessage(`Changed directory to: ${this.currentDirectory}`); } catch (error) { throw error; } } /** * Gère la commande ls pour lister le contenu d'un répertoire * @param {string} command - Commande complète */ async handleLsCommand(command) { try { // Extraire le chemin const dirPath = command.slice(3).trim() || '.'; // Utiliser MCPFileService pour lister le répertoire const files = await this.mcpFileService.listDirectory(dirPath); // Formater la sortie let result = `Directory listing of ${dirPath || this.currentDirectory}:\n\n`; // Trier les fichiers: d'abord les répertoires, puis les fichiers const sortedFiles = [...files].sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); // Formater l'affichage avec plus d'informations sortedFiles.forEach(file => { const modified = new Date(file.modified).toLocaleString(); const sizeStr = file.isDirectory ? '-' : this._formatSize(file.size); const permissions = file.isHidden ? '[Hidden]' : ''; result += `${file.isDirectory ? '📁' : '📄'} ${file.name.padEnd(30)} ${sizeStr.padStart(10)} ${modified} ${permissions}\n`; }); this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Affiche l'arborescence du répertoire * @param {string} command - Commande complète (ex: /tree ou /tree .) */ async handleTreeCommand(command) { try { // Extraire le chemin et la profondeur const parts = command.slice(6).trim().split(' '); const dirPath = parts[0] || '.'; const maxDepth = parts[1] ? parseInt(parts[1]) : 3; this.ui.startLoading('Génération de l\'arborescence...'); // Générer l'arborescence avec MCPFileService const tree = await this.mcpFileService.getDirectoryTree(dirPath, maxDepth); this.ui.stopLoading(); this.ui.addSystemMessage(`Arborescence de ${dirPath || this.currentDirectory}:\n\n${tree}`); } catch (error) { this.ui.stopLoading(); throw error; } } /** * Formatte une taille en bytes en une chaîne lisible * @param {number} bytes - Taille en bytes * @returns {string} - Chaîne formatée */ _formatSize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } /** * Gère la commande mkdir pour créer un répertoire * @param {string} command - Commande complète */ async handleMkdirCommand(command) { try { // Extraire le chemin const dirPath = command.slice(7).trim(); if (!dirPath) { throw new Error('Directory path is required.'); } // Vérifier si la création est autorisée if (!this.mcpService.isOperationAllowed('createDirectory', dirPath)) { throw new Error(`Creation of directory '${dirPath}' is not allowed by security policy.`); } // Créer le répertoire avec MCPFileService const result = await this.mcpFileService.createDirectory(dirPath); this.ui.addSystemMessage(result); } catch (error) { throw error; } } /** * Gère la commande file pour manipuler des fichiers * @param {string} command - Commande complète */ async handleFileCommand(command) { try { // Extraire les arguments const args = command.slice(6).trim().split(' '); const subCommand = args[0]; switch (subCommand) { case 'read': if (args.length < 2) { throw new Error('File path is required for reading.'); } const readPath = args.slice(1).join(' '); const content = await this.fileService.readFile(readPath); this.ui.addSystemMessage(`File content of ${readPath}:\n\n\`\`\`\n${content}\n\`\`\``); break; case 'write': if