cypher-coder / index.js
DJAKOUA KWANKAM BRAYAN STEVE
refactor: update CLI interface with autocomplete, history navigation, and slash commands
26c6a7e
#!/usr/bin/env node
import chalk from 'chalk';
import readline from 'readline';
import ora from 'ora';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { execSync, spawn } from 'child_process';
import { marked } from 'marked';
import TerminalRenderer from 'marked-terminal';
const theme = {
agent: chalk.hex('#00FFAA'),
user: chalk.hex('#C792EA'),
command: chalk.hex('#FFD700'),
error: chalk.hex('#FF5555'),
info: chalk.hex('#569CD6'),
border: chalk.hex('#2A2A2A'),
text: chalk.hex('#E0E0E0'),
dim: chalk.hex('#444444'),
tool: chalk.hex('#FFD700'),
done: chalk.hex('#3ddc97'),
};
marked.setOptions({
renderer: new TerminalRenderer({
code: theme.command,
blockquote: theme.agent,
html: theme.agent,
heading: theme.info,
firstHeading: theme.info,
listitem: theme.agent,
table: theme.agent,
strong: theme.agent,
em: theme.agent,
link: theme.info,
href: theme.info,
unstyled: theme.agent,
tab: 2
})
});
const AUTHOR = "DJAKOUA KWANKAM";
const APP_NAME = "Cypher Coder";
const VERSION = "1.0.0";
let chatMessages = [];
let lastUserInput = "";
let lastAssistantResponse = "";
let commandHistory = [];
let historyIndex = -1;
let savedContexts = {};
let loadedFiles = [];
let macros = {};
let logs = [];
const sessionConfig = {
model: "Qwen/Qwen2.5-Coder-7B-Instruct",
temperature: 0.7,
top_p: 0.9,
max_tokens: 2048,
stream: false,
sandbox: false,
verbose: false,
silent: false,
color: true,
theme: "dark",
format: "md",
lang: "fr",
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 <nom>' },
{ cmd: '/context load', desc: 'Charger une session : /context load <nom>' },
{ 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 <fichier>' },
{ cmd: '/history load', desc: 'Importer un historique : /history load <fichier>' },
{ cmd: '/history search', desc: 'Chercher dans l\'historique : /history search <terme>' },
{ 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 <nom>' },
{ 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 <valeur>' },
{ cmd: '/max_tokens', desc: 'Limite de tokens : /max_tokens <n>' },
{ 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 <chemin>' },
{ cmd: '/file read', desc: 'Lire et afficher un fichier : /file read <chemin>' },
{ 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 <f1> <f2>' },
{ 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>' },
{ cmd: '/shell', desc: 'Basculer en mode shell interactif' },
{ cmd: '/repl', desc: 'Ouvrir un REPL : /repl <python|node|...>' },
{ 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 <nom>' },
{ cmd: '/tool disable', desc: 'Désactiver un outil : /tool disable <nom>' },
{ cmd: '/tool info', desc: 'Description d\'un outil : /tool info <nom>' },
{ cmd: '/plugin list', desc: 'Lister les plugins installés' },
{ cmd: '/plugin install', desc: 'Installer un plugin : /plugin install <nom>' },
{ cmd: '/plugin remove', desc: 'Supprimer un plugin : /plugin remove <nom>' },
{ cmd: '/web search', desc: 'Recherche web : /web search <requête>' },
{ cmd: '/web fetch', desc: 'Récupérer une URL : /web fetch <url>' },
// 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 <cle> <valeur>' },
{ 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 <VAR> <valeur>' },
{ 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 <fichier>' },
{ 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 <var>' },
// Automatisation
{ cmd: '/macro save', desc: 'Sauvegarder une macro : /macro save <nom>' },
{ cmd: '/macro run', desc: 'Exécuter une macro : /macro run <nom>' },
{ cmd: '/macro list', desc: 'Lister toutes les macros' },
{ cmd: '/macro delete', desc: 'Supprimer une macro : /macro delete <nom>' },
{ cmd: '/pipe', desc: 'Chaîner des commandes : /pipe <cmd1> | <cmd2>' },
{ cmd: '/loop', desc: 'Répéter N fois : /loop <n> <commande>' },
{ cmd: '/schedule', desc: 'Planifier : /schedule <cron> <cmd>' },
{ cmd: '/watch', desc: 'Surveiller les changements' },
{ cmd: '/batch', desc: 'Exécuter depuis un fichier : /batch <fichier>' },
];
// ─── 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) {
logs.push(`[${new Date().toISOString()}] [${level}] ${message}`);
}
// ─── BANNER ───────────────────────────────────────────────────────────────────
const BANNER = chalk.hex('#1a6e5a')(`
_____ _---------------+
/ ____| | | ____ _ |
| | _ _ _ __ | |__| _ \\| | |`) + 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')(`+-----------------------+`);
// ─── OUTILS ───────────────────────────────────────────────────────────────────
const tools = [
{ 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"] } } },
];
// ─── API via curl ─────────────────────────────────────────────────────────────
function callApiViaCurl(messages, clientTools) {
const payload = JSON.stringify({
messages,
tools: clientTools,
model: sessionConfig.model,
temperature: sessionConfig.temperature,
top_p: sessionConfig.top_p,
max_tokens: sessionConfig.max_tokens,
username: os.userInfo().username || "local-user"
});
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", `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);
return responseJson.message;
} catch (e) {
throw new Error(`Erreur de connexion: ${e.message}\nRéponse brute: ${output.slice(0, 200)}`);
}
}
// ─── 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);
if (stat.isDirectory()) {
results.push({ path: relativePath, type: 'dossier' });
if (currentDepth < maxDepth) results = results.concat(listFilesRecursive(filePath, maxDepth, currentDepth + 1));
} else {
results.push({ path: relativePath, type: 'fichier', sizeBytes: stat.size });
}
}
} catch (_) {}
return results;
}
// ─── 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': {
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;
}
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 {
const stdout = execSync(args.command, { stdio: 'pipe' }).toString();
console.log(chalk.dim(stdout));
return `Commande exécutée.\nStdout:\n${stdout}`;
} catch (err) {
const errMsg = err.stderr ? err.stderr.toString() : err.message;
console.log(theme.error(` ✖ Erreur: ${errMsg}\n`));
return `Commande échouée.\nErreur:\n${errMsg}`;
}
}
default:
return `Erreur: outil inconnu '${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 (!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.tool(frames[fIdx]) + ` running...\r`);
}, 80);
} else {
console.log(theme.border(` │ `) + theme.info(`confirmation requise...`));
}
const startTime = Date.now();
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.done(`done in ${duration}s`) + `\n`);
return result;
}
// ─── 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 (_) {
return "Impossible de lire le répertoire.";
}
}
function getSystemPrompt() {
const dirContext = getCurrentDirectoryContext();
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 aux outils: read_file, write_file, patch_file, list_dir, find_files, grep_search, run_command.
Tu peux aussi utiliser search_web pour chercher des informations récentes.
Toutes les actions système requièrent une validation explicite de l'utilisateur.
Sois précis, concis et direct. Formate tes réponses en Markdown standard.
[RÉPERTOIRE COURANT]: ${dirContext}`;
}
function initChat() {
chatMessages = [{ role: "system", content: getSystemPrompt() }];
}
// ─── Boucle agent ─────────────────────────────────────────────────────────────
async function runAgentTurn(rl) {
const spinner = ora({ text: theme.agent('Cypher réfléchit...'), color: 'cyan' }).start();
try {
const replyMessage = callApiViaCurl(chatMessages, tools);
spinner.stop();
chatMessages.push(replyMessage);
if (replyMessage.content) {
lastAssistantResponse = replyMessage.content;
addLog("INFO", "Réponse enregistrée.");
process.stdout.write('\n' + theme.agent('▸ Cypher : ') + '\n');
console.log(marked(replyMessage.content));
}
if (replyMessage.tool_calls && replyMessage.tool_calls.length > 0) {
for (const tc of replyMessage.tool_calls) {
const name = tc.function.name;
const args = JSON.parse(tc.function.arguments);
const result = await executeToolWithFormatter(name, args, rl);
chatMessages.push({ role: "tool", name, tool_call_id: tc.id, content: result });
}
return await runAgentTurn(rl);
}
} catch (error) {
spinner.stop();
console.log(theme.error(`\n✖ Erreur : ${error.message}\n`));
}
}
// ─── Commandes slash ──────────────────────────────────────────────────────────
async function handleSlashCommand(text, rl) {
if (!text.startsWith('/')) return false;
const parts = text.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
if (parts.length === 0) return true;
const commandName = parts[0].toLowerCase();
const rawArgs = parts.slice(1).map(a => a.replace(/^["']|["']$/g, ''));
const cleanArgs = [];
const flags = {};
for (let i = 0; i < rawArgs.length; i++) {
if (rawArgs[i].startsWith('--')) {
const key = rawArgs[i].slice(2);
flags[key] = (rawArgs[i+1] && !rawArgs[i+1].startsWith('--')) ? rawArgs[++i] : true;
} else {
cleanArgs.push(rawArgs[i]);
}
}
addLog("INFO", `Commande slash: ${text}`);
commandHistory.push(text);
switch (commandName) {
case '/help':
console.log(theme.info('\n CYPHER CODER CLI — COMMANDES DISPONIBLES\n'));
const categories = [
{ title: 'Session', cmds: SLASH_COMMANDS.slice(0, 9) },
{ title: 'Mémoire & Contexte', cmds: SLASH_COMMANDS.slice(9, 17) },
{ title: 'Historique', cmds: SLASH_COMMANDS.slice(17, 25) },
{ title: 'Modèle & Config', cmds: SLASH_COMMANDS.slice(25, 37) },
{ title: 'Fichiers', cmds: SLASH_COMMANDS.slice(37, 46) },
{ title: 'Code & Exécution', cmds: SLASH_COMMANDS.slice(46, 55) },
{ title: 'Outils & Web', cmds: SLASH_COMMANDS.slice(55, 64) },
{ title: 'Affichage', cmds: SLASH_COMMANDS.slice(64, 72) },
{ title: 'Config & Credentials', cmds: SLASH_COMMANDS.slice(72, 82) },
{ title: 'Monitoring & Debug', cmds: SLASH_COMMANDS.slice(82, 92) },
{ title: 'Automatisation', cmds: SLASH_COMMANDS.slice(92) },
];
for (const cat of categories) {
console.log(theme.command(` ${cat.title} :`));
for (const c of cat.cmds) {
console.log(` ${theme.tool(c.cmd.padEnd(24))} ${theme.dim(c.desc)}`);
}
console.log('');
}
break;
case '/exit': case '/quit':
console.log(theme.dim('\n Fermeture de Cypher Coder. À bientôt !\n'));
rl.close();
process.exit(0);
case '/clear':
console.clear();
console.log(BANNER + '\n');
break;
case '/reset':
initChat(); commandHistory = []; loadedFiles = [];
console.log(theme.done(' Session réinitialisée. Contexte et historique effacés.\n'));
break;
case '/restart':
initChat(); console.clear(); console.log(BANNER + '\n');
console.log(theme.done(' Cypher Coder redémarré.\n'));
break;
case '/version':
console.log(theme.info(`\n Cypher Coder v${VERSION}\n`));
break;
case '/about':
console.log(theme.info('\n === À propos de Cypher Coder ==='));
console.log(` Créateur : ${theme.agent(AUTHOR)}`);
console.log(` Institut : Institut Universitaire de Technologie de Douala (IUT)`);
console.log(` Modèle : ${sessionConfig.model}`);
console.log(` Version : ${VERSION}`);
console.log(` GitHub : TheShellMaster\n`);
break;
case '/status':
console.log(theme.info('\n === Statut de la session ==='));
console.log(` Dossier de travail : ${chalk.cyan(path.resolve('.'))}`);
console.log(` Modèle actif : ${theme.agent(sessionConfig.model)}`);
console.log(` Température : ${sessionConfig.temperature}`);
console.log(` Max tokens : ${sessionConfig.max_tokens}`);
console.log(` Messages en mémoire: ${chatMessages.length}`);
console.log(` Fichiers chargés : ${loadedFiles.length}`);
console.log(` Commandes tapées : ${commandHistory.length}\n`);
break;
case '/tokens':
console.log(theme.info(`\n Messages en contexte : ${chatMessages.length}`));
console.log(theme.info(` Fichiers chargés : ${loadedFiles.length}\n`));
break;
case '/history':
if (cleanArgs[0] === 'clear') { commandHistory = []; console.log(theme.done(' Historique effacé.\n')); break; }
if (cleanArgs[0] === 'save') {
const file = cleanArgs[1] || 'cypher_history.txt';
fs.writeFileSync(file, commandHistory.join('\n'), 'utf8');
console.log(theme.done(` Historique exporté dans ${file}\n`));
break;
}
if (cleanArgs[0] === 'search') {
const term = cleanArgs.slice(1).join(' ').toLowerCase();
const found = commandHistory.filter(h => h.toLowerCase().includes(term));
found.forEach((h, i) => console.log(` ${theme.dim(String(i+1).padStart(3))} ${h}`));
break;
}
console.log(theme.info('\n Historique de la session :'));
commandHistory.forEach((h, i) => console.log(` ${theme.dim(String(i+1).padStart(3))} ${h}`));
console.log('');
break;
case '/last':
console.log(theme.agent('\n▸ Cypher (dernier message) :'));
console.log(lastAssistantResponse || theme.dim(' Aucune réponse enregistrée.'));
console.log('');
break;
case '/redo':
if (lastUserInput) {
console.log(theme.user(` ❯ Relancement : ${lastUserInput}\n`));
chatMessages.push({ role: "user", content: lastUserInput });
await runAgentTurn(rl);
}
break;
case '/model':
if (cleanArgs[0] === 'list') {
console.log(theme.info('\n Modèles disponibles :'));
const models = ['Qwen/Qwen2.5-Coder-32B-Instruct','Qwen/Qwen2.5-Coder-7B-Instruct','bigcode/starcoder2-15b','deepseek-ai/deepseek-coder-33b-instruct'];
models.forEach(m => {
const active = m === sessionConfig.model ? theme.agent(' [actif]') : '';
console.log(` ${theme.command(m)}${active}`);
});
console.log('');
} else if (cleanArgs[0] === 'set') {
sessionConfig.model = cleanArgs.slice(1).join(' ');
console.log(theme.done(` Modèle changé : ${sessionConfig.model}\n`));
} else {
console.log(theme.info(`\n Modèle actif : ${theme.agent(sessionConfig.model)}\n`));
}
break;
case '/temperature':
if (cleanArgs[0]) { sessionConfig.temperature = parseFloat(cleanArgs[0]); console.log(theme.done(` Température : ${sessionConfig.temperature}\n`)); }
else console.log(theme.info(` Température actuelle : ${sessionConfig.temperature}\n`));
break;
case '/max_tokens':
if (cleanArgs[0]) { sessionConfig.max_tokens = parseInt(cleanArgs[0], 10); console.log(theme.done(` Max tokens : ${sessionConfig.max_tokens}\n`)); }
break;
case '/system':
if (cleanArgs[0] === 'reset') { initChat(); console.log(theme.done(' System prompt réinitialisé.\n')); }
else if (cleanArgs[0] === 'set') { chatMessages[0] = { role: 'system', content: cleanArgs.slice(1).join(' ') }; console.log(theme.done(' System prompt modifié.\n')); }
else { console.log(theme.info('\n System prompt actuel :\n')); console.log(theme.dim(chatMessages[0]?.content || 'Aucun')); console.log(''); }
break;
case '/file':
if (cleanArgs[0] === 'load') {
const fp = path.resolve(cleanArgs[1] || '');
if (!fp || !fs.existsSync(fp)) { console.log(theme.error(` Fichier introuvable: ${fp}\n`)); break; }
const content = fs.readFileSync(fp, 'utf8');
loadedFiles.push(fp);
chatMessages.push({ role: 'user', content: `[Fichier chargé: ${fp}]\n\`\`\`\n${content}\n\`\`\`` });
console.log(theme.done(` Fichier chargé en contexte: ${fp}\n`));
} else if (cleanArgs[0] === 'read') {
const fp = path.resolve(cleanArgs[1] || '');
if (!fp || !fs.existsSync(fp)) { console.log(theme.error(` Fichier introuvable\n`)); break; }
console.log(theme.info(`\n === ${fp} ===\n`));
console.log(fs.readFileSync(fp, 'utf8'));
} else if (cleanArgs[0] === 'list') {
console.log(theme.info('\n Fichiers chargés en contexte :'));
loadedFiles.length ? loadedFiles.forEach(f => console.log(` - ${f}`)) : console.log(theme.dim(' Aucun fichier chargé.'));
console.log('');
} else if (cleanArgs[0] === 'clear') {
loadedFiles = []; console.log(theme.done(' Contexte de fichiers effacé.\n'));
} else if (cleanArgs[0] === 'write') {
const fp = cleanArgs[1] || 'cypher_output.md';
fs.writeFileSync(fp, lastAssistantResponse, 'utf8');
console.log(theme.done(` Réponse exportée dans ${fp}\n`));
}
break;
case '/exec': {
const cmd = cleanArgs.join(' ');
if (!cmd) { console.log(theme.error(' Usage: /exec <commande>\n')); break; }
await executeToolWithFormatter('run_command', { command: cmd }, rl);
break;
}
case '/shell':
console.log(theme.info(' Ouverture du shell... (Ctrl+D pour revenir)\n'));
const sh = spawn('/bin/bash', { stdio: 'inherit' });
await new Promise(r => sh.on('close', r));
break;
case '/tools':
console.log(theme.info('\n Outils disponibles :'));
tools.forEach(t => console.log(` ${theme.command(t.function.name.padEnd(16))} ${theme.dim(t.function.description)}`));
console.log('');
break;
case '/config':
if (cleanArgs[0] === 'set' && cleanArgs[1] && cleanArgs[2]) {
sessionConfig[cleanArgs[1]] = cleanArgs[2];
console.log(theme.done(` Config : ${cleanArgs[1]} = ${cleanArgs[2]}\n`));
} else if (cleanArgs[0] === 'reset') {
console.log(theme.done(' Config réinitialisée.\n'));
} else {
console.log(theme.info('\n Configuration actuelle :'));
Object.entries(sessionConfig).forEach(([k, v]) => {
if (k !== 'env') console.log(` ${theme.command(k.padEnd(16))} ${String(v)}`);
});
console.log('');
}
break;
case '/env':
if (cleanArgs[0] === 'list') {
console.log(theme.info('\n Variables d\'environnement :'));
Object.entries(sessionConfig.env).forEach(([k, v]) => console.log(` ${theme.command(k)} = ${v}`));
if (!Object.keys(sessionConfig.env).length) console.log(theme.dim(' Aucune variable définie.'));
console.log('');
} else if (cleanArgs[0] === 'set' && cleanArgs[1] && cleanArgs[2]) {
sessionConfig.env[cleanArgs[1]] = cleanArgs[2];
console.log(theme.done(` ${cleanArgs[1]} défini.\n`));
}
break;
case '/debug':
sessionConfig.verbose = cleanArgs[0] === 'on';
console.log(theme.done(` Mode debug : ${sessionConfig.verbose ? 'activé' : 'désactivé'}\n`));
break;
case '/verbose':
sessionConfig.verbose = cleanArgs[0] === 'on';
console.log(theme.done(` Mode verbose : ${sessionConfig.verbose ? 'activé' : 'désactivé'}\n`));
break;
case '/log':
if (cleanArgs[0] === 'clear') { logs = []; console.log(theme.done(' Logs effacés.\n')); }
else if (cleanArgs[0] === 'save') {
const file = cleanArgs[1] || 'cypher.log';
fs.writeFileSync(file, logs.join('\n'), 'utf8');
console.log(theme.done(` Logs exportés dans ${file}\n`));
} else {
console.log(theme.info('\n === Logs récents ==='));
logs.slice(-20).forEach(l => console.log(theme.dim(` ${l}`)));
console.log('');
}
break;
case '/stats':
console.log(theme.info('\n === Statistiques ==='));
console.log(` Messages en mémoire : ${chatMessages.length}`);
console.log(` Fichiers chargés : ${loadedFiles.length}`);
console.log(` Commandes tapées : ${commandHistory.length}`);
console.log(` Entrées de log : ${logs.length}\n`);
break;
case '/benchmark': {
console.log(theme.info(' Lancement du benchmark API...'));
const start = Date.now();
try {
callApiViaCurl([{ role: "user", content: "Dis hello" }], []);
console.log(theme.done(` Latence aller-retour : ${Date.now() - start}ms\n`));
} catch (e) { console.log(theme.error(` Échec : ${e.message}\n`)); }
break;
}
case '/ping': {
console.log(theme.info(' Ping API...'));
try {
const s = Date.now();
execSync("curl -sI https://theshellmaster-cypher-coder.hf.space/ | head -n 1");
console.log(theme.done(` Connectivité OK (${Date.now() - s}ms)\n`));
} catch (_) { console.log(theme.error(' Connexion échouée.\n')); }
break;
}
case '/trace':
console.log(theme.info(' Dernier log :'), theme.dim(logs[logs.length - 1] || 'Aucun'));
break;
case '/inspect':
const vname = cleanArgs[0];
if (vname === 'chatMessages') console.log(chatMessages);
else if (vname === 'loadedFiles') console.log(loadedFiles);
else console.log(sessionConfig);
break;
case '/macro':
if (cleanArgs[0] === 'save') {
macros[cleanArgs[1]] = [...commandHistory];
console.log(theme.done(` Macro '${cleanArgs[1]}' sauvegardée.\n`));
} else if (cleanArgs[0] === 'run') {
if (macros[cleanArgs[1]]) {
for (const cmd of macros[cleanArgs[1]]) {
if (!cmd.startsWith('/macro')) await handleSlashCommand(cmd, rl);
}
} else console.log(theme.error(' Macro introuvable.\n'));
} else if (cleanArgs[0] === 'list') {
console.log(theme.info(' Macros :'), Object.keys(macros).join(', ') || theme.dim('aucune'));
} else if (cleanArgs[0] === 'delete') {
delete macros[cleanArgs[1]];
console.log(theme.done(` Macro '${cleanArgs[1]}' supprimée.\n`));
}
break;
case '/loop': {
const times = parseInt(cleanArgs[0], 10);
const cmdToLoop = cleanArgs.slice(1).join(' ');
if (!isNaN(times) && cmdToLoop) {
for (let idx = 0; idx < times; idx++) {
console.log(theme.info(` [Boucle ${idx+1}/${times}]`));
await handleSlashCommand(cmdToLoop, rl);
}
}
break;
}
case '/memory':
if (cleanArgs[0] === 'clear') { chatMessages = [chatMessages[0]]; console.log(theme.done(' Mémoire effacée.\n')); }
else {
console.log(theme.info('\n Mémoire de la session :'));
chatMessages.forEach((m, i) => {
if (m.role !== 'system') console.log(` ${theme.dim(String(i).padStart(3))} [${m.role}] ${String(m.content || '').slice(0, 80)}...`);
});
console.log('');
}
break;
case '/context':
if (cleanArgs[0] === 'clear') { loadedFiles = []; console.log(theme.done(' Contexte effacé.\n')); }
else if (cleanArgs[0] === 'save') {
savedContexts[cleanArgs[1]] = { messages: [...chatMessages], files: [...loadedFiles] };
console.log(theme.done(` Contexte '${cleanArgs[1]}' sauvegardé.\n`));
} else if (cleanArgs[0] === 'load') {
if (savedContexts[cleanArgs[1]]) {
chatMessages = [...savedContexts[cleanArgs[1]].messages];
loadedFiles = [...savedContexts[cleanArgs[1]].files];
console.log(theme.done(` Contexte '${cleanArgs[1]}' chargé.\n`));
} else console.log(theme.error(' Contexte introuvable.\n'));
} else if (cleanArgs[0] === 'list') {
console.log(theme.info(' Contextes :'), Object.keys(savedContexts).join(', ') || theme.dim('aucun'));
} else {
console.log(theme.info(` Fichiers chargés : ${loadedFiles.length || 'aucun'}\n`));
}
break;
default:
console.log(theme.error(` Commande inconnue : ${commandName}. Tape /help.\n`));
break;
}
return true;
}
// ─── BOUCLE PRINCIPALE avec readline + autocomplete ───────────────────────────
async function startReadlineLoop() {
const username = os.userInfo().username || "local-user";
// rl avec autocomplete natif sur '/'
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: slashCompleter,
terminal: true,
historySize: 500,
});
// Afficher le menu autocomplete dès que l'user tape '/'
rl.on('line', () => {}); // nécessaire pour activer le mode interactif
const prompt = () => {
rl.question(theme.user(`❯ ${username} : `), async (text) => {
text = text.trim();
if (!text) { prompt(); return; }
const time = new Date().toTimeString().split(' ')[0];
console.log(theme.border(` └ [${time}]`));
if (text.startsWith('/')) {
await handleSlashCommand(text, rl);
prompt();
return;
}
lastUserInput = text;
commandHistory.push(text);
chatMessages.push({ role: "user", content: text });
await runAgentTurn(rl);
prompt();
});
};
// Historique navigable avec ↑↓ (readline le gère nativement via historySize)
rl.on('close', () => {
console.log(theme.dim('\n Session terminée.\n'));
process.exit(0);
});
prompt();
return rl;
}
// ─── MAIN ─────────────────────────────────────────────────────────────────────
async function main() {
const username = os.userInfo().username || "local-user";
const sessionId = `sess_${Math.random().toString(36).substring(2, 10)}`;
console.log(BANNER);
console.log('');
console.log(theme.agent(' CYPHER CODER CLI — L\'IA experte en developpement local'));
console.log(theme.agent(` Cree par DJAKOUA KWANKAM — Institut Universitaire de Technologie de Douala (IUT)`));
console.log(theme.border(' ─────────────────────────────────────────────────────────────────'));
console.log(theme.info(` Connecte en tant que : `) + theme.agent(`@${username}`));
console.log(theme.info(` Modele actif : `) + theme.text(sessionConfig.model));
console.log(theme.info(` Session : `) + theme.text(sessionId));
console.log(theme.info(` Jeu de donnees : `) + theme.agent(`Collecte active pour entrainement`));
console.log(theme.border(' ─────────────────────────────────────────────────────────────────'));
console.log(theme.command(' Tape /help pour voir les commandes disponibles.\n'));
initChat();
// Support arguments directs
const args = process.argv.slice(2);
if (args.length > 0) {
const initialRequest = args.join(" ");
const time = new Date().toTimeString().split(' ')[0];
console.log(theme.user(`❯ ${username} : `) + initialRequest);
console.log(theme.border(` └ [${time}]`));
if (initialRequest.startsWith('/')) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
await handleSlashCommand(initialRequest, rl);
rl.close();
} else {
lastUserInput = initialRequest;
commandHistory.push(initialRequest);
chatMessages.push({ role: "user", content: initialRequest });
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
await runAgentTurn(rl);
rl.close();
}
}
await startReadlineLoop();
}
main();