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.
Files changed (225) hide show
  1. package/.mcp.json.example +21 -0
  2. package/README.md +107 -80
  3. package/assets/overmind.png +0 -0
  4. package/bin/.gitkeep +0 -0
  5. package/bin/README.md +34 -0
  6. package/bin/install-overmind-unix.sh +412 -0
  7. package/bin/install-overmind-windows.bat +407 -0
  8. package/dist/bin/cli.js +438 -7
  9. package/dist/bin/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/InstallHelper.d.ts +14 -0
  15. package/dist/lib/InstallHelper.d.ts.map +1 -0
  16. package/dist/lib/InstallHelper.js +115 -0
  17. package/dist/lib/InstallHelper.js.map +1 -0
  18. package/dist/lib/config.d.ts +31 -1
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +19 -27
  21. package/dist/lib/config.js.map +1 -1
  22. package/dist/lib/envUtils.d.ts +10 -0
  23. package/dist/lib/envUtils.d.ts.map +1 -0
  24. package/dist/lib/envUtils.js +24 -0
  25. package/dist/lib/envUtils.js.map +1 -0
  26. package/dist/lib/loadEnv.d.ts +2 -0
  27. package/dist/lib/loadEnv.d.ts.map +1 -0
  28. package/dist/lib/loadEnv.js +26 -0
  29. package/dist/lib/loadEnv.js.map +1 -0
  30. package/dist/lib/logger.d.ts +8 -0
  31. package/dist/lib/logger.d.ts.map +1 -0
  32. package/dist/lib/logger.js +81 -0
  33. package/dist/lib/logger.js.map +1 -0
  34. package/dist/lib/modelMapping.d.ts +18 -0
  35. package/dist/lib/modelMapping.d.ts.map +1 -0
  36. package/dist/lib/modelMapping.js +90 -0
  37. package/dist/lib/modelMapping.js.map +1 -0
  38. package/dist/lib/orchestration/dispatcher.d.ts +32 -0
  39. package/dist/lib/orchestration/dispatcher.d.ts.map +1 -0
  40. package/dist/lib/orchestration/dispatcher.js +113 -0
  41. package/dist/lib/orchestration/dispatcher.js.map +1 -0
  42. package/dist/lib/orchestration/swarm.d.ts +75 -0
  43. package/dist/lib/orchestration/swarm.d.ts.map +1 -0
  44. package/dist/lib/orchestration/swarm.js +239 -0
  45. package/dist/lib/orchestration/swarm.js.map +1 -0
  46. package/dist/lib/processRegistry.d.ts +10 -0
  47. package/dist/lib/processRegistry.d.ts.map +1 -1
  48. package/dist/lib/processRegistry.js +85 -29
  49. package/dist/lib/processRegistry.js.map +1 -1
  50. package/dist/lib/sessions.d.ts +3 -2
  51. package/dist/lib/sessions.d.ts.map +1 -1
  52. package/dist/lib/sessions.js +94 -25
  53. package/dist/lib/sessions.js.map +1 -1
  54. package/dist/lib/telemetry.d.ts +21 -0
  55. package/dist/lib/telemetry.d.ts.map +1 -0
  56. package/dist/lib/telemetry.js +30 -0
  57. package/dist/lib/telemetry.js.map +1 -0
  58. package/dist/memory/MemoryFactory.d.ts +15 -1
  59. package/dist/memory/MemoryFactory.d.ts.map +1 -1
  60. package/dist/memory/MemoryFactory.js +53 -3
  61. package/dist/memory/MemoryFactory.js.map +1 -1
  62. package/dist/memory/PostgresMemoryProvider.d.ts +7 -0
  63. package/dist/memory/PostgresMemoryProvider.d.ts.map +1 -1
  64. package/dist/memory/PostgresMemoryProvider.js +180 -105
  65. package/dist/memory/PostgresMemoryProvider.js.map +1 -1
  66. package/dist/server.d.ts +41 -0
  67. package/dist/server.d.ts.map +1 -1
  68. package/dist/server.js +145 -41
  69. package/dist/server.js.map +1 -1
  70. package/dist/services/AgentManager.d.ts.map +1 -1
  71. package/dist/services/AgentManager.js +67 -11
  72. package/dist/services/AgentManager.js.map +1 -1
  73. package/dist/services/ClaudeRunner.d.ts +20 -0
  74. package/dist/services/ClaudeRunner.d.ts.map +1 -1
  75. package/dist/services/ClaudeRunner.js +626 -272
  76. package/dist/services/ClaudeRunner.js.map +1 -1
  77. package/dist/services/ClineRunner.d.ts +7 -0
  78. package/dist/services/ClineRunner.d.ts.map +1 -1
  79. package/dist/services/ClineRunner.js +76 -22
  80. package/dist/services/ClineRunner.js.map +1 -1
  81. package/dist/services/GeminiRunner.d.ts +6 -0
  82. package/dist/services/GeminiRunner.d.ts.map +1 -1
  83. package/dist/services/GeminiRunner.js +283 -69
  84. package/dist/services/GeminiRunner.js.map +1 -1
  85. package/dist/services/KiloRunner.d.ts +12 -0
  86. package/dist/services/KiloRunner.d.ts.map +1 -1
  87. package/dist/services/KiloRunner.js +439 -70
  88. package/dist/services/KiloRunner.js.map +1 -1
  89. package/dist/services/NousHermesRunner.d.ts +35 -0
  90. package/dist/services/NousHermesRunner.d.ts.map +1 -0
  91. package/dist/services/NousHermesRunner.js +535 -0
  92. package/dist/services/NousHermesRunner.js.map +1 -0
  93. package/dist/services/OpenClawRunner.d.ts +7 -0
  94. package/dist/services/OpenClawRunner.d.ts.map +1 -1
  95. package/dist/services/OpenClawRunner.js +75 -22
  96. package/dist/services/OpenClawRunner.js.map +1 -1
  97. package/dist/services/OpenCodeRunner.d.ts +7 -0
  98. package/dist/services/OpenCodeRunner.d.ts.map +1 -1
  99. package/dist/services/OpenCodeRunner.js +75 -22
  100. package/dist/services/OpenCodeRunner.js.map +1 -1
  101. package/dist/services/{TraeRunner.d.ts → QwenCliRunner.d.ts} +9 -2
  102. package/dist/services/QwenCliRunner.d.ts.map +1 -0
  103. package/dist/services/QwenCliRunner.js +155 -0
  104. package/dist/services/QwenCliRunner.js.map +1 -0
  105. package/dist/tools/agent_control.d.ts +2 -2
  106. package/dist/tools/agent_control.d.ts.map +1 -1
  107. package/dist/tools/agent_control.js +79 -50
  108. package/dist/tools/agent_control.js.map +1 -1
  109. package/dist/tools/config_example.d.ts +7 -4
  110. package/dist/tools/config_example.d.ts.map +1 -1
  111. package/dist/tools/config_example.js +191 -86
  112. package/dist/tools/config_example.js.map +1 -1
  113. package/dist/tools/create_agent.d.ts +13 -7
  114. package/dist/tools/create_agent.d.ts.map +1 -1
  115. package/dist/tools/create_agent.js +8 -9
  116. package/dist/tools/create_agent.js.map +1 -1
  117. package/dist/tools/get_agent_configs.d.ts +10 -4
  118. package/dist/tools/get_agent_configs.d.ts.map +1 -1
  119. package/dist/tools/get_agent_configs.js.map +1 -1
  120. package/dist/tools/initialization_check.d.ts +2 -0
  121. package/dist/tools/initialization_check.d.ts.map +1 -0
  122. package/dist/tools/initialization_check.js +23 -0
  123. package/dist/tools/initialization_check.js.map +1 -0
  124. package/dist/tools/manage_agents.d.ts +34 -16
  125. package/dist/tools/manage_agents.d.ts.map +1 -1
  126. package/dist/tools/manage_agents.js +2 -2
  127. package/dist/tools/manage_agents.js.map +1 -1
  128. package/dist/tools/manage_prompts.d.ts +13 -8
  129. package/dist/tools/manage_prompts.d.ts.map +1 -1
  130. package/dist/tools/manage_prompts.js.map +1 -1
  131. package/dist/tools/memory_runs.d.ts +3 -4
  132. package/dist/tools/memory_runs.d.ts.map +1 -1
  133. package/dist/tools/memory_runs.js.map +1 -1
  134. package/dist/tools/memory_search.d.ts +3 -4
  135. package/dist/tools/memory_search.d.ts.map +1 -1
  136. package/dist/tools/memory_search.js.map +1 -1
  137. package/dist/tools/memory_store.d.ts +10 -4
  138. package/dist/tools/memory_store.d.ts.map +1 -1
  139. package/dist/tools/memory_store.js.map +1 -1
  140. package/dist/tools/run_agent.d.ts +14 -14
  141. package/dist/tools/run_agent.d.ts.map +1 -1
  142. package/dist/tools/run_agent.js +135 -156
  143. package/dist/tools/run_agent.js.map +1 -1
  144. package/dist/tools/run_agent_cli.d.ts +21 -0
  145. package/dist/tools/run_agent_cli.d.ts.map +1 -0
  146. package/dist/tools/run_agent_cli.js +137 -0
  147. package/dist/tools/run_agent_cli.js.map +1 -0
  148. package/dist/tools/run_agents_parallel.d.ts +35 -0
  149. package/dist/tools/run_agents_parallel.d.ts.map +1 -0
  150. package/dist/tools/run_agents_parallel.js +28 -0
  151. package/dist/tools/run_agents_parallel.js.map +1 -0
  152. package/dist/tools/run_claude.d.ts +9 -3
  153. package/dist/tools/run_claude.d.ts.map +1 -1
  154. package/dist/tools/run_claude.js +67 -41
  155. package/dist/tools/run_claude.js.map +1 -1
  156. package/dist/tools/run_cline.d.ts +13 -4
  157. package/dist/tools/run_cline.d.ts.map +1 -1
  158. package/dist/tools/run_cline.js +55 -43
  159. package/dist/tools/run_cline.js.map +1 -1
  160. package/dist/tools/run_gemini.d.ts +13 -4
  161. package/dist/tools/run_gemini.d.ts.map +1 -1
  162. package/dist/tools/run_gemini.js +53 -33
  163. package/dist/tools/run_gemini.js.map +1 -1
  164. package/dist/tools/run_hermes.d.ts +25 -0
  165. package/dist/tools/run_hermes.d.ts.map +1 -0
  166. package/dist/tools/run_hermes.js +93 -0
  167. package/dist/tools/run_hermes.js.map +1 -0
  168. package/dist/tools/run_kilo.d.ts +17 -7
  169. package/dist/tools/run_kilo.d.ts.map +1 -1
  170. package/dist/tools/run_kilo.js +68 -41
  171. package/dist/tools/run_kilo.js.map +1 -1
  172. package/dist/tools/run_openclaw.d.ts +13 -4
  173. package/dist/tools/run_openclaw.d.ts.map +1 -1
  174. package/dist/tools/run_openclaw.js +52 -44
  175. package/dist/tools/run_openclaw.js.map +1 -1
  176. package/dist/tools/run_opencode.d.ts +13 -4
  177. package/dist/tools/run_opencode.d.ts.map +1 -1
  178. package/dist/tools/run_opencode.js +52 -39
  179. package/dist/tools/run_opencode.js.map +1 -1
  180. package/dist/tools/run_qwencli.d.ts +24 -0
  181. package/dist/tools/run_qwencli.d.ts.map +1 -0
  182. package/dist/tools/run_qwencli.js +59 -0
  183. package/dist/tools/run_qwencli.js.map +1 -0
  184. package/docs/README.md +128 -0
  185. package/docs/agent_control.md +656 -0
  186. package/docs/index.html +493 -0
  187. package/docs/library.html +239 -0
  188. package/docs/prompt.html +1212 -0
  189. package/docs/script.js +428 -0
  190. package/docs/styles.css +2816 -0
  191. package/package.json +54 -25
  192. package/scripts/auto-changelog.mjs +132 -0
  193. package/scripts/auto-install.mjs +322 -0
  194. package/scripts/install-dependencies.mjs +462 -0
  195. package/scripts/postgres-manager.mjs +219 -0
  196. package/scripts/postinstall.mjs +538 -0
  197. package/scripts/setup-overmind-db.mjs +199 -0
  198. package/scripts/setup-windows.js +266 -0
  199. package/scripts/setup.mjs +397 -0
  200. package/scripts/test-installation.mjs +158 -0
  201. package/scripts/uninstall.mjs +238 -0
  202. package/assets/overmind_mcp_pro_banner_v3.png +0 -0
  203. package/dist/services/QwenRunner.d.ts +0 -19
  204. package/dist/services/QwenRunner.d.ts.map +0 -1
  205. package/dist/services/QwenRunner.js +0 -102
  206. package/dist/services/QwenRunner.js.map +0 -1
  207. package/dist/services/TraeRunner.d.ts.map +0 -1
  208. package/dist/services/TraeRunner.js +0 -103
  209. package/dist/services/TraeRunner.js.map +0 -1
  210. package/dist/tools/run_qwen.d.ts +0 -15
  211. package/dist/tools/run_qwen.d.ts.map +0 -1
  212. package/dist/tools/run_qwen.js +0 -61
  213. package/dist/tools/run_qwen.js.map +0 -1
  214. package/dist/tools/run_trae.d.ts +0 -15
  215. package/dist/tools/run_trae.d.ts.map +0 -1
  216. package/dist/tools/run_trae.js +0 -66
  217. package/dist/tools/run_trae.js.map +0 -1
  218. package/dist/tools/shell_execute.d.ts +0 -10
  219. package/dist/tools/shell_execute.d.ts.map +0 -1
  220. package/dist/tools/shell_execute.js +0 -24
  221. package/dist/tools/shell_execute.js.map +0 -1
  222. package/dist/tools/test_agent_control.d.ts +0 -2
  223. package/dist/tools/test_agent_control.d.ts.map +0 -1
  224. package/dist/tools/test_agent_control.js +0 -92
  225. 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 { spawn } from 'child_process';
