overmind-mcp 3.6.1 → 3.7.1

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.
@@ -3,137 +3,203 @@
3
3
  * A2A Hub — Outil MCP pour la communication Agent-to-Agent
4
4
  * ═══════════════════════════════════════════════════════════════════════════
5
5
  *
6
- * Architecture distribuee — chaque worker est un serveur HTTP independant:
6
+ * Architecture distribuée — chaque bridge est un serveur HTTP indépendant:
7
7
  *
8
- * discord-master (:discord) routes !trade → TV Analyst :3002
9
- * routes !sniper Sniperbot :3001
8
+ * a2a_hub parle DIRECTEMENT aux bridges HTTP:
9
+ * POST :31XX/rpc JSON-RPC 2.0 agent.run (NEXUS bridges)
10
+ * POST :30XX/send → legacy message synchrone (discord_llm bridge)
11
+ * GET :XXXX/health → status live
10
12
  *
11
- * a2a_hub parle DIRECTEMENT aux workers HTTP:
12
- * POST :300X/send → message synchrone
13
- * GET :300X/health status live
13
+ * Découverte automatique:
14
+ * 1. Scan ports 3101-3120 (NEXUS bridges) + 3001-3020 (legacy workers)
15
+ * 2. Scan ~/.overmind/hermes/profiles/ (Linux) + ~/AppData/Local/hermes/profiles/ (Windows)
16
+ * 3. Cross-reference: profile ↔ bridge port
17
+ * 4. Lit Nexus/tmp/bridge_pids.json si présent
14
18
  *
15
- * Decouverte automatique:
16
- * 1. Scan ports 3001-3020 (workers connus)
17
- * 2. Scan ~/.overmind/hermes/profiles/ (agents Hermes)
18
- * 3. Cross-reference: profile worker port
19
- * 4. Lit ~/.overmind/bridge/workers.json si present
19
+ * PROD FIXES (2026-07-10):
20
+ * - fetch() async au lieu de execSync('curl') — ne bloque plus l'event loop
21
+ * - Vrai parallélisme pour fanout/broadcast (Promise.all)
22
+ * - Support dual: POST /rpc (JSON-RPC) + POST /send (legacy)
23
+ * - Port scan: 3101-3120 (NEXUS) + 3001-3020 (legacy)
24
+ * - Profils Hermes: ~/.overmind/hermes/ + ~/AppData/Local/hermes/
25
+ * - 127.0.0.1 au lieu de localhost (IPv4/IPv6 fix)
20
26
  */
21
27
  import { z } from 'zod';
22
- import { execSync } from 'child_process';
23
28
  import fs from 'fs';
24
29
  import path from 'path';
25
30
  import os from 'os';
