overmind-mcp 2.8.3 → 2.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.mcp.json.example +20 -20
  2. package/README.md +143 -143
  3. package/bin/launch.bat +40 -0
  4. package/bin/launch.js +78 -0
  5. package/bin/launch.sh +46 -0
  6. package/bin/overmind-pool.mjs +248 -248
  7. package/bin/restart_mcp.bat +3 -0
  8. package/bin/start_server.bat +3 -0
  9. package/bin/test_mcp.bat +4 -0
  10. package/dist/bin/cli.js +13 -0
  11. package/dist/bin/cli.js.map +1 -1
  12. package/dist/bin/launch.js +78 -0
  13. package/dist/bridge/BridgeProxy.d.ts +52 -0
  14. package/dist/bridge/BridgeProxy.d.ts.map +1 -0
  15. package/dist/bridge/BridgeProxy.js +265 -0
  16. package/dist/bridge/BridgeProxy.js.map +1 -0
  17. package/dist/bridge/OverBridgeService.d.ts +96 -0
  18. package/dist/bridge/OverBridgeService.d.ts.map +1 -0
  19. package/dist/bridge/OverBridgeService.js +334 -0
  20. package/dist/bridge/OverBridgeService.js.map +1 -0
  21. package/dist/bridge/index.d.ts +11 -0
  22. package/dist/bridge/index.d.ts.map +1 -0
  23. package/dist/bridge/index.js +11 -0
  24. package/dist/bridge/index.js.map +1 -0
  25. package/dist/bridge/types.d.ts +132 -0
  26. package/dist/bridge/types.d.ts.map +1 -0
  27. package/dist/bridge/types.js +19 -0
  28. package/dist/bridge/types.js.map +1 -0
  29. package/dist/bridge/utils.d.ts +48 -0
  30. package/dist/bridge/utils.d.ts.map +1 -0
  31. package/dist/bridge/utils.js +128 -0
  32. package/dist/bridge/utils.js.map +1 -0
  33. package/dist/lib/config.d.ts +1 -0
  34. package/dist/lib/config.d.ts.map +1 -1
  35. package/dist/lib/config.js +13 -5
  36. package/dist/lib/config.js.map +1 -1
  37. package/dist/lib/envUtils.d.ts +2 -0
  38. package/dist/lib/envUtils.d.ts.map +1 -1
  39. package/dist/lib/envUtils.js +13 -2
  40. package/dist/lib/envUtils.js.map +1 -1
  41. package/dist/lib/logger.js +1 -1
  42. package/dist/lib/orchestration/dispatcher.d.ts.map +1 -1
  43. package/dist/lib/orchestration/dispatcher.js +0 -1
  44. package/dist/lib/orchestration/dispatcher.js.map +1 -1
  45. package/dist/lib/processRegistry.d.ts.map +1 -1
  46. package/dist/lib/processRegistry.js +34 -21
  47. package/dist/lib/processRegistry.js.map +1 -1
  48. package/dist/memory/PostgresMemoryProvider.d.ts.map +1 -1
  49. package/dist/memory/PostgresMemoryProvider.js +14 -3
  50. package/dist/memory/PostgresMemoryProvider.js.map +1 -1
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +2 -1
  53. package/dist/server.js.map +1 -1
  54. package/dist/services/AgentManager.d.ts.map +1 -1
  55. package/dist/services/AgentManager.js +473 -97
  56. package/dist/services/AgentManager.js.map +1 -1
  57. package/dist/services/ClaudeRunner.d.ts.map +1 -1
  58. package/dist/services/ClaudeRunner.js.map +1 -1
  59. package/dist/services/GeminiRunner.d.ts +26 -2
  60. package/dist/services/GeminiRunner.d.ts.map +1 -1
  61. package/dist/services/GeminiRunner.js +146 -136
  62. package/dist/services/GeminiRunner.js.map +1 -1
  63. package/dist/services/KiloRunner.d.ts.map +1 -1
  64. package/dist/services/KiloRunner.js +6 -1
  65. package/dist/services/KiloRunner.js.map +1 -1
  66. package/dist/services/NousHermesRunner.d.ts +1 -0
  67. package/dist/services/NousHermesRunner.d.ts.map +1 -1
  68. package/dist/services/NousHermesRunner.js +476 -549
  69. package/dist/services/NousHermesRunner.js.map +1 -1
  70. package/dist/tools/agent_control.d.ts +1 -1
  71. package/dist/tools/agent_control.d.ts.map +1 -1
  72. package/dist/tools/agent_control.js +4 -3
  73. package/dist/tools/agent_control.js.map +1 -1
  74. package/dist/tools/config_example.d.ts +1 -0
  75. package/dist/tools/config_example.d.ts.map +1 -1
  76. package/dist/tools/config_example.js +45 -2
  77. package/dist/tools/config_example.js.map +1 -1
  78. package/dist/tools/create_agent.d.ts +1 -1
  79. package/dist/tools/manage_agents.d.ts +2 -2
  80. package/dist/tools/run_agent.d.ts +2 -1
  81. package/dist/tools/run_agent.d.ts.map +1 -1
  82. package/dist/tools/run_agent.js +30 -3
  83. package/dist/tools/run_agent.js.map +1 -1
  84. package/dist/tools/run_agents_parallel.d.ts +2 -1
  85. package/dist/tools/run_agents_parallel.d.ts.map +1 -1
  86. package/dist/tools/run_gemini.d.ts +13 -0
  87. package/dist/tools/run_gemini.d.ts.map +1 -1
  88. package/dist/tools/run_gemini.js +6 -2
  89. package/dist/tools/run_gemini.js.map +1 -1
  90. package/dist/tools/run_hermes.d.ts +1 -0
  91. package/dist/tools/run_hermes.d.ts.map +1 -1
  92. package/dist/tools/run_hermes.js +22 -15
  93. package/dist/tools/run_hermes.js.map +1 -1
  94. package/docs/agent-http-tutorial.md +524 -524
  95. package/docs/provider-config-map.md +444 -0
  96. package/package.json +8 -10
  97. package/scripts/_db_check.py +10 -0
  98. package/scripts/status_check.py +20 -0
@@ -3,43 +3,54 @@ import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
- import { CONFIG, resolveConfigPath } from '../lib/config.js';
6
+ import { CONFIG, resolveConfigPath, getWorkspaceDir } from '../lib/config.js';
7
7
  import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
8
+ import { linkSessionToPid } from '../lib/processRegistry.js';
8
9
  import { interpolateEnvVars } from '../lib/envUtils.js';
9
10
  import { withSpan } from '../lib/telemetry.js';
10
11
  import { loadEnvQuietly } from '../lib/loadEnv.js';
11
12
  import pino from 'pino';
12
13
  import { registerProcess, appendOutput, updateProcessStatus, } from '../lib/processRegistry.js';
14
+ import { registerLiveAgent, appendLiveOutput, setLiveStatus, unregisterLiveAgent, } from '../lib/agent_lifecycle.js';
13
15
  const execAsync = promisify(exec);
14
16
  const logger = pino({ name: 'NousHermesRunner' });
15
17
  // Sur Windows, child.kill() ne tue que le wrapper cmd.exe — le child réel devient
