import { z } from 'zod';
import Logger from '../utils/logger.js';
// Schéma pour la validation de l'outil code_preview
export const CodePreviewSchema = z.object({
channelId: z.string().describe('ID du canal où afficher le code'),
code: z.string().describe('Code à afficher avec coloration syntaxique'),
language: z.string().describe('Langage de programmation (js, ts, py, bash, etc.)'),
});
// Langages supportés avec leurs balises markdown
export const SUPPORTED_LANGUAGES: { [key: string]: string } = {
javascript: 'js',
js: 'js',
typescript: 'ts',
ts: 'ts',
python: 'py',
py: 'py',
diff: 'diff',
markdown: 'md',
md: 'md',
json: 'json',
yaml: 'yaml',
bash: 'bash',
shell: 'bash',
sh: 'bash',
css: 'css',
html: 'html',
xml: 'xml',
sql: 'sql',
java: 'java',
c: 'c',
cpp: 'cpp',
csharp: 'cs',
cs: 'cs',
php: 'php',
ruby: 'rb',
go: 'go',
rust: 'rs',
kotlin: 'kt',
swift: 'swift',
r: 'r',
scala: 'scala',
perl: 'pl',
lua: 'lua',
vim: 'vim',
dockerfile: 'dockerfile',
makefile: 'makefile',
ini: 'ini',
toml: 'toml',
properties: 'properties',
};
// Fonction pour envelopper automatiquement le code dans des blocs markdown
const formatCodeBlocks = (content: string): string => {
const lines = content.split('\n');
const formattedLines: string[] = [];
let inCodeBlock = false;
let codeBuffer: string[] = [];
let currentLang = 'bash';
const flushCodeBuffer = (lang: string) => {
if (codeBuffer.length > 0) {
const BACKTICK = '\x60';
formattedLines.push(BACKTICK + BACKTICK + BACKTICK + lang);
formattedLines.push(...codeBuffer);
formattedLines.push(BACKTICK + BACKTICK + BACKTICK);
formattedLines.push(''); // Ligne vide après le bloc
codeBuffer = [];
}
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Détecter une ligne de commande bash
// - Commandes shell : chmod, echo, cd, npm, node, bash, sh, etc.
// - Appels de fonctions : create_channel(...), edit_message(...)
// - Variables : SESSION_ID=, $(), etc.
const isCommandLine = /^(\s*)(chmod|echo|cd|npm|node|pnpm|yarn|bash|sh|\$\s*\(|SESSION[_A-Z]*|create_|edit_|delete_|get_|send_|add_|move_|vote_|appuyer_|selectionner_)/.test(line) ||
/\w+\([^)]*\)/.test(line) || // Détecte les appels de fonction avec ()
/^(\s*)([A-Z_]{2,})(\s*[:=])/.test(line); // Détecte les constantes comme "BASH:"
// Détecter une ligne de code JS/TS (import, const, let, class, async, etc.)
const isJSLine = /^(\s*)(import|export|const|let|var|function|class|async|await|interface|type)/.test(line);
// Si on n'est pas dans un bloc et qu'on trouve une commande ou du code
if (!inCodeBlock && (isCommandLine || isJSLine)) {
// Terminer le markdown précédent si nécessaire
if (formattedLines.length > 0 && formattedLines[formattedLines.length - 1] !== '') {
formattedLines.push('');
}
// Commencer un bloc de code
inCodeBlock = true;
currentLang = isJSLine ? 'javascript' : 'bash';
codeBuffer = [line];
}
// Si on est dans un bloc de code et qu'on trouve une ligne qui n'est pas du code
else if (inCodeBlock && !isCommandLine && !isJSLine && line.trim() !== '' && !/^#{1,6}\s+/.test(line)) {
// Fin du bloc de code (mais continuer si c'est un header markdown)
flushCodeBuffer(currentLang);
inCodeBlock = false;
// Ajouter la ligne actuelle au markdown
formattedLines.push(line);
}
// Si on est dans un bloc de code, ajouter à la buffer
else if (inCodeBlock) {
codeBuffer.push(line);
}
// Sinon, ajouter au markdown normal
else {
formattedLines.push(line);
}
}
// Flush le dernier bloc de code s'il existe
if (inCodeBlock) {
flushCodeBuffer(currentLang);
}
return formattedLines.join('\n');
};
// Créer un ou plusieurs messages avec code (division automatique si trop long)
export const createCodePreviewMessages = (code: string, language: string): string[] => {
// Normaliser le langage
const normalizedLang = language.toLowerCase();
const langTag = SUPPORTED_LANGUAGES[normalizedLang] || normalizedLang;
const lineCount = code.split('\n').length;
const displayLang = language.toUpperCase();
// Le code est toujours utilisé tel quel
const formattedContent = code;
// En-tête pour le markdown
const baseHeader = `📝 **Code Preview** [v2.1-HEX]
Langage: ${displayLang}
Lignes: ${lineCount}
`;
const BACKTICK = '\x60';
const separator = `
---
`;
// Calculer la longueur disponible (max 2000 - marge de sécurité)
const maxTotalLength = 1950;
// TOUJOURS envelopper dans un bloc de code markdown, quel que soit le langage
// BACKTICK est déjà déclaré plus haut
const codeBlockStart = BACKTICK + BACKTICK + BACKTICK + langTag + '\n';
const codeBlockEnd = '\n' + BACKTICK + BACKTICK + BACKTICK;
const totalCodeLength = codeBlockStart.length + formattedContent.length + codeBlockEnd.length;
const totalWithHeader = baseHeader.length + totalCodeLength;
// DEBUG: Afficher les informations de calcul
console.log('[CODE_PREVIEW] DEBUG - Longueur du code:', code.length);
console.log('[CODE_PREVIEW] DEBUG - Longueur après formatage:', formattedContent.length);
console.log('[CODE_PREVIEW] DEBUG - maxTotalLength:', maxTotalLength);
console.log('[CODE_PREVIEW] DEBUG - baseHeader.length:', baseHeader.length);
console.log('[CODE_PREVIEW] DEBUG - codeBlockStart:', JSON.stringify(codeBlockStart));
console.log('[CODE_PREVIEW] DEBUG - codeBlockEnd:', JSON.stringify(codeBlockEnd));
console.log('[CODE_PREVIEW] DEBUG - totalWithHeader:', totalWithHeader);
console.log('[CODE_PREVIEW] DEBUG - totalWithHeader <= maxTotalLength?', totalWithHeader <= maxTotalLength);
// DEBUG: Afficher le message complet qui sera envoyé
const fullMessage = `${baseHeader}${codeBlockStart}${formattedContent}${codeBlockEnd}`;
console.log('[CODE_PREVIEW] DEBUG - Message complet:', JSON.stringify(fullMessage));
// Si le contenu tient dans un seul message
if (totalWithHeader <= maxTotalLength) {
return [`${baseHeader}${codeBlockStart}${formattedContent}${codeBlockEnd}`];
}
// Diviser le code en plusieurs parties (par lignes complètes)
const messages: string[] = [];
const lines = formattedContent.split('\n');
const totalLines = lines.length;
let currentLineIndex = 0;
let partNumber = 1;
while (currentLineIndex < totalLines) {
// Construire l'en-tête avec le numéro de partie
const partHeader = partNumber === 1
? `📝 **Code Preview**
Langage: ${displayLang}
Lignes: ${lineCount}
`
: `📝 **Code Preview** (Suite ${partNumber})
Langage: ${displayLang}
Lignes: ${lineCount}
`;
// Calculer la longueur disponible pour cette partie (en comptant les balises de code)
// TOUJOURS utiliser des blocs de code markdown, quel que soit le langage
// Utilisation des codes hex pour éviter les problèmes d'encodage
const BACKTICK = '\x60'; // Code hex U+0060 pour le backtick
const partCodeBlockStart = BACKTICK + BACKTICK + BACKTICK + langTag + '\n';
const partCodeBlockEnd = '\n' + BACKTICK + BACKTICK + BACKTICK;
const availableLength = maxTotalLength - partHeader.length - partCodeBlockStart.length - partCodeBlockEnd.length;
// Construire un chunk de lignes qui respecte la limite de longueur
const chunkLines: string[] = [];
let chunkLength = 0;
while (currentLineIndex < totalLines) {
const line = lines[currentLineIndex];
// +1 pour le caractère '\n' qui sera ajouté entre les lignes
const lineLength = chunkLines.length > 0 ? line.length + 1 : line.length;
if (chunkLength + lineLength > availableLength) {
// Cette ligne ne rentrera pas, on s'arrête
break;
}
chunkLines.push(line);
chunkLength += lineLength;
currentLineIndex++;
}
// Si aucune ligne n'a été ajoutée (première ligne trop longue), on force au moins une ligne
if (chunkLines.length === 0 && currentLineIndex < totalLines) {
chunkLines.push(lines[currentLineIndex]);
currentLineIndex++;
Logger.warn('[CODE_PREVIEW] Une ligne dépasse la limite, elle sera coupée');
}
// Joindre les lignes du chunk
const codeChunk = chunkLines.join('\n');
// Construire le message de la partie avec bloc de code markdown (sauf pour markdown)
let partMessage = `${partHeader}${partCodeBlockStart}${codeChunk}${partCodeBlockEnd}`;
// Ajouter un séparateur si ce n'est pas la dernière partie
if (currentLineIndex < totalLines) {
partMessage += separator;
partMessage += `*[Suite dans le message suivant...]*`;
}
messages.push(partMessage);
partNumber++;
// Protection contre les boucles infinies
if (chunkLines.length === 0) {
Logger.error('[CODE_PREVIEW] Erreur: aucune ligne ajoutée, arrêt de la division');
break;
}
}
Logger.info(
`[CODE_PREVIEW] Division: ${messages.length} message(s) créé(s) pour ${code.length} caractères`
);
return messages;
};
// Fonction de compatibilité (garder l'ancienne pour les appels existants)
export const createCodePreviewMessage = createCodePreviewMessages;
// Valider le langage
export const validateLanguage = (language: string): boolean => {
const normalizedLang = language.toLowerCase();
return (
normalizedLang in SUPPORTED_LANGUAGES ||
Object.values(SUPPORTED_LANGUAGES).includes(normalizedLang)
);
};
// ============================================================================
// ENREGISTREMENT DE L'OUTIL MCP
// ============================================================================
import type { FastMCP } from 'fastmcp';
import { ensureDiscordConnection } from './common.js';
export function registerCodePreviewTools(server: FastMCP) {
server.addTool({
name: 'code_preview',
description: 'Affiche du code avec coloration syntaxique et division automatique si trop long',
parameters: z.object({
channelId: z.string().describe('ID du canal où afficher le code'),
code: z.string().describe('Code à afficher avec coloration syntaxique'),
language: z.string().describe('Langage de programmation (js, ts, py, bash, etc.)'),
}),
execute: async (args) => {
try {
console.error(
`🔍 [code_preview] Langage: ${args.language}, Taille: ${args.code.length} chars`
);
const client = await ensureDiscordConnection();
const channel = await client.channels.fetch(args.channelId);
if (!channel || !('send' in channel)) {
throw new Error('Canal invalide ou inaccessible');
}
// Valider le langage
if (!validateLanguage(args.language)) {
return `❌ Langage non supporté: ${args.language}`;
}
// Créer les messages avec division automatique
const messages = createCodePreviewMessages(args.code, args.language);
console.error(`📤 [code_preview] ${messages.length} message(s) à envoyer`);
// Envoyer tous les messages
const sentMessages = [];
for (const messageContent of messages) {
const message = await channel.send(messageContent);
sentMessages.push(message.id);
}
return `✅ Code affiché | ${messages.length} message(s) | IDs: ${sentMessages.join(', ')}`;
} catch (error: any) {
console.error(`❌ [code_preview]`, error.message);
return `❌ Erreur: ${error.message}`;
}
},
});
Logger.info('✅ Outils code_preview enregistrés');
}