overmind-mcp 2.8.3 → 2.8.6

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 (46) hide show
  1. package/.mcp.json.example +20 -20
  2. package/README.md +143 -143
  3. package/bin/overmind-pool.mjs +248 -248
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/lib/config.d.ts +1 -0
  6. package/dist/lib/config.d.ts.map +1 -1
  7. package/dist/lib/config.js +2 -1
  8. package/dist/lib/config.js.map +1 -1
  9. package/dist/lib/orchestration/dispatcher.d.ts.map +1 -1
  10. package/dist/lib/orchestration/dispatcher.js +0 -1
  11. package/dist/lib/orchestration/dispatcher.js.map +1 -1
  12. package/dist/services/AntigravityRunner.d.ts +42 -0
  13. package/dist/services/AntigravityRunner.d.ts.map +1 -0
  14. package/dist/services/AntigravityRunner.js +404 -0
  15. package/dist/services/AntigravityRunner.js.map +1 -0
  16. package/dist/services/ClaudeRunner.d.ts.map +1 -1
  17. package/dist/services/ClaudeRunner.js.map +1 -1
  18. package/dist/services/GeminiRunner.d.ts +22 -0
  19. package/dist/services/GeminiRunner.d.ts.map +1 -1
  20. package/dist/services/GeminiRunner.js +115 -91
  21. package/dist/services/GeminiRunner.js.map +1 -1
  22. package/dist/services/KiloRunner.d.ts.map +1 -1
  23. package/dist/services/KiloRunner.js +6 -1
  24. package/dist/services/KiloRunner.js.map +1 -1
  25. package/dist/services/NousHermesRunner.d.ts +1 -0
  26. package/dist/services/NousHermesRunner.d.ts.map +1 -1
  27. package/dist/services/NousHermesRunner.js +366 -555
  28. package/dist/services/NousHermesRunner.js.map +1 -1
  29. package/dist/tools/config_example.d.ts +1 -0
  30. package/dist/tools/config_example.d.ts.map +1 -1
  31. package/dist/tools/config_example.js +45 -2
  32. package/dist/tools/config_example.js.map +1 -1
  33. package/dist/tools/manage_agents.d.ts +1 -1
  34. package/dist/tools/run_agent.d.ts +1 -0
  35. package/dist/tools/run_agent.d.ts.map +1 -1
  36. package/dist/tools/run_agent.js +27 -3
  37. package/dist/tools/run_agent.js.map +1 -1
  38. package/dist/tools/run_agents_parallel.d.ts +1 -0
  39. package/dist/tools/run_agents_parallel.d.ts.map +1 -1
  40. package/dist/tools/run_gemini.d.ts +13 -0
  41. package/dist/tools/run_gemini.d.ts.map +1 -1
  42. package/dist/tools/run_gemini.js +6 -2
  43. package/dist/tools/run_gemini.js.map +1 -1
  44. package/docs/agent-http-tutorial.md +524 -524
  45. package/docs/provider-config-map.md +379 -0
  46. package/package.json +1 -1
