@agenticmail/enterprise 0.5.240 → 0.5.242

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.
@@ -1154,6 +1154,9 @@ export function createAgentRoutes(opts: {
1154
1154
  return c.json({ config: managed.config?.browserConfig || {} });
1155
1155
  });
1156
1156
 
1157
+ // ── In-memory registry of running meeting browsers (survives config save/reload issues) ──
1158
+ const meetingBrowsers = new Map<string, { port: number; cdpUrl: string; pid?: number }>();
1159
+
1157
1160
  /**
1158
1161
  * POST /bridge/agents/:id/browser-config/launch-meeting-browser
1159
1162
  * Launches a meeting-ready headed Chrome instance with virtual display + audio.
@@ -1267,16 +1270,21 @@ export function createAgentRoutes(opts: {
1267
1270
  }, 400);
1268
1271
  }
1269
1272
 
1270
- // Check if a meeting browser is already running for this agent
1271
- const existingPort = (managed.config as any)?.meetingBrowserPort;
1273
+ // Check if a meeting browser is already running for this agent (in-memory registry first, then config fallback)
1274
+ const tracked = meetingBrowsers.get(agentId);
1275
+ const existingPort = tracked?.port || (managed.config as any)?.meetingBrowserPort;
1272
1276
  if (existingPort) {
1273
1277
  try {
1274
1278
  const resp = await fetch(`http://127.0.0.1:${existingPort}/json/version`, { signal: AbortSignal.timeout(2000) });
1275
1279
  if (resp.ok) {
1276
1280
  const data = await resp.json() as any;
1281
+ // Ensure registry is up to date
1282
+ meetingBrowsers.set(agentId, { port: existingPort, cdpUrl: data.webSocketDebuggerUrl, pid: tracked?.pid });
1277
1283
  return c.json({ ok: true, alreadyRunning: true, cdpUrl: data.webSocketDebuggerUrl, port: existingPort, browserVersion: data.Browser });
1278
1284
  }
1279
1285
  } catch { /* not running, will launch new one */ }
1286
+ // Was tracked but not responding — clean up
1287
+ meetingBrowsers.delete(agentId);
1280
1288
  }
1281
1289
 
1282
1290
  // ── Create a realistic browser profile using agent identity ──