5
- import { CONFIG, resolveConfigPath } from '../lib/config.js';
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 { registerProcess, appendOutput, updateProcessStatus, linkSessionToPid } from '../lib/processRegistry.js';
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
- * Interpole les variables d'environnement dans les valeurs de type $VAR
10
- * Ex: "$ANTHROPIC_MODEL_Z" process.env.ANTHROPIC_MODEL_Z
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
- function interpolateEnvVars(obj) {
13
- const result = {};
14
- console.error(`[ClaudeRunner] 🔧 Interpolating ${Object.keys(obj).length} env variables...`);
15
- for (const [key, value] of Object.entries(obj)) {
16
- if (typeof value === 'string' && value.startsWith('$')) {
17
- const varName = value.substring(1); // Enlever le $
18
- const envValue = process.env[varName];
19
- if (envValue !== undefined) {
20
- result[key] = envValue;
21
- console.error(`[ClaudeRunner] ${key} = $${varName} "${envValue}"`);
22
- }
23
- else {
24
- console.error(`[ClaudeRunner] ⚠️ Variable $${varName} non trouvée, garde la valeur littérale: ${value}`);
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
- else {
29
- result[key] = value;
30
- console.error(`[ClaudeRunner] 📋 ${key} = "${value}" (pas d'interpolation nécessaire)`);
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
- return result;
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 || 120000;
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
- const cwd = process.cwd();
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
- const settings = JSON.parse(fs.readFileSync(agentSettingsPath, 'utf8'));
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
- // Mémoriser l'env configuré pour l'injection
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] ⚠️ Erreur lors de la création du settings temporaire: ${e}`);
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
- // DÉCISION IMPORTANTE: On n'ajoute pas de guillemets manuels ici, spawn s'en occupe
165
- argsSpawn.push('--settings', finalSettingsPath);
226
+ argsSpawn.push('--settings', settingsPath);
166
227
  argsSpawn.push('--mcp-config', mcpPath);
167
- if (sessionId) {
168
- argsSpawn.push('--resume', sessionId);
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
- // --- MODEL & NICKNAME FLAGS ---
171
- const modelToUse = agentCustomEnv.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022';
172
- console.error(`[ClaudeRunner] 🛠️ Model override: ${modelToUse}`);
173
- argsSpawn.push('--model', modelToUse);
174
- if (agentCustomEnv.AGENT_NICKNAME) {
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
- else if (agentName) {
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
- return new Promise((resolve) => {
182
- const cleanupTmpFiles = () => {
183
- if (tmpMcpPathToDelete && fs.existsSync(tmpMcpPathToDelete)) {
184
- try {
185
- fs.unlinkSync(tmpMcpPathToDelete);
186
- }
187
- catch (_e) { /* intentionally empty */ }
188
- }
189
- if (tmpSettingsPathToDelete && fs.existsSync(tmpSettingsPathToDelete)) {
190
- try {
191
- fs.unlinkSync(tmpSettingsPathToDelete);
192
- }
193
- catch (_e) { /* intentionally empty */ }
194
- }
195
- };
196
- const isWin = process.platform === 'win32';
197
- let command = 'claude';
198
- let spawnArgs;
199
- // Prepend persona if defined
200
- let finalPrompt = prompt;
201
- if (agentName) {
202
- let agentPromptPath = resolveConfigPath(path.join(path.dirname(PATHS.SETTINGS), 'agents', `${agentName}.md`));
203
- if (!fs.existsSync(agentPromptPath)) {
204
- // Fallback: Check agents/ folder at the root level of settingsDir
205
- agentPromptPath = resolveConfigPath(path.join(path.dirname(path.dirname(PATHS.SETTINGS)), 'agents', `${agentName}.md`));
206
- }
207
- if (fs.existsSync(agentPromptPath)) {
208
- const systemPrompt = fs.readFileSync(agentPromptPath, 'utf8');
209
- finalPrompt = `${systemPrompt}\n\n[USER QUERY]:\n${prompt}`;
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
- if (isWin) {
213
- // Spawn claude.exe directly to bypass PowerShell/cmd argument
214
- // re-tokenization that mangles multi-line prompts containing patterns
215
- // like "0-100" into a stray "-100" flag.
216
- const claudeExe = 'C:\\Users\\Deamon\\AppData\\Roaming\\npm\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe';
217
- if (fs.existsSync(claudeExe)) {
218
- command = claudeExe;
219
- spawnArgs = [...argsSpawn, '-p', finalPrompt];
220
- }
221
- else {
222
- // Fallback: powershell wrapper (may mishandle multi-line prompts).
223
- const claudePath = 'C:\\Users\\Deamon\\AppData\\Roaming\\npm\\claude.ps1';
224
- if (fs.existsSync(claudePath)) {
225
- command = 'powershell.exe';
226
- spawnArgs = [
227
- '-NoProfile',
228
- '-ExecutionPolicy',
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
- else {
244
- spawnArgs = [...argsSpawn, '-p', finalPrompt];
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
- if (agentName) {
247
- const id = agentCustomEnv.AGENT_NICKNAME || agentName;
248
- console.error(`[ClaudeRunner] 🚀 Démarrage de l'agent ${id}...`);
249
- // Debug: Log the prompt size
250
- console.error(`[ClaudeRunner] 📏 Prompt Size: ${finalPrompt.length} chars`);
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 child = spawn(command, spawnArgs, {
253
- stdio: ['ignore', 'pipe', 'pipe'],
254
- cwd: process.cwd(),
255
- // shell: false explicitly (handled by command selection)
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
- ...process.env,
259
- ...agentCustomEnv,
260
- // Compatibilité Anthropic/Z.ai pour Claude Code standard
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
- // Register this process in the registry for agent_control visibility
268
- if (child.pid) {
269
- registerProcess(child.pid, {
270
- agentName: agentName || 'unknown',
271
- runner: 'claude',
272
- }).catch(() => { });
383
+ try {
384
+ fs.closeSync(promptFd);
273
385
  }
274
- let stdout = '';
275
- let stderr = '';
276
- if (child.stdout) {
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
- if (child.stderr) {
291
- child.stderr.on('data', (d) => {
292
- const chunk = d.toString();
293
- stderr += chunk;
294
- if (agentName) {
295
- const id = agentCustomEnv.AGENT_NICKNAME || agentName;
296
- process.stderr.write(`[ClaudeRunner:${id}:ERR] ${chunk}`);
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
- const timeout = setTimeout(() => {
301
- child.kill();
302
- if (child.pid) {
303
- updateProcessStatus(child.pid, 'failed', null).catch(() => { });
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
- const detectError = (text) => {
317
- const lower = text.toLowerCase();
318
- if (lower.includes('api key') || lower.includes('auth') || lower.includes('401')) {
319
- return '🔑 Erreur Auth/API Key (Clé invalide ou manquante)';
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
- if (lower.includes('quota') || lower.includes('exceeded') || lower.includes('429')) {
322
- return '📊 Quota dépassé (API Key épuisée)';
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
- if (lower.includes('rate limit')) {
325
- return '⏳ Rate limit atteint';
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 (lower.includes('model') && lower.includes('404')) {
328
- return '🤖 Modèle introuvable';
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
- if (code !== 0 && !stdout) {
333
- const specificError = detectError(stderr);
334
- return resolve({
335
- result: '',
336
- error: specificError || `EXIT_CODE_${code}`,
337
- rawOutput: stderr,
338
- });
339
- }
340
- try {
341
- let jsonStr = stdout.trim();
342
- const jsonStartIndex = jsonStr.indexOf('{');
343
- const jsonLastIndex = jsonStr.lastIndexOf('}');
344
- if (jsonStartIndex >= 0 && jsonLastIndex > jsonStartIndex) {
345
- jsonStr = jsonStr.substring(jsonStartIndex, jsonLastIndex + 1);
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
- const response = JSON.parse(jsonStr || '{}');
348
- if (agentName && response.session_id) {
349
- await saveSessionId(agentName, response.session_id);
350
- if (child.pid) {
351
- linkSessionToPid(response.session_id, child.pid).catch(() => { });
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
- resolve({
355
- result: response.result || JSON.stringify(response),
356
- sessionId: response.session_id,
357
- rawOutput: stdout,
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
- catch (_error) {
361
- const specificError = detectError(stdout) || detectError(stderr);
362
- resolve({
363
- result: '',
364
- error: specificError || 'JSON_PARSE_ERROR',
365
- rawOutput: stdout,
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
- child.on('error', (err) => {
370
- cleanupTmpFiles();
371
- resolve({ result: '', error: err.message, rawOutput: '' });
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
  }