diff --git "a/index.js" "b/index.js" --- "a/index.js" +++ "b/index.js" @@ -1,12 +1,12 @@ #!/usr/bin/env node import chalk from 'chalk'; -import inquirer from 'inquirer'; +import readline from 'readline'; import ora from 'ora'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import { execSync } from 'child_process'; +import { execSync, spawn } from 'child_process'; import { marked } from 'marked'; import TerminalRenderer from 'marked-terminal'; @@ -17,10 +17,12 @@ const theme = { error: chalk.hex('#FF5555'), info: chalk.hex('#569CD6'), border: chalk.hex('#2A2A2A'), - text: chalk.hex('#E0E0E0') + text: chalk.hex('#E0E0E0'), + dim: chalk.hex('#444444'), + tool: chalk.hex('#FFD700'), + done: chalk.hex('#3ddc97'), }; -// Configurer le rendu Markdown pour le terminal (styles chalk) marked.setOptions({ renderer: new TerminalRenderer({ code: theme.command, @@ -41,12 +43,13 @@ marked.setOptions({ const AUTHOR = "DJAKOUA KWANKAM"; const APP_NAME = "Cypher Coder"; +const VERSION = "1.0.0"; -// Global Session State & Config let chatMessages = []; let lastUserInput = ""; let lastAssistantResponse = ""; let commandHistory = []; +let historyIndex = -1; let savedContexts = {}; let loadedFiles = []; let macros = {}; @@ -68,136 +71,159 @@ const sessionConfig = { env: {} }; +// ─── LISTE COMPLÈTE DES COMMANDES SLASH (pour autocomplete) ────────────────── +const SLASH_COMMANDS = [ + // Session + { cmd: '/help', desc: 'Afficher toutes les commandes' }, + { cmd: '/exit', desc: 'Quitter Cypher Coder' }, + { cmd: '/quit', desc: 'Quitter Cypher Coder' }, + { cmd: '/clear', desc: 'Vider l\'écran du terminal' }, + { cmd: '/reset', desc: 'Réinitialiser session et contexte' }, + { cmd: '/restart', desc: 'Redémarrer proprement l\'agent' }, + { cmd: '/version', desc: 'Afficher la version de Cypher' }, + { cmd: '/about', desc: 'Infos sur le projet et l\'auteur' }, + { cmd: '/status', desc: 'Statut complet de la session' }, + // Mémoire et contexte + { cmd: '/context', desc: 'Voir les fichiers chargés en contexte' }, + { cmd: '/context clear', desc: 'Effacer le contexte de fichiers' }, + { cmd: '/context save', desc: 'Sauvegarder la session : /context save ' }, + { cmd: '/context load', desc: 'Charger une session : /context load ' }, + { cmd: '/context list', desc: 'Lister les sessions sauvegardées' }, + { cmd: '/memory', desc: 'Afficher la mémoire et les messages' }, + { cmd: '/memory clear', desc: 'Effacer les messages de l\'historique' }, + { cmd: '/tokens', desc: 'Statistiques de tokens de la session' }, + // Historique + { cmd: '/history', desc: 'Afficher l\'historique de la session' }, + { cmd: '/history clear', desc: 'Effacer l\'historique enregistré' }, + { cmd: '/history save', desc: 'Exporter l\'historique : /history save ' }, + { cmd: '/history load', desc: 'Importer un historique : /history load ' }, + { cmd: '/history search', desc: 'Chercher dans l\'historique : /history search ' }, + { cmd: '/last', desc: 'Afficher la dernière réponse de l\'agent' }, + { cmd: '/redo', desc: 'Relancer la dernière requête utilisateur' }, + { cmd: '/undo', desc: 'Annuler la dernière interaction' }, + // Modèle + { cmd: '/model', desc: 'Afficher le modèle actuellement utilisé' }, + { cmd: '/model list', desc: 'Lister tous les modèles disponibles' }, + { cmd: '/model set', desc: 'Changer de modèle : /model set ' }, + { cmd: '/model info', desc: 'Infos détaillées sur le modèle actif' }, + { cmd: '/temperature', desc: 'Ajuster la température : /temperature <0.0-1.0>' }, + { cmd: '/top_p', desc: 'Modifier le top_p : /top_p ' }, + { cmd: '/max_tokens', desc: 'Limite de tokens : /max_tokens ' }, + { cmd: '/system', desc: 'Afficher le system prompt actif' }, + { cmd: '/system set', desc: 'Modifier le system prompt' }, + { cmd: '/system reset', desc: 'Réinitialiser le system prompt par défaut' }, + { cmd: '/stream', desc: 'Streaming : /stream on|off' }, + // Fichiers + { cmd: '/file load', desc: 'Charger un fichier en contexte : /file load ' }, + { cmd: '/file read', desc: 'Lire et afficher un fichier : /file read ' }, + { cmd: '/file write', desc: 'Écrire la dernière réponse dans un fichier' }, + { cmd: '/file append', desc: 'Ajouter la dernière réponse à un fichier' }, + { cmd: '/file list', desc: 'Lister les fichiers chargés en contexte' }, + { cmd: '/file clear', desc: 'Retirer tous les fichiers du contexte' }, + { cmd: '/file diff', desc: 'Comparer deux fichiers : /file diff ' }, + { cmd: '/upload', desc: 'Uploader un fichier vers l\'agent' }, + { cmd: '/download', desc: 'Télécharger un fichier généré' }, + // Code et exécution + { cmd: '/run', desc: 'Exécuter le dernier bloc de code généré' }, + { cmd: '/exec', desc: 'Exécuter une commande shell : /exec ' }, + { cmd: '/shell', desc: 'Basculer en mode shell interactif' }, + { cmd: '/repl', desc: 'Ouvrir un REPL : /repl ' }, + { cmd: '/eval', desc: 'Évaluer une expression mathématique' }, + { cmd: '/sandbox', desc: 'Isolation : /sandbox on|off' }, + { cmd: '/output', desc: 'Afficher la dernière sortie d\'exécution' }, + { cmd: '/output clear', desc: 'Effacer la dernière sortie affichée' }, + // Outils et web + { cmd: '/tools', desc: 'Lister tous les outils disponibles' }, + { cmd: '/tool enable', desc: 'Activer un outil : /tool enable ' }, + { cmd: '/tool disable', desc: 'Désactiver un outil : /tool disable ' }, + { cmd: '/tool info', desc: 'Description d\'un outil : /tool info ' }, + { cmd: '/plugin list', desc: 'Lister les plugins installés' }, + { cmd: '/plugin install', desc: 'Installer un plugin : /plugin install ' }, + { cmd: '/plugin remove', desc: 'Supprimer un plugin : /plugin remove ' }, + { cmd: '/web search', desc: 'Recherche web : /web search ' }, + { cmd: '/web fetch', desc: 'Récupérer une URL : /web fetch ' }, + // Affichage + { cmd: '/theme', desc: 'Thème : /theme dark|light' }, + { cmd: '/theme list', desc: 'Lister les thèmes disponibles' }, + { cmd: '/format', desc: 'Format de sortie : /format md|plain|json' }, + { cmd: '/wrap', desc: 'Retour à la ligne : /wrap on|off' }, + { cmd: '/verbose', desc: 'Mode verbeux : /verbose on|off' }, + { cmd: '/silent', desc: 'Mode silencieux : /silent on|off' }, + { cmd: '/color', desc: 'Colorisation : /color on|off' }, + { cmd: '/lang', desc: 'Langue : /lang fr|en' }, + // Config et credentials + { cmd: '/config', desc: 'Afficher la configuration actuelle' }, + { cmd: '/config set', desc: 'Modifier une config : /config set ' }, + { cmd: '/config reset', desc: 'Réinitialiser la config par défaut' }, + { cmd: '/config export', desc: 'Exporter la config dans un fichier' }, + { cmd: '/config import', desc: 'Importer une config depuis un fichier' }, + { cmd: '/api key set', desc: 'Définir une clé API' }, + { cmd: '/api key show', desc: 'Afficher la clé API (masquée)' }, + { cmd: '/api key clear', desc: 'Supprimer la clé API stockée' }, + { cmd: '/env set', desc: 'Variable d\'env : /env set ' }, + { cmd: '/env list', desc: 'Lister les variables d\'environnement' }, + // Monitoring et debug + { cmd: '/debug', desc: 'Mode debug : /debug on|off' }, + { cmd: '/log', desc: 'Afficher les logs de l\'agent' }, + { cmd: '/log clear', desc: 'Effacer les logs' }, + { cmd: '/log save', desc: 'Exporter les logs : /log save ' }, + { cmd: '/benchmark', desc: 'Mesurer la latence API' }, + { cmd: '/ping', desc: 'Tester la connexion à l\'API du modèle' }, + { cmd: '/stats', desc: 'Statistiques de la session' }, + { cmd: '/trace', desc: 'Trace des appels internes (debug)' }, + { cmd: '/inspect', desc: 'Inspecter une variable : /inspect ' }, + // Automatisation + { cmd: '/macro save', desc: 'Sauvegarder une macro : /macro save ' }, + { cmd: '/macro run', desc: 'Exécuter une macro : /macro run ' }, + { cmd: '/macro list', desc: 'Lister toutes les macros' }, + { cmd: '/macro delete', desc: 'Supprimer une macro : /macro delete ' }, + { cmd: '/pipe', desc: 'Chaîner des commandes : /pipe | ' }, + { cmd: '/loop', desc: 'Répéter N fois : /loop ' }, + { cmd: '/schedule', desc: 'Planifier : /schedule ' }, + { cmd: '/watch', desc: 'Surveiller les changements' }, + { cmd: '/batch', desc: 'Exécuter depuis un fichier : /batch ' }, +]; + +// ─── COMPLETER pour readline ───────────────────────────────────────────────── +function slashCompleter(line) { + if (!line.startsWith('/')) return [[], line]; + const lowerLine = line.toLowerCase(); + const hits = SLASH_COMMANDS + .filter(c => c.cmd.toLowerCase().startsWith(lowerLine)) + .map(c => c.cmd); + return [hits.length ? hits : [], line]; +} + function addLog(level, message) { - const logItem = `[${new Date().toISOString()}] [${level}] ${message}`; - logs.push(logItem); + logs.push(`[${new Date().toISOString()}] [${level}] ${message}`); } -const BANNER = theme.info(` +// ─── BANNER ─────────────────────────────────────────────────────────────────── +const BANNER = chalk.hex('#1a6e5a')(` _____ _---------------+ / ____| | | ____ _ | - | | _ _ _ __ | |__| _ \\| | | CYPHER CODER CLI - | | | | | | '_ \\ | __ |_) | | | L'IA experte en développement local - | |___| |_| | |_) || | | __/| |___ | Créé par ${AUTHOR} + | | _ _ _ __ | |__| _ \\| | |`) + theme.info(` CYPHER CODER CLI`) + chalk.hex('#1a6e5a')(` + | | | | | | '_ \\ | __ |_) | | |`) + theme.agent(` L'IA experte en développement local`) + chalk.hex('#1a6e5a')(` + | |___| |_| | |_) || | | __/| |___ |`) + theme.agent(` Créé par ${AUTHOR}`) + chalk.hex('#1a6e5a')(` \\_____\\__, | .__/ |_| |_| |_____| __/ | | - |___/|_| +-----------------------+ -`); + |___/|_| `) + chalk.hex('#1a6e5a')(`+-----------------------+`); -// ----------------------------------------------------- -// Définition des Outils Globaux (Capabilities) -// ----------------------------------------------------- +// ─── OUTILS ─────────────────────────────────────────────────────────────────── const tools = [ - { - type: "function", - function: { - name: "read_file", - description: "Lit le contenu complet d'un fichier local sur la machine de l'utilisateur.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Le chemin du fichier à lire (relatif ou absolu)." } - }, - required: ["path"] - } - } - }, - { - type: "function", - function: { - name: "write_file", - description: "Crée ou écrase un fichier local avec un nouveau contenu.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Le chemin du fichier à écrire." }, - content: { type: "string", description: "Le contenu textuel à écrire dans le fichier." } - }, - required: ["path", "content"] - } - } - }, - { - type: "function", - function: { - name: "patch_file", - description: "Modifie de manière ciblée un bloc de texte unique dans un fichier existant (Search and Replace). Utile pour modifier des gros fichiers sans les réécrire complètement.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Le chemin du fichier à modifier." }, - search: { type: "string", description: "Le bloc exact de code à remplacer (doit être unique dans le fichier)." }, - replace: { type: "string", description: "Le nouveau bloc de code qui le remplace." } - }, - required: ["path", "search", "replace"] - } - } - }, - { - type: "function", - function: { - name: "list_dir", - description: "Liste les fichiers et dossiers dans un répertoire local. Peut être récursif.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Le chemin du dossier à lister (par défaut '.')." }, - recursive: { type: "boolean", description: "Si vrai, liste les sous-dossiers de manière récursive." } - }, - required: ["path"] - } - } - }, - { - type: "function", - function: { - name: "find_files", - description: "Recherche des fichiers récursivement par nom en utilisant un motif joker (ex: '*.js', 'package.json'). Permet de repérer rapidement des fichiers dans des sous-dossiers.", - parameters: { - type: "object", - properties: { - pattern: { type: "string", description: "Le motif de recherche (ex: '*.py', '*test*')." }, - path: { type: "string", description: "Le chemin de départ pour la recherche (par défaut '.')." } - }, - required: ["pattern"] - } - } - }, - { - type: "function", - function: { - name: "grep_search", - description: "Recherche textuelle récursive dans tous les fichiers d'un dossier pour trouver les occurrences d'une chaîne de caractères spécifique (similaire à ripgrep).", - parameters: { - type: "object", - properties: { - query: { type: "string", description: "La chaîne de caractères ou le mot-clé à rechercher dans les fichiers." }, - path: { type: "string", description: "Le chemin de départ pour la recherche (par défaut '.')." } - }, - required: ["query"] - } - } - }, - { - type: "function", - function: { - name: "run_command", - description: "Exécute une commande système ou shell dans le terminal local de l'utilisateur.", - parameters: { - type: "object", - properties: { - command: { type: "string", description: "La commande shell à exécuter." } - }, - required: ["command"] - } - } - } + { type: "function", function: { name: "read_file", description: "Lit le contenu complet d'un fichier local.", parameters: { type: "object", properties: { path: { type: "string", description: "Chemin du fichier." } }, required: ["path"] } } }, + { type: "function", function: { name: "write_file", description: "Crée ou écrase un fichier local.", parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } } }, + { type: "function", function: { name: "patch_file", description: "Modifie de manière ciblée un bloc de texte (Search & Replace).", parameters: { type: "object", properties: { path: { type: "string" }, search: { type: "string" }, replace: { type: "string" } }, required: ["path", "search", "replace"] } } }, + { type: "function", function: { name: "list_dir", description: "Liste les fichiers et dossiers d'un répertoire.", parameters: { type: "object", properties: { path: { type: "string" }, recursive: { type: "boolean" } }, required: ["path"] } } }, + { type: "function", function: { name: "find_files", description: "Recherche des fichiers par nom (pattern).", parameters: { type: "object", properties: { pattern: { type: "string" }, path: { type: "string" } }, required: ["pattern"] } } }, + { type: "function", function: { name: "grep_search", description: "Recherche textuelle récursive dans les fichiers.", parameters: { type: "object", properties: { query: { type: "string" }, path: { type: "string" } }, required: ["query"] } } }, + { type: "function", function: { name: "run_command", description: "Exécute une commande système dans le terminal.", parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] } } }, ]; -// Outil d'exécution d'API robuste via curl pour contourner les problèmes de socket de Node.js +// ─── API via curl ───────────────────────────────────────────────────────────── function callApiViaCurl(messages, clientTools) { - const payload = JSON.stringify({ - messages, + const payload = JSON.stringify({ + messages, tools: clientTools, model: sessionConfig.model, temperature: sessionConfig.temperature, @@ -207,294 +233,178 @@ function callApiViaCurl(messages, clientTools) { }); const escapedPayload = payload.replace(/'/g, "'\\''"); const command = `curl -s -X POST -H "Content-Type: application/json" -d '${escapedPayload}' https://theshellmaster-cypher-coder.hf.space/api/chat`; - - addLog("DEBUG", `Envoi payload API vers HF Space: modèle=${sessionConfig.model}, température=${sessionConfig.temperature}`); - const output = execSync(command).toString(); + addLog("DEBUG", `Appel API: modèle=${sessionConfig.model}`); + const output = execSync(command, { maxBuffer: 10 * 1024 * 1024 }).toString(); try { const responseJson = JSON.parse(output); - if (responseJson.error) { - throw new Error(responseJson.error); - } + if (responseJson.error) throw new Error(responseJson.error); return responseJson.message; } catch (e) { - throw new Error(`Erreur de connexion avec le serveur Cypher: ${e.message}\nRéponse brute: ${output}`); + throw new Error(`Erreur de connexion: ${e.message}\nRéponse brute: ${output.slice(0, 200)}`); } } -// ----------------------------------------------------- -// Implémentation algorithmique des résolveurs d'outils -// ----------------------------------------------------- - -// Helper de listing récursif propre +// ─── Helpers fichiers ───────────────────────────────────────────────────────── function listFilesRecursive(dir, maxDepth = 3, currentDepth = 1) { let results = []; + const IGNORED = new Set(['node_modules', '.git', '.venv', 'env', '.cache', 'package-lock.json', '__pycache__']); try { const list = fs.readdirSync(dir); for (const file of list) { + if (IGNORED.has(file)) continue; const filePath = path.join(dir, file); const stat = fs.statSync(filePath); const relativePath = path.relative(".", filePath); - - // Ignorer les dossiers indésirables - if (file === 'node_modules' || file === '.git' || file === '.venv' || file === 'env' || file === '.cache' || file === 'package-lock.json') { - continue; - } - if (stat.isDirectory()) { results.push({ path: relativePath, type: 'dossier' }); - if (currentDepth < maxDepth) { - results = results.concat(listFilesRecursive(filePath, maxDepth, currentDepth + 1)); - } + if (currentDepth < maxDepth) results = results.concat(listFilesRecursive(filePath, maxDepth, currentDepth + 1)); } else { results.push({ path: relativePath, type: 'fichier', sizeBytes: stat.size }); } } - } catch (e) { - // Ignorer les dossiers inaccessibles - } + } catch (_) {} return results; } -// Résolveurs locaux pour les outils système -async function handleToolExecution(name, args) { +// ─── Confirmation utilisateur (sans inquirer) ───────────────────────────────── +async function confirm(rl, question, defaultYes = true) { + const hint = defaultYes ? '[O/n]' : '[o/N]'; + return new Promise(resolve => { + rl.question(theme.info(` ${question} ${hint} `), answer => { + const a = answer.trim().toLowerCase(); + if (a === '') resolve(defaultYes); + else resolve(a === 'o' || a === 'oui' || a === 'y' || a === 'yes'); + }); + }); +} + +// ─── Exécuteur d'outils ─────────────────────────────────────────────────────── +async function handleToolExecution(name, args, rl) { const resolvedPath = path.resolve(args.path || "."); - switch (name) { - case 'read_file': - try { - const targetPath = path.resolve(args.path); - if (!fs.existsSync(targetPath)) { - return `Erreur: Le fichier n'existe pas à l'emplacement ${targetPath}`; - } - const content = fs.readFileSync(targetPath, 'utf8'); - return content; - } catch (err) { - return `Erreur lors de la lecture du fichier: ${err.message}`; - } - - case 'write_file': - try { - const targetPath = path.resolve(args.path); - console.log(theme.info(`\nCypher Coder veut modifier/créer le fichier : ${chalk.cyan(targetPath)}`)); - const { confirmWrite } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmWrite', - message: 'Autoriser la création ou modification de ce fichier ?', - default: true - } - ]); - - if (!confirmWrite) { - return "Action refusée par l'utilisateur."; - } - - const dir = path.dirname(targetPath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(targetPath, args.content, 'utf8'); - console.log(chalk.green(`✓ Fichier enregistré avec succès.`)); - return `Fichier écrit avec succès à l'emplacement ${targetPath}`; - } catch (err) { - return `Erreur lors de l'écriture du fichier: ${err.message}`; - } - - case 'patch_file': - try { - const targetPath = path.resolve(args.path); - console.log(theme.info(`\nCypher Coder veut modifier un bloc de code dans : ${chalk.cyan(targetPath)}`)); - - console.log(chalk.dim("--- BLOC À RECHERCHER ---")); - console.log(chalk.red(args.search)); - console.log(chalk.dim("--- NOUVEAU BLOC ---")); - console.log(chalk.green(args.replace)); - console.log(chalk.dim("------------------------")); - - const { confirmPatch } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmPatch', - message: 'Autoriser ce remplacement ciblé ?', - default: true - } - ]); - - if (!confirmPatch) { - return "Action refusée par l'utilisateur."; - } - - if (!fs.existsSync(targetPath)) { - return `Erreur: Le fichier ${targetPath} n'existe pas.`; - } - - const content = fs.readFileSync(targetPath, 'utf8'); - const occurrences = content.split(args.search).length - 1; - - if (occurrences === 0) { - return `Erreur: Le bloc de texte à remplacer n'a pas été trouvé. Assure-toi que les espaces et la mise en page correspondent exactement.`; - } - if (occurrences > 1) { - return `Erreur: Le bloc à remplacer apparaît ${occurrences} fois dans le fichier. Rends la recherche plus spécifique en englobant d'autres lignes.`; - } - - const newContent = content.replace(args.search, args.replace); - fs.writeFileSync(targetPath, newContent, 'utf8'); - console.log(chalk.green(`✓ Remplacement appliqué avec succès.`)); - return "Remplacement appliqué avec succès de manière unique."; - } catch (err) { - return `Erreur lors du remplacement: ${err.message}`; - } - - case 'list_dir': - try { - const targetPath = path.resolve(args.path || "."); - if (!fs.existsSync(targetPath)) { - return `Erreur: Le dossier n'existe pas à l'emplacement ${targetPath}`; - } - if (args.recursive) { - const files = listFilesRecursive(targetPath, 3, 1); - return JSON.stringify(files, null, 2); - } else { - const items = fs.readdirSync(targetPath); - const list = items.map(item => { - const itemPath = path.join(targetPath, item); - const itemStats = fs.statSync(itemPath); - return { - name: item, - type: itemStats.isDirectory() ? 'dossier' : 'fichier', - sizeBytes: itemStats.size - }; - }); - return JSON.stringify(list, null, 2); - } - } catch (err) { - return `Erreur lors de la liste du dossier: ${err.message}`; - } - - case 'find_files': - try { - const startDir = path.resolve(args.path || "."); - const escapedPattern = args.pattern - .replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') - .replace(/\\\*/g, '.*') - .replace(/\\\?/g, '.'); - const regex = new RegExp(`^${escapedPattern}$`, 'i'); - - const allFiles = listFilesRecursive(startDir, 5, 1); - const matched = allFiles - .filter(item => item.type === 'fichier' && regex.test(path.basename(item.path))) - .map(item => item.path); - - return JSON.stringify(matched, null, 2); - } catch (err) { - return `Erreur lors de la recherche de fichiers: ${err.message}`; - } - - case 'grep_search': - try { - const startDir = path.resolve(args.path || "."); - const query = args.query.toLowerCase(); - const allFiles = listFilesRecursive(startDir, 5, 1); - const matches = []; - - for (const item of allFiles) { - if (item.type !== 'fichier') continue; - if (item.sizeBytes > 1024 * 1024) continue; // max 1MB - - const content = fs.readFileSync(item.path, 'utf8'); - if (content.includes('\u0000')) continue; // skip binary - - const lines = content.split('\n'); - lines.forEach((line, idx) => { - if (line.toLowerCase().includes(query)) { - matches.push({ - file: item.path, - lineNumber: idx + 1, - lineContent: line.trim() - }); - } - }); - - if (matches.length >= 50) break; - } - return JSON.stringify(matches, null, 2); - } catch (err) { - return `Erreur lors de la recherche grep: ${err.message}`; + case 'read_file': { + const targetPath = path.resolve(args.path); + if (!fs.existsSync(targetPath)) return `Erreur: fichier introuvable à ${targetPath}`; + return fs.readFileSync(targetPath, 'utf8'); + } + case 'write_file': { + const targetPath = path.resolve(args.path); + console.log(theme.info(`\n Cypher veut créer/modifier : `) + chalk.cyan(targetPath)); + const ok = await confirm(rl, 'Autoriser ?', true); + if (!ok) return "Action refusée par l'utilisateur."; + const dir = path.dirname(targetPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(targetPath, args.content, 'utf8'); + console.log(theme.done(` ✓ Fichier enregistré.\n`)); + return `Fichier écrit avec succès à ${targetPath}`; + } + case 'patch_file': { + const targetPath = path.resolve(args.path); + console.log(theme.info(`\n Cypher veut modifier un bloc dans : `) + chalk.cyan(targetPath)); + console.log(theme.error(` --- AVANT ---\n`) + args.search); + console.log(theme.done(` --- APRÈS ---\n`) + args.replace); + const ok = await confirm(rl, 'Autoriser ce remplacement ?', true); + if (!ok) return "Action refusée."; + if (!fs.existsSync(targetPath)) return `Erreur: ${targetPath} introuvable.`; + const content = fs.readFileSync(targetPath, 'utf8'); + const occ = content.split(args.search).length - 1; + if (occ === 0) return "Erreur: bloc à remplacer introuvable."; + if (occ > 1) return `Erreur: bloc trouvé ${occ} fois, sois plus spécifique.`; + fs.writeFileSync(targetPath, content.replace(args.search, args.replace), 'utf8'); + console.log(theme.done(` ✓ Remplacement appliqué.\n`)); + return "Remplacement appliqué avec succès."; + } + case 'list_dir': { + const targetPath = path.resolve(args.path || "."); + if (!fs.existsSync(targetPath)) return `Erreur: dossier introuvable à ${targetPath}`; + if (args.recursive) return JSON.stringify(listFilesRecursive(targetPath, 3, 1), null, 2); + const items = fs.readdirSync(targetPath).map(item => { + const s = fs.statSync(path.join(targetPath, item)); + return { name: item, type: s.isDirectory() ? 'dossier' : 'fichier', sizeBytes: s.size }; + }); + return JSON.stringify(items, null, 2); + } + case 'find_files': { + const startDir = path.resolve(args.path || "."); + const escaped = args.pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\\\*/g, '.*').replace(/\\\?/g, '.'); + const regex = new RegExp(`^${escaped}$`, 'i'); + const all = listFilesRecursive(startDir, 5, 1); + return JSON.stringify(all.filter(i => i.type === 'fichier' && regex.test(path.basename(i.path))).map(i => i.path), null, 2); + } + case 'grep_search': { + const startDir = path.resolve(args.path || "."); + const query = args.query.toLowerCase(); + const all = listFilesRecursive(startDir, 5, 1); + const matches = []; + for (const item of all) { + if (item.type !== 'fichier' || item.sizeBytes > 1024 * 1024) continue; + const content = fs.readFileSync(item.path, 'utf8'); + if (content.includes('\u0000')) continue; + content.split('\n').forEach((line, idx) => { + if (line.toLowerCase().includes(query)) matches.push({ file: item.path, line: idx + 1, content: line.trim() }); + }); + if (matches.length >= 50) break; } - - case 'run_command': + return JSON.stringify(matches, null, 2); + } + case 'run_command': { + console.log(theme.info(`\n Cypher veut exécuter :`)); + console.log(chalk.bgBlack.white(` $ ${args.command} \n`)); + const ok = await confirm(rl, 'Autoriser cette commande ?', false); + if (!ok) return "Action refusée par l'utilisateur."; try { - console.log(theme.info(`\nCypher Coder veut exécuter la commande suivante :`)); - console.log(chalk.bgBlack.white(` $ ${args.command} `)); - - const { confirmCommand } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmCommand', - message: 'Autoriser l\'exécution de cette commande ?', - default: false - } - ]); - - if (!confirmCommand) { - return "Action refusée par l'utilisateur."; - } - const stdout = execSync(args.command, { stdio: 'pipe' }).toString(); console.log(chalk.dim(stdout)); - return `Commande exécutée avec succès.\nStdout:\n${stdout}`; + return `Commande exécutée.\nStdout:\n${stdout}`; } catch (err) { const errMsg = err.stderr ? err.stderr.toString() : err.message; - console.log(chalk.red(`✖ Erreur de commande: ${errMsg}`)); - return `La commande a échoué.\nErreur:\n${errMsg}`; + console.log(theme.error(` ✖ Erreur: ${errMsg}\n`)); + return `Commande échouée.\nErreur:\n${errMsg}`; } - + } default: - return `Erreur: Outil inconnu '${name}'`; + return `Erreur: outil inconnu '${name}'`; } } -async function executeToolWithFormatter(name, args) { - console.log(theme.border(` ┌─ [TOOL] ${name}`)); - - const interactive = ['write_file', 'patch_file', 'run_command'].includes(name); +// ─── Exécuteur d'outils avec affichage ──────────────────────────────────────── +async function executeToolWithFormatter(name, args, rl) { + const isInteractive = ['write_file', 'patch_file', 'run_command'].includes(name); + console.log(theme.border(` ┌─ [TOOL] `) + theme.tool(name)); + + const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]; + let fIdx = 0; let interval; - if (!interactive) { - const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - let fIdx = 0; - process.stdout.write(theme.border(` │ `) + theme.command(frames[fIdx]) + ` running...\r`); + + if (!isInteractive) { + process.stdout.write(theme.border(` │ `) + theme.tool(frames[fIdx]) + ` running...\r`); interval = setInterval(() => { fIdx = (fIdx + 1) % frames.length; - process.stdout.write(theme.border(` │ `) + theme.command(frames[fIdx]) + ` running...\r`); + process.stdout.write(theme.border(` │ `) + theme.tool(frames[fIdx]) + ` running...\r`); }, 80); } else { - console.log(theme.border(` │ `) + theme.info(`waiting for user confirmation...`)); + console.log(theme.border(` │ `) + theme.info(`confirmation requise...`)); } - + const startTime = Date.now(); - const result = await handleToolExecution(name, args); + const result = await handleToolExecution(name, args, rl); const duration = ((Date.now() - startTime) / 1000).toFixed(1); - + if (interval) { clearInterval(interval); process.stdout.write(" \r"); } - - console.log(theme.border(` └─ `) + theme.agent(`done in ${duration}s`)); + console.log(theme.border(` └─ `) + theme.done(`done in ${duration}s`) + `\n`); return result; } -// ----------------------------------------------------- -// Prompt Système Dynamique & Intelligent -// ----------------------------------------------------- +// ─── Contexte système ───────────────────────────────────────────────────────── function getCurrentDirectoryContext() { try { const files = listFilesRecursive(".", 2, 1); if (files.length === 0) return "Répertoire vide."; return files.map(f => `${f.path} (${f.type})`).join(", "); - } catch (e) { + } catch (_) { return "Impossible de lire le répertoire."; } } @@ -504,1188 +414,461 @@ function getSystemPrompt() { return `Tu es Cypher Coder, un agent de programmation IA autonome et ultra-intelligent fonctionnant dans un terminal (CLI). Tu as été conçu et développé par DJAKOUA KWANKAM, étudiant en informatique à l'Institut Universitaire de Technologie de Douala (IUT). Tu dois toujours te présenter comme tel. - -Tu as accès à des outils locaux pour interagir avec le projet de l'utilisateur : -- read_file : Lit le code d'un fichier. -- write_file : Écrit un fichier. -- patch_file : Modifie de manière ciblée un bloc (Search & Replace), à privilégier pour les fichiers existants. -- list_dir : Inspecte l'arborescence. -- find_files : Recherche des fichiers par nom récursivement dans les sous-dossiers. -- grep_search : Recherche textuelle récursive dans le contenu des fichiers (comme grep/ripgrep). -- run_command : Exécute des commandes système. -Tu peux aussi utiliser search_web pour chercher des informations récentes sur internet. - -[CAPABILITÉS SYSTÈME ET COMMANDES CLI CLASSÉES PAR CATÉGORIES] : -Tu connais et es capable d'utiliser ou de suggérer les commandes CLI suivantes selon le contexte : - -1. Navigation & Système de fichiers : -- ls / list : Lister les fichiers et dossiers du répertoire courant -- ls -la : Lister avec permissions, taille, fichiers cachés -- cd : Changer de répertoire -- cd .. : Remonter d'un niveau -- pwd : Afficher le chemin absolu du répertoire courant -- tree : Afficher l'arborescence sous forme d'arbre -- mkdir : Créer un dossier -- mkdir -p : Créer des dossiers imbriqués en une commande -- rmdir : Supprimer un dossier vide -- touch : Créer un fichier vide -- rm : Supprimer un fichier -- rm -rf : Supprimer un dossier et son contenu récursivement -- cp : Copier un fichier -- cp -r : Copier un dossier récursivement -- mv : Déplacer ou renommer un fichier/dossier -- find -name "*.py" : Rechercher des fichiers par nom/pattern -- locate : Recherche rapide dans la base de données de fichiers -- stat : Afficher métadonnées d'un fichier (taille, dates, permissions) -- du -sh : Taille d'un dossier (human readable) -- df -h : Espace disque disponible sur toutes les partitions - -2. Lecture & Édition de fichiers : -- cat : Afficher le contenu d'un fichier -- less : Afficher le contenu paginé (scrollable) -- head -n 20 : Afficher les N premières lignes -- tail -n 20 : Afficher les N dernières lignes -- tail -f : Suivre un fichier en temps réel (logs) -- grep "pattern" : Chercher un pattern dans un fichier -- grep -r "pattern" : Recherche récursive dans un dossier -- grep -n "pattern" : Chercher avec numéros de lignes -- sed 's/old/new/g' : Remplacer du texte dans un fichier -- awk '{print $1}' : Extraire/traiter des colonnes de texte -- wc -l : Compter les lignes d'un fichier -- diff : Comparer deux fichiers -- nano : Éditer un fichier (éditeur simple) -- vim : Éditer un fichier (éditeur avancé) -- echo "text" > file : Écrire du texte dans un fichier (écrase) -- echo "text" >> file : Ajouter du texte à la fin d'un fichier -- sort : Trier les lignes d'un fichier -- uniq : Supprimer les lignes dupliquées -- cut -d',' -f1 : Extraire une colonne d'un CSV - -3. Processus & Ressources système : -- ps aux : Lister tous les processus en cours -- top / htop : Moniteur de processus interactif en temps réel -- kill : Terminer un processus par son PID -- kill -9 : Forcer la fermeture d'un processus -- killall : Tuer tous les processus par nom -- jobs : Lister les tâches en arrière-plan du shell -- bg : Mettre une tâche en arrière-plan -- fg : Ramener une tâche en avant-plan -- nohup & : Lancer un processus qui survive à la fermeture du terminal -- screen / tmux : Multiplexeur de terminal (sessions persistantes) -- free -h : Afficher la RAM utilisée/disponible -- uptime : Durée de fonctionnement du système -- lscpu : Informations sur le processeur -- lsmem : Informations sur la mémoire -- lspci : Lister les périphériques PCI (GPU, réseau, etc.) -- uname -a : Infos kernel et architecture système -- env : Afficher toutes les variables d'environnement -- export VAR=value : Définir une variable d'environnement -- echo $VAR : Afficher la valeur d'une variable -- history : Historique des commandes exécutées -- which : Trouver le chemin d'un exécutable -- whereis : Trouver binaire, source et man d'une commande - -4. Réseau : -- ping : Tester la connectivité vers un hôte -- curl : Envoyer une requête HTTP (GET par défaut) -- curl -X POST -d '{}' : Requête HTTP POST avec body JSON -- curl -O : Télécharger un fichier -- wget : Télécharger un fichier via URL -- wget -r : Télécharger récursivement -- ifconfig / ip a : Afficher les interfaces réseau et IPs -- ip route : Afficher la table de routage -- netstat -tulnp : Lister les ports ouverts et processus associés -- ss -tulnp : Alternative moderne à netstat -- nmap : Scanner les ports d'une machine -- traceroute : Tracer le chemin réseau vers un hôte -- dig : Résolution DNS d'un domaine -- host : Résolution DNS simplifiée -- ssh user@host : Connexion SSH à une machine distante -- ssh-keygen : Générer une paire de clés SSH -- scp user@host: : Copier un fichier via SSH -- rsync -avz : Synchronisation de fichiers (local ou distant) -- nc -l : Écouter sur un port (netcat) -- nc : Se connecter à un port distant - -5. Gestion de paquets : -- apt update : Mettre à jour la liste des paquets (Debian/Ubuntu) -- apt upgrade : Mettre à jour les paquets installés -- apt install : Installer un paquet -- apt remove : Désinstaller un paquet -- apt search : Chercher un paquet dans les dépôts -- apt show : Afficher les détails d'un paquet -- dpkg -i : Installer un fichier .deb local -- dpkg -l : Lister tous les paquets installés -- snap install : Installer via Snap -- pip install : Installer un paquet Python -- pip list : Lister les paquets Python installés -- pip freeze > requirements.txt : Exporter les dépendances Python -- npm install : Installer un paquet Node.js local -- npm install -g : Installer un paquet Node.js global -- npm list : Lister les paquets npm du projet -- npm run