16
- // orphelin. On utilise taskkill /F /T pour propager au sous-arbre complet.
18
+ // orphelin. On utilise taskkill /F /T pour propager le kill au sous-arbre complet.
17
19
  const killProcessTree = (child) => {
18
- if (!child || child.exitCode !== null)
19
- return;
20
- if (process.platform === 'win32' && child.pid) {
21
- exec(`taskkill /F /T /PID ${child.pid}`, () => {
22
- // taskkill peut échouer si déjà mort — ignoré
23
- });
24
- }
25
- else {
26
- try {
27
- child.kill('SIGTERM');
20
+ return new Promise((resolve) => {
21
+ if (!child || child.exitCode !== null || child.killed) {
22
+ resolve();
23
+ return;
28
24
  }
29
- catch {
30
- // Ignored
25
+ let settled = false;
26
+ const finish = () => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ resolve();
31
+ };
32
+ child.once('exit', finish);
33
+ if (process.platform === 'win32' && child.pid) {
34
+ exec(`taskkill /F /T /PID ${child.pid}`, () => {
35
+ // taskkill peut échouer si le process est déjà mort
36
+ });
31
37
  }
32
- setTimeout(() => {
33
- if (child.exitCode === null && !child.killed) {
34
- try {
35
- child.kill('SIGKILL');
36
- }
37
- catch {
38
- // Ignored
39
- }
38
+ else {
39
+ try {
40
+ child.kill('SIGTERM');
40
41
  }
41
- }, 5000);
42
- }
42
+ catch { /* ignored */ }
43
+ setTimeout(() => {
44
+ if (child.exitCode === null && !child.killed) {
45
+ try {
46
+ child.kill('SIGKILL');
47
+ }
48
+ catch { /* ignored */ }
49
+ }
50
+ }, 2000);
51
+ }
52
+ setTimeout(finish, 5000);
53
+ });
43
54
  };
44
55
  /**
45
56
  * Find hermes binary across platforms (Windows, Linux, macOS)
@@ -149,586 +160,502 @@ export class NousHermesRunner {
149
160
  this.tempFiles = [];
150
161
  }
151
162
  async runAgent(options) {
152
- return runAgentWrapper.call(this, options);
163
+ try {
164
+ const result = await withSpan('hermes.runAgent', async (span) => {
165
+ span.setAttribute('agentName', options.agentName || '');
166
+ span.setAttribute('model', options.model || '');
167
+ span.setAttribute('runner', 'hermes');
168
+ return await this.runAgentInternal(options);
169
+ }, {
170
+ agentName: options.agentName || '',
171
+ model: options.model || '',
172
+ runner: 'hermes',
173
+ });
174
+ this.cleanupTempFiles();
175
+ if (options.agentName && result.sessionId) {
176
+ await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
177
+ }
178
+ return result;
179
+ }
180
+ catch (error) {
181
+ this.cleanupTempFiles();
182
+ logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, 'Hermes runner failed');
183
+ throw error;
184
+ }
153
185
  }
154
186
  async runAgentInternal(options) {
155
187
  const { prompt, agentName, autoResume, silent } = options;
156
188
  let { sessionId } = options;
157
- // --- Load .env files first (before anything else) ---
158
189
  const cwd = options.cwd || process.cwd();
190
+ const configPath = options.configPath || getWorkspaceDir();
191
+ // Load .env files FIRST
159
192
  loadEnvQuietly(path.join(cwd, '.env'));
160
193
  loadEnvQuietly(path.join(cwd, '../Workflow/.env'));
161
- // --- Auto Resume ---
194
+ // Auto Resume
162
195
  if (autoResume && agentName && !sessionId) {
163
- const lastId = await getLastSessionId(agentName, options.configPath, 'hermes');
196
+ const lastId = await getLastSessionId(agentName, configPath, 'hermes');
164
197
  if (lastId) {
165
198
  sessionId = lastId;
199
+ if (!silent)
200
+ console.error(`[NousHermesRunner] Auto-resume session: ${sessionId}`);
166
201
  }
167
202
  }
203
+ const MAX_BUF = 10 * 1024 * 1024;
204
+ const timeoutMs = this.timeoutMs;
205
+ const HARD_TIMEOUT_MS = 60000;
206
+ // HERMES_HOME setup
207
+ const overmindHermesPath = path.resolve(cwd, '.overmind', 'hermes', agentName ? `agent_${agentName}` : 'central');
208
+ const overmindHermesSubPath = path.join(overmindHermesPath, '.hermes');
209
+ if (agentName && !fs.existsSync(overmindHermesPath)) {
210
+ return { result: '', error: `INVALID_AGENT: Agent Hermes "${agentName}" non trouvé.` };
211
+ }
212
+ // Load agent settings + MCP config (same pattern as ClaudeRunner)
213
+ let systemPrompt = '';
214
+ let resolvedModel;
215
+ let resolvedProvider;
168
216
  const agentCustomEnv = {
169
217
  ...process.env,
170
- PYTHONIOENCODING: 'utf-8',
171
- PYTHONUTF8: '1',
172
- PYTHONUNBUFFERED: '1',
173
- PYTHONLEGACYWINDOWSSTDIO: '1',
174
- TERM: 'emacs',
175
- PROMPT_TOOLKIT_NO_INTERACTIVE: '1',
176
- // Force non-interactive for prompt_toolkit
177
- ANSICON: '1',
178
- // Map OpenRouter key if needed
179
- OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || process.env.OVERMIND_EMBEDDING_KEY,
180
- // Map NVIDIA NIM key
218
+ PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', PYTHONUNBUFFERED: '1',
219
+ PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
220
+ PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
221
+ OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '',
181
222
  NVIDIA_API_KEY: process.env.NVIDIA_API_KEY || process.env.NVAPI_KEY,
182
223
  NVIDIA_API_BASE: process.env.NVIDIA_API_BASE || 'https://integrate.api.nvidia.com/v1',
183
224
  ...(agentName ? { OVERMIND_AGENT_NAME: agentName } : {}),
225
+ // OVERMIND_AGENT_HOME tells Hermes (v0.13.0+) to read agent-specific .env FIRST
226
+ // get_env_value() in Hermes checks OVERMIND_AGENT_HOME/.hermes/.env before HERMES_HOME/.env
227
+ // This allows $VAR expansion done by Overmind to take precedence over gateway .env
228
+ ...(agentName ? { OVERMIND_AGENT_HOME: path.resolve(cwd, '.overmind', 'hermes', `agent_${agentName}`) } : {}),
229
+ // GLM_API_KEY in spawn env — zai provider resolves credentials via os.environ.get("GLM_API_KEY")
230
+ // before checking .env files. This is the most reliable path for Z.AI tokens.
231
+ ...(agentName ? { GLM_API_KEY: '' } : {}),
184
232
  };
185
- // --- Isolation / Settings / Prompt ---
186
- const overmindHermesPath = path.resolve(cwd, '.overmind', 'hermes', agentName ? `agent_${agentName}` : 'central');
187
- const overmindHermesSubPath = path.join(overmindHermesPath, '.hermes');
188
- if (!fs.existsSync(overmindHermesSubPath)) {
189
- fs.mkdirSync(overmindHermesSubPath, { recursive: true });
190
- }
191
- // On définit l'environnement pour Hermes
192
- // IMPORTANT: HERMES_HOME doit pointer vers le dossier contenant config.yaml
193
- agentCustomEnv.HERMES_HOME = overmindHermesSubPath;
194
- if (process.platform === 'win32') {
195
- agentCustomEnv.USERPROFILE = overmindHermesPath;
196
- }
197
- else {
198
- agentCustomEnv.HOME = overmindHermesPath;
199
- }
200
- // ─── Pre-write API keys to HERMES_HOME/.env ───────────────────────────────
201
- // Hermes (et son credential pool) lisent ~/.hermes/.env très tôt au démarrage,
202
- // avant même que le credential pool soit initialisé. On écrit les clés
203
- // critiques dans:
204
- // 1. HERMES_HOME/.env (notre isolation)
205
- // 2. ~/.hermes/.env (fallback pour l'init Hermes avant lecture HERMES_HOME)
206
- const writeHermesDotEnv = (dotEnvPath) => {
207
- const dotEnvEntries = [];
208
- const dotEnvKeys = [
209
- 'MINIMAXI_API_KEY',
210
- 'MINIMAX_API_KEY',
211
- 'ANTHROPIC_AUTH_TOKEN',
212
- 'ANTHROPIC_AUTH_TOKEN_1',
213
- 'ANTHROPIC_AUTH_TOKEN_2',
214
- 'ANTHROPIC_AUTH_TOKEN_3',
215
- 'ANTHROPIC_AUTH_TOKEN_4',
216
- 'MINIMAX_CN_API_KEY',
217
- 'OPENROUTER_API_KEY',
218
- 'OPENAI_API_KEY',
219
- 'Z_AI_API_KEY',
220
- 'GLM_API_KEY',
221
- 'Z_AI_API_KEY_2',
222
- 'MISTRAL_API_KEY',
223
- 'NVIDIA_API_KEY',
224
- ];
225
- for (const key of dotEnvKeys) {
226
- if (agentCustomEnv[key]) {
227
- dotEnvEntries.push(`${key}=${agentCustomEnv[key]}`);
228
- }
229
- }
230
- if (dotEnvEntries.length > 0) {
231
- const existingContent = fs.existsSync(dotEnvPath)
232
- ? fs.readFileSync(dotEnvPath, 'utf8')
233
- : '';
234
- const newContent = dotEnvEntries.join('\n') + '\n';
235
- const finalContent = existingContent ? newContent + existingContent : newContent;
236
- fs.writeFileSync(dotEnvPath, finalContent, 'utf8');
237
- if (!silent)
238
- console.error(`[NousHermesRunner] Wrote ${dotEnvEntries.length} keys to ${dotEnvPath}`);
239
- }
240
- };
241
- let systemPrompt = '';
233
+ let tmpSettingsPath = null;
234
+ let tmpMcpPath = null;
242
235
  if (agentName) {
236
+ const agentPromptPath = path.join(overmindHermesSubPath, 'SOUL.md');
237
+ if (fs.existsSync(agentPromptPath)) {
238
+ systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
239
+ }
240
+ // Load environment variables from .claude/settings_<agentName>.json
243
241
  try {
244
- const settingsDir = path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS);
245
- const agentSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`), options.configPath);
246
- if (!fs.existsSync(agentSettingsPath)) {
247
- // Lister les agents disponibles pour aider au debugging
248
- let availableAgents = [];
249
- try {
250
- const files = fs.readdirSync(settingsDir);
251
- availableAgents = files
252
- .filter((f) => f.startsWith('settings_') && f.endsWith('.json'))
253
- .map((f) => f.replace('settings_', '').replace('.json', ''));
254
- }
255
- catch (e) {
256
- logger.warn({ settingsDir, error: e }, 'Error reading settings directory');
257
- }
258
- return {
259
- result: '',
260
- error: `INVALID_AGENT: Agent Hermes "${agentName}" non trouvé.
261
- Veuillez utiliser 'create_agent' au préalable.
262
- Fichier attendu: ${agentSettingsPath}
263
- ${availableAgents.length > 0 ? `Agents disponibles: ${availableAgents.join(', ')}` : 'Aucun agent disponible'}
264
- `
265
- .replace(/\s+/g, ' ')
266
- .trim(),
267
- };
268
- }
269
- const rawSettings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
270
- const settings = interpolateEnvVars(rawSettings);
271
- // Create a temporary settings file with interpolated values (same approach as ClaudeRunner)
272
- // This ensures $VAR placeholders are resolved before Hermes reads them
273
- const tmpSettingsPath = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
274
- fs.writeFileSync(tmpSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
275
- this.tempFiles.push(tmpSettingsPath);
276
- const interpolatedSettingsPath = tmpSettingsPath;
277
- // Only use settings.model if it's a string (not a config object like {provider:"custom",...})
278
- if (!options.model && typeof settings.model === 'string') {
279
- options.model = settings.model;
280
- }
281
- // Extract ANTHROPIC_MODEL from env (used by some agents like sniperbot_analyst)
282
- if (!options.model && settings.env?.ANTHROPIC_MODEL && !settings.env.ANTHROPIC_MODEL.startsWith('$')) {
283
- options.model = settings.env.ANTHROPIC_MODEL;
284
- }
285
- // Extract ANTHROPIC_PROVIDER from env if present
286
- if (!options.provider && settings.env?.ANTHROPIC_PROVIDER && !settings.env.ANTHROPIC_PROVIDER.startsWith('$')) {
287
- options.provider = settings.env.ANTHROPIC_PROVIDER;
288
- }
289
- if (settings.env) {
290
- // Fusion intelligente : préserver les clés critiques (API keys)
291
- const criticalKeys = [
292
- // OpenAI
293
- 'OPENAI_API_KEY',
294
- 'OPENAI_API_BASE',
295
- 'OPENAI_BASE_URL',
296
- // Mistral
297
- 'MISTRAL_API_KEY',
298
- 'MISTRAL_API_KEY_2',
299
- 'MISTRAL_API_KEY_3',
300
- 'MISTRAL_API_KEY_4',
301
- 'MISTRAL_API_KEY_5',
302
- 'MISTRAL_API_KEY_6',
303
- 'MISTRAL_API_KEY_7',
304
- // NVIDIA
305
- 'NVIDIA_API_KEY',
306
- 'NVAPI_KEY',
307
- 'NVIDIA_API_BASE',
308
- // OpenRouter / Overmind
309
- 'OPENROUTER_API_KEY',
310
- 'OVERMIND_EMBEDDING_KEY',
311
- // MiniMax
312
- 'MINIMAXI_API_KEY',
313
- // ZhipuAI / GLM
314
- 'Z_AI_API_KEY',
315
- // Google / Gemini
316
- 'GOOGLE_API_KEY',
317
- 'GEMINI_API_KEY',
318
- // Anthropic
319
- 'ANTHROPIC_API_KEY',
320
- 'ANTHROPIC_AUTH_TOKEN',
321
- ];
322
- const envCopy = { ...settings.env };
323
- for (const key of criticalKeys) {
324
- if (agentCustomEnv[key] && !envCopy[key]) {
325
- envCopy[key] = agentCustomEnv[key];
242
+ const agentSettingsPath = resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `settings_${agentName}.json`), configPath);
243
+ if (fs.existsSync(agentSettingsPath)) {
244
+ let settings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
245
+ settings = interpolateEnvVars(settings);
246
+ // Create temporary settings file
247
+ const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
248
+ fs.writeFileSync(tempSettings, JSON.stringify(settings, null, 2));
249
+ tmpSettingsPath = tempSettings;
250
+ this.tempFiles.push(tempSettings);
251
+ if (settings.env) {
252
+ Object.assign(agentCustomEnv, settings.env);
253
+ if (!options.model && settings.env.MODEL) {
254
+ agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
326
255
  }
327
256
  }
328
- Object.assign(agentCustomEnv, envCopy);
329
- // ─── Resolve $VAR placeholders in agentCustomEnv values ───────────────
330
- // Hermes reads from process.env, so any "$ANTHROPIC_AUTH_TOKEN_2" style
331
- // placeholders must be resolved NOW before Hermes is spawned.
332
- // We iterate all keys and replace known placeholders with resolved values.
333
- const placeholders = {
334
- 'ANTHROPIC_AUTH_TOKEN_2': process.env.ANTHROPIC_AUTH_TOKEN_2,
335
- 'ANTHROPIC_AUTH_TOKEN_Y': process.env.ANTHROPIC_AUTH_TOKEN_Y,
336
- 'ANTHROPIC_AUTH_TOKEN_E': process.env.ANTHROPIC_AUTH_TOKEN_E,
337
- 'ANTHROPIC_BASE_URL_2': process.env.ANTHROPIC_BASE_URL_2,
338
- 'ANTHROPIC_BASE_URL_Y': process.env.ANTHROPIC_BASE_URL_Y,
339
- 'ANTHROPIC_BASE_URL_E': process.env.ANTHROPIC_BASE_URL_E,
340
- 'MINIMAXI_API_KEY_2': process.env.MINIMAXI_API_KEY_2,
341
- 'OPENAI_API_KEY_2': process.env.OPENAI_API_KEY_2,
342
- 'Z_AI_API_KEY_2': process.env.Z_AI_API_KEY_2,
343
- };
344
- for (const [key, value] of Object.entries(agentCustomEnv)) {
345
- if (typeof value === 'string' && value.startsWith('$')) {
346
- const resolved = placeholders[value.substring(1)];
347
- if (resolved) {
348
- agentCustomEnv[key] = resolved;
349
- if (!silent)
350
- console.error(`[NousHermesRunner] Resolved ${key}=${value.substring(1)} (resolved)`);
351
- }
352
- }
257
+ // MCP configurations
258
+ const agentMcpPath = resolveConfigPath(path.join(path.dirname(CONFIG.CLAUDE.PATHS.SETTINGS), `.mcp.${agentName}.json`), configPath);
259
+ if (fs.existsSync(agentMcpPath)) {
260
+ // Write temporary mcp path
261
+ const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_${agentName}_tmp.json`);
262
+ fs.writeFileSync(tempMcp, fs.readFileSync(agentMcpPath, 'utf8'));
263
+ tmpMcpPath = tempMcp;
264
+ this.tempFiles.push(tempMcp);
353
265
  }
354
- // Map ANTHROPIC_AUTH_TOKEN to provider-specific env vars
355
- // Hermes z-ai provider needs GLM_API_KEY, not ANTHROPIC_AUTH_TOKEN
356
- const providerForEnv = options.provider || settings.env?.ANTHROPIC_PROVIDER || '';
357
- if (providerForEnv.toLowerCase().includes('z-ai') || providerForEnv.toLowerCase().includes('zai')) {
358
- if (agentCustomEnv.ANTHROPIC_AUTH_TOKEN && !agentCustomEnv['GLM_API_KEY']) {
359
- agentCustomEnv['GLM_API_KEY'] = agentCustomEnv.ANTHROPIC_AUTH_TOKEN;
360
- if (!silent)
361
- console.error(`[NousHermesRunner] Mapped ANTHROPIC_AUTH_TOKEN → GLM_API_KEY for z-ai provider`);
362
- }
363
- }
364
- }
365
- // --- Load System Prompt (agents/agentName.md) ---
366
- const agentPromptPath = resolveConfigPath(path.join(path.dirname(settingsDir), 'agents', `${agentName}.md`), options.configPath);
367
- if (fs.existsSync(agentPromptPath)) {
368
- systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
369
- }
370
- // --- MCP Config Translation (JSON -> YAML for Hermes) ---
371
- const agentMcpPath = resolveConfigPath(path.join(path.dirname(settingsDir), `.mcp.${agentName}.json`), options.configPath);
372
- if (fs.existsSync(agentMcpPath)) {
373
- try {
374
- const rawMcpConfig = JSON.parse(fs.readFileSync(agentMcpPath, 'utf8'));
375
- const mcpConfig = interpolateEnvVars(rawMcpConfig);
376
- const hermesConfigDir = overmindHermesSubPath;
377
- if (!fs.existsSync(hermesConfigDir))
378
- fs.mkdirSync(hermesConfigDir, { recursive: true });
379
- const mcpJsonPath = path.join(hermesConfigDir, 'mcp.json');
380
- const configYamlPath = path.join(hermesConfigDir, 'config.yaml');
381
- // Helper pour convertir le format MCP JSON vers le format mcp.json Hermes (identique à Claude Desktop)
382
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2), 'utf8');
383
- // Generer aussi config.yaml (format snake_case attendu par Hermes)
384
- let yamlContent = 'mcp_servers:\n';
385
- for (const [name, server] of Object.entries(mcpConfig.mcpServers || {})) {
386
- const s = server;
387
- yamlContent += ` ${name}:\n`;
388
- if (s.command)
389
- yamlContent += ` command: "${s.command}"\n`;
390
- if (s.args && Array.isArray(s.args)) {
391
- yamlContent += ` args:\n`;
392
- for (const arg of s.args) {
393
- yamlContent += ` - "${String(arg).replace(/"/g, '\\"')}"\n`;
394
- }
395
- }
396
- if (s.env && typeof s.env === 'object') {
397
- yamlContent += ` env:\n`;
398
- for (const [k, v] of Object.entries(s.env)) {
399
- yamlContent += ` ${k}: "${String(v).replace(/"/g, '\\"')}"\n`;
266
+ else if (settings.enableAllProjectMcpServers === false &&
267
+ Array.isArray(settings.enabledMcpjsonServers)) {
268
+ const projectMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP, configPath);
269
+ if (fs.existsSync(projectMcpPath)) {
270
+ const fullMcp = JSON.parse(fs.readFileSync(projectMcpPath, 'utf8'));
271
+ const filteredMcp = { mcpServers: {} };
272
+ for (const serverName of settings.enabledMcpjsonServers) {
273
+ if (fullMcp.mcpServers && fullMcp.mcpServers[serverName]) {
274
+ filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName];
400
275
  }
401
276
  }
402
- if (s.url)
403
- yamlContent += ` url: "${s.url}"\n`;
277
+ const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_${agentName}_tmp.json`);
278
+ fs.writeFileSync(tempMcp, JSON.stringify(filteredMcp, null, 2));
279
+ tmpMcpPath = tempMcp;
280
+ this.tempFiles.push(tempMcp);
404
281
  }
405
- fs.writeFileSync(configYamlPath, yamlContent, 'utf8');
406
- // Remove the model config append - it uses 'provider: custom' which Hermes doesn't accept
407
- // Instead, rely on MINIMAX_BASE_URL_OVERRIDE + MINIMAXI_API_KEY env vars for MiniMaxi
408
- // The model config in config.yaml is not the right approach
409
- if (!silent)
410
- console.error(`[NousHermesRunner] 🛠️ Hermes configs (mcp.json & config.yaml) generated in ${hermesConfigDir}`);
411
- }
412
- catch (err) {
413
- logger.error({ error: err }, 'Error translating MCP config');
414
282
  }
415
283
  }
416
284
  }
417
285
  catch (e) {
418
- if (e instanceof Error && e.message?.includes('INVALID_AGENT'))
419
- throw e;
420
- logger.warn({ agentName, error: e }, 'Error processing agent settings');
286
+ logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
421
287
  }
422
- }
423
- // --- CLI Arguments & Prompt Handling ---
424
- const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
425
- // Tronquer si nécessaire pour éviter les limites Windows (8191)
426
- const MAX_PROMPT_LEN = 7000;
427
- let cliPrompt = finalPrompt;
428
- if (cliPrompt.length > MAX_PROMPT_LEN) {
429
- console.warn(`[NousHermesRunner] ⚠️ Prompt tronqué de ${cliPrompt.length} à ${MAX_PROMPT_LEN} chars`);
430
- cliPrompt = cliPrompt.substring(0, MAX_PROMPT_LEN);
431
- }
432
- // Hermes CLI modes:
433
- // - `hermes -z <prompt>` : top-level one-shot (no banner, clean stdout, auto-exit)
434
- // - `hermes chat -q <prompt>` : query mode with banner (interactive)
435
- // - `hermes chat -z <prompt>` : INVALID (subcommand doesn't accept -z)
436
- // We use top-level `-z` for runner mode (clean output, auto-exit).
437
- const cleanArgs = ['-z', cliPrompt];
438
- // --- Model & Provider selection ---
439
- const DEFAULT_MODEL = 'tencent/hy3-preview:free'; // Modèle OpenRouter gratuit
440
- const originalModel = options.model || DEFAULT_MODEL;
441
- // Guard: ensure model is always a string (not an object from settings.model)
442
- const modelStr = typeof originalModel === 'string' ? originalModel : DEFAULT_MODEL;
443
- // Don't use resolveKiloModel here - it adds provider prefix like "minimax/" which
444
- // causes Hermes to route to OpenRouter instead of MiniMaxi
445
- const model = modelStr;
446
- const isNvidiaModel = model.includes('deepseek') || model.includes('nvidia');
447
- const hasNvidiaKey = !!(agentCustomEnv.NVIDIA_API_KEY || agentCustomEnv.NVAPI_KEY);
448
- const lowModel = model.toLowerCase();
449
- const isOpenAIModel = lowModel.includes('gpt') ||
450
- lowModel.includes('o1') ||
451
- lowModel.includes('o3');
452
- const hasOpenAIKey = !!agentCustomEnv.OPENAI_API_KEY;
453
- const isMiniMaxModel = lowModel.includes('minimax') || lowModel.includes('mini-max');
454
- const hasMiniMaxKey = !!(agentCustomEnv.MINIMAXI_API_KEY ||
455
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN ||
456
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_1 ||
457
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_2 ||
458
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_3 ||
459
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_4);
460
- const isGLMModel = lowModel.includes('glm');
461
- const hasGLMKey = !!agentCustomEnv.Z_AI_API_KEY;
462
- const isMistralModel = model.includes('mistral') || model.includes('codestral') || model.includes('devstral');
463
- const hasMistralKey = !!agentCustomEnv.MISTRAL_API_KEY;
464
- cleanArgs.push('--model', model);
465
- if (isOpenAIModel && hasOpenAIKey) {
466
- logger.info({ model, provider: 'openai' }, 'Using OpenAI provider');
467
- cleanArgs.push('--provider', 'openai');
468
- // Nettoyage des clés conflictuelles
469
- delete agentCustomEnv.OPENROUTER_API_KEY;
470
- delete agentCustomEnv.NVIDIA_API_KEY;
471
- delete agentCustomEnv.NVAPI_KEY;
472
- delete agentCustomEnv.MINIMAXI_API_KEY;
473
- delete agentCustomEnv.Z_AI_API_KEY;
474
- }
475
- else if (isMiniMaxModel && hasMiniMaxKey) {
476
- // Use minimax-cn provider which correctly uses api.minimaxi.com (NOT api.minimax.io)
477
- // The minimax provider hardcodes api.minimax.io which is wrong for MiniMaxi
478
- logger.info({ model, provider: 'minimax-cn' }, 'Using MiniMax via minimax-cn provider');
479
- cleanArgs.push('--provider', 'minimax-cn');
480
- // Write base_url to config.yaml later (see below, after config generation)
481
- const resolvedMiniMaxKey = agentCustomEnv.ANTHROPIC_AUTH_TOKEN_4 ||
482
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_3 ||
483
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_2 ||
484
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN ||
485
- agentCustomEnv.MINIMAXI_API_KEY ||
486
- process.env.MINIMAXI_API_KEY;
487
- if (resolvedMiniMaxKey) {
488
- agentCustomEnv.ANTHROPIC_TOKEN = resolvedMiniMaxKey;
489
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN = resolvedMiniMaxKey;
490
- agentCustomEnv.ANTHROPIC_AUTH_TOKEN_4 = resolvedMiniMaxKey;
491
- agentCustomEnv.MINIMAXI_API_KEY = resolvedMiniMaxKey;
492
- agentCustomEnv.MINIMAX_API_KEY = resolvedMiniMaxKey;
493
- // minimax-cn provider reads MINIMAX_CN_API_KEY specifically
494
- agentCustomEnv.MINIMAX_CN_API_KEY = resolvedMiniMaxKey;
495
- // Force rewrite the auth.json so Hermes picks up the new key
288
+ // Load environment from isolated .env file (to allow overrides)
289
+ const envPath = path.join(overmindHermesSubPath, '.env');
290
+ if (fs.existsSync(envPath)) {
496
291
  try {
497
- const authPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.hermes', 'auth.json');
498
- if (fs.existsSync(authPath)) {
499
- const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
500
- if (auth.credential_pool?.['minimax-cn']?.[0]) {
501
- auth.credential_pool['minimax-cn'][0].access_token = resolvedMiniMaxKey;
502
- auth.credential_pool['minimax-cn'][0].last_status = null;
503
- auth.credential_pool['minimax-cn'][0].last_error_code = null;
504
- fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
505
- if (!silent)
506
- console.error(`[NousHermesRunner] Updated minimax-cn credential in auth.json`);
292
+ const content = fs.readFileSync(envPath, 'utf8');
293
+ content.split('\n').forEach((line) => {
294
+ const trimmed = line.trim();
295
+ if (!trimmed || trimmed.startsWith('#'))
296
+ return;
297
+ const eqIdx = trimmed.indexOf('=');
298
+ if (eqIdx === -1)
299
+ return;
300
+ const key = trimmed.slice(0, eqIdx).trim();
301
+ let value = trimmed.slice(eqIdx + 1).trim();
302
+ if (value.startsWith('"') && value.endsWith('"'))
303
+ value = value.slice(1, -1);
304
+ else if (value.startsWith("'") && value.endsWith("'"))
305
+ value = value.slice(1, -1);
306
+ if (key) {
307
+ agentCustomEnv[key] = value;
507
308
  }
508
- }
309
+ });
310
+ }
311
+ catch (e) {
312
+ logger.warn({ envPath, error: e }, 'Failed to read agent env file');
509
313
  }
510
- catch (e) { /* non-critical */ }
511
- if (!silent)
512
- console.error(`[NousHermesRunner] Set ANTHROPIC_AUTH_TOKEN_4 for MiniMax via minimax-cn`);
513
314
  }
