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.
- package/.mcp.json.example +20 -20
- package/README.md +143 -143
- package/bin/overmind-pool.mjs +248 -248
- package/dist/bin/cli.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/orchestration/dispatcher.d.ts.map +1 -1
- package/dist/lib/orchestration/dispatcher.js +0 -1
- package/dist/lib/orchestration/dispatcher.js.map +1 -1
- package/dist/services/AntigravityRunner.d.ts +42 -0
- package/dist/services/AntigravityRunner.d.ts.map +1 -0
- package/dist/services/AntigravityRunner.js +404 -0
- package/dist/services/AntigravityRunner.js.map +1 -0
- package/dist/services/ClaudeRunner.d.ts.map +1 -1
- package/dist/services/ClaudeRunner.js.map +1 -1
- package/dist/services/GeminiRunner.d.ts +22 -0
- package/dist/services/GeminiRunner.d.ts.map +1 -1
- package/dist/services/GeminiRunner.js +115 -91
- package/dist/services/GeminiRunner.js.map +1 -1
- package/dist/services/KiloRunner.d.ts.map +1 -1
- package/dist/services/KiloRunner.js +6 -1
- package/dist/services/KiloRunner.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts +1 -0
- package/dist/services/NousHermesRunner.d.ts.map +1 -1
- package/dist/services/NousHermesRunner.js +366 -555
- package/dist/services/NousHermesRunner.js.map +1 -1
- package/dist/tools/config_example.d.ts +1 -0
- package/dist/tools/config_example.d.ts.map +1 -1
- package/dist/tools/config_example.js +45 -2
- package/dist/tools/config_example.js.map +1 -1
- package/dist/tools/manage_agents.d.ts +1 -1
- package/dist/tools/run_agent.d.ts +1 -0
- package/dist/tools/run_agent.d.ts.map +1 -1
- package/dist/tools/run_agent.js +27 -3
- package/dist/tools/run_agent.js.map +1 -1
- package/dist/tools/run_agents_parallel.d.ts +1 -0
- package/dist/tools/run_agents_parallel.d.ts.map +1 -1
- package/dist/tools/run_gemini.d.ts +13 -0
- package/dist/tools/run_gemini.d.ts.map +1 -1
- package/dist/tools/run_gemini.js +6 -2
- package/dist/tools/run_gemini.js.map +1 -1
- package/docs/agent-http-tutorial.md +524 -524
- package/docs/provider-config-map.md +379 -0
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
child.kill('SIGKILL');
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// Ignored
|
|
39
|
-
}
|
|
38
|
+
else {
|
|
39
|
+
try {
|
|
40
|
+
child.kill('SIGTERM');
|
|
40
41
|
}
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
194
|
+
// Auto Resume
|
|
162
195
|
if (autoResume && agentName && !sessionId) {
|
|
163
|
-
const lastId = await getLastSessionId(agentName,
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
273
|
+
const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
|
|
424
274
|
const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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]
|
|
371
|
+
console.error(`[NousHermesRunner] MCP config.yaml written to ${yamlPath}`);
|
|
513
372
|
}
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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
|