overmind-mcp 2.8.13 → 2.8.14

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 (57) hide show
  1. package/dist/bin/launch.js +78 -0
  2. package/dist/bin/overmind-bridge.d.ts +42 -0
  3. package/dist/bin/overmind-bridge.d.ts.map +1 -0
  4. package/dist/bin/overmind-bridge.js +503 -0
  5. package/dist/bin/overmind-bridge.js.map +1 -0
  6. package/dist/bridge/ArgParser.d.ts +45 -0
  7. package/dist/bridge/ArgParser.d.ts.map +1 -0
  8. package/dist/bridge/ArgParser.js +134 -0
  9. package/dist/bridge/ArgParser.js.map +1 -0
  10. package/dist/bridge/BridgeHttpClient.d.ts +61 -0
  11. package/dist/bridge/BridgeHttpClient.d.ts.map +1 -0
  12. package/dist/bridge/BridgeHttpClient.js +164 -0
  13. package/dist/bridge/BridgeHttpClient.js.map +1 -0
  14. package/dist/bridge/DirectiveParser.d.ts +82 -0
  15. package/dist/bridge/DirectiveParser.d.ts.map +1 -0
  16. package/dist/bridge/DirectiveParser.js +154 -0
  17. package/dist/bridge/DirectiveParser.js.map +1 -0
  18. package/dist/bridge/JsonSanitizer.d.ts +34 -0
  19. package/dist/bridge/JsonSanitizer.d.ts.map +1 -0
  20. package/dist/bridge/JsonSanitizer.js +90 -0
  21. package/dist/bridge/JsonSanitizer.js.map +1 -0
  22. package/dist/bridge/OverBridgeServer.d.ts +36 -2
  23. package/dist/bridge/OverBridgeServer.d.ts.map +1 -1
  24. package/dist/bridge/OverBridgeServer.js +351 -23
  25. package/dist/bridge/OverBridgeServer.js.map +1 -1
  26. package/dist/bridge/PromptSource.d.ts +66 -0
  27. package/dist/bridge/PromptSource.d.ts.map +1 -0
  28. package/dist/bridge/PromptSource.js +152 -0
  29. package/dist/bridge/PromptSource.js.map +1 -0
  30. package/dist/bridge/RequestContext.d.ts +19 -0
  31. package/dist/bridge/RequestContext.d.ts.map +1 -0
  32. package/dist/bridge/RequestContext.js +34 -0
  33. package/dist/bridge/RequestContext.js.map +1 -0
  34. package/dist/bridge/ScenarioLoader.d.ts +124 -0
  35. package/dist/bridge/ScenarioLoader.d.ts.map +1 -0
  36. package/dist/bridge/ScenarioLoader.js +333 -0
  37. package/dist/bridge/ScenarioLoader.js.map +1 -0
  38. package/dist/bridge/SessionStore.d.ts +109 -0
  39. package/dist/bridge/SessionStore.d.ts.map +1 -0
  40. package/dist/bridge/SessionStore.js +220 -0
  41. package/dist/bridge/SessionStore.js.map +1 -0
  42. package/dist/bridge/WebhookAdapter.d.ts +76 -0
  43. package/dist/bridge/WebhookAdapter.d.ts.map +1 -0
  44. package/dist/bridge/WebhookAdapter.js +186 -0
  45. package/dist/bridge/WebhookAdapter.js.map +1 -0
  46. package/dist/bridge/index.d.ts +16 -0
  47. package/dist/bridge/index.d.ts.map +1 -1
  48. package/dist/bridge/index.js +10 -0
  49. package/dist/bridge/index.js.map +1 -1
  50. package/dist/bridge/utils.d.ts +9 -0
  51. package/dist/bridge/utils.d.ts.map +1 -1
  52. package/dist/bridge/utils.js +17 -0
  53. package/dist/bridge/utils.js.map +1 -1
  54. package/dist/services/NousHermesRunner.d.ts.map +1 -1
  55. package/dist/services/NousHermesRunner.js +47 -8
  56. package/dist/services/NousHermesRunner.js.map +1 -1
  57. package/package.json +2 -1
@@ -20,9 +20,16 @@
20
20
  */
21
21
  import http from 'node:http';
22
22
  import { URL } from 'node:url';
23
+ import path from 'node:path';
24
+ import fs from 'node:fs';
23
25
  import { z } from 'zod';
24
26
  import { AgentRegistry } from './AgentRegistry.js';
