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
JavaScript
/**
* 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