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

190 lines • 8.44 kB
/** * HermesPoolClient — Thin wrapper over the native Hermes credential pool. * * ╔════════════════════════════════════════════════════════════════════════╗ * ║ WHY THIS EXISTS ║ * ║ ║ * ║ Hermes manages API keys centrally via `hermes auth add <provider>`. ║ * ║ Credentials are stored in a global pool with automatic rotation ║ * ║ (round_robin by default) and rate-limit aware failover. ║ * ║ ║ * ║ Overmind previously wrote `.env` files with hardcoded credentials per ║ * ║ profile. That defeated the pool — one key per profile, no rotation, ║ * ║ no failover. The right approach is to add credentials to the POOL ║ * ║ and let Hermes resolve them per profile. ║ * ║ ║ * ║ Providers in the pool (from `hermes auth list`): ║ * ║ - anthropic, copilot, deepseek, minimax, minimax-cn, ║ * ║ openai-api, qwen-oauth, xai, zai ║ * ║ ║ * ║ Per-profile: only config.yaml's `model.provider` is needed. ║ * ║ Hermes resolves the credentials + base_url from the pool automatically. ║ * ╚════════════════════════════════════════════════════════════════════════╝ */ import { exec } from 'child_process'; import { promisify } from 'util'; import { rootLogger } from '../lib/logger.js'; const execAsync = promisify(exec); const logger = rootLogger.child({ module: 'HermesPoolClient' }); export class HermesPoolClient { /** * List all providers and their credentials from `hermes auth list`. * The output format is: * * <provider-id> (N credentials): * #1 <label> api_key <source> [<status>] [←] * #2 <label> api_key <source> * ... */ static async list() { const { stdout } = await execAsync('hermes auth list', { timeout: 10000 }); return this.parseAuthList(stdout); } static parseAuthList(raw) { const providers = []; const lines = raw.split(/\r?\n/); let current = null; for (const line of lines) { // Header line: "<provider-id> (N credentials):" const headerMatch = line.match(/^([a-z0-9_-]+)\s+\((\d+)\s+credentials?\):\s*$/i); if (headerMatch) { if (current) providers.push(current); current = { id: headerMatch[1], count: parseInt(headerMatch[2], 10), hasActive: false, credentials: [], }; continue; } // Credential line: " #1 <label> api_key <source> [<status>] [←]" const credMatch = line.match(/^\s*#\d+\s+(.+?)\s{2,}api_key\s+(\S+)(?:\s+(.+?))?(?:\s+←)?\s*$/); if (credMatch && current) { const label = credMatch[1].trim(); const rawSource = credMatch[2]; const statusField = credMatch[3]?.trim(); const active = line.includes('←'); // Detect source type let source = 'unknown'; if (rawSource.startsWith('env:')) source = 'env'; else if (rawSource === 'manual') source = 'manual'; else if (rawSource === 'oauth') source = 'oauth'; else if (rawSource === 'gh_cli') source = 'gh_cli'; current.credentials.push({ label, source, active, preview: statusField && /^[a-z]+-/i.test(statusField) ? statusField : undefined, status: statusField && (statusField.includes('rate-limited') || statusField.includes('error')) ? statusField : undefined, }); if (active) current.hasActive = true; continue; } } if (current) providers.push(current); return providers; } /** * Get a specific provider's pool info. Returns null if provider doesn't exist. */ static async getProvider(providerId) { const all = await this.list(); return all.find((p) => p.id === providerId) ?? null; } /** * Add a credential to a provider's pool. * Wraps `hermes auth add <provider> --api-key <key> [--inference-url <url>]`. */ static async addCredential(providerId, apiKey, opts = {}) { const args = ['hermes', 'auth', 'add', providerId, '--type', 'api-key', '--api-key', apiKey]; if (opts.label) args.push('--label', opts.label); if (opts.inferenceUrl) args.push('--inference-url', opts.inferenceUrl); await execAsync(args.join(' '), { timeout: 30000 }); logger.info({ providerId, label: opts.label }, '[POOL] Credential added.'); } /** * Remove a credential from a provider's pool by label. * Hermes auth remove uses positional <provider> <label>. */ static async removeCredential(providerId, label) { await execAsync(`hermes auth remove "${providerId}" "${label}"`, { timeout: 15000 }); logger.info({ providerId, label }, '[POOL] Credential removed.'); } /** * Sync a list of credentials into the pool, deduped by label. * Useful for bulk-importing from an Overmind workspace .env. */ static async syncCredentials(providerId, credentials) { const existing = await this.getProvider(providerId); const existingLabels = new Set(existing?.credentials.map((c) => c.label) ?? []); let added = 0; let skipped = 0; for (const cred of credentials) { const label = cred.label ?? `imported-${Date.now()}-${added}`; if (existingLabels.has(label)) { skipped++; continue; } try { await this.addCredential(providerId, cred.apiKey, { label, inferenceUrl: cred.inferenceUrl, }); added++; } catch (e) { logger.warn({ providerId, label, error: e }, '[POOL] Failed to add credential.'); skipped++; } } return { added, skipped }; } /** * Check if a provider has at least one healthy credential available. */ static async isProviderHealthy(providerId) { const provider = await this.getProvider(providerId); return provider?.hasActive ?? false; } /** * List all healthy providers (at least one non-rate-limited credential). */ static async listHealthyProviders() { const all = await this.list(); return all.filter((p) => p.hasActive).map((p) => p.id); } /** * Compute a short summary of the pool state — useful for status embeds / dashboards. */ static async summary() { const all = await this.list(); const healthy = []; const rateLimited = []; let totalCreds = 0; for (const p of all) { totalCreds += p.credentials.length; if (p.hasActive) healthy.push(p.id); else rateLimited.push(p.id); } return { totalProviders: all.length, totalCredentials: totalCreds, healthyProviders: healthy, rateLimitedProviders: rateLimited, }; } } //# sourceMappingURL=HermesPoolClient.js.map