ultra-dex 3.2.0 → 3.3.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.
@@ -0,0 +1,780 @@
1
+ /**
2
+ * ultra-dex cloud command
3
+ * Hosted cloud dashboard for team collaboration and SaaS mode
4
+ * This enables Ultra-Dex to run as a service, not just a local CLI
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import http from 'http';
12
+ import { WebSocketServer } from 'ws';
13
+
14
+ // ============================================================================
15
+ // CLOUD CONFIGURATION
16
+ // ============================================================================
17
+
18
+ const CLOUD_CONFIG = {
19
+ // Default ports
20
+ ports: {
21
+ api: 4001,
22
+ websocket: 4002,
23
+ dashboard: 4003,
24
+ },
25
+
26
+ // State storage
27
+ stateDir: '.ultra-dex/cloud',
28
+ sessionsFile: '.ultra-dex/cloud/sessions.json',
29
+ teamsFile: '.ultra-dex/cloud/teams.json',
30
+
31
+ // Rate limits
32
+ rateLimit: {
33
+ requestsPerMinute: 60,
34
+ swarmRunsPerHour: 10,
35
+ },
36
+
37
+ // Session settings
38
+ sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours
39
+ };
40
+
41
+ // ============================================================================
42
+ // SESSION MANAGER
43
+ // ============================================================================
44
+
45
+ class SessionManager {
46
+ constructor() {
47
+ this.sessions = new Map();
48
+ this.teams = new Map();
49
+ }
50
+
51
+ async load() {
52
+ try {
53
+ const sessionsData = await fs.readFile(CLOUD_CONFIG.sessionsFile, 'utf8');
54
+ const sessions = JSON.parse(sessionsData);
55
+ for (const [id, session] of Object.entries(sessions)) {
56
+ this.sessions.set(id, session);
57
+ }
58
+ } catch {
59
+ // No existing sessions
60
+ }
61
+
62
+ try {
63
+ const teamsData = await fs.readFile(CLOUD_CONFIG.teamsFile, 'utf8');
64
+ const teams = JSON.parse(teamsData);
65
+ for (const [id, team] of Object.entries(teams)) {
66
+ this.teams.set(id, team);
67
+ }
68
+ } catch {
69
+ // No existing teams
70
+ }
71
+ }
72
+
73
+ async save() {
74
+ await fs.mkdir(CLOUD_CONFIG.stateDir, { recursive: true });
75
+
76
+ const sessions = Object.fromEntries(this.sessions);
77
+ await fs.writeFile(CLOUD_CONFIG.sessionsFile, JSON.stringify(sessions, null, 2));
78
+
79
+ const teams = Object.fromEntries(this.teams);
80
+ await fs.writeFile(CLOUD_CONFIG.teamsFile, JSON.stringify(teams, null, 2));
81
+ }
82
+
83
+ createSession(userId, teamId = null) {
84
+ const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
85
+ const session = {
86
+ id: sessionId,
87
+ userId,
88
+ teamId,
89
+ createdAt: new Date().toISOString(),
90
+ expiresAt: new Date(Date.now() + CLOUD_CONFIG.sessionTimeout).toISOString(),
91
+ state: {},
92
+ };
93
+
94
+ this.sessions.set(sessionId, session);
95
+ return session;
96
+ }
97
+
98
+ getSession(sessionId) {
99
+ const session = this.sessions.get(sessionId);
100
+ if (!session) return null;
101
+
102
+ // Check expiration
103
+ if (new Date(session.expiresAt) < new Date()) {
104
+ this.sessions.delete(sessionId);
105
+ return null;
106
+ }
107
+
108
+ return session;
109
+ }
110
+
111
+ createTeam(name, ownerId) {
112
+ const teamId = `team_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
113
+ const team = {
114
+ id: teamId,
115
+ name,
116
+ ownerId,
117
+ members: [ownerId],
118
+ createdAt: new Date().toISOString(),
119
+ projects: [],
120
+ settings: {},
121
+ };
122
+
123
+ this.teams.set(teamId, team);
124
+ return team;
125
+ }
126
+
127
+ addTeamMember(teamId, userId) {
128
+ const team = this.teams.get(teamId);
129
+ if (!team) return false;
130
+
131
+ if (!team.members.includes(userId)) {
132
+ team.members.push(userId);
133
+ }
134
+ return true;
135
+ }
136
+ }
137
+
138
+ const sessionManager = new SessionManager();
139
+
140
+ // ============================================================================
141
+ // API SERVER
142
+ // ============================================================================
143
+
144
+ function createAPIServer(options = {}) {
145
+ const { port = CLOUD_CONFIG.ports.api } = options;
146
+
147
+ const server = http.createServer(async (req, res) => {
148
+ // CORS headers
149
+ res.setHeader('Access-Control-Allow-Origin', '*');
150
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
151
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
152
+
153
+ if (req.method === 'OPTIONS') {
154
+ res.writeHead(204);
155
+ res.end();
156
+ return;
157
+ }
158
+
159
+ const url = new URL(req.url, `http://localhost:${port}`);
160
+ const path = url.pathname;
161
+
162
+ // Parse body for POST/PUT
163
+ let body = '';
164
+ if (req.method === 'POST' || req.method === 'PUT') {
165
+ for await (const chunk of req) {
166
+ body += chunk;
167
+ }
168
+ }
169
+
170
+ try {
171
+ // Route handling
172
+ if (path === '/api/health') {
173
+ return sendJSON(res, { status: 'ok', version: '3.3.0' });
174
+ }
175
+
176
+ if (path === '/api/session' && req.method === 'POST') {
177
+ const { userId, teamId } = JSON.parse(body);
178
+ const session = sessionManager.createSession(userId, teamId);
179
+ await sessionManager.save();
180
+ return sendJSON(res, session);
181
+ }
182
+
183
+ if (path === '/api/team' && req.method === 'POST') {
184
+ const { name, ownerId } = JSON.parse(body);
185
+ const team = sessionManager.createTeam(name, ownerId);
186
+ await sessionManager.save();
187
+ return sendJSON(res, team);
188
+ }
189
+
190
+ if (path === '/api/state' && req.method === 'GET') {
191
+ const sessionId = req.headers.authorization?.replace('Bearer ', '');
192
+ const session = sessionManager.getSession(sessionId);
193
+
194
+ if (!session) {
195
+ return sendJSON(res, { error: 'Invalid session' }, 401);
196
+ }
197
+
198
+ return sendJSON(res, { session, state: session.state });
199
+ }
200
+
201
+ if (path === '/api/state' && req.method === 'PUT') {
202
+ const sessionId = req.headers.authorization?.replace('Bearer ', '');
203
+ const session = sessionManager.getSession(sessionId);
204
+
205
+ if (!session) {
206
+ return sendJSON(res, { error: 'Invalid session' }, 401);
207
+ }
208
+
209
+ session.state = JSON.parse(body);
210
+ await sessionManager.save();
211
+ return sendJSON(res, { success: true });
212
+ }
213
+
214
+ if (path === '/api/swarm' && req.method === 'POST') {
215
+ const sessionId = req.headers.authorization?.replace('Bearer ', '');
216
+ const session = sessionManager.getSession(sessionId);
217
+
218
+ if (!session) {
219
+ return sendJSON(res, { error: 'Invalid session' }, 401);
220
+ }
221
+
222
+ const { task, options } = JSON.parse(body);
223
+
224
+ // Queue swarm run (in real implementation, would use job queue)
225
+ const runId = `run_${Date.now()}`;
226
+
227
+ return sendJSON(res, {
228
+ runId,
229
+ status: 'queued',
230
+ task,
231
+ message: 'Swarm run queued for execution',
232
+ });
233
+ }
234
+
235
+ // 404
236
+ sendJSON(res, { error: 'Not found' }, 404);
237
+
238
+ } catch (err) {
239
+ sendJSON(res, { error: err.message }, 500);
240
+ }
241
+ });
242
+
243
+ return server;
244
+ }
245
+
246
+ function sendJSON(res, data, statusCode = 200) {
247
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify(data));
249
+ }
250
+
251
+ // ============================================================================
252
+ // WEBSOCKET SERVER (Real-time sync)
253
+ // ============================================================================
254
+
255
+ function createWebSocketServer(options = {}) {
256
+ const { port = CLOUD_CONFIG.ports.websocket } = options;
257
+
258
+ const wss = new WebSocketServer({ port });
259
+
260
+ const clients = new Map(); // sessionId -> WebSocket
261
+
262
+ wss.on('connection', (ws) => {
263
+ let sessionId = null;
264
+
265
+ ws.on('message', async (data) => {
266
+ try {
267
+ const message = JSON.parse(data.toString());
268
+
269
+ if (message.type === 'auth') {
270
+ sessionId = message.sessionId;
271
+ const session = sessionManager.getSession(sessionId);
272
+
273
+ if (session) {
274
+ clients.set(sessionId, ws);
275
+ ws.send(JSON.stringify({ type: 'auth_success', session }));
276
+ } else {
277
+ ws.send(JSON.stringify({ type: 'auth_failed' }));
278
+ }
279
+ }
280
+
281
+ if (message.type === 'state_update' && sessionId) {
282
+ const session = sessionManager.getSession(sessionId);
283
+ if (session) {
284
+ session.state = { ...session.state, ...message.data };
285
+ await sessionManager.save();
286
+
287
+ // Broadcast to team members
288
+ if (session.teamId) {
289
+ const team = sessionManager.teams.get(session.teamId);
290
+ if (team) {
291
+ for (const [sid, client] of clients) {
292
+ const clientSession = sessionManager.getSession(sid);
293
+ if (clientSession?.teamId === session.teamId && sid !== sessionId) {
294
+ client.send(JSON.stringify({
295
+ type: 'state_sync',
296
+ from: sessionId,
297
+ data: message.data,
298
+ }));
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ if (message.type === 'swarm_event' && sessionId) {
307
+ // Broadcast swarm events to team
308
+ const session = sessionManager.getSession(sessionId);
309
+ if (session?.teamId) {
310
+ for (const [sid, client] of clients) {
311
+ const clientSession = sessionManager.getSession(sid);
312
+ if (clientSession?.teamId === session.teamId) {
313
+ client.send(JSON.stringify({
314
+ type: 'swarm_event',
315
+ from: sessionId,
316
+ event: message.event,
317
+ }));
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ } catch (err) {
324
+ ws.send(JSON.stringify({ type: 'error', message: err.message }));
325
+ }
326
+ });
327
+
328
+ ws.on('close', () => {
329
+ if (sessionId) {
330
+ clients.delete(sessionId);
331
+ }
332
+ });
333
+ });
334
+
335
+ return wss;
336
+ }
337
+
338
+ // ============================================================================
339
+ // CLOUD DASHBOARD HTML
340
+ // ============================================================================
341
+
342
+ const CLOUD_DASHBOARD_HTML = `<!DOCTYPE html>
343
+ <html lang="en">
344
+ <head>
345
+ <meta charset="UTF-8">
346
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
347
+ <title>Ultra-Dex Cloud Dashboard</title>
348
+ <style>
349
+ :root {
350
+ --bg: #0f0f23;
351
+ --surface: #1a1a2e;
352
+ --primary: #6366f1;
353
+ --secondary: #8b5cf6;
354
+ --text: #e2e8f0;
355
+ --muted: #64748b;
356
+ --success: #22c55e;
357
+ --warning: #f59e0b;
358
+ --error: #ef4444;
359
+ }
360
+
361
+ * { box-sizing: border-box; margin: 0; padding: 0; }
362
+
363
+ body {
364
+ font-family: 'SF Mono', 'Fira Code', monospace;
365
+ background: var(--bg);
366
+ color: var(--text);
367
+ min-height: 100vh;
368
+ }
369
+
370
+ .container {
371
+ max-width: 1400px;
372
+ margin: 0 auto;
373
+ padding: 2rem;
374
+ }
375
+
376
+ header {
377
+ display: flex;
378
+ justify-content: space-between;
379
+ align-items: center;
380
+ margin-bottom: 2rem;
381
+ padding-bottom: 1rem;
382
+ border-bottom: 1px solid var(--surface);
383
+ }
384
+
385
+ h1 {
386
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
387
+ -webkit-background-clip: text;
388
+ -webkit-text-fill-color: transparent;
389
+ font-size: 2rem;
390
+ }
391
+
392
+ .status-badge {
393
+ display: inline-flex;
394
+ align-items: center;
395
+ gap: 0.5rem;
396
+ padding: 0.5rem 1rem;
397
+ background: var(--surface);
398
+ border-radius: 9999px;
399
+ font-size: 0.875rem;
400
+ }
401
+
402
+ .status-dot {
403
+ width: 8px;
404
+ height: 8px;
405
+ border-radius: 50%;
406
+ background: var(--success);
407
+ animation: pulse 2s infinite;
408
+ }
409
+
410
+ @keyframes pulse {
411
+ 0%, 100% { opacity: 1; }
412
+ 50% { opacity: 0.5; }
413
+ }
414
+
415
+ .grid {
416
+ display: grid;
417
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
418
+ gap: 1.5rem;
419
+ margin-bottom: 2rem;
420
+ }
421
+
422
+ .card {
423
+ background: var(--surface);
424
+ border-radius: 12px;
425
+ padding: 1.5rem;
426
+ border: 1px solid rgba(255,255,255,0.05);
427
+ }
428
+
429
+ .card h2 {
430
+ font-size: 1rem;
431
+ color: var(--muted);
432
+ margin-bottom: 1rem;
433
+ text-transform: uppercase;
434
+ letter-spacing: 0.05em;
435
+ }
436
+
437
+ .metric {
438
+ font-size: 2.5rem;
439
+ font-weight: bold;
440
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
441
+ -webkit-background-clip: text;
442
+ -webkit-text-fill-color: transparent;
443
+ }
444
+
445
+ .agent-list {
446
+ display: flex;
447
+ flex-direction: column;
448
+ gap: 0.75rem;
449
+ }
450
+
451
+ .agent-item {
452
+ display: flex;
453
+ justify-content: space-between;
454
+ align-items: center;
455
+ padding: 0.75rem;
456
+ background: rgba(255,255,255,0.02);
457
+ border-radius: 8px;
458
+ }
459
+
460
+ .agent-name {
461
+ font-weight: 600;
462
+ }
463
+
464
+ .agent-status {
465
+ font-size: 0.75rem;
466
+ padding: 0.25rem 0.5rem;
467
+ border-radius: 4px;
468
+ }
469
+
470
+ .agent-status.idle { background: var(--muted); }
471
+ .agent-status.running { background: var(--primary); }
472
+ .agent-status.complete { background: var(--success); }
473
+
474
+ .timeline {
475
+ display: flex;
476
+ flex-direction: column;
477
+ gap: 1rem;
478
+ }
479
+
480
+ .timeline-item {
481
+ display: flex;
482
+ gap: 1rem;
483
+ padding-left: 1rem;
484
+ border-left: 2px solid var(--primary);
485
+ }
486
+
487
+ .timeline-time {
488
+ color: var(--muted);
489
+ font-size: 0.75rem;
490
+ min-width: 60px;
491
+ }
492
+
493
+ .timeline-content {
494
+ flex: 1;
495
+ }
496
+
497
+ .btn {
498
+ display: inline-flex;
499
+ align-items: center;
500
+ gap: 0.5rem;
501
+ padding: 0.75rem 1.5rem;
502
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
503
+ color: white;
504
+ border: none;
505
+ border-radius: 8px;
506
+ font-family: inherit;
507
+ font-size: 0.875rem;
508
+ cursor: pointer;
509
+ transition: transform 0.2s, box-shadow 0.2s;
510
+ }
511
+
512
+ .btn:hover {
513
+ transform: translateY(-2px);
514
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
515
+ }
516
+
517
+ .team-section {
518
+ margin-top: 2rem;
519
+ }
520
+
521
+ .team-members {
522
+ display: flex;
523
+ gap: 0.5rem;
524
+ flex-wrap: wrap;
525
+ }
526
+
527
+ .member-avatar {
528
+ width: 40px;
529
+ height: 40px;
530
+ border-radius: 50%;
531
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: center;
535
+ font-weight: bold;
536
+ font-size: 0.875rem;
537
+ }
538
+
539
+ footer {
540
+ text-align: center;
541
+ padding-top: 2rem;
542
+ color: var(--muted);
543
+ font-size: 0.875rem;
544
+ }
545
+ </style>
546
+ </head>
547
+ <body>
548
+ <div class="container">
549
+ <header>
550
+ <h1>Ultra-Dex Cloud</h1>
551
+ <div class="status-badge">
552
+ <span class="status-dot"></span>
553
+ <span>Connected</span>
554
+ </div>
555
+ </header>
556
+
557
+ <div class="grid">
558
+ <div class="card">
559
+ <h2>Active Swarms</h2>
560
+ <div class="metric" id="activeSwarms">0</div>
561
+ </div>
562
+ <div class="card">
563
+ <h2>Tasks Completed</h2>
564
+ <div class="metric" id="tasksCompleted">0</div>
565
+ </div>
566
+ <div class="card">
567
+ <h2>Team Members</h2>
568
+ <div class="metric" id="teamMembers">1</div>
569
+ </div>
570
+ <div class="card">
571
+ <h2>API Calls Today</h2>
572
+ <div class="metric" id="apiCalls">0</div>
573
+ </div>
574
+ </div>
575
+
576
+ <div class="grid">
577
+ <div class="card">
578
+ <h2>Agent Status</h2>
579
+ <div class="agent-list" id="agentList">
580
+ <div class="agent-item">
581
+ <span class="agent-name">@Planner</span>
582
+ <span class="agent-status idle">Idle</span>
583
+ </div>
584
+ <div class="agent-item">
585
+ <span class="agent-name">@Backend</span>
586
+ <span class="agent-status idle">Idle</span>
587
+ </div>
588
+ <div class="agent-item">
589
+ <span class="agent-name">@Frontend</span>
590
+ <span class="agent-status idle">Idle</span>
591
+ </div>
592
+ <div class="agent-item">
593
+ <span class="agent-name">@Reviewer</span>
594
+ <span class="agent-status idle">Idle</span>
595
+ </div>
596
+ </div>
597
+ </div>
598
+
599
+ <div class="card">
600
+ <h2>Recent Activity</h2>
601
+ <div class="timeline" id="timeline">
602
+ <div class="timeline-item">
603
+ <span class="timeline-time">Now</span>
604
+ <span class="timeline-content">Cloud dashboard started</span>
605
+ </div>
606
+ </div>
607
+ </div>
608
+ </div>
609
+
610
+ <div class="team-section card">
611
+ <h2>Team Collaboration</h2>
612
+ <p style="color: var(--muted); margin-bottom: 1rem;">
613
+ Invite team members to collaborate in real-time on agent swarms.
614
+ </p>
615
+ <div class="team-members" id="teamMembers">
616
+ <div class="member-avatar">You</div>
617
+ </div>
618
+ <button class="btn" style="margin-top: 1rem;">
619
+ + Invite Member
620
+ </button>
621
+ </div>
622
+
623
+ <footer>
624
+ Ultra-Dex v3.3.0 | Cloud Dashboard | <a href="https://github.com/Srujan0798/Ultra-Dex" style="color: var(--primary);">GitHub</a>
625
+ </footer>
626
+ </div>
627
+
628
+ <script>
629
+ // WebSocket connection for real-time updates
630
+ let ws;
631
+
632
+ function connect() {
633
+ ws = new WebSocket('ws://localhost:4002');
634
+
635
+ ws.onopen = () => {
636
+ console.log('Connected to Ultra-Dex Cloud');
637
+ addTimelineItem('Connected to cloud server');
638
+ };
639
+
640
+ ws.onmessage = (event) => {
641
+ const data = JSON.parse(event.data);
642
+ handleMessage(data);
643
+ };
644
+
645
+ ws.onclose = () => {
646
+ console.log('Disconnected, reconnecting...');
647
+ setTimeout(connect, 3000);
648
+ };
649
+ }
650
+
651
+ function handleMessage(data) {
652
+ if (data.type === 'state_sync') {
653
+ addTimelineItem('State synced from ' + data.from);
654
+ }
655
+
656
+ if (data.type === 'swarm_event') {
657
+ updateAgentStatus(data.event.agent, data.event.status);
658
+ addTimelineItem(data.event.agent + ': ' + data.event.message);
659
+ }
660
+ }
661
+
662
+ function updateAgentStatus(agent, status) {
663
+ const agentList = document.getElementById('agentList');
664
+ const items = agentList.querySelectorAll('.agent-item');
665
+
666
+ items.forEach(item => {
667
+ const name = item.querySelector('.agent-name').textContent;
668
+ if (name === agent) {
669
+ const statusEl = item.querySelector('.agent-status');
670
+ statusEl.textContent = status;
671
+ statusEl.className = 'agent-status ' + status.toLowerCase();
672
+ }
673
+ });
674
+ }
675
+
676
+ function addTimelineItem(content) {
677
+ const timeline = document.getElementById('timeline');
678
+ const item = document.createElement('div');
679
+ item.className = 'timeline-item';
680
+ item.innerHTML = '<span class="timeline-time">' + new Date().toLocaleTimeString() + '</span>' +
681
+ '<span class="timeline-content">' + content + '</span>';
682
+ timeline.insertBefore(item, timeline.firstChild);
683
+
684
+ // Keep only last 10 items
685
+ while (timeline.children.length > 10) {
686
+ timeline.removeChild(timeline.lastChild);
687
+ }
688
+ }
689
+
690
+ // Initialize
691
+ connect();
692
+ </script>
693
+ </body>
694
+ </html>`;
695
+
696
+ // ============================================================================
697
+ // DASHBOARD SERVER
698
+ // ============================================================================
699
+
700
+ function createDashboardServer(options = {}) {
701
+ const { port = CLOUD_CONFIG.ports.dashboard } = options;
702
+
703
+ const server = http.createServer((req, res) => {
704
+ res.writeHead(200, { 'Content-Type': 'text/html' });
705
+ res.end(CLOUD_DASHBOARD_HTML);
706
+ });
707
+
708
+ return server;
709
+ }
710
+
711
+ // ============================================================================
712
+ // CLI COMMAND
713
+ // ============================================================================
714
+
715
+ export function registerCloudCommand(program) {
716
+ program
717
+ .command('cloud')
718
+ .description('Start Ultra-Dex cloud server for team collaboration')
719
+ .option('-p, --port <port>', 'API port', '4001')
720
+ .option('--ws-port <port>', 'WebSocket port', '4002')
721
+ .option('--dashboard-port <port>', 'Dashboard port', '4003')
722
+ .option('--no-dashboard', 'Disable dashboard server')
723
+ .action(async (options) => {
724
+ console.log(chalk.cyan('\n☁️ Ultra-Dex Cloud Server\n'));
725
+
726
+ const spinner = ora('Starting cloud services...').start();
727
+
728
+ try {
729
+ // Load existing sessions
730
+ await sessionManager.load();
731
+
732
+ // Start API server
733
+ const apiPort = parseInt(options.port, 10);
734
+ const apiServer = createAPIServer({ port: apiPort });
735
+ apiServer.listen(apiPort);
736
+
737
+ // Start WebSocket server
738
+ const wsPort = parseInt(options.wsPort, 10);
739
+ createWebSocketServer({ port: wsPort });
740
+
741
+ // Start dashboard server
742
+ let dashboardPort = null;
743
+ if (options.dashboard !== false) {
744
+ dashboardPort = parseInt(options.dashboardPort, 10);
745
+ const dashboardServer = createDashboardServer({ port: dashboardPort });
746
+ dashboardServer.listen(dashboardPort);
747
+ }
748
+
749
+ spinner.succeed('Cloud services started');
750
+
751
+ console.log(chalk.bold('\n📡 Endpoints:'));
752
+ console.log(` API: ${chalk.cyan(`http://localhost:${apiPort}`)}`);
753
+ console.log(` WebSocket: ${chalk.cyan(`ws://localhost:${wsPort}`)}`);
754
+ if (dashboardPort) {
755
+ console.log(` Dashboard: ${chalk.cyan(`http://localhost:${dashboardPort}`)}`);
756
+ }
757
+
758
+ console.log(chalk.gray('\n✨ Cloud server running. Press Ctrl+C to stop.\n'));
759
+
760
+ // Keep process running
761
+ process.on('SIGINT', async () => {
762
+ console.log(chalk.yellow('\n\nShutting down...'));
763
+ await sessionManager.save();
764
+ process.exit(0);
765
+ });
766
+
767
+ } catch (err) {
768
+ spinner.fail(`Failed to start: ${err.message}`);
769
+ }
770
+ });
771
+ }
772
+
773
+ export default {
774
+ registerCloudCommand,
775
+ createAPIServer,
776
+ createWebSocketServer,
777
+ createDashboardServer,
778
+ sessionManager,
779
+ CLOUD_CONFIG,
780
+ };