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
255 lines • 10.1 kB
JavaScript
/**
* HermesGatewayManager — Manages the Hermes API Server lifecycle.
*
* ╔══════════════════════════════════════════════════════════════════════╗
* ║ PURPOSE ║
* ║ ║
* ║ Singleton that detects, validates, and ensures the Hermes API ║
* ║ Server (gateway/platforms/api_server.py) is reachable. ║
* ║ ║
* ║ The API Server is a platform adapter inside the Hermes gateway ║
* ║ process. It exposes an OpenAI-compatible HTTP+SSE API: ║
* ║ ║
* ║ GET /health → {"status": "ok", ...} ║
* ║ GET /v1/models → OpenAI model list ║
* ║ GET /v1/capabilities → Feature flags ║
* ║ POST /v1/chat/completions → Chat (streaming SSE or JSON) ║
* ║ POST /v1/responses → Responses API (stateful) ║
* ║ POST /v1/runs → Async run (returns run_id) ║
* ║ GET /v1/runs/{id}/events → SSE lifecycle events ║
* ║ POST /v1/runs/{id}/stop → Interrupt a run ║
* ║ POST /v1/runs/{id}/approval → Resolve pending approval ║
* ║ ║
* ║ Configuration (read from Hermes .env): ║
* ║ API_SERVER_ENABLED=1 → enables the adapter ║
* ║ API_SERVER_KEY=<secret> → Bearer token for auth ║
* ║ API_SERVER_PORT=8642 → listen port (default: 8642) ║
* ║ API_SERVER_HOST=127.0.0.1 → listen host (default: loopback) ║
* ║ ║
* ║ This manager does NOT start the gateway — that's Hermes' job ║
* ║ (via `hermes gateway start`). It only detects and health-checks. ║
* ╚══════════════════════════════════════════════════════════════════════╝
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import { rootLogger } from '../lib/logger.js';
const logger = rootLogger.child({ module: 'HermesGatewayManager' });
/**
* Resolve the Hermes home directory.
* On Windows: AppData\Local\hermes (official install)
* On Linux/macOS: ~/.hermes
*/
function getHermesHome() {
// 1. Explicit env var
if (process.env.HERMES_HOME)
return process.env.HERMES_HOME;
// 2. Windows official install
if (process.platform === 'win32') {
const localAppData = process.env.LOCALAPPDATA || process.env.USERPROFILE || os.homedir();
return path.join(localAppData, 'hermes');
}
// 3. POSIX
return path.join(os.homedir(), '.hermes');
}
/**
* Read API_SERVER_KEY from the Hermes .env file.
* The key is required by the API server for Bearer auth.
*/
function readApiKeyFromEnv() {
const hermesHome = getHermesHome();
const envPath = path.join(hermesHome, '.env');
try {
const content = fs.readFileSync(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('API_SERVER_KEY=')) {
const value = trimmed.slice('API_SERVER_KEY='.length).trim();
// Remove surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
return value;
}
}
}
catch {
// .env not found or unreadable
}
// Fallback: environment variable
return process.env.API_SERVER_KEY || null;
}
/** Read port from .env or use default */
function readPortFromEnv() {
const hermesHome = getHermesHome();
const envPath = path.join(hermesHome, '.env');
try {
const content = fs.readFileSync(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('API_SERVER_PORT=')) {
const port = parseInt(trimmed.slice('API_SERVER_PORT='.length).trim(), 10);
if (!isNaN(port) && port > 0 && port < 65536)
return port;
}
}
}
catch {
// ignore
}
// Fallback: env var or default
const envPort = parseInt(process.env.API_SERVER_PORT || '', 10);
return !isNaN(envPort) && envPort > 0 ? envPort : 8642;
}
/** Read host from .env or use default */
function readHostFromEnv() {
const hermesHome = getHermesHome();
const envPath = path.join(hermesHome, '.env');
try {
const content = fs.readFileSync(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('API_SERVER_HOST=')) {
return trimmed.slice('API_SERVER_HOST='.length).trim().replace(/['"]/g, '');
}
}
}
catch {
// ignore
}
return process.env.API_SERVER_HOST || '127.0.0.1';
}
/**
* Singleton manager for the Hermes API Server connection.
*
* Call `await HermesGatewayManager.getInstance()` to get a healthy gateway
* info object. The first call probes the server; subsequent calls return
* the cached state with periodic re-validation.
*/
export class HermesGatewayManager {
static instance = null;
cachedInfo = null;
lastHealthCheck = 0;
HEALTH_CHECK_TTL_MS = 10_000; // re-check at most every 10s
constructor() { }
static getInstance() {
if (!HermesGatewayManager.instance) {
HermesGatewayManager.instance = new HermesGatewayManager();
}
return HermesGatewayManager.instance;
}
/**
* Probe the API server health endpoint.
* Returns the raw JSON response or null on failure.
*/
async probeHealth(baseUrl, apiKey) {
try {
const response = await fetch(`${baseUrl}/health`, {
method: 'GET',
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(5000),
});
if (!response.ok)
return null;
return (await response.json());
}
catch {
return null;
}
}
/**
* Get detailed health info (includes PID, gateway state, platform status).
*/
async getDetailedHealth() {
const info = await this.ensureReady();
if (!info)
return null;
try {
const response = await fetch(`${info.url}/health/detailed`, {
method: 'GET',
headers: { Authorization: `Bearer ${info.apiKey}` },
signal: AbortSignal.timeout(5000),
});
if (!response.ok)
return null;
return (await response.json());
}
catch {
return null;
}
}
/**
* Ensure the API server is reachable. Probes health, caches result.
*
* @param forceRefresh Skip the cache TTL and re-probe.
* @returns GatewayInfo if healthy, null if unreachable.
*/
async ensureReady(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh &&
this.cachedInfo &&
this.cachedInfo.healthy &&
now - this.lastHealthCheck < this.HEALTH_CHECK_TTL_MS) {
return this.cachedInfo;
}
// Read config from Hermes .env
const apiKey = readApiKeyFromEnv();
const port = readPortFromEnv();
const host = readHostFromEnv();
const baseUrl = `http://${host}:${port}`;
if (!apiKey) {
logger.warn('[HermesGatewayManager] API_SERVER_KEY not found in Hermes .env — API server not configured.');
this.cachedInfo = {
url: baseUrl,
apiKey: '',
port,
host,
healthy: false,
pid: null,
version: null,
};
return null;
}
// Probe health
const health = await this.probeHealth(baseUrl, apiKey);
const healthy = health?.status === 'ok';
this.cachedInfo = {
url: baseUrl,
apiKey,
port,
host,
healthy,
pid: null,
version: health?.version || null,
};
this.lastHealthCheck = now;
if (healthy) {
logger.info({ url: baseUrl, version: this.cachedInfo.version }, '[HermesGatewayManager] ✅ API server healthy.');
}
else {
logger.warn({ url: baseUrl }, '[HermesGatewayManager] ⚠️ API server not reachable. Run `hermes gateway restart` after setting API_SERVER_ENABLED=1.');
}
return healthy ? this.cachedInfo : null;
}
/**
* Check if the gateway is ready (quick boolean check).
*/
async isReady() {
const info = await this.ensureReady();
return info !== null;
}
/**
* Get cached info without probing. Returns last known state.
*/
getCachedInfo() {
return this.cachedInfo;
}
/**
* Force a fresh health check on next ensureReady() call.
*/
invalidate() {
this.lastHealthCheck = 0;
}
}
//# sourceMappingURL=HermesGatewayManager.js.map