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

345 lines • 16.4 kB
/** * GeminiRunner — Exécute des agents IA via @google/gemini-cli npm * * NOTE: "gemini" dans run_agent = runner utilisant @google/gemini-cli npm. * * Le CLI @google/gemini-cli est installé via `npm install -g @google/gemini-cli`. * Flags utilisés (headless): * gemini -p "prompt" --approval-mode yolo --session-id <uuid> --acp [--model <mode>] * * Les 11 modes Antigravity sont passés via --model pour donner du contexte. * Le flag --acp active le mode agent (ACP protocol). * * Sorties: * --output-format text → texte lisible * --output-format json → JSON structuré (utilisé pour extraire session_id) */ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { spawn } from 'child_process'; import { CONFIG, resolveConfigPath } from '../lib/config.js'; import { getLastSessionId, saveSessionId } from '../lib/sessions.js'; import { interpolateEnvVars } from '../lib/envUtils.js'; import { withSpan } from '../lib/telemetry.js'; import { loadEnvQuietly } from '../lib/loadEnv.js'; import { rootLogger } from '../lib/logger.js'; import { registerProcess, linkSessionToPid, appendOutput, updateProcessStatus, killProcessTree, } from '../lib/processRegistry.js'; const logger = rootLogger.child({ module: 'GeminiRunner' }); // ============================================================================ // CHEMINS — @google/gemini-cli npm // ============================================================================ /** CLI gemini (npm bin — @google/gemini-cli v0.43.0) */ const GEMINI_CLI = 'gemini'; // ============================================================================ // GEMINIRUNNER // ============================================================================ export class GeminiRunner { config; timeoutMs; constructor() { this.config = CONFIG.CLAUDE; this.timeoutMs = CONFIG.TIMEOUT_MS || 900000; // 15 min default } /** * Vérifie si @google/gemini-cli est installé */ async isInstalled() { return new Promise((resolve) => { const child = spawn(GEMINI_CLI, ['--version'], { shell: true, windowsHide: true }); let output = ''; child.stdout?.on('data', (d) => (output += d.toString())); child.on('close', (code) => resolve(code === 0 && output.includes('0.43'))); child.on('error', () => resolve(false)); }); } async runAgent(options) { if (options.agentName) { // Inline validation — prevents path traversal on settings_${agentName}.json if (!/^[a-zA-Z0-9_-]+$/.test(options.agentName)) { return { result: '', error: `INVALID_AGENT_NAME: '${options.agentName}' contains invalid characters. Only [a-zA-Z0-9_-] allowed.`, }; } } const cwd = options.cwd || process.cwd(); loadEnvQuietly(path.join(cwd, '.env')); loadEnvQuietly(path.join(cwd, '../Workflow/.env')); const { prompt, agentName, autoResume, mode = 'GENERAL' } = options; let { sessionId } = options; const { PATHS } = this.config; // ======================================================================== // VÉRIFICATION gemini CLI // ======================================================================== const installed = await this.isInstalled(); if (!installed) { return { result: '', error: 'GEMINI_CLI_NOT_INSTALLED: @google/gemini-cli non trouvé.\n' + 'Installez avec: npm install -g @google/gemini-cli\n' + 'Vérifiez avec: gemini --version', }; } // ======================================================================== // ENV + SESSION // ======================================================================== const agentCustomEnv = { ...process.env, ...(agentName ? { OVERMIND_AGENT_NAME: agentName } : {}), }; if (autoResume && agentName && !sessionId) { const lastId = await getLastSessionId(agentName, options.configPath, 'gemini'); if (lastId) sessionId = lastId; } // Session ID: génère si non fourni if (!sessionId) { sessionId = crypto.randomUUID(); } // ======================================================================== // SYSTEM PROMPT LOADING // ======================================================================== let finalPrompt = prompt; if (agentName) { try { const settingsDir = path.dirname(PATHS.SETTINGS); let agentPromptPath = resolveConfigPath(path.join(settingsDir, 'agents', `${agentName}.md`), options.configPath); if (!fs.existsSync(agentPromptPath)) { agentPromptPath = resolveConfigPath(path.join(path.dirname(settingsDir), 'agents', `${agentName}.md`), options.configPath); } if (fs.existsSync(agentPromptPath)) { const systemPrompt = fs.readFileSync(agentPromptPath, 'utf8'); finalPrompt = `${systemPrompt}\n\n[USER QUERY]:\n${prompt}`; } } catch (err) { logger.warn({ agentName, error: err }, 'Failed to load agent prompt, using raw prompt'); } } // ======================================================================== // MCP CONFIG // ======================================================================== const agentConfigDir = path.join(options.configPath || cwd, '.antigravity', agentName ? `agent_${agentName}` : 'default'); if (!fs.existsSync(agentConfigDir)) { fs.mkdirSync(agentConfigDir, { recursive: true }); } const mcpPath = path.join(agentConfigDir, 'mcp.json'); if (agentName) { const settingsDir = path.dirname(PATHS.SETTINGS); const agentSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`), options.configPath); if (fs.existsSync(agentSettingsPath)) { let settings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8')); settings = interpolateEnvVars(settings); if (settings.env) { Object.assign(agentCustomEnv, settings.env); } const originalMcpPath = resolveConfigPath(PATHS.MCP, options.configPath); if (fs.existsSync(originalMcpPath)) { const fullMcp = JSON.parse(fs.readFileSync(originalMcpPath, 'utf8')); let mcpToUse = fullMcp; if (settings.enableAllProjectMcpServers === false && Array.isArray(settings.enabledMcpjsonServers)) { const filteredMcp = { mcpServers: {} }; for (const serverName of settings.enabledMcpjsonServers) { if (fullMcp.mcpServers && fullMcp.mcpServers[serverName]) { filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName]; } } mcpToUse = filteredMcp; } fs.writeFileSync(mcpPath, JSON.stringify(mcpToUse, null, 2)); logger.info({ mcpPath }, 'MCP configuration synchronized for Gemini'); if (!options.silent) { process.stderr.write(`[GeminiRunner] MCP synchronisé: ${mcpPath}\n`); } } } } // ======================================================================== // CONSTRUCTION DES ARGS gemini CLI // ======================================================================== // // Mode Antigravity → --model <mode> (pour donner du contexte au prompt) // ACP mode active le protocol agent (--acp) // --approval-mode yolo = auto-approve tous les outils // --output-format json = output structuré pour parser session_id // ======================================================================== const argsSpawn = []; // Headless prompt (obligatoire pour non-interactive) argsSpawn.push('-p', finalPrompt); // Mode Antigravity → passe au model pour context argsSpawn.push('--model', `antigravity/${mode}`); // ACP mode (agent protocol) argsSpawn.push('--acp'); // Auto-approve tous les outils argsSpawn.push('--approval-mode', 'yolo'); // Session ID persistante argsSpawn.push('--session-id', sessionId); // Output JSON pour parser proprement argsSpawn.push('--output-format', 'json'); // ======================================================================== // SPAWN gemini CLI // ======================================================================== const runImpl = async (span) => { span.setAttribute('agentName', agentName || ''); span.setAttribute('runner', 'gemini'); span.setAttribute('mode', mode); return new Promise((resolve) => { let resolved = false; const safeResolve = (value) => { if (!resolved) { resolved = true; resolve(value); } }; const child = spawn(GEMINI_CLI, argsSpawn, { cwd, shell: false, windowsHide: true, env: agentCustomEnv, }); if (child.pid) { void registerProcess(child.pid, { agentName: agentName || '', runner: 'gemini', configPath: options.configPath, }); } let stdout = ''; let stderr = ''; const MAX_BUF = 10 * 1024 * 1024; child.stdout?.on('data', (data) => { const d = data.toString(); if (child.pid && d) void appendOutput(child.pid, d, options.configPath); stdout += d; if (stdout.length > MAX_BUF) stdout = stdout.slice(-MAX_BUF); }); child.stderr?.on('data', (data) => { const d = data.toString(); if (child.pid && d) void appendOutput(child.pid, d, options.configPath); stderr += d; if (stderr.length > MAX_BUF) stderr = stderr.slice(-MAX_BUF); }); const timeout = setTimeout(async () => { if (child.pid) await killProcessTree(child.pid); else child.kill(); await new Promise((res) => setTimeout(res, 5000)); if (!child.killed) { if (child.pid) await killProcessTree(child.pid); else child.kill('SIGKILL'); } if (child.pid) { void updateProcessStatus(child.pid, 'failed', null, options.configPath); } safeResolve({ result: '', error: 'TIMEOUT', rawOutput: stdout + stderr }); }, this.timeoutMs); child.on('error', (err) => { clearTimeout(timeout); safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' }); }); child.on('close', async (code) => { clearTimeout(timeout); if (code !== 0 && !stdout.trim()) { return safeResolve({ result: '', error: `EXIT_CODE_${code}`, rawOutput: stderr, }); } try { const trimmed = stdout.trim(); let jsonOutput = null; // Parse JSON: d'abord ligne simple, puis extraire du milieu try { jsonOutput = JSON.parse(trimmed); } catch { // Strip les warnings ANSI avant de parser (ex: "Warning: True color...") const lines = trimmed.split('\n'); const jsonLines = []; let inJson = false; for (const line of lines) { const trimmed2 = line.trim(); // Détecte début JSON if (trimmed2 === '{' || trimmed2.startsWith('{')) { inJson = true; } if (inJson || trimmed2 === '}' || trimmed2.endsWith('}')) { inJson = true; jsonLines.push(trimmed2); } } const cleaned = jsonLines.join(''); if (cleaned.startsWith('{')) { try { jsonOutput = JSON.parse(cleaned); } catch { // pas de JSON → output brut } } } if (jsonOutput) { const resultText = jsonOutput.reply || jsonOutput.result || jsonOutput.output || trimmed; const newSessionId = jsonOutput.session_id || sessionId; if (newSessionId && agentName) { await saveSessionId(agentName, newSessionId, options.configPath, 'gemini'); if (child.pid) { void linkSessionToPid(newSessionId, child.pid, options.configPath); } } return safeResolve({ result: resultText, sessionId: newSessionId, rawOutput: stdout, }); } // Fallback: texte brut (le mode --output-format text est utilisé si json échoue) return safeResolve({ result: trimmed, sessionId: sessionId, rawOutput: stdout, }); } catch { return safeResolve({ result: stdout.trim(), sessionId: sessionId, rawOutput: stdout, }); } }); if (child.stdin) { child.stdin.end(); } }); }; const result = await withSpan('gemini.runAgent', runImpl, { agentName: agentName || '', runner: 'gemini', }); // Cleanup fichier MCP temporaire if (fs.existsSync(mcpPath)) { try { fs.unlinkSync(mcpPath); } catch { // ignore cleanup errors } } return result; } } //# sourceMappingURL=GeminiRunner.js.map