overmind-mcp 2.8.52 → 2.8.53
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/bridge/ScenarioLoader.d.ts +20 -1
- package/dist/bridge/ScenarioLoader.d.ts.map +1 -1
- package/dist/bridge/ScenarioLoader.js +42 -0
- package/dist/bridge/ScenarioLoader.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/agent_lifecycle.d.ts +19 -0
- package/dist/lib/agent_lifecycle.d.ts.map +1 -1
- package/dist/lib/agent_lifecycle.js +92 -1
- package/dist/lib/agent_lifecycle.js.map +1 -1
- package/dist/lib/orchestration/dispatcher.d.ts +13 -0
- package/dist/lib/orchestration/dispatcher.d.ts.map +1 -1
- package/dist/lib/orchestration/dispatcher.js +100 -0
- package/dist/lib/orchestration/dispatcher.js.map +1 -1
- package/dist/services/AgentManager.d.ts +4 -4
- package/dist/services/AgentManager.d.ts.map +1 -1
- package/dist/services/AgentManager.js +139 -191
- package/dist/services/AgentManager.js.map +1 -1
- package/dist/services/HermesProfileManager.d.ts +99 -0
- package/dist/services/HermesProfileManager.d.ts.map +1 -0
- package/dist/services/HermesProfileManager.js +356 -0
- package/dist/services/HermesProfileManager.js.map +1 -0
- package/dist/services/HermesRunner.d.ts +53 -0
- package/dist/services/HermesRunner.d.ts.map +1 -0
- package/dist/services/HermesRunner.js +313 -0
- package/dist/services/HermesRunner.js.map +1 -0
- package/dist/services/KanbanAdapter.d.ts +135 -0
- package/dist/services/KanbanAdapter.d.ts.map +1 -0
- package/dist/services/KanbanAdapter.js +320 -0
- package/dist/services/KanbanAdapter.js.map +1 -0
- package/dist/tools/manage_agents.js +4 -4
- package/dist/tools/manage_agents.js.map +1 -1
- package/dist/tools/memory_store.d.ts +1 -1
- package/dist/tools/run_hermes.d.ts +1 -0
- package/dist/tools/run_hermes.d.ts.map +1 -1
- package/dist/tools/run_hermes.js +12 -6
- package/dist/tools/run_hermes.js.map +1 -1
- package/docs/PLAN_MIGRATION_V3.md +339 -0
- package/package.json +2 -2
- package/scripts/migrate-to-profiles.mjs +205 -0
- package/dist/lib/orchestration/swarm.d.ts +0 -76
- package/dist/lib/orchestration/swarm.d.ts.map +0 -1
- package/dist/lib/orchestration/swarm.js +0 -241
- package/dist/lib/orchestration/swarm.js.map +0 -1
- package/dist/prompts/agent_prompts.d.ts +0 -2
- package/dist/prompts/agent_prompts.d.ts.map +0 -1
- package/dist/prompts/agent_prompts.js +0 -21
- package/dist/prompts/agent_prompts.js.map +0 -1
- package/dist/services/NousHermesRunner.d.ts +0 -83
- package/dist/services/NousHermesRunner.d.ts.map +0 -1
- package/dist/services/NousHermesRunner.js +0 -1241
- package/dist/services/NousHermesRunner.js.map +0 -1
- package/dist/services/hermes/binaryFinder.d.ts +0 -8
- package/dist/services/hermes/binaryFinder.d.ts.map +0 -1
- package/dist/services/hermes/binaryFinder.js +0 -92
- package/dist/services/hermes/binaryFinder.js.map +0 -1
- package/dist/services/hermes/configYamlFilter.d.ts +0 -12
- package/dist/services/hermes/configYamlFilter.d.ts.map +0 -1
- package/dist/services/hermes/configYamlFilter.js +0 -70
- package/dist/services/hermes/configYamlFilter.js.map +0 -1
- package/dist/services/hermes/providerConfig.d.ts +0 -19
- package/dist/services/hermes/providerConfig.d.ts.map +0 -1
- package/dist/services/hermes/providerConfig.js +0 -53
- package/dist/services/hermes/providerConfig.js.map +0 -1
- package/dist/services/hermesTokenResolver.d.ts +0 -74
- package/dist/services/hermesTokenResolver.d.ts.map +0 -1
- package/dist/services/hermesTokenResolver.js +0 -140
- package/dist/services/hermesTokenResolver.js.map +0 -1
- package/dist/tools/initialization_check.d.ts +0 -2
- package/dist/tools/initialization_check.d.ts.map +0 -1
- package/dist/tools/initialization_check.js +0 -23
- package/dist/tools/initialization_check.js.map +0 -1
- /package/bin/{launch.js → launch.cjs} +0 -0
- /package/dist/bin/{launch.js → launch.cjs} +0 -0
|
@@ -1,1241 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import { CONFIG, resolveConfigPath, getWorkspaceDir, getAgentHermesHome, getAgentOvermindHome, getSharedHermesHome } from '../lib/config.js';
|
|
5
|
-
import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
|
|
6
|
-
import { linkSessionToPid } from '../lib/processRegistry.js';
|
|
7
|
-
import { interpolateEnvVars } from '../lib/envUtils.js';
|
|
8
|
-
import { withSpan } from '../lib/telemetry.js';
|
|
9
|
-
import { loadEnvQuietly } from '../lib/loadEnv.js';
|
|
10
|
-
import { rootLogger } from '../lib/logger.js';
|
|
11
|
-
import { registerProcess, appendOutput, updateProcessStatus, } from '../lib/processRegistry.js';
|
|
12
|
-
import { registerLiveAgent, appendLiveOutput, setLiveStatus, unregisterLiveAgent, } from '../lib/agent_lifecycle.js';
|
|
13
|
-
import { killProcessTree } from './hermes/processUtils.js';
|
|
14
|
-
import { findHermesBinary } from './hermes/binaryFinder.js';
|
|
15
|
-
import { TOKEN_KEYS } from './hermes/providerConfig.js';
|
|
16
|
-
import { filterConfigYaml } from './hermes/configYamlFilter.js';
|
|
17
|
-
const logger = rootLogger.child({ module: 'NousHermesRunner' });
|
|
18
|
-
/**
|
|
19
|
-
* NousHermesRunner — Runner Hermes Agent pour Overmind (v2.8.45+).
|
|
20
|
-
*
|
|
21
|
-
* ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
22
|
-
* ║ ⭐ ARCHITECTURE CREDENTIALS HERMES — LIRE CECI EN PREMIER ║
|
|
23
|
-
* ╠══════════════════════════════════════════════════════════════════════════════╣
|
|
24
|
-
* ║ ║
|
|
25
|
-
* ║ Les credentials Hermes sont dans LE DOSSIER NATIF HERMES UNIQUEMENT : ║
|
|
26
|
-
* ║ ║
|
|
27
|
-
* ║ <HERMES_HOME>/agents/<name>/settings.json ║
|
|
28
|
-
* ║ ║
|
|
29
|
-
* ║ Sur Linux (npm -g) : /home/demon/.overmind/hermes/agents/<name>/ ║
|
|
30
|
-
* ║ Sur Windows (dev) : Workflow/.overmind/hermes/agents/<name>/ ║
|
|
31
|
-
* ║ ║
|
|
32
|
-
* ║ ❌ .claude/settings_<name>.json → CLAUDE CODE / KILO SEULEMENT ║
|
|
33
|
-
* ║ ❌ NousHermesRunner NE LIT JAMAIS depuis .claude/ ║
|
|
34
|
-
* ║ ❌ Ne pas éditer hermes/agents/<name>/settings.json manuellement ║
|
|
35
|
-
* ║ entre les runs — le runner le met à jour automatiquement. ║
|
|
36
|
-
* ║ ║
|
|
37
|
-
* ║ FORMAT du settings.json Hermes : ║
|
|
38
|
-
* ║ { ║
|
|
39
|
-
* ║ "env": { ║
|
|
40
|
-
* ║ "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ← token MiniMax / Z.AI ║
|
|
41
|
-
* ║ "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ║
|
|
42
|
-
* ║ "ANTHROPIC_MODEL": "MiniMax-M3", ║
|
|
43
|
-
* ║ "ANTHROPIC_PROVIDER": "minimax-cn", ║
|
|
44
|
-
* ║ "MINIMAX_CN_API_KEY": "sk-cp-...", ← injecté auto par le runner ║
|
|
45
|
-
* ║ "MINIMAX_CN_BASE_URL": "https://api.minimaxi.com/anthropic" ║
|
|
46
|
-
* ║ }, ║
|
|
47
|
-
* ║ "enableAllProjectMcpServers": false, ║
|
|
48
|
-
* ║ "enabledMcpjsonServers": ["memory", "discord", "postgres"], ║
|
|
49
|
-
* ║ "agent": "<name>", ║
|
|
50
|
-
* ║ "runner": "hermes" ║
|
|
51
|
-
* ║ } ║
|
|
52
|
-
* ║ ║
|
|
53
|
-
* ║ Supporte l'interpolation $VAR (ex: "$ANTHROPIC_AUTH_TOKEN_1") ║
|
|
54
|
-
* ║ résolue depuis process.env au moment du spawn. ║
|
|
55
|
-
* ║ ║
|
|
56
|
-
* ║ HERMES_HOME résolu dans l'ordre : ║
|
|
57
|
-
* ║ 1. OVERMIND_HERMES_HOME (env var explicite, ex: systemd EnvironmentFile) ║
|
|
58
|
-
* ║ 2. <OVERMIND_WORKSPACE>/.overmind/hermes/ (dev local) ║
|
|
59
|
-
* ║ 3. ~/.overmind/hermes/ (Linux) / %LOCALAPPDATA%/overmind/hermes/ (Win) ║
|
|
60
|
-
* ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
61
|
-
*
|
|
62
|
-
* • Providers supportés : MiniMax CN/GLOBAL, Z.AI/GLM, Mistral, OpenAI, NVIDIA NIM
|
|
63
|
-
* • OpenRouter = embeddings UNIQUEMENT (bloqué pour LLM inference)
|
|
64
|
-
* • auth.json purgé à chaque run (évite les credentials stale de l'ancien provider)
|
|
65
|
-
* • HOME/USERPROFILE propagé au process Hermes pour résolution ~/.hermes canonique
|
|
66
|
-
* • Voir hermesTokenResolver.ts pour le 3-pass token detection (sk-cp-/32hex/sk-ant-)
|
|
67
|
-
*/
|
|
68
|
-
export class NousHermesRunner {
|
|
69
|
-
timeoutMs;
|
|
70
|
-
tempFiles = [];
|
|
71
|
-
MAX_BUF = 10 * 1024 * 1024; // 10MB buffer limit
|
|
72
|
-
constructor() {
|
|
73
|
-
this.timeoutMs = CONFIG.TIMEOUT_MS || 900000; // 15 min default
|
|
74
|
-
}
|
|
75
|
-
cleanupTempFiles() {
|
|
76
|
-
logger.debug({ count: this.tempFiles.length }, '[CLEANUP] Cleaning up temporary run files.');
|
|
77
|
-
for (const tempFile of this.tempFiles) {
|
|
78
|
-
try {
|
|
79
|
-
if (fs.existsSync(tempFile)) {
|
|
80
|
-
fs.unlinkSync(tempFile);
|
|
81
|
-
logger.debug({ tempFile }, '[CLEANUP] Cleaned up temp file');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
logger.warn({ tempFile, error: err }, '[CLEANUP] Failed to cleanup temp file');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
this.tempFiles = [];
|
|
89
|
-
}
|
|
90
|
-
async runAgent(options) {
|
|
91
|
-
if (options.agentName) {
|
|
92
|
-
// Inline validation — prevents path traversal on settings_${agentName}.json
|
|
93
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(options.agentName)) {
|
|
94
|
-
return { result: '', error: `INVALID_AGENT_NAME: '${options.agentName}' contains invalid characters. Only [a-zA-Z0-9_-] allowed.` };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
logger.info({ agentName: options.agentName, model: options.model, sessionId: options.sessionId }, '[RUN_AGENT] Initiating runAgent entrypoint.');
|
|
98
|
-
try {
|
|
99
|
-
const result = await withSpan('hermes.runAgent', async (span) => {
|
|
100
|
-
span.setAttribute('agentName', options.agentName || '');
|
|
101
|
-
span.setAttribute('model', options.model || '');
|
|
102
|
-
span.setAttribute('runner', 'hermes');
|
|
103
|
-
return await this.runAgentInternal(options);
|
|
104
|
-
}, {
|
|
105
|
-
agentName: options.agentName || '',
|
|
106
|
-
model: options.model || '',
|
|
107
|
-
runner: 'hermes',
|
|
108
|
-
});
|
|
109
|
-
this.cleanupTempFiles();
|
|
110
|
-
if (options.agentName && result.sessionId) {
|
|
111
|
-
logger.info({ agentName: options.agentName, sessionId: result.sessionId }, '[RUN_AGENT] Saving completed session ID.');
|
|
112
|
-
await saveSessionId(options.agentName, result.sessionId, options.configPath, 'hermes');
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
this.cleanupTempFiles();
|
|
118
|
-
logger.error({ error: error instanceof Error ? error.message : String(error), agentName: options.agentName }, '[RUN_AGENT] Hermes runner execution flow threw an error.');
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async runAgentInternal(options) {
|
|
123
|
-
const { prompt, agentName, autoResume, silent } = options;
|
|
124
|
-
let { sessionId } = options;
|
|
125
|
-
const cwd = options.cwd || process.cwd();
|
|
126
|
-
const configPath = options.configPath || getWorkspaceDir();
|
|
127
|
-
logger.info({ agentName, autoResume, cwd, configPath, silent }, '[RUN_AGENT_INTERNAL] Starting internal agent runner workflow.');
|
|
128
|
-
// Load .env files FIRST
|
|
129
|
-
const envPaths = [path.join(cwd, '.env'), path.join(cwd, '../Workflow/.env')];
|
|
130
|
-
for (const envPath of envPaths) {
|
|
131
|
-
if (fs.existsSync(envPath)) {
|
|
132
|
-
logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Loading quiet environment file.');
|
|
133
|
-
loadEnvQuietly(envPath);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
logger.debug({ envPath }, '[RUN_AGENT_INTERNAL] Environment file not found, skipping.');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// Auto Resume
|
|
140
|
-
if (autoResume && agentName && !sessionId) {
|
|
141
|
-
logger.info({ agentName }, '[RUN_AGENT_INTERNAL] Auto-resume enabled. Querying last session ID.');
|
|
142
|
-
const lastId = await getLastSessionId(agentName, configPath, 'hermes');
|
|
143
|
-
if (lastId) {
|
|
144
|
-
sessionId = lastId;
|
|
145
|
-
if (!silent)
|
|
146
|
-
logger.info({ sessionId }, '[NousHermesRunner] Auto-resume session.');
|
|
147
|
-
logger.info({ sessionId }, '[RUN_AGENT_INTERNAL] Resolved last session ID for resume.');
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
logger.info('[RUN_AGENT_INTERNAL] No previous session ID found for auto-resume.');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const MAX_BUF = 10 * 1024 * 1024;
|
|
154
|
-
const timeoutMs = this.timeoutMs;
|
|
155
|
-
const HARD_TIMEOUT_MS = 60000;
|
|
156
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
157
|
-
// HERMES_HOME — résolu via getAgentHermesHome() (multi-OS, multi-install).
|
|
158
|
-
// Priorité : OVERMIND_HERMES_HOME > <workspace>/.overmind/hermes/ > ~/.overmind/hermes/
|
|
159
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
160
|
-
const overmindHermesPath = getAgentOvermindHome(agentName);
|
|
161
|
-
const overmindHermesSubPath = getAgentHermesHome(agentName);
|
|
162
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
-
// ⭐ CHEMIN CREDENTIALS HERMES — SOURCE DE VÉRITÉ UNIQUE
|
|
164
|
-
//
|
|
165
|
-
// <HERMES_HOME>/agents/<name>/settings.json
|
|
166
|
-
//
|
|
167
|
-
// ❌ NE PAS utiliser .claude/settings_<name>.json — c'est pour Claude/Kilo.
|
|
168
|
-
// ❌ Ce fichier EST le fichier natif Hermes. Le runner le lit ET le met à jour.
|
|
169
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
-
const agentSettingsPath = agentName ? path.join(overmindHermesSubPath, 'settings.json') : '';
|
|
171
|
-
if (agentName && !fs.existsSync(overmindHermesSubPath)) {
|
|
172
|
-
return {
|
|
173
|
-
result: '',
|
|
174
|
-
error: `INVALID_AGENT: Dossier Hermes manquant pour l'agent "${agentName}". ` +
|
|
175
|
-
`Créez le dossier et le fichier : ${agentSettingsPath} ` +
|
|
176
|
-
`avec { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
|
|
177
|
-
`"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
|
|
178
|
-
`"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
|
|
179
|
-
`"agent": "${agentName}", "runner": "hermes" }`,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
// Load agent settings + MCP config (same pattern as ClaudeRunner)
|
|
183
|
-
let systemPrompt = '';
|
|
184
|
-
let resolvedModel;
|
|
185
|
-
let resolvedProvider;
|
|
186
|
-
const agentCustomEnv = {
|
|
187
|
-
...process.env,
|
|
188
|
-
PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: '1',
|
|
189
|
-
PYTHONLEGACYWINDOWSSTDIO: '1', TERM: 'emacs',
|
|
190
|
-
PROMPT_TOOLKIT_NO_INTERACTIVE: '1', ANSICON: '1',
|
|
191
|
-
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '',
|
|
192
|
-
NVIDIA_API_KEY: process.env.NVIDIA_API_KEY || process.env.NVAPI_KEY,
|
|
193
|
-
NVIDIA_API_BASE: process.env.NVIDIA_API_BASE || 'https://integrate.api.nvidia.com/v1',
|
|
194
|
-
...(agentName ? { OVERMIND_AGENT_NAME: agentName } : {}),
|
|
195
|
-
// OVERMIND_AGENT_HOME tells Hermes (v0.13.0+) to read agent-specific .env FIRST
|
|
196
|
-
// get_env_value() in Hermes checks OVERMIND_AGENT_HOME/.hermes/.env before HERMES_HOME/.env
|
|
197
|
-
// This allows $VAR expansion done by Overmind to take precedence over gateway .env
|
|
198
|
-
...(agentName ? { OVERMIND_AGENT_HOME: getAgentOvermindHome(agentName) } : {}),
|
|
199
|
-
// NOTE: do NOT pre-seed GLM_API_KEY with '' here. The real value comes from
|
|
200
|
-
// settings_<agent>.json (merged below) or from the agent's .hermes/.env file.
|
|
201
|
-
// Seeding '' here used to win against Object.assign() whenever interpolateEnvVars()
|
|
202
|
-
// returned an empty value for $GLM_API_KEY (e.g. shell parent didn't export it),
|
|
203
|
-
// which silently nulled out getTokenForIndex() and caused EXIT_CODE_1 / 401 errors.
|
|
204
|
-
};
|
|
205
|
-
let tmpSettingsPath = null;
|
|
206
|
-
let tmpMcpPath = null;
|
|
207
|
-
let loadedSettings = null;
|
|
208
|
-
// Capture the RAW (pre-interpolation) settings tokens so getTokenForIndex can
|
|
209
|
-
// fail-loud on unresolved $VAR references and report which one is missing.
|
|
210
|
-
// (Once interpolateEnvVars() runs, $VAR has been replaced with its value, and
|
|
211
|
-
// we lose the information that the user explicitly asked for THAT var.)
|
|
212
|
-
const rawExplicitSettingsTokens = [];
|
|
213
|
-
// TOKEN_KEYS is now imported from ./hermes/providerConfig.ts (extracted module).
|
|
214
|
-
if (agentName) {
|
|
215
|
-
// Locate the per-agent SOUL.md (system prompt). We support the canonical
|
|
216
|
-
// Hermes layout (HERMES_HOME/agents/<name>/SOUL.md) and a one-shot legacy
|
|
217
|
-
// path (HERMES_HOME/agent_<name>/.hermes/SOUL.md) for existing installs.
|
|
218
|
-
// The canonical path wins; the legacy is fallback so we don't break
|
|
219
|
-
// agents that haven't been migrated yet.
|
|
220
|
-
const canonicalSoul = path.join(overmindHermesSubPath, 'SOUL.md');
|
|
221
|
-
const legacySoul = path.join(getSharedHermesHome(), `agent_${agentName}`, '.hermes', 'SOUL.md');
|
|
222
|
-
const claudeSoul = path.join(configPath, '.claude', 'agents', `${agentName}.md`);
|
|
223
|
-
const agentPromptPath = fs.existsSync(claudeSoul)
|
|
224
|
-
? claudeSoul
|
|
225
|
-
: fs.existsSync(canonicalSoul)
|
|
226
|
-
? canonicalSoul
|
|
227
|
-
: fs.existsSync(legacySoul)
|
|
228
|
-
? legacySoul
|
|
229
|
-
: null;
|
|
230
|
-
if (agentPromptPath && fs.existsSync(agentPromptPath)) {
|
|
231
|
-
systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
|
|
232
|
-
// Sync system prompt to canonical layout if loaded from legacy or Claude paths
|
|
233
|
-
if (agentPromptPath !== canonicalSoul) {
|
|
234
|
-
try {
|
|
235
|
-
if (!fs.existsSync(overmindHermesSubPath)) {
|
|
236
|
-
fs.mkdirSync(overmindHermesSubPath, { recursive: true });
|
|
237
|
-
}
|
|
238
|
-
fs.writeFileSync(canonicalSoul, systemPrompt, 'utf8');
|
|
239
|
-
logger.info({ agentName, source: agentPromptPath, target: canonicalSoul }, 'Synced SOUL.md to canonical Hermes path.');
|
|
240
|
-
}
|
|
241
|
-
catch (e) {
|
|
242
|
-
logger.warn({ error: e }, 'Failed to sync SOUL.md to canonical Hermes path');
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
247
|
-
// LECTURE CREDENTIALS HERMES — <HERMES_HOME>/agents/<name>/settings.json
|
|
248
|
-
//
|
|
249
|
-
// Ce chemin est résolu UNE SEULE FOIS en haut de la fonction (agentSettingsPath).
|
|
250
|
-
// Il pointe vers le dossier natif Hermes, PAS vers .claude/.
|
|
251
|
-
// Si le fichier est absent, l'agent tourne sans LLM (erreur claire dans les logs).
|
|
252
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
253
|
-
try {
|
|
254
|
-
// agentSettingsPath = <HERMES_HOME>/agents/<name>/settings.json (défini ligne ~405)
|
|
255
|
-
// ❌ NE PAS changer ceci pour pointer vers .claude/ — c'est intentionnel.
|
|
256
|
-
if (!fs.existsSync(agentSettingsPath)) {
|
|
257
|
-
logger.error({
|
|
258
|
-
agentName,
|
|
259
|
-
expected: agentSettingsPath,
|
|
260
|
-
hermesHome: overmindHermesSubPath,
|
|
261
|
-
action: `Créer ${agentSettingsPath} avec les credentials. ` +
|
|
262
|
-
`Format minimal : { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-cp-...", ` +
|
|
263
|
-
`"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", ` +
|
|
264
|
-
`"ANTHROPIC_MODEL": "MiniMax-M3", "ANTHROPIC_PROVIDER": "minimax-cn" }, ` +
|
|
265
|
-
`"agent": "${agentName}", "runner": "hermes" }`,
|
|
266
|
-
}, '[HERMES] ❌ settings.json introuvable dans le dossier Hermes natif. ' +
|
|
267
|
-
'Hermes NE cherche PAS dans .claude/ — uniquement dans .overmind/hermes/agents/<name>/settings.json. ' +
|
|
268
|
-
'Voir la documentation dans le commentaire du constructeur NousHermesRunner.');
|
|
269
|
-
}
|
|
270
|
-
if (fs.existsSync(agentSettingsPath)) {
|
|
271
|
-
// Read the RAW settings (pre-interpolation) to capture $VAR references
|
|
272
|
-
// before they get resolved. We iterate the FULL TOKEN_KEYS list (100%
|
|
273
|
-
// exhaustive) so any env-var name the runner knows about gets captured
|
|
274
|
-
// for fail-loud validation later.
|
|
275
|
-
const rawSettings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
|
|
276
|
-
if (rawSettings.env) {
|
|
277
|
-
for (const tk of TOKEN_KEYS) {
|
|
278
|
-
const v = rawSettings.env[tk];
|
|
279
|
-
if (v && typeof v === 'string' && v.length > 0) {
|
|
280
|
-
rawExplicitSettingsTokens.push({ key: tk, value: v });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
let settings = rawSettings;
|
|
285
|
-
settings = interpolateEnvVars(settings);
|
|
286
|
-
loadedSettings = settings;
|
|
287
|
-
// Fichier temporaire interpolé (valeurs $VAR résolues) — nettoyé après le spawn.
|
|
288
|
-
// Situé dans le même dossier que settings.json : <HERMES_HOME>/agents/<name>/
|
|
289
|
-
const tempSettings = path.join(path.dirname(agentSettingsPath), `settings_tmp.json`);
|
|
290
|
-
fs.writeFileSync(tempSettings, JSON.stringify(settings, null, 2));
|
|
291
|
-
tmpSettingsPath = tempSettings;
|
|
292
|
-
this.tempFiles.push(tempSettings);
|
|
293
|
-
if (settings.env) {
|
|
294
|
-
Object.assign(agentCustomEnv, settings.env);
|
|
295
|
-
if (!options.model && settings.env.MODEL) {
|
|
296
|
-
agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
300
|
-
// CONFIG MCP — Résolution dans l'ordre suivant :
|
|
301
|
-
// 1. <HERMES_HOME>/agents/<name>/.mcp.json (override par agent)
|
|
302
|
-
// 2. <OVERMIND_WORKSPACE>/.mcp.json filtré par enabledMcpjsonServers
|
|
303
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
304
|
-
const agentMcpPath = path.join(overmindHermesSubPath, '.mcp.json');
|
|
305
|
-
if (fs.existsSync(agentMcpPath)) {
|
|
306
|
-
// Override MCP par agent : <HERMES_HOME>/agents/<name>/.mcp.json
|
|
307
|
-
const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
|
|
308
|
-
fs.writeFileSync(tempMcp, fs.readFileSync(agentMcpPath, 'utf8'));
|
|
309
|
-
tmpMcpPath = tempMcp;
|
|
310
|
-
this.tempFiles.push(tempMcp);
|
|
311
|
-
}
|
|
312
|
-
else if (settings.enableAllProjectMcpServers === false &&
|
|
313
|
-
Array.isArray(settings.enabledMcpjsonServers)) {
|
|
314
|
-
// Filtre le .mcp.json du workspace selon enabledMcpjsonServers
|
|
315
|
-
const projectMcpPath = resolveConfigPath(CONFIG.CLAUDE.PATHS.MCP, configPath);
|
|
316
|
-
if (fs.existsSync(projectMcpPath)) {
|
|
317
|
-
const fullMcp = JSON.parse(fs.readFileSync(projectMcpPath, 'utf8'));
|
|
318
|
-
const filteredMcp = { mcpServers: {} };
|
|
319
|
-
for (const serverName of settings.enabledMcpjsonServers) {
|
|
320
|
-
if (fullMcp.mcpServers && fullMcp.mcpServers[serverName]) {
|
|
321
|
-
filteredMcp.mcpServers[serverName] = fullMcp.mcpServers[serverName];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const tempMcp = path.join(path.dirname(agentSettingsPath), `mcp_tmp.json`);
|
|
325
|
-
fs.writeFileSync(tempMcp, JSON.stringify(filteredMcp, null, 2));
|
|
326
|
-
tmpMcpPath = tempMcp;
|
|
327
|
-
this.tempFiles.push(tempMcp);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch (e) {
|
|
333
|
-
logger.warn({ error: e }, `Failed to process settings/mcp configurations for Hermes agent ${agentName}`);
|
|
334
|
-
}
|
|
335
|
-
// Charge le .env de l'agent (dans <HERMES_HOME>/agents/<name>/.env) en FALLBACK UNIQUEMENT.
|
|
336
|
-
// Les clés déjà définies dans settings.json ne sont PAS écrasées.
|
|
337
|
-
//
|
|
338
|
-
// CRITICAL (2.8.29): The .hermes/.env file is a STALE WRITE of the previous
|
|
339
|
-
// spawn — it gets re-written by the runner itself at line ~1013, but if a
|
|
340
|
-
// user's first run was for Z.AI and they later switch the agent to MiniMax
|
|
341
|
-
// CN, the stale .hermes/.env from the previous run gets re-loaded into
|
|
342
|
-
// agentCustomEnv HERE, OVERWRITING the MiniMax settings that were just merged
|
|
343
|
-
// from settings_<agent>.json in the block above (line ~412). Symptom: the
|
|
344
|
-
// agent silently reverts to the old provider (e.g. Z.AI glm-5.1) on every
|
|
345
|
-
// spawn, causing persistent 401s.
|
|
346
|
-
//
|
|
347
|
-
// Fix: only load keys from .hermes/.env that are NOT already in agentCustomEnv
|
|
348
|
-
// (i.e. settings_<agent>.json wins; .hermes/.env is a fallback for unrelated
|
|
349
|
-
// custom vars the user might have set manually).
|
|
350
|
-
const envPath = path.join(overmindHermesSubPath, '.env');
|
|
351
|
-
if (fs.existsSync(envPath)) {
|
|
352
|
-
try {
|
|
353
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
354
|
-
content.split('\n').forEach((line) => {
|
|
355
|
-
const trimmed = line.trim();
|
|
356
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
357
|
-
return;
|
|
358
|
-
const eqIdx = trimmed.indexOf('=');
|
|
359
|
-
if (eqIdx === -1)
|
|
360
|
-
return;
|
|
361
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
362
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
363
|
-
if (value.startsWith('"') && value.endsWith('"'))
|
|
364
|
-
value = value.slice(1, -1);
|
|
365
|
-
else if (value.startsWith("'") && value.endsWith("'"))
|
|
366
|
-
value = value.slice(1, -1);
|
|
367
|
-
if (key && agentCustomEnv[key] === undefined) {
|
|
368
|
-
// Only fill in keys that settings_<agent>.json did NOT set.
|
|
369
|
-
// settings_<agent>.json is the user's source of truth; .hermes/.env
|
|
370
|
-
// is a stale write from a previous spawn and must not override it.
|
|
371
|
-
agentCustomEnv[key] = value;
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
catch (e) {
|
|
376
|
-
logger.warn({ envPath, error: e }, 'Failed to read agent env file');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
resolvedModel = agentCustomEnv.MODEL || agentCustomEnv.ANTHROPIC_MODEL;
|
|
380
|
-
resolvedProvider = agentCustomEnv.PROVIDER || agentCustomEnv.ANTHROPIC_PROVIDER;
|
|
381
|
-
if (resolvedProvider && (resolvedProvider.startsWith('http://') || resolvedProvider.startsWith('https://'))) {
|
|
382
|
-
if (resolvedProvider.includes('minimax')) {
|
|
383
|
-
resolvedProvider = 'minimax-cn';
|
|
384
|
-
}
|
|
385
|
-
else if (resolvedProvider.includes('z.ai') || resolvedProvider.includes('bigmodel')) {
|
|
386
|
-
resolvedProvider = 'zai';
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
resolvedProvider = undefined;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const finalModel = options.model || resolvedModel || CONFIG.HERMES.DEFAULT_MODEL;
|
|
394
|
-
const finalPrompt = systemPrompt ? `${systemPrompt}\n\n[USER QUERY]:\n${prompt}` : prompt;
|
|
395
|
-
const cliPrompt = finalPrompt;
|
|
396
|
-
// Build CLI args: chat -q (persistent session, NOT -z oneshot)
|
|
397
|
-
// -z + --resume doesn't work — resume is ignored in oneshot mode
|
|
398
|
-
//
|
|
399
|
-
// 2.8.33: RE-ADD --provider for MiniMax/Z.AI cases. The empirical 2.8.28
|
|
400
|
-
// observation ("`hermes chat -q --provider minimax-cn` 401s while `--yolo`
|
|
401
|
-
// alone works") was based on a specific `Hermes-MiniMax-2.bat` test
|
|
402
|
-
// where the env was set perfectly. In our sniperbot_analyst scenario
|
|
403
|
-
// (a real production setup with stale state, multiple providers in the
|
|
404
|
-
// auth.json pool, and Hermes upstream's auto-router that picks
|
|
405
|
-
// openrouter for `MiniMax-M3` as an OpenRouter alias), NOT passing
|
|
406
|
-
// --provider makes Hermes upstream fall back to openrouter, which
|
|
407
|
-
// then 401s because the OPENROUTER_API_KEY is purged.
|
|
408
|
-
//
|
|
409
|
-
// So: pass --provider when we have a resolved provider that matches
|
|
410
|
-
// a registered plugin. This forces Hermes upstream to use the right
|
|
411
|
-
// plugin profile (and the right credential pool bucket).
|
|
412
|
-
const cleanArgs = ['chat', '-q', cliPrompt, '-Q'];
|
|
413
|
-
cleanArgs.push('--model', finalModel);
|
|
414
|
-
if (options.provider || resolvedProvider) {
|
|
415
|
-
const provider = (options.provider || resolvedProvider);
|
|
416
|
-
cleanArgs.push('--provider', provider);
|
|
417
|
-
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).');
|
|
418
|
-
}
|
|
419
|
-
// ============================================================
|
|
420
|
-
// 2.8.36 — TOOLSET DISCOVERY (LOG ONLY — NOT PASSED TO CLI)
|
|
421
|
-
// ============================================================
|
|
422
|
-
// Previously we passed MCP server names via `--toolsets` to Hermes.
|
|
423
|
-
// This caused `Warning: Unknown toolsets: mcp-memory, mcp-discord, ...`
|
|
424
|
-
// because Hermes's toolset registry does NOT use the `mcp-<name>` format.
|
|
425
|
-
//
|
|
426
|
-
// The MCP servers load correctly WITHOUT `--toolsets` because:
|
|
427
|
-
// 1. `HERMES_HOME` is overridden to the per-agent isolated run home.
|
|
428
|
-
// 2. `filterConfigYaml` writes a filtered `config.yaml` that lists only
|
|
429
|
-
// the allowed MCP servers under `mcp_servers:`.
|
|
430
|
-
// 3. Hermes reads `config.yaml` at startup and connects to every server
|
|
431
|
-
// listed there — no `--toolsets` flag needed.
|
|
432
|
-
//
|
|
433
|
-
// We still build `toolsetList` for diagnostic logging so that the log
|
|
434
|
-
// shows which MCP servers are expected for this agent run.
|
|
435
|
-
//
|
|
436
|
-
// Source of truth for the list:
|
|
437
|
-
// - If settings has `enabledMcpjsonServers: [...non-empty...]`, use that.
|
|
438
|
-
// - Else, if `enableAllProjectMcpServers: true`, use ALL server names
|
|
439
|
-
// from the Hermes config.yaml.
|
|
440
|
-
// - Else, skip — no toolset hint.
|
|
441
|
-
const toolsetList = [];
|
|
442
|
-
// Read the canonical settings.json we just wrote to find the MCP server hints.
|
|
443
|
-
// (settingsJson lives inside the `if (agentName)` block above, so we re-read
|
|
444
|
-
// it from disk here to keep the args-building code path-independent.)
|
|
445
|
-
const effectiveSettings = {};
|
|
446
|
-
if (agentName) {
|
|
447
|
-
try {
|
|
448
|
-
if (loadedSettings) {
|
|
449
|
-
if (Array.isArray(loadedSettings.enabledMcpjsonServers)) {
|
|
450
|
-
effectiveSettings.enabledMcpjsonServers = loadedSettings.enabledMcpjsonServers.filter(Boolean);
|
|
451
|
-
}
|
|
452
|
-
if (loadedSettings.enableAllProjectMcpServers !== undefined) {
|
|
453
|
-
effectiveSettings.enableAllProjectMcpServers = loadedSettings.enableAllProjectMcpServers === true;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
const canonicalPath = path.join(overmindHermesSubPath, 'settings.json');
|
|
458
|
-
if (fs.existsSync(canonicalPath)) {
|
|
459
|
-
const raw = JSON.parse(fs.readFileSync(canonicalPath, 'utf8'));
|
|
460
|
-
if (Array.isArray(raw.enabledMcpjsonServers)) {
|
|
461
|
-
effectiveSettings.enabledMcpjsonServers = raw.enabledMcpjsonServers.filter(Boolean);
|
|
462
|
-
}
|
|
463
|
-
if (raw.enableAllProjectMcpServers !== undefined) {
|
|
464
|
-
effectiveSettings.enableAllProjectMcpServers = raw.enableAllProjectMcpServers === true;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
catch (e) {
|
|
470
|
-
logger.warn({ error: e }, '[HERMES_ARGS] Failed to read settings for toolset hints.');
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const enabledInSettings = effectiveSettings.enabledMcpjsonServers || [];
|
|
474
|
-
if (enabledInSettings.length > 0) {
|
|
475
|
-
toolsetList.push(...enabledInSettings);
|
|
476
|
-
}
|
|
477
|
-
else if (effectiveSettings.enableAllProjectMcpServers === true) {
|
|
478
|
-
// ============================================================
|
|
479
|
-
// 2.8.39 — READ MCP SERVERS FROM HERMES CONFIG.YAML (NOT .mcp.json)
|
|
480
|
-
// ============================================================
|
|
481
|
-
// Earlier (2.8.36) we read `Workflow/.mcp.json` for the toolset names,
|
|
482
|
-
// but that's the Overmind-CLI/Claude-Code format. Hermes upstream's
|
|
483
|
-
// `--toolsets` flag expects names that match the `mcp_servers:` block
|
|
484
|
-
// of the **Hermes config.yaml** (i.e. <HERMES_HOME>/config.yaml).
|
|
485
|
-
// The two registries use DIFFERENT names — `serveur_discord` vs
|
|
486
|
-
// `discord-server`, `X` vs `x_server`, etc. — and the Hermes one is
|
|
487
|
-
// the one Hermes actually recognises. Passing `serveur_discord` to
|
|
488
|
-
// `--toolsets` produces the `Warning: Unknown toolsets` we saw at
|
|
489
|
-
// 17:50, which silently dropped those tools and the agent reported
|
|
490
|
-
// "j'ai pas de MCP".
|
|
491
|
-
//
|
|
492
|
-
// Read the Hermes config.yaml `mcp_servers:` block and pass its keys.
|
|
493
|
-
// Fall back to `.mcp.json` only if the Hermes config has no mcp_servers.
|
|
494
|
-
try {
|
|
495
|
-
const hermesConfigPath = path.join(getSharedHermesHome(), 'config.yaml');
|
|
496
|
-
if (fs.existsSync(hermesConfigPath)) {
|
|
497
|
-
const yamlText = fs.readFileSync(hermesConfigPath, 'utf8');
|
|
498
|
-
// Line-by-line YAML parser for the `mcp_servers` section (handles comments, varying indentation, and exits correctly)
|
|
499
|
-
const lines = yamlText.split(/\r?\n/);
|
|
500
|
-
let inMcpServers = false;
|
|
501
|
-
const serverNames = [];
|
|
502
|
-
for (const line of lines) {
|
|
503
|
-
const trimmed = line.trim();
|
|
504
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
505
|
-
continue;
|
|
506
|
-
if (line.match(/^mcp_servers:\s*$/) || line.match(/^mcp_servers:\s*#.*$/)) {
|
|
507
|
-
inMcpServers = true;
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (inMcpServers) {
|
|
511
|
-
const indentMatch = line.match(/^(\s+)/);
|
|
512
|
-
if (!indentMatch) {
|
|
513
|
-
inMcpServers = false;
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*:/);
|
|
517
|
-
if (keyMatch) {
|
|
518
|
-
serverNames.push(keyMatch[1]);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (serverNames.length > 0) {
|
|
523
|
-
toolsetList.push(...serverNames);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (toolsetList.length === 0) {
|
|
527
|
-
// Fallback: read .mcp.json (Overmind format) for any servers not in
|
|
528
|
-
// Hermes config. This won't help if names don't match, but at least
|
|
529
|
-
// surfaces the names to the user via the warning.
|
|
530
|
-
const projectMcpPath = path.join(getWorkspaceDir(), '.mcp.json');
|
|
531
|
-
if (fs.existsSync(projectMcpPath)) {
|
|
532
|
-
const projectMcp = JSON.parse(fs.readFileSync(projectMcpPath, 'utf8'));
|
|
533
|
-
const allServers = Object.keys(projectMcp?.mcpServers || {});
|
|
534
|
-
toolsetList.push(...allServers);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
catch (e) {
|
|
539
|
-
logger.warn({ error: e }, '[HERMES_ARGS] Failed to read Hermes config.yaml for toolsets, continuing without --toolsets.');
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
if (toolsetList.length > 0) {
|
|
543
|
-
// Log which MCP servers are expected for this run — do NOT pass to CLI.
|
|
544
|
-
// Hermes loads them via the isolated config.yaml; passing --toolsets
|
|
545
|
-
// would produce `Warning: Unknown toolsets: mcp-<name>` noise with no benefit.
|
|
546
|
-
logger.info({ agentName, expectedMcpServers: toolsetList }, '[HERMES_ARGS] Expected MCP servers for this run (loaded via config.yaml, NOT via --toolsets).');
|
|
547
|
-
}
|
|
548
|
-
if (sessionId)
|
|
549
|
-
cleanArgs.push('--resume', sessionId);
|
|
550
|
-
// Token fallback setup (same as ClaudeRunner)
|
|
551
|
-
const FALLBACK_KEYS = ['AUTH_FALLBACK_1', 'AUTH_FALLBACK_2', 'AUTH_FALLBACK_3'];
|
|
552
|
-
// ============================================================
|
|
553
|
-
// TOKEN PREFIX → PROVIDER MAPPING (Hermes v0.16.0)
|
|
554
|
-
// ============================================================
|
|
555
|
-
// The token PREFIX is the most reliable signal for the provider.
|
|
556
|
-
// We detect it from the literal value, NOT from a hardcoded env-var name,
|
|
557
|
-
// because the same env-var name (e.g. ANTHROPIC_AUTH_TOKEN) can be reused
|
|
558
|
-
// across providers when the user copy-pastes keys from one service to another.
|
|
559
|
-
//
|
|
560
|
-
// Convention (observed in real .env files and provider dashboards):
|
|
561
|
-
// MiniMax → "sk-cp-..." → env MINIMAX_API_KEY (or MINIMAX_CN_API_KEY)
|
|
562
|
-
// Z.AI / GLM → "32hex.32hex" → env ZAI_ANTHROPIC_FALLBACK_KEY (or GLM_API_KEY)
|
|
563
|
-
// e.g. "c78a134949fc4c369911c24e9fa4b84c.OZhHX5Obs6qF1ISt"
|
|
564
|
-
// Z.AI alt → 32-char hex (single block, no dot) → env ZAI_ANTHROPIC_FALLBACK_KEY
|
|
565
|
-
// e.g. "5f650035e5a845549e4765184d8179b1"
|
|
566
|
-
// Anthropic → "sk-ant-..." → env ANTHROPIC_AUTH_TOKEN
|
|
567
|
-
// OpenAI → "sk-..." → env OPENAI_API_KEY (no -ant, no -cp)
|
|
568
|
-
// OpenRouter → "sk-or-..." → env OPENROUTER_API_KEY (BLOCKED for LLM)
|
|
569
|
-
// Mistral → (variable) → env MISTRAL_API_KEY_*
|
|
570
|
-
// Other → unknown → env ANTHROPIC_AUTH_TOKEN (default Anthropic)
|
|
571
|
-
//
|
|
572
|
-
// NOTE: This function-local copy mirrors the canonical implementation in
|
|
573
|
-
// src/services/hermesTokenResolver.ts. The runner uses these local closures
|
|
574
|
-
// so it doesn't have to thread env/logger through every call site; the
|
|
575
|
-
// canonical exported versions exist for testing and for any future caller
|
|
576
|
-
// that wants the same behavior without instantiating NousHermesRunner.
|
|
577
|
-
const TOKEN_PREFIX_PROVIDERS = [
|
|
578
|
-
// Z.AI: c78a134949fc4c369911c24e9fa4b84c.OZhHX5Obs6qF1ISt (32hex.32hex — 2 blocks)
|
|
579
|
-
{ test: (t) => /^[0-9a-f]{32}\.[0-9a-zA-Z]+$/i.test(t), provider: 'zai', envKey: 'ZAI_ANTHROPIC_FALLBACK_KEY' },
|
|
580
|
-
// Z.AI: 5f6500...q3m3 (32-char hex single block, no dot, no dashes)
|
|
581
|
-
{ test: (t) => /^[0-9a-f]{32}$/i.test(t), provider: 'zai', envKey: 'ZAI_ANTHROPIC_FALLBACK_KEY' },
|
|
582
|
-
// MiniMax: sk-cp-...qNmo (with cp prefix)
|
|
583
|
-
{ test: (t) => t.startsWith('sk-cp-'), provider: 'minimax', envKey: 'MINIMAX_API_KEY' },
|
|
584
|
-
// MiniMax: sk-mm-... (alternative prefix)
|
|
585
|
-
{ test: (t) => t.startsWith('sk-mm-'), provider: 'minimax', envKey: 'MINIMAX_API_KEY' },
|
|
586
|
-
// Anthropic
|
|
587
|
-
{ test: (t) => t.startsWith('sk-ant-'), provider: 'anthropic', envKey: 'ANTHROPIC_AUTH_TOKEN' },
|
|
588
|
-
// OpenRouter (BLOCKED for LLM, but we still detect it for diagnostic)
|
|
589
|
-
{ test: (t) => t.startsWith('sk-or-'), provider: 'openrouter', envKey: 'OPENROUTER_API_KEY' },
|
|
590
|
-
// OpenAI (no -ant, no -cp, no -or)
|
|
591
|
-
{ test: (t) => t.startsWith('sk-'), provider: 'openai', envKey: 'OPENAI_API_KEY' },
|
|
592
|
-
// Generic 16+ hex without dot — probably a Z.AI token variant
|
|
593
|
-
{ test: (t) => /^[0-9a-f]{16,}$/i.test(t), provider: 'zai', envKey: 'ZAI_ANTHROPIC_FALLBACK_KEY' },
|
|
594
|
-
];
|
|
595
|
-
function detectTokenProvider(token) {
|
|
596
|
-
for (const rule of TOKEN_PREFIX_PROVIDERS) {
|
|
597
|
-
if (rule.test(token))
|
|
598
|
-
return { provider: rule.provider, envKey: rule.envKey };
|
|
599
|
-
}
|
|
600
|
-
return { provider: 'unknown', envKey: 'ANTHROPIC_AUTH_TOKEN' };
|
|
601
|
-
}
|
|
602
|
-
// ============================================================
|
|
603
|
-
// TOKEN RESOLUTION ORDER (settings.env first, then process.env, then detection)
|
|
604
|
-
// ============================================================
|
|
605
|
-
// See src/services/hermesTokenResolver.ts for the canonical exported version
|
|
606
|
-
// and the rationale for the 3-pass strategy. The local closure below
|
|
607
|
-
// captures agentCustomEnv, TOKEN_KEYS, agentName, logger, and tmpSettingsPath
|
|
608
|
-
// from the enclosing scope so call sites stay terse.
|
|
609
|
-
function resolveTokenWithDetection(explicitSettingsTokens) {
|
|
610
|
-
// Step 1: settings_<agent>.json env block takes ABSOLUTE priority
|
|
611
|
-
// (whatever the user explicitly set in their agent config wins)
|
|
612
|
-
if (explicitSettingsTokens.length > 0) {
|
|
613
|
-
const t = explicitSettingsTokens[0];
|
|
614
|
-
// If the value is a $VAR reference, RESOLVE it against process.env
|
|
615
|
-
let resolvedValue = t.value;
|
|
616
|
-
if (typeof t.value === 'string' && t.value.startsWith('$')) {
|
|
617
|
-
const varName = t.value.slice(1);
|
|
618
|
-
const fromEnv = process.env[varName];
|
|
619
|
-
if (!fromEnv || fromEnv.length === 0) {
|
|
620
|
-
// FAIL LOUD — do not silently fall back. The user explicitly asked
|
|
621
|
-
// for THIS var, and it doesn't exist. Surface the misconfiguration.
|
|
622
|
-
logger.error({
|
|
623
|
-
agentName,
|
|
624
|
-
requestedVar: varName,
|
|
625
|
-
requestedKey: t.key,
|
|
626
|
-
settingsPath: tmpSettingsPath,
|
|
627
|
-
}, '[FAIL-LOUD] settings_<agent>.json references $' + varName + ' but it is not set in process.env. ' +
|
|
628
|
-
'Either export it in the parent .env, or fix the reference in settings_<agent>.json. ' +
|
|
629
|
-
'Refusing to fall back to a different credential.');
|
|
630
|
-
throw new Error(`MISSING_ENV_VAR: settings_<agent>.json env.${t.key}="$` + varName + '" ' +
|
|
631
|
-
`but process.env.${varName} is empty. Add it to /home/demon/.overmind/.env or fix the settings reference.`);
|
|
632
|
-
}
|
|
633
|
-
resolvedValue = fromEnv;
|
|
634
|
-
logger.info({ agentName, sourceKey: t.key, referencedVar: varName, resolvedLen: resolvedValue.length }, '[TOKEN_RESOLVER] Resolved $VAR reference from settings_<agent>.json against process.env.');
|
|
635
|
-
}
|
|
636
|
-
const detected = detectTokenProvider(resolvedValue);
|
|
637
|
-
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.');
|
|
638
|
-
return { tokenEnvKey: t.key, tokenValue: resolvedValue, detectedProvider: detected.provider, source: 'settings-explicit' };
|
|
639
|
-
}
|
|
640
|
-
const candidates = [];
|
|
641
|
-
for (const tk of TOKEN_KEYS) {
|
|
642
|
-
const v = agentCustomEnv[tk];
|
|
643
|
-
if (v && typeof v === 'string' && v.length > 0) {
|
|
644
|
-
candidates.push({ key: tk, value: v, detected: detectTokenProvider(v) });
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (candidates.length === 0)
|
|
648
|
-
return null;
|
|
649
|
-
// Pass A: any candidate whose env-var name already matches its detected provider
|
|
650
|
-
// (e.g. MINIMAX_API_KEY=sk-cp-... → detected.envKey='MINIMAX_API_KEY' → match).
|
|
651
|
-
// TOKEN_KEYS ordering means provider-specific keys come first, so this preserves
|
|
652
|
-
// the user's explicit choice over a generic-key re-map.
|
|
653
|
-
for (const c of candidates) {
|
|
654
|
-
if (c.detected.provider !== 'unknown' && c.detected.envKey === c.key) {
|
|
655
|
-
return {
|
|
656
|
-
tokenEnvKey: c.key,
|
|
657
|
-
tokenValue: c.value,
|
|
658
|
-
detectedProvider: c.detected.provider,
|
|
659
|
-
source: 'env-fallback',
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Pass B: re-map the first candidate to the right provider env-var name.
|
|
664
|
-
const first = candidates[0];
|
|
665
|
-
if (first.detected.provider !== 'unknown' && first.detected.envKey !== first.key) {
|
|
666
|
-
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.');
|
|
667
|
-
return {
|
|
668
|
-
tokenEnvKey: first.detected.envKey,
|
|
669
|
-
tokenValue: first.value,
|
|
670
|
-
detectedProvider: first.detected.provider,
|
|
671
|
-
source: 'detected',
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
// Pass C (rare): all candidates have provider='unknown' or envKey already matches.
|
|
675
|
-
return {
|
|
676
|
-
tokenEnvKey: first.key,
|
|
677
|
-
tokenValue: first.value,
|
|
678
|
-
detectedProvider: first.detected.provider,
|
|
679
|
-
source: 'env-fallback',
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
const getAvailableFallbacks = () => {
|
|
683
|
-
const fb = [];
|
|
684
|
-
for (const k of FALLBACK_KEYS) {
|
|
685
|
-
const v = agentCustomEnv[k];
|
|
686
|
-
if (v && typeof v === 'string' && v.length > 0)
|
|
687
|
-
fb.push({ key: k, value: v });
|
|
688
|
-
}
|
|
689
|
-
return fb;
|
|
690
|
-
};
|
|
691
|
-
const getTokenForIndex = (idx) => {
|
|
692
|
-
if (idx === 0) {
|
|
693
|
-
// Use the RAW (pre-interpolation) settings tokens captured at load time.
|
|
694
|
-
// This way, if the user set "$GLM_API_KEY_Y" in settings, we can detect
|
|
695
|
-
// that it was a $VAR reference and fail-loud if it's not in process.env.
|
|
696
|
-
// Reading tmpSettingsPath here would be too late (it's deleted by
|
|
697
|
-
// cleanupTmpFiles() before this runs in some code paths).
|
|
698
|
-
const explicitSettingsTokens = rawExplicitSettingsTokens;
|
|
699
|
-
// The resolver may throw MISSING_ENV_VAR if a $VAR in settings is unresolved.
|
|
700
|
-
// We catch and surface it as a clear NO_LLM_TOKEN error (the caller wraps it).
|
|
701
|
-
const resolved = resolveTokenWithDetection(explicitSettingsTokens);
|
|
702
|
-
if (resolved)
|
|
703
|
-
return { tokenEnvKey: resolved.tokenEnvKey, tokenValue: resolved.tokenValue };
|
|
704
|
-
// Diagnostic: dump which keys were checked and their (masked) state so a future
|
|
705
|
-
// EXIT_CODE_1 + 401 has a clear breadcrumb back to the empty/missing credential.
|
|
706
|
-
const probe = TOKEN_KEYS.map((tk) => {
|
|
707
|
-
const v = agentCustomEnv[tk];
|
|
708
|
-
if (typeof v !== 'string' || v.length === 0)
|
|
709
|
-
return `${tk}=<empty>`;
|
|
710
|
-
return `${tk}=<set len=${v.length}>`;
|
|
711
|
-
});
|
|
712
|
-
logger.error({ agentName, checked: probe, settingsPath: tmpSettingsPath, envFile: path.join(overmindHermesSubPath, '.env') }, 'No usable LLM token found. Check settings_<agent>.json env block and the agent .hermes/.env file.');
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
const fb = getAvailableFallbacks();
|
|
716
|
-
return fb[idx - 1] ? { tokenEnvKey: fb[idx - 1].key, tokenValue: fb[idx - 1].value } : null;
|
|
717
|
-
};
|
|
718
|
-
const isRetryableError = (stderr) => {
|
|
719
|
-
const lower = stderr.toLowerCase();
|
|
720
|
-
return lower.includes('401') || lower.includes('unauthorized') ||
|
|
721
|
-
lower.includes('invalid api key') || lower.includes('authentication failed') ||
|
|
722
|
-
lower.includes('invalid authentication') || lower.includes('429') ||
|
|
723
|
-
lower.includes('rate limit') || lower.includes('quota exhausted') ||
|
|
724
|
-
lower.includes('limit exhausted') || lower.includes('503') ||
|
|
725
|
-
lower.includes('service unavailable') || lower.includes('500') ||
|
|
726
|
-
lower.includes('internal server error');
|
|
727
|
-
};
|
|
728
|
-
// HERMES_HOME setup — the SHARED root, not the per-agent home.
|
|
729
|
-
// Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`, etc.
|
|
730
|
-
// relative to this single root. We do NOT seed `agentCustomEnv.HERMES_HOME`
|
|
731
|
-
// here anymore because spawnHermes() sets it explicitly from getSharedHermesHome().
|
|
732
|
-
const sharedHome = getSharedHermesHome();
|
|
733
|
-
if (!fs.existsSync(overmindHermesSubPath))
|
|
734
|
-
fs.mkdirSync(overmindHermesSubPath, { recursive: true });
|
|
735
|
-
// ============================================================
|
|
736
|
-
// 2.8.37 — BOOTSTRAP MINIMAL config.yaml IN OVERRIDDEN HERMES_HOME
|
|
737
|
-
// ============================================================
|
|
738
|
-
// When we redirect HERMES_HOME to <workspace>/.overmind/hermes/ (the
|
|
739
|
-
// Overmind-shared root), Hermes upstream has NO config.yaml in that
|
|
740
|
-
// path yet. It only writes one after the FIRST successful startup. But
|
|
741
|
-
// for the FIRST startup, the agent would have ZERO MCP servers
|
|
742
|
-
// registered (because mcp_servers: lives in config.yaml). That was the
|
|
743
|
-
// root cause of "le sniperbot n'a pas de MCP" — the sniperbot_analyst
|
|
744
|
-
// was being spawned with HERMES_HOME pointing at an empty dir.
|
|
745
|
-
//
|
|
746
|
-
// Fix: at spawn time, if <sharedHome>/config.yaml doesn't exist, write
|
|
747
|
-
// a minimal one copied from the default ~/.hermes/config.yaml. The user
|
|
748
|
-
// can also point OVERMIND_HERMES_CONFIG_TEMPLATE at a custom file. We
|
|
749
|
-
// preserve anything Hermes has already written in <sharedHome>/config.yaml
|
|
750
|
-
// (e.g. if the user already has a real config there, we don't overwrite it).
|
|
751
|
-
const sharedConfigPath = path.join(sharedHome, 'config.yaml');
|
|
752
|
-
if (!fs.existsSync(sharedConfigPath)) {
|
|
753
|
-
// Look for a source config to copy: default Hermes home, then env override
|
|
754
|
-
const defaultHermesHome = process.platform === 'win32'
|
|
755
|
-
? path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || '', 'AppData', 'Local'), 'hermes')
|
|
756
|
-
: path.join(process.env.HOME || '~', '.hermes');
|
|
757
|
-
const defaultConfigPath = process.env.OVERMIND_HERMES_CONFIG_TEMPLATE
|
|
758
|
-
|| path.join(defaultHermesHome, 'config.yaml');
|
|
759
|
-
if (fs.existsSync(defaultConfigPath)) {
|
|
760
|
-
try {
|
|
761
|
-
fs.copyFileSync(defaultConfigPath, sharedConfigPath);
|
|
762
|
-
logger.info({ sharedHome, sourceConfig: defaultConfigPath }, '[HERMES_HOME] Bootstrapped minimal config.yaml from default Hermes home (2.8.37).');
|
|
763
|
-
}
|
|
764
|
-
catch (e) {
|
|
765
|
-
logger.warn({ error: e, defaultConfigPath }, '[HERMES_HOME] Failed to bootstrap config.yaml; will create empty one.');
|
|
766
|
-
fs.writeFileSync(sharedConfigPath, 'mcp_servers: {}\n', 'utf8');
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
// No default config to copy — write a stub that the user can fill in.
|
|
771
|
-
fs.writeFileSync(sharedConfigPath, 'mcp_servers: {}\n', 'utf8');
|
|
772
|
-
logger.warn({ defaultConfigPath, sharedConfigPath }, '[HERMES_HOME] No default Hermes config.yaml found. Wrote empty stub. MCP servers will not be available until you populate this file.');
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
// HOME / USERPROFILE override: point Hermes at the parent .overmind dir,
|
|
776
|
-
// NOT the cwd. This makes relative .hermes lookups inside Hermes
|
|
777
|
-
// (e.g. `~/.hermes/.env` resolution) resolve to the same canonical
|
|
778
|
-
// location regardless of where the spawn came from.
|
|
779
|
-
if (process.platform === 'win32')
|
|
780
|
-
agentCustomEnv.USERPROFILE = overmindHermesPath;
|
|
781
|
-
else
|
|
782
|
-
agentCustomEnv.HOME = overmindHermesPath;
|
|
783
|
-
// AbortSignal
|
|
784
|
-
if (options.signal?.aborted)
|
|
785
|
-
return Promise.reject(new Error('ABORTED'));
|
|
786
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
787
|
-
// MISE À JOUR settings.json Hermes natif (2.8.45)
|
|
788
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
789
|
-
//
|
|
790
|
-
// Le runner LIT et ÉCRIT dans le MÊME fichier :
|
|
791
|
-
// <HERMES_HOME>/agents/<name>/settings.json
|
|
792
|
-
//
|
|
793
|
-
// Cycle complet :
|
|
794
|
-
// 1. LECTURE : settings.json → interpolation $VAR → agentCustomEnv
|
|
795
|
-
// 2. DÉTECTION: token prefix (sk-cp-* → MiniMax, 32hex → Z.AI, etc.)
|
|
796
|
-
// 3. INJECTION: MINIMAX_CN_API_KEY, MINIMAX_CN_BASE_URL, etc. auto-injectés
|
|
797
|
-
// 4. ÉCRITURE : settings.json mis à jour avec les clés injectées
|
|
798
|
-
// 5. SPAWN : hermes chat -q avec HERMES_HOME = runs/<name>/
|
|
799
|
-
//
|
|
800
|
-
// ❌ Overmind NE convertit PAS depuis .claude/ — c'est le chemin Claude/Kilo.
|
|
801
|
-
// ✅ settings.json est géré par l'utilisateur ET mis à jour par Overmind.
|
|
802
|
-
// Les $VAR restent en tant que littéraux après la première résolution.
|
|
803
|
-
// Pour changer un credential, éditer directement settings.json.
|
|
804
|
-
//
|
|
805
|
-
// Autres fichiers gérés par Hermes upstream (ne pas toucher) :
|
|
806
|
-
// config.yaml, auth.json, sessions/, logs/
|
|
807
|
-
if (agentName) {
|
|
808
|
-
const agentHome = overmindHermesSubPath; // = <HERMES_HOME>/agents/<name>/
|
|
809
|
-
if (!fs.existsSync(agentHome))
|
|
810
|
-
fs.mkdirSync(agentHome, { recursive: true });
|
|
811
|
-
// Build the canonical Hermes settings.json from the agent's settings_<name>.json.
|
|
812
|
-
// We preserve: env, enableAllProjectMcpServers, enabledMcpjsonServers, agent, runner.
|
|
813
|
-
// We do NOT touch: config.yaml, auth.json, .env — Hermes upstream owns those.
|
|
814
|
-
const tmpAgentSettings = path.join(agentHome, 'settings.json');
|
|
815
|
-
const settingsJson = {};
|
|
816
|
-
// Read the interpolated settings the runner just merged (line ~412 above)
|
|
817
|
-
// and copy the Hermes-relevant fields. We don't re-interpolate here because
|
|
818
|
-
// the caller already did it on the raw `settings` object.
|
|
819
|
-
if (tmpSettingsPath && fs.existsSync(tmpSettingsPath)) {
|
|
820
|
-
try {
|
|
821
|
-
const raw = JSON.parse(fs.readFileSync(tmpSettingsPath, 'utf8'));
|
|
822
|
-
if (raw.env)
|
|
823
|
-
settingsJson.env = { ...raw.env };
|
|
824
|
-
if (raw.enableAllProjectMcpServers !== undefined) {
|
|
825
|
-
settingsJson.enableAllProjectMcpServers = raw.enableAllProjectMcpServers;
|
|
826
|
-
}
|
|
827
|
-
if (Array.isArray(raw.enabledMcpjsonServers)) {
|
|
828
|
-
settingsJson.enabledMcpjsonServers = raw.enabledMcpjsonServers;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
catch (e) {
|
|
832
|
-
logger.warn({ tmpSettingsPath, error: e }, 'Failed to read tmp settings for canonical write');
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// ============================================================
|
|
836
|
-
// 2.8.31 — INJECT PROVIDER-SPECIFIC ENV VARS INTO settings.json
|
|
837
|
-
// ============================================================
|
|
838
|
-
// The Hermes plugins (e.g. `minimax`, `zai`, `openai`) read PROVIDER-
|
|
839
|
-
// SPECIFIC env vars from the per-agent settings.json — NOT the generic
|
|
840
|
-
// `ANTHROPIC_AUTH_TOKEN`. For example the `minimax-cn` plugin reads
|
|
841
|
-
// `MINIMAX_CN_API_KEY`, and the `minimax` (GLOBAL) plugin reads
|
|
842
|
-
// `MINIMAX_API_KEY`. If those vars aren't in the agent's settings.json,
|
|
843
|
-
// the plugin can't find the credential and the upstream falls back to
|
|
844
|
-
// the wrong provider (we saw it pick `openrouter` or `nvidia` instead
|
|
845
|
-
// of `minimax-cn`).
|
|
846
|
-
//
|
|
847
|
-
// We inject by:
|
|
848
|
-
// 1. Detecting the provider from the user's ANTHROPIC_BASE_URL hint
|
|
849
|
-
// (api.minimaxi.com → CN, api.minimax.io → GLOBAL, etc.).
|
|
850
|
-
// 2. For MiniMax: ONLY seed the matching env var (CN vs GLOBAL) so
|
|
851
|
-
// the upstream plugin's first-match resolver picks the right one.
|
|
852
|
-
// Do NOT seed both — that was the bug in 2.8.30 (caused it to pick
|
|
853
|
-
// GLOBAL even when the URL was CN).
|
|
854
|
-
// 3. For Z.AI: seed ZAI_ANTHROPIC_FALLBACK_KEY + GLM_API_KEY.
|
|
855
|
-
// 4. Leave ANTHROPIC_AUTH_TOKEN in place as a fallback (some Hermes
|
|
856
|
-
// code paths still read it generically).
|
|
857
|
-
const envObj = settingsJson.env;
|
|
858
|
-
if (envObj) {
|
|
859
|
-
const baseUrl = (envObj['ANTHROPIC_BASE_URL'] || '').toLowerCase();
|
|
860
|
-
const anthropicToken = envObj['ANTHROPIC_AUTH_TOKEN'] || envObj['ANTHROPIC_API_KEY'] || '';
|
|
861
|
-
if (anthropicToken && (anthropicToken.startsWith('sk-cp-') || anthropicToken.startsWith('sk-mm-'))) {
|
|
862
|
-
// MiniMax token — pick CN vs GLOBAL based on URL
|
|
863
|
-
if (baseUrl.includes('minimaxi')) {
|
|
864
|
-
// CN: api.minimaxi.com
|
|
865
|
-
envObj['MINIMAX_CN_API_KEY'] = anthropicToken;
|
|
866
|
-
// The Hermes `providers.py` resolver reads `MINIMAX_CN_BASE_URL`
|
|
867
|
-
// (NOT just `ANTHROPIC_BASE_URL`) to dispatch to the CN plugin
|
|
868
|
-
// profile. Seed it explicitly so the provider resolver picks
|
|
869
|
-
// `minimax-cn` (not `minimax` GLOBAL on first-match).
|
|
870
|
-
envObj['MINIMAX_CN_BASE_URL'] = 'https://api.minimaxi.com/anthropic';
|
|
871
|
-
logger.info({ agentName, cnApiKeySet: true, cnBaseUrlSet: true, globalApiKeySet: false, detectedFrom: 'api.minimaxi.com (CN)' }, '[SETTINGS_JSON] Seeded MINIMAX_CN_API_KEY + MINIMAX_CN_BASE_URL (CN plugin resolver).');
|
|
872
|
-
}
|
|
873
|
-
else if (baseUrl.includes('minimax') || baseUrl === '') {
|
|
874
|
-
// GLOBAL: api.minimax.io, OR no URL hint (default to CN per OVERMIND_MINIMAX_DEFAULT=cn)
|
|
875
|
-
const defaultCn = (process.env.OVERMIND_MINIMAX_DEFAULT || 'cn').toLowerCase() === 'cn';
|
|
876
|
-
if (defaultCn) {
|
|
877
|
-
envObj['MINIMAX_CN_API_KEY'] = anthropicToken;
|
|
878
|
-
envObj['MINIMAX_CN_BASE_URL'] = 'https://api.minimaxi.com/anthropic';
|
|
879
|
-
logger.info({ agentName, detectedFrom: 'no URL + OVERMIND_MINIMAX_DEFAULT=cn' }, '[SETTINGS_JSON] Seeded MINIMAX_CN_API_KEY + MINIMAX_CN_BASE_URL (default CN per OVERMIND_MINIMAX_DEFAULT).');
|
|
880
|
-
}
|
|
881
|
-
else {
|
|
882
|
-
envObj['MINIMAX_API_KEY'] = anthropicToken;
|
|
883
|
-
envObj['MINIMAX_BASE_URL'] = 'https://api.minimax.io/anthropic';
|
|
884
|
-
logger.info({ agentName, detectedFrom: 'api.minimax.io (GLOBAL)' }, '[SETTINGS_JSON] Seeded MINIMAX_API_KEY + MINIMAX_BASE_URL (GLOBAL plugin resolver).');
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
else if (anthropicToken && /^[0-9a-f]{32}(\.[0-9a-zA-Z]+)?$/i.test(anthropicToken)) {
|
|
889
|
-
// Z.AI token (32hex or 32hex.32hex)
|
|
890
|
-
envObj['ZAI_ANTHROPIC_FALLBACK_KEY'] = anthropicToken;
|
|
891
|
-
envObj['GLM_API_KEY'] = anthropicToken;
|
|
892
|
-
logger.info({ agentName }, '[SETTINGS_JSON] Seeded ZAI_ANTHROPIC_FALLBACK_KEY + GLM_API_KEY (Z.AI token).');
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
// Always declare the agent name + runner so Hermes can route to the right MCP servers.
|
|
896
|
-
settingsJson.agent = agentName;
|
|
897
|
-
settingsJson.runner = 'hermes';
|
|
898
|
-
fs.writeFileSync(tmpAgentSettings, JSON.stringify(settingsJson, null, 2) + '\n', 'utf8');
|
|
899
|
-
// 2.8.32: DO NOT push tmpAgentSettings to this.tempFiles — the
|
|
900
|
-
// canonical settings.json is the AGENT'S PERMANENT Hermes config,
|
|
901
|
-
// not a temp file. cleanupTempFiles() (called in finally + after
|
|
902
|
-
// spawn) would otherwise unlink it on every spawn, forcing the
|
|
903
|
-
// Hermes upstream plugin resolver to re-derive provider routing
|
|
904
|
-
// from the (now-empty) .env block on the next run. This was the
|
|
905
|
-
// root cause of the 13:51 "Erreur inconnue" — manual settings.json
|
|
906
|
-
// edits were being silently deleted after each spawn.
|
|
907
|
-
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).');
|
|
908
|
-
}
|
|
909
|
-
let effectiveHermesHome = sharedHome;
|
|
910
|
-
if (agentName && enabledInSettings.length > 0) {
|
|
911
|
-
try {
|
|
912
|
-
const runsDir = path.join(sharedHome, 'runs');
|
|
913
|
-
if (!fs.existsSync(runsDir))
|
|
914
|
-
fs.mkdirSync(runsDir, { recursive: true });
|
|
915
|
-
const runHome = path.join(runsDir, agentName);
|
|
916
|
-
if (!fs.existsSync(runHome))
|
|
917
|
-
fs.mkdirSync(runHome, { recursive: true });
|
|
918
|
-
// Copy auth.json if exists
|
|
919
|
-
const sharedAuth = path.join(sharedHome, 'auth.json');
|
|
920
|
-
const runAuth = path.join(runHome, 'auth.json');
|
|
921
|
-
if (fs.existsSync(sharedAuth)) {
|
|
922
|
-
fs.copyFileSync(sharedAuth, runAuth);
|
|
923
|
-
}
|
|
924
|
-
// Robust directory junction/symlink helper
|
|
925
|
-
const linkDirRobust = (target, source) => {
|
|
926
|
-
if (!fs.existsSync(source)) {
|
|
927
|
-
try {
|
|
928
|
-
fs.mkdirSync(source, { recursive: true });
|
|
929
|
-
}
|
|
930
|
-
catch (e) {
|
|
931
|
-
logger.warn({ source, error: e }, '[HERMES_HOME] Failed to create link source directory');
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
let exists = false;
|
|
935
|
-
let stats = null;
|
|
936
|
-
try {
|
|
937
|
-
stats = fs.lstatSync(target);
|
|
938
|
-
exists = true;
|
|
939
|
-
}
|
|
940
|
-
catch {
|
|
941
|
-
// target does not exist
|
|
942
|
-
}
|
|
943
|
-
if (exists) {
|
|
944
|
-
logger.debug({ target, isJunction: stats ? (stats.isSymbolicLink() || stats.isDirectory()) : false }, '[HERMES_HOME] Target directory/link already exists. Skipping link creation.');
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
logger.info({ target, source }, '[HERMES_HOME] Creating junction/symbolic link.');
|
|
948
|
-
if (process.platform === 'win32') {
|
|
949
|
-
try {
|
|
950
|
-
fs.symlinkSync(source, target, 'junction');
|
|
951
|
-
}
|
|
952
|
-
catch {
|
|
953
|
-
// Junction may already exist — ignore
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
fs.symlinkSync(source, target);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
// Link agents directory
|
|
962
|
-
const sharedAgents = path.join(sharedHome, 'agents');
|
|
963
|
-
const runAgents = path.join(runHome, 'agents');
|
|
964
|
-
linkDirRobust(runAgents, sharedAgents);
|
|
965
|
-
// Link sessions directory
|
|
966
|
-
const sharedSessions = path.join(sharedHome, 'sessions');
|
|
967
|
-
const runSessions = path.join(runHome, 'sessions');
|
|
968
|
-
linkDirRobust(runSessions, sharedSessions);
|
|
969
|
-
// Generate filtered config.yaml
|
|
970
|
-
const sharedConfig = path.join(sharedHome, 'config.yaml');
|
|
971
|
-
const runConfig = path.join(runHome, 'config.yaml');
|
|
972
|
-
const filteredConfig = filterConfigYaml(sharedConfig, enabledInSettings);
|
|
973
|
-
fs.writeFileSync(runConfig, filteredConfig, 'utf8');
|
|
974
|
-
effectiveHermesHome = runHome;
|
|
975
|
-
logger.info({ agentName, runHome, enabledMcp: enabledInSettings }, '[HERMES_HOME] Isolated agent Hermes Home and filtered config.yaml successfully.');
|
|
976
|
-
}
|
|
977
|
-
catch (e) {
|
|
978
|
-
logger.warn({ error: e }, 'Failed to setup isolated run home; falling back to sharedHome');
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
// AbortSignal
|
|
982
|
-
if (options.signal?.aborted)
|
|
983
|
-
return Promise.reject(new Error('ABORTED'));
|
|
984
|
-
let currentChildRef = null;
|
|
985
|
-
return new Promise((resolve) => {
|
|
986
|
-
let resolved = false;
|
|
987
|
-
let retryCount = 0;
|
|
988
|
-
const maxRetries = getAvailableFallbacks().length + 1;
|
|
989
|
-
let currentSessionId = sessionId;
|
|
990
|
-
const abortListener = () => {
|
|
991
|
-
if (currentChildRef) {
|
|
992
|
-
killProcessTree(currentChildRef).then(() => {
|
|
993
|
-
cleanupTmpFiles();
|
|
994
|
-
safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
else {
|
|
998
|
-
cleanupTmpFiles();
|
|
999
|
-
safeResolve({ result: '', error: 'ABORTED', rawOutput: '' });
|
|
1000
|
-
}
|
|
1001
|
-
};
|
|
1002
|
-
const safeResolve = (v) => {
|
|
1003
|
-
if (!resolved) {
|
|
1004
|
-
resolved = true;
|
|
1005
|
-
if (options.signal) {
|
|
1006
|
-
options.signal.removeEventListener('abort', abortListener);
|
|
1007
|
-
}
|
|
1008
|
-
resolve(v);
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
|
-
const cleanupTmpFiles = () => {
|
|
1012
|
-
for (const f of [tmpSettingsPath, tmpMcpPath]) {
|
|
1013
|
-
if (f && fs.existsSync(f)) {
|
|
1014
|
-
try {
|
|
1015
|
-
fs.unlinkSync(f);
|
|
1016
|
-
}
|
|
1017
|
-
catch { /* ignored */ }
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
|
-
const spawnHermes = async (tokenInfo) => {
|
|
1022
|
-
const spawnEnv = { ...process.env, ...agentCustomEnv };
|
|
1023
|
-
if (tokenInfo) {
|
|
1024
|
-
// Purge ALL known LLM provider env vars from the spawn env. We
|
|
1025
|
-
// re-seed ONLY the ones this agent actually uses, derived from the
|
|
1026
|
-
// token prefix. Without this purge, a stale `MINIMAX_CN_API_KEY` or
|
|
1027
|
-
// `ZAI_ANTHROPIC_FALLBACK_KEY` left over from a previous provider
|
|
1028
|
-
// (e.g. in `Workflow/.env`) can shadow the correct credential.
|
|
1029
|
-
for (const tk of TOKEN_KEYS)
|
|
1030
|
-
delete spawnEnv[tk];
|
|
1031
|
-
// Also purge provider-specific env vars that the user might have
|
|
1032
|
-
// left in the workflow .env (e.g. Z.AI legacy keys) but that don't
|
|
1033
|
-
// match the agent's current provider.
|
|
1034
|
-
for (const stale of [
|
|
1035
|
-
'MINIMAX_CN_API_KEY', 'MINIMAX_API_KEY',
|
|
1036
|
-
'ZAI_ANTHROPIC_FALLBACK_KEY', 'GLM_API_KEY',
|
|
1037
|
-
'Z_AI_API_KEY', 'Z_AI_BASE_URL', 'GLM_BASE_URL',
|
|
1038
|
-
'NVIDIA_API_KEY', 'NVIDIA_API_BASE',
|
|
1039
|
-
]) {
|
|
1040
|
-
delete spawnEnv[stale];
|
|
1041
|
-
}
|
|
1042
|
-
let resolvedToken = tokenInfo.tokenValue;
|
|
1043
|
-
if (resolvedToken.startsWith('$'))
|
|
1044
|
-
resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
|
|
1045
|
-
spawnEnv[tokenInfo.tokenEnvKey] = resolvedToken;
|
|
1046
|
-
// ============================================================
|
|
1047
|
-
// 2.8.30 — Seed provider-specific env vars for Hermes plugins.
|
|
1048
|
-
// ============================================================
|
|
1049
|
-
// The Hermes plugins read provider-specific env vars, not the
|
|
1050
|
-
// generic `ANTHROPIC_AUTH_TOKEN`. For example the `minimax` plugin
|
|
1051
|
-
// reads `MINIMAX_CN_API_KEY` (CN) or `MINIMAX_API_KEY` (GLOBAL),
|
|
1052
|
-
// and the `zai` plugin reads `ZAI_ANTHROPIC_FALLBACK_KEY`.
|
|
1053
|
-
//
|
|
1054
|
-
// Without this seed, Hermes falls back to the WRONG plugin (we saw
|
|
1055
|
-
// it pick `nvidia` because `ANTHROPIC_BASE_URL` wasn't set yet and
|
|
1056
|
-
// it could match an NVIDIA-style model name pattern). The fix is
|
|
1057
|
-
// tiny: when we know the token prefix (e.g. `sk-cp-` for MiniMax),
|
|
1058
|
-
// ALSO seed the provider-specific env var that the upstream plugin
|
|
1059
|
-
// actually reads. Reference: the .bat launchers do the same.
|
|
1060
|
-
//
|
|
1061
|
-
// We do NOT write to the agent's `.hermes/.env` file anymore —
|
|
1062
|
-
// this is a process-env-only seed, scoped to this single spawn.
|
|
1063
|
-
if (resolvedToken.startsWith('sk-cp-') || resolvedToken.startsWith('sk-mm-')) {
|
|
1064
|
-
// MiniMax token — seed BOTH env vars so either CN or GLOBAL plugin
|
|
1065
|
-
// can pick it up. The plugin's own URL/host detection will pick
|
|
1066
|
-
// the right one.
|
|
1067
|
-
spawnEnv['MINIMAX_CN_API_KEY'] = resolvedToken;
|
|
1068
|
-
spawnEnv['MINIMAX_API_KEY'] = resolvedToken;
|
|
1069
|
-
logger.info({ agentName, envKey: tokenInfo.tokenEnvKey, alsoSeeded: ['MINIMAX_CN_API_KEY', 'MINIMAX_API_KEY'] }, '[SPAWN_ENV] Seeded MiniMax provider-specific env vars from sk-cp-* token (plugin compat).');
|
|
1070
|
-
}
|
|
1071
|
-
else if (/^[0-9a-f]{32}(\.[0-9a-zA-Z]+)?$/i.test(resolvedToken)) {
|
|
1072
|
-
// Z.AI token (32hex or 32hex.32hex) — seed Z.AI env vars.
|
|
1073
|
-
spawnEnv['ZAI_ANTHROPIC_FALLBACK_KEY'] = resolvedToken;
|
|
1074
|
-
spawnEnv['GLM_API_KEY'] = resolvedToken;
|
|
1075
|
-
logger.info({ agentName, envKey: tokenInfo.tokenEnvKey, alsoSeeded: ['ZAI_ANTHROPIC_FALLBACK_KEY', 'GLM_API_KEY'] }, '[SPAWN_ENV] Seeded Z.AI provider-specific env vars from 32hex token (plugin compat).');
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
// BLOCK: OpenRouter is for embeddings only — never pass to Hermes for LLM inference
|
|
1079
|
-
delete spawnEnv['OPENROUTER_API_KEY'];
|
|
1080
|
-
delete spawnEnv['OPENROUTER_BASE_URL'];
|
|
1081
|
-
delete spawnEnv['OVERMIND_EMBEDDING_KEY'];
|
|
1082
|
-
const hermesBin = await findHermesBinary();
|
|
1083
|
-
const isWin = process.platform === 'win32';
|
|
1084
|
-
const venvRoot = process.env.HERMES_AGENT_ROOT
|
|
1085
|
-
|| (isWin
|
|
1086
|
-
? path.join(process.env.LOCALAPPDATA || '', 'hermes', 'hermes-agent', 'venv')
|
|
1087
|
-
: path.join(process.env.HOME || '', '.local', 'share', 'hermes-agent', 'venv'));
|
|
1088
|
-
// Only override VIRTUAL_ENV/PATH when hermes actually lives inside a venv
|
|
1089
|
-
// (i.e. <venv>/bin/hermes or <venv>/Scripts/hermes.exe). If the binary is a
|
|
1090
|
-
// system install (e.g. /usr/local/bin/hermes), leave the parent PATH alone.
|
|
1091
|
-
const venvBin = isWin ? path.join(venvRoot, 'Scripts') : path.join(venvRoot, 'bin');
|
|
1092
|
-
const isVenvInstall = hermesBin.startsWith(venvBin + path.sep) || hermesBin === venvBin;
|
|
1093
|
-
const pathSep = isWin ? ';' : ':';
|
|
1094
|
-
// 2.8.30: HERMES_HOME is the SHARED root, not the per-agent home.
|
|
1095
|
-
// Hermes upstream resolves `agents/<name>/`, `config.yaml`, `auth.json`,
|
|
1096
|
-
// etc. relative to HERMES_HOME. Setting it to the per-agent home would
|
|
1097
|
-
// tell Hermes "this IS the Hermes root" and make it look for config.yaml
|
|
1098
|
-
// IN the agent dir — wrong layout.
|
|
1099
|
-
const child = spawn(hermesBin, cleanArgs, {
|
|
1100
|
-
cwd, shell: false, windowsHide: true,
|
|
1101
|
-
env: {
|
|
1102
|
-
...spawnEnv,
|
|
1103
|
-
HERMES_HOME: effectiveHermesHome,
|
|
1104
|
-
...(isVenvInstall
|
|
1105
|
-
? {
|
|
1106
|
-
VIRTUAL_ENV: venvRoot,
|
|
1107
|
-
PATH: `${venvRoot}${isWin ? ';' : ':'}${venvBin}${pathSep}${process.env.PATH || ''}`,
|
|
1108
|
-
}
|
|
1109
|
-
: {}),
|
|
1110
|
-
},
|
|
1111
|
-
});
|
|
1112
|
-
currentChildRef = child;
|
|
1113
|
-
if (child.pid) {
|
|
1114
|
-
void registerProcess(child.pid, { agentName: agentName || '', runner: 'hermes', configPath });
|
|
1115
|
-
void registerLiveAgent({
|
|
1116
|
-
pid: child.pid, runner: 'hermes', agentName: agentName || '',
|
|
1117
|
-
sessionId: currentSessionId || '',
|
|
1118
|
-
cleanupFn: async () => { await killProcessTree(child); },
|
|
1119
|
-
childRef: child,
|
|
1120
|
-
});
|
|
1121
|
-
child.once('exit', (code) => {
|
|
1122
|
-
setLiveStatus(child.pid, code === 0 ? 'done' : 'failed', code ?? null);
|
|
1123
|
-
void unregisterLiveAgent(child.pid);
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
let stdout = '';
|
|
1127
|
-
let stderr = '';
|
|
1128
|
-
child.stdout?.on('data', (d) => {
|
|
1129
|
-
const chunk = d.toString();
|
|
1130
|
-
if (child.pid) {
|
|
1131
|
-
void appendOutput(child.pid, chunk, configPath);
|
|
1132
|
-
void appendLiveOutput(child.pid, chunk);
|
|
1133
|
-
}
|
|
1134
|
-
if (stdout.length + chunk.length > this.MAX_BUF)
|
|
1135
|
-
stdout = stdout.slice(-this.MAX_BUF);
|
|
1136
|
-
else
|
|
1137
|
-
stdout += chunk;
|
|
1138
|
-
if (!silent && agentName)
|
|
1139
|
-
process.stderr.write(`[Hermes:${agentName}] ${chunk}`);
|
|
1140
|
-
});
|
|
1141
|
-
child.stderr?.on('data', (d) => {
|
|
1142
|
-
const chunk = d.toString();
|
|
1143
|
-
if (stderr.length + chunk.length > this.MAX_BUF)
|
|
1144
|
-
stderr = stderr.slice(-this.MAX_BUF);
|
|
1145
|
-
else
|
|
1146
|
-
stderr += chunk;
|
|
1147
|
-
if (!silent && agentName)
|
|
1148
|
-
process.stderr.write(`[Hermes:${agentName}:ERR] ${chunk}`);
|
|
1149
|
-
});
|
|
1150
|
-
const timer = setTimeout(() => {
|
|
1151
|
-
if (child.stdin && !child.stdin.destroyed) {
|
|
1152
|
-
try {
|
|
1153
|
-
child.stdin.write('\n');
|
|
1154
|
-
}
|
|
1155
|
-
catch { /* ignore */ }
|
|
1156
|
-
}
|
|
1157
|
-
setTimeout(async () => {
|
|
1158
|
-
await killProcessTree(child);
|
|
1159
|
-
cleanupTmpFiles();
|
|
1160
|
-
safeResolve({ result: '', error: 'HARD_TIMEOUT', rawOutput: stdout + stderr });
|
|
1161
|
-
}, HARD_TIMEOUT_MS);
|
|
1162
|
-
}, timeoutMs);
|
|
1163
|
-
child.on('close', async (code) => {
|
|
1164
|
-
clearTimeout(timer);
|
|
1165
|
-
if (child.pid)
|
|
1166
|
-
void updateProcessStatus(child.pid, code === 0 ? 'done' : 'failed', code, configPath);
|
|
1167
|
-
const sessionMatch = stdout.match(/session_id:\s*(\S+)/i) ||
|
|
1168
|
-
stderr.match(/session_id:\s*(\S+)/i) ||
|
|
1169
|
-
stdout.match(/Session:\s*(\S+)/) ||
|
|
1170
|
-
stderr.match(/Session:\s*(\S+)/);
|
|
1171
|
-
if (sessionMatch)
|
|
1172
|
-
currentSessionId = sessionMatch[1];
|
|
1173
|
-
const retryable = isRetryableError(stderr) || isRetryableError(stdout);
|
|
1174
|
-
if (code !== 0 && retryable && retryCount < maxRetries) {
|
|
1175
|
-
retryCount++;
|
|
1176
|
-
const ti = getTokenForIndex(retryCount);
|
|
1177
|
-
if (!silent) {
|
|
1178
|
-
process.stderr.write(`\n\x1b[41m\x1b[37m[NousHermesRunner] Retry ${retryCount}/${maxRetries} avec ${ti?.tokenEnvKey || 'UNKNOWN'}...\x1b[0m\n`);
|
|
1179
|
-
}
|
|
1180
|
-
await killProcessTree(child);
|
|
1181
|
-
setImmediate(() => spawnHermes(ti));
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
cleanupTmpFiles();
|
|
1185
|
-
if (currentSessionId && agentName) {
|
|
1186
|
-
await saveSessionId(agentName, currentSessionId, configPath, 'hermes');
|
|
1187
|
-
if (child.pid)
|
|
1188
|
-
void linkSessionToPid(currentSessionId, child.pid, configPath);
|
|
1189
|
-
}
|
|
1190
|
-
if (code !== 0 && !stdout.trim()) {
|
|
1191
|
-
safeResolve({ result: '', error: `EXIT_CODE_${code}`, rawOutput: stderr || stdout, sessionId: currentSessionId });
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
safeResolve({ result: stdout.trim(), sessionId: currentSessionId, rawOutput: stdout });
|
|
1195
|
-
});
|
|
1196
|
-
child.on('error', (err) => {
|
|
1197
|
-
clearTimeout(timer);
|
|
1198
|
-
killProcessTree(child).then(() => {
|
|
1199
|
-
cleanupTmpFiles();
|
|
1200
|
-
safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
|
|
1201
|
-
});
|
|
1202
|
-
});
|
|
1203
|
-
};
|
|
1204
|
-
if (options.signal) {
|
|
1205
|
-
options.signal.addEventListener('abort', abortListener);
|
|
1206
|
-
}
|
|
1207
|
-
let firstToken;
|
|
1208
|
-
try {
|
|
1209
|
-
firstToken = getTokenForIndex(0);
|
|
1210
|
-
}
|
|
1211
|
-
catch (e) {
|
|
1212
|
-
// getTokenForIndex throws MISSING_ENV_VAR when settings_<agent>.json references
|
|
1213
|
-
// a $VAR that doesn't exist in process.env. Surface it as a clear error.
|
|
1214
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1215
|
-
logger.error({ agentName, error: msg }, '[NO_LLM_TOKEN] Token resolution failed.');
|
|
1216
|
-
safeResolve({
|
|
1217
|
-
result: '',
|
|
1218
|
-
error: `NO_LLM_TOKEN: ${msg}`,
|
|
1219
|
-
rawOutput: '',
|
|
1220
|
-
});
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
if (!firstToken && agentName) {
|
|
1224
|
-
// No credential was resolved at all — refuse to spawn hermes with an empty
|
|
1225
|
-
// API key (which would silently 401 and report the misleading EXIT_CODE_1).
|
|
1226
|
-
// The diagnostic log inside getTokenForIndex(0) already lists the checked keys.
|
|
1227
|
-
const settingsHint = tmpSettingsPath
|
|
1228
|
-
? `Look at ${tmpSettingsPath} (env block) and ${path.join(overmindHermesSubPath, '.env')}.`
|
|
1229
|
-
: `No settings_${agentName}.json was loaded. Check the .claude/ folder.`;
|
|
1230
|
-
safeResolve({
|
|
1231
|
-
result: '',
|
|
1232
|
-
error: `NO_LLM_TOKEN: settings_<agent>.json env block is empty or missing required keys (${TOKEN_KEYS.join(', ')}). ${settingsHint}`,
|
|
1233
|
-
rawOutput: '',
|
|
1234
|
-
});
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
spawnHermes(firstToken);
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
//# sourceMappingURL=NousHermesRunner.js.map
|