overmind-mcp 2.8.43 → 2.8.45
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/dist/bin/launch.js +78 -0
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/services/AgentManager.d.ts +5 -5
- package/dist/services/AgentManager.d.ts.map +1 -1
- package/dist/services/AgentManager.js +174 -270
- package/dist/services/AgentManager.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts +0 -16
- package/dist/services/NousHermesRunner.d.ts.map +1 -1
- package/dist/services/NousHermesRunner.js +407 -150
- package/dist/services/NousHermesRunner.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import { exec } from 'child_process';
|
|
4
|
+
import { exec, execSync } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { CONFIG, resolveConfigPath, getWorkspaceDir, getAgentHermesHome, getAgentOvermindHome, getSharedHermesHome } from '../lib/config.js';
|
|
7
7
|
import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
|
|
@@ -18,34 +18,55 @@ const logger = pino({ name: 'NousHermesRunner' });
|
|
|
18
18
|
// orphelin. On utilise taskkill /F /T pour propager le kill au sous-arbre complet.
|
|
19
19
|
const killProcessTree = (child) => {
|
|
20
20
|
return new Promise((resolve) => {
|
|
21
|
-
if (!child
|
|
21
|
+
if (!child) {
|
|
22
|
+
logger.debug('[KILL] No child process reference provided.');
|
|
22
23
|
resolve();
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
26
|
+
if (child.exitCode !== null || child.killed) {
|
|
27
|
+
logger.debug({ pid: child.pid, exitCode: child.exitCode, killed: child.killed }, '[KILL] Process is already dead or marked killed.');
|
|
28
|
+
resolve();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
logger.info({ pid: child.pid }, '[KILL] Initiating process tree termination...');
|
|
25
32
|
let settled = false;
|
|
26
33
|
const finish = () => {
|
|
27
34
|
if (settled)
|
|
28
35
|
return;
|
|
29
36
|
settled = true;
|
|
37
|
+
logger.debug({ pid: child.pid }, '[KILL] Process tree termination sequence completed.');
|
|
30
38
|
resolve();
|
|
31
39
|
};
|
|
32
40
|
child.once('exit', finish);
|
|
33
|
-
if (process.platform === 'win32' && child.pid) {
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
if (process.platform === 'win32' && child.pid && typeof child.pid === 'number' && child.pid > 0) {
|
|
42
|
+
const cmd = `taskkill /F /T /PID ${child.pid}`;
|
|
43
|
+
logger.debug({ cmd }, '[KILL] Executing Windows taskkill...');
|
|
44
|
+
exec(cmd, (err, stdout, stderr) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
logger.debug({ err, stderr }, '[KILL] taskkill failed or process was already dead.');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
logger.debug({ stdout }, '[KILL] taskkill completed successfully.');
|
|
50
|
+
}
|
|
36
51
|
});
|
|
37
52
|
}
|
|
38
53
|
else {
|
|
39
54
|
try {
|
|
55
|
+
logger.debug({ pid: child.pid }, '[KILL] Dispatched SIGTERM signal.');
|
|
40
56
|
child.kill('SIGTERM');
|
|
41
57
|
}
|
|
42
|
-
catch {
|
|
58
|
+
catch (e) {
|
|
59
|
+
logger.debug({ pid: child.pid, error: e }, '[KILL] SIGTERM dispatch failed.');
|
|
60
|
+
}
|
|
43
61
|
setTimeout(() => {
|
|
44
62
|
if (child.exitCode === null && !child.killed) {
|
|
45
63
|
try {
|
|
64
|
+
logger.warn({ pid: child.pid }, '[KILL] SIGTERM ignored. Escalating to SIGKILL...');
|
|
46
65
|
child.kill('SIGKILL');
|
|
47
66
|
}
|
|
48
|
-
catch {
|
|
67
|
+
catch (e) {
|
|
68
|
+
logger.debug({ pid: child.pid, error: e }, '[KILL] SIGKILL dispatch failed.');
|
|
69
|
+
}
|
|
49
70
|
}
|
|
50
71
|
}, 2000);
|
|
51
72
|
}
|
|
@@ -95,30 +116,33 @@ async function findHermesBinary() {
|
|
|
95
116
|
// Not found in PATH
|
|
96
117
|
}
|
|
97
118
|
// 3. Platform-specific paths
|
|
98
|
-
const platformPaths =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const platformPaths = [];
|
|
120
|
+
if (isWin) {
|
|
121
|
+
platformPaths.push(
|
|
122
|
+
// Hermes venv (Nous Research install) — PRIORITÉ haute (v0.13.0, supporte -z)
|
|
123
|
+
path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
|
|
124
|
+
// Officiel installer Windows (install.ps1) — chemin natif
|
|
125
|
+
path.join(process.env.LOCALAPPDATA || '', 'hermes', 'bin', 'hermes.exe'), path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes.exe'));
|
|
126
|
+
// Dynamically scan for Python versions in LOCALAPPDATA
|
|
127
|
+
const programsPython = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python');
|
|
128
|
+
if (fs.existsSync(programsPython)) {
|
|
129
|
+
try {
|
|
130
|
+
const dirs = fs.readdirSync(programsPython);
|
|
131
|
+
for (const dir of dirs) {
|
|
132
|
+
if (dir.toLowerCase().startsWith('python')) {
|
|
133
|
+
platformPaths.push(path.join(programsPython, dir, 'Scripts', 'hermes.exe'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignored */ }
|
|
138
|
+
}
|
|
139
|
+
platformPaths.push(
|
|
140
|
+
// Fallback installations via pip (legacy)
|
|
141
|
+
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python312', 'Scripts', 'hermes.exe'), path.join(process.env.APPDATA || '', 'Python', 'Python312', 'Scripts', 'hermes.exe'), path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Python', 'Python311', 'Scripts', 'hermes.exe'), path.join(process.env.APPDATA || '', 'Python', 'Python311', 'Scripts', 'hermes.exe'), 'C:\\Python312\\Scripts\\hermes.exe', 'C:\\Python311\\Scripts\\hermes.exe', 'C:\\Program Files\\Hermes\\hermes.exe');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
platformPaths.push(path.join(process.env.HOME || '', '.local', 'bin', 'hermes'), path.join(process.env.HOME || '', 'miniconda3', 'bin', 'hermes'), path.join(process.env.HOME || '', 'anaconda3', 'bin', 'hermes'), '/usr/local/bin/hermes', '/usr/bin/hermes', '/opt/homebrew/bin/hermes');
|
|
145
|
+
}
|
|
122
146
|
for (const p of platformPaths) {
|
|
123
147
|
if (fs.existsSync(p)) {
|
|
124
148
|
logger.info({ path: p }, 'Found hermes at platform path');
|
|
@@ -148,21 +172,111 @@ async function findHermesBinary() {
|
|
|
148
172
|
return 'hermes';
|
|
149
173
|
}
|
|
150
174
|
/**
|
|
151
|
-
* NousHermesRunner — Runner
|
|
175
|
+
* NousHermesRunner — Runner Hermes Agent pour Overmind (v2.8.45+).
|
|
176
|
+
*
|
|
177
|
+
* ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
178
|
+
* ║ ⭐ ARCHITECTURE CREDENTIALS HERMES — LIRE CECI EN PREMIER ║
|
|
179
|
+
* ╠══════════════════════════════════════════════════════════════════════════════╣
|
|
180
|
+
* ║ ║
|
|
181
|
+
* ║ Les credentials Hermes sont dans LE DOSSIER NATIF HERMES UNIQUEMENT : ║
|
|
182
|
+
* ║ ║
|
|
183
|
+
* ║ <HERMES_HOME>/agents/<name>/settings.json ║
|
|
184
|
+
* ║ ║
|
|
185
|
+
* ║ Sur Linux (npm -g) : /home/demon/.overmind/hermes/agents/<name>/ ║
|
|
186
|
+
* ║ Sur Windows (dev) : Workflow/.overmind/hermes/agents/<name>/ ║
|
|
187
|
+
* ║ ║
|
|
188
|
+
* ║ ❌ .claude/settings_<name>.json → CLAUDE CODE / KILO SEULEMENT ║
|
|
189
|
+
* ║ ❌ NousHermesRunner NE LIT JAMAIS depuis .claude/ ║
|
|
190
|
+
* ║ ❌ Ne pas éditer hermes/agents/<name>/settings.json manuellement ║
|
|
191
|
+
* ║ entre les runs — le runner le met à jour automatiquement. ║
|
|
192
|
+
* ║ ║
|
|
193
|
+
* ║ FORMAT du settings.json Hermes : ║
|
|
194
|
+
* ║ { ║
|
|
195
|
+
* ║ "env": { ║
|
|
196
|
+
* ║ "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ← token MiniMax / Z.AI ║
|
|
197
|
+
* ║ "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ║
|
|
198
|
+
* ║ "ANTHROPIC_MODEL": "MiniMax-M3", ║
|
|
199
|
+
* ║ "ANTHROPIC_PROVIDER": "minimax-cn", ║
|
|
200
|
+
* ║ "MINIMAX_CN_API_KEY": "sk-cp-...", ← injecté auto par le runner ║
|
|
201
|
+
* ║ "MINIMAX_CN_BASE_URL": "https://api.minimaxi.com/anthropic" ║
|
|
202
|
+
* ║ }, ║
|
|
203
|
+
* ║ "enableAllProjectMcpServers": false, ║
|
|
204
|
+
* ║ "enabledMcpjsonServers": ["memory", "discord", "postgres"], ║
|
|
205
|
+
* ║ "agent": "<name>", ║
|
|
206
|
+
* ║ "runner": "hermes" ║
|
|
207
|
+
* ║ } ║
|
|
208
|
+
* ║ ║
|
|
209
|
+
* ║ Supporte l'interpolation $VAR (ex: "$ANTHROPIC_AUTH_TOKEN_1") ║
|
|
210
|
+
* ║ résolue depuis process.env au moment du spawn. ║
|
|
211
|
+
* ║ ║
|
|
212
|
+
* ║ HERMES_HOME résolu dans l'ordre : ║
|
|
213
|
+
* ║ 1. OVERMIND_HERMES_HOME (env var explicite, ex: systemd EnvironmentFile) ║
|
|
214
|
+
* ║ 2. <OVERMIND_WORKSPACE>/.overmind/hermes/ (dev local) ║
|
|
215
|
+
* ║ 3. ~/.overmind/hermes/ (Linux) / %LOCALAPPDATA%/overmind/hermes/ (Win) ║
|
|
216
|
+
* ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
152
217
|
*
|
|
153
|
-
* • Providers :
|
|
154
|
-
* •
|
|
155
|
-
* •
|
|
156
|
-
* •
|
|
157
|
-
*
|
|
158
|
-
* • CN/GLOBAL disambiguation for sk-cp-* via ANTHROPIC_BASE_URL (URL wins)
|
|
159
|
-
* • OVERMIND_MINIMAX_DEFAULT=cn|global|auto for setups where all MiniMax tokens are CN
|
|
160
|
-
* • HERMES_HOME resolved via getAgentHermesHome() — deterministic across cwd (2.8.27+)
|
|
161
|
-
* Priority: OVERMIND_AGENT_HOME > legacy workspace > $HOME/.overmind/hermes/agent_<name>/
|
|
162
|
-
* • auth.json credential_pool is PRUNED every run (keep version+oauth, drop stale creds)
|
|
163
|
-
* to prevent Hermes from picking an exhausted bucket from a previous provider config
|
|
164
|
-
* • HOME/USERPROFILE propagated to spawned Hermes so ~/.hermes lookups resolve canonically
|
|
218
|
+
* • Providers supportés : MiniMax CN/GLOBAL, Z.AI/GLM, Mistral, OpenAI, NVIDIA NIM
|
|
219
|
+
* • OpenRouter = embeddings UNIQUEMENT (bloqué pour LLM inference)
|
|
220
|
+
* • auth.json purgé à chaque run (évite les credentials stale de l'ancien provider)
|
|
221
|
+
* • HOME/USERPROFILE propagé au process Hermes pour résolution ~/.hermes canonique
|
|
222
|
+
* • Voir hermesTokenResolver.ts pour le 3-pass token detection (sk-cp-/32hex/sk-ant-)
|
|
165
223
|
*/
|
|
224
|
+
function filterConfigYaml(sourceYamlPath, allowedServers) {
|
|
225
|
+
try {
|
|
226
|
+
if (!fs.existsSync(sourceYamlPath))
|
|
227
|
+
return 'mcp_servers: {}\n';
|
|
228
|
+
const yamlText = fs.readFileSync(sourceYamlPath, 'utf8');
|
|
229
|
+
const lines = yamlText.split(/\r?\n/);
|
|
230
|
+
let inMcpServers = false;
|
|
231
|
+
let currentServerName = '';
|
|
232
|
+
const serverBlocks = {};
|
|
233
|
+
const headerLines = [];
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
|
|
237
|
+
inMcpServers = true;
|
|
238
|
+
headerLines.push(line);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (inMcpServers) {
|
|
242
|
+
const indentMatch = line.match(/^(\s+)/);
|
|
243
|
+
if (!indentMatch) {
|
|
244
|
+
inMcpServers = false;
|
|
245
|
+
headerLines.push(line);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const indent = indentMatch[1].length;
|
|
249
|
+
const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
|
|
250
|
+
if (keyMatch && indent === 2) {
|
|
251
|
+
currentServerName = keyMatch[1];
|
|
252
|
+
serverBlocks[currentServerName] = [line];
|
|
253
|
+
}
|
|
254
|
+
else if (currentServerName) {
|
|
255
|
+
serverBlocks[currentServerName].push(line);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
headerLines.push(line);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let result = '';
|
|
263
|
+
for (const line of headerLines) {
|
|
264
|
+
result += line + '\n';
|
|
265
|
+
if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
|
|
266
|
+
for (const srv of allowedServers) {
|
|
267
|
+
if (serverBlocks[srv]) {
|
|
268
|
+
result += serverBlocks[srv].join('\n') + '\n';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
logger.error({ sourceYamlPath, error: err }, '[YAML_FILTER] Unexpected failure while filtering config.yaml.');
|
|
277
|
+
return 'mcp_servers: {}\n';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
166
280
|
export class NousHermesRunner {
|
|
167
281
|
timeoutMs;
|
|
168
282
|
tempFiles = [];
|
|
@@ -171,20 +285,22 @@ export class NousHermesRunner {
|
|
|
171
285
|
this.timeoutMs = CONFIG.TIMEOUT_MS || 900000; // 15 min default
|
|
172
286
|
}
|
|
173
287
|
cleanupTempFiles() {
|
|
288
|
+
logger.debug({ count: this.tempFiles.length }, '[CLEANUP] Cleaning up temporary run files.');
|
|
174
289
|
for (const tempFile of this.tempFiles) {
|
|
175
290
|
try {
|
|
176
291
|
if (fs.existsSync(tempFile)) {
|
|
177
292
|
fs.unlinkSync(tempFile);
|
|
178
|
-
logger.debug({ tempFile }, 'Cleaned up temp file');
|
|
293
|
+
logger.debug({ tempFile }, '[CLEANUP] Cleaned up temp file');
|
|
179
294
|
}
|
|
180
295
|
}
|
|
181
296
|
catch (err) {
|
|
182
|
-
logger.warn({ tempFile, error: err }, 'Failed to cleanup temp file');
|
|
297
|
+
logger.warn({ tempFile, error: err }, '[CLEANUP] Failed to cleanup temp file');
|
|
183
298
|
}
|
|
184
299
|
}
|
|
185
300
|
this.tempFiles = [];
|
|
186
301
|
}
|
|
187
302
|
async runAgent(options) {
|
|
303
|
+
logger.info({ agentName: options.agentName, model: options.model, sessionId: options.sessionId }, '[RUN_AGENT] Initiating runAgent entrypoint.');
|
|
188
304
|
try {
|
|
189
305
|
const result = await withSpan('hermes.runAgent', async (span) => {
|
|
190
306
|
span.setAttribute('agentName', options.agentName || '');
|
|
@@ -198,13 +314,14 @@ export class NousHermesRunner {
|
|
|
198
314
|
});
|
|
199
315
|
this.cleanupTempFiles();
|
|
200
316
|
if (options.agentName && result.sessionId) {
|
|
317
|
+
logger.info({ agentName: options.agentName, sessionId: result.sessionId }, '[RUN_AGENT] Saving completed session ID.');
|
|
201
318
|
await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
|
|
202
319
|
}
|
|
203
320
|
return result;
|
|
204
321
|
}
|
|
205
322
|
catch (error) {
|
|
206
323
|
this.cleanupTempFiles();
|
|
207
|
-
logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, 'Hermes runner
|
|
324
|
+
logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, '[RUN_AGENT] Hermes runner execution flow threw an error.');
|
|
208
325
|
throw error;
|
|
209
326
|
}
|
|
210
327
|
}
|
|
@@ -213,29 +330,60 @@ export class NousHermesRunner {
|
|
|
213
330
|
let { sessionId } = options;
|
|
214
331
|
const cwd = options.cwd || process.cwd();
|
|
215
332
|
const configPath = options.configPath || getWorkspaceDir();
|
|
333
|
+
logger.info({ agentName, autoResume, cwd, configPath, silent }, '[RUN_AGENT_INTERNAL] Starting internal agent runner workflow.');
|
|
216
334
|
// Load .env files FIRST
|
|
217
|
-
|
|
218
|
-
|
|
335
|
+
const envPaths = [path.join(cwd, '.env'), path.join(cwd, '../Workflow/.env')];
|
|
336
|
+
for (const envPath of envPaths) {
|
|
337
|
+
if (fs.existsSync(envPath)) {
|
|
338
|
+
logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Loading quiet environment file.');
|
|
339
|
+
loadEnvQuietly(envPath);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Environment file not found, skipping.');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
219
345
|
// Auto Resume
|
|
220
346
|
if (autoResume && agentName && !sessionId) {
|
|
347
|
+
logger.info({ agentName }, '[RUN_AGENT_INTERNAL] Auto-resume enabled. Querying last session ID.');
|
|
221
348
|
const lastId = await getLastSessionId(agentName, configPath, 'hermes');
|
|
222
349
|
if (lastId) {
|
|
223
350
|
sessionId = lastId;
|
|
224
351
|
if (!silent)
|
|
225
352
|
console.error(`[NousHermesRunner] Auto-resume session: ${sessionId}`);
|
|
353
|
+
logger.info({ sessionId }, '[RUN_AGENT_INTERNAL] Resolved last session ID for resume.');
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
logger.info('[RUN_AGENT_INTERNAL] No previous session ID found for auto-resume.');
|
|
226
357
|
}
|
|
227
358
|
}
|
|
228
359
|
const MAX_BUF = 10 * 1024 * 1024;
|
|
229
360
|
const timeoutMs = this.timeoutMs;
|
|
230
361
|
const HARD_TIMEOUT_MS = 60000;
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
// HERMES_HOME — résolu via getAgentHermesHome() (multi-OS, multi-install).
|
|
364
|
+
// Priorité : OVERMIND_HERMES_HOME > <workspace>/.overmind/hermes/ > ~/.overmind/hermes/
|
|
365
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
234
366
|
const overmindHermesPath = getAgentOvermindHome(agentName);
|
|
235
367
|
const overmindHermesSubPath = getAgentHermesHome(agentName);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
369
|
+
// ⭐ CHEMIN CREDENTIALS HERMES — SOURCE DE VÉRITÉ UNIQUE
|
|
370
|
+
//
|
|
371
|
+
// <HERMES_HOME>/agents/<name>/settings.json
|
|
372
|
+
//
|
|
373
|
+
// ❌ NE PAS utiliser .claude/settings_<name>.json — c'est pour Claude/Kilo.
|
|
374
|
+
// ❌ Ce fichier EST le fichier natif Hermes. Le runner le lit ET le met à jour.
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
376
|
+
const agentSettingsPath = agentName ? path.join(overmindHermesSubPath, 'settings.json') : '';
|
|
377
|
+
if (agentName && !fs.existsSync(overmindHermesSubPath)) {
|
|
378
|
+
return {
|
|
379
|
+
result: '',
|
|
380
|
+
error: `INVALID_AGENT: Dossier Hermes manquant pour l'agent "${agentName}". ` +
|
|
381
|
+
`Créez le dossier et le fichier : ${agentSettingsPath} ` +
|
|
382
|
+
`avec { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
|
|
383
|
+
`"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
|
|
384
|
+
`"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
|
|
385
|
+
`"agent": "${agentName}", "runner": "hermes" }`,
|
|
386
|
+
};
|
|
239
387
|
}
|
|
240
388
|
// Load agent settings + MCP config (same pattern as ClaudeRunner)
|
|
241
389
|
let systemPrompt = '';
|
|
@@ -243,7 +391,7 @@ export class NousHermesRunner {
|
|
|
243
391
|
let resolvedProvider;
|
|
244
392
|
const agentCustomEnv = {
|
|
245
393
|
...process.env,
|
|
246
|
-
PYTHONIOENCODING: 'utf-8',
|
|
394
|
+
PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: '1',
|
|
247
395
|
PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
|
|
248
396
|
PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
|
|
249
397
|
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '',
|
|
@@ -307,12 +455,12 @@ export class NousHermesRunner {
|
|
|
307
455
|
const canonicalSoul = path.join(overmindHermesSubPath, 'SOUL.md');
|
|
308
456
|
const legacySoul = path.join(getSharedHermesHome(), `agent_${agentName}`, '.hermes', 'SOUL.md');
|
|
309
457
|
const claudeSoul = path.join(configPath, '.claude', 'agents', `${agentName}.md`);
|
|
310
|
-
const agentPromptPath = fs.existsSync(
|
|
311
|
-
?
|
|
312
|
-
: fs.existsSync(
|
|
313
|
-
?
|
|
314
|
-
: fs.existsSync(
|
|
315
|
-
?
|
|
458
|
+
const agentPromptPath = fs.existsSync(claudeSoul)
|
|
459
|
+
? claudeSoul
|
|
460
|
+
: fs.existsSync(canonicalSoul)
|
|
461
|
+
? canonicalSoul
|
|
462
|
+
: fs.existsSync(legacySoul)
|
|
463
|
+
? legacySoul
|
|
316
464
|
: null;
|
|
317
465
|
if (agentPromptPath && fs.existsSync(agentPromptPath)) {
|
|
318
466
|
systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
|
|
@@ -330,24 +478,29 @@ export class NousHermesRunner {
|
|
|
330
478
|
}
|
|
331
479
|
}
|
|
332
480
|
}
|
|
333
|
-
//
|
|
481
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
482
|
+
// LECTURE CREDENTIALS HERMES — <HERMES_HOME>/agents/<name>/settings.json
|
|
483
|
+
//
|
|
484
|
+
// Ce chemin est résolu UNE SEULE FOIS en haut de la fonction (agentSettingsPath).
|
|
485
|
+
// Il pointe vers le dossier natif Hermes, PAS vers .claude/.
|
|
486
|
+
// Si le fichier est absent, l'agent tourne sans LLM (erreur claire dans les logs).
|
|
487
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
334
488
|
try {
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
// explicitly along with the alternative paths that DO exist. Without this log,
|
|
338
|
-
// a user putting the file under .claude/agents/ (or just `agents/`) will see a
|
|
339
|
-
// cryptic 401 ten minutes later with no breadcrumb back to the misplacement.
|
|
489
|
+
// agentSettingsPath = <HERMES_HOME>/agents/<name>/settings.json (défini ligne ~405)
|
|
490
|
+
// ❌ NE PAS changer ceci pour pointer vers .claude/ — c'est intentionnel.
|
|
340
491
|
if (!fs.existsSync(agentSettingsPath)) {
|
|
341
|
-
|
|
342
|
-
path.join(configPath, '.claude', 'agents', `settings_${agentName}.json`),
|
|
343
|
-
path.join(configPath, `settings_${agentName}.json`),
|
|
344
|
-
path.join(configPath, 'agents', `settings_${agentName}.json`),
|
|
345
|
-
];
|
|
346
|
-
logger.warn({
|
|
492
|
+
logger.error({
|
|
347
493
|
agentName,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
494
|
+
expected: agentSettingsPath,
|
|
495
|
+
hermesHome: overmindHermesSubPath,
|
|
496
|
+
action: `Créer ${agentSettingsPath} avec les credentials. ` +
|
|
497
|
+
`Format minimal : { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
|
|
498
|
+
`"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
|
|
499
|
+
`"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
|
|
500
|
+
`"agent": "${agentName}", "runner": "hermes" }`,
|
|
501
|
+
}, '[HERMES] ❌ settings.json introuvable dans le dossier Hermes natif. ' +
|
|
502
|
+
'Hermes NE cherche PAS dans .claude/ — uniquement dans .overmind/hermes/agents/<name>/settings.json. ' +
|
|
503
|
+
'Voir la documentation dans le commentaire du constructeur NousHermesRunner.');
|
|
351
504
|
}
|
|
352
505
|
if (fs.existsSync(agentSettingsPath)) {
|
|
353
506
|
// Read the RAW settings (pre-interpolation) to capture $VAR references
|
|
@@ -366,8 +519,9 @@ export class NousHermesRunner {
|
|
|
366
519
|
let settings = rawSettings;
|
|
367
520
|
settings = interpolateEnvVars(settings);
|
|
368
521
|
loadedSettings = settings;
|
|
369
|
-
//
|
|
370
|
-
|
|
522
|
+
// Fichier temporaire interpolé (valeurs $VAR résolues) — nettoyé après le spawn.
|
|
523
|
+
// Situé dans le même dossier que settings.json : <HERMES_HOME>/agents/<name>/
|
|
524
|
+
const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_tmp.json`);
|
|
371
525
|
fs.writeFileSync(tempSettings, JSON.stringify(settings, null, 2));
|
|
372
526
|
tmpSettingsPath = tempSettings;
|
|
373
527
|
this.tempFiles.push(tempSettings);
|
|
@@ -377,17 +531,22 @@ export class NousHermesRunner {
|
|
|
377
531
|
agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
|
|
378
532
|
}
|
|
379
533
|
}
|
|
380
|
-
//
|
|
381
|
-
|
|
534
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
535
|
+
// CONFIG MCP — Résolution dans l'ordre suivant :
|
|
536
|
+
// 1. <HERMES_HOME>/agents/<name>/.mcp.json (override par agent)
|
|
537
|
+
// 2. <OVERMIND_WORKSPACE>/.mcp.json filtré par enabledMcpjsonServers
|
|
538
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
539
|
+
const agentMcpPath = path.join(overmindHermesSubPath, '.mcp.json');
|
|
382
540
|
if (fs.existsSync(agentMcpPath)) {
|
|
383
|
-
//
|
|
384
|
-
const tempMcp = path.join(path.dirname(agentSettingsPath), `
|
|
541
|
+
// Override MCP par agent : <HERMES_HOME>/agents/<name>/.mcp.json
|
|
542
|
+
const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
|
|
385
543
|
fs.writeFileSync(tempMcp, fs.readFileSync(agentMcpPath, 'utf8'));
|
|
386
544
|
tmpMcpPath = tempMcp;
|
|
387
545
|
this.tempFiles.push(tempMcp);
|
|
388
546
|
}
|
|
389
547
|
else if (settings.enableAllProjectMcpServers === false &&
|
|
390
548
|
Array.isArray(settings.enabledMcpjsonServers)) {
|
|
549
|
+
// Filtre le .mcp.json du workspace selon enabledMcpjsonServers
|
|
391
550
|
const projectMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP, configPath);
|
|
392
551
|
if (fs.existsSync(projectMcpPath)) {
|
|
393
552
|
const fullMcp = JSON.parse(fs.readFileSync(projectMcpPath, 'utf8'));
|
|
@@ -397,7 +556,7 @@ export class NousHermesRunner {
|
|
|
397
556
|
filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName];
|
|
398
557
|
}
|
|
399
558
|
}
|
|
400
|
-
const tempMcp = path.join(path.dirname(agentSettingsPath), `
|
|
559
|
+
const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
|
|
401
560
|
fs.writeFileSync(tempMcp, JSON.stringify(filteredMcp, null, 2));
|
|
402
561
|
tmpMcpPath = tempMcp;
|
|
403
562
|
this.tempFiles.push(tempMcp);
|
|
@@ -408,8 +567,8 @@ export class NousHermesRunner {
|
|
|
408
567
|
catch (e) {
|
|
409
568
|
logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
|
|
410
569
|
}
|
|
411
|
-
//
|
|
412
|
-
//
|
|
570
|
+
// Charge le .env de l'agent (dans <HERMES_HOME>/agents/<name>/.env) en FALLBACK UNIQUEMENT.
|
|
571
|
+
// Les clés déjà définies dans settings.json ne sont PAS écrasées.
|
|
413
572
|
//
|
|
414
573
|
// CRITICAL (2.8.29): The .hermes/.env file is a STALE WRITE of the previous
|
|
415
574
|
// spawn — it gets re-written by the runner itself at line ~1013, but if a
|
|
@@ -468,7 +627,7 @@ export class NousHermesRunner {
|
|
|
468
627
|
}
|
|
469
628
|
const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
|
|
470
629
|
const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
|
|
471
|
-
const cliPrompt = finalPrompt
|
|
630
|
+
const cliPrompt = finalPrompt;
|
|
472
631
|
// Build CLI args: chat -q (persistent session, NOT -z oneshot)
|
|
473
632
|
// -z + --resume doesn't work — resume is ignored in oneshot mode
|
|
474
633
|
//
|
|
@@ -493,25 +652,26 @@ export class NousHermesRunner {
|
|
|
493
652
|
logger.info({ agentName, provider, model: finalModel }, '[HERMES_ARGS] Passing --provider (2.8.33: needed to bypass upstream auto-router that picked openrouter for MiniMax-M3).');
|
|
494
653
|
}
|
|
495
654
|
// ============================================================
|
|
496
|
-
// 2.8.36 —
|
|
655
|
+
// 2.8.36 — TOOLSET DISCOVERY (LOG ONLY — NOT PASSED TO CLI)
|
|
497
656
|
// ============================================================
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
// `
|
|
657
|
+
// Previously we passed MCP server names via `--toolsets` to Hermes.
|
|
658
|
+
// This caused `Warning: Unknown toolsets: mcp-memory, mcp-discord, ...`
|
|
659
|
+
// because Hermes's toolset registry does NOT use the `mcp-<name>` format.
|
|
660
|
+
//
|
|
661
|
+
// The MCP servers load correctly WITHOUT `--toolsets` because:
|
|
662
|
+
// 1. `HERMES_HOME` is overridden to the per-agent isolated run home.
|
|
663
|
+
// 2. `filterConfigYaml` writes a filtered `config.yaml` that lists only
|
|
664
|
+
// the allowed MCP servers under `mcp_servers:`.
|
|
665
|
+
// 3. Hermes reads `config.yaml` at startup and connects to every server
|
|
666
|
+
// listed there — no `--toolsets` flag needed.
|
|
505
667
|
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
// will then ensure those toolsets are loaded AND exposed to the
|
|
509
|
-
// conversation as `mcp__<server>__<tool>` callable tools.
|
|
668
|
+
// We still build `toolsetList` for diagnostic logging so that the log
|
|
669
|
+
// shows which MCP servers are expected for this agent run.
|
|
510
670
|
//
|
|
511
671
|
// Source of truth for the list:
|
|
512
672
|
// - If settings has `enabledMcpjsonServers: [...non-empty...]`, use that.
|
|
513
673
|
// - Else, if `enableAllProjectMcpServers: true`, use ALL server names
|
|
514
|
-
// from
|
|
674
|
+
// from the Hermes config.yaml.
|
|
515
675
|
// - Else, skip — no toolset hint.
|
|
516
676
|
const toolsetList = [];
|
|
517
677
|
// Read the canonical settings.json we just wrote to find the MCP server hints.
|
|
@@ -570,19 +730,33 @@ export class NousHermesRunner {
|
|
|
570
730
|
const hermesConfigPath = path.join(getSharedHermesHome(), 'config.yaml');
|
|
571
731
|
if (fs.existsSync(hermesConfigPath)) {
|
|
572
732
|
const yamlText = fs.readFileSync(hermesConfigPath, 'utf8');
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
733
|
+
// Line-by-line YAML parser for the `mcp_servers` section (handles comments, varying indentation, and exits correctly)
|
|
734
|
+
const lines = yamlText.split(/\r?\n/);
|
|
735
|
+
let inMcpServers = false;
|
|
736
|
+
const serverNames = [];
|
|
737
|
+
for (const line of lines) {
|
|
738
|
+
const trimmed = line.trim();
|
|
739
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
740
|
+
continue;
|
|
741
|
+
if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
|
|
742
|
+
inMcpServers = true;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (inMcpServers) {
|
|
746
|
+
const indentMatch = line.match(/^(\s+)/);
|
|
747
|
+
if (!indentMatch) {
|
|
748
|
+
inMcpServers = false;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
|
|
752
|
+
if (keyMatch) {
|
|
753
|
+
serverNames.push(keyMatch[1]);
|
|
754
|
+
}
|
|
584
755
|
}
|
|
585
756
|
}
|
|
757
|
+
if (serverNames.length > 0) {
|
|
758
|
+
toolsetList.push(...serverNames);
|
|
759
|
+
}
|
|
586
760
|
}
|
|
587
761
|
if (toolsetList.length === 0) {
|
|
588
762
|
// Fallback: read .mcp.json (Overmind format) for any servers not in
|
|
@@ -601,8 +775,10 @@ export class NousHermesRunner {
|
|
|
601
775
|
}
|
|
602
776
|
}
|
|
603
777
|
if (toolsetList.length > 0) {
|
|
604
|
-
|
|
605
|
-
|
|
778
|
+
// Log which MCP servers are expected for this run — do NOT pass to CLI.
|
|
779
|
+
// Hermes loads them via the isolated config.yaml; passing --toolsets
|
|
780
|
+
// would produce `Warning: Unknown toolsets: mcp-<name>` noise with no benefit.
|
|
781
|
+
logger.info({ agentName, expectedMcpServers: toolsetList }, '[HERMES_ARGS] Expected MCP servers for this run (loaded via config.yaml, NOT via --toolsets).');
|
|
606
782
|
}
|
|
607
783
|
if (sessionId)
|
|
608
784
|
cleanArgs.push('--resume', sessionId);
|
|
@@ -690,10 +866,10 @@ export class NousHermesRunner {
|
|
|
690
866
|
`but process.env.${varName} is empty. Add it to /home/demon/.overmind/.env or fix the settings reference.`);
|
|
691
867
|
}
|
|
692
868
|
resolvedValue = fromEnv;
|
|
693
|
-
logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[
|
|
869
|
+
logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[TOKEN_RESOLVER] Resolved $VAR reference from settings_<agent>.json against process.env.');
|
|
694
870
|
}
|
|
695
871
|
const detected = detectTokenProvider(resolvedValue);
|
|
696
|
-
logger.info({ agentName, tokenKey: t.key, detectedProvider: detected.provider, mappedTo: detected.envKey }, '[
|
|
872
|
+
logger.info({ agentName, tokenKey: t.key, detectedProvider: detected.provider, mappedTo: detected.envKey }, '[TOKEN_RESOLVER] Using explicit settings_<agent>.json token, re-mapping to detected provider env var.');
|
|
697
873
|
return { tokenEnvKey: t.key, tokenValue: resolvedValue, detectedProvider: detected.provider, source: 'settings-explicit' };
|
|
698
874
|
}
|
|
699
875
|
const candidates = [];
|
|
@@ -722,7 +898,7 @@ export class NousHermesRunner {
|
|
|
722
898
|
// Pass B: re-map the first candidate to the right provider env-var name.
|
|
723
899
|
const first = candidates[0];
|
|
724
900
|
if (first.detected.provider !== 'unknown' && first.detected.envKey !== first.key) {
|
|
725
|
-
logger.info({ agentName, sourceKey: first.key, detectedProvider: first.detected.provider, remappedTo: first.detected.envKey }, '[
|
|
901
|
+
logger.info({ agentName, sourceKey: first.key, detectedProvider: first.detected.provider, remappedTo: first.detected.envKey }, '[TOKEN_RESOLVER] Token prefix detected provider mismatch — re-mapping env var.');
|
|
726
902
|
return {
|
|
727
903
|
tokenEnvKey: first.detected.envKey,
|
|
728
904
|
tokenValue: first.value,
|
|
@@ -842,25 +1018,27 @@ export class NousHermesRunner {
|
|
|
842
1018
|
// AbortSignal
|
|
843
1019
|
if (options.signal?.aborted)
|
|
844
1020
|
return Promise.reject(new Error('ABORTED'));
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// <HERMES_HOME>/agents/<name>/
|
|
851
|
-
// <HERMES_HOME>/config.yaml ← global, Hermes manages
|
|
852
|
-
// <HERMES_HOME>/auth.json ← global, Hermes manages
|
|
1021
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1022
|
+
// MISE À JOUR settings.json Hermes natif (2.8.45)
|
|
1023
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1024
|
+
//
|
|
1025
|
+
// Le runner LIT et ÉCRIT dans le MÊME fichier :
|
|
1026
|
+
// <HERMES_HOME>/agents/<name>/settings.json
|
|
853
1027
|
//
|
|
854
|
-
//
|
|
855
|
-
// 1.
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
1028
|
+
// Cycle complet :
|
|
1029
|
+
// 1. LECTURE : settings.json → interpolation $VAR → agentCustomEnv
|
|
1030
|
+
// 2. DÉTECTION: token prefix (sk-cp-* → MiniMax, 32hex → Z.AI, etc.)
|
|
1031
|
+
// 3. INJECTION: MINIMAX_CN_API_KEY, MINIMAX_CN_BASE_URL, etc. auto-injectés
|
|
1032
|
+
// 4. ÉCRITURE : settings.json mis à jour avec les clés injectées
|
|
1033
|
+
// 5. SPAWN : hermes chat -q avec HERMES_HOME = runs/<name>/
|
|
859
1034
|
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
//
|
|
1035
|
+
// ❌ Overmind NE convertit PAS depuis .claude/ — c'est le chemin Claude/Kilo.
|
|
1036
|
+
// ✅ settings.json est géré par l'utilisateur ET mis à jour par Overmind.
|
|
1037
|
+
// Les $VAR restent en tant que littéraux après la première résolution.
|
|
1038
|
+
// Pour changer un credential, éditer directement settings.json.
|
|
1039
|
+
//
|
|
1040
|
+
// Autres fichiers gérés par Hermes upstream (ne pas toucher) :
|
|
1041
|
+
// config.yaml, auth.json, sessions/, logs/
|
|
864
1042
|
if (agentName) {
|
|
865
1043
|
const agentHome = overmindHermesSubPath; // = <HERMES_HOME>/agents/<name>/
|
|
866
1044
|
if (!fs.existsSync(agentHome))
|
|
@@ -963,6 +1141,73 @@ export class NousHermesRunner {
|
|
|
963
1141
|
// edits were being silently deleted after each spawn.
|
|
964
1142
|
logger.info({ agentName, settingsPath: tmpAgentSettings, envKeys: Object.keys(settingsJson.env || {}).length }, '[HERMES] Wrote canonical agents/<name>/settings.json (env block from settings_<name>.json + provider-specific seeds).');
|
|
965
1143
|
}
|
|
1144
|
+
let effectiveHermesHome = sharedHome;
|
|
1145
|
+
if (agentName && enabledInSettings.length > 0) {
|
|
1146
|
+
try {
|
|
1147
|
+
const runsDir = path.join(sharedHome, 'runs');
|
|
1148
|
+
if (!fs.existsSync(runsDir))
|
|
1149
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
1150
|
+
const runHome = path.join(runsDir, agentName);
|
|
1151
|
+
if (!fs.existsSync(runHome))
|
|
1152
|
+
fs.mkdirSync(runHome, { recursive: true });
|
|
1153
|
+
// Copy auth.json if exists
|
|
1154
|
+
const sharedAuth = path.join(sharedHome, 'auth.json');
|
|
1155
|
+
const runAuth = path.join(runHome, 'auth.json');
|
|
1156
|
+
if (fs.existsSync(sharedAuth)) {
|
|
1157
|
+
fs.copyFileSync(sharedAuth, runAuth);
|
|
1158
|
+
}
|
|
1159
|
+
// Robust directory junction/symlink helper
|
|
1160
|
+
const linkDirRobust = (target, source) => {
|
|
1161
|
+
if (!fs.existsSync(source)) {
|
|
1162
|
+
try {
|
|
1163
|
+
fs.mkdirSync(source, { recursive: true });
|
|
1164
|
+
}
|
|
1165
|
+
catch (e) {
|
|
1166
|
+
logger.warn({ source, error: e }, '[HERMES_HOME] Failed to create link source directory');
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
let exists = false;
|
|
1170
|
+
let stats = null;
|
|
1171
|
+
try {
|
|
1172
|
+
stats = fs.lstatSync(target);
|
|
1173
|
+
exists = true;
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
// target does not exist
|
|
1177
|
+
}
|
|
1178
|
+
if (exists) {
|
|
1179
|
+
logger.debug({ target, isJunction: stats ? (stats.isSymbolicLink() || stats.isDirectory()) : false }, '[HERMES_HOME] Target directory/link already exists. Skipping link creation.');
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
logger.info({ target, source }, '[HERMES_HOME] Creating junction/symbolic link.');
|
|
1183
|
+
if (process.platform === 'win32') {
|
|
1184
|
+
execSync(`cmd /c mklink /J "${target}" "${source}"`);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
fs.symlinkSync(source, target);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
// Link agents directory
|
|
1192
|
+
const sharedAgents = path.join(sharedHome, 'agents');
|
|
1193
|
+
const runAgents = path.join(runHome, 'agents');
|
|
1194
|
+
linkDirRobust(runAgents, sharedAgents);
|
|
1195
|
+
// Link sessions directory
|
|
1196
|
+
const sharedSessions = path.join(sharedHome, 'sessions');
|
|
1197
|
+
const runSessions = path.join(runHome, 'sessions');
|
|
1198
|
+
linkDirRobust(runSessions, sharedSessions);
|
|
1199
|
+
// Generate filtered config.yaml
|
|
1200
|
+
const sharedConfig = path.join(sharedHome, 'config.yaml');
|
|
1201
|
+
const runConfig = path.join(runHome, 'config.yaml');
|
|
1202
|
+
const filteredConfig = filterConfigYaml(sharedConfig, enabledInSettings);
|
|
1203
|
+
fs.writeFileSync(runConfig, filteredConfig, 'utf8');
|
|
1204
|
+
effectiveHermesHome = runHome;
|
|
1205
|
+
logger.info({ agentName, runHome, enabledMcp: enabledInSettings }, '[HERMES_HOME] Isolated agent Hermes Home and filtered config.yaml successfully.');
|
|
1206
|
+
}
|
|
1207
|
+
catch (e) {
|
|
1208
|
+
logger.warn({ error: e }, 'Failed to setup isolated run home; falling back to sharedHome');
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
966
1211
|
// AbortSignal
|
|
967
1212
|
if (options.signal?.aborted)
|
|
968
1213
|
return Promise.reject(new Error('ABORTED'));
|
|
@@ -972,10 +1217,27 @@ export class NousHermesRunner {
|
|
|
972
1217
|
let retryCount = 0;
|
|
973
1218
|
const maxRetries = getAvailableFallbacks().length + 1;
|
|
974
1219
|
let currentSessionId = sessionId;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1220
|
+
const abortListener = () => {
|
|
1221
|
+
if (currentChildRef) {
|
|
1222
|
+
killProcessTree(currentChildRef).then(() => {
|
|
1223
|
+
cleanupTmpFiles();
|
|
1224
|
+
safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
cleanupTmpFiles();
|
|
1229
|
+
safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
const safeResolve = (v) => {
|
|
1233
|
+
if (!resolved) {
|
|
1234
|
+
resolved = true;
|
|
1235
|
+
if (options.signal) {
|
|
1236
|
+
options.signal.removeEventListener('abort', abortListener);
|
|
1237
|
+
}
|
|
1238
|
+
resolve(v);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
979
1241
|
const cleanupTmpFiles = () => {
|
|
980
1242
|
for (const f of [tmpSettingsPath, tmpMcpPath]) {
|
|
981
1243
|
if (f && fs.existsSync(f)) {
|
|
@@ -1064,12 +1326,11 @@ export class NousHermesRunner {
|
|
|
1064
1326
|
// etc. relative to HERMES_HOME. Setting it to the per-agent home would
|
|
1065
1327
|
// tell Hermes "this IS the Hermes root" and make it look for config.yaml
|
|
1066
1328
|
// IN the agent dir — wrong layout.
|
|
1067
|
-
const sharedHermesHome = getSharedHermesHome();
|
|
1068
1329
|
const child = spawn(hermesBin, cleanArgs, {
|
|
1069
1330
|
cwd, shell: false, windowsHide: true,
|
|
1070
1331
|
env: {
|
|
1071
1332
|
...spawnEnv,
|
|
1072
|
-
HERMES_HOME:
|
|
1333
|
+
HERMES_HOME: effectiveHermesHome,
|
|
1073
1334
|
...(isVenvInstall
|
|
1074
1335
|
? {
|
|
1075
1336
|
VIRTUAL_ENV: venvRoot,
|
|
@@ -1100,8 +1361,8 @@ export class NousHermesRunner {
|
|
|
1100
1361
|
void appendOutput(child.pid, chunk, configPath);
|
|
1101
1362
|
void appendLiveOutput(child.pid, chunk);
|
|
1102
1363
|
}
|
|
1103
|
-
if (stdout.length + chunk.length > MAX_BUF)
|
|
1104
|
-
stdout = stdout.slice(-MAX_BUF);
|
|
1364
|
+
if (stdout.length + chunk.length > this.MAX_BUF)
|
|
1365
|
+
stdout = stdout.slice(-this.MAX_BUF);
|
|
1105
1366
|
else
|
|
1106
1367
|
stdout += chunk;
|
|
1107
1368
|
if (!silent && agentName)
|
|
@@ -1109,8 +1370,8 @@ export class NousHermesRunner {
|
|
|
1109
1370
|
});
|
|
1110
1371
|
child.stderr?.on('data', (d) => {
|
|
1111
1372
|
const chunk = d.toString();
|
|
1112
|
-
if (stderr.length + chunk.length > MAX_BUF)
|
|
1113
|
-
stderr = stderr.slice(-MAX_BUF);
|
|
1373
|
+
if (stderr.length + chunk.length > this.MAX_BUF)
|
|
1374
|
+
stderr = stderr.slice(-this.MAX_BUF);
|
|
1114
1375
|
else
|
|
1115
1376
|
stderr += chunk;
|
|
1116
1377
|
if (!silent && agentName)
|
|
@@ -1170,13 +1431,9 @@ export class NousHermesRunner {
|
|
|
1170
1431
|
});
|
|
1171
1432
|
});
|
|
1172
1433
|
};
|
|
1173
|
-
options.signal
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
cleanupTmpFiles();
|
|
1177
|
-
safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
|
|
1178
|
-
});
|
|
1179
|
-
});
|
|
1434
|
+
if (options.signal) {
|
|
1435
|
+
options.signal.addEventListener('abort', abortListener);
|
|
1436
|
+
}
|
|
1180
1437
|
let firstToken;
|
|
1181
1438
|
try {
|
|
1182
1439
|
firstToken = getTokenForIndex(0);
|