@@ -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,386 @@ 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
+ // Load agent settings + MCP config (same pattern as ClaudeRunner)
207
+ let systemPrompt = '';
208
+ let resolvedModel;
209
+ let resolvedProvider;
168
210
  const agentCustomEnv = {
169
211
  ...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
212
+ PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', PYTHONUNBUFFERED: '1',
213
+ PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
214
+ PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
179
215
  OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || process.env.OVERMIND_EMBEDDING_KEY,
180
- // Map NVIDIA NIM key
181
216
  NVIDIA_API_KEY: process.env.NVIDIA_API_KEY || process.env.NVAPI_KEY,
182
217
  NVIDIA_API_BASE: process.env.NVIDIA_API_BASE || 'https://integrate.api.nvidia.com/v1',
183
218
  ...(agentName ? { OVERMIND_AGENT_NAME: agentName } : {}),
184
219
  };
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
- }
220
+ let tmpSettingsPath = null;
221
+ let tmpMcpPath = null;
222
+ if (agentName) {
223
+ const settingsDir = path.dirname(CONFIG.HERMES.PATHS.SETTINGS);
224
+ const agentSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`), configPath);
225
+ if (!fs.existsSync(agentSettingsPath)) {
226
+ return { result: '', error: `INVALID_AGENT: Agent Hermes "${agentName}" non trouvé.` };
229
227
  }
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}`);
228
+ const settings = interpolateEnvVars(JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8')));
229
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
+ const s = settings;
231
+ tmpSettingsPath = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
232
+ fs.writeFileSync(tmpSettingsPath, JSON.stringify(s, null, 2), 'utf8');
233
+ if (!options.model && typeof s.model === 'string')
234
+ resolvedModel = s.model;
235
+ if (!options.model && s.env?.ANTHROPIC_MODEL && !String(s.env.ANTHROPIC_MODEL).startsWith('$')) {
236
+ resolvedModel = s.env.ANTHROPIC_MODEL;
239
237
  }
240
- };
241
- let systemPrompt = '';
242
- if (agentName) {
243
- 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;
238
+ if (!options.provider && s.env?.ANTHROPIC_PROVIDER && !String(s.env.ANTHROPIC_PROVIDER).startsWith('$')) {
239
+ resolvedProvider = s.env.ANTHROPIC_PROVIDER;
240
+ }
241
+ if (s.env) {
242
+ for (const [k, v] of Object.entries(s.env)) {
243
+ if (typeof v === 'string')
244
+ agentCustomEnv[k] = v;
288
245
  }
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];
326
- }
327
- }
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
- }
353
- }
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`);
246
+ }
247
+ const agentPromptPath = resolveConfigPath(path.join(settingsDir, 'agents', `${agentName}.md`), configPath);
248
+ if (fs.existsSync(agentPromptPath)) {
249
+ systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
250
+ }
251
+ // MCP config filtered by enabledMcpjsonServers
252
+ const agentMcpPath = resolveConfigPath(path.join(settingsDir, `.mcp.${agentName}.json`), configPath);
253
+ if (fs.existsSync(agentMcpPath)) {
254
+ try {
255
+ const mcpConfig = interpolateEnvVars(JSON.parse(fs.readFileSync(agentMcpPath, 'utf8')));
256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
+ const mc = mcpConfig;
258
+ const filteredMcp = { mcpServers: {} };
259
+ const enabled = s.enabledMcpjsonServers || [];
260
+ for (const sn of enabled) {
261
+ if (mc.mcpServers?.[sn]) {
262
+ filteredMcp.mcpServers[sn] = mc.mcpServers[sn];
362
263
  }
363
264
  }
265
+ tmpMcpPath = path.join(path.dirname(agentMcpPath), `mcp_${agentName}_tmp.json`);
266
+ fs.writeFileSync(tmpMcpPath, JSON.stringify(filteredMcp, null, 2), 'utf8');
364
267
  }
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`;
400
- }
401
- }
402
- if (s.url)
403
- yamlContent += ` url: "${s.url}"\n`;
404
- }
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
- }
268
+ catch (e) {
269
+ console.error(`[NousHermesRunner] MCP config error: ${e}`);
415
270
  }
416
271
  }
417
- 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');
421
- }
422
272
  }
423
- // --- CLI Arguments & Prompt Handling ---
273
+ const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
424
274
  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);