25
27
  import { MessageLog } from './MessageLog.js';
28
+ import { SessionStore } from './SessionStore.js';
29
+ import { DirectiveParser } from './DirectiveParser.js';
30
+ import { WebhookAdapter } from './WebhookAdapter.js';
31
+ import { sanitizeAndParse, looksLikeWindowsPathIssue } from './JsonSanitizer.js';
32
+ import { getOrCreateRequestId } from './RequestContext.js';
26
33
  import { createBridgeLogger, validateAgentName } from './utils.js';
27
34
  const JSON_RPC_ERRORS = {
28
35
  PARSE_ERROR: { code: -32700, message: 'Parse error' },
@@ -44,6 +51,10 @@ const RunAgentParams = z.object({
44
51
  mode: z.string().optional(),
45
52
  silent: z.boolean().optional(),
46
53
  metadata: z.record(z.string(), z.unknown()).optional(),
54
+ /** Clé externe pour SessionStore (phone, userId, etc.) — auto-résolution de sessionId */
55
+ externalKey: z.string().optional(),
56
+ /** Active le parsing de directives dans la réponse (default: true si enableDirectives) */
57
+ parseDirectives: z.boolean().optional(),
47
58
  });
48
59
  const A2AParams = z.object({
49
60
  fromAgent: z.string().min(1),
@@ -85,6 +96,9 @@ export class OverBridgeServer {
85
96
  registry;
86
97
  log;
87
98
  messageLog;
99
+ sessions;
100
+ directiveParser;
101
+ webhookAdapter;
88
102
  config;
89
103
  server;
90
104
  startTime = 0;
@@ -93,13 +107,22 @@ export class OverBridgeServer {
93
107
  this.config = config;
94
108
  this.log = logger ?? createBridgeLogger('overbridge-server');
95
109
  this.registry = new AgentRegistry(this.log);
110
+ this.directiveParser = new DirectiveParser({ logger: this.log });
111
+ this.webhookAdapter = new WebhookAdapter({ logger: this.log });
96
112
  if (config.enableMessageLog) {
97
113
  this.messageLog = new MessageLog(config.postgres, this.log);
98
114
  }
115
+ if (config.enableSessionStore) {
116
+ const defaultPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '.', '.overmind', 'bridge', 'sessions.json');
117
+ this.sessions = new SessionStore({
118
+ persistPath: config.sessionStorePath ?? defaultPath,
119
+ ttlMs: config.sessionTtlMs,
120
+ }, this.log);
121
+ }
99
122
  }
100
123
  // ─── Lifecycle ───────────────────────────────────────────────────────────
101
124
  /**
102
- * Démarre le serveur HTTP et initialise les dépendances (MessageLog, OverBridgeService).
125
+ * Démarre le serveur HTTP et initialise les dépendances (MessageLog, SessionStore, OverBridgeService).
103
126
  */
104
127
  async start() {
105
128
  this.startTime = Date.now();
@@ -107,7 +130,11 @@ export class OverBridgeServer {
107
130
  if (this.messageLog) {
108
131
  await this.messageLog.init();
109
132
  }
110
- // 2) Connect OverBridgeService (MCP healthcheck)
133
+ // 2) Init SessionStore
134
+ if (this.sessions) {
135
+ await this.sessions.init();
136
+ }
137
+ // 3) Connect OverBridgeService (MCP healthcheck)
111
138
  try {
112
139
  const status = await this.service.connect(this.config.healthCheckIntervalMs);
113
140
  this.log.info(`🔌 OverBridgeService connected: ${status.status}`);
@@ -116,7 +143,7 @@ export class OverBridgeServer {
116
143
  this.log.warn(`⚠️ OverBridgeService connect failed: ${err.message}`);
117
144
  // On continue quand même, le serveur répondra avec erreurs si MCP down
118
145
  }
119
- // 3) Démarre le serveur HTTP
146
+ // 4) Démarre le serveur HTTP
120
147
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
121
148
  await new Promise((resolve) => {
122
149
  this.server.listen(this.config.port, this.config.host, () => resolve());
@@ -127,6 +154,15 @@ export class OverBridgeServer {
127
154
  this.log.info(`🚀 OverBridgeServer listening on ${url}`);
128
155
  this.log.info(` POST ${url}/rpc (JSON-RPC 2.0)`);
129
156
  this.log.info(` GET ${url}/health`);
157
+ if (this.config.enableWebhooks) {
158
+ this.log.info(` POST ${url}/webhook/:provider (voipms, twilio, discord, generic)`);
159
+ }
160
+ if (this.sessions) {
161
+ this.log.info(` SessionStore: enabled (TTL ${(this.config.sessionTtlMs ?? 14_400_000) / 1000}s)`);
162
+ }
163
+ if (this.config.enableDirectives) {
164
+ this.log.info(` DirectiveParser: enabled (SESSION_ID, CONTEXT_UPDATE, BRIDGE_NEXT)`);
165
+ }
130
166
  return { port, host: this.config.host, url };
131
167
  }
132
168
  /**
@@ -142,6 +178,9 @@ export class OverBridgeServer {
142
178
  if (this.messageLog) {
143
179
  await this.messageLog.close();
144
180
  }
181
+ if (this.sessions) {
182
+ await this.sessions.close();
183
+ }
145
184
  }
146
185
  /**
147
186
  * Expose le registry (pour tests / inspection).
@@ -155,9 +194,18 @@ export class OverBridgeServer {
155
194
  get messages() {
156
195
  return this.messageLog;
157
196
  }
197
+ /**
198
+ * Expose le SessionStore (pour tests / inspection).
199
+ */
200
+ get sessionStore() {
201
+ return this.sessions;
202
+ }
158
203
  // ─── HTTP Request Handler ───────────────────────────────────────────────
159
204
  async handleRequest(req, res) {
160
205
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
206
+ const reqId = getOrCreateRequestId(req.headers);
207
+ // Set reqId header in response
208
+ res.setHeader('X-Request-Id', reqId);
161
209
  // CORS preflight
162
210
  if (req.method === 'OPTIONS') {
163
211
  this.writeCors(res);
@@ -174,15 +222,25 @@ export class OverBridgeServer {
174
222
  }
175
223
  // JSON-RPC endpoint
176
224
  if (req.method === 'POST' && url.pathname === '/rpc') {
177
- await this.handleRpc(req, res);
225
+ await this.handleRpc(req, res, reqId);
226
+ return;
227
+ }
228
+ // Webhook endpoint (si activé) — /webhook/:provider
229
+ if (this.config.enableWebhooks && req.method === 'POST' && url.pathname.startsWith('/webhook/')) {
230
+ await this.handleWebhook(req, res, url, reqId);
231
+ return;
232
+ }
233
+ // File serve — /f/:filename (statique, comme bt-sms)
234
+ if (req.method === 'GET' && url.pathname.startsWith('/f/')) {
235
+ await this.handleStaticFile(req, res, url);
178
236
  return;
179
237
  }
180
238
  // 404
181
- this.writeJson(res, 404, { error: 'Not found', path: url.pathname });
239
+ this.writeJson(res, 404, { error: 'Not found', path: url.pathname, reqId });
182
240
  }
183
241
  catch (err) {
184
- this.log.error(`💥 Unhandled error: ${err.message}`);
185
- this.writeJson(res, 500, { error: 'Internal server error' });
242
+ this.log.error(`💥 Unhandled error: ${err.message} (reqId=${reqId})`);
243
+ this.writeJson(res, 500, { error: 'Internal server error', reqId });
186
244
  }
187
245
  }
188
246
  writeCors(res) {
@@ -194,6 +252,77 @@ export class OverBridgeServer {
194
252
  res.writeHead(status, { 'Content-Type': 'application/json' });
195
253
  res.end(JSON.stringify(body));
196
254
  }
255
+ // ─── /webhook/:provider Endpoint ─────────────────────────────────────────
256
+ async handleWebhook(req, res, url, reqId) {
257
+ // /webhook/:provider — extrait le provider du path
258
+ const parts = url.pathname.split('/').filter(Boolean);
259
+ const provider = (parts[1] ?? 'voipms');
260
+ const raw = await this.readBody(req);
261
+ let payload;
262
+ try {
263
+ payload = JSON.parse(raw);
264
+ }
265
+ catch {
266
+ // Tente sanitizer (VoIP.ms envoie parfois du form-encoded mal formé)
267
+ try {
268
+ payload = sanitizeAndParse(raw);
269
+ }
270
+ catch {
271
+ this.writeJson(res, 400, { error: 'Invalid JSON body', reqId });
272
+ return;
273
+ }
274
+ }
275
+ // Adapte via WebhookAdapter
276
+ const adapter = new WebhookAdapter({ provider, logger: this.log });
277
+ let normalized;
278
+ try {
279
+ normalized = adapter.adapt(payload);
280
+ }
281
+ catch (err) {
282
+ this.writeJson(res, 400, { error: err.message, reqId });
283
+ return;
284
+ }
285
+ // Auto-dispatch vers agent.run via internal call
286
+ this.log.info(`[reqId=${reqId}] 📨 Webhook ${provider} from ${normalized.externalKey}`);
287
+ // Cherche un agent par défaut (config-driven ou fallback premier agent)
288
+ // Pour l'instant on retourne le normalized et on log ; les utilisateurs
289
+ // peuvent soit :
290
+ // 1. Définir une config par-provider pour auto-dispatch
291
+ // 2. Appeler manuellement /rpc agent.run après réception
292
+ this.writeJson(res, 200, {
293
+ received: true,
294
+ reqId,
295
+ provider,
296
+ externalKey: normalized.externalKey,
297
+ promptPreview: normalized.prompt.slice(0, 200),
298
+ mediaCount: normalized.mediaUrls.length,
299
+ metadata: normalized.metadata,
300
+ });
301
+ }
302
+ // ─── /f/:filename Static File Serve ──────────────────────────────────────
303
+ async handleStaticFile(_req, res, url) {
304
+ const filename = url.pathname.slice(3); // strip /f/
305
+ if (!filename || filename.includes('..')) {
306
+ this.writeJson(res, 400, { error: 'Invalid filename' });
307
+ return;
308
+ }
309
+ // Le dossier est configuré via BRIDGE_STATIC_DIR env ou défaut './public'
310
+ const staticDir = process.env.BRIDGE_STATIC_DIR ?? './public';
311
+ const filepath = path.join(staticDir, filename);
312
+ if (!fs.existsSync(filepath)) {
313
+ this.writeJson(res, 404, { error: 'Not found' });
314
+ return;
315
+ }
316
+ const ext = path.extname(filepath).toLowerCase();
317
+ const mimeMap = {
318
+ '.html': 'text/html', '.pdf': 'application/pdf', '.png': 'image/png',
319
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.json': 'application/json',
320
+ '.txt': 'text/plain', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4',
321
+ '.gif': 'image/gif', '.wav': 'audio/wav', '.xml': 'text/xml',
322
+ };
323
+ res.setHeader('Content-Type', mimeMap[ext] ?? 'application/octet-stream');
324
+ fs.createReadStream(filepath).pipe(res);
325
+ }
197
326
  // ─── /health Endpoint ───────────────────────────────────────────────────
198
327
  async handleHealth(res) {
199
328
  const mcpHealth = await this.service.proxyAccess.healthCheck();
@@ -207,17 +336,26 @@ export class OverBridgeServer {
207
336
  msgStats = null;
208
337
  }
209
338
  }
339
+ const sessionStats = this.sessions ? this.sessions.stats() : null;
210
340
  this.writeJson(res, 200, {
211
341
  status: mcpHealth.status,
212
342
  uptime: Date.now() - this.startTime,
213
343
  mcp: mcpHealth,
214
344
  agents: regStats,
215
345
  messages: msgStats,
216
- version: '1.0.0',
346
+ sessions: sessionStats,
347
+ features: {
348
+ sessionStore: !!this.sessions,
349
+ directives: !!this.config.enableDirectives,
350
+ webhooks: !!this.config.enableWebhooks,
351
+ sanitizeJson: !!this.config.sanitizeJson,
352
+ messageLog: !!this.messageLog,
353
+ },
354
+ version: '1.1.0',
217
355
  });
218
356
  }
219
357
  // ─── /rpc Endpoint (JSON-RPC 2.0 Dispatcher) ────────────────────────────
220
- async handleRpc(req, res) {
358
+ async handleRpc(req, res, reqId) {
221
359
  // Auth check
222
360
  if (this.config.authToken) {
223
361
  const auth = req.headers.authorization;
@@ -225,7 +363,7 @@ export class OverBridgeServer {
225
363
  this.writeJson(res, 401, {
226
364
  jsonrpc: '2.0',
227
365
  id: null,
228
- error: { code: -32000, message: 'Unauthorized' },
366
+ error: { code: -32000, message: 'Unauthorized', data: { reqId } },
229
367
  });
230
368
  return;
231
369
  }
@@ -234,12 +372,39 @@ export class OverBridgeServer {
234
372
  const raw = await this.readBody(req);
235
373
  let parsed;
236
374
  try {
375
+ // Try direct parse first
237
376
  parsed = JSON.parse(raw);
238
377
  }
239
378
  catch {
240
- this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
241
- return;
379
+ // Si sanitizer activé et body ressemble à un Windows path, on tente la repair
380
+ if (this.config.sanitizeJson && looksLikeWindowsPathIssue(raw)) {
381
+ try {
382
+ parsed = sanitizeAndParse(raw);
383
+ this.log.warn(`[reqId=${reqId}] ⚠️ JSON body sanitized (Windows path issue)`);
384
+ }
385
+ catch {
386
+ this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
387
+ return;
388
+ }
389
+ }
390
+ else {
391
+ this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
392
+ return;
393
+ }
242
394
  }
395
+ // Inject reqId into all calls (pour corrélation)
396
+ const injectReqId = (reqs) => {
397
+ const inject = (r) => {
398
+ if (!r.params)
399
+ r.params = {};
400
+ r.params.__reqId = reqId;
401
+ };
402
+ if (Array.isArray(reqs))
403
+ reqs.forEach(inject);
404
+ else
405
+ inject(reqs);
406
+ };
407
+ injectReqId(parsed);
243
408
  // Batch ou single
244
409
  if (Array.isArray(parsed)) {
245
410
  if (parsed.length === 0) {
@@ -282,6 +447,16 @@ export class OverBridgeServer {
282
447
  return this.methodMessageReplay(req);
283
448
  case 'message.stats':
284
449
  return this.methodMessageStats(req);
450
+ case 'session.get':
451
+ return this.methodSessionGet(req);
452
+ case 'session.list':
453
+ return this.methodSessionList(req);
454
+ case 'session.delete':
455
+ return this.methodSessionDelete(req);
456
+ case 'session.stats':
457
+ return this.methodSessionStats(req);
458
+ case 'webhook.sms':
459
+ return this.methodWebhookSms(req);
285
460
  case 'health.ping':
286
461
  return { jsonrpc: '2.0', id: req.id ?? null, result: { pong: true, ts: Date.now() } };
287
462
  default:
@@ -300,15 +475,26 @@ export class OverBridgeServer {
300
475
  // ─── RPC Methods ─────────────────────────────────────────────────────────
301
476
  /**
302
477
  * agent.run — Lance un agent (from client externe OU from autre agent).
478
+ * Avec support SessionStore (externalKey) et DirectiveParser.
303
479
  */
304
480
  async methodAgentRun(req) {
305
481
  const params = this.validateParams(RunAgentParams, req.params, req.id);
306
482
  if (!params.ok)
307
483
  return params.response;
308
- const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata } = params.data;
484
+ const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata, externalKey, parseDirectives } = params.data;
309
485
  const validatedAgentName = validateAgentName(agentName);
486
+ const reqId = req.params?.__reqId;
310
487
  // Register
311
488
  this.registry.register(validatedAgentName, runner);
489
+ // SessionStore resolution (si externalKey fourni)
490
+ let effectiveSessionId = sessionId;
491
+ if (this.sessions && externalKey) {
492
+ const stored = this.sessions.get(externalKey, validatedAgentName);
493
+ if (stored) {
494
+ effectiveSessionId = stored.sessionId;
495
+ this.log.info(`[reqId=${reqId}] 🔗 Session restored for ${externalKey} → ${effectiveSessionId}`);
496
+ }
497
+ }
312
498
  // Persist (pending)
313
499
  let messageId;
314
500
  if (this.messageLog) {
@@ -317,39 +503,79 @@ export class OverBridgeServer {
317
503
  toAgent: validatedAgentName,
318
504
  runner: runner,
319
505
  prompt,
320
- sessionId,
321
- metadata: metadata ?? null,
506
+ sessionId: effectiveSessionId,
507
+ metadata: { ...(metadata ?? {}), ...(reqId ? { reqId } : {}), ...(externalKey ? { externalKey } : {}) },
322
508
  });
323
509
  }
324
510
  // Run avec mutex (1 run par agent à la fois)
325
511
  const result = await this.registry.withLock(validatedAgentName, async () => {
326
- this.registry.markBusy(validatedAgentName, sessionId);
512
+ this.registry.markBusy(validatedAgentName, effectiveSessionId);
327
513
  if (messageId && this.messageLog) {
328
- await this.messageLog.markRunning(messageId, sessionId);
514
+ await this.messageLog.markRunning(messageId, effectiveSessionId);
329
515
  }
330
516
  try {
331
517
  const agentResult = await this.service.runAgent({
332
518
  runner: runner,
333
519
  prompt,
334
520
  agentName: validatedAgentName,
335
- sessionId,
521
+ sessionId: effectiveSessionId,
336
522
  path,
337
523
  model,
338
524
  mode,
339
525
  silent,
340
526
  });
341
- const responseText = agentResult.content.map((c) => c.text).join('\n');
527
+ const rawResponseText = agentResult.content.map((c) => c.text).join('\n');
528
+ // ─── Directive parsing ─────────────────────────────────────────
529
+ let cleanText = rawResponseText;
530
+ let directives;
531
+ const shouldParseDirectives = parseDirectives ?? this.config.enableDirectives;
532
+ if (shouldParseDirectives) {
533
+ directives = this.directiveParser.parse(rawResponseText);
534
+ cleanText = directives.cleanText;
535
+ for (const action of directives.actions) {
536
+ if (action.kind === 'session' && this.sessions && externalKey) {
537
+ this.sessions.updateSessionId(externalKey, validatedAgentName, action.sessionId, runner);
538
+ this.log.info(`[reqId=${reqId}] 🔗 SESSION_ID directive → ${action.sessionId}`);
539
+ }
540
+ else if (action.kind === 'context' && this.sessions && externalKey) {
541
+ this.sessions.updateContext(externalKey, validatedAgentName, action.patch);
542
+ this.log.info(`[reqId=${reqId}] 📝 CONTEXT_UPDATE directive → ${JSON.stringify(action.patch)}`);
543
+ }
544
+ else if (action.kind === 'hint') {
545
+ this.log.info(`[reqId=${reqId}] 💡 BRIDGE_HINT: ${action.text}`);
546
+ }
547
+ }
548
+ }
549
+ // Save session
550
+ if (this.sessions && externalKey && agentResult.sessionId) {
551
+ this.sessions.set({
552
+ externalKey,
553
+ agentName: validatedAgentName,
554
+ runner: runner,
555
+ sessionId: agentResult.sessionId,
556
+ context: directives?.actions.find((a) => a.kind === 'context')
557
+ ? directives.actions.find((a) => a.kind === 'context').patch
558
+ : undefined,
559
+ });
560
+ }
342
561
  if (messageId && this.messageLog) {
343
- await this.messageLog.markDone(messageId, responseText, agentResult.sessionId);
562
+ await this.messageLog.markDone(messageId, cleanText, agentResult.sessionId);
344
563
  }
345
564
  this.registry.markIdle(validatedAgentName, !agentResult.isError);
346
565
  if (!agentResult.isError)
347
566
  this.registry.markOnline(validatedAgentName);
567
+ // Return content nettoyé des directives (si parsing activé)
568
+ const resultContent = shouldParseDirectives
569
+ ? [{ type: 'text', text: cleanText }]
570
+ : agentResult.content;
348
571
  return {
349
572
  messageId,
350
573
  sessionId: agentResult.sessionId,
351
- content: agentResult.content,
574
+ content: resultContent,
352
575
  isError: agentResult.isError,
576
+ directives: shouldParseDirectives
577
+ ? directives?.actions.map((a) => a.kind)
578
+ : undefined,
353
579
  };
354
580
  }
355
581
  catch (err) {
@@ -589,6 +815,84 @@ export class OverBridgeServer {
589
815
  const stats = await this.messageLog.stats();
590
816
  return { jsonrpc: '2.0', id: req.id ?? null, result: stats };
591
817
  }
818
+ // ─── SessionStore RPC Methods ───────────────────────────────────────────
819
+ async methodSessionGet(req) {
820
+ if (!this.sessions) {
821
+ return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
822
+ }
823
+ const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
824
+ if (!params.ok)
825
+ return params.response;
826
+ const entry = this.sessions.get(params.data.externalKey, params.data.agentName);
827
+ return { jsonrpc: '2.0', id: req.id ?? null, result: { session: entry } };
828
+ }
829
+ async methodSessionList(_req) {
830
+ if (!this.sessions) {
831
+ return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
832
+ }
833
+ const list = this.sessions.list();
834
+ const stats = this.sessions.stats();
835
+ return { jsonrpc: '2.0', id: _req.id ?? null, result: { sessions: list, stats } };
836
+ }
837
+ async methodSessionDelete(req) {
838
+ if (!this.sessions) {
839
+ return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
840
+ }
841
+ const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
842
+ if (!params.ok)
843
+ return params.response;
844
+ const deleted = this.sessions.delete(params.data.externalKey, params.data.agentName);
845
+ return { jsonrpc: '2.0', id: req.id ?? null, result: { deleted } };
846
+ }
847
+ async methodSessionStats(_req) {
848
+ if (!this.sessions) {
849
+ return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
850
+ }
851
+ return { jsonrpc: '2.0', id: _req.id ?? null, result: this.sessions.stats() };
852
+ }
853
+ // ─── Webhook RPC Method (programmatic, sans passer par HTTP) ────────────
854
+ async methodWebhookSms(req) {
855
+ const params = this.validateParams(z.object({
856
+ provider: z.enum(['voipms', 'twilio', 'discord', 'generic']).default('voipms'),
857
+ payload: z.record(z.string(), z.unknown()),
858
+ externalKey: z.string().optional(),
859
+ /** Si fourni, dispatch automatique vers agent.run après adaptation */
860
+ autoDispatch: z.object({
861
+ agentName: z.string().min(1),
862
+ runner: z.string().min(1),
863
+ model: z.string().optional(),
864
+ mode: z.string().optional(),
865
+ }).optional(),
866
+ }), req.params, req.id);
867
+ if (!params.ok)
868
+ return params.response;
869
+ const adapter = new WebhookAdapter({ provider: params.data.provider, logger: this.log });
870
+ const normalized = adapter.adapt(params.data.payload);
871
+ const externalKey = params.data.externalKey ?? normalized.externalKey;
872
+ if (!params.data.autoDispatch) {
873
+ return {
874
+ jsonrpc: '2.0',
875
+ id: req.id ?? null,
876
+ result: { externalKey, prompt: normalized.prompt, mediaUrls: normalized.mediaUrls, metadata: normalized.metadata },
877
+ };
878
+ }
879
+ // Auto-dispatch vers agent.run
880
+ const innerReq = {
881
+ jsonrpc: '2.0',
882
+ id: req.id,
883
+ method: 'agent.run',
884
+ params: {
885
+ agentName: params.data.autoDispatch.agentName,
886
+ runner: params.data.autoDispatch.runner,
887
+ prompt: normalized.prompt,
888
+ model: params.data.autoDispatch.model,
889
+ mode: params.data.autoDispatch.mode,
890
+ externalKey,
891
+ metadata: { webhook: true, provider: params.data.provider, ...normalized.metadata },
892
+ },
893
+ };
894
+ return this.methodAgentRun(innerReq);
895
+ }
592
896
  // ─── Helpers ────────────────────────────────────────────────────────────
593
897
  /**
594
898
  * Valide les params d'une requête JSON-RPC via Zod.
@@ -625,14 +929,38 @@ export class OverBridgeServer {
625
929
  },
626
930
  };
627
931
  }
628
- readBody(req) {
932
+ readBody(req, maxBytes) {
933
+ const limit = maxBytes ?? this.parseBodyLimit(this.config.jsonBodyLimit);
629
934
  return new Promise((resolve, reject) => {
630
935
  const chunks = [];
631
- req.on('data', (chunk) => chunks.push(chunk));
936
+ let total = 0;
937
+ req.on('data', (chunk) => {
938
+ total += chunk.length;
939
+ if (total > limit) {
940
+ req.destroy();
941
+ reject(Object.assign(new Error('Request body too large'), { code: 'EBODYTOOLARGE' }));
942
+ return;
943
+ }
944
+ chunks.push(chunk);
945
+ });
632
946
  req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
633
947
  req.on('error', reject);
634
948
  });
635
949
  }
950
+ /**
951
+ * Parse "10mb" / "1gb" en bytes.
952
+ */
953
+ parseBodyLimit(limit) {
954
+ if (!limit)
955
+ return 10 * 1024 * 1024; // 10mb default
956
+ const m = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i);
957
+ if (!m)
958
+ return 10 * 1024 * 1024;
959
+ const num = Number(m[1]);
960
+ const unit = (m[2] ?? 'b').toLowerCase();
961
+ const mult = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
962
+ return Math.floor(num * (mult[unit] ?? 1));
963
+ }
636
964
  /**
637
965
  * Parse le retour de agent_control pour extraire le status de l'agent distant.
638
966
  */