overmind-mcp 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json.example +21 -0
- package/README.md +107 -80
- package/assets/overmind.png +0 -0
- package/bin/.gitkeep +0 -0
- package/bin/README.md +34 -0
- package/bin/install-overmind-unix.sh +412 -0
- package/bin/install-overmind-windows.bat +407 -0
- package/dist/bin/cli.js +438 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/InstallHelper.d.ts +14 -0
- package/dist/lib/InstallHelper.d.ts.map +1 -0
- package/dist/lib/InstallHelper.js +115 -0
- package/dist/lib/InstallHelper.js.map +1 -0
- package/dist/lib/config.d.ts +31 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +19 -27
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/envUtils.d.ts +10 -0
- package/dist/lib/envUtils.d.ts.map +1 -0
- package/dist/lib/envUtils.js +24 -0
- package/dist/lib/envUtils.js.map +1 -0
- package/dist/lib/loadEnv.d.ts +2 -0
- package/dist/lib/loadEnv.d.ts.map +1 -0
- package/dist/lib/loadEnv.js +26 -0
- package/dist/lib/loadEnv.js.map +1 -0
- package/dist/lib/logger.d.ts +8 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +81 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/modelMapping.d.ts +18 -0
- package/dist/lib/modelMapping.d.ts.map +1 -0
- package/dist/lib/modelMapping.js +90 -0
- package/dist/lib/modelMapping.js.map +1 -0
- package/dist/lib/orchestration/dispatcher.d.ts +32 -0
- package/dist/lib/orchestration/dispatcher.d.ts.map +1 -0
- package/dist/lib/orchestration/dispatcher.js +113 -0
- package/dist/lib/orchestration/dispatcher.js.map +1 -0
- package/dist/lib/orchestration/swarm.d.ts +75 -0
- package/dist/lib/orchestration/swarm.d.ts.map +1 -0
- package/dist/lib/orchestration/swarm.js +239 -0
- package/dist/lib/orchestration/swarm.js.map +1 -0
- package/dist/lib/processRegistry.d.ts +10 -0
- package/dist/lib/processRegistry.d.ts.map +1 -1
- package/dist/lib/processRegistry.js +85 -29
- package/dist/lib/processRegistry.js.map +1 -1
- package/dist/lib/sessions.d.ts +3 -2
- package/dist/lib/sessions.d.ts.map +1 -1
- package/dist/lib/sessions.js +94 -25
- package/dist/lib/sessions.js.map +1 -1
- package/dist/lib/telemetry.d.ts +21 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +30 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/memory/MemoryFactory.d.ts +15 -1
- package/dist/memory/MemoryFactory.d.ts.map +1 -1
- package/dist/memory/MemoryFactory.js +53 -3
- package/dist/memory/MemoryFactory.js.map +1 -1
- package/dist/memory/PostgresMemoryProvider.d.ts +7 -0
- package/dist/memory/PostgresMemoryProvider.d.ts.map +1 -1
- package/dist/memory/PostgresMemoryProvider.js +180 -105
- package/dist/memory/PostgresMemoryProvider.js.map +1 -1
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +145 -41
- package/dist/server.js.map +1 -1
- package/dist/services/AgentManager.d.ts.map +1 -1
- package/dist/services/AgentManager.js +67 -11
- package/dist/services/AgentManager.js.map +1 -1
- package/dist/services/ClaudeRunner.d.ts +20 -0
- package/dist/services/ClaudeRunner.d.ts.map +1 -1
- package/dist/services/ClaudeRunner.js +626 -272
- package/dist/services/ClaudeRunner.js.map +1 -1
- package/dist/services/ClineRunner.d.ts +7 -0
- package/dist/services/ClineRunner.d.ts.map +1 -1
- package/dist/services/ClineRunner.js +76 -22
- package/dist/services/ClineRunner.js.map +1 -1
- package/dist/services/GeminiRunner.d.ts +6 -0
- package/dist/services/GeminiRunner.d.ts.map +1 -1
- package/dist/services/GeminiRunner.js +283 -69
- package/dist/services/GeminiRunner.js.map +1 -1
- package/dist/services/KiloRunner.d.ts +12 -0
- package/dist/services/KiloRunner.d.ts.map +1 -1
- package/dist/services/KiloRunner.js +439 -70
- package/dist/services/KiloRunner.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts +35 -0
- package/dist/services/NousHermesRunner.d.ts.map +1 -0
- package/dist/services/NousHermesRunner.js +535 -0
- package/dist/services/NousHermesRunner.js.map +1 -0
- package/dist/services/OpenClawRunner.d.ts +7 -0
- package/dist/services/OpenClawRunner.d.ts.map +1 -1
- package/dist/services/OpenClawRunner.js +75 -22
- package/dist/services/OpenClawRunner.js.map +1 -1
- package/dist/services/OpenCodeRunner.d.ts +7 -0
- package/dist/services/OpenCodeRunner.d.ts.map +1 -1
- package/dist/services/OpenCodeRunner.js +75 -22
- package/dist/services/OpenCodeRunner.js.map +1 -1
- package/dist/services/{TraeRunner.d.ts → QwenCliRunner.d.ts} +9 -2
- package/dist/services/QwenCliRunner.d.ts.map +1 -0
- package/dist/services/QwenCliRunner.js +155 -0
- package/dist/services/QwenCliRunner.js.map +1 -0
- package/dist/tools/agent_control.d.ts +2 -2
- package/dist/tools/agent_control.d.ts.map +1 -1
- package/dist/tools/agent_control.js +79 -50
- package/dist/tools/agent_control.js.map +1 -1
- package/dist/tools/config_example.d.ts +7 -4
- package/dist/tools/config_example.d.ts.map +1 -1
- package/dist/tools/config_example.js +191 -86
- package/dist/tools/config_example.js.map +1 -1
- package/dist/tools/create_agent.d.ts +13 -7
- package/dist/tools/create_agent.d.ts.map +1 -1
- package/dist/tools/create_agent.js +8 -9
- package/dist/tools/create_agent.js.map +1 -1
- package/dist/tools/get_agent_configs.d.ts +10 -4
- package/dist/tools/get_agent_configs.d.ts.map +1 -1
- package/dist/tools/get_agent_configs.js.map +1 -1
- package/dist/tools/initialization_check.d.ts +2 -0
- package/dist/tools/initialization_check.d.ts.map +1 -0
- package/dist/tools/initialization_check.js +23 -0
- package/dist/tools/initialization_check.js.map +1 -0
- package/dist/tools/manage_agents.d.ts +34 -16
- package/dist/tools/manage_agents.d.ts.map +1 -1
- package/dist/tools/manage_agents.js +2 -2
- package/dist/tools/manage_agents.js.map +1 -1
- package/dist/tools/manage_prompts.d.ts +13 -8
- package/dist/tools/manage_prompts.d.ts.map +1 -1
- package/dist/tools/manage_prompts.js.map +1 -1
- package/dist/tools/memory_runs.d.ts +3 -4
- package/dist/tools/memory_runs.d.ts.map +1 -1
- package/dist/tools/memory_runs.js.map +1 -1
- package/dist/tools/memory_search.d.ts +3 -4
- package/dist/tools/memory_search.d.ts.map +1 -1
- package/dist/tools/memory_search.js.map +1 -1
- package/dist/tools/memory_store.d.ts +10 -4
- package/dist/tools/memory_store.d.ts.map +1 -1
- package/dist/tools/memory_store.js.map +1 -1
- package/dist/tools/run_agent.d.ts +14 -14
- package/dist/tools/run_agent.d.ts.map +1 -1
- package/dist/tools/run_agent.js +135 -156
- package/dist/tools/run_agent.js.map +1 -1
- package/dist/tools/run_agent_cli.d.ts +21 -0
- package/dist/tools/run_agent_cli.d.ts.map +1 -0
- package/dist/tools/run_agent_cli.js +137 -0
- package/dist/tools/run_agent_cli.js.map +1 -0
- package/dist/tools/run_agents_parallel.d.ts +35 -0
- package/dist/tools/run_agents_parallel.d.ts.map +1 -0
- package/dist/tools/run_agents_parallel.js +28 -0
- package/dist/tools/run_agents_parallel.js.map +1 -0
- package/dist/tools/run_claude.d.ts +9 -3
- package/dist/tools/run_claude.d.ts.map +1 -1
- package/dist/tools/run_claude.js +67 -41
- package/dist/tools/run_claude.js.map +1 -1
- package/dist/tools/run_cline.d.ts +13 -4
- package/dist/tools/run_cline.d.ts.map +1 -1
- package/dist/tools/run_cline.js +55 -43
- package/dist/tools/run_cline.js.map +1 -1
- package/dist/tools/run_gemini.d.ts +13 -4
- package/dist/tools/run_gemini.d.ts.map +1 -1
- package/dist/tools/run_gemini.js +53 -33
- package/dist/tools/run_gemini.js.map +1 -1
- package/dist/tools/run_hermes.d.ts +25 -0
- package/dist/tools/run_hermes.d.ts.map +1 -0
- package/dist/tools/run_hermes.js +93 -0
- package/dist/tools/run_hermes.js.map +1 -0
- package/dist/tools/run_kilo.d.ts +17 -7
- package/dist/tools/run_kilo.d.ts.map +1 -1
- package/dist/tools/run_kilo.js +68 -41
- package/dist/tools/run_kilo.js.map +1 -1
- package/dist/tools/run_openclaw.d.ts +13 -4
- package/dist/tools/run_openclaw.d.ts.map +1 -1
- package/dist/tools/run_openclaw.js +52 -44
- package/dist/tools/run_openclaw.js.map +1 -1
- package/dist/tools/run_opencode.d.ts +13 -4
- package/dist/tools/run_opencode.d.ts.map +1 -1
- package/dist/tools/run_opencode.js +52 -39
- package/dist/tools/run_opencode.js.map +1 -1
- package/dist/tools/run_qwencli.d.ts +24 -0
- package/dist/tools/run_qwencli.d.ts.map +1 -0
- package/dist/tools/run_qwencli.js +59 -0
- package/dist/tools/run_qwencli.js.map +1 -0
- package/docs/README.md +128 -0
- package/docs/agent_control.md +656 -0
- package/docs/index.html +493 -0
- package/docs/library.html +239 -0
- package/docs/prompt.html +1212 -0
- package/docs/script.js +428 -0
- package/docs/styles.css +2816 -0
- package/package.json +54 -25
- package/scripts/auto-changelog.mjs +132 -0
- package/scripts/auto-install.mjs +322 -0
- package/scripts/install-dependencies.mjs +462 -0
- package/scripts/postgres-manager.mjs +219 -0
- package/scripts/postinstall.mjs +538 -0
- package/scripts/setup-overmind-db.mjs +199 -0
- package/scripts/setup-windows.js +266 -0
- package/scripts/setup.mjs +397 -0
- package/scripts/test-installation.mjs +158 -0
- package/scripts/uninstall.mjs +238 -0
- package/assets/overmind_mcp_pro_banner_v3.png +0 -0
- package/dist/services/QwenRunner.d.ts +0 -19
- package/dist/services/QwenRunner.d.ts.map +0 -1
- package/dist/services/QwenRunner.js +0 -102
- package/dist/services/QwenRunner.js.map +0 -1
- package/dist/services/TraeRunner.d.ts.map +0 -1
- package/dist/services/TraeRunner.js +0 -103
- package/dist/services/TraeRunner.js.map +0 -1
- package/dist/tools/run_qwen.d.ts +0 -15
- package/dist/tools/run_qwen.d.ts.map +0 -1
- package/dist/tools/run_qwen.js +0 -61
- package/dist/tools/run_qwen.js.map +0 -1
- package/dist/tools/run_trae.d.ts +0 -15
- package/dist/tools/run_trae.d.ts.map +0 -1
- package/dist/tools/run_trae.js +0 -66
- package/dist/tools/run_trae.js.map +0 -1
- package/dist/tools/shell_execute.d.ts +0 -10
- package/dist/tools/shell_execute.d.ts.map +0 -1
- package/dist/tools/shell_execute.js +0 -24
- package/dist/tools/shell_execute.js.map +0 -1
- package/dist/tools/test_agent_control.d.ts +0 -2
- package/dist/tools/test_agent_control.d.ts.map +0 -1
- package/dist/tools/test_agent_control.js +0 -92
- package/dist/tools/test_agent_control.js.map +0 -1
|
@@ -1,108 +1,202 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { spawn, exec, execSync } from 'child_process';
|
|
5
|
+
import { CONFIG, resolveConfigPath, getWorkspaceDir } from '../lib/config.js';
|
|
6
6
|
import { getLastSessionId, saveSessionId } from '../lib/sessions.js';
|
|
7
|
-
import {
|
|
7
|
+
import { interpolateEnvVars } from '../lib/envUtils.js';
|
|
8
|
+
import { resolveModel } from '../lib/modelMapping.js';
|
|
9
|
+
import { withSpan } from '../lib/telemetry.js';
|
|
10
|
+
import { loadEnvQuietly } from '../lib/loadEnv.js';
|
|
11
|
+
import { registerProcess, linkSessionToPid, appendOutput, updateProcessStatus, } from '../lib/processRegistry.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
// Sur Windows, `child.kill()` ne tue que cmd.exe (le wrapper) — claude.exe
|
|
15
|
+
// devient orphelin et garde la session bound au token initial côté provider.
|
|
16
|
+
// On utilise `taskkill /F /T /PID` pour propager le kill au sous-arbre,
|
|
17
|
+
// puis on attend l'event 'exit' avant de respawn.
|
|
8
18
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
19
|
+
* On Windows, `claude` on PATH is a `.cmd` shim. cmd.exe-based shims don't
|
|
20
|
+
* reliably forward inherited file FDs to their child `.exe` when the whole
|
|
21
|
+
* thing is spawned `detached`, which leaves the log file empty. To dodge that,
|
|
22
|
+
* the detached path needs to invoke `claude.exe` directly.
|
|
23
|
+
*
|
|
24
|
+
* Returns null if we cannot locate the .exe — caller falls back to cmd.exe.
|
|
11
25
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
result[key] = value;
|
|
26
|
-
}
|
|
26
|
+
let cachedClaudeExe;
|
|
27
|
+
function resolveClaudeExePath() {
|
|
28
|
+
if (cachedClaudeExe !== undefined)
|
|
29
|
+
return cachedClaudeExe;
|
|
30
|
+
try {
|
|
31
|
+
const out = execSync('where claude.exe', {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
});
|
|
35
|
+
const found = out.split(/\r?\n/).map((l) => l.trim()).find((l) => l.toLowerCase().endsWith('.exe'));
|
|
36
|
+
if (found && fs.existsSync(found)) {
|
|
37
|
+
cachedClaudeExe = found;
|
|
38
|
+
return found;
|
|
27
39
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// `where` may not be on PATH (extremely unusual) — fall through.
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
// Fallback: derive from `claude.cmd` location → npm prefix → node_modules path.
|
|
46
|
+
const cmdOut = execSync('where claude', {
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
49
|
+
});
|
|
50
|
+
const cmdPath = cmdOut.split(/\r?\n/).map((l) => l.trim()).find((l) => l.toLowerCase().endsWith('.cmd'));
|
|
51
|
+
if (cmdPath) {
|
|
52
|
+
const npmPrefix = path.dirname(cmdPath);
|
|
53
|
+
const exePath = path.join(npmPrefix, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
|
54
|
+
if (fs.existsSync(exePath)) {
|
|
55
|
+
cachedClaudeExe = exePath;
|
|
56
|
+
return exePath;
|
|
57
|
+
}
|
|
31
58
|
}
|
|
32
59
|
}
|
|
33
|
-
|
|
60
|
+
catch {
|
|
61
|
+
// ignored
|
|
62
|
+
}
|
|
63
|
+
cachedClaudeExe = null;
|
|
64
|
+
return null;
|
|
34
65
|
}
|
|
66
|
+
const killProcessTree = (child) => {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
if (!child || child.exitCode !== null || child.killed) {
|
|
69
|
+
resolve();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let settled = false;
|
|
73
|
+
const finish = () => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
resolve();
|
|
78
|
+
};
|
|
79
|
+
child.once('exit', finish);
|
|
80
|
+
if (process.platform === 'win32' && child.pid) {
|
|
81
|
+
exec(`taskkill /F /T /PID ${child.pid}`, () => {
|
|
82
|
+
// taskkill peut échouer si le process est déjà mort — on s'appuie sur 'exit'
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
try {
|
|
87
|
+
child.kill('SIGTERM');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignored
|
|
91
|
+
}
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (child.exitCode === null && !child.killed) {
|
|
94
|
+
try {
|
|
95
|
+
child.kill('SIGKILL');
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignored
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, 2000);
|
|
102
|
+
}
|
|
103
|
+
// Filet de sécurité : si 'exit' ne se déclenche pas (process zombie),
|
|
104
|
+
// on débloque le respawn après 5s plutôt que de bloquer indéfiniment.
|
|
105
|
+
setTimeout(finish, 5000);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
35
108
|
export class ClaudeRunner {
|
|
36
109
|
config;
|
|
37
110
|
timeoutMs;
|
|
38
111
|
constructor() {
|
|
39
112
|
this.config = CONFIG.CLAUDE;
|
|
40
|
-
this.timeoutMs = CONFIG.TIMEOUT_MS ||
|
|
113
|
+
this.timeoutMs = CONFIG.TIMEOUT_MS || 900000;
|
|
41
114
|
}
|
|
42
115
|
async runAgent(options) {
|
|
43
116
|
const { prompt, agentName, autoResume } = options;
|
|
44
117
|
let { sessionId } = options;
|
|
45
118
|
const { CORE, PERMISSIONS, PATHS } = this.config;
|
|
46
119
|
const agentCustomEnv = {};
|
|
120
|
+
// Load environment variables FIRST before any processing
|
|
121
|
+
const workspaceEnvPath = path.resolve(options.configPath || getWorkspaceDir(), '.env');
|
|
122
|
+
loadEnvQuietly(workspaceEnvPath);
|
|
123
|
+
// Also load from Workflow directory as fallback
|
|
124
|
+
const workflowEnvPath = path.resolve(__dirname, '../../.env');
|
|
125
|
+
loadEnvQuietly(workflowEnvPath);
|
|
126
|
+
// Debug: check if variables are loaded
|
|
127
|
+
if (!options.silent) {
|
|
128
|
+
console.error(`[ClaudeRunner] Env check - ANTHROPIC_MODEL_Z present: ${!!process.env.ANTHROPIC_MODEL_Z}`);
|
|
129
|
+
console.error(`[ClaudeRunner] Auth tokens present: ${!!process.env.ANTHROPIC_AUTH_TOKEN_Y || !!process.env.ANTHROPIC_AUTH_TOKEN_E}`);
|
|
130
|
+
console.error(`[ClaudeRunner] workspaceEnvPath: ${workspaceEnvPath}`);
|
|
131
|
+
console.error(`[ClaudeRunner] workflowEnvPath: ${workflowEnvPath}`);
|
|
132
|
+
}
|
|
133
|
+
if (agentName) {
|
|
134
|
+
agentCustomEnv.OVERMIND_AGENT_NAME = agentName;
|
|
135
|
+
}
|
|
47
136
|
// --- Auto Resume ---
|
|
48
137
|
if (autoResume && agentName && !sessionId) {
|
|
49
|
-
const lastId = await getLastSessionId(agentName);
|
|
138
|
+
const lastId = await getLastSessionId(agentName, options.configPath, 'claude');
|
|
50
139
|
if (lastId) {
|
|
51
140
|
sessionId = lastId;
|
|
141
|
+
if (!options.silent) {
|
|
142
|
+
console.log(`[ClaudeRunner] Auto-resume session: ${sessionId}`);
|
|
143
|
+
}
|
|
52
144
|
}
|
|
53
145
|
}
|
|
54
|
-
let settingsPath = resolveConfigPath(PATHS.SETTINGS);
|
|
146
|
+
let settingsPath = resolveConfigPath(PATHS.SETTINGS, options.configPath);
|
|
55
147
|
if (agentName) {
|
|
56
148
|
const settingsDir = path.dirname(PATHS.SETTINGS);
|
|
57
|
-
const specificSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`));
|
|
149
|
+
const specificSettingsPath = resolveConfigPath(path.join(settingsDir, `settings_${agentName}.json`), options.configPath);
|
|
58
150
|
if (!fs.existsSync(specificSettingsPath)) {
|
|
59
151
|
return {
|
|
60
152
|
result: '',
|
|
61
|
-
error: `INVALID_AGENT
|
|
153
|
+
error: `INVALID_AGENT: Agent "${agentName}" non trouvé.`,
|
|
62
154
|
};
|
|
63
155
|
}
|
|
64
156
|
settingsPath = specificSettingsPath;
|
|
65
157
|
}
|
|
66
|
-
|
|
67
|
-
const relativeSettings = path.relative(cwd, settingsPath);
|
|
68
|
-
if (!relativeSettings.startsWith('..') && !path.isAbsolute(relativeSettings)) {
|
|
69
|
-
settingsPath = relativeSettings.startsWith('./') ? relativeSettings : `./${relativeSettings}`;
|
|
70
|
-
}
|
|
71
|
-
let mcpPath = resolveConfigPath(PATHS.MCP);
|
|
158
|
+
let mcpPath = resolveConfigPath(PATHS.MCP, options.configPath);
|
|
72
159
|
let tmpMcpPathToDelete = null;
|
|
160
|
+
let tmpSettingsPathToDelete = null;
|
|
73
161
|
let customTimeoutMs = this.timeoutMs;
|
|
74
|
-
// --- Isolation ---
|
|
75
162
|
if (agentName) {
|
|
76
163
|
try {
|
|
77
|
-
const agentSettingsPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), `settings_${agentName}.json`));
|
|
164
|
+
const agentSettingsPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), `settings_${agentName}.json`), options.configPath);
|
|
78
165
|
if (fs.existsSync(agentSettingsPath)) {
|
|
79
|
-
|
|
166
|
+
let settings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
|
|
167
|
+
// Debug: log environment variables
|
|
168
|
+
if (!options.silent) {
|
|
169
|
+
console.error(`[ClaudeRunner] ANTHROPIC_MODEL_Z present: ${!!process.env.ANTHROPIC_MODEL_Z}`);
|
|
170
|
+
console.error(`[ClaudeRunner] Auth tokens present: ${!!process.env.ANTHROPIC_AUTH_TOKEN_Y || !!process.env.ANTHROPIC_AUTH_TOKEN_E}`);
|
|
171
|
+
console.error(`[ClaudeRunner] ANTHROPIC_BASE_URL_Z present: ${!!process.env.ANTHROPIC_BASE_URL_Z}`);
|
|
172
|
+
}
|
|
173
|
+
// --- New interpolation logic ---
|
|
174
|
+
settings = interpolateEnvVars(settings);
|
|
175
|
+
// Debug: log interpolated values
|
|
176
|
+
if (!options.silent) {
|
|
177
|
+
console.error(`[ClaudeRunner] Interpolated ANTHROPIC_MODEL = ${settings.env?.ANTHROPIC_MODEL}`);
|
|
178
|
+
console.error(`[ClaudeRunner] Interpolated ANTHROPIC_BASE_URL = ${settings.env?.ANTHROPIC_BASE_URL}`);
|
|
179
|
+
}
|
|
180
|
+
// 1. Create a temporary settings file with interpolated values
|
|
181
|
+
const tmpSettingsPath = path.join(path.dirname(agentSettingsPath), `settings_${agentName}_tmp.json`);
|
|
182
|
+
fs.writeFileSync(tmpSettingsPath, JSON.stringify(settings, null, 2));
|
|
183
|
+
settingsPath = tmpSettingsPath;
|
|
184
|
+
tmpSettingsPathToDelete = tmpSettingsPath;
|
|
80
185
|
if (settings.env) {
|
|
81
|
-
|
|
82
|
-
const interpolatedEnv = interpolateEnvVars(settings.env);
|
|
83
|
-
Object.assign(agentCustomEnv, interpolatedEnv);
|
|
84
|
-
// --- SMART NICKNAME FALLBACK ---
|
|
85
|
-
const currentModel = settings.env.ANTHROPIC_MODEL;
|
|
86
|
-
const isTechnicalModelId = currentModel && (currentModel.includes('claude') ||
|
|
87
|
-
currentModel.includes('gpt') ||
|
|
88
|
-
currentModel.includes('glm') ||
|
|
89
|
-
currentModel.includes('minimax') ||
|
|
90
|
-
currentModel.includes('deepseek') ||
|
|
91
|
-
currentModel.includes('moonshot'));
|
|
92
|
-
if (currentModel && !isTechnicalModelId) {
|
|
93
|
-
// Si le modèle est un surnom, on l'utilise pour l'affichage mais on remet un vrai ID de modèle pour l'API
|
|
94
|
-
agentCustomEnv.AGENT_NICKNAME = currentModel;
|
|
95
|
-
// On utilise le modèle Sonnet par défaut ou la valeur configurée si elle semble valide
|
|
96
|
-
agentCustomEnv.ANTHROPIC_MODEL = (settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL && settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL.includes('claude'))
|
|
97
|
-
? settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
|
98
|
-
: 'claude-3-5-sonnet-20241022';
|
|
99
|
-
}
|
|
186
|
+
Object.assign(agentCustomEnv, settings.env);
|
|
100
187
|
if (settings.env.AGENT_TIMEOUT_MS || settings.env.API_TIMEOUT_MS) {
|
|
101
188
|
const timeoutValue = settings.env.AGENT_TIMEOUT_MS || settings.env.API_TIMEOUT_MS;
|
|
102
189
|
customTimeoutMs = parseInt(timeoutValue, 10) || customTimeoutMs;
|
|
103
190
|
}
|
|
191
|
+
if (!options.model && settings.env.MODEL) {
|
|
192
|
+
agentCustomEnv.ANTHROPIC_MODEL = settings.env.MODEL;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const agentMcpPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), `.mcp.${agentName}.json`));
|
|
196
|
+
if (fs.existsSync(agentMcpPath)) {
|
|
197
|
+
mcpPath = agentMcpPath;
|
|
104
198
|
}
|
|
105
|
-
if (settings.enableAllProjectMcpServers === false &&
|
|
199
|
+
else if (settings.enableAllProjectMcpServers === false &&
|
|
106
200
|
Array.isArray(settings.enabledMcpjsonServers)) {
|
|
107
201
|
if (fs.existsSync(mcpPath)) {
|
|
108
202
|
const fullMcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
@@ -120,40 +214,8 @@ export class ClaudeRunner {
|
|
|
120
214
|
}
|
|
121
215
|
}
|
|
122
216
|
}
|
|
123
|
-
catch (_e) {
|
|
124
|
-
// Warning
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const relativeMcp = path.relative(cwd, mcpPath);
|
|
128
|
-
if (!relativeMcp.startsWith('..') && !path.isAbsolute(relativeMcp)) {
|
|
129
|
-
mcpPath = relativeMcp.startsWith('./') ? relativeMcp : `./${relativeMcp}`;
|
|
130
|
-
}
|
|
131
|
-
let tmpSettingsPathToDelete = null;
|
|
132
|
-
let finalSettingsPath = settingsPath;
|
|
133
|
-
if (agentCustomEnv.AGENT_NICKNAME) {
|
|
134
|
-
try {
|
|
135
|
-
// On crée un fichier settings temporaire pour substituer le surnom par un vrai modèle
|
|
136
|
-
// car le CLI Claude ne valide pas les surnoms dynamiques en interne
|
|
137
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
138
|
-
const tempSettings = JSON.parse(JSON.stringify(settings));
|
|
139
|
-
// IMPORTANT: Mettre à jour toutes les variables env avec les valeurs interpolées
|
|
140
|
-
// pour éviter que le CLI Claude ne reçoive les valeurs $VAR littérales
|
|
141
|
-
if (tempSettings.env) {
|
|
142
|
-
for (const [key, value] of Object.entries(agentCustomEnv)) {
|
|
143
|
-
if (key !== 'AGENT_NICKNAME' && key !== 'AGENT_TIMEOUT_MS' && key !== 'API_TIMEOUT_MS') {
|
|
144
|
-
tempSettings.env[key] = value;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
tempSettings.env.ANTHROPIC_MODEL = agentCustomEnv.ANTHROPIC_MODEL;
|
|
149
|
-
const tmpSettingsPath = path.join(os.tmpdir(), `settings-${agentName || 'agent'}-${Date.now()}.json`);
|
|
150
|
-
fs.writeFileSync(tmpSettingsPath, JSON.stringify(tempSettings, null, 2));
|
|
151
|
-
finalSettingsPath = tmpSettingsPath;
|
|
152
|
-
tmpSettingsPathToDelete = tmpSettingsPath;
|
|
153
|
-
console.error(`[ClaudeRunner] 📝 Settings temporaire créé avec variables interpolées`);
|
|
154
|
-
}
|
|
155
217
|
catch (e) {
|
|
156
|
-
console.error(`[ClaudeRunner]
|
|
218
|
+
console.error(`[ClaudeRunner] [WARN] Error processing agent settings: ${e}`);
|
|
157
219
|
}
|
|
158
220
|
}
|
|
159
221
|
const argsSpawn = [];
|
|
@@ -161,215 +223,507 @@ export class ClaudeRunner {
|
|
|
161
223
|
argsSpawn.push(...CORE.split(' ').filter(Boolean));
|
|
162
224
|
if (PERMISSIONS)
|
|
163
225
|
argsSpawn.push(...PERMISSIONS.split(' ').filter(Boolean));
|
|
164
|
-
|
|
165
|
-
argsSpawn.push('--settings', finalSettingsPath);
|
|
226
|
+
argsSpawn.push('--settings', settingsPath);
|
|
166
227
|
argsSpawn.push('--mcp-config', mcpPath);
|
|
167
|
-
|
|
168
|
-
|
|
228
|
+
argsSpawn.push('--output-format', 'json');
|
|
229
|
+
let modelUsed = options.model;
|
|
230
|
+
if (!modelUsed && agentCustomEnv.ANTHROPIC_MODEL) {
|
|
231
|
+
modelUsed = agentCustomEnv.ANTHROPIC_MODEL;
|
|
169
232
|
}
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
console.error(`[ClaudeRunner] 👤 Nickname: ${agentCustomEnv.AGENT_NICKNAME}`);
|
|
176
|
-
argsSpawn.push('--name', agentCustomEnv.AGENT_NICKNAME);
|
|
233
|
+
// Remember original value (nickname or raw model) for display
|
|
234
|
+
const originalModel = modelUsed ?? '';
|
|
235
|
+
// Resolve nickname → real model ID before calling the API
|
|
236
|
+
if (modelUsed) {
|
|
237
|
+
modelUsed = resolveModel(modelUsed);
|
|
177
238
|
}
|
|
178
|
-
|
|
239
|
+
if (sessionId)
|
|
240
|
+
argsSpawn.push('--resume', sessionId);
|
|
241
|
+
if (modelUsed)
|
|
242
|
+
argsSpawn.push('--model', modelUsed);
|
|
243
|
+
if (agentName)
|
|
179
244
|
argsSpawn.push('--name', agentName);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
246
|
+
// 🔄 FALLBACK TOKEN RETRY LOGIC
|
|
247
|
+
//
|
|
248
|
+
// Overmind lit les tokens fallback depuis agentCustomEnv (résolus depuis $VAR).
|
|
249
|
+
// Si une erreur 401 (auth) survient, on tente chaque fallback séquentiellement :
|
|
250
|
+
// AUTH_FALLBACK_1 → AUTH_FALLBACK_2 → AUTH_FALLBACK_3
|
|
251
|
+
//
|
|
252
|
+
// Settings exemple :
|
|
253
|
+
// { "env": { "ANTHROPIC_AUTH_TOKEN": "$ANTHROPIC_AUTH_FALLBACK_1" } }
|
|
254
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
255
|
+
const FALLBACK_KEYS = ['AUTH_FALLBACK_1', 'AUTH_FALLBACK_2', 'AUTH_FALLBACK_3'];
|
|
256
|
+
const TOKEN_KEYS = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_AUTH_TOKEN_E'];
|
|
257
|
+
/**
|
|
258
|
+
* Vérifie si une erreur est retryable (fallback recommended).
|
|
259
|
+
* 401 = auth error (token invalide/expiré)
|
|
260
|
+
* 429 = rate limit / quota exhausted
|
|
261
|
+
* 500/502/503 = server error
|
|
262
|
+
*/
|
|
263
|
+
const isRetryableError = (stderr, jsonEnv) => {
|
|
264
|
+
const lower = stderr.toLowerCase();
|
|
265
|
+
const status = jsonEnv?.api_error_status;
|
|
266
|
+
if (status === 401)
|
|
267
|
+
return true;
|
|
268
|
+
if (status === 429)
|
|
269
|
+
return true;
|
|
270
|
+
if (status === 500 || status === 502 || status === 503)
|
|
271
|
+
return true;
|
|
272
|
+
return (lower.includes('401') ||
|
|
273
|
+
lower.includes('unauthorized') ||
|
|
274
|
+
lower.includes('invalid api key') ||
|
|
275
|
+
lower.includes('api key invalid') ||
|
|
276
|
+
lower.includes('authentication failed') ||
|
|
277
|
+
lower.includes('auth error') ||
|
|
278
|
+
lower.includes('invalid authentication') ||
|
|
279
|
+
lower.includes('429') ||
|
|
280
|
+
lower.includes('rate limit') ||
|
|
281
|
+
lower.includes('quota exhausted') ||
|
|
282
|
+
lower.includes('limit exhausted') ||
|
|
283
|
+
lower.includes('503') ||
|
|
284
|
+
lower.includes('service unavailable') ||
|
|
285
|
+
lower.includes('500') ||
|
|
286
|
+
lower.includes('internal server error'));
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Extrait les tokens fallback disponibles depuis agentCustomEnv.
|
|
290
|
+
* Retourne un tableau de { key, value } pour chaque fallback non vide.
|
|
291
|
+
*/
|
|
292
|
+
const getAvailableFallbacks = () => {
|
|
293
|
+
const fallbacks = [];
|
|
294
|
+
for (const key of FALLBACK_KEYS) {
|
|
295
|
+
const val = agentCustomEnv[key];
|
|
296
|
+
if (val && typeof val === 'string' && val.length > 0) {
|
|
297
|
+
fallbacks.push({ key, value: val });
|
|
210
298
|
}
|
|
211
299
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'Bypass',
|
|
230
|
-
'-File',
|
|
231
|
-
claudePath,
|
|
232
|
-
...argsSpawn,
|
|
233
|
-
'-p',
|
|
234
|
-
finalPrompt,
|
|
235
|
-
];
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
command = 'cmd.exe';
|
|
239
|
-
spawnArgs = ['/c', 'claude', ...argsSpawn, '-p', finalPrompt];
|
|
300
|
+
return fallbacks;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Détermine quel token utiliser selon l'index de retry.
|
|
304
|
+
* - index 0 = tentative initiale → use primary token (ANTHROPIC_AUTH_TOKEN)
|
|
305
|
+
* - index 1+ = retry → skip primary, use fallbacks directly
|
|
306
|
+
*/
|
|
307
|
+
const getTokenForIndex = (index) => {
|
|
308
|
+
if (index === 0) {
|
|
309
|
+
// Tentative initiale : utiliser le primary token
|
|
310
|
+
// NOTE: si la valeur est un $VAR non résolu (interpolateEnvVars n'a pas trouvé
|
|
311
|
+
// la variable dans process.env à ce moment), on le passe quand même à spawnWithToken
|
|
312
|
+
// qui fera la résolution finale via process.env.
|
|
313
|
+
for (const tk of TOKEN_KEYS) {
|
|
314
|
+
const val = agentCustomEnv[tk];
|
|
315
|
+
if (val && typeof val === 'string' && val.length > 0) {
|
|
316
|
+
return { tokenEnvKey: tk, tokenValue: val };
|
|
240
317
|
}
|
|
241
318
|
}
|
|
319
|
+
// Aucun primary token trouvé — retourner null plutôt que de tomber dans les fallbacks
|
|
320
|
+
return null;
|
|
242
321
|
}
|
|
243
|
-
|
|
244
|
-
|
|
322
|
+
// Retry (index >= 1) : skip primary, use fallbacks directly
|
|
323
|
+
const fallbacks = getAvailableFallbacks();
|
|
324
|
+
const fallbackIndex = index - 1; // index 1 → fallback[0] (AUTH_FALLBACK_1)
|
|
325
|
+
if (fallbackIndex < fallbacks.length) {
|
|
326
|
+
return { tokenEnvKey: fallbacks[fallbackIndex].key, tokenValue: fallbacks[fallbackIndex].value };
|
|
245
327
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
328
|
+
return null;
|
|
329
|
+
};
|
|
330
|
+
// ─── AbortSignal support ─────────────────────────────────────────────────────
|
|
331
|
+
let currentChildRef = null;
|
|
332
|
+
if (options.signal?.aborted) {
|
|
333
|
+
return Promise.reject(new Error('ABORTED'));
|
|
334
|
+
}
|
|
335
|
+
options.signal?.addEventListener('abort', () => {
|
|
336
|
+
if (currentChildRef)
|
|
337
|
+
void killProcessTree(currentChildRef);
|
|
338
|
+
});
|
|
339
|
+
// ─── DETACHED FAST-PATH ─────────────────────────────────────────────────────
|
|
340
|
+
// Fire-and-forget: parent returns immediately after spawn. Monitoring is done
|
|
341
|
+
// via the process registry + agent_control. No token retry, no in-process
|
|
342
|
+
// output capture — stdio is redirected to a log file the child writes to
|
|
343
|
+
// directly, so output keeps flowing even if our MCP server exits.
|
|
344
|
+
if (options.detached) {
|
|
345
|
+
const tokenInfo = getTokenForIndex(0);
|
|
346
|
+
const spawnEnv = {
|
|
347
|
+
...process.env,
|
|
348
|
+
...agentCustomEnv,
|
|
349
|
+
};
|
|
350
|
+
if (tokenInfo) {
|
|
351
|
+
for (const tk of TOKEN_KEYS)
|
|
352
|
+
delete spawnEnv[tk];
|
|
353
|
+
let resolvedToken = tokenInfo.tokenValue;
|
|
354
|
+
if (resolvedToken.startsWith('$')) {
|
|
355
|
+
resolvedToken = process.env[resolvedToken.slice(1)] || resolvedToken;
|
|
356
|
+
}
|
|
357
|
+
spawnEnv['ANTHROPIC_AUTH_TOKEN'] = resolvedToken;
|
|
251
358
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
359
|
+
const logDir = path.resolve(options.configPath || getWorkspaceDir(), '.claude/agent_logs');
|
|
360
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
361
|
+
const stamp = `${Date.now()}`;
|
|
362
|
+
const logFile = path.join(logDir, `${agentName || 'agent'}_${stamp}.log`);
|
|
363
|
+
const promptFile = path.join(logDir, `${agentName || 'agent'}_${stamp}.prompt`);
|
|
364
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
365
|
+
// On Windows the `claude` shim is a .cmd file. Inherited file FDs survive
|
|
366
|
+
// a direct .exe spawn fine, but cmd.exe (in detached mode) silently drops
|
|
367
|
+
// them before they reach claude.cmd → log file ends up empty. We resolve
|
|
368
|
+
// `claude.exe` directly to skip the shim entirely. On Unix we just call
|
|
369
|
+
// `claude` (a real executable on PATH).
|
|
370
|
+
const promptFd = fs.openSync(promptFile, 'r');
|
|
371
|
+
const logFd = fs.openSync(logFile, 'a');
|
|
372
|
+
const claudeExe = process.platform === 'win32' ? resolveClaudeExePath() : null;
|
|
373
|
+
const detachedCommand = claudeExe ?? 'claude';
|
|
374
|
+
const detachedArgs = [...argsSpawn, '-p'];
|
|
375
|
+
const child = spawn(detachedCommand, detachedArgs, {
|
|
376
|
+
cwd: options.cwd || process.cwd(),
|
|
256
377
|
windowsHide: true,
|
|
257
|
-
env:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
...(agentCustomEnv.ANTHROPIC_AUTH_TOKEN && !agentCustomEnv.ANTHROPIC_API_KEY
|
|
262
|
-
? { ANTHROPIC_API_KEY: agentCustomEnv.ANTHROPIC_AUTH_TOKEN }
|
|
263
|
-
: {}),
|
|
264
|
-
...(agentName ? { OVERMIND_AGENT_NAME: agentName } : {}),
|
|
265
|
-
},
|
|
378
|
+
env: spawnEnv,
|
|
379
|
+
shell: false,
|
|
380
|
+
detached: true,
|
|
381
|
+
stdio: [promptFd, logFd, logFd],
|
|
266
382
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
registerProcess(child.pid, {
|
|
270
|
-
agentName: agentName || 'unknown',
|
|
271
|
-
runner: 'claude',
|
|
272
|
-
}).catch(() => { });
|
|
383
|
+
try {
|
|
384
|
+
fs.closeSync(promptFd);
|
|
273
385
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
child.stdout.on('data', (d) => {
|
|
278
|
-
const chunk = d.toString();
|
|
279
|
-
stdout += chunk;
|
|
280
|
-
if (agentName) {
|
|
281
|
-
const id = agentCustomEnv.AGENT_NICKNAME || agentName;
|
|
282
|
-
process.stderr.write(`[ClaudeRunner:${id}] ${chunk}`);
|
|
283
|
-
}
|
|
284
|
-
// Append to registry for stream tracking
|
|
285
|
-
if (child.pid) {
|
|
286
|
-
appendOutput(child.pid, chunk).catch(() => { });
|
|
287
|
-
}
|
|
288
|
-
});
|
|
386
|
+
catch { /* ignored */ }
|
|
387
|
+
try {
|
|
388
|
+
fs.closeSync(logFd);
|
|
289
389
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
390
|
+
catch { /* ignored */ }
|
|
391
|
+
if (child.pid) {
|
|
392
|
+
await registerProcess(child.pid, {
|
|
393
|
+
agentName: agentName || '',
|
|
394
|
+
runner: 'claude',
|
|
395
|
+
configPath: options.configPath,
|
|
396
|
+
logFile,
|
|
397
|
+
detached: true,
|
|
298
398
|
});
|
|
299
399
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
cleanupTmpFiles();
|
|
306
|
-
resolve({ result: '', error: `TIMEOUT`, rawOutput: stdout });
|
|
307
|
-
}, customTimeoutMs);
|
|
308
|
-
child.on('close', async (code) => {
|
|
309
|
-
clearTimeout(timeout);
|
|
310
|
-
cleanupTmpFiles();
|
|
311
|
-
// Update process registry status
|
|
312
|
-
if (child.pid) {
|
|
313
|
-
const status = code === 0 ? 'done' : 'failed';
|
|
314
|
-
updateProcessStatus(child.pid, status, code).catch(() => { });
|
|
400
|
+
// Best-effort cleanup of the prompt file once the child is done reading it.
|
|
401
|
+
// 30s is generous: claude reads stdin within the first second normally.
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
try {
|
|
404
|
+
fs.unlinkSync(promptFile);
|
|
315
405
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
406
|
+
catch { /* ignored */ }
|
|
407
|
+
}, 30000).unref();
|
|
408
|
+
child.unref();
|
|
409
|
+
return {
|
|
410
|
+
result: `DETACHED: pid=${child.pid ?? 'unknown'}, agentName=${agentName || ''}\n` +
|
|
411
|
+
`logFile=${logFile}\n` +
|
|
412
|
+
`Use agent_control({action:"status"|"stream"|"wait"|"kill", agentName, runner:"claude"}) to monitor.`,
|
|
413
|
+
sessionId: undefined,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const runImpl = async (span) => {
|
|
417
|
+
span.setAttribute('agentName', agentName || '');
|
|
418
|
+
span.setAttribute('model', modelUsed || '');
|
|
419
|
+
span.setAttribute('runner', 'claude');
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
let resolved = false;
|
|
422
|
+
let retryCount = 0;
|
|
423
|
+
const maxRetries = getAvailableFallbacks().length + 1; // primary + fallbacks
|
|
424
|
+
currentChildRef = null;
|
|
425
|
+
let currentStderr = '';
|
|
426
|
+
let currentStdout = '';
|
|
427
|
+
const MAX_BUF = 10 * 1024 * 1024;
|
|
428
|
+
let currentSessionId = sessionId;
|
|
429
|
+
let earlyExitTriggered = false; // Prevent double-exit on early retry
|
|
430
|
+
const safeResolve = (value) => {
|
|
431
|
+
if (!resolved) {
|
|
432
|
+
resolved = true;
|
|
433
|
+
resolve(value);
|
|
320
434
|
}
|
|
321
|
-
|
|
322
|
-
|
|
435
|
+
};
|
|
436
|
+
const triggerRetry = async (targetRetryCount) => {
|
|
437
|
+
if (earlyExitTriggered)
|
|
438
|
+
return;
|
|
439
|
+
earlyExitTriggered = true;
|
|
440
|
+
if (hardTimeoutTimer)
|
|
441
|
+
clearTimeout(hardTimeoutTimer);
|
|
442
|
+
if (killTimer)
|
|
443
|
+
clearTimeout(killTimer);
|
|
444
|
+
// Attendre la mort effective de l'arbre (cmd.exe + claude.exe sur Win)
|
|
445
|
+
// avant de respawn, sinon le nouveau process tape sur la même session
|
|
446
|
+
// encore "vivante" côté provider → 429 fantôme.
|
|
447
|
+
if (currentChildRef) {
|
|
448
|
+
await killProcessTree(currentChildRef);
|
|
449
|
+
}
|
|
450
|
+
retryCount = targetRetryCount;
|
|
451
|
+
const tokenInfo = getTokenForIndex(retryCount);
|
|
452
|
+
if (!options.silent) {
|
|
453
|
+
process.stderr.write(`\n\x1b[41m\x1b[37m[ClaudeRunner] 🔄 Retry ${retryCount}/${maxRetries} avec ${tokenInfo?.tokenEnvKey || 'UNKNOWN'}...\x1b[0m\n`);
|
|
323
454
|
}
|
|
324
|
-
|
|
325
|
-
|
|
455
|
+
setImmediate(() => spawnWithToken(tokenInfo));
|
|
456
|
+
};
|
|
457
|
+
const cleanupTmpFiles = () => {
|
|
458
|
+
if (tmpMcpPathToDelete && fs.existsSync(tmpMcpPathToDelete)) {
|
|
459
|
+
try {
|
|
460
|
+
fs.unlinkSync(tmpMcpPathToDelete);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Ignored
|
|
464
|
+
}
|
|
326
465
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
466
|
+
if (tmpSettingsPathToDelete && fs.existsSync(tmpSettingsPathToDelete)) {
|
|
467
|
+
try {
|
|
468
|
+
fs.unlinkSync(tmpSettingsPathToDelete);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Ignored
|
|
472
|
+
}
|
|
329
473
|
}
|
|
330
|
-
return null;
|
|
331
474
|
};
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
475
|
+
let killTimer = null;
|
|
476
|
+
let hardTimeoutTimer = null;
|
|
477
|
+
/**
|
|
478
|
+
* Fonction centrale qui spawn le processus Claude avec le bon token.
|
|
479
|
+
* Appelé initialement et après chaque retry.
|
|
480
|
+
*/
|
|
481
|
+
const spawnWithToken = (tokenInfo) => {
|
|
482
|
+
// Nettoyer les listeners/timers de la tentative précédente
|
|
483
|
+
if (hardTimeoutTimer) {
|
|
484
|
+
clearTimeout(hardTimeoutTimer);
|
|
485
|
+
hardTimeoutTimer = null;
|
|
486
|
+
}
|
|
487
|
+
if (killTimer) {
|
|
488
|
+
clearTimeout(killTimer);
|
|
489
|
+
killTimer = null;
|
|
346
490
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
491
|
+
// Construire l'env avec le bon token
|
|
492
|
+
// NOTE: Overmind gère la substitution des variables $VAR dans les settings.
|
|
493
|
+
// Les fallback tokens (AUTH_FALLBACK_1/2/3) sont résolus ici pour le retry 401.
|
|
494
|
+
const spawnEnv = {
|
|
495
|
+
...process.env,
|
|
496
|
+
...agentCustomEnv,
|
|
497
|
+
};
|
|
498
|
+
if (tokenInfo) {
|
|
499
|
+
// Remplacer le token actif par celui du fallback
|
|
500
|
+
// NOTE: Les tokens peuvent encore contenir des $VAR non résolus
|
|
501
|
+
// (interpolateEnvVars n'a pas trouvé ces vars dans process.env au moment du load).
|
|
502
|
+
// On résout ici via process.env (qui a été peuplé par loadEnvQuietly).
|
|
503
|
+
for (const tk of TOKEN_KEYS) {
|
|
504
|
+
delete spawnEnv[tk];
|
|
352
505
|
}
|
|
506
|
+
let resolvedToken = tokenInfo.tokenValue;
|
|
507
|
+
if (resolvedToken.startsWith('$')) {
|
|
508
|
+
const envKey = resolvedToken.slice(1);
|
|
509
|
+
resolvedToken = process.env[envKey] || resolvedToken;
|
|
510
|
+
}
|
|
511
|
+
// Le Claude CLI lit ANTHROPIC_AUTH_TOKEN — on injecte toujours sous ce nom,
|
|
512
|
+
// peu importe que tokenInfo vienne du primary ou d'un AUTH_FALLBACK_n.
|
|
513
|
+
spawnEnv['ANTHROPIC_AUTH_TOKEN'] = resolvedToken;
|
|
514
|
+
}
|
|
515
|
+
currentStderr = '';
|
|
516
|
+
currentStdout = '';
|
|
517
|
+
const command = process.platform === 'win32' ? 'cmd.exe' : 'claude';
|
|
518
|
+
const spawnArgs = process.platform === 'win32'
|
|
519
|
+
? ['/c', 'claude', ...argsSpawn, '-p']
|
|
520
|
+
: ['claude', ...argsSpawn, '-p'];
|
|
521
|
+
if (!options.silent) {
|
|
522
|
+
const tokenLabel = tokenInfo ? ` (token: ${tokenInfo.tokenEnvKey})` : '';
|
|
523
|
+
process.stderr.write(`\n\x1b[33m[ClaudeRunner]${tokenLabel} Spawning Claude CLI...\x1b[0m\n`);
|
|
353
524
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
525
|
+
currentChildRef = spawn(command, spawnArgs, {
|
|
526
|
+
cwd: options.cwd || process.cwd(),
|
|
527
|
+
windowsHide: true,
|
|
528
|
+
env: spawnEnv,
|
|
529
|
+
shell: false,
|
|
530
|
+
signal: options.signal,
|
|
358
531
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
532
|
+
// Register process immediately after spawn
|
|
533
|
+
if (currentChildRef.pid) {
|
|
534
|
+
void registerProcess(currentChildRef.pid, {
|
|
535
|
+
agentName: agentName || '',
|
|
536
|
+
runner: 'claude',
|
|
537
|
+
configPath: options.configPath,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (currentChildRef.stdout) {
|
|
541
|
+
currentChildRef.stdout.on('data', (d) => {
|
|
542
|
+
const chunk = d.toString();
|
|
543
|
+
if (currentChildRef && currentChildRef.pid && chunk) {
|
|
544
|
+
void appendOutput(currentChildRef.pid, chunk, options.configPath);
|
|
545
|
+
}
|
|
546
|
+
if (currentStdout.length + chunk.length > MAX_BUF)
|
|
547
|
+
currentStdout = currentStdout.slice(-MAX_BUF);
|
|
548
|
+
else
|
|
549
|
+
currentStdout += chunk;
|
|
550
|
+
if (agentName && !options.silent) {
|
|
551
|
+
process.stderr.write(`[ClaudeRunner:${agentName}] ${chunk}`);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (currentChildRef.stderr) {
|
|
556
|
+
currentChildRef.stderr.on('data', (d) => {
|
|
557
|
+
const chunk = d.toString();
|
|
558
|
+
if (currentChildRef && currentChildRef.pid && chunk) {
|
|
559
|
+
void appendOutput(currentChildRef.pid, chunk, options.configPath);
|
|
560
|
+
}
|
|
561
|
+
if (currentStderr.length + chunk.length > MAX_BUF)
|
|
562
|
+
currentStderr = currentStderr.slice(-MAX_BUF);
|
|
563
|
+
else
|
|
564
|
+
currentStderr += chunk;
|
|
565
|
+
if (agentName && !options.silent) {
|
|
566
|
+
process.stderr.write(`[ClaudeRunner:${agentName}:ERR] ${chunk}`);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (currentChildRef.stdin) {
|
|
571
|
+
currentChildRef.stdin.write(prompt);
|
|
572
|
+
currentChildRef.stdin.end();
|
|
573
|
+
}
|
|
574
|
+
const timeout = setTimeout(() => {
|
|
575
|
+
if (currentChildRef && currentChildRef.stdin && !currentChildRef.stdin.destroyed) {
|
|
576
|
+
try {
|
|
577
|
+
currentChildRef.stdin.write('\n');
|
|
578
|
+
if (!options.silent) {
|
|
579
|
+
process.stderr.write(`\n\x1b[33m[ClaudeRunner] [WARN] Agent stagnant (${customTimeoutMs}ms). Envoi d'un keep-alive (\\n)...\x1b[0m\n`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (_e) {
|
|
583
|
+
// Ignore
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const hardTimeoutDelay = CONFIG.HARD_TIMEOUT_MS || 60000;
|
|
587
|
+
hardTimeoutTimer = setTimeout(() => {
|
|
588
|
+
if (currentChildRef)
|
|
589
|
+
void killProcessTree(currentChildRef);
|
|
590
|
+
cleanupTmpFiles();
|
|
591
|
+
safeResolve({
|
|
592
|
+
result: '',
|
|
593
|
+
error: 'HARD_TIMEOUT',
|
|
594
|
+
rawOutput: currentStdout + currentStderr,
|
|
595
|
+
});
|
|
596
|
+
}, hardTimeoutDelay);
|
|
597
|
+
}, customTimeoutMs);
|
|
598
|
+
currentChildRef.on('error', (err) => {
|
|
599
|
+
clearTimeout(timeout);
|
|
600
|
+
if (hardTimeoutTimer)
|
|
601
|
+
clearTimeout(hardTimeoutTimer);
|
|
602
|
+
cleanupTmpFiles();
|
|
603
|
+
safeResolve({ result: '', error: `SPAWN_ERROR: ${err.message}`, rawOutput: '' });
|
|
366
604
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
605
|
+
currentChildRef.on('close', async (code) => {
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
if (hardTimeoutTimer)
|
|
608
|
+
clearTimeout(hardTimeoutTimer);
|
|
609
|
+
const fullRaw = currentStdout + (currentStderr ? `\n\n--- STDERR ---\n${currentStderr}` : '');
|
|
610
|
+
// ─── Parser le JSON en premier (pour extraire api_error_status) ───
|
|
611
|
+
let jsonEnvelope = null;
|
|
612
|
+
const trimmedStdout = currentStdout.trim();
|
|
613
|
+
try {
|
|
614
|
+
jsonEnvelope = JSON.parse(trimmedStdout);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
const lastBrace = trimmedStdout.lastIndexOf('}');
|
|
618
|
+
const firstBrace = trimmedStdout.lastIndexOf('{', lastBrace);
|
|
619
|
+
if (firstBrace !== -1 && lastBrace !== -1) {
|
|
620
|
+
try {
|
|
621
|
+
jsonEnvelope = JSON.parse(trimmedStdout.substring(firstBrace, lastBrace + 1));
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Ignored
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// ─── Fallback retry ─────────────────────────────────────────────────
|
|
629
|
+
// Sur 401/429/5xx, on tue l'arbre du process courant via killProcessTree
|
|
630
|
+
// (taskkill /F /T sur Windows pour ne pas laisser claude.exe orphelin),
|
|
631
|
+
// puis on respawn un nouveau noeud avec --resume + le token fallback
|
|
632
|
+
// suivant (AUTH_FALLBACK_1 → 2 → 3).
|
|
633
|
+
// ───────────────────────────────────────────────────────────────────
|
|
634
|
+
const FALLBACK_RETRY_ENABLED = true;
|
|
635
|
+
const isRetryable = isRetryableError(currentStderr, jsonEnvelope);
|
|
636
|
+
const hasRetryableStatus = jsonEnvelope !== null &&
|
|
637
|
+
(jsonEnvelope.api_error_status === 401 ||
|
|
638
|
+
jsonEnvelope.api_error_status === 429 ||
|
|
639
|
+
jsonEnvelope.api_error_status === 500 ||
|
|
640
|
+
jsonEnvelope.api_error_status === 502 ||
|
|
641
|
+
jsonEnvelope.api_error_status === 503);
|
|
642
|
+
const isFailure = FALLBACK_RETRY_ENABLED && ((code !== 0 && isRetryable) || hasRetryableStatus);
|
|
643
|
+
if (isFailure) {
|
|
644
|
+
if (retryCount < maxRetries) {
|
|
645
|
+
triggerRetry(retryCount + 1);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
if (!options.silent) {
|
|
650
|
+
process.stderr.write(`\n\x1b[41m\x1b[37m[ClaudeRunner] ❌ Tous les tokens fallback épuisés. Error retryable finale.\x1b[0m\n`);
|
|
651
|
+
}
|
|
652
|
+
if (currentChildRef?.pid) {
|
|
653
|
+
void updateProcessStatus(currentChildRef.pid, 'failed', code, options.configPath);
|
|
654
|
+
}
|
|
655
|
+
cleanupTmpFiles();
|
|
656
|
+
safeResolve({
|
|
657
|
+
result: '',
|
|
658
|
+
error: 'RETRYABLE_ERROR_ALL_FALLBACKS_EXHAUSTED',
|
|
659
|
+
rawOutput: fullRaw,
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
cleanupTmpFiles();
|
|
665
|
+
try {
|
|
666
|
+
if (jsonEnvelope) {
|
|
667
|
+
let foundSessionId = currentSessionId;
|
|
668
|
+
if (jsonEnvelope.session_id && agentName) {
|
|
669
|
+
foundSessionId = jsonEnvelope.session_id;
|
|
670
|
+
currentSessionId = foundSessionId;
|
|
671
|
+
await saveSessionId(agentName, jsonEnvelope.session_id, options.configPath, 'claude');
|
|
672
|
+
if (currentChildRef?.pid) {
|
|
673
|
+
void linkSessionToPid(jsonEnvelope.session_id, currentChildRef.pid, options.configPath);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (currentChildRef?.pid) {
|
|
677
|
+
void updateProcessStatus(currentChildRef.pid, code === 0 ? 'done' : 'failed', code ?? null, options.configPath);
|
|
678
|
+
}
|
|
679
|
+
return safeResolve({
|
|
680
|
+
result: jsonEnvelope.reply ||
|
|
681
|
+
jsonEnvelope.result ||
|
|
682
|
+
currentStdout.trim(),
|
|
683
|
+
sessionId: foundSessionId,
|
|
684
|
+
rawOutput: currentStdout,
|
|
685
|
+
model: modelUsed ?? undefined,
|
|
686
|
+
nickname: originalModel !== modelUsed ? originalModel : undefined,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (code === 0) {
|
|
690
|
+
if (currentChildRef?.pid) {
|
|
691
|
+
void updateProcessStatus(currentChildRef.pid, 'done', code, options.configPath);
|
|
692
|
+
}
|
|
693
|
+
return safeResolve({
|
|
694
|
+
result: currentStdout.trim(),
|
|
695
|
+
sessionId: currentSessionId,
|
|
696
|
+
rawOutput: currentStdout,
|
|
697
|
+
model: modelUsed ?? undefined,
|
|
698
|
+
nickname: originalModel !== modelUsed ? originalModel : undefined,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (currentChildRef?.pid) {
|
|
702
|
+
void updateProcessStatus(currentChildRef.pid, 'failed', code, options.configPath);
|
|
703
|
+
}
|
|
704
|
+
safeResolve({
|
|
705
|
+
result: '',
|
|
706
|
+
error: code !== 0 ? `EXIT_CODE_${code}` : 'JSON_PARSE_ERROR',
|
|
707
|
+
rawOutput: fullRaw,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
safeResolve({
|
|
712
|
+
result: '',
|
|
713
|
+
error: `INTERNAL_ERROR: ${error instanceof Error ? error.message : String(error)}`,
|
|
714
|
+
rawOutput: fullRaw,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
};
|
|
719
|
+
// ─── Démarrage initial avec le primary token ───
|
|
720
|
+
spawnWithToken(getTokenForIndex(0));
|
|
372
721
|
});
|
|
722
|
+
};
|
|
723
|
+
return withSpan('claude.runAgent', runImpl, {
|
|
724
|
+
agentName: agentName || '',
|
|
725
|
+
model: modelUsed || '',
|
|
726
|
+
runner: 'claude',
|
|
373
727
|
});
|
|
374
728
|
}
|
|
375
729
|
}
|