275
+ const cliPrompt = finalPrompt.length > 7000 ? finalPrompt.substring(0, 7000) : finalPrompt;
276
+ // Build CLI args: chat -q (persistent session, NOT -z oneshot)
277
+ // -z + --resume doesn't work — resume is ignored in oneshot mode
278
+ const cleanArgs = ['chat', '-q', cliPrompt, '-Q'];
279
+ cleanArgs.push('--model', finalModel);
280
+ if (options.provider || resolvedProvider) {
281
+ cleanArgs.push('--provider', options.provider || resolvedProvider);
431
282
  }
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;
283
+ if (sessionId)
284
+ cleanArgs.push('--resume', sessionId);
285
+ // Token fallback setup (same as ClaudeRunner)
286
+ const FALLBACK_KEYS = ['AUTH_FALLBACK_1', 'AUTH_FALLBACK_2', 'AUTH_FALLBACK_3'];
287
+ const TOKEN_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_AUTH_TOKEN_E', 'GLM_API_KEY', 'Z_AI_API_KEY'];
288
+ const getAvailableFallbacks = () => {
289
+ const fb = [];
290
+ for (const k of FALLBACK_KEYS) {
291
+ const v = agentCustomEnv[k];
292
+ if (v && typeof v === 'string' && v.length > 0)
293
+ fb.push({ key: k, value: v });
294
+ }
295
+ return fb;
296
+ };
297
+ const getTokenForIndex = (idx) => {
298
+ if (idx === 0) {
299
+ for (const tk of TOKEN_KEYS) {
300
+ const v = agentCustomEnv[tk];
301
+ if (v && typeof v === 'string' && v.length > 0)
302
+ return { tokenEnvKey: tk, tokenValue: v };
303
+ }
304
+ return null;
305
+ }
306
+ const fb = getAvailableFallbacks();
307
+ return fb[idx - 1] ? { tokenEnvKey: fb[idx - 1].key, tokenValue: fb[idx - 1].value } : null;
308
+ };
309
+ const isRetryableError = (stderr) => {
310
+ const lower = stderr.toLowerCase();
311
+ return lower.includes('401') || lower.includes('unauthorized') ||
312
+ lower.includes('invalid api key') || lower.includes('authentication failed') ||
313
+ lower.includes('invalid authentication') || lower.includes('429') ||
314
+ lower.includes('rate limit') || lower.includes('quota exhausted') ||
315
+ lower.includes('limit exhausted') || lower.includes('503') ||
316
+ lower.includes('service unavailable') || lower.includes('500') ||
317
+ lower.includes('internal server error');
318
+ };
319
+ // HERMES_HOME setup
320
+ const overmindHermesPath = path.resolve(cwd, '.overmind', 'hermes', agentName ? `agent_${agentName}` : 'central');
321
+ const overmindHermesSubPath = path.join(overmindHermesPath, '.hermes');
322
+ if (!fs.existsSync(overmindHermesSubPath))
323
+ fs.mkdirSync(overmindHermesSubPath, { recursive: true });
324
+ agentCustomEnv.HERMES_HOME = overmindHermesSubPath;
325
+ if (process.platform === 'win32')
326
+ agentCustomEnv.USERPROFILE = overmindHermesPath;
327
+ else
328
+ agentCustomEnv.HOME = overmindHermesPath;
329
+ // Write .env to HERMES_HOME (credential auto-discovery)
330
+ const credRegex = /(?:api_key|auth_token|base_url|endpoint|url)$/i;
331
+ const dotEntries = [];
332
+ for (const [k, v] of Object.entries(agentCustomEnv)) {
333
+ if (typeof v === 'string' && v.length > 0 && credRegex.test(k))
334
+ dotEntries.push(`${k}=${v}`);
474
335
  }
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
496
- 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`);
507
- }
508
- }
336
+ if (dotEntries.length > 0) {
337
+ const dotPath = path.join(overmindHermesSubPath, '.env');
338
+ const existing = fs.existsSync(dotPath) ? fs.readFileSync(dotPath, 'utf8') : '';
339
+ fs.writeFileSync(dotPath, dotEntries.join('\n') + '\n' + existing, 'utf8');
340
+ }
341
+ // Generate config.yaml in HERMES_HOME (MCP servers)
342
+ if (tmpMcpPath && fs.existsSync(tmpMcpPath)) {
343
+ try {
344
+ const mc = JSON.parse(fs.readFileSync(tmpMcpPath, 'utf8'));
345
+ const yamlPath = path.join(overmindHermesSubPath, 'config.yaml');
346
+ // Preserve existing config.yaml (tts, llm, etc.) — merge mcp_servers only
347
+ let existingYaml = '';
348
+ if (fs.existsSync(yamlPath)) {
349
+ existingYaml = fs.readFileSync(yamlPath, 'utf8');
350
+ }
351
+ // Build new mcp_servers section
352
+ let newMcpSection = 'mcp_servers:\n';
353
+ for (const [name, srv] of Object.entries(mc.mcpServers || {})) {
354
+ const s = srv;
355
+ newMcpSection += ` ${name}:\n`;
356
+ if (s.url)
357
+ newMcpSection += ` url: "${s.url}"\n`;
358
+ if (s.command)
359
+ newMcpSection += ` command: "${s.command}"\n`;
509
360
  }
510
- catch (e) { /* non-critical */ }
361
+ // Merge: replace mcp_servers block in existing yaml or append
362
+ let finalYaml;
363
+ if (existingYaml.includes('mcp_servers:')) {
364
+ finalYaml = existingYaml.replace(/mcp_servers:\n([\s\S]*?)(?=\n\w|\n$|$)/, newMcpSection.trimEnd() + '\n');
365
+ }
366
+ else {
367
+ finalYaml = existingYaml.trimEnd() + '\n' + newMcpSection;
368
+ }
369
+ fs.writeFileSync(yamlPath, finalYaml, 'utf8');
511
370
  if (!silent)
512
- console.error(`[NousHermesRunner] Set ANTHROPIC_AUTH_TOKEN_4 for MiniMax via minimax-cn`);
371
+ console.error(`[NousHermesRunner] MCP config.yaml written to ${yamlPath}`);
513
372
  }
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;
373
+ catch (e) {
374
+ console.error(`[NousHermesRunner] config.yaml error: ${e}`);
529
375
  }
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
- }
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');
555
- }
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) {
565
- cleanArgs.push('--resume', sessionId);
566
- }
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);
573
- }
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');
581
376
  }
377
+ // AbortSignal
378
+ if (options.signal?.aborted)
379
+ return Promise.reject(new Error('ABORTED'));
380
+ let currentChildRef = null;
582
381
  return new Promise((resolve) => {
583
382
  let resolved = false;
584
- const safeResolve = (value) => {
585
- if (!resolved) {
586
- resolved = true;
587
- resolve(value);
383
+ let retryCount = 0;
384
+ const maxRetries = getAvailableFallbacks().length + 1;
385
+ let currentSessionId = sessionId;
386
+ const safeResolve = (v) => { if (!resolved) {
387
+ resolved = true;
388
+ resolve(v);
389
+ } };
390
+ const cleanupTmpFiles = () => {
391
+ for (const f of [tmpSettingsPath, tmpMcpPath]) {
392
+ if (f && fs.existsSync(f)) {
393
+ try {
394
+ fs.unlinkSync(f);
395
+ }
396
+ catch { /* ignored */ }
397
+ }
588
398
  }
589
399
  };
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);
400
+ const writeAuthJson = (tokenInfo) => {
401
+ if (!tokenInfo || !overmindHermesSubPath)
402
+ return;
403
+ try {
404
+ const authPath = path.join(overmindHermesSubPath, 'auth.json');
405
+ const auth = { version: 1, providers: {}, credential_pool: {} };
406
+ if (fs.existsSync(authPath))
407
+ Object.assign(auth, JSON.parse(fs.readFileSync(authPath, 'utf8')));
408
+ if (!auth.credential_pool)
409
+ auth.credential_pool = {};
410
+ const cp = auth.credential_pool;
411
+ cp['zai'] = [{
412
+ id: 'zai-default', label: tokenInfo.tokenEnvKey, auth_type: 'api_key',
413
+ priority: 0, source: `env:${tokenInfo.tokenEnvKey}`, access_token: tokenInfo.tokenValue,
414
+ last_status: null, last_error_code: null,
415
+ base_url: agentCustomEnv['GLM_BASE_URL'] || 'https://api.z.ai/api/coding/paas/v4',
416
+ request_count: 0,
417
+ }];
418
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
619
419
  }
620
- stderr += chunk;
621
- if (!silent) {
622
- process.stderr.write(`[Hermes:ERR] ${chunk}`);
420
+ catch (_e) { /* non-critical */ }
421
+ };
422
+ const spawnHermes = async (tokenInfo) => {
423
+ const spawnEnv = { ...process.env, ...agentCustomEnv };
424
+ if (tokenInfo) {
425
+ for (const tk of TOKEN_KEYS)
426
+ delete spawnEnv[tk];
427
+ let resolvedToken = tokenInfo.tokenValue;
428
+ if (resolvedToken.startsWith('$'))
429
+ resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
430
+ spawnEnv[tokenInfo.tokenEnvKey] = resolvedToken;
623
431
  }
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,
432
+ writeAuthJson(tokenInfo);
433
+ const hermesBin = await findHermesBinary();
434
+ const child = spawn(hermesBin, cleanArgs, {
435
+ cwd, shell: false, windowsHide: true,
436
+ env: {
437
+ ...spawnEnv,
438
+ HERMES_HOME: overmindHermesSubPath,
439
+ VIRTUAL_ENV: process.env.HERMES_AGENT_ROOT
440
+ ? path.join(process.env.HERMES_AGENT_ROOT, 'venv')
441
+ : path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv'),
442
+ PATH: `${process.env.HERMES_AGENT_ROOT || path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv')};${process.env.PATH || ''}`,
443
+ },
635
444
  });
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,
445
+ currentChildRef = child;
446
+ if (child.pid) {
447
+ void registerProcess(child.pid, { agentName: agentName || '', runner: 'hermes', configPath });
448
+ void registerLiveAgent({
449
+ pid: child.pid, runner: 'hermes', agentName: agentName || '',
450
+ sessionId: currentSessionId || '',
451
+ cleanupFn: async () => { await killProcessTree(child); },
452
+ childRef: child,
650
453
  });
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,
454
+ child.once('exit', (code) => {
455
+ setLiveStatus(child.pid, code === 0 ? 'done' : 'failed', code ?? null);
456
+ void unregisterLiveAgent(child.pid);
680
457
  });
681
458
  }
