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