514
- delete agentCustomEnv.OPENROUTER_API_KEY;
515
- delete agentCustomEnv.OVERMIND_EMBEDDING_KEY;
516
- delete agentCustomEnv.NVIDIA_API_KEY;
517
- delete agentCustomEnv.NVIDIA_API_BASE;
518
- delete agentCustomEnv.NVAPI_KEY;
519
- delete agentCustomEnv.OPENAI_API_KEY;
520
- delete agentCustomEnv.Z_AI_API_KEY;
521
- }
522
- else if (isGLMModel && hasGLMKey) {
523
- logger.info({ model, provider: 'z-ai' }, 'Using ZhipuAI/GLM provider');
524
- cleanArgs.push('--provider', 'z-ai');
525
- // Hermes z-ai provider needs GLM_API_KEY specifically
526
- const resolvedGLMKey = agentCustomEnv.Z_AI_API_KEY;
527
- if (resolvedGLMKey) {
528
- agentCustomEnv['GLM_API_KEY'] = resolvedGLMKey;
315
+ resolvedModel = agentCustomEnv.MODEL || agentCustomEnv.ANTHROPIC_MODEL;
316
+ resolvedProvider = agentCustomEnv.PROVIDER || agentCustomEnv.ANTHROPIC_PROVIDER;
317
+ if (resolvedProvider && (resolvedProvider.startsWith('http://') || resolvedProvider.startsWith('https://'))) {
318
+ if (resolvedProvider.includes('minimax')) {
319
+ resolvedProvider = 'minimax-cn';
320
+ }
321
+ else if (resolvedProvider.includes('z.ai') || resolvedProvider.includes('bigmodel')) {
322
+ resolvedProvider = 'zai';
323
+ }
324
+ else {
325
+ resolvedProvider = undefined;
326
+ }
529
327
  }
530
- // Nettoyage des clés conflictuelles
531
- delete agentCustomEnv.OPENROUTER_API_KEY;
532
- delete agentCustomEnv.NVIDIA_API_KEY;
533
- delete agentCustomEnv.NVAPI_KEY;
534
- delete agentCustomEnv.OPENAI_API_KEY;
535
- delete agentCustomEnv.MINIMAXI_API_KEY;
536
- delete agentCustomEnv.ANTHROPIC_AUTH_TOKEN_4;
537
- }
538
- else if (isMistralModel && hasMistralKey) {
539
- logger.info({ model, provider: 'mistral' }, 'Using Mistral provider');
540
- cleanArgs.push('--provider', 'mistral');
541
- // Nettoyage des clés conflictuelles
542
- delete agentCustomEnv.OPENROUTER_API_KEY;
543
- delete agentCustomEnv.NVIDIA_API_KEY;
544
- delete agentCustomEnv.NVAPI_KEY;
545
- delete agentCustomEnv.OPENAI_API_KEY;
546
328
  }
547
- else if (isNvidiaModel && hasNvidiaKey) {
548
- logger.info({ model, provider: 'nvidia' }, 'Using NVIDIA NIM provider');
549
- cleanArgs.push('--provider', 'nvidia');
550
- }
551
- else {
552
- // Fallback OpenRouter pour tout le reste ou si clé NIM manquante
553
- logger.info({ model, provider: 'openrouter' }, 'Using OpenRouter provider');
554
- cleanArgs.push('--provider', 'openrouter');
329
+ const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
330
+ const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
331
+ const cliPrompt = finalPrompt.length > 7000 ? finalPrompt.substring(0, 7000) : finalPrompt;
332
+ // Build CLI args: chat -q (persistent session, NOT -z oneshot)
333
+ // -z + --resume doesn't work — resume is ignored in oneshot mode
334
+ const cleanArgs = ['chat', '-q', cliPrompt, '-Q'];
335
+ cleanArgs.push('--model', finalModel);
336
+ if (options.provider || resolvedProvider) {
337
+ cleanArgs.push('--provider', options.provider || resolvedProvider);
555
338
  }
556
- // Re-write .env with all provider-specific keys now resolved (e.g. GLM_API_KEY for z-ai)
557
- const defaultHermesHome = path.join(process.env.HOME || process.env.USERPROFILE || '', '.hermes');
558
- writeHermesDotEnv(path.join(overmindHermesSubPath, '.env'));
559
- writeHermesDotEnv(path.join(defaultHermesHome, '.env'));
560
- // --- Hermes-native flags: --resume, --mcp-config ---
561
- // NOTE: --name is NOT supported by Hermes CLI v0.11.0+ (unrecognized argument error).
562
- // Session naming works via --resume or --continue, not --name.
563
- // --resume: continue existing session
564
- if (sessionId) {
339
+ if (sessionId)
565
340
  cleanArgs.push('--resume', sessionId);
341
+ // Token fallback setup (same as ClaudeRunner)
342
+ const FALLBACK_KEYS = ['AUTH_FALLBACK_1', 'AUTH_FALLBACK_2', 'AUTH_FALLBACK_3'];
343
+ const TOKEN_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_AUTH_TOKEN_E', 'GLM_API_KEY', 'Z_AI_API_KEY', 'MINIMAX_CN_API_KEY'];
344
+ const getAvailableFallbacks = () => {
345
+ const fb = [];
346
+ for (const k of FALLBACK_KEYS) {
347
+ const v = agentCustomEnv[k];
348
+ if (v && typeof v === 'string' && v.length > 0)
349
+ fb.push({ key: k, value: v });
350
+ }
351
+ return fb;
352
+ };
353
+ const getTokenForIndex = (idx) => {
354
+ if (idx === 0) {
355
+ for (const tk of TOKEN_KEYS) {
356
+ const v = agentCustomEnv[tk];
357
+ if (v && typeof v === 'string' && v.length > 0)
358
+ return { tokenEnvKey: tk, tokenValue: v };
359
+ }
360
+ return null;
361
+ }
362
+ const fb = getAvailableFallbacks();
363
+ return fb[idx - 1] ? { tokenEnvKey: fb[idx - 1].key, tokenValue: fb[idx - 1].value } : null;
364
+ };
365
+ const isRetryableError = (stderr) => {
366
+ const lower = stderr.toLowerCase();
367
+ return lower.includes('401') || lower.includes('unauthorized') ||
368
+ lower.includes('invalid api key') || lower.includes('authentication failed') ||
369
+ lower.includes('invalid authentication') || lower.includes('429') ||
370
+ lower.includes('rate limit') || lower.includes('quota exhausted') ||
371
+ lower.includes('limit exhausted') || lower.includes('503') ||
372
+ lower.includes('service unavailable') || lower.includes('500') ||
373
+ lower.includes('internal server error');
374
+ };
375
+ // HERMES_HOME setup
376
+ if (!fs.existsSync(overmindHermesSubPath))
377
+ fs.mkdirSync(overmindHermesSubPath, { recursive: true });
378
+ agentCustomEnv.HERMES_HOME = overmindHermesSubPath;
379
+ if (process.platform === 'win32')
380
+ agentCustomEnv.USERPROFILE = overmindHermesPath;
381
+ else
382
+ agentCustomEnv.HOME = overmindHermesPath;
383
+ // Write .env to HERMES_HOME (credential auto-discovery) - Cleaned to prevent duplicates
384
+ // EXCLUDE all OpenRouter keys — OpenRouter is managed internally by Overmind, Hermes must never see it
385
+ const credRegex = /(?:api_key|auth_token|base_url|endpoint|url)$/i;
386
+ const openRouterPrefixes = ['OPENROUTER', 'OVERMIND_EMBEDDING'];
387
+ const envMap = new Map();
388
+ const dotPath = path.join(overmindHermesSubPath, '.env');
389
+ if (fs.existsSync(dotPath)) {
390
+ try {
391
+ const existing = fs.readFileSync(dotPath, 'utf8');
392
+ existing.split('\n').forEach((line) => {
393
+ const trimmed = line.trim();
394
+ if (!trimmed || trimmed.startsWith('#'))
395
+ return;
396
+ const eqIdx = trimmed.indexOf('=');
397
+ if (eqIdx === -1)
398
+ return;
399
+ const k = trimmed.slice(0, eqIdx).trim();
400
+ let v = trimmed.slice(eqIdx + 1).trim();
401
+ if (v.startsWith('"') && v.endsWith('"'))
402
+ v = v.slice(1, -1);
403
+ else if (v.startsWith("'") && v.endsWith("'"))
404
+ v = v.slice(1, -1);
405
+ if (k) {
406
+ if (openRouterPrefixes.some(p => k.toUpperCase().startsWith(p)))
407
+ return;
408
+ envMap.set(k, v);
409
+ }
410
+ });
411
+ }
412
+ catch (e) {
413
+ logger.warn({ envPath: dotPath, error: e }, 'Failed to read existing agent env file for deduplication');
414
+ }
566
415
  }
567
- // --mcp-config: point Hermes to our generated config.yaml
568
- // Generated at lines 419-448 in overmindHermesSubPath
569
- const configYamlPath = path.join(overmindHermesSubPath, 'config.yaml');
570
- if (fs.existsSync(configYamlPath)) {
571
- cleanArgs.push('--mcp-config', configYamlPath);
572
- this.tempFiles.push(path.join(overmindHermesSubPath, 'mcp.json'), configYamlPath);
416
+ for (const [k, v] of Object.entries(agentCustomEnv)) {
417
+ if (typeof v === 'string' && v.length > 0 && credRegex.test(k)) {
418
+ if (openRouterPrefixes.some(p => k.toUpperCase().startsWith(p)))
419
+ continue;
420
+ envMap.set(k, v);
421
+ }
422
+ }
423
+ const finalDotEntries = [];
424
+ for (const [k, v] of envMap.entries()) {
425
+ finalDotEntries.push(`${k}=${v}`);
573
426
  }
574
- // --hermes-dir: isolate this agent's hermes state (auth.json, .env, sessions)
575
- // Pass via HERMES_DIR env var (not as CLI flag — --hermes-dir is only for subcommands like "chat")
576
- const hermesDirEnv = { HERMES_DIR: overmindHermesSubPath };
577
- // --- Find Hermes Binary (cross-platform) ---
578
- const spawnCommand = await findHermesBinary();
579
- if (!silent) {
580
- logger.info({ command: spawnCommand, args: cleanArgs }, 'Starting Hermes Agent');
427
+ fs.writeFileSync(dotPath, finalDotEntries.join('\n') + '\n', 'utf8');
428
+ // Generate config.yaml in HERMES_HOME (MCP servers)
429
+ if (tmpMcpPath && fs.existsSync(tmpMcpPath)) {
430
+ try {
431
+ const mc = JSON.parse(fs.readFileSync(tmpMcpPath, 'utf8'));
432
+ const yamlPath = path.join(overmindHermesSubPath, 'config.yaml');
433
+ // Preserve existing config.yaml (tts, llm, etc.) merge mcp_servers only
434
+ let existingYaml = '';
435
+ if (fs.existsSync(yamlPath)) {
436
+ existingYaml = fs.readFileSync(yamlPath, 'utf8');
437
+ }
438
+ // Build new mcp_servers section
439
+ let newMcpSection = 'mcp_servers:\n';
440
+ for (const [name, srv] of Object.entries(mc.mcpServers || {})) {
441
+ const s = srv;
442
+ newMcpSection += ` ${name}:\n`;
443
+ if (s.url)
444
+ newMcpSection += ` url: "${s.url}"\n`;
445
+ if (s.command)
446
+ newMcpSection += ` command: "${s.command}"\n`;
447
+ }
448
+ // Merge: replace mcp_servers block in existing yaml or append
449
+ let finalYaml;
450
+ if (existingYaml.includes('mcp_servers:')) {
451
+ finalYaml = existingYaml.replace(/mcp_servers:\n([\s\S]*?)(?=\n\w|\n$|$)/, newMcpSection.trimEnd() + '\n');
452
+ }
453
+ else {
454
+ finalYaml = existingYaml.trimEnd() + '\n' + newMcpSection;
455
+ }
456
+ fs.writeFileSync(yamlPath, finalYaml, 'utf8');
457
+ if (!silent)
458
+ console.error(`[NousHermesRunner] MCP config.yaml written to ${yamlPath}`);
459
+ }
460
+ catch (e) {
461
+ console.error(`[NousHermesRunner] config.yaml error: ${e}`);
462
+ }
581
463
  }
464
+ // AbortSignal
465
+ if (options.signal?.aborted)
466
+ return Promise.reject(new Error('ABORTED'));
467
+ let currentChildRef = null;
582
468
  return new Promise((resolve) => {
583
469
  let resolved = false;
584
- const safeResolve = (value) => {
585
- if (!resolved) {
586
- resolved = true;
587
- resolve(value);
470
+ let retryCount = 0;
471
+ const maxRetries = getAvailableFallbacks().length + 1;
472
+ let currentSessionId = sessionId;
473
+ const safeResolve = (v) => { if (!resolved) {
474
+ resolved = true;
475
+ resolve(v);
476
+ } };
477
+ const cleanupTmpFiles = () => {
478
+ for (const f of [tmpSettingsPath, tmpMcpPath]) {
479
+ if (f && fs.existsSync(f)) {
480
+ try {
481
+ fs.unlinkSync(f);
482
+ }
483
+ catch { /* ignored */ }
484
+ }
588
485
  }
589
486
  };
590
- // shell: false if absolute path (direct binary), true if just "hermes" (needs PATH resolution)
591
- const useShell = !path.isAbsolute(spawnCommand);
592
- const child = spawn(spawnCommand, cleanArgs, {
593
- cwd: options.cwd || process.cwd(),
594
- shell: useShell,
595
- windowsHide: true,
596
- env: { ...agentCustomEnv, HERMES_DIR: overmindHermesSubPath },
597
- });
598
- if (child.pid) {
599
- void registerProcess(child.pid, { agentName: agentName || '', runner: 'hermes', configPath: options.configPath });
600
- }
601
- let stdout = '';
602
- let stderr = '';
603
- child.stdout?.on('data', (d) => {
604
- const chunk = d.toString();
605
- if (child.pid)
606
- void appendOutput(child.pid, chunk, options.configPath);
607
- if (stdout.length + chunk.length > this.MAX_BUF) {
608
- stdout = stdout.slice(-this.MAX_BUF);
609
- }
610
- stdout += chunk;
611
- if (!silent) {
612
- process.stderr.write(`[Hermes] ${chunk}`);
613
- }
614
- });
615
- child.stderr?.on('data', (d) => {
616
- const chunk = d.toString();
617
- if (stderr.length + chunk.length > this.MAX_BUF) {
618
- stderr = stderr.slice(-this.MAX_BUF);
487
+ const writeAuthJson = (tokenInfo) => {
488
+ if (!tokenInfo || !overmindHermesSubPath)
489
+ return;
490
+ try {
491
+ const authPath = path.join(overmindHermesSubPath, 'auth.json');
492
+ const auth = { version: 1, providers: {}, credential_pool: {} };
493
+ if (fs.existsSync(authPath))
494
+ Object.assign(auth, JSON.parse(fs.readFileSync(authPath, 'utf8')));
495
+ if (!auth.credential_pool)
496
+ auth.credential_pool = {};
497
+ const cleanCp = auth.credential_pool;
498
+ // Déterminer le provider effectif (CLI > settings)
499
+ const effectiveProvider = resolvedProvider || 'zai';
500
+ cleanCp[effectiveProvider] = [{
501
+ id: `${effectiveProvider}-default`, label: tokenInfo.tokenEnvKey, auth_type: 'api_key',
502
+ priority: 0, source: `env:${tokenInfo.tokenEnvKey}`, access_token: tokenInfo.tokenValue,
503
+ last_status: null, last_error_code: null,
504
+ base_url: agentCustomEnv['GLM_BASE_URL'] || agentCustomEnv['ANTHROPIC_BASE_URL'] || 'https://api.z.ai/api/coding/paas/v4',
505
+ request_count: 0,
506
+ }];
507
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
508
+ // Écrire .env pour OVERMIND_AGENT_HOME — le provider effective détermine la clé d'env à écrire
509
+ // minimax-cn attend MINIMAX_CN_API_KEY, zai attend GLM_API_KEY (ou ANTHROPIC_AUTH_TOKEN)
510
+ const dotEnvPath = path.join(overmindHermesSubPath, '.env');
511
+ const baseUrl = agentCustomEnv['GLM_BASE_URL'] || agentCustomEnv['ANTHROPIC_BASE_URL'] || 'https://api.z.ai/api/coding/paas/v4';
512
+ const dotLines = [
513
+ `ANTHROPIC_PROVIDER=${effectiveProvider}`,
514
+ `ANTHROPIC_BASE_URL=${baseUrl}`,
515
+ ];
516
+ // Le provider determine quelle variable d'env écrire dans .env pour seed credentials
517
+ if (effectiveProvider === 'minimax-cn') {
518
+ dotLines.unshift(`MINIMAX_CN_API_KEY=${tokenInfo.tokenValue}`);
519
+ }
520
+ else if (effectiveProvider === 'zai' || effectiveProvider === 'z-ai') {
521
+ dotLines.unshift(`GLM_API_KEY=${tokenInfo.tokenValue}`);
522
+ }
523
+ else {
524
+ // Default: ANTHROPIC_AUTH_TOKEN
525
+ dotLines.unshift(`ANTHROPIC_AUTH_TOKEN=${tokenInfo.tokenValue}`);
526
+ }
527
+ fs.writeFileSync(dotEnvPath, dotLines.join('\n') + '\n', 'utf8');
619
528
  }
620
- stderr += chunk;
621
- if (!silent) {
622
- process.stderr.write(`[Hermes:ERR] ${chunk}`);
529
+ catch (_e) { /* non-critical */ }
530
+ };
531
+ const spawnHermes = async (tokenInfo) => {
532
+ const spawnEnv = { ...process.env, ...agentCustomEnv };
533
+ if (tokenInfo) {
534
+ for (const tk of TOKEN_KEYS)
535
+ delete spawnEnv[tk];
536
+ let resolvedToken = tokenInfo.tokenValue;
537
+ if (resolvedToken.startsWith('$'))
538
+ resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
539
+ spawnEnv[tokenInfo.tokenEnvKey] = resolvedToken;
623
540
  }
624
- });
625
- const timeout = setTimeout(() => {
626
- killProcessTree(child);
627
- if (child.pid)
628
- void updateProcessStatus(child.pid, 'failed', null, options.configPath);
629
- safeResolve({
630
- result: stdout.trim(),
631
- error: 'TIMEOUT',
632
- rawOutput: stdout + '\n\n' + stderr,
633
- model,
634
- nickname: originalModel !== model ? originalModel : undefined,
541
+ writeAuthJson(tokenInfo);
542
+ // BLOCK: OpenRouter is for embeddings only — never pass to Hermes for LLM inference
543
+ delete spawnEnv['OPENROUTER_API_KEY'];
544
+ delete spawnEnv['OPENROUTER_BASE_URL'];
545
+ delete spawnEnv['OVERMIND_EMBEDDING_KEY'];
546
+ const hermesBin = await findHermesBinary();
547
+ const child = spawn(hermesBin, cleanArgs, {
548
+ cwd, shell: false, windowsHide: true,
549
+ env: {
550
+ ...spawnEnv,
551
+ HERMES_HOME: overmindHermesSubPath,
552
+ VIRTUAL_ENV: process.env.HERMES_AGENT_ROOT
553
+ ? path.join(process.env.HERMES_AGENT_ROOT, 'venv')
554
+ : path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv'),
555
+ PATH: `${process.env.HERMES_AGENT_ROOT || path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv')};${process.env.PATH || ''}`,
556
+ },
635
557
  });
636
- }, this.timeoutMs);
637
- // AbortSignal support (like ClaudeRunner)
638
- if (options.signal) {
639
- const onAbort = () => {
640
- clearTimeout(timeout);
641
- killProcessTree(child);
642
- if (child.pid)
643
- void updateProcessStatus(child.pid, 'failed', null, options.configPath);
644
- safeResolve({
645
- result: stdout.trim(),
646
- error: 'ABORTED',
647
- rawOutput: stdout + '\n\n' + stderr,
648
- model,
649
- nickname: originalModel !== model ? originalModel : undefined,
558
+ currentChildRef = child;
559
+ if (child.pid) {
560
+ void registerProcess(child.pid, { agentName: agentName || '', runner: 'hermes', configPath });
561
+ void registerLiveAgent({
562
+ pid: child.pid, runner: 'hermes', agentName: agentName || '',
563
+ sessionId: currentSessionId || '',
564
+ cleanupFn: async () => { await killProcessTree(child); },
565
+ childRef: child,
650
566
  });
651
- };
652
- if (options.signal.aborted) {
653
- onAbort();
654
- }
655
- else {
656
- options.signal.addEventListener('abort', onAbort, { once: true });
657
- }
658
- }
659
- child.on('close', async (code) => {
660
- clearTimeout(timeout);
661
- if (child.pid)
662
- void updateProcessStatus(child.pid, code === 0 ? 'done' : 'failed', code, options.configPath);
663
- // Parse session ID from Hermes output (e.g. "Session: 20260515_204158_7093cd")
664
- // This works even when Hermes exits with error, as the banner is still printed
665
- let parsedSessionId = sessionId;
666
- const sessionMatch = stdout.match(/Session:\s+(\S+)/);
667
- if (sessionMatch) {
668
- parsedSessionId = sessionMatch[1];
669
- }
670
- // Hermes exits code 2 on API errors (e.g. max_tokens > 40000).
671
- // When stdout has content, return it even on non-zero exit — it's useful output.
672
- if (code !== 0 && !stdout) {
673
- return safeResolve({
674
- result: '',
675
- error: `EXIT_CODE_${code}`,
676
- rawOutput: stderr || stdout,
677
- sessionId: parsedSessionId,
678
- model,
679
- nickname: originalModel !== model ? originalModel : undefined,
567
+ child.once('exit', (code) => {
568
+ setLiveStatus(child.pid, code === 0 ? 'done' : 'failed', code ?? null);
569
+ void unregisterLiveAgent(child.pid);
680
570
  });
681
571
  }
682
- safeResolve({
683
- result: stdout.trim(),
684
- sessionId: parsedSessionId,
685
- rawOutput: stdout,
686
- model,
687
- nickname: originalModel !== model ? originalModel : undefined,
572
+ let stdout = '';
573
+ let stderr = '';
574
+ child.stdout?.on('data', (d) => {
575
+ const chunk = d.toString();
576
+ if (child.pid) {
577
+ void appendOutput(child.pid, chunk, configPath);
578
+ void appendLiveOutput(child.pid, chunk);
579
+ }
580
+ if (stdout.length + chunk.length > MAX_BUF)
581
+ stdout = stdout.slice(-MAX_BUF);
582
+ else
583
+ stdout += chunk;
584
+ if (!silent && agentName)
585
+ process.stderr.write(`[Hermes:${agentName}] ${chunk}`);
688
586
  });
587
+ child.stderr?.on('data', (d) => {
588
+ const chunk = d.toString();
589
+ if (stderr.length + chunk.length > MAX_BUF)
590
+ stderr = stderr.slice(-MAX_BUF);
591
+ else
592
+ stderr += chunk;
593
+ if (!silent && agentName)
594
+ process.stderr.write(`[Hermes:${agentName}:ERR] ${chunk}`);
595
+ });
596
+ const timer = setTimeout(() => {
597
+ if (child.stdin && !child.stdin.destroyed) {
598
+ try {
599
+ child.stdin.write('\n');
600
+ }
601
+ catch { /* ignore */ }
602
+ }
603
+ setTimeout(async () => {
604
+ await killProcessTree(child);
605
+ cleanupTmpFiles();
606
+ safeResolve({ result: '', error: 'HARD_TIMEOUT', rawOutput: stdout + stderr });
607
+ }, HARD_TIMEOUT_MS);
608
+ }, timeoutMs);
609
+ child.on('close', async (code) => {
610
+ clearTimeout(timer);
611
+ if (child.pid)
612
+ void updateProcessStatus(child.pid, code === 0 ? 'done' : 'failed', code, configPath);
613
+ const sessionMatch = stdout.match(/session_id:\s*(\S+)/i) ||
614
+ stderr.match(/session_id:\s*(\S+)/i) ||
615
+ stdout.match(/Session:\s*(\S+)/) ||
616
+ stderr.match(/Session:\s*(\S+)/);
617
+ if (sessionMatch)
618
+ currentSessionId = sessionMatch[1];
619
+ const retryable = isRetryableError(stderr) || isRetryableError(stdout);
620
+ if (code !== 0 && retryable && retryCount < maxRetries) {
621
+ retryCount++;
622
+ const ti = getTokenForIndex(retryCount);
623
+ if (!silent) {
624
+ process.stderr.write(`\n\x1b[41m\x1b[37m[NousHermesRunner] Retry ${retryCount}/${maxRetries} avec ${ti?.tokenEnvKey || 'UNKNOWN'}...\x1b[0m\n`);
625
+ }
626
+ await killProcessTree(child);
627
+ setImmediate(() => spawnHermes(ti));
628
+ return;
629
+ }
630
+ cleanupTmpFiles();
631
+ if (currentSessionId && agentName) {
632
+ await saveSessionId(agentName, currentSessionId, configPath, 'hermes');
633
+ if (child.pid)
634
+ void linkSessionToPid(currentSessionId, child.pid, configPath);
635
+ }
636
+ if (code !== 0 && !stdout.trim()) {
637
+ safeResolve({ result: '', error: `EXIT_CODE_${code}`, rawOutput: stderr || stdout, sessionId: currentSessionId });
638
+ return;
639
+ }
640
+ safeResolve({ result: stdout.trim(), sessionId: currentSessionId, rawOutput: stdout });
641
+ });
642
+ child.on('error', (err) => {
643
+ clearTimeout(timer);
644
+ killProcessTree(child).then(() => {
645
+ cleanupTmpFiles();
646
+ safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
647
+ });
648
+ });
649
+ };
650
+ options.signal?.addEventListener('abort', () => {
651
+ if (currentChildRef)
652
+ killProcessTree(currentChildRef).then(() => {
653
+ cleanupTmpFiles();
654
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
655
+ });
689
656
  });
690
- child.on('error', (err) => {
691
- clearTimeout(timeout);
692
- if (child.pid)
693
- void updateProcessStatus(child.pid, 'failed', null, options.configPath);
694
- safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
695
- });
696
- // Do NOT call child.stdin.end() — it sends EOF and Hermes closes.
697
- // Keep stdin open so Hermes stays alive for resume.
657
+ spawnHermes(getTokenForIndex(0));
698
658
  });
699
659
  }
700
660
  }
701
- /**
702
- * Run agent with proper cleanup and telemetry
703
- */
704
- async function runAgentWrapper(options) {
705
- try {
706
- const result = await withSpan('hermes.runAgent', async (span) => {
707
- span.setAttribute('agentName', options.agentName || '');
708
- span.setAttribute('model', options.model || '');
709
- span.setAttribute('runner', 'hermes');
710
- return await this.runAgentInternal(options);
711
- }, {
712
- agentName: options.agentName || '',
713
- model: options.model || '',
714
- runner: 'hermes',
715
- });
716
- // Cleanup on success
717
- this.cleanupTempFiles();
718
- // Save session if needed
719
- if (options.agentName && result.sessionId) {
720
- await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
721
- }
722
- return result;
723
- }
724
- catch (error) {
725
- // Cleanup on error
726
- this.cleanupTempFiles();
727
- logger.error({
728
- error: error instanceof Error ? error.message : String(error),
729
- agentName: options.agentName,
730
- }, 'Hermes runner failed');
731
- throw error;
732
- }
733
- }
734
661
  //# sourceMappingURL=NousHermesRunner.js.map