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
JavaScript
/**
* 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;
}
}