UNPKG

overmind-mcp

Version:

Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour

791 lines (787 loc) • 42.7 kB
import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { CONFIG, resolveConfigPath, getWorkspaceDir } from '../lib/config.js'; import { getMemoryProvider } from '../memory/MemoryFactory.js'; import { interpolateEnvVars } from '../lib/envUtils.js'; // ═══════════════════════════════════════════════════════════════════════════════ // ⭐ LAYOUT DES FICHIERS — v3.1 // ═══════════════════════════════════════════════════════════════════════════════ // // HERMES agents → ~/.overmind/hermes/profiles/<name>/ // ├── profile.yaml (kanban — OBLIGATOIRE) // ├── config.yaml (Hermes config) // ├── SOUL.md (system prompt) // ├── .env (credentials) // ├── .mcp.json (override MCP) // ├── state.db (local SQLite) // ├── workspace.yaml (kind: scratch|persistent|shared) // └── README.md (role + owner) // // CLAUDE/KILO → <WORKSPACE>/.claude/agents/<name>.md (legacy support) // // getSharedHermesHome() = OVERMIND_HERMES_HOME || ~/.overmind/hermes/ // ═══════════════════════════════════════════════════════════════════════════════ /** * Validate agent name to prevent path traversal attacks. * Only allows alphanumeric, underscores, hyphens — no path separators or special chars. */ const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/; export function validateAgentName(name) { if (!SAFE_NAME_RE.test(name)) { throw new Error(`Invalid agent name "${name}": only alphanumeric, underscores, and hyphens allowed (no path separators or special chars)`); } } export class AgentManager { claudeDir; constructor(customClaudeDir) { if (customClaudeDir) { this.claudeDir = customClaudeDir; } else { this.claudeDir = path.join(getWorkspaceDir(), '.claude'); } } async getAvailableMcpServers() { try { const mcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP); const content = await fs.readFile(mcpPath, 'utf-8'); const json = JSON.parse(content); return Object.keys(json.mcpServers || {}); } catch (_e) { return []; } } async listAgents(details = false) { const claudeAgentsDir = path.join(this.claudeDir, 'agents'); await fs.mkdir(claudeAgentsDir, { recursive: true }); // 1. Scan .claude/agents/*.md (agents Claude/Kilo/Gemini) const claudeFiles = await fs.readdir(claudeAgentsDir).catch(() => []); const claudeAgentNames = claudeFiles .filter((f) => f.endsWith('.md')) .map((f) => f.replace('.md', '')); // 2. Scan Hermes profiles via `hermes profile list` (v3.0 — native profiles) // Pas de scan de dossier manuel — on utilise la CLI native Hermes. const { HermesProfileManager } = await import('./HermesProfileManager.js'); const hermesProfiles = await HermesProfileManager.list(); const agentsMap = new Map(); const availableServers = await this.getAvailableMcpServers(); // Process Claude agents for (const name of claudeAgentNames) { let runner = 'claude'; let model = 'settings-default'; let provider = 'auto'; let mcpServers = []; let missingConfig = false; const settingsPath = path.join(this.claudeDir, `settings_${name}.json`); try { const settingsContent = await fs.readFile(settingsPath, 'utf-8'); let settings = JSON.parse(settingsContent); settings = interpolateEnvVars(settings); model = settings.env?.ANTHROPIC_MODEL || settings.model || 'settings-default'; runner = settings.runner || 'claude'; mcpServers = settings.enabledMcpjsonServers || []; provider = settings.provider || settings.env?.PROVIDER || 'auto'; if (provider === 'auto') { if (settings.env?.MISTRAL_API_KEY) provider = 'mistral'; else if (settings.env?.OPENAI_API_KEY) provider = 'openai'; else if (settings.env?.NVIDIA_API_KEY || settings.env?.NVAPI_KEY) provider = 'nvidia'; else if (settings.env?.GEMINI_API_KEY) provider = 'google'; else if (model.includes('mistral') || model.includes('codestral') || model.includes('devstral')) provider = 'mistral'; else if (model.includes('gpt-')) provider = 'openai'; else if (model.includes('gemini')) provider = 'google'; else if (model.includes('claude')) provider = 'anthropic'; } } catch { missingConfig = true; } let promptSize = 0; try { const stat = await fs.stat(path.join(claudeAgentsDir, `${name}.md`)); promptSize = stat.size; } catch { // Ignored } agentsMap.set(name, { name, runner, model, provider, mcpServers, origin: 'claude', missingConfig, promptSize, }); } // Process Hermes agents — lit depuis les profils Hermes natifs for (const profile of hermesProfiles.filter((p) => p.name !== 'default')) { const name = profile.name; const model = profile.model || 'unknown'; const provider = model.includes('(') ? model.match(/\(([^)]+)\)/)?.[1] || 'auto' : 'auto'; // Read real data from the profile directory let promptSize = 0; const mcpServers = []; let missingConfig = false; try { const profilePath = await HermesProfileManager.getProfilePath(name); if (profilePath) { // Read SOUL.md size try { const stat = await fs.stat(path.join(profilePath, 'SOUL.md')); promptSize = stat.size; } catch { missingConfig = true; } // Read MCP servers from config.yaml try { const configContent = await fs.readFile(path.join(profilePath, 'config.yaml'), 'utf-8'); // Parse mcp_servers: section from YAML const mcpMatch = configContent.match(/^mcp_servers:\s*\n((?:\s{2}\S.*\n?)+)/m); if (mcpMatch) { const serverLines = mcpMatch[1].split('\n'); for (const line of serverLines) { const srvMatch = line.match(/^\s{2}(\S+):\s*$/); if (srvMatch) mcpServers.push(srvMatch[1]); } } } catch { // config.yaml not readable — not critical } } } catch { // Profile path unresolvable — keep defaults } if (!agentsMap.has(name)) { agentsMap.set(name, { name, runner: 'hermes', model: model.replace(/\s*\(.*\)$/, '').trim() || 'unknown', provider, mcpServers, origin: 'hermes', missingConfig, promptSize, }); } } // Group by runner const runners = [ 'hermes', 'claude', 'kilo', 'gemini', 'qwencli', 'openclaw', 'cline', 'opencode', ]; const grouped = new Map(); for (const r of runners) { grouped.set(r, []); } grouped.set('unknown/other', []); for (const agent of agentsMap.values()) { const r = agent.runner.toLowerCase(); if (grouped.has(r)) { grouped.get(r).push(agent); } else { grouped.get('unknown/other').push(agent); } } const outputLines = []; for (const [runName, list] of grouped.entries()) { if (list.length === 0) continue; let sectionText = `### 🏃 Runner/Harnais : **${runName.toUpperCase()}** (${list.length} agent(s))\n`; const items = []; for (const agent of list) { if (!details) { const warnText = agent.missingConfig ? ' ⚠️ (Config settings manquante)' : ''; items.push(` - **${agent.name}**${warnText}`); } else { let agentDesc = ` - **${agent.name}**`; if (agent.missingConfig) { agentDesc += `\n ⚠️ Config settings manquante (settings_${agent.name}.json)`; } else { agentDesc += `\n - Modèle : ${agent.model}`; agentDesc += `\n - Provider : ${agent.provider}`; const serverStatus = agent.mcpServers.map((s) => { if (availableServers.includes(s)) return s; const sugg = availableServers.find((v) => v.toLowerCase().includes(s.toLowerCase()) || s.toLowerCase().includes(v.toLowerCase())); return sugg ? `${s} (⚠️ Suggestion: ${sugg})` : `${s} (⚠️ Absent)`; }); agentDesc += `\n - MCPs : ${serverStatus.length > 0 ? serverStatus.join(', ') : 'Aucun'}`; } agentDesc += `\n - Prompt : ${agent.promptSize} bytes (${agent.origin === 'hermes' ? 'SOUL.md' : 'agents/' + agent.name + '.md'})`; items.push(agentDesc); } } sectionText += items.join('\n'); outputLines.push(sectionText); } return outputLines; } /** * Lecture non-destructive du runner effectif d'un agent. * - Hermes : `hermes profile list` contient <name> → 'hermes' * - Claude/Kilo : .claude/settings_<name>.json → settings.runner || 'claude' * - Sinon : undefined */ async peekRunner(name) { // Hermes profile check via CLI (v3.0) try { const { HermesProfileManager } = await import('./HermesProfileManager.js'); const profiles = await HermesProfileManager.list(); if (profiles.some((p) => p.name === name)) { return 'hermes'; } } catch { /* fall through to Claude check */ } // Claude/Kilo check via filesystem const claudeSettingsPath = path.join(this.claudeDir, `settings_${name}.json`); try { if (fsSync.existsSync(claudeSettingsPath)) { const raw = fsSync.readFileSync(claudeSettingsPath, 'utf-8'); const parsed = JSON.parse(raw); return (parsed && typeof parsed.runner === 'string' && parsed.runner) || 'claude'; } } catch { /* Ignored */ } return undefined; } async deleteAgent(name) { validateAgentName(name); const deletedFiles = []; const errors = []; // ═══════════════════════════════════════════════════════════════════════ // HERMES PROFILE (v3.0 — Délègue à `hermes profile delete`) // ═══════════════════════════════════════════════════════════════════════ const { HermesProfileManager } = await import('./HermesProfileManager.js'); const hermesProfiles = await HermesProfileManager.list(); const isHermes = hermesProfiles.some((p) => p.name === name); if (isHermes) { try { await HermesProfileManager.delete(name); deletedFiles.push(`hermes profile: ${name}`); } catch (e) { errors.push(`hermes_profile: ${e instanceof Error ? e.message : String(e)}`); } return { deletedFiles, errors }; } // ═══════════════════════════════════════════════════════════════════════ // CLAUDE/KILO/etc. — garde le code existant // ═══════════════════════════════════════════════════════════════════════ const agentsDir = path.join(this.claudeDir, 'agents'); const promptPath = path.join(agentsDir, `${name}.md`); const settingsPath = path.join(this.claudeDir, `settings_${name}.json`); const tmpMcpPath = path.join(this.claudeDir, `mcp_tmp.json`); for (const file of [promptPath, settingsPath, tmpMcpPath]) { try { await fs.unlink(file); deletedFiles.push(file); } catch (e) { if (e && typeof e === 'object' && 'code' in e && e.code !== 'ENOENT') { errors.push(`${path.basename(file)}: ${e instanceof Error ? e.message : String(e)}`); } } } return { deletedFiles, errors }; } async updateAgentConfig(name, updates) { validateAgentName(name); const changes = []; const claudeDir = this.claudeDir; // ═══════════════════════════════════════════════════════════════════════ // HERMES PROFILE (v3.0 — Délègue à HermesProfileManager) // ═══════════════════════════════════════════════════════════════════════ const { HermesProfileManager } = await import('./HermesProfileManager.js'); const profiles = await HermesProfileManager.list(); // Filet de sécurité : runner explicitement 'hermes' force la branche profil // même si `hermes profile list` a échoué (catch silencieux → liste vide). const isHermes = profiles.some((p) => p.name === name) || updates.runner === 'hermes'; const hermesAgentDir = isHermes ? (await HermesProfileManager.getProfilePath(name)) || '' : ''; // --- MODE RÉÉCRITURE DE FICHIER COMPLET --- if (updates.file && updates.content) { if (isHermes && hermesAgentDir) { // Profil Hermes natif : config.yaml, SOUL.md, mcp.json, .env let filePath; switch (updates.file) { case 'prompt.md': filePath = path.join(hermesAgentDir, 'SOUL.md'); await fs.writeFile(filePath, updates.content, 'utf-8'); changes.push(`✅ **SOUL.md** réécrit → ${filePath}`); break; case 'settings.json': // Pour Hermes, settings.json = config.yaml filePath = path.join(hermesAgentDir, 'config.yaml'); await fs.writeFile(filePath, updates.content, 'utf-8'); changes.push(`✅ **config.yaml** réécrit → ${filePath}`); break; case '.mcp.json': filePath = path.join(hermesAgentDir, 'mcp.json'); await fs.writeFile(filePath, updates.content, 'utf-8'); changes.push(`✅ **mcp.json** réécrit → ${filePath}`); break; case 'skill.md': filePath = path.join(hermesAgentDir, 'skills', 'skill.md'); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, updates.content, 'utf-8'); changes.push(`✅ **skill.md** réécrit → ${filePath}`); break; default: throw new Error(`Fichier non supporté: ${updates.file}`); } return changes; } else { let filePath; switch (updates.file) { case 'prompt.md': filePath = path.join(claudeDir, 'agents', `${name}.md`); break; case 'settings.json': filePath = path.join(claudeDir, `settings_${name}.json`); break; case '.mcp.json': filePath = path.join(claudeDir, `.mcp.${name}.json`); break; case 'skill.md': filePath = path.join(claudeDir, 'agents', `${name}_skill.md`); await fs.mkdir(path.dirname(filePath), { recursive: true }); break; default: throw new Error(`Fichier non supporté: ${updates.file}`); } await fs.writeFile(filePath, updates.content, 'utf-8'); changes.push(`✅ Fichier **${updates.file}** réécrit pour l'agent **${name}**.`); if (updates.file === 'settings.json') return changes; } } if (isHermes && hermesAgentDir) { // ═══════════════════════════════════════════════════════════════════════ // HERMES PROFILE — update via HermesProfileManager (v3.0) // Délègue à `hermes config set` + écriture directe .env + SOUL.md // ═══════════════════════════════════════════════════════════════════════ if (updates.model) { const oldModel = profiles.find((p) => p.name === name)?.model || '(none)'; await HermesProfileManager.update(name, { model: updates.model }); changes.push(`- Modèle : ${oldModel} → ${updates.model}`); } if (updates.env) { const envMap = {}; for (const [key, value] of Object.entries(updates.env)) { envMap[key] = value; changes.push(`- Env '${key}' → ${value ? '***' : '(vide)'}`); } await HermesProfileManager.update(name, { credentials: envMap }); } if (updates.prompt) { await HermesProfileManager.update(name, { prompt: updates.prompt }); changes.push(`- SOUL.md mis à jour`); } if (updates.mcpServers) { await HermesProfileManager.update(name, { mcpServers: updates.mcpServers }); changes.push(`- MCPs : [${updates.mcpServers.join(', ')}]`); } if (updates.runner) { // Runner n'est plus stocké dans le profil — c'est Overmind qui route changes.push(`- Runner : ${updates.runner} (routing Overmind)`); } return changes; } // --- MODE MISE À JOUR UNITAIRE (settings.json) pour Claude/Kilo/etc. --- const settingsPath = path.join(claudeDir, `settings_${name}.json`); let settings; try { const content = await fs.readFile(settingsPath, 'utf-8'); settings = JSON.parse(content); } catch (_e) { if (updates.model || updates.mcpServers || updates.env || updates.runner || updates.mode || updates.cliPath) { throw new Error(`Impossible de modifier les paramètres unitaires : settings_${name}.json est introuvable.`, { cause: _e }); } return changes; } if (updates.model) { settings.env = settings.env || {}; const oldModel = settings.env.ANTHROPIC_MODEL; settings.env.ANTHROPIC_MODEL = updates.model; changes.push(`- Modèle : ${oldModel} -> ${updates.model}`); } if (updates.mcpServers) { const oldServers = settings.enabledMcpjsonServers || []; const availableServers = await this.getAvailableMcpServers(); const unknownServers = updates.mcpServers.filter((s) => !availableServers.includes(s)); if (unknownServers.length > 0 && availableServers.length > 0) { changes.push(`⚠️ **ATTENTION:** Serveurs inconnus détectés: ${unknownServers.join(', ')}. Ils ne sont PAS dans mcp.json.\n Serveurs valides: ${availableServers.join(', ')}`); } settings.enabledMcpjsonServers = updates.mcpServers; changes.push(`- Serveurs MCP : [${oldServers.join(', ')}] -> [${updates.mcpServers.join(', ')}]`); try { const globalMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP); const globalMcpContent = await fs.readFile(globalMcpPath, 'utf-8'); const globalMcp = JSON.parse(globalMcpContent); const agentMcpServers = {}; updates.mcpServers.forEach((serverName) => { if (globalMcp.mcpServers && globalMcp.mcpServers[serverName]) { agentMcpServers[serverName] = globalMcp.mcpServers[serverName]; } }); const agentMcpPath = path.join(this.claudeDir, `.mcp.${name}.json`); await fs.writeFile(agentMcpPath, JSON.stringify({ mcpServers: agentMcpServers }, null, 2), 'utf-8'); changes.push(`✅ Fichier .mcp.${name}.json mis à jour avec les nouveaux serveurs.`); } catch (e) { changes.push(`⚠️ Échec de la mise à jour de .mcp.${name}.json: ${e.message}`); } } if (updates.env) { settings.env = settings.env || {}; for (const [key, value] of Object.entries(updates.env)) { const oldVal = settings.env[key] ? '***' : '(undefined)'; settings.env[key] = value; changes.push(`- Env Var '${key}' : ${oldVal} -> ${value ? '***' : '(vide)'}`); } } if (updates.runner) { const old = settings.runner || 'claude'; settings.runner = updates.runner; changes.push(`- Runner : ${old} -> ${updates.runner}`); } if (updates.mode) { const old = settings.mode || '(none)'; settings.mode = updates.mode; changes.push(`- Mode : ${old} -> ${updates.mode}`); } if (updates.cliPath) { const old = settings.cliPath || '(none)'; settings.cliPath = updates.cliPath; changes.push(`- CLI Path : ${old} -> ${updates.cliPath}`); } if (changes.length > 0) { await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); } return changes; } async createAgent(name, prompt, model, copyEnvFrom, projectRoot, runner, mode, cliPath) { if (!/^[a-zA-Z0-9_-]+$/.test(name)) { return { promptPath: '', settingsPath: '', error: 'INVALID_NAME' }; } const memoryInstructions = ` --- ## 🧠 Intelligence et Mémoire Long Terme (Overmind Protocol) Tu es un agent de l'écosystème Overmind, équipé d'une mémoire sémantique persistante et d'une conscience de tes capacités. ### 📜 Protocole d'Initialisation (OBLIGATOIRE) Avant toute action ou modification de code : 1. **Analyse tes Capacités (MCP)** : Vérifie quels serveurs MCP sont actifs dans ta configuration actuelle (\`settings_${name}.json\`). Tu DOIS identifier les outils à ta disposition (Base de données, Scrapers, Discord, etc.) avant de commencer. 2. **Mémoire des Standards** : Appelle \`memory_search(query: "architecture projet standard overmind")\`. Tu DOIS respecter l'usage de pnpm, TypeScript (dist/), ESModules et FastMCP. 3. **Récupération du Contexte** : Appelle \`memory_search(query: "contexte projet ${name}")\` pour retrouver les dernières décisions ou l'état d'avancement. 4. **Auto-Évaluation** : Appelle \`memory_runs(limit: 5)\` pour analyser tes succès et échecs récents. ### 💾 Protocole de Mémoire Proactive Tu es jugé sur ta capacité à transmettre ton savoir. FOUILLIE ta mémoire et ENRICHIS-la constamment : - **Recherche Intensive** : Pour chaque nouveau concept ou bug rencontré, fais systématiquement un \`memory_search\`. Ne présume jamais que tu sais tout. - **Pattern & Décision** : Si tu identifies une règle métier ou si tu prends une décision architecturale, utilise \`memory_store\` avec \`source: "decision"\` ou \`source: "pattern"\`. - **Identité Auto-Gérée** : OverMind détecte automatiquement ton identité (\`${name}\`). Ne spécifie plus le paramètre \`agent_name\` sauf si tu souhaites explicitement consulter ou écrire dans la mémoire d'un AUTRE agent spécifique. - **Transmission Éternelle** : Si tu termines une tâche, résume les points clés via \`memory_store\` pour que ton futur "soi" ne reparte pas de zéro. ### 🌐 Architecture Polyglotte & Multi-Runner Tu es conçu pour être exécuté par différents runners (Claude, Kilo, Gemini, Hermes, etc.). - **Compatibilité** : Ton prompt et tes compétences sont agnostiques au runner. - **Orchestration** : Tu peux être sollicité dans des workflows parallèles (\`run_agents_parallel\`) ou séquentiels. - **Auto-Évaluation** : Adapte tes réponses au format attendu par le runner actuel (JSON pour Claude Code, texte brut pour Kilo, etc.).`; const finalPrompt = prompt + memoryInstructions; // Resolve auth token: prefer ANTHROPIC_AUTH_TOKEN, fallback to any ANTHROPIC_AUTH_TOKEN_<N> // Hermes profiles use their own .env per-profile — skip this requirement for runner=hermes let authToken = process.env.ANTHROPIC_AUTH_TOKEN; if (!authToken) { const suffixedKeys = Object.keys(process.env) .filter((k) => /^ANTHROPIC_AUTH_TOKEN_\d+$/.test(k)) .sort(); if (suffixedKeys.length > 0) { authToken = process.env[suffixedKeys[0]]; console.warn(`[AgentManager] ⚠️ ANTHROPIC_AUTH_TOKEN absent — fallback sur ${suffixedKeys[0]}. ` + `Pour un comportement stable, définissez ANTHROPIC_AUTH_TOKEN dans le .env du service.`); } } if (!authToken && runner !== 'hermes') { const err = "MISSING_AUTH_TOKEN: ANTHROPIC_AUTH_TOKEN (ou ANTHROPIC_AUTH_TOKEN_<N>) absent de l'environnement. " + "Impossible de créer l'agent de manière sécurisée."; console.error(`[AgentManager] ❌ ${err}`); return { promptPath: '', settingsPath: '', error: err, }; } let envVars = { ANTHROPIC_MODEL: model, ANTHROPIC_AUTH_TOKEN: authToken || '', ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com', ANTHROPIC_DEFAULT_HAIKU_MODEL: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL || 'claude-3-5-haiku-20241022', ANTHROPIC_DEFAULT_OPUS_MODEL: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL || 'claude-3-opus-20240229', ANTHROPIC_DEFAULT_SONNET_MODEL: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL || 'claude-3-5-sonnet-20241022', AGENT_TIMEOUT_MS: process.env.AGENT_TIMEOUT_MS || process.env.API_TIMEOUT_MS || '3000000', MISTRAL_API_KEY: process.env.MISTRAL_API_KEY || '', OPENAI_API_KEY: process.env.OPENAI_API_KEY || '', ALIBABA_API_KEY: process.env.ALIBABA_API_KEY || '', SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY || '', MINIMAXI_API_KEY: process.env.MINIMAXI_API_KEY || '', Z_AI_API_KEY: process.env.Z_AI_API_KEY || '', agent: name, }; const availableServers = await this.getAvailableMcpServers(); let mcpServers = availableServers.length > 0 ? availableServers : [ 'postgresql-server', 'news-server', 'discord-server', 'overmind', 'memory', 'news-btc-server', ]; if (copyEnvFrom && projectRoot) { try { const sourceSettingsPath = path.resolve(projectRoot, copyEnvFrom); if (!sourceSettingsPath.startsWith(path.resolve(projectRoot))) { throw new Error(`copyEnvFrom path escapes project root: ${copyEnvFrom}`); } const sourceContent = await fs.readFile(sourceSettingsPath, 'utf-8'); const sourceJson = JSON.parse(sourceContent); if (sourceJson.env) envVars = { ...envVars, ...sourceJson.env }; if (sourceJson.enabledMcpjsonServers) mcpServers = sourceJson.enabledMcpjsonServers; } catch (e) { console.warn(`⚠️ Impossible de copier la config depuis ${copyEnvFrom}: ${e.message}`); } } envVars.ANTHROPIC_MODEL = model; // Ensure model is correctly set if (runner === 'hermes') { // ═══════════════════════════════════════════════════════════════════════ // NATIVE HERMES PROFILE // // Delegates to `hermes profile create` which creates a native profile: // HERMES_HOME/profiles/<name>/ // ├── config.yaml (provider + model + MCP — managed by Hermes) // ├── .env (credentials — managed by Overmind) // ├── SOUL.md (system prompt) // ├── memories/ (state.db — native isolation) // └── sessions/ (conversation history — managed by Hermes) // // getProfilePath() queries `hermes profile show <name>` for the real path. // ═══════════════════════════════════════════════════════════════════════ const { HermesProfileManager } = await import('./HermesProfileManager.js'); // Pull the auth token (if any) so we can optionally add it to the global pool. // Hermes resolves credentials from the pool — we DON'T need to write a // credentials map per profile anymore. const authToken = envVars.ANTHROPIC_AUTH_TOKEN || ''; // The Hermes profile uses the GLOBAL credential pool. // If an auth token was passed, we add it as a single credential to the pool // (keyed by the resolved provider). All other keys come from `hermes auth list`. const credentials = authToken ? { api_key: authToken } : {}; const { profilePath, soulPath } = await HermesProfileManager.create({ name, prompt: finalPrompt, model, credentials, mcpServers, description: `Overmind agent: ${name}`, }); // Fix #11: Also write a .mcp.json file for compatibility with tools that // expect the Claude-style .mcp.json format (get_agent_configs, list_agents). // The authoritative MCP config remains config.yaml's mcp_servers: section. if (mcpServers.length > 0) { try { const { getWorkspaceDir } = await import('../lib/config.js'); const globalMcpPath = path.join(getWorkspaceDir(), '.mcp.json'); const globalMcpContent = await fs.readFile(globalMcpPath, 'utf-8'); const globalMcp = JSON.parse(globalMcpContent); const agentMcpServers = {}; mcpServers.forEach((serverName) => { if (globalMcp.mcpServers?.[serverName]) { agentMcpServers[serverName] = globalMcp.mcpServers[serverName]; } }); const mcpJsonPath = path.join(profilePath, '.mcp.json'); await fs.writeFile(mcpJsonPath, JSON.stringify({ mcpServers: agentMcpServers }, null, 2), 'utf-8'); } catch (e) { console.warn(`[AgentManager] ⚠️ Failed to write .mcp.json for hermes profile: ${e.message}`); } } // Mémoriser la création de l'agent try { const memory = getMemoryProvider(); await memory.storeKnowledge({ text: `Nouvel agent Hermes créé : '${name}'. Profile path : ${profilePath}.`, source: 'agent', agentName: name, }); } catch { /* Ignored */ } return { promptPath: soulPath, settingsPath: path.join(profilePath, 'config.yaml') }; } const agentsDir = path.join(this.claudeDir, 'agents'); await fs.mkdir(this.claudeDir, { recursive: true }); await fs.mkdir(agentsDir, { recursive: true }); const promptPath = path.join(agentsDir, `${name}.md`); await fs.writeFile(promptPath, finalPrompt, 'utf-8'); const settings = { env: envVars, enableAllProjectMcpServers: false, enabledMcpjsonServers: mcpServers, agent: name, runner: runner || 'claude', }; if (mode) settings.mode = mode; if (cliPath) settings.cliPath = cliPath; const settingsFileName = `settings_${name}.json`; const settingsPath = path.join(this.claudeDir, settingsFileName); await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); try { const globalMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP); const globalMcpContent = await fs.readFile(globalMcpPath, 'utf-8'); const globalMcp = JSON.parse(globalMcpContent); const agentMcpServers = {}; mcpServers.forEach((serverName) => { if (globalMcp.mcpServers && globalMcp.mcpServers[serverName]) { agentMcpServers[serverName] = globalMcp.mcpServers[serverName]; } }); const agentMcpPath = path.join(this.claudeDir, `.mcp.${name}.json`); await fs.writeFile(agentMcpPath, JSON.stringify({ mcpServers: agentMcpServers }, null, 2), 'utf-8'); } catch (e) { console.error(`[AgentManager] ⚠️ Failed to create .mcp.${name}.json:`, e.message); } try { const memory = getMemoryProvider(); await memory.storeKnowledge({ text: `Nouvel agent IA créé : '${name}'. Runner : ${runner || 'claude'}.`, source: 'agent', agentName: name, }); } catch (_e) { // Ignored } return { promptPath, settingsPath }; } async getDetailedConfigs(name) { validateAgentName(name); const result = {}; const readFileSafe = async (filePath) => { try { return await fs.readFile(filePath, 'utf-8'); } catch { return 'MISSING'; } }; // ═══════════════════════════════════════════════════════════════════════ // HERMES PROFILE (v3.0 — lit depuis ~/.hermes/profiles/<name>/) // ═══════════════════════════════════════════════════════════════════════ const { HermesProfileManager } = await import('./HermesProfileManager.js'); const profiles = await HermesProfileManager.list(); const isHermes = profiles.some((p) => p.name === name); if (isHermes) { const profilePath = await HermesProfileManager.getProfilePath(name); if (profilePath) { result['settings.json'] = await readFileSafe(path.join(profilePath, 'config.yaml')); result['prompt.md'] = await readFileSafe(path.join(profilePath, 'SOUL.md')); // Hermes stores MCP servers in config.yaml under mcp_servers:, not a separate .mcp.json // Check both locations for compatibility result['.mcp.json'] = await readFileSafe(path.join(profilePath, 'mcp.json')); if (result['.mcp.json'] === 'MISSING') { // Extract mcp_servers section from config.yaml as fallback const configContent = result['settings.json']; if (configContent !== 'MISSING') { const mcpMatch = configContent.match(/^(mcp_servers:\s*\n(?:\s{2,}\S.*\n?)+)/m); result['.mcp.json'] = mcpMatch ? mcpMatch[1] : '(no mcp_servers in config.yaml)'; } } result['.env'] = await readFileSafe(path.join(profilePath, '.env')); } return result; } // ═══════════════════════════════════════════════════════════════════════ // CLAUDE/KILO/etc. — garde le code existant // ═══════════════════════════════════════════════════════════════════════ const agentsDir = path.join(this.claudeDir, 'agents'); const promptPath = path.join(agentsDir, `${name}.md`); const settingsPath = path.join(this.claudeDir, `settings_${name}.json`); const mcpPath = path.join(this.claudeDir, `.mcp.${name}.json`); const skillPath = path.join(this.claudeDir, `agents/${name}_skill.md`); const alternativeSkillPath = path.join(this.claudeDir, `skills/${name}.md`); // Fallback paths for Claude/Kilo agents const workspaceDir = getWorkspaceDir(); const fallbackAgentsDir = path.join(workspaceDir, '.overmind', 'agents'); const fallbackPromptPath = path.join(fallbackAgentsDir, `${name}.md`); const fallbackSettingsDir = path.join(workspaceDir, '.overmind', 'agents', name); const fallbackSettingsPath = path.join(fallbackSettingsDir, 'settings.json'); const fallbackMcpPath = path.join(fallbackSettingsDir, '.mcp.json'); result['prompt.md'] = await readFileSafe(promptPath); if (result['prompt.md'] === 'MISSING') { result['prompt.md'] = await readFileSafe(fallbackPromptPath); } result['settings.json'] = await readFileSafe(settingsPath); if (result['settings.json'] === 'MISSING') { result['settings.json'] = await readFileSafe(fallbackSettingsPath); } result['.mcp.json'] = await readFileSafe(mcpPath); if (result['.mcp.json'] === 'MISSING') { result['.mcp.json'] = await readFileSafe(fallbackMcpPath); } const skillContent = await readFileSafe(skillPath); if (skillContent !== 'MISSING') { result['skill.md'] = skillContent; } else { const altSkillContent = await readFileSafe(alternativeSkillPath); if (altSkillContent !== 'MISSING') { result['skill.md'] = altSkillContent; } } return result; } } //# sourceMappingURL=AgentManager.js.map