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

848 lines (728 loc) 32.5 kB
/** * MCP (Master Control Program) Service * * Ce service permet à l'IA d'interagir directement avec le système de fichiers * en analysant les intentions exprimées dans le texte et en exécutant les actions * appropriées pour créer, modifier ou supprimer des fichiers et dossiers. */ import fs from 'fs/promises'; import path from 'path'; import { execSync } from 'child_process'; export class MCPService { /** * Initialise le service MCP * @param {object} options Options de configuration */ constructor(options = {}) { this.baseDirectory = options.baseDirectory || process.cwd(); this.confirmDangerousOperations = options.confirmDangerousOperations !== false; this.debug = options.debug || false; // Pour le debug - console.log par défaut pour assurer la visibilité this.logFunction = console.log; // Permissions par défaut - tout autoriser pour une meilleure expérience utilisateur this.permissions = { createFile: true, createDirectory: true, updateFile: true, deleteFile: options.allowDelete !== undefined ? options.allowDelete : true, // Activer par défaut executeCommand: options.allowExecute !== undefined ? options.allowExecute : true, // Activer par défaut // Répertoires protégés qu'on ne peut pas modifier protectedDirs: ['/etc', '/bin', '/sbin', '/usr/bin', '/usr/sbin', '/boot', '/root', '/proc', '/sys'] }; } /** * Définit le répertoire de base pour les opérations * @param {string} directory Chemin du répertoire */ setBaseDirectory(directory) { this.baseDirectory = directory; } /** * Vérifie si une opération est autorisée en fonction des permissions * @param {string} operation Type d'opération * @param {string} targetPath Chemin cible * @returns {boolean} True si l'opération est autorisée */ isOperationAllowed(operation, targetPath) { if (!this.permissions[operation]) { return false; } // Vérifier que le chemin n'est pas dans un répertoire protégé const absolutePath = path.resolve(this.baseDirectory, targetPath); return !this.permissions.protectedDirs.some(dir => absolutePath.startsWith(dir) || absolutePath === dir ); } /** * Analyse du texte pour détecter les intentions de création/modification de fichiers * @param {string} text Texte à analyser * @returns {Array} Liste des intentions détectées */ detectFileOperationIntents(text) { const intents = []; // Vérifier d'abord les instructions simples du type "fichier.ext avec contenu" const simpleIntents = this._analyzeSimpleInstruction(text); intents.push(...simpleIntents); // Détection d'une suggestion de créer un fichier - expressions très inclusives const createFileRegex = /(?:cr[ée]e[rz]?|cr[ée]ation|g[ée]n[ée]re[rz]?|ajoute[rz]?|impl[ée]mente[rz]?|sauvegarde[rz]?|[ée]cri[rtsz]|enregistre[rz]?|create|make|add) (?:un|le|ce|du|nouveau|suivant|a|the|new|this|some) (?:fichier|script|module|code|document|classe|composant|programme|file|class|module)(?: (?:dans|sous|nommé|appelé|:|named|called|containing))? ["']?([^"'\n,;]+?)["']?(?:\.|,|;|\s|$|:|with|nommé|appelé|contenant)/gi; let match; while ((match = createFileRegex.exec(text)) !== null) { const filePath = match[1].trim(); if (filePath && !filePath.includes(' avec ') && !filePath.includes(' contenant ')) { intents.push({ type: 'createFile', path: filePath, confidence: 0.8, position: match.index }); } } // Détection avec le pattern "voici le contenu/code du fichier X" - supportant français et anglais const fileContentRegex = /(?:voici|cr[ée]er?|ceci est|voil[aà]|[ée]cri[rtse]?|here['']s|this is|here is|let's add|add)(?: le| the)? (?:contenu|code|interface|impl[ée]mentation|content|implementation|code for)(?: du| de la| de l['']| pour le| pour| of| for| in)? (?:fichier|classe|script|module|composant|file|class|component|module) ["']?([^"'\n,;]+?)["']?(?:\.|,|;|\s|:|$)/gi; while ((match = fileContentRegex.exec(text)) !== null) { const filePath = match[1].trim(); if (filePath) { intents.push({ type: 'createFile', path: filePath, confidence: 0.9, position: match.index }); } } // Détection d'une suggestion de créer un dossier - expressions améliorées const createDirRegex = /(?:cr[ée]e[rz]?|cr[ée]ation|g[ée]n[ée]re[rz]?|ajoute[rz]?|impl[ée]mente[rz]?|organise[rz]?) (?:un|le|ce|du|nouveau|suivant) (?:dossier|répertoire|directory|sous-dossier|folder)(?: (?:nommé|appelé|:))? ["']?([^"'\n,;]+?)["']?(?:\.|,|;|\s|$|:)/gi; while ((match = createDirRegex.exec(text)) !== null) { intents.push({ type: 'createDirectory', path: match[1].trim(), confidence: 0.8, position: match.index }); } // Détection de blocs de code avec indication du fichier destination // Amélioration: recherche plus large de blocs de code avec fichier de destination const codeBlockRegex = /```\w*\s*(?:\n|\r\n)[\s\S]*?(?:\n|\r\n)```/g; // Patterns plus inclusifs pour détecter les mentions de fichiers const filePathRegex = /(?:Fichier|File|Path|Chemin|Dans|Dans le fichier|Code pour|Code pour le fichier|Pour le fichier|Ajouter dans|[ÉE]crire dans|Créer dans|Placer dans|Enregistrer dans)[\s:]+["']?([^"'<>]+?)["']?(?:\s|$|\.|:|\))/i; const saveToRegex = /(?:enregistrer|sauvegarder|save to|write to|save|write|create|put in|place in|store in|add to|append to)[\s:]+["']?([^"'<>]+?)["']?(?:\s|$|\.|:|\))/i; // Pattern pour détecter les fichiers à créer à partir des noms de fichiers après les blocs de code const fileCommentRegex = /```[\w]*(?:\n|\r\n)[\s\S]*?(?:\n|\r\n)```\s*(?:\/\/|#|--)\s*(?:fichier|file|path|dans|in|to):\s*["']?([^"'\n<>]+?)["']?(?:\s|$|\.|,|;)/gi; // Détecter les fichiers mentionnés après les blocs de code let commentMatch; while ((commentMatch = fileCommentRegex.exec(text)) !== null) { const filePath = commentMatch[1].trim(); if (filePath) { // Extraire le code du bloc associé const codeBlockPart = commentMatch[0].substring(0, commentMatch[0].lastIndexOf('```') + 3); const code = codeBlockPart.substring(codeBlockPart.indexOf('\n') + 1, codeBlockPart.lastIndexOf('```')).trim(); intents.push({ type: 'createFile', path: filePath, content: code, confidence: 0.95, position: commentMatch.index }); } } let codeBlockMatch; while ((codeBlockMatch = codeBlockRegex.exec(text)) !== null) { // Chercher dans un contexte plus large avant et après le bloc de code const contextBefore = text.substring(Math.max(0, codeBlockMatch.index - 300), codeBlockMatch.index); const contextAfter = text.substring(codeBlockMatch.index + codeBlockMatch[0].length, Math.min(text.length, codeBlockMatch.index + codeBlockMatch[0].length + 100)); // Chercher les mentions de fichiers dans le contexte let filePath = null; let filePathMatch = filePathRegex.exec(contextBefore); if (filePathMatch) { filePath = filePathMatch[1].trim(); } else { filePathMatch = saveToRegex.exec(contextBefore); if (filePathMatch) { filePath = filePathMatch[1].trim(); } else { // Chercher aussi après le bloc de code filePathMatch = filePathRegex.exec(contextAfter); if (filePathMatch) { filePath = filePathMatch[1].trim(); } else { filePathMatch = saveToRegex.exec(contextAfter); if (filePathMatch) { filePath = filePathMatch[1].trim(); } } } } if (filePath) { // Extraire le code du bloc const codeBlock = codeBlockMatch[0]; const code = codeBlock.substring(codeBlock.indexOf('\n') + 1, codeBlock.lastIndexOf('```')).trim(); // Éviter les doublons avec le commentaire après le bloc const alreadyHasIntent = intents.some(intent => intent.type === 'createFile' && intent.path === filePath && Math.abs(intent.position - codeBlockMatch.index) < 500 ); if (!alreadyHasIntent) { intents.push({ type: 'createFile', path: filePath, content: code, confidence: 0.9, position: codeBlockMatch.index }); } } } // Détection directe d'un fichier mentionné avec son extension const filenameWithExtRegex = /\b(\w+\.[a-zA-Z0-9]{1,4})\b/g; while ((match = filenameWithExtRegex.exec(text)) !== null) { const fileName = match[1].trim(); // Ne pas inclure les noms de fichiers trop génériques ou connus if (fileName && !['readme.md', 'index.html', 'style.css', 'script.js'].includes(fileName.toLowerCase())) { // Vérifier s'il y a du contexte autour du nom de fichier qui suggère une création const contextBefore = text.substring(Math.max(0, match.index - 30), match.index); const contextAfter = text.substring(match.index + match[0].length, Math.min(text.length, match.index + match[0].length + 30)); // Si le nom du fichier est mentionné dans un contexte de création if (contextBefore.match(/cr[ée]e|creat|nouv|new|add|ajout/i) || contextAfter.match(/avec|with|content|contenu/i)) { intents.push({ type: 'createFile', path: fileName, confidence: 0.85, position: match.index }); } } } // Détection d'une demande de modification de fichier - expressions améliorées et bilingues const modifyFileRegex = /(?:modifie[rz]?|change[rz]?|update[rz]?|mets? [àa] jour|remplace[rz]?|corrige[rz]?|fixe[rz]?|am[ée]liore[rz]?|optimise[rz]?|ajuste[rz]?|adapte[rz]?|r[ée]vise[rz]?|modify|edit|change|update|fix|improve) (?:le fichier|le code|le contenu|la classe|le composant|le module|le script|l['']impl[ée]mentation|ce fichier|cette classe|the file|the code|the content|the class|this file)(?: dans| de| du| in| of)? ["']?([^"'\n,;]+?)["']?(?:\.|,|;|\s|$|:|pour|to)/gi; while ((match = modifyFileRegex.exec(text)) !== null) { const filePath = match[1].trim(); if (filePath && !filePath.includes(' avec ') && !filePath.includes(' pour ')) { intents.push({ type: 'updateFile', path: filePath, confidence: 0.7, position: match.index }); } } // Détection avec le pattern "le fichier X doit être modifié" const needsUpdateRegex = /(?:le fichier|la classe|le module|le composant|le script) ["']?([^"'\n,;]+?)["']? (?:doit|devrait|doit [êe]tre|devrait [êe]tre|a besoin d['']être|n[ée]cessite d['']être) (?:modifi[ée]|chang[ée]|mis[e]? [àa] jour|corrig[ée]|adapt[ée]|optimis[ée]|r[ée]vis[ée])/gi; while ((match = needsUpdateRegex.exec(text)) !== null) { intents.push({ type: 'updateFile', path: match[1].trim(), confidence: 0.7, position: match.index }); } return this._removeDuplicateIntents(intents); } /** * Filtre les intentions redondantes * @param {Array} intents Liste des intentions * @returns {Array} Liste filtrée */ _removeDuplicateIntents(intents) { // Trier par position dans le texte (pour privilégier les plus récentes) intents.sort((a, b) => b.position - a.position); // Filtrer les doublons (même chemin) const seen = new Set(); return intents.filter(intent => { const key = `${intent.type}_${intent.path}`; if (seen.has(key)) return false; seen.add(key); return true; }); } /** * Exécute les opérations sur les fichiers en fonction des intentions détectées * @param {Array} intents Liste des intentions * @param {string} responseText Texte complet de la réponse (pour extraire le contenu) * @returns {Array} Résultats des opérations */ async executeFileOperations(intents, responseText) { const results = []; // Pour le debug if (this.debug) { this.logFunction(`MCP: Exécution de ${intents.length} opérations de fichier détectées`); intents.forEach((intent, i) => { this.logFunction(`MCP: Intent ${i+1}: ${intent.type} - ${intent.path} (confidence: ${intent.confidence})`); }); } for (const intent of intents) { try { switch (intent.type) { case 'createFile': if (this.isOperationAllowed('createFile', intent.path)) { // Si le contenu n'est pas fourni, on cherche dans les blocs de code ou l'indice de contenu if (!intent.content) { if (intent.contentHint) { // Utiliser l'indice de contenu détecté dans l'instruction simple intent.content = intent.contentHint; if (this.debug) { console.log(`Utilisation du contenu suggéré pour ${intent.path}`); } } else { intent.content = this._findCodeForFile(responseText, intent.path); } } if (intent.content) { // S'assurer que le chemin du fichier est correctement formaté let filePath = intent.path; // Si le chemin ne contient pas d'extension et qu'on peut la déduire du contenu if (!path.extname(filePath) && intent.content) { // Essayer de détecter l'extension à partir du contenu const extension = this._detectFileExtension(intent.content); if (extension) { filePath = `${filePath}.${extension}`; } } await this._createFile(filePath, intent.content); if (this.debug) { this.logFunction(`MCP: Fichier créé avec succès: ${filePath}`); } results.push({ success: true, type: 'createFile', path: filePath, message: `Fichier créé: ${filePath}` }); } else { if (this.debug) { this.logFunction(`MCP: Impossible de trouver le contenu pour ${intent.path}`); } results.push({ success: false, type: 'createFile', path: intent.path, message: `Impossible de trouver le contenu pour ${intent.path}` }); } } else { results.push({ success: false, type: 'createFile', path: intent.path, message: `Opération non autorisée: création de ${intent.path}` }); } break; case 'createDirectory': if (this.isOperationAllowed('createDirectory', intent.path)) { await this._createDirectory(intent.path); if (this.debug) { this.logFunction(`MCP: Dossier créé avec succès: ${intent.path}`); } results.push({ success: true, type: 'createDirectory', path: intent.path, message: `Dossier créé: ${intent.path}` }); } else { results.push({ success: false, type: 'createDirectory', path: intent.path, message: `Opération non autorisée: création du dossier ${intent.path}` }); } break; case 'updateFile': if (this.isOperationAllowed('updateFile', intent.path)) { // Trouver le contenu de mise à jour const newContent = this._findCodeForFile(responseText, intent.path); if (newContent) { await this._updateFile(intent.path, newContent); results.push({ success: true, type: 'updateFile', path: intent.path, message: `Fichier mis à jour: ${intent.path}` }); } else { results.push({ success: false, type: 'updateFile', path: intent.path, message: `Impossible de trouver le nouveau contenu pour ${intent.path}` }); } } else { results.push({ success: false, type: 'updateFile', path: intent.path, message: `Opération non autorisée: modification de ${intent.path}` }); } break; } } catch (error) { results.push({ success: false, type: intent.type, path: intent.path, message: `Erreur: ${error.message}` }); if (this.debug) { console.error(`MCP Error (${intent.type} - ${intent.path}):`, error); } } } return results; } /** * Crée un fichier avec le contenu spécifié * @param {string} filePath Chemin relatif du fichier * @param {string} content Contenu du fichier */ async _createFile(filePath, content) { const fullPath = path.resolve(this.baseDirectory, filePath); // Créer les répertoires parents si nécessaire await fs.mkdir(path.dirname(fullPath), { recursive: true }); // Créer le fichier await fs.writeFile(fullPath, content); if (this.debug) { this.logFunction(`MCP: Fichier créé - ${fullPath}`); } return true; } /** * Crée un répertoire * @param {string} dirPath Chemin relatif du répertoire */ async _createDirectory(dirPath) { const fullPath = path.resolve(this.baseDirectory, dirPath); await fs.mkdir(fullPath, { recursive: true }); if (this.debug) { this.logFunction(`MCP: Répertoire créé - ${fullPath}`); } return true; } /** * Met à jour un fichier existant * @param {string} filePath Chemin relatif du fichier * @param {string} newContent Nouveau contenu */ async _updateFile(filePath, newContent) { const fullPath = path.resolve(this.baseDirectory, filePath); // Vérifier si le fichier existe try { await fs.access(fullPath); } catch (error) { // Si le fichier n'existe pas, on le crée return this._createFile(filePath, newContent); } // Sauvegarder l'ancien contenu pour le debug if (this.debug) { const oldContent = await fs.readFile(fullPath, 'utf-8'); this.logFunction(`MCP: Modification du fichier ${fullPath}`); this.logFunction(`MCP: Ancien contenu: ${oldContent.substring(0, 100)}...`); this.logFunction(`MCP: Nouveau contenu: ${newContent.substring(0, 100)}...`); } // Mettre à jour le fichier await fs.writeFile(fullPath, newContent); return true; } /** * Détecte l'extension de fichier à partir du contenu * @param {string} content - Contenu du fichier * @returns {string|null} - Extension détectée ou null * @private */ _detectFileExtension(content) { // Signatures pour détecter les types de fichier const signatures = [ { pattern: /^(?:import|export|const|let|var|function|class|interface|type)/m, ext: 'js' }, { pattern: /^(?:import|export|const|let|var|function|class|interface|type).*:\s+/m, ext: 'ts' }, { pattern: /<\?php/m, ext: 'php' }, { pattern: /^(?:def|class|if\s+__name__\s*==\s*['"]__main__['"]|import\s+\w+|from\s+\w+\s+import)/m, ext: 'py' }, { pattern: /^(?:<!DOCTYPE\s+html|<html|<head|<body)/mi, ext: 'html' }, { pattern: /^(?:body|html|div|\.[\w-]+|#[\w-]+)\s*\{/m, ext: 'css' }, { pattern: /^(?:\{|\[)[\s\n]*(?:"[\w]+":|'[\w]+':|[\w]+:)/m, ext: 'json' }, { pattern: /^#!/m, ext: 'sh' }, { pattern: /^public\s+(?:class|interface|enum)\s+\w+/m, ext: 'java' }, { pattern: /^#include/m, ext: 'c' }, { pattern: /^#include\s+<(?:vector|iostream|string|map|memory)/m, ext: 'cpp' }, { pattern: /^using\s+System;|^namespace\s+\w+/m, ext: 'cs' }, { pattern: /^(?:module|require\s+['"][.\w]+['"])/m, ext: 'rb' }, { pattern: /^package\s+main/m, ext: 'go' }, { pattern: /^import\s+React|^import.*from\s+['"]react['"]|^const\s+\w+\s*=\s*\(\s*props\s*\)\s*=>/m, ext: 'jsx' }, { pattern: /^import\s+React|^import.*from\s+['"]react['"]|^\s*<[A-Z]\w+(?:\s+[\w]+\s*=\s*(?:{.*}|["'].*["']))*\s*>/m, ext: 'tsx' }, { pattern: /^#\s+[\w\s]+/m, ext: 'md' } ]; for (const { pattern, ext } of signatures) { if (pattern.test(content)) { return ext; } } return null; } /** * Analyse le message pour extraire une simple instruction de création de fichier avec contenu * @param {string} text - Texte à analyser * @returns {Array} - Liste des intentions détectées (fichier + contenu) */ _analyzeSimpleInstruction(text) { const intents = []; // Pattern très inclusif: "<nom_fichier.ext> avec <contenu>" const simplePattern = /\b(\w+\.\w+)\s+(?:avec|with|contenant|containing)\s+(.+)/gim; let match; while ((match = simplePattern.exec(text)) !== null) { const fileName = match[1].trim(); const contentHint = match[2].trim(); if (fileName && contentHint) { if (this.debug) { console.log(`Instruction simple détectée: ${fileName} avec ${contentHint.substring(0, 20)}...`); } intents.push({ type: 'createFile', path: fileName, contentHint: contentHint, confidence: 0.95, position: match.index }); } } return intents; } /** * Trouve le code approprié pour un fichier dans le texte de réponse * @param {string} text Texte complet de la réponse * @param {string} filePath Chemin du fichier cible * @returns {string|null} Contenu du code ou null */ _findCodeForFile(text, filePath) { // Récupérer le nom du fichier sans le chemin complet pour la recherche const fileName = path.basename(filePath); // Récupérer l'extension du fichier const ext = path.extname(filePath).slice(1); let language = ext; // Mapping des extensions courantes vers les langages const langMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'typescript', 'tsx': 'typescript', 'py': 'python', 'rb': 'ruby', 'php': 'php', 'go': 'go', 'java': 'java', 'c': 'c', 'cpp': 'cpp', 'cs': 'csharp', 'html': 'html', 'css': 'css', 'json': 'json', 'md': 'markdown', 'sh': 'bash' }; // Normaliser le langage if (langMap[ext]) { language = langMap[ext]; } // Patterns pour les commentaires qui pourraient indiquer le fichier const commentPatterns = { 'javascript': ['// ', '/* ', '*/'], 'typescript': ['// ', '/* ', '*/'], 'python': ['# ', '"""', '"""'], 'ruby': ['# ', '=begin', '=end'], 'php': ['// ', '/* ', '*/'], 'go': ['// ', '/* ', '*/'], 'java': ['// ', '/* ', '*/'], 'c': ['// ', '/* ', '*/'], 'cpp': ['// ', '/* ', '*/'], 'csharp': ['// ', '/* ', '*/'], 'html': ['<!-- ', ' -->'], 'css': ['/* ', '*/'], 'bash': ['# '] }; // --- 1. Recherche explicite avec le nom de fichier exact avant ou après un bloc de code --- // Échapper les caractères spéciaux dans le nom de fichier pour l'expression régulière const escapedFileName = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedFilePath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Créer un pattern pour trouver un bloc de code avec mention explicite du fichier const filenameMentionPattern = `(?:${[ `(?:Fichier|File|Path|Chemin|Code pour|Dans|Dans le fichier|Pour|Pour le fichier|Code de)\\s*[:=]\\s*["'\']\`?${escapedFilePath}["'\']\`?`, `(?:Fichier|File|Path|Chemin|Code pour|Dans|Dans le fichier|Pour|Pour le fichier|Code de)\\s*[:=]\\s*["'\']\`?${escapedFileName}["'\']\`?`, `["'\']\`?${escapedFilePath}["'\']\`?\\s*:`, `["'\']\`?${escapedFileName}["'\']\`?\\s*:` ].join('|')})`; const filenameMentionRegex = new RegExp(`${filenameMentionPattern}[^\\n]*\\n\\s*\`\`\`(?:${language})?\\s*\\n([\\s\\S]*?)\\n\`\`\`|\\s*\`\`\`(?:${language})?\\s*\\n([\\s\\S]*?)\\n\`\`\`\\s*\\n[^\\n]*${filenameMentionPattern}`, 'i'); const specificMatch = filenameMentionRegex.exec(text); if (specificMatch) { // Le pattern peut matcher dans deux groupes différents selon si la mention est avant ou après return (specificMatch[1] || specificMatch[2]).trim(); } // --- 2. Recherche de blocs de code avec commentaires indiquant le fichier --- const codeBlocks = []; const codeBlockRegex = /```(\w*)\s*\n([\s\S]*?)\n```/g; let blockMatch; while ((blockMatch = codeBlockRegex.exec(text)) !== null) { const blockLang = blockMatch[1].trim(); const blockContent = blockMatch[2]; const blockStart = blockMatch.index; const blockEnd = blockMatch.index + blockMatch[0].length; const contextBefore = text.substring(Math.max(0, blockStart - 300), blockStart); const contextAfter = text.substring(blockEnd, Math.min(text.length, blockEnd + 200)); // Vérifier si le nom du fichier est mentionné dans le contexte const filenameMention = contextBefore.includes(filePath) || contextBefore.includes(fileName) || contextAfter.includes(filePath) || contextAfter.includes(fileName); // Vérifier le contenu pour des commentaires avec le nom du fichier const hasFileComment = blockContent.includes(fileName) || blockContent.includes(filePath); codeBlocks.push({ language: blockLang, content: blockContent, filenameMention, hasFileComment, languageMatch: blockLang === language || (language && blockLang.includes(language)) || (langMap[blockLang] === language), position: blockStart }); } // Trier les blocs de code par pertinence const sortedBlocks = codeBlocks.sort((a, b) => { // Prioriser les blocs qui mentionnent explicitement le fichier if (a.filenameMention && !b.filenameMention) return -1; if (!a.filenameMention && b.filenameMention) return 1; // Ensuite prioriser les blocs qui ont un commentaire avec le nom du fichier if (a.hasFileComment && !b.hasFileComment) return -1; if (!a.hasFileComment && b.hasFileComment) return 1; // Ensuite prioriser les blocs qui correspondent au langage du fichier if (a.languageMatch && !b.languageMatch) return -1; if (!a.languageMatch && b.languageMatch) return 1; // Enfin, prioriser les blocs qui apparaissent plus tôt dans la réponse return a.position - b.position; }); // Prendre le meilleur bloc if (sortedBlocks.length > 0) { return sortedBlocks[0].content.trim(); } // --- 3. Dernier recours: prendre n'importe quel bloc de code --- const anyCodeBlockRegex = /```\w*\s*\n([\s\S]*?)\n```/g; blockMatch = anyCodeBlockRegex.exec(text); if (blockMatch) { return blockMatch[1].trim(); } return null; } /** * Exécute une commande système * @param {string} command Commande à exécuter * @returns {object} Résultat de l'exécution */ async executeCommand(command) { if (!this.permissions.executeCommand) { throw new Error("L'exécution de commandes n'est pas autorisée"); } try { const output = execSync(command, { cwd: this.baseDirectory, encoding: 'utf-8', timeout: 10000, // 10 secondes max maxBuffer: 1024 * 1024 // 1MB max output }); return { success: true, output }; } catch (error) { return { success: false, error: error.message, stderr: error.stderr?.toString(), stdout: error.stdout?.toString() }; } } /** * Navigue dans la structure de fichiers pour analyser le projet * @param {string} startPath Chemin de départ * @param {object} options Options de recherche * @returns {Array} Liste des fichiers trouvés */ async exploreProject(startPath = '.', options = {}) { const fullStartPath = path.resolve(this.baseDirectory, startPath); const maxDepth = options.maxDepth || 3; const maxFiles = options.maxFiles || 50; const ignoreDirs = options.ignoreDirs || [ 'node_modules', '.git', 'dist', 'build', 'target', 'bin', 'obj', 'out', '.idea', '.vscode' ]; const ignorePatterns = options.ignorePatterns || [ /\.pyc$/, /\.class$/, /\.o$/, /\.obj$/, /\.dll$/, /\.exe$/, /\.so$/, /\.jpg$/, /\.png$/, /\.gif$/, /\.pdf$/, /\.zip$/, /\.rar$/ ]; const files = []; // Fonction récursive pour explorer les fichiers const explore = async (dirPath, depth = 0) => { if (depth > maxDepth || files.length >= maxFiles) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const relativePath = path.relative(this.baseDirectory, fullPath); // Ignorer les répertoires spécifiés if (entry.isDirectory() && ignoreDirs.includes(entry.name)) { continue; } // Ignorer les fichiers correspondant aux patterns if (entry.isFile() && ignorePatterns.some(pattern => pattern.test(entry.name))) { continue; } if (entry.isFile()) { files.push(relativePath); if (files.length >= maxFiles) break; } else if (entry.isDirectory()) { await explore(fullPath, depth + 1); } } } catch (error) { if (this.debug) { console.error(`Erreur lors de l'exploration de ${dirPath}:`, error); } } }; await explore(fullStartPath); return files; } /** * Lit et analyse les fichiers importants du projet * @param {Array} filePaths Liste des chemins de fichiers * @param {number} maxSize Taille maximale en octets * @returns {object} Contenu des fichiers */ async readProjectFiles(filePaths, maxSize = 30000) { const result = {}; for (const filePath of filePaths) { try { const fullPath = path.resolve(this.baseDirectory, filePath); // Vérifier la taille du fichier avant de le lire const stats = await fs.stat(fullPath); if (stats.size > maxSize) { // Fichier trop volumineux, le lire partiellement const handle = await fs.open(fullPath, 'r'); const buffer = Buffer.alloc(maxSize); await handle.read(buffer, 0, maxSize, 0); await handle.close(); result[filePath] = buffer.toString('utf-8') + '\n... [Contenu tronqué, fichier trop volumineux] ...'; } else { // Lire le fichier complet const content = await fs.readFile(fullPath, 'utf-8'); result[filePath] = content; } } catch (error) { result[filePath] = `[Erreur lors de la lecture: ${error.message}]`; } } return result; } }