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
JavaScript
/**
* ╔══════════════════════════════════════════════════════════════════════╗
* ║ 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