UNPKG

agile-planner-mcp-server

Version:

Serveur MCP pour la génération d'artefacts agiles (backlogs, features, user stories) avec IA - compatible Windsurf, Claude et Cursor

314 lines (273 loc) 12.3 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const { createSlug } = require('./utils'); const { parseJsonResponse } = require('./utils/json-parser'); const { generateFeatureMarkdown } = require('./markdown-generator'); /** * Génère une feature avec des user stories en utilisant l'API OpenAI ou GROQ * * @param {Object} params - Les paramètres pour la génération de feature * @param {string} params.featureDescription - La description de la feature à générer * @param {number} params.storyCount - Le nombre de user stories à générer * @param {string} params.businessValue - La valeur métier de la feature (optionnel) * @param {string} params.epicName - Le nom de l'epic parent (optionnel) * @param {Object} client - Le client API (OpenAI ou GROQ) * @param {string} provider - Le fournisseur d'API ('openai' ou 'groq') * @returns {Promise<Object>} - La feature générée */ async function generateFeature(params, client, provider = 'openai') { try { console.error(chalk.blue(`Génération d'une feature à partir de la description: ${params.featureDescription}`)); const { featureDescription, storyCount = 3, businessValue, epicName = 'Fonctionnalités principales' } = params; const systemPrompt = ` Tu es un expert en analyse fonctionnelle et en méthodologie agile. Je te demande de générer une feature complète accompagnée de user stories pour un projet informatique. RÈGLES IMPORTANTES: - Génère exactement ${storyCount} user stories, ni plus ni moins - Utilise le format "En tant que... Je veux... Afin de..." - Chaque critère d'acceptation doit suivre le format "Critère: Étant donné [contexte], quand [action], alors [résultat]" - Chaque tâche technique doit être concrète et implémentable (éviter les généralités) - Les estimations doivent être réalistes (1 = très simple, 8 = complexe) - AUCUN texte avant ou après l'objet JSON CONTEXTE: - Feature à créer: ${featureDescription} - Epic parent: ${epicName} ${businessValue ? `- Valeur métier: ${businessValue}` : ''} FORMAT DE RÉPONSE (JSON uniquement): { "feature": { "title": "Titre de la feature", // Titre concis représentant la fonctionnalité "description": "Description détaillée et concrète de la fonctionnalité", "businessValue": "Valeur métier et impact pour les utilisateurs" }, "epicName": "${epicName}", // Utiliser exactement cette valeur "userStories": [ { "title": "Titre concis et explicite", // 5-10 mots maximum "asA": "En tant que [rôle précis]", // Rôle spécifique, pas générique "iWant": "Je veux [action spécifique et concrète]", // Action claire et actionnable "soThat": "Afin de [bénéfice tangible et mesurable]", // Bénéfice réel pour l'utilisateur "acceptanceCriteria": [ { "given": "Étant donné que [contexte précis]", // Contexte initial "when": "Quand [action de l'utilisateur]", // Action déclenchante "then": "Alors [résultat vérifiable]", // Résultat attendu et vérifiable "andThen": "Et [condition supplémentaire optionnelle]" // Optionnel }, { "given": "Étant donné que [contexte alternatif]", "when": "Quand [autre action]", "then": "Alors [autre résultat attendu]" } ], "tasks": [ { "description": "Tâche technique spécifique 1", // Tâche technique implémentable "estimate": "2" // Utiliser uniquement les valeurs 1, 2, 3, 5 ou 8 }, { "description": "Tâche technique spécifique 2", "estimate": "3" }, { "description": "Tâche technique spécifique 3", "estimate": "1" } ] } // Répéter ce modèle pour chaque user story demandée ] } `; const userPrompt = ` Génère une feature complète avec ${storyCount} user stories pour: "${featureDescription}" ${businessValue ? `La valeur métier principale est: "${businessValue}"` : ''} L'epic parent est: "${epicName}" INSTRUCTIONS SUPPLÉMENTAIRES : 1. Détaille clairement la feature avec un titre explicite 2. Crée ${storyCount} user stories complètes et distinctes 3. Pour chaque user story : - Précise le rôle de l'utilisateur (qui) - Décris l'action concrète (quoi) - Explique le bénéfice tangible (pourquoi) - Fournis au moins 2 critères d'acceptation précis - Décompose en 2-4 tâches techniques 4. Estime chaque tâche technique (1=simple, 8=complexe) Réponds uniquement avec un objet JSON conforme au format demandé, sans texte avant ou après. `; const model = provider === 'groq' ? 'llama3-70b-8192' : 'gpt-4-turbo'; const options = { model: model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], temperature: 0.7, max_tokens: 3000 }; const response = await client.chat.completions.create(options); const content = response.choices[0].message.content; console.error(chalk.blue(`🔍 Tentative de parsing de la réponse API pour la feature...`)); try { // Utiliser notre parser JSON robuste plutôt que JSON.parse simple const result = parseJsonResponse(content, true); console.error(chalk.green(`✅ JSON parsé avec succès`)); // Vérifie la présence des champs obligatoires avec chaînage optionnel if (!result?.feature?.title || !result?.feature?.description || !result?.userStories || result.userStories?.length !== storyCount) { throw new Error("La réponse de l'API ne respecte pas le format attendu"); } // Assure que epicName est défini if (!result.epicName) { result.epicName = epicName; } console.error(chalk.green(`Feature générée avec succès: ${result.feature.title}`)); console.error(chalk.green(`${storyCount} user stories créées`)); return result; } catch (error) { console.error(chalk.red('Erreur lors du parsing de la réponse JSON:'), error); console.error(chalk.yellow('Réponse reçue:'), content); throw new Error(`Erreur de format dans la réponse de l'API: ${error.message}`); } } catch (error) { console.error(chalk.red('Erreur lors de la génération de la feature:'), error); throw error; } } /** * Sauvegarde le résultat brut d'une génération de feature dans un fichier JSON * et le combine avec un backlog existant s'il existe * * @param {Object} result - Le résultat de la génération de feature * @param {string} outputDir - Le répertoire de sortie * @returns {Promise<string>} - Le chemin du fichier JSON sauvegardé */ async function saveRawFeatureResult(result, outputDir) { try { console.error(chalk.blue('Sauvegarde du résultat de la feature...')); // Prépare le répertoire de sortie await fs.ensureDir(outputDir); // Chemin du fichier JSON const jsonPath = path.join(outputDir, '.agile-planner-backlog', 'backlog.json'); // Crée le dossier .agile-planner-backlog s'il n'existe pas await fs.ensureDir(path.dirname(jsonPath)); // Structure initiale du backlog vide let backlog = { epics: [] }; // Vérifie si un backlog existe déjà if (await fs.pathExists(jsonPath)) { const existingContent = await fs.readFile(jsonPath, 'utf8'); backlog = JSON.parse(existingContent); } // Extraction des données de result const { feature, userStories, epicName } = result; // Vérifie si l'epic existe déjà let epic = backlog.epics.find(e => e.name === epicName); // Si l'epic n'existe pas, le crée if (!epic) { epic = { name: epicName, description: `Epic pour ${epicName}`, slug: createSlug(epicName), features: [] }; backlog.epics.push(epic); } // Crée un slug pour la feature const featureSlug = createSlug(feature.title); // Prépare la feature à ajouter const featureToAdd = { title: feature.title, description: feature.description, businessValue: feature.businessValue, slug: featureSlug, userStories: userStories.map((story, index) => { // Génère un ID pour chaque user story const storyId = `US${Date.now().toString().slice(-4)}${index + 1}`; return { id: storyId, title: story.title, description: `${story.asA} ${story.iWant} ${story.soThat}`, acceptance_criteria: story.acceptanceCriteria.map(ac => `${ac.given} ${ac.when} ${ac.then}` ), tasks: story.tasks.map(task => task.description), slug: createSlug(story.title), status: 'to-do', priority: 'medium', estimate: story.tasks.reduce((sum, task) => sum + parseInt(task.estimate || '0'), 0) }; }) }; // Ajoute la feature à l'epic // Toujours garantir la cohérence : stories = userStories if (!Array.isArray(featureToAdd.userStories)) { featureToAdd.userStories = []; } // Pour compatibilité markdown : ajouter aussi stories featureToAdd.stories = featureToAdd.userStories; if (featureToAdd.userStories.length === 0) { console.error(chalk.yellow('⚠️ Feature sans user stories : un dossier user-stories vide sera généré.')); } epic.features.push(featureToAdd); // Écrit le backlog dans le fichier JSON await fs.writeFile(jsonPath, JSON.stringify(backlog, null, 2), 'utf8'); console.error(chalk.green(`Feature sauvegardée dans: ${jsonPath}`)); return jsonPath; } catch (error) { console.error(chalk.red('Erreur lors de la sauvegarde du résultat:'), error); throw error; } } /** * Processus complet de génération d'une feature: * 1. Génère la feature avec l'API * 2. Sauvegarde le résultat brut * 3. Génère les fichiers Markdown * * @param {Object} params - Les paramètres pour la génération * @param {string} outputDir - Le répertoire de sortie * @param {Object} client - Le client API (OpenAI ou GROQ) * @param {string} provider - Le fournisseur d'API ('openai' ou 'groq') * @returns {Promise<Object>} - Le résultat de l'opération */ async function generateFeatureAndMarkdown(params, outputDir, client, provider = 'openai') { try { console.error(chalk.blue('Début du processus de génération de feature...')); // 1. Génère la feature const featureResult = await generateFeature(params, client, provider); // 2. Sauvegarde le résultat brut const jsonPath = await saveRawFeatureResult(featureResult, outputDir); // 3. Génère les fichiers Markdown await generateFeatureMarkdown(featureResult, outputDir); console.error(chalk.green('Processus de génération de feature terminé avec succès!')); return { success: true, result: { feature: featureResult.feature, userStories: featureResult.userStories, epicName: featureResult.epicName, files: { json: jsonPath } } }; } catch (error) { console.error(chalk.red('Erreur lors du processus de génération:'), error); return { success: false, error: { message: error.message, stack: error.stack } }; } } module.exports = { generateFeature, saveRawFeatureResult, generateFeatureAndMarkdown };