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
1,363 lines (1,138 loc) • 57.6 kB
JavaScript
/**
* Mode interactif avec interface TUI
*/
import { UIManager } from './tui.js';
import { GitService } from '../services/git-service.js';
import { FileService } from '../services/file-service.js';
import { MCPFileService } from '../services/mcp-file-service.js';
import { MCPService } from '../services/mcp-service.js';
import { CodeAnalyzer } from '../services/code-analyzer.js';
import { CodeExecutor } from '../services/code-executor.js';
import { CacheService } from '../services/cache-service.js';
import { PromptService } from '../services/prompt-service.js';
import { Config } from '../utils/config.js';
import path from 'path';
export class InteractiveMode {
/**
* Initialise le mode interactif
* @param {OllamaService} ollamaService - Service Ollama
* @param {ContextManager} contextManager - Gestionnaire de contexte
*/
constructor(ollamaService, contextManager) {
this.ollamaService = ollamaService;
this.contextManager = contextManager;
this.config = new Config();
// Initialiser les services
this.gitService = new GitService();
this.fileService = new FileService();
// Initialiser le service MCP pour la gestion avancée des fichiers
this.mcpFileService = new MCPFileService(process.cwd());
// Initialiser le service MCP pour les intentions de fichiers - avec debug actif pour plus de visibilité
this.mcpService = new MCPService({
baseDirectory: process.cwd(),
allowDelete: true, // Activer par défaut pour une meilleure expérience utilisateur
allowExecute: true, // Activer par défaut pour une meilleure expérience utilisateur
debug: true // Activer le debug pour voir ce qui se passe en cas de problème
});
this.codeAnalyzer = new CodeAnalyzer(this.fileService);
this.codeExecutor = new CodeExecutor({
timeout: this.config.get('codeExecutionTimeout') || 10000
});
// Initialiser le service de cache si activé
if (this.config.get('enableCache')) {
this.cacheService = new CacheService({
ttl: this.config.get('cacheTTL') || 3600000
});
this.cacheService.init().then(() => {
// Passer le service de cache au gestionnaire de contexte
this.contextManager.cacheService = this.cacheService;
});
}
// Initialiser le service de prompts
this.promptService = new PromptService(this.config);
// Initialiser l'interface utilisateur
this.ui = new UIManager({
model: this.ollamaService.modelName,
host: this.ollamaService.host,
version: '0.3.1',
onSubmit: this.handleUserInput.bind(this),
onCommand: this.handleCommand.bind(this),
onExit: this.handleExit.bind(this)
});
// Initialiser l'historique de conversation
this.conversation = [];
this.projectContext = '';
// Répertoire de travail courant
this.currentDirectory = process.cwd();
// Support du mode focus
this.focusedPath = null; // Chemin actuellement en focus
this.focusMode = false; // Mode focus activé/désactivé
// Informations sur le projet détectées
this.projectInfo = null;
}
/**
* Démarre le mode interactif
*/
async start() {
try {
// Afficher l'interface
this.ui.start();
// Vérifier si on est dans un dépôt Git
const isGitRepo = await this.gitService.isGitRepository();
this.ui.updateStatus(`Git: ${isGitRepo ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`);
// Charger le contexte du projet
this.ui.startLoading('Loading project context and analyzing project structure...');
this.projectContext = await this.contextManager.getContext();
// Détecter le type de projet
this.projectInfo = await this.contextManager.detectProjectType();
this.ui.stopLoading();
// Initialiser la conversation avec le contexte du projet
const projectTypeInfo = this.projectInfo.language ?
`Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` :
'';
this.conversation = [
{
role: 'system',
content: `You are an AI coding assistant. You help with programming tasks and questions.
${projectTypeInfo}
Current project context:
${this.projectContext}
IMPORTANT: When the user asks you to create or modify files, you can do so directly. If they ask you to "create a file named X", you can write the content and it will be automatically created. You do not need to use commands like /write or /edit. Just clearly mention the file name and provide the code.
Examples:
- When asked to create a file, you can say "Here's the content for file.js:" and then provide the code
- When asked to modify a file, you can say "Here's the updated version of file.js:" and provide the full content
`
}
];
this.ui.addSystemMessage(`Project context loaded. ${projectTypeInfo ? 'Detected ' + projectTypeInfo + '.' : ''} Ready for your questions!`);
this.ui.focusInput();
} catch (error) {
this.ui.showError(`Failed to start interactive mode: ${error.message}`);
}
}
/**
* Gère la commande pwd pour afficher le répertoire courant
*/
handlePwdCommand() {
const currentDir = this.mcpFileService.pwd();
this.ui.addSystemMessage(`Current directory: ${currentDir}`);
}
/**
* Gère la commande find pour rechercher des fichiers
* @param {string} command - Commande complète
*/
async handleFindCommand(command) {
try {
// Extraire le motif et le chemin
const parts = command.slice(6).trim().split(' ');
if (parts.length === 0 || !parts[0]) {
throw new Error('Search pattern is required. Usage: /find <pattern> [path]');
}
const pattern = parts[0];
const dirPath = parts.length > 1 ? parts.slice(1).join(' ') : '.';
this.ui.startLoading(`Searching for files matching '${pattern}'...`);
// Rechercher les fichiers avec MCPFileService
const files = await this.mcpFileService.findFiles(pattern, dirPath);
this.ui.stopLoading();
if (files.length === 0) {
this.ui.addSystemMessage(`No files matching '${pattern}' found in ${dirPath}`);
return;
}
let result = `Found ${files.length} file(s) matching '${pattern}' in ${dirPath}:\n\n`;
files.forEach(file => {
result += `- ${file.relativePath}\n`;
});
this.ui.addSystemMessage(result);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Gère la commande grep pour rechercher du texte dans les fichiers
* @param {string} command - Commande complète
*/
async handleGrepCommand(command) {
try {
// Extraire le motif, le filtre de fichiers et le chemin
const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
if (!parts || parts.length === 0) {
throw new Error('Search pattern is required. Usage: /grep <pattern> [file_pattern] [path]');
}
const pattern = parts[0].replace(/^"|"$/g, '');
const filePattern = parts.length > 1 ? parts[1].replace(/^"|"$/g, '') : '*';
const dirPath = parts.length > 2 ? parts.slice(2).join(' ').replace(/^"|"$/g, '') : '.';
this.ui.startLoading(`Searching for text '${pattern}' in files...`);
// Rechercher dans les fichiers avec MCPFileService
const results = await this.mcpFileService.grepFiles(pattern, filePattern, dirPath);
this.ui.stopLoading();
if (results.length === 0) {
this.ui.addSystemMessage(`No matches found for '${pattern}' in ${filePattern} files under ${dirPath}`);
return;
}
let output = `Found ${results.length} match(es) for '${pattern}' in ${filePattern} files under ${dirPath}:\n\n`;
const uniqueFiles = new Set();
results.forEach(result => uniqueFiles.add(result.file));
output += `Matches in ${uniqueFiles.size} file(s):\n\n`;
let currentFile = '';
results.forEach(result => {
if (currentFile !== result.file) {
currentFile = result.file;
output += `\n## ${result.file}\n`;
}
output += `Line ${result.line}: ${result.content.trim()}\n`;
});
this.ui.addSystemMessage(output);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Gère la commande copy pour copier un fichier
* @param {string} command - Commande complète
*/
async handleCopyCommand(command) {
try {
// Extraire les chemins source et destination
const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
if (!parts || parts.length < 2) {
throw new Error('Source and destination paths are required. Usage: /copy <source> <destination>');
}
const sourcePath = parts[0].replace(/^"|"$/g, '');
const destPath = parts[1].replace(/^"|"$/g, '');
// Vérifier si l'opération est autorisée
if (!this.mcpService.isOperationAllowed('updateFile', destPath)) {
throw new Error(`Copy operation to '${destPath}' is not allowed by security policy.`);
}
// Copier le fichier avec MCPFileService
const result = await this.mcpFileService.copyFile(sourcePath, destPath);
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Gère la commande move pour déplacer un fichier
* @param {string} command - Commande complète
*/
async handleMoveCommand(command) {
try {
// Extraire les chemins source et destination
const parts = command.slice(6).trim().match(/(?:[^\s"]+|"[^"]*")+/g);
if (!parts || parts.length < 2) {
throw new Error('Source and destination paths are required. Usage: /move <source> <destination>');
}
const sourcePath = parts[0].replace(/^"|"$/g, '');
const destPath = parts[1].replace(/^"|"$/g, '');
// Vérifier si l'opération est autorisée
if (!this.mcpService.isOperationAllowed('updateFile', destPath) ||
!this.mcpService.isOperationAllowed('deleteFile', sourcePath)) {
throw new Error(`Move operation from '${sourcePath}' to '${destPath}' is not allowed by security policy.`);
}
// Déplacer le fichier avec MCPFileService
const result = await this.mcpFileService.moveFile(sourcePath, destPath);
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Gère la commande delete pour supprimer un fichier
* @param {string} command - Commande complète
*/
async handleDeleteCommand(command) {
try {
// Extraire le chemin
const filePath = command.slice(8).trim();
if (!filePath) {
throw new Error('File path is required. Usage: /delete <path>');
}
// Vérifier si l'opération est autorisée
if (!this.mcpService.isOperationAllowed('deleteFile', filePath)) {
throw new Error(`Deletion of '${filePath}' is not allowed by security policy.`);
}
// Vérifier si c'est un fichier ou un répertoire
const info = await this.mcpFileService.getInfo(filePath);
let result;
if (info.type === 'directory') {
// Demander confirmation pour un répertoire
const confirmMessage = `Are you sure you want to delete the directory '${filePath}'? This cannot be undone. (y/n)`;
this.ui.addSystemMessage(confirmMessage);
// Pour une véritable implémentation, il faudrait attendre la réponse utilisateur
// Ici, on considère que c'est confirmé pour l'exemple
result = await this.mcpFileService.removeDirectory(filePath, true);
} else {
// Supprimer le fichier avec MCPFileService
result = await this.mcpFileService.removeFile(filePath);
}
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Gère la commande info pour afficher les informations sur un fichier
* @param {string} command - Commande complète
*/
async handleInfoCommand(command) {
try {
// Extraire le chemin
const filePath = command.slice(6).trim();
if (!filePath) {
throw new Error('File path is required. Usage: /info <path>');
}
// Obtenir les informations avec MCPFileService
const info = await this.mcpFileService.getInfo(filePath);
let result = `Information about ${filePath}:\n\n`;
result += `- Type: ${info.type}\n`;
result += `- Size: ${this._formatSize(info.size)}\n`;
result += `- Created: ${new Date(info.created).toLocaleString()}\n`;
result += `- Modified: ${new Date(info.modified).toLocaleString()}\n`;
result += `- Accessed: ${new Date(info.accessed).toLocaleString()}\n`;
result += `- Readable: ${info.isReadable ? 'Yes' : 'No'}\n`;
result += `- Writable: ${info.isWritable ? 'Yes' : 'No'}\n`;
if (info.type === 'file') {
result += `- Extension: ${info.extension || 'None'}\n`;
result += `- MIME Type: ${info.mime || 'Unknown'}\n`;
}
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Gère l'entrée utilisateur
* @param {string} input - Texte saisi par l'utilisateur
*/
async handleUserInput(input) {
try {
// Ajouter le message utilisateur à la conversation
this.conversation.push({ role: 'user', content: input });
// Utiliser le PromptService pour générer un meilleur prompt
const promptConfig = this.promptService.generateSystemPrompt(
input,
this.projectContext
);
// Mettre à jour le premier message système si nécessaire
if (this.conversation.length > 1 && this.conversation[0].role === 'system') {
this.conversation[0].content = promptConfig.systemPrompt;
}
if (this.config.get('enableStreaming')) {
// Mode streaming (affichage progressif de la réponse)
this.ui.startResponseStream();
let streamedResponse = '';
const onToken = (token) => {
streamedResponse += token;
this.ui.updateResponseStream(streamedResponse);
};
// Obtenir une réponse du modèle en streaming
await this.ollamaService.streamChat(
this.conversation,
promptConfig.temperature,
this.config.get('maxTokens') || 4096,
onToken
);
this.ui.stopResponseStream();
// Analyser la réponse pour les commandes spéciales
const processedResponse = await this.processResponse(streamedResponse);
// Ajouter la réponse à la conversation
this.conversation.push({ role: 'assistant', content: processedResponse });
// Afficher la réponse complète si elle a été modifiée
if (processedResponse !== streamedResponse) {
this.ui.addAssistantMessage(processedResponse);
}
} else {
// Mode classique (sans streaming)
this.ui.startLoading('Réflexion en cours...');
const response = await this.ollamaService.chat(
this.conversation,
promptConfig.temperature,
this.config.get('maxTokens') || 4096
);
this.ui.stopLoading();
// Analyser la réponse pour les commandes spéciales
const processedResponse = await this.processResponse(response);
// Ajouter la réponse à la conversation
this.conversation.push({ role: 'assistant', content: processedResponse });
// Afficher la réponse
this.ui.addAssistantMessage(processedResponse);
}
} catch (error) {
if (this.config.get('enableStreaming')) {
this.ui.stopResponseStream();
} else {
this.ui.stopLoading();
}
this.ui.showError(`Error: ${error.message}`);
} finally {
this.ui.focusInput();
}
}
/**
* Traite la réponse pour détecter les commandes spéciales
* @param {string} response - Réponse du modèle
* @returns {Promise<string>} - Réponse traitée
*/
async processResponse(response) {
// Traiter les blocs de fichiers avec CodeAnalyzer
const fileResult = await this.codeAnalyzer.processFileBlocks(response, true);
let processedResponse = fileResult.modifiedResponse;
// Ajouter un résumé des fichiers sauvegardés
if (fileResult.savedFiles.length > 0) {
const successFiles = fileResult.savedFiles.filter(f => f.success);
if (successFiles.length > 0) {
processedResponse += `\n\n${successFiles.length} file${successFiles.length > 1 ? 's' : ''} saved successfully.`;
// Si on est en mode focus, mettre à jour le contexte
if (this.focusMode) {
await this.refreshFocusedContext();
}
}
}
// Traiter les commandes shell
const shellResult = await this.codeAnalyzer.processShellCommands(processedResponse, false);
processedResponse = shellResult.modifiedResponse;
// Utiliser MCPService pour détecter les intentions d'actions sur les fichiers
// Important: Analyser à la fois la requête de l'utilisateur et la réponse du modèle
const userMessage = this.conversation[this.conversation.length - 2]?.content || '';
// Toujours activer le debug pour voir les intentions
console.log("Analysant l'intention utilisateur: ", userMessage);
// Détecter les intentions dans le message de l'utilisateur d'abord
// Analyse directe de l'entrée utilisateur pour des patterns simples
const userIntents = [];
// Trouver des patterns très simples comme "crée un fichier X avec Y"
const simplePattern = /(?:cr[ée]e|crée|créer|create)\s+(?:un\s+)?(?:fichier|file)\s+([^\s]+)(?:\s+avec|\s+with|\s+containing|\s+contenant)\s+(.+)/i;
const simpleMatch = simplePattern.exec(userMessage);
if (simpleMatch) {
const fileName = simpleMatch[1].trim();
const content = simpleMatch[2].trim();
console.log(`Détection simple: "${fileName}" avec "${content}"`);
userIntents.push({
type: 'createFile',
path: fileName,
content: content,
confidence: 0.99,
position: 0
});
} else {
// Si pas de pattern super simple, utiliser le détecteur complet
const detectedIntents = this.mcpService.detectFileOperationIntents(userMessage);
userIntents.push(...detectedIntents);
}
// Puis dans la réponse du modèle
const responseIntents = this.mcpService.detectFileOperationIntents(response);
// Combiner les intentions (déduplication gérée par MCPService)
const fileIntents = [...userIntents, ...responseIntents];
// Toujours afficher les détails des intentions trouvées
console.log(`Détection d'intentions: ${fileIntents.length} intentions trouvées`);
if (fileIntents.length > 0) {
fileIntents.forEach((intent, i) => console.log(`Intent ${i+1}: ${intent.type} - ${intent.path} - Contenu: ${intent.contentHint ? 'Oui (hint)' : (intent.content ? 'Oui' : 'Non')}`));
}
if (fileIntents.length > 0) {
// Exécuter les opérations détectées
this.ui.startLoading('Processing file operations...');
const fileResults = await this.mcpService.executeFileOperations(fileIntents, response);
this.ui.stopLoading();
// Ajouter un résumé des opérations effectuées
const successOps = fileResults.filter(r => r.success);
if (successOps.length > 0) {
let summary = `\n\n${successOps.length} file operation${successOps.length > 1 ? 's' : ''} completed:`;
successOps.forEach(op => {
summary += `\n- ${op.message}`;
});
processedResponse += summary;
// Si on est en mode focus, mettre à jour le contexte
if (this.focusMode) {
await this.refreshFocusedContext();
}
}
// Ajouter les erreurs éventuelles
const failedOps = fileResults.filter(r => !r.success);
if (failedOps.length > 0) {
let errors = `\n\n${failedOps.length} file operation${failedOps.length > 1 ? 's' : ''} failed:`;
failedOps.forEach(op => {
errors += `\n- ${op.message}`;
});
processedResponse += errors;
}
}
}
// Extraire et éventuellement exécuter les blocs de code si l'exécution est activée
if (this.config.get('enableCodeExecution')) {
const codeBlocks = this.codeExecutor.extractCodeBlocks(processedResponse);
// Exécuter automatiquement les blocs de code JavaScript si marqués comme exécutables
if (processedResponse.includes('```javascript:run') || processedResponse.includes('```js:run')) {
const executableBlocks = codeBlocks.filter(([lang]) =>
lang === 'javascript:run' || lang === 'js:run'
);
if (executableBlocks.length > 0) {
this.ui.addSystemMessage('Executing JavaScript code...');
for (const [_, code] of executableBlocks) {
try {
const result = await this.codeExecutor.executeCode('javascript', code);
if (result.success) {
processedResponse += `\n\n**Execution result:**\n\`\`\`\n${result.stdout || 'No output'}\n\`\`\``;
} else {
processedResponse += `\n\n**Execution failed:**\n\`\`\`\n${result.stderr || result.error}\n\`\`\``;
}
} catch (error) {
processedResponse += `\n\n**Execution error:**\n\`\`\`\n${error.message}\n\`\`\``;
}
}
}
}
}
return processedResponse;
}
/**
* Gère les commandes spéciales
* @param {string} command - Commande saisie par l'utilisateur
*/
async handleCommand(command) {
const cmd = command.trim().toLowerCase();
try {
if (cmd === '/help') {
this.showHelp();
} else if (cmd === '/exit' || cmd === '/quit') {
this.handleExit();
process.exit(0);
} else if (cmd === '/clear') {
this.ui.clearChat();
} else if (cmd === '/init') {
this.resetConversation();
} else if (cmd === '/refresh') {
await this.refreshContext();
} else if (cmd === '/context') {
this.showContext();
} else if (cmd.startsWith('/git')) {
await this.handleGitCommand(cmd);
} else if (cmd.startsWith('/run-code')) {
await this.handleRunCodeCommand(cmd);
} else if (cmd.startsWith('/file')) {
await this.handleFileCommand(cmd);
} else if (cmd.startsWith('/cd')) {
await this.handleCdCommand(cmd);
} else if (cmd.startsWith('/ls')) {
await this.handleLsCommand(cmd);
} else if (cmd.startsWith('/tree')) {
await this.handleTreeCommand(cmd);
} else if (cmd.startsWith('/find')) {
await this.handleFindCommand(cmd);
} else if (cmd.startsWith('/grep')) {
await this.handleGrepCommand(cmd);
} else if (cmd.startsWith('/mkdir')) {
await this.handleMkdirCommand(cmd);
} else if (cmd.startsWith('/write')) {
await this.handleWriteCommand(cmd);
} else if (cmd.startsWith('/read')) {
await this.handleReadCommand(cmd);
} else if (cmd.startsWith('/edit')) {
await this.handleEditCommand(cmd);
} else if (cmd.startsWith('/copy')) {
await this.handleCopyCommand(cmd);
} else if (cmd.startsWith('/move')) {
await this.handleMoveCommand(cmd);
} else if (cmd.startsWith('/delete')) {
await this.handleDeleteCommand(cmd);
} else if (cmd.startsWith('/info')) {
await this.handleInfoCommand(cmd);
} else if (cmd.startsWith('/run')) {
await this.handleRunCommand(cmd);
} else if (cmd.startsWith('/focus')) {
await this.handleFocusCommand(cmd);
} else if (cmd === '/unfocus') {
await this.disableFocusMode();
} else if (cmd === '/project-info') {
this.showProjectInfo();
} else if (cmd === '/suggest-commit') {
await this.handleSuggestCommitCommand();
} else if (cmd === '/analyze-git') {
await this.handleAnalyzeGitCommand();
} else if (cmd.startsWith('/pwd')) {
this.handlePwdCommand();
} else {
this.ui.showError(`Unknown command: ${command}. Type /help for available commands.`);
}
} catch (error) {
this.ui.showError(`Error executing command: ${error.message}`);
} finally {
this.ui.focusInput();
}
}
/**
* Affiche l'aide des commandes disponibles
*/
showHelp() {
const helpText = `
Available commands:
Basic commands:
/help - Show this help message
/exit, /quit - Exit the application
/clear - Clear the chat history
/init - Reset the conversation but keep project context
/refresh - Refresh the project context
/context - Show the current project context
/project-info - Display detected project information
Focus mode:
/focus [path] - Focus on a specific file or directory
/unfocus - Disable focus mode
File Management:
/pwd - Show current working directory
/cd [path] - Change current directory
/ls [path] - List directory contents
/tree [path] [max_depth] - Display directory tree
/find [pattern] [path] - Find files by pattern
/grep [pattern] [file_pattern] - Search in file contents
/mkdir [path] - Create a new directory
/read [path] - Read file contents
/write [path] [text] - Write text to a file
/edit [path] [old] [new] - Replace text in a file
/copy [source] [dest] - Copy file or directory
/move [source] [dest] - Move file or directory
/delete [path] - Delete file or directory
/info [path] - Display file information
/run [command] - Execute a system command
/run-code [file] - Execute code from a file
Git Commands:
/git status - Show Git repository status
/git add [path] - Stage files for commit
/git commit -m "message" - Create a new commit
/git pull - Pull changes from remote
/git push - Push changes to remote
/git log - Show commit history
/git branch - List all branches
/git checkout [branch] - Switch to a branch
/suggest-commit - Get AI-generated commit message based on changes
/analyze-git - Analyze recent Git activity
You can also press Ctrl+G to access the Git menu.
Press Ctrl+L to clear the screen.
Press Ctrl+R to refresh the context.
Press Ctrl+C to exit.
`;
this.ui.addSystemMessage(helpText);
}
/**
* Réinitialise la conversation mais conserve le contexte du projet
*/
resetConversation() {
const projectTypeInfo = this.projectInfo && this.projectInfo.language ?
`Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` :
'';
this.conversation = [
{
role: 'system',
content: `You are an AI coding assistant. You help with programming tasks and questions.\n\n${projectTypeInfo}\n\nCurrent project context:\n${this.projectContext}`
}
];
this.ui.addSystemMessage('Conversation reset. Project context is preserved.');
}
/**
* Actualise le contexte du projet
*/
async refreshContext() {
try {
this.ui.startLoading('Refreshing project context...');
this.projectContext = await this.contextManager.getContext();
// Mettre à jour le message système avec le nouveau contexte et instructions pour les fichiers
const projectTypeInfo = this.projectInfo && this.projectInfo.language ?
`Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` :
'';
if (this.conversation.length > 0 && this.conversation[0].role === 'system') {
this.conversation[0].content = `You are an AI coding assistant. You help with programming tasks and questions.
${projectTypeInfo}
Current project context:
${this.projectContext}
IMPORTANT: When the user asks you to create or modify files, you can do so directly. If they ask you to "create a file named X", you can write the content and it will be automatically created. You do not need to use commands like /write or /edit. Just clearly mention the file name and provide the code.
Examples:
- When asked to create a file, you can say "Here's the content for file.js:" and then provide the code
- When asked to modify a file, you can say "Here's the updated version of file.js:" and provide the full content
`;
}
this.ui.stopLoading();
this.ui.addSystemMessage('Project context refreshed.');
} catch (error) {
throw new Error(`Failed to refresh context: ${error.message}`);
}
}
/**
* Affiche le contexte du projet actuel
*/
showContext() {
if (this.focusMode) {
this.ui.addSystemMessage(`Currently focusing on: ${this.focusedPath}`);
}
this.ui.addSystemMessage('Current Project Context:');
this.ui.addSystemMessage(this.projectContext);
}
/**
* Active le mode focus sur un fichier ou dossier spécifique
* @param {string} path - Chemin du fichier ou dossier
*/
async enableFocusMode(path) {
try {
const stats = await this.fileService.getFileStats(path);
if (stats.isFile() || stats.isDirectory()) {
this.focusedPath = path;
this.focusMode = true;
// Si c'est un fichier, le marquer comme consulté
if (stats.isFile()) {
this.contextManager.trackFileAccess(path);
}
// Mettre à jour le contexte avec focus
await this.refreshFocusedContext();
const relPath = this.fileService.getRelativePath(path);
this.ui.addSystemMessage(`Focus activé sur: ${relPath}`);
this.ui.updateStatus(`Focus: ${relPath} | Git: ${await this.gitService.isGitRepository() ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName}`);
} else {
throw new Error(`Chemin invalide: ${path}`);
}
} catch (error) {
throw new Error(`Impossible d'activer le mode focus: ${error.message}`);
}
}
/**
* Désactive le mode focus
*/
async disableFocusMode() {
this.focusedPath = null;
this.focusMode = false;
// Restaurer le contexte complet
await this.refreshContext();
this.ui.addSystemMessage('Mode focus désactivé.');
this.ui.updateStatus(`Git: ${await this.gitService.isGitRepository() ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`);
}
/**
* Rafraîchit le contexte en mode focus
*/
async refreshFocusedContext() {
try {
this.ui.startLoading('Actualisation du contexte focalisé...');
let focusedContext = '';
const stats = await this.fileService.getFileStats(this.focusedPath);
if (stats.isFile()) {
// Focus sur un seul fichier
const content = await this.fileService.readFile(this.focusedPath);
const relPath = this.fileService.getRelativePath(this.focusedPath);
focusedContext = `# Fichier en focus: ${relPath}\n\n\`\`\`\n${content}\n\`\`\`\n`;
// Ajouter les imports/dépendances si possible
const deps = await this.codeAnalyzer.findDependencies(this.focusedPath);
if (deps.length > 0) {
focusedContext += '\n# Dépendances et imports:\n';
for (const dep of deps) {
focusedContext += `- ${dep}\n`;
}
}
} else if (stats.isDirectory()) {
// Focus sur un dossier
focusedContext = await this.contextManager.getContextForDirectory(this.focusedPath);
}
// Mettre à jour le contexte du projet
this.projectContext = focusedContext;
// Mettre à jour le message système avec le nouveau contexte
const projectTypeInfo = this.projectInfo && this.projectInfo.language ?
`Project type: ${this.projectInfo.language}${this.projectInfo.framework ? ' with ' + this.projectInfo.framework : ''}` :
'';
if (this.conversation.length > 0 && this.conversation[0].role === 'system') {
this.conversation[0].content = `You are an AI coding assistant. You help with programming tasks and questions.\n\n${projectTypeInfo}\n\nCurrent focused context:\n${this.projectContext}`;
}
this.ui.stopLoading();
} catch (error) {
this.ui.stopLoading();
throw new Error(`Failed to refresh focused context: ${error.message}`);
}
}
/**
* Gère la commande focus
* @param {string} command - Commande complète
*/
async handleFocusCommand(command) {
try {
// Extraire le chemin
const focusPath = command.slice(7).trim();
if (!focusPath) {
throw new Error('Path is required for focus mode. Usage: /focus <path>');
}
// Activer le mode focus
await this.enableFocusMode(focusPath);
} catch (error) {
throw error;
}
}
/**
* Affiche les informations détectées sur le projet
*/
showProjectInfo() {
if (!this.projectInfo) {
this.ui.addSystemMessage('Project information not available. Try refreshing the context.');
return;
}
let infoText = '# Project Information\n\n';
infoText += `- **Language**: ${this.projectInfo.language || 'Not detected'}\n`;
infoText += `- **Framework**: ${this.projectInfo.framework || 'None detected'}\n`;
infoText += `- **Type**: ${this.projectInfo.type || 'Not detected'}\n`;
if (this.projectInfo.buildTools && this.projectInfo.buildTools.length > 0) {
infoText += `- **Build Tools**: ${this.projectInfo.buildTools.join(', ')}\n`;
}
if (this.projectInfo.testing && this.projectInfo.testing.length > 0) {
infoText += `- **Testing**: ${this.projectInfo.testing.join(', ')}\n`;
}
if (this.projectInfo.database) {
infoText += `- **Database**: ${this.projectInfo.database}\n`;
}
this.ui.addSystemMessage(infoText);
}
/**
* Gère la commande de suggestion de message de commit
*/
async handleSuggestCommitCommand() {
try {
const isGitRepo = await this.gitService.isGitRepository();
if (!isGitRepo) {
throw new Error('Not a Git repository. Use "/git init" to initialize one.');
}
this.ui.startLoading('Analyzing changes and suggesting commit message...');
const suggestedMessage = await this.gitService.suggestCommitMessage();
this.ui.stopLoading();
this.ui.addSystemMessage(`Suggested commit message:\n\n${suggestedMessage}\n\nTo use this message, run:\n/git commit -m "${suggestedMessage}"`);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Gère la commande d'analyse Git
*/
async handleAnalyzeGitCommand() {
try {
const isGitRepo = await this.gitService.isGitRepository();
if (!isGitRepo) {
throw new Error('Not a Git repository. Use "/git init" to initialize one.');
}
this.ui.startLoading('Analyzing Git repository...');
const days = 7; // Par défaut, analyser les 7 derniers jours
const analysis = await this.gitService.analyzeRecentChanges(days);
this.ui.stopLoading();
let result = `# Git Repository Analysis (Last ${days} days)\n\n`;
result += `- **Period**: ${analysis.period}\n`;
result += `- **Total commits**: ${analysis.commitCount}\n\n`;
if (analysis.authors && analysis.authors.length > 0) {
result += '## Contributors\n\n';
analysis.authors.forEach(([author, count]) => {
result += `- **${author}**: ${count} commit${count > 1 ? 's' : ''}\n`;
});
result += '\n';
}
if (analysis.mostChangedFiles && analysis.mostChangedFiles.length > 0) {
result += '## Most Active Files\n\n';
analysis.mostChangedFiles.forEach(([file, count]) => {
result += `- **${file}**: ${count} change${count > 1 ? 's' : ''}\n`;
});
}
this.ui.addSystemMessage(result);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Gère les commandes Git
* @param {string} command - Commande Git complète
*/
async handleGitCommand(command) {
// Vérifier si on est dans un dépôt Git
const isGitRepo = await this.gitService.isGitRepository();
// Extraire les arguments de la commande Git
const gitArgs = command.slice(5).trim().split(' ');
const gitCommand = gitArgs[0];
if (!isGitRepo && gitCommand !== 'init') {
throw new Error('Not a Git repository. Use "/git init" to initialize one.');
}
this.ui.startLoading(`Executing Git command: ${gitCommand}...`);
try {
let result = '';
switch (gitCommand) {
case 'init':
result = await this.gitService.initRepository();
break;
case 'status':
const status = await this.gitService.status();
result = `Branch: ${status.current}\n`;
result += `Staged changes: ${status.staged}\n`;
result += `Changed files: ${status.changed}\n`;
if (status.files.length > 0) {
result += '\nFiles:\n';
status.files.forEach(file => {
const state = file.working_dir === 'M' ? 'Modified' :
file.working_dir === 'D' ? 'Deleted' :
file.working_dir === 'A' ? 'Added' : 'Changed';
result += ` ${state}: ${file.path} ${file.staged ? '(staged)' : ''}\n`;
});
}
break;
case 'add':
const path = gitArgs.length > 1 ? gitArgs.slice(1).join(' ') : '.';
result = await this.gitService.add(path);
break;
case 'commit':
if (gitArgs.includes('-m')) {
const messageIndex = gitArgs.indexOf('-m') + 1;
if (messageIndex < gitArgs.length) {
let message = gitArgs[messageIndex];
// Si le message est entre guillemets, prendre tout ce qui est entre
if (message.startsWith('"') && !message.endsWith('"')) {
const msgParts = [];
for (let i = messageIndex; i < gitArgs.length; i++) {
msgParts.push(gitArgs[i]);
if (gitArgs[i].endsWith('"')) break;
}
message = msgParts.join(' ').replace(/^"|"$/g, '');
} else if (message.startsWith('"') && message.endsWith('"')) {
message = message.slice(1, -1);
}
result = await this.gitService.commit(message);
} else {
throw new Error('No commit message provided.');
}
} else {
throw new Error('Please use -m flag to provide a commit message.');
}
break;
case 'pull':
const pullRemote = gitArgs.length > 1 ? gitArgs[1] : 'origin';
const pullBranch = gitArgs.length > 2 ? gitArgs[2] : null;
result = await this.gitService.pull(pullRemote, pullBranch);
break;
case 'push':
const pushRemote = gitArgs.length > 1 ? gitArgs[1] : 'origin';
const pushBranch = gitArgs.length > 2 ? gitArgs[2] : null;
result = await this.gitService.push(pushRemote, pushBranch);
break;
case 'log':
const logs = await this.gitService.log();
result = 'Recent commits:\n\n';
logs.forEach(commit => {
result += `${commit.hash.substring(0, 7)} - ${commit.date.substring(0, 10)} - ${commit.message}\n`;
result += `Author: ${commit.author_name} <${commit.author_email}>\n\n`;
});
break;
case 'branch':
if (gitArgs.length === 1) {
// Lister les branches
const branches = await this.gitService.listBranches();
result = `Current branch: ${branches.current}\n\nAll branches:\n`;
branches.all.forEach(branch => {
result += `${branch === branches.current ? '* ' : ' '}${branch}\n`;
});
} else {
// Créer une branche
result = await this.gitService.createBranch(gitArgs[1]);
}
break;
case 'checkout':
if (gitArgs.length < 2) {
throw new Error('Branch name is required for checkout.');
}
if (gitArgs[1] === '-b' && gitArgs.length > 2) {
// Créer et basculer vers une nouvelle branche
result = await this.gitService.createBranch(gitArgs[2]);
} else {
// Basculer vers une branche existante
result = await this.gitService.checkout(gitArgs[1]);
}
break;
case 'conflicts':
const conflicts = await this.gitService.handleMergeConflicts();
if (conflicts.hasConflicts) {
result = conflicts.message + '\n\n';
conflicts.files.forEach(file => {
result += `- ${file.file}: ${file.sections} conflict section(s)\n`;
});
result += '\nUse "/git show-conflict <file>" to view a specific conflict.';
} else {
result = conflicts.message;
}
break;
case 'show-conflict':
if (gitArgs.length < 2) {
throw new Error('File path is required. Usage: /git show-conflict <path>');
}
const conflictPath = gitArgs.slice(1).join(' ');
const conflictInfo = await this.gitService.showConflictsInFile(conflictPath);
if (conflictInfo.hasConflicts) {
result = `Found ${conflictInfo.conflictCount} conflict(s) in ${conflictPath}:\n\n`;
conflictInfo.conflicts.forEach((conflict, i) => {
result += `## Conflict ${i+1}:\n\n`;
result += `Our version:\n\`\`\`\n${conflict.ours}\`\`\`\n\n`;
result += `Their version:\n\`\`\`\n${conflict.theirs}\`\`\`\n\n`;
});
result += 'To resolve, use: /git resolve-conflict <path> <strategy>';
} else {
result = conflictInfo.message;
}
break;
case 'resolve-conflict':
if (gitArgs.length < 3) {
throw new Error('File path and strategy required. Usage: /git resolve-conflict <path> <strategy>');
}
const resolvePath = gitArgs[1];
const strategy = gitArgs[2];
const resolution = await this.gitService.resolveConflict(resolvePath, strategy);
result = resolution.message;
break;
default:
// Exécuter une commande Git arbitraire
result = await this.gitService.raw(gitArgs);
}
this.ui.stopLoading();
this.ui.addSystemMessage(`Git ${gitCommand} result:\n${result}`);
// Mettre à jour le statut Git dans la barre d'état
const isGitRepoNow = await this.gitService.isGitRepository();
const statusText = this.focusMode
? `Focus: ${this.fileService.getRelativePath(this.focusedPath)} | Git: ${isGitRepoNow ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName}`
: `Git: ${isGitRepoNow ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`;
this.ui.updateStatus(statusText);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Gère la commande cd pour changer de répertoire
* @param {string} command - Commande complète
*/
async handleCdCommand(command) {
try {
// Extraire le chemin
const dirPath = command.slice(4).trim();
if (!dirPath) {
// Aucun chemin spécifié, aller au répertoire utilisateur
await this.mcpFileService.changeDirectory(process.env.HOME || process.env.USERPROFILE);
} else {
// Utiliser MCPFileService pour changer de répertoire
await this.mcpFileService.changeDirectory(dirPath);
}
// Mettre à jour le répertoire courant
this.currentDirectory = this.mcpFileService.pwd();
// Mettre à jour les autres services
this.contextManager.basePath = this.currentDirectory;
this.fileService.setBasePath(this.currentDirectory);
this.gitService = new GitService(this.currentDirectory);
this.mcpService.setBaseDirectory(this.currentDirectory);
// Si on était en mode focus, le désactiver
if (this.focusMode) {
await this.disableFocusMode();
} else {
// Sinon, juste actualiser le contexte
await this.refreshContext();
}
// Mettre à jour la barre d'état
const isGitRepo = await this.gitService.isGitRepository();
this.ui.updateStatus(`Git: ${isGitRepo ? 'Yes' : 'No'} | Model: ${this.ollamaService.modelName} | Dir: ${path.basename(this.currentDirectory)}`);
this.ui.addSystemMessage(`Changed directory to: ${this.currentDirectory}`);
} catch (error) {
throw error;
}
}
/**
* Gère la commande ls pour lister le contenu d'un répertoire
* @param {string} command - Commande complète
*/
async handleLsCommand(command) {
try {
// Extraire le chemin
const dirPath = command.slice(3).trim() || '.';
// Utiliser MCPFileService pour lister le répertoire
const files = await this.mcpFileService.listDirectory(dirPath);
// Formater la sortie
let result = `Directory listing of ${dirPath || this.currentDirectory}:\n\n`;
// Trier les fichiers: d'abord les répertoires, puis les fichiers
const sortedFiles = [...files].sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
// Formater l'affichage avec plus d'informations
sortedFiles.forEach(file => {
const modified = new Date(file.modified).toLocaleString();
const sizeStr = file.isDirectory ? '-' : this._formatSize(file.size);
const permissions = file.isHidden ? '[Hidden]' : '';
result += `${file.isDirectory ? '📁' : '📄'} ${file.name.padEnd(30)} ${sizeStr.padStart(10)} ${modified} ${permissions}\n`;
});
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Affiche l'arborescence du répertoire
* @param {string} command - Commande complète (ex: /tree ou /tree .)
*/
async handleTreeCommand(command) {
try {
// Extraire le chemin et la profondeur
const parts = command.slice(6).trim().split(' ');
const dirPath = parts[0] || '.';
const maxDepth = parts[1] ? parseInt(parts[1]) : 3;
this.ui.startLoading('Génération de l\'arborescence...');
// Générer l'arborescence avec MCPFileService
const tree = await this.mcpFileService.getDirectoryTree(dirPath, maxDepth);
this.ui.stopLoading();
this.ui.addSystemMessage(`Arborescence de ${dirPath || this.currentDirectory}:\n\n${tree}`);
} catch (error) {
this.ui.stopLoading();
throw error;
}
}
/**
* Formatte une taille en bytes en une chaîne lisible
* @param {number} bytes - Taille en bytes
* @returns {string} - Chaîne formatée
*/
_formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
/**
* Gère la commande mkdir pour créer un répertoire
* @param {string} command - Commande complète
*/
async handleMkdirCommand(command) {
try {
// Extraire le chemin
const dirPath = command.slice(7).trim();
if (!dirPath) {
throw new Error('Directory path is required.');
}
// Vérifier si la création est autorisée
if (!this.mcpService.isOperationAllowed('createDirectory', dirPath)) {
throw new Error(`Creation of directory '${dirPath}' is not allowed by security policy.`);
}
// Créer le répertoire avec MCPFileService
const result = await this.mcpFileService.createDirectory(dirPath);
this.ui.addSystemMessage(result);
} catch (error) {
throw error;
}
}
/**
* Gère la commande file pour manipuler des fichiers
* @param {string} command - Commande complète
*/
async handleFileCommand(command) {
try {
// Extraire les arguments
const args = command.slice(6).trim().split(' ');
const subCommand = args[0];
switch (subCommand) {
case 'read':
if (args.length < 2) {
throw new Error('File path is required for reading.');
}
const readPath = args.slice(1).join(' ');
const content = await this.fileService.readFile(readPath);
this.ui.addSystemMessage(`File content of ${readPath}:\n\n\`\`\`\n${content}\n\`\`\``);
break;
case 'write':
if