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

521 lines (506 loc) • 23.3 kB
#!/usr/bin/env node /** * ╔══════════════════════════════════════════════════════════════════════╗ * ║ OVERMIND BRIDGE — CLI (overmind-bridge) ║ * ║ ║ * ║ Point d'entrée unifié pour le bridge Overmind. ║ * ║ Supporte 8 subcommands : server, call, scenario, replay, status, ║ * ║ health, send, agent. ║ * ║ ║ * ║ USAGE EXAMPLES ║ * ║ ────────────── ║ * ║ # Start server ║ * ║ overmind-bridge server --port 3100 ║ * ║ ║ * ║ # One-shot agent call (8 prompt sources) ║ * ║ overmind-bridge call agent.run --agent scout --runner kilo \ ║ * ║ --prompt "Analyse BTC" ║ * ║ echo "Analyse BTC" | overmind-bridge call agent.run \ ║ * ║ --agent scout --runner kilo --prompt-stdin ║ * ║ overmind-bridge call agent.run --agent scout --runner kilo \ ║ * ║ --prompt-file ./brief.txt --var ticker=BTC ║ * ║ ║ * ║ # A2A — agent A parle à agent B ║ * ║ overmind-bridge call agent.a2a \ ║ * ║ --from scout --to analyst --runner kilo \ ║ * ║ --prompt "Valide mon analyse" ║ * ║ ║ * ║ # Multi-agent scenario from JSON ║ * ║ overmind-bridge scenario ./workflow.json --var ticker=BTC ║ * ║ ║ * ║ # Replay a failed message ║ * ║ overmind-bridge replay --id 7f3e8a1b-... ║ * ║ ║ * ║ # Status of all agents ║ * ║ overmind-bridge status ║ * ║ ║ * ║ # Server health ║ * ║ overmind-bridge health ║ * ╚══════════════════════════════════════════════════════════════════════╝ */ import { parseArgs, requireFlag, getFlag } from '../bridge/ArgParser.js'; import { resolvePrompt, parseVars } from '../bridge/PromptSource.js'; import { loadScenario, runScenario } from '../bridge/ScenarioLoader.js'; import { BridgeHttpClient } from '../bridge/BridgeHttpClient.js'; import { OverBridgeService, OverBridgeServer, loadMessageLogConfigFromEnv, createBridgeLogger, } from '../bridge/index.js'; const log = createBridgeLogger('overmind-bridge'); const clog = (msg) => log.info(msg); // ─── Entry Point ─────────────────────────────────────────────────────────── async function main() { const args = parseArgs(); const command = args.command ?? 'help'; try { switch (command) { case 'server': await cmdServer(args); break; case 'call': await cmdCall(args); break; case 'scenario': await cmdScenario(args); break; case 'replay': await cmdReplay(args); break; case 'status': await cmdStatus(args); break; case 'health': await cmdHealth(args); break; case 'agents': case 'agent': await cmdAgents(args); break; case 'history': await cmdHistory(args); break; case 'sessions': case 'session': await cmdSessions(args); break; case 'webhook': await cmdWebhook(args); break; case 'send': await cmdSend(args); break; case 'version': case '--version': case '-v': console.log('overmind-bridge 1.0.0'); break; case 'help': case '--help': case '-h': case undefined: printHelp(); break; default: console.error(`❌ Unknown command: ${command}`); console.error(`Run 'overmind-bridge help' for usage.`); process.exit(1); } } catch (err) { console.error(`💥 ${command} failed: ${err.message}`); if (process.env.DEBUG) { console.error(err.stack); } process.exit(1); } } // ─── Subcommand: server ──────────────────────────────────────────────────── async function cmdServer(args) { const port = Number(getFlag(args, 'port', 3100)); const host = String(getFlag(args, 'host', '127.0.0.1')); const authToken = getFlag(args, 'auth-token') ?? process.env.BRIDGE_AUTH_TOKEN; const enableLog = getFlag(args, 'no-log', undefined) === undefined; // --no-log désactive const healthInterval = Number(getFlag(args, 'health-interval', 30_000)); const mcpUrl = String(getFlag(args, 'mcp-url', process.env.MCP_URL ?? 'http://[::1]:3099/mcp')); const enableSessionStore = getFlag(args, 'no-session-store', undefined) === undefined; const enableDirectives = getFlag(args, 'no-directives', undefined) === undefined; const enableWebhooks = getFlag(args, 'webhooks', false); const enableSanitize = getFlag(args, 'sanitize-json', true); const sessionTtlMs = Number(getFlag(args, 'session-ttl', 4 * 60 * 60 * 1000)); clog(`🚀 Starting OverBridgeServer on ${host}:${port}`); clog(` MCP upstream: ${mcpUrl}`); clog(` MessageLog: ${enableLog ? 'enabled' : 'disabled'}`); clog(` SessionStore: ${enableSessionStore ? 'enabled' : 'disabled'}`); clog(` Directives: ${enableDirectives ? 'enabled' : 'disabled'}`); clog(` Webhooks: ${enableWebhooks ? 'enabled' : 'disabled'}`); clog(` Sanitize JSON: ${enableSanitize ? 'enabled' : 'disabled'}`); if (authToken) clog(` Auth: enabled`); const service = new OverBridgeService({ mcpUrl }); const server = new OverBridgeServer(service, { port, host, postgres: loadMessageLogConfigFromEnv(), enableMessageLog: enableLog, authToken, healthCheckIntervalMs: healthInterval, enableSessionStore, enableDirectives, enableWebhooks, sanitizeJson: enableSanitize, sessionTtlMs, }, log); const { url } = await server.start(); console.log(`\n OverBridgeServer ready at ${url}`); console.log(` POST ${url}/rpc (JSON-RPC 2.0)`); console.log(` GET ${url}/health`); if (enableWebhooks) { console.log(` POST ${url}/webhook/:provider (voipms, twilio, discord, generic)`); } console.log(''); // Graceful shutdown const shutdown = async (signal) => { clog(`\n🛑 Received ${signal}, shutting down...`); await server.stop(); process.exit(0); }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); // Keep alive await new Promise(() => { }); } // ─── Subcommand: call (one-shot JSON-RPC) ────────────────────────────────── async function cmdCall(args) { // 2 formats acceptés: // call <method> [--param1 v1 --param2 v2 ...] // call --method <method> [--param1 v1 ...] const method = (args.positionals[0] ?? getFlag(args, 'method')); if (!method) { throw new Error('call requires a method (e.g. "call agent.run --agent scout ..." or "call --method agent.run ...")'); } const client = getClient(args); // Prompt resolution (8 sources) const prompt = await resolvePrompt({ prompt: getFlag(args, 'prompt'), promptFile: getFlag(args, 'prompt-file'), promptStdin: getFlag(args, 'prompt-stdin', false), promptBase64: getFlag(args, 'prompt-base64'), promptFileBase64: getFlag(args, 'prompt-file-base64'), promptUrl: getFlag(args, 'prompt-url'), promptJson: getFlag(args, 'prompt-json'), promptTemplate: getFlag(args, 'prompt-template'), vars: parseVars(getFlag(args, 'var')), }); // Construit les params dynamiquement depuis tous les flags non-préfixés const params = {}; const reservedFlags = new Set([ 'method', 'server', 'port', 'host', 'auth-token', 'no-log', 'health-interval', 'mcp-url', 'prompt', 'prompt-file', 'prompt-stdin', 'prompt-base64', 'prompt-file-base64', 'prompt-url', 'prompt-json', 'prompt-template', 'var', 'id', 'output', 'json', 'pretty', ]); for (const [k, v] of Object.entries(args.flags)) { if (!reservedFlags.has(k)) { params[k] = v; } } // Si prompt résolu, l'injecte (sauf si déjà fourni via params) if (prompt.text && !params.prompt) { params.prompt = prompt.text; } // Métadonnées (vars, prompt source) if (Object.keys(params).length > 0 || prompt.text) { const metadata = { promptSource: prompt.source, promptBytes: prompt.meta.bytes, }; if (prompt.meta.origin) metadata.promptOrigin = prompt.meta.origin; params.metadata = { ...params.metadata, ...metadata }; } clog(`📞 ${method} → ${client.baseUrl}`); const result = await client.call(method, params, Number(getFlag(args, 'timeout', 600_000))); printResult(result, getFlag(args, 'pretty', false)); } // ─── Subcommand: send (alias pour call agent.run) ────────────────────────── async function cmdSend(args) { // send est un raccourci : call agent.run avec les bons flags const agentName = requireFlag(args, 'agent'); const runner = requireFlag(args, 'runner'); const prompt = await resolvePrompt({ prompt: getFlag(args, 'prompt'), promptFile: getFlag(args, 'prompt-file'), promptStdin: getFlag(args, 'prompt-stdin', true), promptBase64: getFlag(args, 'prompt-base64'), promptFileBase64: getFlag(args, 'prompt-file-base64'), promptUrl: getFlag(args, 'prompt-url'), promptJson: getFlag(args, 'prompt-json'), promptTemplate: getFlag(args, 'prompt-template'), vars: parseVars(getFlag(args, 'var')), }); const client = getClient(args); const result = await client.call('agent.run', { agentName, runner, prompt: prompt.text, sessionId: getFlag(args, 'session'), model: getFlag(args, 'model'), mode: getFlag(args, 'mode'), path: getFlag(args, 'path'), silent: getFlag(args, 'silent', false), }, Number(getFlag(args, 'timeout', 600_000))); printResult(result, getFlag(args, 'pretty', false)); } // ─── Subcommand: scenario ────────────────────────────────────────────────── async function cmdScenario(args) { const file = getFlag(args, 'file') ?? args.positionals[0]; if (!file) throw new Error('scenario requires --file <path.json> or positional <path.json>'); const client = getClient(args); const scenario = await loadScenario(file); const inputVars = parseVars(getFlag(args, 'var')); clog(`📜 Scenario: ${scenario.name} (${scenario.steps.length} steps)`); const results = await runScenario(scenario, { vars: inputVars, log: (msg) => clog(msg), runAgent: async (params) => { const r = await client.call('agent.run', params); return { text: r.content.map((c) => c.text).join('\n'), sessionId: r.sessionId, isError: r.isError, messageId: r.messageId, }; }, runA2A: async (params) => { const r = await client.call('agent.a2a', params); return { text: r.content.map((c) => c.text).join('\n'), sessionId: r.sessionId, isError: r.isError, messageId: r.messageId, }; }, }); printResult({ scenario: scenario.name, results }, getFlag(args, 'pretty', false)); const allOk = results.every((r) => r.success); if (!allOk) process.exit(2); } // ─── Subcommand: replay ──────────────────────────────────────────────────── async function cmdReplay(args) { const id = requireFlag(args, 'id'); const client = getClient(args); clog(`🔁 Replaying message ${id}...`); const result = await client.call('message.replay', { id }, Number(getFlag(args, 'timeout', 600_000))); printResult(result, getFlag(args, 'pretty', false)); } // ─── Subcommand: status (alias agent.list) ───────────────────────────────── async function cmdStatus(args) { const client = getClient(args); const result = await client.call('agent.list', { status: getFlag(args, 'status'), runner: getFlag(args, 'runner'), }); printResult(result, getFlag(args, 'pretty', false)); } // ─── Subcommand: agents (alias) ──────────────────────────────────────────── async function cmdAgents(args) { return cmdStatus(args); } // ─── Subcommand: health ──────────────────────────────────────────────────── async function cmdHealth(args) { const client = getClient(args); try { const result = await client.health(); printResult(result, getFlag(args, 'pretty', false)); } catch (err) { console.error(`❌ Server unreachable: ${err.message}`); process.exit(1); } } // ─── Subcommand: history (alias message.history) ─────────────────────────── async function cmdHistory(args) { const client = getClient(args); const result = await client.call('message.history', { toAgent: getFlag(args, 'to'), fromAgent: getFlag(args, 'from'), status: getFlag(args, 'status'), limit: Number(getFlag(args, 'limit', 50)), offset: Number(getFlag(args, 'offset', 0)), sinceHours: getFlag(args, 'since-hours', undefined), }); printResult(result, getFlag(args, 'pretty', false)); } // ─── Subcommand: sessions (multi-tenant) ───────────────────────────────── async function cmdSessions(args) { const client = getClient(args); const method = args.positionals[1] ?? 'list'; if (method === 'list' || method === 'stats') { const result = await client.call('session.list'); printResult(result, getFlag(args, 'pretty', false)); } else if (method === 'get') { const result = await client.call('session.get', { externalKey: requireFlag(args, 'key'), agentName: requireFlag(args, 'agent'), }); printResult(result, getFlag(args, 'pretty', false)); } else if (method === 'delete' || method === 'rm') { const result = await client.call('session.delete', { externalKey: requireFlag(args, 'key'), agentName: requireFlag(args, 'agent'), }); printResult(result, getFlag(args, 'pretty', false)); } else { throw new Error(`Unknown session subcommand: ${method}. Use: list, get, delete`); } } // ─── Subcommand: webhook (envoi programmatique) ─────────────────────────── async function cmdWebhook(args) { const provider = String(getFlag(args, 'provider', 'voipms')); const client = getClient(args); // Payload depuis --payload-file ou --payload (JSON string) let payload; const payloadFile = getFlag(args, 'payload-file'); const payloadStr = getFlag(args, 'payload'); if (payloadFile) { const fs = await import('node:fs/promises'); const raw = await fs.readFile(payloadFile, 'utf-8'); payload = JSON.parse(raw); } else if (payloadStr) { payload = JSON.parse(payloadStr); } else { throw new Error('webhook requires --payload <json> or --payload-file <path>'); } const autoDispatch = getFlag(args, 'agent') ? { agentName: requireFlag(args, 'agent'), runner: requireFlag(args, 'runner'), model: getFlag(args, 'model'), mode: getFlag(args, 'mode'), } : undefined; const result = await client.call('webhook.sms', { provider, payload, externalKey: getFlag(args, 'key'), autoDispatch, }); printResult(result, getFlag(args, 'pretty', false)); } // ─── Helpers ─────────────────────────────────────────────────────────────── function getClient(args) { const serverUrl = String(getFlag(args, 'server', process.env.BRIDGE_URL ?? 'http://127.0.0.1:3100')); const authToken = getFlag(args, 'auth-token') ?? process.env.BRIDGE_AUTH_TOKEN; return new BridgeHttpClient({ baseUrl: serverUrl, authToken }); } function printResult(result, pretty) { if (pretty) { console.log(JSON.stringify(result, null, 2)); } else { console.log(JSON.stringify(result)); } } // ─── Help ────────────────────────────────────────────────────────────────── function printHelp() { console.log(` overmind-bridge — Multi-agent orchestrator CLI USAGE overmind-bridge <command> [options] COMMANDS server Start the HTTP JSON-RPC 2.0 server call One-shot JSON-RPC call (use --method or positional) send Shortcut for 'call agent.run' scenario Run a multi-agent scenario from JSON/YAML status List all known agents and their state history Show persisted message history sessions Manage multi-tenant sessions (list, get, delete) webhook Send a webhook payload (voipms/twilio/discord/generic) and optionally auto-dispatch replay Replay a failed/stuck message by ID health Ping the server's /health endpoint version Print version COMMON OPTIONS --server <url> Bridge server URL (default: http://127.0.0.1:3100) or env BRIDGE_URL --auth-token <token> Bearer token (or env BRIDGE_AUTH_TOKEN) --pretty Pretty-print JSON output --timeout <ms> RPC timeout in ms (default: 600000) --var <key=value> Variable for templating (repeatable) PROMPT SOURCES (for 'call' and 'send') --prompt "..." Literal string --prompt-file <path> Read from file --prompt-stdin Read from stdin (auto-detect if TTY absent) --prompt-base64 "..." Decode base64 string --prompt-file-base64 Decode base64 from file --prompt-url <url> Fetch URL --prompt-json '{...}' Parse JSON object, reformat as text --prompt-template "..." Use --var for interpolation EXAMPLES # Server overmind-bridge server --port 3100 --auth-token secret # Agent call overmind-bridge call agent.run --agent scout --runner kilo \\ --prompt "Analyse BTC" # From file with template overmind-bridge send --agent scout --runner kilo \\ --prompt-file ./brief.txt --var ticker=BTC # A2A (agent-to-agent) overmind-bridge call agent.a2a --from scout --to analyst \\ --runner kilo --prompt "Validate my analysis" # Multi-agent scenario overmind-bridge scenario ./workflow.json --var ticker=BTC # Status overmind-bridge status --status busy # Multi-tenant sessions overmind-bridge sessions list overmind-bridge sessions get --key "+14187207735" --agent pdf_bon_travail overmind-bridge sessions rm --key "+14187207735" --agent pdf_bon_travail # Webhook programmatic dispatch overmind-bridge webhook --provider voipms \\ --payload '{"from":"+14187207735","message":"Salut","id":"abc"}' \\ --agent pdf_bon_travail --runner hermes # Server with all features overmind-bridge server --port 3100 --webhooks --sanitize-json ENV VARS BRIDGE_URL Default server URL BRIDGE_AUTH_TOKEN Default auth token MCP_URL Overmind MCP URL (default: http://localhost:3099/mcp) POSTGRES_HOST Postgres host POSTGRES_PORT Postgres port (default: 5432) POSTGRES_USER Postgres user POSTGRES_PASSWORD Postgres password POSTGRES_DATABASE Postgres database (v3.x) POSTGRES_DB Alias (v2.x compat, fallback) For more help on a specific command: overmind-bridge <command> --help `); } // ─── Run ─────────────────────────────────────────────────────────────────── main().catch((err) => { console.error('💥 Fatal:', err.message); if (process.env.DEBUG) console.error(err.stack); process.exit(1); }); //# sourceMappingURL=overmind-bridge.js.map