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

635 lines (546 loc) 21.6 kB
/** * Mode interactif avec interface simplifiée pour les connexions distantes * Avec support complet pour la gestion de fichiers */ import readline from 'readline'; import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; import { marked } from 'marked'; import TerminalRenderer from 'marked-terminal'; import { spawn } from 'child_process'; import { createInterface } from 'readline'; // Configuration du renderer pour Markdown dans le terminal marked.setOptions({ renderer: new TerminalRenderer({ code: chalk.cyan, blockquote: chalk.gray.italic, table: chalk.white, listitem: chalk.yellow, strong: chalk.bold.green, em: chalk.italic.cyan, heading: chalk.bold.blueBright, }) }); export class SimpleInteractiveMode { /** * Initialise le mode interactif simplifié * @param {OllamaService} ollamaService - Service Ollama * @param {ContextManager} contextManager - Gestionnaire de contexte */ constructor(ollamaService, contextManager) { this.ollamaService = ollamaService; this.contextManager = contextManager; this.conversation = []; this.projectContext = ''; this.currentDirectory = process.cwd(); // Interface de ligne de commande this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '>>> ' }); } /** * Démarre le mode interactif */ async start() { console.log(chalk.green.bold('\nOllama Code - Mode Interactif Simplifié')); console.log(chalk.blue(`Modèle: ${this.ollamaService.modelName}`)); console.log(chalk.blue(`Répertoire: ${this.currentDirectory}`)); console.log(chalk.yellow('Tapez /help pour voir les commandes disponibles.\n')); // Initialiser la conversation this.conversation = [ { role: 'system', content: `Tu es un assistant IA de développement. Tu aides avec les questions de programmation et les tâches de codage.` } ]; // Démarrer la boucle d'interaction this.promptUser(); } /** * Affiche le prompt et attend l'entrée utilisateur */ promptUser() { this.rl.question('>>> ', async (input) => { await this.processInput(input); this.promptUser(); }); } /** * Traite l'entrée utilisateur * @param {string} input - Entrée utilisateur */ async processInput(input) { // Commandes spéciales if (input.startsWith('/')) { await this.processCommand(input); return; } // Traitement normal de la demande if (input.trim()) { try { console.log(chalk.yellow('Réflexion en cours...')); // Ajouter la question à la conversation this.conversation.push({ role: 'user', content: input }); // Obtenir une réponse const response = await this.ollamaService.chat(this.conversation); // Vérifier si la réponse contient des instructions pour créer/éditer des fichiers const processedResponse = await this.processFileOperations(response); // Ajouter la réponse à la conversation this.conversation.push({ role: 'assistant', content: processedResponse }); // Afficher la réponse console.log('\n' + marked(processedResponse) + '\n'); } catch (error) { console.error(chalk.red(`Erreur: ${error.message}`)); } } } /** * Traite les opérations de fichiers dans la réponse * @param {string} response - Réponse de l'assistant * @returns {Promise<string>} - Réponse potentiellement modifiée */ async processFileOperations(response) { // Recherche les instructions pour créer des fichiers // Format: ```file:path/to/file.ext\ncontent``` let modifiedResponse = response; const fileRegex = /```file:(.*?)\n([\s\S]*?)```/g; const fileMatches = [...response.matchAll(fileRegex)]; if (fileMatches.length > 0) { for (const match of fileMatches) { const filePath = match[1].trim(); const content = match[2]; try { const fullPath = path.resolve(this.currentDirectory, filePath); // Créer les répertoires parents si nécessaire await fs.mkdir(path.dirname(fullPath), { recursive: true }); // Écrire le fichier await fs.writeFile(fullPath, content); // Remplacer le bloc dans la réponse modifiedResponse = modifiedResponse.replace( match[0], `Fichier créé: \`${filePath}\`\n\`\`\`\n${content}\n\`\`\`` ); console.log(chalk.green(`✓ Fichier créé: ${filePath}`)); } catch (error) { console.error(chalk.red(`Erreur lors de la création du fichier ${filePath}: ${error.message}`)); } } } return modifiedResponse; } /** * Traite les commandes spéciales * @param {string} command - Commande à traiter */ async processCommand(command) { const cmd = command.trim().toLowerCase(); try { if (cmd === '/help' || cmd === '/?') { this.showHelp(); } else if (cmd === '/exit' || cmd === '/quit') { console.log(chalk.green('Au revoir!')); this.rl.close(); process.exit(0); } else if (cmd === '/clear') { console.clear(); } else if (cmd === '/context') { await this.loadContext(); } else if (cmd === '/status') { this.showStatus(); } else if (cmd.startsWith('/cd ')) { await this.changeDirectory(cmd.substring(4).trim()); } else if (cmd.startsWith('/read ')) { await this.readFile(cmd.substring(6).trim()); } else if (cmd.startsWith('/ls') || cmd.startsWith('/dir')) { await this.listDirectory(cmd.substring(cmd.indexOf(' ') + 1).trim()); } else if (cmd.startsWith('/mkdir ')) { await this.createDirectory(cmd.substring(7).trim()); } else if (cmd.startsWith('/touch ') || cmd.startsWith('/create ')) { const parts = cmd.split(' ').slice(1); if (parts.length < 1) { throw new Error('Nom de fichier requis'); } await this.createFile(parts[0], parts.slice(1).join(' ')); } else if (cmd.startsWith('/write ')) { await this.processWriteCommand(cmd.substring(7).trim()); } else if (cmd.startsWith('/edit ')) { await this.editFile(cmd.substring(6).trim()); } else if (cmd.startsWith('/find ')) { await this.findFiles(cmd.substring(6).trim()); } else if (cmd.startsWith('/cat ')) { await this.readFile(cmd.substring(5).trim()); } else if (cmd.startsWith('/run ')) { await this.runCommand(cmd.substring(5).trim()); } else if (cmd.startsWith('/explorer')) { await this.openExplorer(cmd.substring(10).trim()); } else if (cmd === '/pwd') { console.log(chalk.green(`Répertoire actuel: ${this.currentDirectory}`)); } else { console.log(chalk.yellow(`Commande inconnue: ${command}`)); console.log(chalk.yellow('Tapez /help pour voir les commandes disponibles.')); } } catch (error) { console.error(chalk.red(`Erreur lors de l'exécution de la commande: ${error.message}`)); } } /** * Affiche l'aide */ showHelp() { console.log(chalk.green('\nCommandes disponibles:')); console.log('\n' + chalk.cyan('Commandes de base:')); console.log(chalk.cyan('/help') + ' - Affiche cette aide'); console.log(chalk.cyan('/exit') + ' ou ' + chalk.cyan('/quit') + ' - Quitte le mode interactif'); console.log(chalk.cyan('/clear') + ' - Efface l\'écran'); console.log(chalk.cyan('/context') + ' - Charge le contexte du projet courant'); console.log(chalk.cyan('/status') + ' - Affiche le statut actuel'); console.log(chalk.cyan('/pwd') + ' - Affiche le répertoire de travail actuel'); console.log('\n' + chalk.cyan('Navigation et recherche:')); console.log(chalk.cyan('/cd <chemin>') + ' - Change de répertoire'); console.log(chalk.cyan('/ls [chemin]') + ' ou ' + chalk.cyan('/dir [chemin]') + ' - Liste le contenu d\'un répertoire'); console.log(chalk.cyan('/find <pattern>') + ' - Recherche des fichiers'); console.log(chalk.cyan('/explorer [chemin]') + ' - Ouvre l\'explorateur de fichiers au chemin spécifié'); console.log('\n' + chalk.cyan('Opérations sur les fichiers:')); console.log(chalk.cyan('/read <fichier>') + ' ou ' + chalk.cyan('/cat <fichier>') + ' - Lit le contenu d\'un fichier'); console.log(chalk.cyan('/touch <fichier> [contenu]') + ' ou ' + chalk.cyan('/create <fichier> [contenu]') + ' - Crée un fichier vide ou avec le contenu spécifié'); console.log(chalk.cyan('/write <fichier>') + ' - Écrit du contenu dans un fichier (mode interactif)'); console.log(chalk.cyan('/edit <fichier>') + ' - Modifie un fichier avec l\'éditeur par défaut'); console.log(chalk.cyan('/mkdir <dossier>') + ' - Crée un nouveau répertoire'); console.log('\n' + chalk.cyan('Exécution:')); console.log(chalk.cyan('/run <commande>') + ' - Exécute une commande système'); console.log(''); } /** * Charge le contexte du projet */ async loadContext() { console.log(chalk.yellow('Chargement du contexte du projet...')); try { this.projectContext = await this.contextManager.getContext(); // Mettre à jour le message système if (this.conversation.length > 0 && this.conversation[0].role === 'system') { this.conversation[0].content = `Tu es un assistant IA de développement. Tu aides avec les questions de programmation et les tâches de codage.\n\nContexte du projet:\n${this.projectContext}`; } console.log(chalk.green('Contexte chargé avec succès!')); } catch (error) { console.error(chalk.red(`Erreur lors du chargement du contexte: ${error.message}`)); } } /** * Affiche le statut actuel */ showStatus() { console.log(chalk.green('\nStatut actuel:')); console.log(`Modèle: ${chalk.cyan(this.ollamaService.modelName)}`); console.log(`Répertoire: ${chalk.cyan(this.currentDirectory)}`); console.log(`Contexte chargé: ${chalk.cyan(this.projectContext ? 'Oui' : 'Non')}`); console.log(`Messages dans la conversation: ${chalk.cyan(this.conversation.length)}`); console.log(''); } /** * Change le répertoire courant * @param {string} dirPath - Chemin du répertoire */ async changeDirectory(dirPath) { try { // Gérer les cas spéciaux if (dirPath === '..') { this.currentDirectory = path.dirname(this.currentDirectory); console.log(chalk.green(`Répertoire courant: ${this.currentDirectory}`)); return; } else if (dirPath === '~') { this.currentDirectory = process.env.HOME || process.env.USERPROFILE; console.log(chalk.green(`Répertoire courant: ${this.currentDirectory}`)); return; } // Résoudre le chemin const resolvedPath = path.resolve(this.currentDirectory, dirPath); // Vérifier que le chemin existe try { const stats = await fs.stat(resolvedPath); if (!stats.isDirectory()) { throw new Error(`${resolvedPath} n'est pas un répertoire.`); } } catch (error) { throw new Error(`Répertoire inexistant: ${resolvedPath}`); } // Changer le répertoire this.currentDirectory = resolvedPath; this.contextManager.basePath = resolvedPath; console.log(chalk.green(`Répertoire courant: ${this.currentDirectory}`)); } catch (error) { throw error; } } /** * Lit le contenu d'un fichier * @param {string} filePath - Chemin du fichier */ async readFile(filePath) { try { const resolvedPath = path.resolve(this.currentDirectory, filePath); // Lire le fichier try { const content = await fs.readFile(resolvedPath, 'utf-8'); console.log(chalk.green(`\nContenu de ${filePath}:`)); console.log('```'); console.log(content); console.log('```\n'); // Ajouter le contenu du fichier au contexte de la conversation this.conversation.push({ role: 'user', content: `Voici le contenu du fichier ${filePath} que je viens de lire:\n\`\`\`\n${content}\n\`\`\`` }); this.conversation.push({ role: 'assistant', content: `J'ai enregistré le contenu du fichier \`${filePath}\` dans mon contexte. Vous pouvez maintenant me poser des questions à son sujet ou me demander de le modifier.` }); console.log(chalk.green(`Le contenu du fichier a été ajouté au contexte de la conversation.`)); } catch (error) { throw new Error(`Impossible de lire le fichier: ${error.message}`); } } catch (error) { throw error; } } /** * Liste le contenu d'un répertoire * @param {string} dirPath - Chemin du répertoire */ async listDirectory(dirPath) { try { const targetDir = dirPath ? path.resolve(this.currentDirectory, dirPath) : this.currentDirectory; // Lister le contenu try { const items = await fs.readdir(targetDir, { withFileTypes: true }); console.log(chalk.green(`\nContenu de ${targetDir}:`)); // Trier: d'abord les répertoires, puis les fichiers const sorted = [...items].sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); // Afficher sorted.forEach(item => { const type = item.isDirectory() ? chalk.blue('[DIR] ') : chalk.yellow('[FILE] '); console.log(`${type}${item.name}`); }); console.log(''); } catch (error) { throw new Error(`Impossible de lister le répertoire: ${error.message}`); } } catch (error) { throw error; } } /** * Crée un nouveau répertoire * @param {string} dirPath - Chemin du répertoire à créer */ async createDirectory(dirPath) { try { const resolvedPath = path.resolve(this.currentDirectory, dirPath); try { await fs.mkdir(resolvedPath, { recursive: true }); console.log(chalk.green(`Répertoire créé: ${dirPath}`)); } catch (error) { throw new Error(`Impossible de créer le répertoire: ${error.message}`); } } catch (error) { throw error; } } /** * Crée un nouveau fichier * @param {string} filePath - Chemin du fichier à créer * @param {string} content - Contenu optionnel du fichier */ async createFile(filePath, content = '') { try { const resolvedPath = path.resolve(this.currentDirectory, filePath); // Créer les répertoires parents si nécessaire await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); try { await fs.writeFile(resolvedPath, content); console.log(chalk.green(`Fichier créé: ${filePath}`)); } catch (error) { throw new Error(`Impossible de créer le fichier: ${error.message}`); } } catch (error) { throw error; } } /** * Traite la commande write pour écrire dans un fichier en mode interactif * @param {string} args - Arguments de la commande */ async processWriteCommand(args) { const parts = args.split(' '); if (parts.length < 1) { throw new Error('Chemin du fichier requis'); } const filePath = parts[0]; const resolvedPath = path.resolve(this.currentDirectory, filePath); console.log(chalk.green(`\nÉcriture dans le fichier: ${filePath}`)); console.log(chalk.yellow('Entrez le contenu ligne par ligne. Tapez ".end" sur une ligne seule pour terminer.')); let content = ''; const contentRl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' }); return new Promise((resolve) => { contentRl.prompt(); contentRl.on('line', async (line) => { if (line.trim() === '.end') { contentRl.close(); try { // Créer les répertoires parents si nécessaire await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); // Écrire le fichier await fs.writeFile(resolvedPath, content); console.log(chalk.green(`\nFichier sauvegardé: ${filePath}`)); resolve(); } catch (error) { console.error(chalk.red(`Erreur lors de l'écriture du fichier: ${error.message}`)); resolve(); } } else { content += line + '\n'; contentRl.prompt(); } }); }); } /** * Édite un fichier avec l'éditeur par défaut * @param {string} filePath - Chemin du fichier à éditer */ async editFile(filePath) { try { const resolvedPath = path.resolve(this.currentDirectory, filePath); // Vérifier si le fichier existe try { await fs.access(resolvedPath); } catch (error) { // Créer le fichier s'il n'existe pas await fs.writeFile(resolvedPath, ''); console.log(chalk.yellow(`Le fichier n'existait pas et a été créé.`)); } // Déterminer l'éditeur à utiliser const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'nano'); console.log(chalk.yellow(`Ouverture du fichier avec ${editor}...`)); return new Promise((resolve) => { const child = spawn(editor, [resolvedPath], { stdio: 'inherit', shell: true }); child.on('exit', () => { console.log(chalk.green(`Édition terminée.`)); resolve(); }); }); } catch (error) { throw error; } } /** * Recherche des fichiers * @param {string} pattern - Motif de recherche */ async findFiles(pattern) { if (!pattern) { throw new Error('Motif de recherche requis'); } console.log(chalk.yellow(`Recherche de fichiers correspondant à "${pattern}"...`)); try { const foundFiles = []; // Fonction récursive pour parcourir les répertoires const searchDir = async (dir, depth = 0) => { if (depth > 10) return; // Limiter la profondeur pour éviter une récursion infinie const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const itemPath = path.join(dir, item.name); // Ignorer les répertoires communs à exclure if (item.isDirectory() && ['node_modules', '.git', 'dist', 'build', '.vscode'].includes(item.name)) { continue; } if (item.isDirectory()) { await searchDir(itemPath, depth + 1); } else if (item.name.includes(pattern)) { foundFiles.push(path.relative(this.currentDirectory, itemPath)); } } }; await searchDir(this.currentDirectory); if (foundFiles.length > 0) { console.log(chalk.green(`\nFichiers trouvés (${foundFiles.length}):`)); foundFiles.forEach(file => { console.log(`- ${file}`); }); } else { console.log(chalk.yellow(`Aucun fichier trouvé pour le motif "${pattern}".`)); } console.log(''); } catch (error) { throw new Error(`Erreur lors de la recherche: ${error.message}`); } } /** * Exécute une commande système * @param {string} command - Commande à exécuter */ async runCommand(command) { if (!command) { throw new Error('Commande requise'); } console.log(chalk.yellow(`Exécution de: ${command}`)); return new Promise((resolve) => { const child = spawn(command, [], { stdio: 'inherit', shell: true, cwd: this.currentDirectory }); child.on('exit', (code) => { if (code === 0) { console.log(chalk.green(`\nCommande terminée avec succès.`)); } else { console.log(chalk.red(`\nCommande terminée avec le code d'erreur ${code}.`)); } resolve(); }); }); } /** * Ouvre l'explorateur de fichiers * @param {string} dirPath - Chemin du répertoire à ouvrir */ async openExplorer(dirPath = '') { try { const targetDir = dirPath ? path.resolve(this.currentDirectory, dirPath) : this.currentDirectory; let command; switch (process.platform) { case 'win32': command = `explorer "${targetDir}"`; break; case 'darwin': command = `open "${targetDir}"`; break; default: command = `xdg-open "${targetDir}"`; break; } console.log(chalk.yellow(`Ouverture de l'explorateur de fichiers...`)); const child = spawn(command, [], { shell: true }); child.on('error', (error) => { console.error(chalk.red(`Erreur lors de l'ouverture de l'explorateur: ${error.message}`)); }); } catch (error) { throw error; } } }