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