682
- safeResolve({
683
- result: stdout.trim(),
684
- sessionId: parsedSessionId,
685
- rawOutput: stdout,
686
- model,
687
- nickname: originalModel !== model ? originalModel : undefined,
459
+ let stdout = '';
460
+ let stderr = '';
461
+ child.stdout?.on('data', (d) => {
462
+ const chunk = d.toString();
463
+ if (child.pid) {
464
+ void appendOutput(child.pid, chunk, configPath);
465
+ void appendLiveOutput(child.pid, chunk);
466
+ }
467
+ if (stdout.length + chunk.length > MAX_BUF)
468
+ stdout = stdout.slice(-MAX_BUF);
469
+ else
470
+ stdout += chunk;
471
+ if (!silent && agentName)
472
+ process.stderr.write(`[Hermes:${agentName}] ${chunk}`);
688
473
  });
474
+ child.stderr?.on('data', (d) => {
475
+ const chunk = d.toString();
476
+ if (stderr.length + chunk.length > MAX_BUF)
477
+ stderr = stderr.slice(-MAX_BUF);
478
+ else
479
+ stderr += chunk;
480
+ if (!silent && agentName)
481
+ process.stderr.write(`[Hermes:${agentName}:ERR] ${chunk}`);
482
+ });
483
+ const timer = setTimeout(() => {
484
+ if (child.stdin && !child.stdin.destroyed) {
485
+ try {
486
+ child.stdin.write('\n');
487
+ }
488
+ catch { /* ignore */ }
489
+ }
490
+ setTimeout(async () => {
491
+ await killProcessTree(child);
492
+ cleanupTmpFiles();
493
+ safeResolve({ result: '', error: 'HARD_TIMEOUT', rawOutput: stdout + stderr });
494
+ }, HARD_TIMEOUT_MS);
495
+ }, timeoutMs);
496
+ child.on('close', async (code) => {
497
+ clearTimeout(timer);
498
+ if (child.pid)
499
+ void updateProcessStatus(child.pid, code === 0 ? 'done' : 'failed', code, configPath);
500
+ const sessionMatch = stdout.match(/Session:\s+(\S+)/);
501
+ if (sessionMatch)
502
+ currentSessionId = sessionMatch[1];
503
+ const retryable = isRetryableError(stderr) || isRetryableError(stdout);
504
+ if (code !== 0 && retryable && retryCount < maxRetries) {
505
+ retryCount++;
506
+ const ti = getTokenForIndex(retryCount);
507
+ if (!silent) {
508
+ process.stderr.write(`\n\x1b[41m\x1b[37m[NousHermesRunner] Retry ${retryCount}/${maxRetries} avec ${ti?.tokenEnvKey || 'UNKNOWN'}...\x1b[0m\n`);
509
+ }
510
+ await killProcessTree(child);
511
+ setImmediate(() => spawnHermes(ti));
512
+ return;
513
+ }
514
+ cleanupTmpFiles();
515
+ if (currentSessionId && agentName) {
516
+ await saveSessionId(agentName, currentSessionId, configPath, 'hermes');
517
+ if (child.pid)
518
+ void linkSessionToPid(currentSessionId, child.pid, configPath);
519
+ }
520
+ if (code !== 0 && !stdout.trim()) {
521
+ safeResolve({ result: '', error: `EXIT_CODE_${code}`, rawOutput: stderr || stdout, sessionId: currentSessionId });
522
+ return;
523
+ }
524
+ safeResolve({ result: stdout.trim(), sessionId: currentSessionId, rawOutput: stdout });
525
+ });
526
+ child.on('error', (err) => {
527
+ clearTimeout(timer);
528
+ killProcessTree(child).then(() => {
529
+ cleanupTmpFiles();
530
+ safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
531
+ });
532
+ });
533
+ };
534
+ options.signal?.addEventListener('abort', () => {
535
+ if (currentChildRef)
536
+ killProcessTree(currentChildRef).then(() => {
537
+ cleanupTmpFiles();
538
+ safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
539
+ });
689
540
  });
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.
541
+ spawnHermes(getTokenForIndex(0));
698
542
  });
699
543
  }
700
544
  }
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
545
  //# sourceMappingURL=NousHermesRunner.js.map