@@ -1398,12 +1406,13 @@ export function createAgentRoutes(opts: {
1398
1406
  return c.json({ error: 'Chrome launched but CDP not responding after 15s' });
1399
1407
  }
1400
1408
 
1401
- // Save port to agent config for reuse
1409
+ // Save to in-memory registry (primary) and agent config (backup)
1410
+ meetingBrowsers.set(agentId, { port, cdpUrl, pid: child.pid });
1402
1411
  if (!managed.config) managed.config = {} as any;
1403
1412
  (managed.config as any).meetingBrowserPort = port;
1404
1413
  (managed.config as any).meetingBrowserCdpUrl = cdpUrl;
1405
1414
  managed.updatedAt = new Date().toISOString();
1406
- await lifecycle.saveAgent(agentId);
1415
+ try { await lifecycle.saveAgent(agentId); } catch (e) { console.warn('[meeting-browser] Config save failed (non-fatal):', e); }
1407
1416
 
1408
1417
  return c.json({ ok: true, cdpUrl, port, browserVersion, pid: child.pid });
1409
1418
  } catch (e: any) {
@@ -1421,7 +1430,8 @@ export function createAgentRoutes(opts: {
1421
1430
  if (!managed) return c.json({ error: 'Agent not found' }, 404);
1422
1431
 
1423
1432
  try {
1424
- const port = (managed.config as any)?.meetingBrowserPort;
1433
+ const tracked = meetingBrowsers.get(agentId);
1434
+ const port = tracked?.port || (managed.config as any)?.meetingBrowserPort;
1425
1435
  if (!port) return c.json({ error: 'No meeting browser is tracked for this agent' }, 400);
1426
1436
 
1427
1437
  // Try to close gracefully via CDP
@@ -1440,20 +1450,28 @@ export function createAgentRoutes(opts: {
1440
1450
  }
1441
1451
  } catch { /* not running or not reachable */ }
1442
1452
 
1443
- // Fallback: kill by port
1453
+ // Fallback: kill by PID first (most reliable), then by port
1454
+ if (!closed && tracked?.pid) {
1455
+ try { process.kill(tracked.pid, 'SIGTERM'); closed = true; } catch { /* already dead */ }
1456
+ }
1444
1457
  if (!closed) {
1445
1458
  try {
1446
1459
  const { execSync } = await import('node:child_process');
1447
- execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { timeout: 5000 });
1460
+ if (process.platform === 'win32') {
1461
+ execSync(`for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port}') do taskkill /PID %a /F 2>nul`, { timeout: 5000 });
1462
+ } else {
1463
+ execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || fuser -k ${port}/tcp 2>/dev/null || true`, { timeout: 5000 });
1464
+ }
1448
1465
  closed = true;
1449
1466
  } catch { /* already dead */ }
1450
1467
  }
1451
1468
 
1452
- // Clear config
1469
+ // Clear from registry and config
1470
+ meetingBrowsers.delete(agentId);
1453
1471
  delete (managed.config as any).meetingBrowserPort;
1454
1472
  delete (managed.config as any).meetingBrowserCdpUrl;
1455
1473
  managed.updatedAt = new Date().toISOString();
1456
- await lifecycle.saveAgent(agentId);
1474
+ try { await lifecycle.saveAgent(agentId); } catch (e) { console.warn('[meeting-browser] Config save failed (non-fatal):', e); }
1457
1475
 
1458
1476
  return c.json({ ok: true, stopped: true, port });
1459
1477
  } catch (e: any) {
@@ -341,6 +341,219 @@ engine.get('/hierarchy/escalations/:agentId', async (c) => {
341
341
  }
342
342
  });
343
343
 
344
+ // ─── MCP Server Management ──────────────────────────────────────────
345
+ // CRUD for external MCP server connections (stdio, SSE, HTTP)
346
+ engine.get('/mcp-servers', async (c) => {
347
+ try {
348
+ const rows = await engineDb.query(`SELECT * FROM mcp_servers WHERE org_id = $1 ORDER BY created_at DESC`, ['default']);
349
+ const servers = (rows || []).map((r: any) => {
350
+ const config = typeof r.config === 'string' ? JSON.parse(r.config) : (r.config || {});
351
+ return { id: r.id, ...config, status: r.status || 'unknown', toolCount: r.tool_count || 0, tools: r.tools ? (typeof r.tools === 'string' ? JSON.parse(r.tools) : r.tools) : [] };
352
+ });
353
+ return c.json({ servers });
354
+ } catch (e: any) {
355
+ // Table may not exist yet
356
+ if (e.message?.includes('does not exist') || e.message?.includes('no such table')) {
357
+ return c.json({ servers: [] });
358
+ }
359
+ return c.json({ error: e.message }, 500);
360
+ }
361
+ });
362
+
363
+ engine.post('/mcp-servers', async (c) => {
364
+ try {
365
+ await engineDb.exec(`CREATE TABLE IF NOT EXISTS mcp_servers (
366
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
367
+ org_id TEXT NOT NULL DEFAULT 'default',
368
+ config JSONB NOT NULL DEFAULT '{}',
369
+ status TEXT DEFAULT 'unknown',
370
+ tool_count INTEGER DEFAULT 0,
371
+ tools JSONB DEFAULT '[]',
372
+ created_at TIMESTAMPTZ DEFAULT NOW(),
373
+ updated_at TIMESTAMPTZ DEFAULT NOW()
374
+ )`);
375
+ const body = await c.req.json();
376
+ const id = crypto.randomUUID();
377
+ await engineDb.exec(`INSERT INTO mcp_servers (id, org_id, config) VALUES ($1, $2, $3)`, [id, 'default', JSON.stringify(body)]);
378
+ return c.json({ ok: true, id });
379
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
380
+ });
381
+
382
+ engine.put('/mcp-servers/:id', async (c) => {
383
+ try {
384
+ const id = c.req.param('id');
385
+ const body = await c.req.json();
386
+ // Merge with existing config
387
+ const rows = await engineDb.query(`SELECT config FROM mcp_servers WHERE id = $1`, [id]);
388
+ if (!rows?.length) return c.json({ error: 'Server not found' }, 404);
389
+ const existing = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
390
+ const merged = { ...existing, ...body };
391
+ await engineDb.exec(`UPDATE mcp_servers SET config = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(merged), id]);
392
+ return c.json({ ok: true });
393
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
394
+ });
395
+
396
+ engine.delete('/mcp-servers/:id', async (c) => {
397
+ try {
398
+ await engineDb.exec(`DELETE FROM mcp_servers WHERE id = $1`, [c.req.param('id')]);
399
+ return c.json({ ok: true });
400
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
401
+ });
402
+
403
+ engine.post('/mcp-servers/:id/test', async (c) => {
404
+ try {
405
+ const id = c.req.param('id');
406
+ const rows = await engineDb.query(`SELECT config FROM mcp_servers WHERE id = $1`, [id]);
407
+ if (!rows?.length) return c.json({ error: 'Server not found' }, 404);
408
+ const config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
409
+
410
+ if (config.type === 'stdio') {
411
+ // Test stdio: spawn process, send initialize, read response
412
+ const { spawn } = await import('node:child_process');
413
+ const args = config.args || [];
414
+ const env = { ...process.env, ...(config.env || {}) };
415
+ const child = spawn(config.command, args, { stdio: ['pipe', 'pipe', 'pipe'], env, timeout: (config.timeout || 30) * 1000 });
416
+
417
+ const initMsg = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'AgenticMail', version: '1.0' } } });
418
+ child.stdin.write(initMsg + '\n');
419
+
420
+ const result: any = await new Promise((resolve) => {
421
+ let buf = '';
422
+ const timer = setTimeout(() => { child.kill(); resolve({ error: 'Timeout after ' + (config.timeout || 30) + 's' }); }, (config.timeout || 30) * 1000);
423
+ child.stdout.on('data', (chunk: Buffer) => {
424
+ buf += chunk.toString();
425
+ try {
426
+ const parsed = JSON.parse(buf.trim());
427
+ clearTimeout(timer);
428
+ child.kill();
429
+ resolve(parsed);
430
+ } catch { /* wait for more data */ }
431
+ });
432
+ child.on('error', (err: any) => { clearTimeout(timer); resolve({ error: err.message }); });
433
+ child.on('exit', (code: number) => { if (!buf) { clearTimeout(timer); resolve({ error: 'Process exited with code ' + code }); } });
434
+ });
435
+
436
+ if (result.error) return c.json({ error: typeof result.error === 'string' ? result.error : result.error.message || 'Unknown error' });
437
+
438
+ // Now list tools
439
+ const listMsg = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
440
+ const child2 = spawn(config.command, args, { stdio: ['pipe', 'pipe', 'pipe'], env, timeout: 15000 });
441
+ child2.stdin.write(initMsg + '\n');
442
+
443
+ const tools: any[] = await new Promise((resolve) => {
444
+ let phase = 'init';
445
+ let buf2 = '';
446
+ const timer2 = setTimeout(() => { child2.kill(); resolve([]); }, 15000);
447
+ child2.stdout.on('data', (chunk: Buffer) => {
448
+ buf2 += chunk.toString();
449
+ const lines = buf2.split('\n');
450
+ for (const line of lines) {
451
+ if (!line.trim()) continue;
452
+ try {
453
+ const parsed = JSON.parse(line.trim());
454
+ if (phase === 'init' && parsed.id === 1) {
455
+ phase = 'tools';
456
+ buf2 = '';
457
+ child2.stdin.write(listMsg + '\n');
458
+ } else if (phase === 'tools' && parsed.id === 2) {
459
+ clearTimeout(timer2);
460
+ child2.kill();
461
+ resolve(parsed.result?.tools || []);
462
+ return;
463
+ }
464
+ } catch { /* partial line */ }
465
+ }
466
+ });
467
+ child2.on('error', () => { clearTimeout(timer2); resolve([]); });
468
+ });
469
+
470
+ // Save discovered tools
471
+ await engineDb.exec(`UPDATE mcp_servers SET status = 'connected', tool_count = $1, tools = $2, updated_at = NOW() WHERE id = $3`,
472
+ [tools.length, JSON.stringify(tools.map((t: any) => ({ name: t.name, description: t.description }))), id]);
473
+
474
+ return c.json({ ok: true, tools: tools.length, serverInfo: result.result?.serverInfo });
475
+
476
+ } else {
477
+ // Test HTTP/SSE: send initialize via HTTP
478
+ const url = config.url;
479
+ const headers: Record<string, string> = { 'Content-Type': 'application/json', ...(config.headers || {}) };
480
+ if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
481
+
482
+ const resp = await fetch(url, {
483
+ method: 'POST',
484
+ headers,
485
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'AgenticMail', version: '1.0' } } }),
486
+ signal: AbortSignal.timeout((config.timeout || 30) * 1000),
487
+ });
488
+
489
+ if (!resp.ok) return c.json({ error: `Server returned ${resp.status}` });
490
+ const data = await resp.json() as any;
491
+ if (data.error) return c.json({ error: data.error.message || 'Server error' });
492
+
493
+ // List tools
494
+ const toolResp = await fetch(url, {
495
+ method: 'POST', headers,
496
+ body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
497
+ signal: AbortSignal.timeout(15000),
498
+ });
499
+ let tools: any[] = [];
500
+ if (toolResp.ok) {
501
+ const td = await toolResp.json() as any;
502
+ tools = td.result?.tools || [];
503
+ }
504
+
505
+ await engineDb.exec(`UPDATE mcp_servers SET status = 'connected', tool_count = $1, tools = $2, updated_at = NOW() WHERE id = $3`,
506
+ [tools.length, JSON.stringify(tools.map((t: any) => ({ name: t.name, description: t.description }))), id]);
507
+
508
+ return c.json({ ok: true, tools: tools.length, serverInfo: data.result?.serverInfo });
509
+ }
510
+ } catch (e: any) {
511
+ // Update status to error
512
+ try { await engineDb.exec(`UPDATE mcp_servers SET status = 'error', updated_at = NOW() WHERE id = $1`, [c.req.param('id')]); } catch {}
513
+ return c.json({ error: e.message });
514
+ }
515
+ });
516
+
517
+ // ─── Integration Credentials Management ─────────────────
518
+ engine.put('/integrations/:skillId/credentials', async (c) => {
519
+ try {
520
+ const skillId = c.req.param('skillId');
521
+ const body = await c.req.json();
522
+ const orgId = c.req.query('orgId') || 'default';
523
+ // Store each credential field as a vault secret
524
+ for (const [key, value] of Object.entries(body)) {
525
+ if (!value) continue;
526
+ const secretName = `skill:${skillId}:${key}`;
527
+ // Check if exists, update or create
528
+ try {
529
+ const entries = await vault.getSecretsByOrg(orgId, 'skill_credential');
530
+ const existing = entries.find((e: any) => e.name === secretName);
531
+ if (existing) {
532
+ await vault.updateSecret(existing.id, { value: value as string });
533
+ } else {
534
+ await vault.storeSecret({ name: secretName, value: value as string, category: 'skill_credential', orgId });
535
+ }
536
+ } catch {
537
+ await vault.storeSecret({ name: secretName, value: value as string, category: 'skill_credential', orgId });
538
+ }
539
+ }
540
+ return c.json({ ok: true });
541
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
542
+ });
543
+
544
+ engine.delete('/integrations/:skillId/credentials', async (c) => {
545
+ try {
546
+ const skillId = c.req.param('skillId');
547
+ const orgId = c.req.query('orgId') || 'default';
548
+ const entries = await vault.getSecretsByOrg(orgId, 'skill_credential');
549
+ const matching = entries.filter((e: any) => e.name?.startsWith(`skill:${skillId}:`));
550
+ for (const entry of matching) {
551
+ await vault.deleteSecret(entry.id);
552
+ }
553
+ return c.json({ ok: true });
554
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
555
+ });
556
+
344
557
  // ─── Integration catalog (serves all 144 MCP adapter integrations) ──
345
558
  engine.get('/integrations/catalog', async (c) => {
346
559
  try {