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

659 lines (583 loc) 21.2 kB
/** * Service MCP (Master Control Program) pour la gestion complète des fichiers et dossiers * Ce service permet à l'IA d'interagir avec le système de fichiers de manière autonome */ import fs from 'fs/promises'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import chalk from 'chalk'; import { fileURLToPath } from 'url'; const execAsync = promisify(exec); export class MCPFileService { /** * Initialise le service de fichiers MCP * @param {string} basePath - Chemin de base initial */ constructor(basePath = process.cwd()) { this.currentDirectory = basePath; // Log des opérations pour debug et audit this.operationLog = []; // Vérifier et créer un dossier .mcp-workspace si nécessaire this._initializeWorkspace(); } /** * Initialise l'espace de travail MCP * @private */ async _initializeWorkspace() { try { const mcpDir = path.join(this.currentDirectory, '.mcp-workspace'); try { await fs.mkdir(mcpDir, { recursive: true }); } catch (error) { // Ignore if directory already exists } // Créer un fichier journal pour les opérations this.logFilePath = path.join(mcpDir, 'mcp-operations.log'); try { await fs.access(this.logFilePath); } catch (error) { // Le fichier n'existe pas, le créer await fs.writeFile(this.logFilePath, 'MCP Operations Log\n=================\n\n'); } this._logOperation('MCP File Service initialized'); } catch (error) { console.error('Error initializing MCP workspace:', error.message); } } /** * Enregistre une opération dans le journal * @param {string} operation - Description de l'opération * @private */ async _logOperation(operation) { const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${operation}\n`; this.operationLog.push({ timestamp, operation }); // Écrire dans le fichier journal si disponible if (this.logFilePath) { try { await fs.appendFile(this.logFilePath, logEntry); } catch (error) { // Silently fail if we can't write to the log } } } /** * Résout un chemin relatif par rapport au répertoire courant * @param {string} relativePath - Chemin relatif * @returns {string} - Chemin absolu */ resolvePath(relativePath) { return path.resolve(this.currentDirectory, relativePath); } /** * Obtient un chemin relatif par rapport au répertoire courant * @param {string} absolutePath - Chemin absolu * @returns {string} - Chemin relatif */ getRelativePath(absolutePath) { return path.relative(this.currentDirectory, absolutePath); } /** * Change le répertoire courant * @param {string} dirPath - Chemin du nouveau répertoire * @returns {Promise<string>} - Message de résultat */ async changeDirectory(dirPath) { let targetDir; if (dirPath === '~') { // Aller au répertoire de l'utilisateur targetDir = process.env.HOME || process.env.USERPROFILE; } else if (dirPath === '..') { // Remonter d'un niveau targetDir = path.dirname(this.currentDirectory); } else if (dirPath === '.') { // Rester au même endroit targetDir = this.currentDirectory; } else if (path.isAbsolute(dirPath)) { // Chemin absolu targetDir = dirPath; } else { // Chemin relatif targetDir = this.resolvePath(dirPath); } try { const stats = await fs.stat(targetDir); if (!stats.isDirectory()) { throw new Error(`'${targetDir}' n'est pas un répertoire.`); } this.currentDirectory = targetDir; await this._logOperation(`Changed directory to: ${targetDir}`); return `Changed directory to: ${targetDir}`; } catch (error) { throw new Error(`Failed to change directory: ${error.message}`); } } /** * Affiche le répertoire de travail actuel * @returns {string} - Chemin du répertoire courant */ pwd() { return this.currentDirectory; } /** * Liste les fichiers et dossiers dans un répertoire * @param {string} dirPath - Chemin du répertoire (relatif ou absolu) * @returns {Promise<Array>} - Liste des fichiers et dossiers */ async listDirectory(dirPath = '.') { try { const targetDir = this.resolvePath(dirPath); const entries = await fs.readdir(targetDir, { withFileTypes: true }); const result = []; for (const entry of entries) { try { const fullPath = path.join(targetDir, entry.name); const stats = await fs.stat(fullPath); result.push({ name: entry.name, path: fullPath, isDirectory: entry.isDirectory(), size: stats.size, modified: stats.mtime, created: stats.birthtime, isHidden: entry.name.startsWith('.') }); } catch (error) { // Ignorer les erreurs individuelles (fichiers inaccessibles) result.push({ name: entry.name, path: path.join(targetDir, entry.name), isDirectory: entry.isDirectory(), error: error.message }); } } await this._logOperation(`Listed directory: ${targetDir}`); return result; } catch (error) { throw new Error(`Failed to list directory: ${error.message}`); } } /** * Vérifie si un fichier existe * @param {string} filePath - Chemin du fichier * @returns {Promise<boolean>} - True si le fichier existe */ async fileExists(filePath) { try { const targetPath = this.resolvePath(filePath); await fs.access(targetPath, fs.constants.F_OK); return true; } catch (error) { return false; } } /** * Lit le contenu d'un fichier * @param {string} filePath - Chemin du fichier * @returns {Promise<string>} - Contenu du fichier */ async readFile(filePath) { try { const targetPath = this.resolvePath(filePath); const content = await fs.readFile(targetPath, 'utf-8'); await this._logOperation(`Read file: ${targetPath}`); return content; } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } } /** * Écrit du contenu dans un fichier (crée ou écrase) * @param {string} filePath - Chemin du fichier * @param {string} content - Contenu à écrire * @returns {Promise<string>} - Message de résultat */ async writeFile(filePath, content) { try { const targetPath = this.resolvePath(filePath); // Créer le répertoire parent si nécessaire const dirPath = path.dirname(targetPath); await fs.mkdir(dirPath, { recursive: true }); await fs.writeFile(targetPath, content, 'utf-8'); await this._logOperation(`Wrote file: ${targetPath}`); return `File ${targetPath} successfully written.`; } catch (error) { throw new Error(`Failed to write file: ${error.message}`); } } /** * Ajoute du contenu à un fichier existant * @param {string} filePath - Chemin du fichier * @param {string} content - Contenu à ajouter * @returns {Promise<string>} - Message de résultat */ async appendFile(filePath, content) { try { const targetPath = this.resolvePath(filePath); // Créer le fichier s'il n'existe pas if (!(await this.fileExists(targetPath))) { await this.writeFile(targetPath, content); return `File ${targetPath} created with content.`; } await fs.appendFile(targetPath, content, 'utf-8'); await this._logOperation(`Appended to file: ${targetPath}`); return `Content appended to ${targetPath}.`; } catch (error) { throw new Error(`Failed to append to file: ${error.message}`); } } /** * Modifie un fichier en remplaçant du texte * @param {string} filePath - Chemin du fichier * @param {string} oldText - Texte à remplacer * @param {string} newText - Nouveau texte * @returns {Promise<string>} - Message de résultat */ async editFile(filePath, oldText, newText) { try { const targetPath = this.resolvePath(filePath); // Lire le contenu du fichier const content = await this.readFile(targetPath); // Remplacer le texte const newContent = content.replace(oldText, newText); // Si le contenu n'a pas changé, le texte n'a pas été trouvé if (newContent === content) { return `No changes made. Text '${oldText}' not found in ${targetPath}.`; } // Écrire le nouveau contenu await fs.writeFile(targetPath, newContent, 'utf-8'); await this._logOperation(`Edited file: ${targetPath}`); return `File ${targetPath} edited successfully.`; } catch (error) { throw new Error(`Failed to edit file: ${error.message}`); } } /** * Supprime un fichier * @param {string} filePath - Chemin du fichier * @returns {Promise<string>} - Message de résultat */ async removeFile(filePath) { try { const targetPath = this.resolvePath(filePath); await fs.unlink(targetPath); await this._logOperation(`Removed file: ${targetPath}`); return `File ${targetPath} removed.`; } catch (error) { throw new Error(`Failed to remove file: ${error.message}`); } } /** * Copie un fichier * @param {string} sourcePath - Chemin du fichier source * @param {string} destPath - Chemin de destination * @returns {Promise<string>} - Message de résultat */ async copyFile(sourcePath, destPath) { try { const sourceAbsPath = this.resolvePath(sourcePath); const destAbsPath = this.resolvePath(destPath); // Créer le répertoire de destination si nécessaire const destDir = path.dirname(destAbsPath); await fs.mkdir(destDir, { recursive: true }); await fs.copyFile(sourceAbsPath, destAbsPath); await this._logOperation(`Copied file from ${sourceAbsPath} to ${destAbsPath}`); return `File copied from ${sourceAbsPath} to ${destAbsPath}.`; } catch (error) { throw new Error(`Failed to copy file: ${error.message}`); } } /** * Déplace ou renomme un fichier * @param {string} sourcePath - Chemin du fichier source * @param {string} destPath - Chemin de destination * @returns {Promise<string>} - Message de résultat */ async moveFile(sourcePath, destPath) { try { const sourceAbsPath = this.resolvePath(sourcePath); const destAbsPath = this.resolvePath(destPath); // Créer le répertoire de destination si nécessaire const destDir = path.dirname(destAbsPath); await fs.mkdir(destDir, { recursive: true }); await fs.rename(sourceAbsPath, destAbsPath); await this._logOperation(`Moved file from ${sourceAbsPath} to ${destAbsPath}`); return `File moved from ${sourceAbsPath} to ${destAbsPath}.`; } catch (error) { throw new Error(`Failed to move file: ${error.message}`); } } /** * Crée un nouveau répertoire * @param {string} dirPath - Chemin du répertoire * @returns {Promise<string>} - Message de résultat */ async createDirectory(dirPath) { try { const targetPath = this.resolvePath(dirPath); await fs.mkdir(targetPath, { recursive: true }); await this._logOperation(`Created directory: ${targetPath}`); return `Directory ${targetPath} created.`; } catch (error) { throw new Error(`Failed to create directory: ${error.message}`); } } /** * Supprime un répertoire * @param {string} dirPath - Chemin du répertoire * @param {boolean} recursive - Supprimer récursivement * @returns {Promise<string>} - Message de résultat */ async removeDirectory(dirPath, recursive = false) { try { const targetPath = this.resolvePath(dirPath); if (recursive) { // Utiliser rimraf ou équivalent pour suppression récursive await fs.rm(targetPath, { recursive: true, force: true }); } else { await fs.rmdir(targetPath); } await this._logOperation(`Removed directory: ${targetPath}`); return `Directory ${targetPath} removed.`; } catch (error) { throw new Error(`Failed to remove directory: ${error.message}`); } } /** * Affiche l'arborescence d'un répertoire * @param {string} dirPath - Chemin du répertoire * @param {number} maxDepth - Profondeur maximale * @returns {Promise<string>} - Arborescence formatée */ async getDirectoryTree(dirPath = '.', maxDepth = 3) { const targetPath = this.resolvePath(dirPath); const result = []; // Fonction récursive pour construire l'arborescence const buildTree = async (currentPath, depth = 0, prefix = '') => { if (depth > maxDepth) { result.push(`${prefix}... (max depth reached)`); return; } try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); // Trier: répertoires d'abord, puis fichiers const sorted = [...entries].sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); // Parcourir les entrées for (let i = 0; i < sorted.length; i++) { const entry = sorted[i]; const isLast = i === sorted.length - 1; const entryPath = path.join(currentPath, entry.name); if (entry.name.startsWith('.') && depth > 0) { continue; // Ignorer les fichiers cachés après le premier niveau } if (entry.isDirectory()) { result.push(`${prefix}${isLast ? '└── ' : '├── '}📁 ${entry.name}`); await buildTree( entryPath, depth + 1, `${prefix}${isLast ? ' ' : '│ '}` ); } else { result.push(`${prefix}${isLast ? '└── ' : '├── '}📄 ${entry.name}`); } } } catch (error) { result.push(`${prefix}Error: ${error.message}`); } }; try { await buildTree(targetPath); await this._logOperation(`Generated tree for: ${targetPath}`); return result.join('\n'); } catch (error) { throw new Error(`Failed to generate tree: ${error.message}`); } } /** * Recherche des fichiers par motif * @param {string} pattern - Motif de recherche * @param {string} dirPath - Répertoire de départ * @returns {Promise<Array>} - Liste des fichiers trouvés */ async findFiles(pattern, dirPath = '.') { const targetDir = this.resolvePath(dirPath); const results = []; // Fonction récursive pour explorer les dossiers const explore = async (currentPath) => { try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { // Ignorer les dossiers cachés ou spécifiques if (entry.name.startsWith('.') || ['node_modules', 'dist', 'build'].includes(entry.name)) { continue; } await explore(fullPath); } else if (entry.name.includes(pattern) || new RegExp(pattern).test(entry.name)) { results.push({ path: fullPath, name: entry.name, relativePath: path.relative(this.currentDirectory, fullPath) }); } } } catch (error) { // Ignorer les erreurs (accès refusé, etc.) } }; await explore(targetDir); await this._logOperation(`Searched for files matching '${pattern}' in ${targetDir}`); return results; } /** * Recherche du texte dans les fichiers * @param {string} pattern - Motif de recherche * @param {string} filePattern - Type de fichiers à rechercher * @param {string} dirPath - Répertoire de départ * @returns {Promise<Array>} - Résultats de la recherche */ async grepFiles(pattern, filePattern = '*', dirPath = '.') { try { // Utiliser une commande grep/find si possible let command; const targetDir = this.resolvePath(dirPath); if (process.platform === 'win32') { // Windows command = `findstr /s /i /n "${pattern}" "${targetDir}\\${filePattern}"`; } else { // Unix/Linux/Mac command = `grep -r -n --include="${filePattern}" "${pattern}" "${targetDir}"`; } try { const { stdout } = await execAsync(command); const results = stdout.split('\n') .filter(line => line.trim()) .map(line => { const parts = line.split(':'); return { file: parts[0], line: parts[1], content: parts.slice(2).join(':') }; }); await this._logOperation(`Searched for text '${pattern}' in ${targetDir}`); return results; } catch (error) { // Fallback to manual search if command fails const results = []; const files = await this.findFiles(filePattern, dirPath); for (const file of files) { try { const content = await this.readFile(file.path); const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes(pattern) || new RegExp(pattern).test(line)) { results.push({ file: file.path, line: index + 1, content: line }); } }); } catch (error) { // Skip files that can't be read } } return results; } } catch (error) { throw new Error(`Failed to search in files: ${error.message}`); } } /** * Obtient des informations sur un fichier ou dossier * @param {string} filePath - Chemin du fichier/dossier * @returns {Promise<Object>} - Informations sur le fichier */ async getInfo(filePath) { try { const targetPath = this.resolvePath(filePath); const stats = await fs.stat(targetPath); let type = 'unknown'; if (stats.isFile()) type = 'file'; if (stats.isDirectory()) type = 'directory'; if (stats.isSymbolicLink()) type = 'symlink'; const result = { path: targetPath, type, size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isReadable: true, isWritable: true }; // Vérifier si le fichier est lisible/modifiable try { await fs.access(targetPath, fs.constants.R_OK); } catch (error) { result.isReadable = false; } try { await fs.access(targetPath, fs.constants.W_OK); } catch (error) { result.isWritable = false; } // Obtenir l'extension et le type MIME if (type === 'file') { result.extension = path.extname(targetPath).slice(1); result.basename = path.basename(targetPath); // Type MIME simplifié const extToMime = { 'js': 'application/javascript', 'ts': 'application/typescript', 'json': 'application/json', 'html': 'text/html', 'css': 'text/css', 'txt': 'text/plain', 'md': 'text/markdown', 'py': 'text/x-python', 'rb': 'text/x-ruby', 'java': 'text/x-java', 'c': 'text/x-c', 'cpp': 'text/x-c++', 'jpg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'pdf': 'application/pdf' }; result.mime = extToMime[result.extension] || 'application/octet-stream'; } await this._logOperation(`Retrieved info for: ${targetPath}`); return result; } catch (error) { throw new Error(`Failed to get file info: ${error.message}`); } } /** * Exécute une commande système * @param {string} command - Commande à exécuter * @returns {Promise<Object>} - Résultat de la commande */ async executeCommand(command) { try { const { stdout, stderr } = await execAsync(command, { cwd: this.currentDirectory, maxBuffer: 1024 * 1024 // 1MB buffer }); await this._logOperation(`Executed command: ${command}`); return { stdout, stderr }; } catch (error) { throw new Error(`Command execution failed: ${error.message}`); } } }