26
- // ─── HTTP Helper (curl synchrone) ──────────────────────────────────────────
27
- function httpGet(url, timeoutMs = 5000) {
31
+ import { randomUUID } from 'crypto';
32
+ // ─── HTTP Helpers (async fetch non-blocking) ─────────────────────────────
33
+ async function httpGet(url, timeoutMs = 5000) {
28
34
  try {
29
- const result = execSync(`curl -s -m ${Math.floor(timeoutMs / 1000)} "${url}" 2>/dev/null`, {
30
- encoding: 'utf8',
31
- timeout: timeoutMs + 1000,
32
- });
33
- return JSON.parse(result);
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
37
+ const response = await fetch(url, { method: 'GET', signal: controller.signal });
38
+ clearTimeout(timer);
39
+ if (!response.ok)
40
+ return null;
41
+ const text = await response.text();
42
+ return JSON.parse(text);
34
43
  }
35
44
  catch {
36
45
  return null;
37
46
  }
38
47
  }
39
- function httpPost(url, body, timeoutMs = 300000) {
40
- const payload = JSON.stringify(body);
48
+ async function httpPost(url, body, timeoutMs = 300000) {
41
49
  try {
42
- const escapedPayload = payload.replace(/'/g, "'\\''");
43
- const result = execSync(`curl -s -m ${Math.floor(timeoutMs / 1000)} -X POST "${url}" -H "Content-Type: application/json" -d '${escapedPayload}' 2>/dev/null`, { encoding: 'utf8', timeout: timeoutMs + 1000, maxBuffer: 50 * 1024 * 1024 });
44
- return JSON.parse(result);
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
52
+ const response = await fetch(url, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify(body),
56
+ signal: controller.signal,
57
+ });
58
+ clearTimeout(timer);
59
+ if (!response.ok)
60
+ return null;
61
+ const text = await response.text();
62
+ return JSON.parse(text);
45
63
  }
46
64
  catch {
47
65
  return null;
48
66
  }
49
67
  }
50
- // ─── Worker Discovery ──────────────────────────────────────────────────────
51
- function discoverWorkers() {
52
- const workers = [];
53
- const workersJsonPath = path.join(os.homedir(), '.overmind', 'bridge', 'workers.json');
54
- // 1. Try reading workers registry
55
- let knownPorts = [];
68
+ // ─── Bridge Discovery ──────────────────────────────────────────────────────
69
+ /** Get candidate Hermes profile directories (Windows + Linux) */
70
+ function getProfileDirs() {
71
+ const dirs = [];
72
+ const home = os.homedir();
73
+ // Linux/Ubuntu: ~/.overmind/hermes/profiles/
74
+ const overmindHermes = path.join(home, '.overmind', 'hermes', 'profiles');
75
+ if (fs.existsSync(overmindHermes))
76
+ dirs.push(overmindHermes);
77
+ // Windows: ~/AppData/Local/hermes/profiles/
78
+ const winHermes = path.join(home, 'AppData', 'Local', 'hermes', 'profiles');
79
+ if (fs.existsSync(winHermes))
80
+ dirs.push(winHermes);
81
+ // Linux native: ~/.hermes/profiles/
82
+ const nativeHermes = path.join(home, '.hermes', 'profiles');
83
+ if (fs.existsSync(nativeHermes))
84
+ dirs.push(nativeHermes);
85
+ return dirs;
86
+ }
87
+ /** Get known bridge ports from pid files */
88
+ function getKnownPorts() {
89
+ const known = [];
90
+ const home = os.homedir();
91
+ // 1. NEXUS pid file: Nexus/tmp/bridge_pids.json
92
+ const nexusPidPath = path.join(home, 'Nexus', 'tmp', 'bridge_pids.json');
93
+ if (fs.existsSync(nexusPidPath)) {
94
+ try {
95
+ const data = JSON.parse(fs.readFileSync(nexusPidPath, 'utf8'));
96
+ if (Array.isArray(data)) {
97
+ for (const entry of data) {
98
+ if (entry.port && entry.name) {
99
+ known.push({ port: entry.port, agentName: entry.name });
100
+ }
101
+ }
102
+ }
103
+ else if (data.workers && Array.isArray(data.workers)) {
104
+ for (const w of data.workers) {
105
+ if (w.port)
106
+ known.push({ port: w.port, agentName: w.agentName || '' });
107
+ }
108
+ }
109
+ }
110
+ catch { /* ignore */ }
111
+ }
112
+ // 2. Legacy workers.json: ~/.overmind/bridge/workers.json
113
+ const workersJsonPath = path.join(home, '.overmind', 'bridge', 'workers.json');
56
114
  if (fs.existsSync(workersJsonPath)) {
57
115
  try {
58
116
  const data = JSON.parse(fs.readFileSync(workersJsonPath, 'utf8'));
59
117
  if (Array.isArray(data.workers)) {
60
- knownPorts = data.workers.map((w) => ({
61
- port: w.port,
62
- agentName: w.agentName || 'unknown',
63
- }));
118
+ for (const w of data.workers) {
119
+ if (w.port)
120
+ known.push({ port: w.port, agentName: w.agentName || '' });
121
+ }
64
122
  }
65
123
  }
66
- catch {
67
- // ignore
68
- }
124
+ catch { /* ignore */ }
69
125
  }
70
- // 2. Fallback: scan ports 3001-3020
71
- if (knownPorts.length === 0) {
72
- knownPorts = [];
73
- for (let port = 3001; port <= 3020; port++) {
74
- knownPorts.push({ port, agentName: '' });
75
- }
126
+ return known;
127
+ }
128
+ /** Probe a single port for bridge health */
129
+ async function probeBridge(port, agentName) {
130
+ const url = `http://127.0.0.1:${port}`;
131
+ const health = await httpGet(`${url}/health`, 3000);
132
+ if (health) {
133
+ const detectedAgent = agentName ||
134
+ health.agent ||
135
+ health.agentName ||
136
+ health.service ||
137
+ 'unknown';
138
+ // NEXUS bridges expose rpcMethods[] in health — they support POST /rpc
139
+ const hasRpc = Array.isArray(health.rpcMethods) || 'jsonrpc' in health;
140
+ return { port, url, online: true, agentName: detectedAgent, health, hasRpc };
76
141
  }
77
- // 3. Probe each port
142
+ return { port, url, online: false, agentName: agentName || '', hasRpc: false };
143
+ }
144
+ /** Discover all bridges (scan ports + pid files) */
145
+ async function discoverBridges() {
146
+ const knownPorts = getKnownPorts();
147
+ // Build port list: known ports + scan ranges
148
+ const portSet = new Set();
149
+ const portAgentMap = new Map();
150
+ // Add known ports from pid files
78
151
  for (const { port, agentName } of knownPorts) {
79
- const url = `http://localhost:${port}`;
80
- const health = httpGet(`${url}/health`, 3000);
81
- if (health) {
82
- // Worker is online — extract agent name from health
83
- const detectedAgent = agentName ||
84
- health.agent ||
85
- health.agentName ||
86
- health.service ||
87
- 'unknown';
88
- workers.push({
89
- port,
90
- url,
91
- online: true,
92
- agentName: detectedAgent,
93
- health,
94
- });
95
- }
96
- else {
97
- // Check if port is listening at all
98
- workers.push({
99
- port,
100
- url,
101
- online: false,
102
- agentName: agentName || '',
103
- });
104
- }
152
+ portSet.add(port);
153
+ if (agentName)
154
+ portAgentMap.set(port, agentName);
105
155
  }
106
- return workers;
156
+ // NEXUS range: 3101-3120
157
+ for (let p = 3101; p <= 3120; p++)
158
+ portSet.add(p);
159
+ // Legacy range: 3001-3020
160
+ for (let p = 3001; p <= 3020; p++)
161
+ portSet.add(p);
162
+ // Probe all ports in parallel (true parallel — async fetch)
163
+ const ports = Array.from(portSet).sort((a, b) => a - b);
164
+ const results = await Promise.all(ports.map((port) => probeBridge(port, portAgentMap.get(port) || '')));
165
+ return results;
107
166
  }
108
167
  // ─── Agent Discovery ───────────────────────────────────────────────────────
109
- function discoverAgents() {
110
- const home = os.homedir();
111
- const profilesDir = path.join(home, '.overmind', 'hermes', 'profiles');
168
+ async function discoverAgents() {
112
169
  const selfAgent = process.env.OVERMIND_AGENT_NAME || null;
113
170
  const agents = [];
114
- const workers = discoverWorkers();
115
- // Build worker lookup: agentName → worker
116
- const workerByAgent = new Map();
117
- for (const w of workers) {
118
- if (w.online && w.agentName && w.agentName !== 'unknown') {
119
- workerByAgent.set(w.agentName, w);
171
+ const bridges = await discoverBridges();
172
+ // Build bridge lookup: agentName → bridge
173
+ const bridgeByAgent = new Map();
174
+ for (const b of bridges) {
175
+ if (b.online && b.agentName && b.agentName !== 'unknown') {
176
+ bridgeByAgent.set(b.agentName, b);
120
177
  }
121
178
  }
122
- // Scan Hermes profiles
123
- if (fs.existsSync(profilesDir)) {
124
- const entries = fs.readdirSync(profilesDir, { withFileTypes: true });
179
+ // Scan all profile directories
180
+ for (const profilesDir of getProfileDirs()) {
181
+ let entries;
182
+ try {
183
+ entries = fs.readdirSync(profilesDir, { withFileTypes: true });
184
+ }
185
+ catch {
186
+ continue;
187
+ }
125
188
  for (const entry of entries) {
126
189
  if (!entry.isDirectory())
127
190
  continue;
128
191
  const profilePath = path.join(profilesDir, entry.name);
192
+ // Avoid duplicates (same profile name in different dirs)
193
+ if (agents.some((a) => a.name === entry.name))
194
+ continue;
129
195
  // Read config.yaml
130
196
  const configPath = path.join(profilePath, 'config.yaml');
131
197
  let model = 'unknown';
132
198
  let provider = 'unknown';
133
199
  if (fs.existsSync(configPath)) {
134
- const config = fs.readFileSync(configPath, 'utf8');
135
- const modelMatch = config.match(/^\s*model:\s*(?!provider)(.+)/m);
136
- const providerMatch = config.match(/^\s*provider:\s*(.+)/m);
200
+ const configContent = fs.readFileSync(configPath, 'utf8');
201
+ const modelMatch = configContent.match(/^\s*model:\s*(?!provider)(.+)/m);
202
+ const providerMatch = configContent.match(/^\s*provider:\s*(.+)/m);
137
203
  if (modelMatch)
138
204
  model = modelMatch[1].trim().replace(/['"]/g, '');
139
205
  if (providerMatch)
@@ -156,43 +222,38 @@ function discoverAgents() {
156
222
  const walk = (dir) => {
157
223
  let count = 0;
158
224
  for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
159
- if (f.isDirectory()) {
225
+ if (f.isDirectory())
160
226
  count += walk(path.join(dir, f.name));
161
- }
162
- else if (f.name === 'SKILL.md') {
227
+ else if (f.name === 'SKILL.md')
163
228
  count++;
164
- }
165
229
  }
166
230
  return count;
167
231
  };
168
232
  skillsCount = walk(skillsDir);
169
233
  }
170
- catch {
171
- // ignore
172
- }
234
+ catch { /* ignore */ }
173
235
  }
174
- // Check memory
175
- const hasMemory = fs.existsSync(path.join(profilePath, 'memories', 'MEMORY.md'));
236
+ // Check memory (state.db or MEMORY.md)
237
+ const hasMemory = fs.existsSync(path.join(profilePath, 'memories', 'state.db')) ||
238
+ fs.existsSync(path.join(profilePath, 'memories', 'MEMORY.md'));
176
239
  // Last activity
177
240
  let lastActivity = null;
178
- const stateDb = path.join(profilePath, 'state.db');
241
+ const stateDb = path.join(profilePath, 'memories', 'state.db');
179
242
  if (fs.existsSync(stateDb)) {
180
243
  try {
181
244
  lastActivity = fs.statSync(stateDb).mtime.toISOString();
182
245
  }
183
- catch {
184
- // ignore
185
- }
246
+ catch { /* ignore */ }
186
247
  }
187
- // Cross-reference with workers
188
- const worker = workerByAgent.get(entry.name);
248
+ // Cross-reference with bridges
249
+ const bridge = bridgeByAgent.get(entry.name);
189
250
  agents.push({
190
251
  name: entry.name,
191
252
  model,
192
253
  provider,
193
- status: worker ? 'online' : 'offline',
194
- workerPort: worker?.port ?? null,
195
- workerUrl: worker?.url ?? null,
254
+ status: bridge ? 'online' : 'offline',
255
+ bridgePort: bridge?.port ?? null,
256
+ bridgeUrl: bridge?.url ?? null,
196
257
  description,
197
258
  skillsCount,
198
259
  hasMemory,
@@ -202,35 +263,64 @@ function discoverAgents() {
202
263
  }
203
264
  return {
204
265
  totalAgents: agents.length,
205
- onlineWorkers: workers.filter((w) => w.online).length,
266
+ onlineBridges: bridges.filter((b) => b.online).length,
206
267
  agents,
207
- workers,
268
+ bridges,
208
269
  selfAgent,
209
270
  };
210
271
  }
211
- // ─── Send message to a worker ──────────────────────────────────────────────
212
- function sendToWorker(port, message, opts = {}) {
213
- const url = `http://localhost:${port}/send`;
214
- const body = {
215
- message,
216
- userId: `a2a_${process.env.OVERMIND_AGENT_NAME || 'system'}`,
217
- username: process.env.OVERMIND_AGENT_NAME || 'A2A Hub',
272
+ // ─── Send message to a bridge (supports /rpc + /send) ──────────────────────
273
+ async function sendToBridge(bridge, message, opts = {}) {
274
+ const selfAgent = process.env.OVERMIND_AGENT_NAME || 'A2A Hub';
275
+ const enrichedMessage = `[A2A — Message from ${selfAgent}]\n${message}`;
276
+ const timeout = opts.timeoutMs ?? 300000;
277
+ // Strategy 1: NEXUS bridge with POST /rpc (JSON-RPC 2.0)
278
+ if (bridge.hasRpc) {
279
+ const rpcBody = {
280
+ jsonrpc: '2.0',
281
+ id: randomUUID(),
282
+ method: 'agent.run',
283
+ params: {
284
+ agentName: bridge.agentName,
285
+ runner: 'hermes',
286
+ prompt: enrichedMessage,
287
+ ...(opts.model ? { model: opts.model } : {}),
288
+ },
289
+ };
290
+ const response = await httpPost(`${bridge.url}/rpc`, rpcBody, timeout);
291
+ if (response === null) {
292
+ return { success: false, text: `Bridge :${bridge.port} injoignable`, raw: null };
293
+ }
294
+ if (response.error) {
295
+ return { success: false, text: `Erreur :${bridge.port}: ${response.error.message || JSON.stringify(response.error)}`, raw: response };
296
+ }
297
+ // Extract text from JSON-RPC result
298
+ const result = response.result;
299
+ if (result) {
300
+ const content = result.content;
301
+ const text = content
302
+ ? content.map((c) => c.text || '').join('\n')
303
+ : result.output || result.text || JSON.stringify(result);
304
+ return { success: true, text, raw: response };
305
+ }
306
+ return { success: true, text: JSON.stringify(response), raw: response };
307
+ }
308
+ // Strategy 2: Legacy bridge with POST /send
309
+ const sendBody = {
310
+ message: enrichedMessage,
311
+ userId: `a2a_${selfAgent}`,
312
+ username: selfAgent,
218
313
  channelId: 'a2a',
219
314
  };
220
315
  if (opts.model)
221
- body.model = opts.model;
222
- const response = httpPost(url, body, opts.timeoutMs ?? 300000);
316
+ sendBody.model = opts.model;
317
+ const response = await httpPost(`${bridge.url}/send`, sendBody, timeout);
223
318
  if (response === null) {
224
- return { success: false, text: `Worker :${port} injoignable`, raw: null };
319
+ return { success: false, text: `Bridge :${bridge.port} injoignable`, raw: null };
225
320
  }
226
321
  if (response.error) {
227
- return {
228
- success: false,
229
- text: `Erreur :${port}: ${response.error}`,
230
- raw: response,
231
- };
322
+ return { success: false, text: `Erreur :${bridge.port}: ${response.error}`, raw: response };
232
323
  }
233
- // Workers may return { result, content, response, output }
234
324
  const text = response.result ||
235
325
  response.response ||
236
326
  response.output ||
@@ -244,11 +334,11 @@ function sendToWorker(port, message, opts = {}) {
244
334
  export const a2aHubSchema = z.object({
245
335
  action: z
246
336
  .enum(['discover', 'status', 'send', 'delegate', 'pipeline', 'fanout', 'query', 'broadcast'])
247
- .describe("Action: discover=liste tous les agents+workers, status=état d'un worker, send=message synchrone, delegate=async, pipeline=chaîne A→B→C, fanout=1→N+merge, query=multi-agents, broadcast=global"),
337
+ .describe("Action: discover=liste tous les agents+bridges, status=état d'un bridge, send=message synchrone, delegate=async, pipeline=chaîne A→B→C, fanout=1→N+merge, query=multi-agents, broadcast=global"),
248
338
  target: z
249
339
  .string()
250
340
  .optional()
251
- .describe("Nom de l'agent cible (ex: 'sniperbot_analyst') OU port du worker (ex: '3001')"),
341
+ .describe("Nom de l'agent cible (ex: 'nexus_master') OU port du bridge (ex: '3101')"),
252
342
  message: z.string().optional().describe('Le message/prompt à envoyer aux agents'),
253
343
  targets: z
254
344
  .array(z.string())
@@ -258,7 +348,7 @@ export const a2aHubSchema = z.object({
258
348
  .array(z.object({
259
349
  agentName: z
260
350
  .string()
261
- .describe("Nom de l'agent ou port (ex: 'sniperbot_analyst' ou '3001')"),
351
+ .describe("Nom de l'agent ou port (ex: 'nexus_master' ou '3101')"),
262
352
  promptPrefix: z.string().optional(),
263
353
  }))
264
354
  .optional()
@@ -292,26 +382,27 @@ export const a2aHubSchema = z.object({
292
382
  .describe('Timeout par agent en ms (default: 300000 = 5min)'),
293
383
  model: z.string().optional().describe('Modèle LLM override'),
294
384
  });
295
- // ─── Resolve agent name to worker port ─────────────────────────────────────
385
+ // ─── Resolve agent name to bridge ──────────────────────────────────────────
296
386
  function resolveTarget(target, discovery) {
297
387
  // If target is a port number
298
388
  const portMatch = target.match(/^(\d{4,5})$/);
299
389
  if (portMatch) {
300
390
  const port = parseInt(portMatch[1], 10);
301
- const worker = discovery.workers.find((w) => w.port === port);
302
- return { port, agentName: worker?.agentName || target };
391
+ const bridge = discovery.bridges.find((b) => b.port === port);
392
+ return { bridge: bridge ?? null, agentName: bridge?.agentName || target };
303
393
  }
304
- // If target is an agent name, find its worker
394
+ // If target is an agent name, find its bridge
305
395
  const agent = discovery.agents.find((a) => a.name === target);
306
- if (agent?.workerPort) {
307
- return { port: agent.workerPort, agentName: target };
308
- }
309
- // Try online workers
310
- const worker = discovery.workers.find((w) => w.online && w.agentName === target);
311
- if (worker) {
312
- return { port: worker.port, agentName: target };
396
+ if (agent?.bridgePort) {
397
+ const bridge = discovery.bridges.find((b) => b.port === agent.bridgePort);
398
+ if (bridge)
399
+ return { bridge, agentName: target };
313
400
  }
314
- return { port: null, agentName: target };
401
+ // Try online bridges by agentName
402
+ const bridge = discovery.bridges.find((b) => b.online && b.agentName === target);
403
+ if (bridge)
404
+ return { bridge, agentName: target };
405
+ return { bridge: null, agentName: target };
315
406
  }
316
407
  // ─── Execute ───────────────────────────────────────────────────────────────
317
408
  export async function a2aHub(args) {
@@ -323,47 +414,47 @@ export async function a2aHub(args) {
323
414
  // DISCOVER
324
415
  // ═══════════════════════════════════════════════════════════════════════
325
416
  case 'discover': {
326
- const d = discoverAgents();
417
+ const d = await discoverAgents();
327
418
  const lines = [
328
419
  `🌐 **A2A Hub — Découverte du système multi-agents**`,
329
420
  ``,
330
421
  `**Self:** ${d.selfAgent || '(inconnu)'}`,
331
- `**Workers online:** ${d.onlineWorkers}`,
422
+ `**Bridges online:** ${d.onlineBridges}`,
332
423
  `**Total agents:** ${d.totalAgents}`,
333
424
  ``,
334
- `### Workers HTTP`,
425
+ `### Bridges HTTP`,
335
426
  ``,
336
427
  ];
337
- const onlineWorkers = d.workers.filter((w) => w.online);
338
- if (onlineWorkers.length === 0) {
339
- lines.push('Aucun worker HTTP en ligne.');
428
+ const onlineBridges = d.bridges.filter((b) => b.online);
429
+ if (onlineBridges.length === 0) {
430
+ lines.push('Aucun bridge HTTP en ligne.');
340
431
  }
341
432
  else {
342
- lines.push('| Port | Agent | Status | URL |');
343
- lines.push('|------|-------|--------|-----|');
344
- for (const w of onlineWorkers) {
345
- lines.push(`| :${w.port} | ${w.agentName} | 🟢 online | ${w.url} |`);
433
+ lines.push('| Port | Agent | Type | URL |');
434
+ lines.push('|------|-------|------|-----|');
435
+ for (const b of onlineBridges) {
436
+ const type = b.hasRpc ? 'RPC' : 'Legacy';
437
+ lines.push(`| :${b.port} | ${b.agentName} | ${type} | ${b.url} |`);
346
438
  }
347
439
  }
348
440
  lines.push('');
349
- lines.push('### Agents Hermes');
441
+ lines.push('### Agents');
350
442
  lines.push('');
351
443
  if (d.agents.length === 0) {
352
- lines.push('Aucun agent trouvé dans ~/.overmind/hermes/profiles/');
444
+ lines.push('Aucun agent trouvé.');
353
445
  }
354
446
  else {
355
- lines.push('| Agent | Status | Worker | Model | Provider | Skills | Description |');
447
+ lines.push('| Agent | Status | Bridge | Model | Provider | Skills | Description |');
356
448
  lines.push('|-------|--------|--------|-------|----------|--------|-------------|');
357
449
  for (const a of d.agents) {
358
450
  const statusIcon = a.status === 'online' ? '🟢' : '🔴';
359
- const workerInfo = a.workerPort ? `:${a.workerPort}` : '—';
451
+ const bridgeInfo = a.bridgePort ? `:${a.bridgePort}` : '—';
360
452
  const nameDisplay = a.name === selfAgent ? `**${a.name} (self)**` : a.name;
361
- lines.push(`| ${nameDisplay} | ${statusIcon} ${a.status} | ${workerInfo} | ${a.model} | ${a.provider} | ${a.skillsCount} | ${a.description.slice(0, 40)} |`);
453
+ lines.push(`| ${nameDisplay} | ${statusIcon} ${a.status} | ${bridgeInfo} | ${a.model} | ${a.provider} | ${a.skillsCount} | ${a.description.slice(0, 40)} |`);
362
454
  }
363
455
  }
364
456
  lines.push('');
365
457
  lines.push('**Actions disponibles:** send, delegate, pipeline, fanout, query, broadcast');
366
- lines.push('**Exemple:** a2a_hub(action: "send", target: "sniperbot_analyst", message: "Analyse BTC")');
367
458
  return { content: [{ type: 'text', text: lines.join('\n') }] };
368
459
  }
369
460
  // ═══════════════════════════════════════════════════════════════════════
@@ -376,25 +467,16 @@ export async function a2aHub(args) {
376
467
  isError: true,
377
468
  };
378
469
  }
379
- const d = discoverAgents();
380
- const { port, agentName } = resolveTarget(args.target, d);
381
- if (!port) {
470
+ const d = await discoverAgents();
471
+ const { bridge, agentName } = resolveTarget(args.target, d);
472
+ if (!bridge || !bridge.online) {
382
473
  return {
383
- content: [
384
- { type: 'text', text: `❌ Worker pour "${args.target}" introuvable` },
385
- ],
474
+ content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable` }],
386
475
  isError: true,
387
476
  };
388
477
  }
389
- const health = httpGet(`http://localhost:${port}/health`);
390
- if (!health) {
391
- return {
392
- content: [{ type: 'text', text: `❌ Worker :${port} injoignable` }],
393
- isError: true,
394
- };
395
- }
396
- const lines = [`📊 **Status: ${agentName} (:${port})**`, ``];
397
- for (const [key, value] of Object.entries(health)) {
478
+ const lines = [`📊 **Status: ${agentName} (:${bridge.port})**`, ``];
479
+ for (const [key, value] of Object.entries(bridge.health ?? {})) {
398
480
  lines.push(`**${key}:** ${typeof value === 'object' ? JSON.stringify(value).slice(0, 100) : value}`);
399
481
  }
400
482
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -409,21 +491,15 @@ export async function a2aHub(args) {
409
491
  isError: true,
410
492
  };
411
493
  }
412
- const d = discoverAgents();
413
- const { port, agentName } = resolveTarget(args.target, d);
414
- if (!port) {
494
+ const d = await discoverAgents();
495
+ const { bridge, agentName } = resolveTarget(args.target, d);
496
+ if (!bridge || !bridge.online) {
415
497
  return {
416
- content: [
417
- {
418
- type: 'text',
419
- text: `❌ Worker pour "${args.target}" introuvable. Utilisez action=discover pour lister les agents.`,
420
- },
421
- ],
498
+ content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable. Utilisez action=discover.` }],
422
499
  isError: true,
423
500
  };
424
501
  }
425
- const enrichedMessage = `[A2A Message from ${selfAgent}]\n${args.message}`;
426
- const result = sendToWorker(port, enrichedMessage, {
502
+ const result = await sendToBridge(bridge, args.message, {
427
503
  model: args.model,
428
504
  timeoutMs: timeout,
429
505
  });
@@ -434,12 +510,7 @@ export async function a2aHub(args) {
434
510
  };
435
511
  }
436
512
  return {
437
- content: [
438
- {
439
- type: 'text',
440
- text: `📤 **Message envoyé à ${agentName} (:${port})**\n\n${result.text}`,
441
- },
442
- ],
513
+ content: [{ type: 'text', text: `📤 **Message envoyé à ${agentName} (:${bridge.port})**\n\n${result.text}` }],
443
514
  };
444
515
  }
445
516
  // ═══════════════════════════════════════════════════════════════════════
@@ -452,41 +523,22 @@ export async function a2aHub(args) {
452
523
  isError: true,
453
524
  };
454
525
  }
455
- const d = discoverAgents();
456
- const { port, agentName } = resolveTarget(args.target, d);
457
- if (!port) {
526
+ const d = await discoverAgents();
527
+ const { bridge, agentName } = resolveTarget(args.target, d);
528
+ if (!bridge || !bridge.online) {
458
529
  return {
459
- content: [
460
- { type: 'text', text: `❌ Worker pour "${args.target}" introuvable` },
461
- ],
530
+ content: [{ type: 'text', text: `❌ Bridge pour "${args.target}" introuvable` }],
462
531
  isError: true,
463
532
  };
464
533
  }
465
- // Fire and forget — don't wait for response
466
- const enrichedMessage = `[A2A Delegate from ${selfAgent}]\n${args.message}`;
467
- const url = `http://localhost:${port}/send`;
468
- const body = {
469
- message: enrichedMessage,
470
- userId: `a2a_${selfAgent}`,
471
- username: selfAgent,
472
- channelId: 'a2a_delegate',
473
- ...(args.model ? { model: args.model } : {}),
474
- };
475
- // Spawn curl in background (fire-and-forget)
476
- try {
477
- const payload = JSON.stringify(body).replace(/'/g, "'\\''");
478
- execSync(`nohup curl -s -m ${Math.floor(timeout / 1000)} -X POST "${url}" -H "Content-Type: application/json" -d '${payload}' > /dev/null 2>&1 &`, { timeout: 2000 });
479
- }
480
- catch {
481
- // ignore — fire and forget
482
- }
534
+ // Fire and forget — don't await
535
+ void sendToBridge(bridge, args.message, { model: args.model, timeoutMs: timeout })
536
+ .catch(() => { });
483
537
  return {
484
- content: [
485
- {
538
+ content: [{
486
539
  type: 'text',
487
- text: `🤝 **Tâche déléguée à ${agentName} (:${port})**\n\nLe worker traite la requête en arrière-plan. Le résultat sera disponible dans sa session.`,
488
- },
489
- ],
540
+ text: `🤝 **Tâche déléguée à ${agentName} (:${bridge.port})**\n\nLe bridge traite la requête en arrière-plan.`,
541
+ }],
490
542
  };
491
543
  }
492
544
  // ═══════════════════════════════════════════════════════════════════════
@@ -495,30 +547,25 @@ export async function a2aHub(args) {
495
547
  case 'pipeline': {
496
548
  if (!args.message || !args.steps || args.steps.length === 0) {
497
549
  return {
498
- content: [
499
- {
500
- type: 'text',
501
- text: '❌ `message` (prompt initial) et `steps` requis pour pipeline',
502
- },
503
- ],
550
+ content: [{ type: 'text', text: '❌ `message` (prompt initial) et `steps` requis' }],
504
551
  isError: true,
505
552
  };
506
553
  }
507
- const d = discoverAgents();
554
+ const d = await discoverAgents();
508
555
  const outputs = [];
509
556
  let currentPrompt = args.message;
510
557
  for (let i = 0; i < args.steps.length; i++) {
511
558
  const step = args.steps[i];
512
- const { port, agentName } = resolveTarget(step.agentName, d);
513
- if (!port) {
514
- outputs.push({ agent: step.agentName, output: 'Worker introuvable', success: false });
559
+ const { bridge, agentName } = resolveTarget(step.agentName, d);
560
+ if (!bridge || !bridge.online) {
561
+ outputs.push({ agent: step.agentName, output: 'Bridge introuvable', success: false });
515
562
  break;
516
563
  }
517
564
  const stepPrompt = (step.promptPrefix ? step.promptPrefix + '\n\n' : '') +
518
565
  `[Pipeline Step ${i + 1}/${args.steps.length}]\n${currentPrompt}`;
519
- const result = sendToWorker(port, stepPrompt, { timeoutMs: timeout });
566
+ const result = await sendToBridge(bridge, stepPrompt, { timeoutMs: timeout });
520
567
  outputs.push({
521
- agent: `${agentName} (:${port})`,
568
+ agent: `${agentName} (:${bridge.port})`,
522
569
  output: result.text,
523
570
  success: result.success,
524
571
  });
@@ -528,10 +575,7 @@ export async function a2aHub(args) {
528
575
  ? outputs.map((o) => `[${o.agent}]: ${o.output}`).join('\n\n---\n\n')
529
576
  : result.text;
530
577
  }
531
- const lines = [
532
- `🔗 **Pipeline terminé** (${outputs.length}/${args.steps.length} steps)`,
533
- ``,
534
- ];
578
+ const lines = [`🔗 **Pipeline** (${outputs.length}/${args.steps.length} steps)`, ``];
535
579
  for (const o of outputs) {
536
580
  lines.push(`**${o.success ? '✅' : '❌'} ${o.agent}:**`);
537
581
  lines.push(o.output.slice(0, 800) + (o.output.length > 800 ? '...' : ''));
@@ -540,7 +584,7 @@ export async function a2aHub(args) {
540
584
  return { content: [{ type: 'text', text: lines.join('\n') }] };
541
585
  }
542
586
  // ═══════════════════════════════════════════════════════════════════════
543
- // FANOUT
587
+ // FANOUT (true parallel — Promise.all)
544
588
  // ═══════════════════════════════════════════════════════════════════════
545
589
  case 'fanout': {
546
590
  if (!args.message || !args.targets || args.targets.length === 0) {
@@ -549,21 +593,18 @@ export async function a2aHub(args) {
549
593
  isError: true,
550
594
  };
551
595
  }
552
- const d = discoverAgents();
553
- const enrichedMessage = `[A2AFanout from ${selfAgent}]\n${args.message}`;
554
- // Run all in parallel
555
- const results = args.targets
556
- .map((target) => {
557
- const { port } = resolveTarget(target, d);
558
- if (!port) {
559
- return { agent: target, success: false, text: 'Worker introuvable' };
596
+ const d = await discoverAgents();
597
+ // Run ALL targets in parallel (true parallel async fetch)
598
+ const results = await Promise.all(args.targets.map(async (target) => {
599
+ const { bridge, agentName } = resolveTarget(target, d);
600
+ if (!bridge || !bridge.online) {
601
+ return { agent: target, success: false, text: 'Bridge introuvable' };
560
602
  }
561
- return sendToWorker(port, enrichedMessage, { model: args.model, timeoutMs: timeout });
562
- })
563
- .map((r, i) => ({
564
- agent: args.targets[i],
565
- success: r.success,
566
- text: r.text,
603
+ const r = await sendToBridge(bridge, args.message, {
604
+ model: args.model,
605
+ timeoutMs: timeout,
606
+ });
607
+ return { agent: `${agentName} (:${bridge.port})`, success: r.success, text: r.text };
567
608
  }));
568
609
  // Merge
569
610
  let merged;
@@ -601,7 +642,7 @@ export async function a2aHub(args) {
601
642
  return { content: [{ type: 'text', text: lines.join('\n') }] };
602
643
  }
603
644
  // ═══════════════════════════════════════════════════════════════════════
604
- // QUERY
645
+ // QUERY (parallel, short timeout)
605
646
  // ═══════════════════════════════════════════════════════════════════════
606
647
  case 'query': {
607
648
  if (!args.message || !args.targets || args.targets.length === 0) {
@@ -610,27 +651,21 @@ export async function a2aHub(args) {
610
651
  isError: true,
611
652
  };
612
653
  }
613
- const d = discoverAgents();
614
- const enrichedMessage = `[A2A — Query from ${selfAgent}]\n${args.message}`;
654
+ const d = await discoverAgents();
615
655
  const queryTimeout = Math.min(timeout, 60000);
616
- const results = args.targets
617
- .map((target) => {
618
- const { port } = resolveTarget(target, d);
619
- if (!port) {
620
- return { agent: target, success: false, text: 'Worker introuvable' };
656
+ const results = await Promise.all(args.targets.map(async (target) => {
657
+ const { bridge, agentName } = resolveTarget(target, d);
658
+ if (!bridge || !bridge.online) {
659
+ return { agent: target, success: false, text: 'Bridge introuvable' };
621
660
  }
622
- return sendToWorker(port, enrichedMessage, {
661
+ const r = await sendToBridge(bridge, args.message, {
623
662
  model: args.model,
624
663
  timeoutMs: queryTimeout,
625
664
  });
626
- })
627
- .map((r, i) => ({
628
- agent: args.targets[i],
629
- success: r.success,
630
- text: r.text,
665
+ return { agent: `${agentName} (:${bridge.port})`, success: r.success, text: r.text };
631
666
  }));
632
667
  const lines = [
633
- `❓ **Query multi-agents** (${results.filter((r) => r.success).length}/${results.length} réponses)`,
668
+ `❓ **Query** (${results.filter((r) => r.success).length}/${results.length} réponses)`,
634
669
  ``,
635
670
  ];
636
671
  for (const r of results) {
@@ -650,60 +685,50 @@ export async function a2aHub(args) {
650
685
  isError: true,
651
686
  };
652
687
  }
653
- const d = discoverAgents();
654
- // Determine targets
655
- let targetWorkers;
688
+ const d = await discoverAgents();
689
+ let targetBridges;
656
690
  if (args.targets && args.targets.length > 0) {
657
- // Resolve each target to a worker
658
- targetWorkers = args.targets
659
- .map((t) => {
660
- const { port } = resolveTarget(t, d);
661
- return d.workers.find((w) => w.port === port);
662
- })
663
- .filter((w) => w !== undefined);
691
+ targetBridges = args.targets
692
+ .map((t) => resolveTarget(t, d).bridge)
693
+ .filter((b) => b !== null && b.online);
664
694
  }
665
695
  else {
666
- // All online workers
667
- targetWorkers = d.workers.filter((w) => w.online);
696
+ targetBridges = d.bridges.filter((b) => b.online);
668
697
  }
669
- if (targetWorkers.length === 0) {
698
+ if (targetBridges.length === 0) {
670
699
  return {
671
- content: [{ type: 'text', text: '❌ Aucun worker online pour broadcast' }],
700
+ content: [{ type: 'text', text: '❌ Aucun bridge online pour broadcast' }],
672
701
  isError: true,
673
702
  };
674
703
  }
675
- const enrichedMessage = `[A2A — Broadcast from ${selfAgent}]\n${args.message}`;
676
704
  if (args.race) {
677
- // First to respond wins
678
- for (const w of targetWorkers) {
679
- const result = sendToWorker(w.port, enrichedMessage, {
680
- model: args.model,
681
- timeoutMs: timeout,
682
- });
683
- if (result.success) {
684
- return {
685
- content: [
686
- {
687
- type: 'text',
688
- text: `📡 **Broadcast race — ${w.agentName} (:${w.port}) a gagné!**\n\n${result.text}`,
689
- },
690
- ],
691
- };
692
- }
705
+ // First to respond wins — use Promise.any
706
+ try {
707
+ const firstResult = await Promise.any(targetBridges.map(async (b) => {
708
+ const r = await sendToBridge(b, args.message, { model: args.model, timeoutMs: timeout });
709
+ if (!r.success)
710
+ throw new Error(r.text);
711
+ return { bridge: b, text: r.text };
712
+ }));
713
+ return {
714
+ content: [{
715
+ type: 'text',
716
+ text: `📡 **Broadcast race — ${firstResult.bridge.agentName} (:${firstResult.bridge.port}) a gagné!**\n\n${firstResult.text}`,
717
+ }],
718
+ };
719
+ }
720
+ catch {
721
+ return {
722
+ content: [{ type: 'text', text: '❌ Tous les bridges ont échoué' }],
723
+ isError: true,
724
+ };
693
725
  }
694
- return {
695
- content: [{ type: 'text', text: '❌ Tous les workers ont échoué' }],
696
- isError: true,
697
- };
698
726
  }
699
727
  // Send to all in parallel
700
- const results = targetWorkers.map((w) => {
701
- const r = sendToWorker(w.port, enrichedMessage, {
702
- model: args.model,
703
- timeoutMs: timeout,
704
- });
705
- return { agent: `${w.agentName} (:${w.port})`, success: r.success, text: r.text };
706
- });
728
+ const results = await Promise.all(targetBridges.map(async (b) => {
729
+ const r = await sendToBridge(b, args.message, { model: args.model, timeoutMs: timeout });
730
+ return { agent: `${b.agentName} (:${b.port})`, success: r.success, text: r.text };
731
+ }));
707
732
  const successCount = results.filter((r) => r.success).length;
708
733
  const lines = [`📡 **Broadcast** — ${successCount}/${results.length} succès`, ``];
709
734
  for (const r of results) {
@@ -715,24 +740,17 @@ export async function a2aHub(args) {
715
740
  }
716
741
  default:
717
742
  return {
718
- content: [
719
- {
720
- type: 'text',
721
- text: `❌ Action inconnue: ${args.action}`,
722
- },
723
- ],
743
+ content: [{ type: 'text', text: `❌ Action inconnue: ${args.action}` }],
724
744
  isError: true,
725
745
  };
726
746
  }
727
747
  }
728
748
  catch (error) {
729
749
  return {
730
- content: [
731
- {
750
+ content: [{
732
751
  type: 'text',
733
752
  text: `❌ Erreur A2A Hub: ${error instanceof Error ? error.message : String(error)}`,
734
- },
735
- ],
753
+ }],
736
754
  isError: true,
737
755
  };
738
756
  }