@agenticmail/enterprise 0.5.324 → 0.5.325

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.
@@ -343,6 +343,124 @@ engine.get('/cluster/best-node', (c) => {
343
343
  const node = cluster.findBestNode(caps);
344
344
  return node ? c.json(node) : c.json({ error: 'No suitable node available' }, 404);
345
345
  });
346
+ engine.post('/cluster/test-connection', async (c) => {
347
+ const { host, port } = await c.req.json();
348
+ if (!host) return c.json({ error: 'host required' }, 400);
349
+ const p = port || 3101;
350
+ const start = Date.now();
351
+ try {
352
+ const ctrl = new AbortController();
353
+ const timeout = setTimeout(() => ctrl.abort(), 5000);
354
+ const res = await fetch(`http://${host}:${p}/health`, { signal: ctrl.signal }).catch(() => null);
355
+ clearTimeout(timeout);
356
+ if (res && res.ok) {
357
+ const data = await res.json().catch(() => ({}));
358
+ return c.json({ success: true, latencyMs: Date.now() - start, version: data.version, agentId: data.agentId });
359
+ }
360
+ // Try the enterprise status endpoint as fallback
361
+ const ctrl2 = new AbortController();
362
+ const timeout2 = setTimeout(() => ctrl2.abort(), 5000);
363
+ const res2 = await fetch(`http://${host}:${p}/api/status`, { signal: ctrl2.signal }).catch(() => null);
364
+ clearTimeout(timeout2);
365
+ if (res2 && res2.ok) {
366
+ return c.json({ success: true, latencyMs: Date.now() - start, version: 'enterprise' });
367
+ }
368
+ return c.json({ success: false, error: 'No response from ' + host + ':' + p, latencyMs: Date.now() - start });
369
+ } catch (e: any) {
370
+ return c.json({ success: false, error: e.message || 'Connection failed', latencyMs: Date.now() - start });
371
+ }
372
+ });
373
+
374
+ engine.post('/cluster/deploy-via-ssh', async (c) => {
375
+ const body = await c.req.json();
376
+ if (!body.host) return c.json({ error: 'host required' }, 400);
377
+ // Use the deployment engine's SSH infrastructure
378
+ try {
379
+ const dbUrl = process.env.DATABASE_URL || '';
380
+ const enterpriseUrl = process.env.ENTERPRISE_URL || `http://${body.host}:${body.port || 3101}`;
381
+ const nodeId = body.name?.toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'worker-' + body.host.replace(/\./g, '-');
382
+
383
+ // Build remote setup script
384
+ const script = [
385
+ '#!/bin/bash',
386
+ 'set -e',
387
+ 'export NVM_DIR="$HOME/.nvm"',
388
+ '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
389
+ '',
390
+ '# Install Node.js if not present',
391
+ 'if ! command -v node &> /dev/null; then',
392
+ ' if command -v apt-get &> /dev/null; then',
393
+ ' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
394
+ ' sudo apt-get install -y nodejs',
395
+ ' elif command -v brew &> /dev/null; then',
396
+ ' brew install node',
397
+ ' fi',
398
+ 'fi',
399
+ '',
400
+ '# Install PM2 and AgenticMail',
401
+ 'npm install -g pm2 @agenticmail/enterprise',
402
+ '',
403
+ '# Write env file',
404
+ 'mkdir -p ~/.agenticmail',
405
+ `cat > ~/.agenticmail/worker.env << 'ENVEOF'`,
406
+ `ENTERPRISE_URL=${enterpriseUrl}`,
407
+ `WORKER_NODE_ID=${nodeId}`,
408
+ `WORKER_NAME="${body.name || nodeId}"`,
409
+ `DATABASE_URL=${dbUrl}`,
410
+ `PORT=${body.port || 3101}`,
411
+ 'LOG_LEVEL=warn',
412
+ 'ENVEOF',
413
+ '',
414
+ '# Start agents via PM2',
415
+ ...(body.agentIds || []).map((id: string, i: number) =>
416
+ `pm2 start "$(which agenticmail-enterprise || echo npx @agenticmail/enterprise) agent --id ${id}" --name "agent-${i}" --env ~/.agenticmail/worker.env`
417
+ ),
418
+ 'pm2 save',
419
+ 'echo "DEPLOY_SUCCESS"',
420
+ ].join('\n');
421
+
422
+ const { execSync } = await import('child_process');
423
+ const sshTarget = `${body.user || 'root'}@${body.host}`;
424
+ const keyOpt = body.privateKey ? `-i /tmp/_am_ssh_key_${Date.now()}` : '';
425
+
426
+ if (body.privateKey) {
427
+ const { writeFileSync, chmodSync } = await import('fs');
428
+ const keyPath = `/tmp/_am_ssh_key_${Date.now()}`;
429
+ writeFileSync(keyPath, body.privateKey, { mode: 0o600 });
430
+ }
431
+
432
+ const scriptB64 = Buffer.from(script).toString('base64');
433
+ const sshCmd = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${keyOpt} ${sshTarget} "echo '${scriptB64}' | base64 -d | bash"`;
434
+
435
+ // Run async — don't block the request
436
+ const { exec } = await import('child_process');
437
+ exec(sshCmd, { timeout: 120_000 }, (err, stdout, stderr) => {
438
+ if (err) {
439
+ console.warn(`[cluster] SSH deploy to ${body.host} failed:`, err.message);
440
+ } else if (stdout.includes('DEPLOY_SUCCESS')) {
441
+ console.log(`[cluster] SSH deploy to ${body.host} succeeded`);
442
+ }
443
+ });
444
+
445
+ return c.json({ message: 'Deployment started via SSH to ' + sshTarget + '. Check the Cluster page for the node to appear.', nodeId });
446
+ } catch (e: any) {
447
+ return c.json({ error: 'SSH deploy failed: ' + e.message }, 500);
448
+ }
449
+ });
450
+
451
+ engine.post('/cluster/nodes/:nodeId/restart', async (c) => {
452
+ const node = cluster.getNode(c.req.param('nodeId'));
453
+ if (!node) return c.json({ error: 'Node not found' }, 404);
454
+ try {
455
+ const ctrl = new AbortController();
456
+ setTimeout(() => ctrl.abort(), 10000);
457
+ await fetch(`${node.url}/restart`, { method: 'POST', signal: ctrl.signal }).catch(() => {});
458
+ return c.json({ ok: true, message: 'Restart signal sent to ' + node.name });
459
+ } catch (e: any) {
460
+ return c.json({ error: e.message }, 500);
461
+ }
462
+ });
463
+
346
464
  engine.get('/cluster/stream', (c) => {
347
465
  const stream = new ReadableStream({
348
466
  start(controller) {
@@ -1,52 +0,0 @@
1
- All done. Here's what I built:
2
-
3
- ### 1. Setup Wizard — Reverted (no LLM step)
4
-
5
- Reverted back to original. Instead, SDKs auto-install during bootstrap — when an admin creates the first account, the server installs
6
- @anthropic-ai/sdk, openai, and elevenlabs in the background.
7
-
8
- ### 2. Anthropic Token + API Key Support
9
-
10
- The LLM client now supports two auth modes:
11
-
12
- - api-key (default): Standard x-api-key header — for Anthropic API keys (sk-ant-...)
13
- - token: Authorization: Bearer header — for Anthropic Max subscription tokens, OAuth tokens, and proxy tokens (like OpenClaw uses)
14
-
15
- This flows through: ModelConfig.authMode → AgentConfig → agent-loop.ts → callLLM() → callAnthropic() → Anthropic SDK's authToken parameter.
16
-
17
- You can also set a custom baseUrl to point to any proxy endpoint.
18
-
19
- ### 3. Meeting Voice (ElevenLabs + BlackHole)
20
-
21
- Three new tools when meetings is enabled:
22
-
23
- - meeting_speak — Converts text to speech via ElevenLabs, plays through BlackHole virtual mic. Meeting participants hear the agent speak.
24
- - meeting_audio_setup — Checks if BlackHole, sox, and ElevenLabs are configured.
25
- - meeting_voices — Lists available voices (13 built-in + custom from ElevenLabs account).
26
-
27
- Architecture (works on any enterprise deployment):
28
-
29
- ```
30
- Agent LLM → "meeting_speak" tool → ElevenLabs API → MP3 file
31
- → sox plays to BlackHole virtual device
32
- → Browser picks up BlackHole as microphone
33
- → Meeting participants hear the agent
34
- ```
35
-
36
- To enable on Mac Mini, you need to run:
37
-
38
- ```bash
39
- brew install blackhole-2ch # Virtual audio driver (needs sudo)
40
- brew install sox # Audio routing to BlackHole
41
- ```
42
-
43
- Then set ELEVENLABS_API_KEY in .env.fola.
44
-
45
- ### 4. PM2 Fix
46
-
47
- Ran pm2 save after stopping Fola — this persists the stopped state to the PM2 dump file. The dashboard's stop button only updates DB state but
48
- doesn't call pm2 stop. That's a separate fix needed in the deployer (the local stop handler is a no-op).
49
- connected | idle
50
-
51
-
52
-