natureco-cli 1.0.14 → 1.0.16

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,687 @@
1
+ const chalk = require('chalk');
2
+ const http = require('http');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { exec } = require('child_process');
6
+ const { getConfig, CONFIG_DIR } = require('../utils/config');
7
+
8
+ const PORT = 3848;
9
+ const PID_FILE = path.join(CONFIG_DIR, 'dashboard.pid');
10
+
11
+ async function dashboard(action) {
12
+ if (!action || action === 'start') {
13
+ return startDashboard();
14
+ }
15
+
16
+ if (action === 'stop') {
17
+ return stopDashboard();
18
+ }
19
+
20
+ if (action === 'status') {
21
+ return statusDashboard();
22
+ }
23
+
24
+ console.log(chalk.red('\n❌ Unknown action\n'));
25
+ console.log(chalk.gray('Available actions: start, stop, status\n'));
26
+ process.exit(1);
27
+ }
28
+
29
+ function startDashboard() {
30
+ // Check if already running
31
+ if (fs.existsSync(PID_FILE)) {
32
+ const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
33
+ try {
34
+ process.kill(pid, 0);
35
+ console.log(chalk.yellow('\n⚠️ Dashboard is already running\n'));
36
+ console.log(chalk.cyan('URL:'), chalk.white(`http://localhost:${PORT}`));
37
+ console.log(chalk.gray('\nStop with: natureco dashboard stop\n'));
38
+ return;
39
+ } catch {
40
+ // Process not running, remove stale PID file
41
+ fs.unlinkSync(PID_FILE);
42
+ }
43
+ }
44
+
45
+ const config = getConfig();
46
+
47
+ if (!config.apiKey) {
48
+ console.log(chalk.red('\n❌ Not logged in. Run "natureco login" first.\n'));
49
+ process.exit(1);
50
+ }
51
+
52
+ console.log(chalk.cyan('🚀 Starting NatureCo Dashboard...\n'));
53
+
54
+ const server = http.createServer((req, res) => {
55
+ // CORS headers
56
+ res.setHeader('Access-Control-Allow-Origin', '*');
57
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
58
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
59
+
60
+ if (req.method === 'OPTIONS') {
61
+ res.writeHead(200);
62
+ res.end();
63
+ return;
64
+ }
65
+
66
+ if (req.url === '/' || req.url === '/index.html') {
67
+ res.writeHead(200, { 'Content-Type': 'text/html' });
68
+ res.end(getHTML(config));
69
+ } else if (req.url === '/config') {
70
+ res.writeHead(200, { 'Content-Type': 'application/json' });
71
+ res.end(JSON.stringify({
72
+ apiKey: config.apiKey,
73
+ defaultBot: config.defaultBot,
74
+ defaultBotId: config.defaultBotId,
75
+ }));
76
+ } else {
77
+ res.writeHead(404);
78
+ res.end('Not Found');
79
+ }
80
+ });
81
+
82
+ server.listen(PORT, () => {
83
+ // Save PID
84
+ fs.writeFileSync(PID_FILE, process.pid.toString());
85
+
86
+ console.log(chalk.green('✅ Dashboard started!\n'));
87
+ console.log(chalk.cyan('URL:'), chalk.white(`http://localhost:${PORT}`));
88
+ console.log(chalk.gray('\nOpening browser...\n'));
89
+
90
+ // Open browser
91
+ const url = `http://localhost:${PORT}`;
92
+ const platform = process.platform;
93
+
94
+ if (platform === 'darwin') {
95
+ exec(`open ${url}`);
96
+ } else if (platform === 'win32') {
97
+ exec(`start ${url}`);
98
+ } else {
99
+ exec(`xdg-open ${url}`);
100
+ }
101
+
102
+ console.log(chalk.gray('Press Ctrl+C to stop the server\n'));
103
+ });
104
+
105
+ // Handle shutdown
106
+ process.on('SIGINT', () => {
107
+ console.log(chalk.yellow('\n\n⏳ Shutting down dashboard...\n'));
108
+ server.close(() => {
109
+ if (fs.existsSync(PID_FILE)) {
110
+ fs.unlinkSync(PID_FILE);
111
+ }
112
+ console.log(chalk.green('✅ Dashboard stopped\n'));
113
+ process.exit(0);
114
+ });
115
+ });
116
+ }
117
+
118
+ function stopDashboard() {
119
+ if (!fs.existsSync(PID_FILE)) {
120
+ console.log(chalk.gray('\n⚠️ Dashboard is not running\n'));
121
+ return;
122
+ }
123
+
124
+ const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
125
+
126
+ try {
127
+ process.kill(pid, 'SIGTERM');
128
+ fs.unlinkSync(PID_FILE);
129
+ console.log(chalk.green('\n✅ Dashboard stopped\n'));
130
+ } catch (err) {
131
+ console.log(chalk.red(`\n❌ Failed to stop dashboard: ${err.message}\n`));
132
+ // Remove stale PID file
133
+ if (fs.existsSync(PID_FILE)) {
134
+ fs.unlinkSync(PID_FILE);
135
+ }
136
+ }
137
+ }
138
+
139
+ function statusDashboard() {
140
+ if (!fs.existsSync(PID_FILE)) {
141
+ console.log(chalk.gray('\n⚠️ Dashboard is not running\n'));
142
+ console.log(chalk.gray('Start with: natureco dashboard start\n'));
143
+ return;
144
+ }
145
+
146
+ const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
147
+
148
+ try {
149
+ process.kill(pid, 0);
150
+ console.log(chalk.green('\n✅ Dashboard is running\n'));
151
+ console.log(chalk.cyan('PID:'), chalk.white(pid));
152
+ console.log(chalk.cyan('URL:'), chalk.white(`http://localhost:${PORT}`));
153
+ console.log(chalk.gray('\nStop with: natureco dashboard stop\n'));
154
+ } catch {
155
+ console.log(chalk.gray('\n⚠️ Dashboard is not running (stale PID file)\n'));
156
+ fs.unlinkSync(PID_FILE);
157
+ }
158
+ }
159
+
160
+ function getHTML(config) {
161
+ return `<!DOCTYPE html>
162
+ <html lang="en">
163
+ <head>
164
+ <meta charset="UTF-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title>NatureCo Dashboard</title>
167
+ <style>
168
+ * {
169
+ margin: 0;
170
+ padding: 0;
171
+ box-sizing: border-box;
172
+ }
173
+
174
+ body {
175
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
176
+ background: #0d1117;
177
+ color: #c9d1d9;
178
+ height: 100vh;
179
+ overflow: hidden;
180
+ }
181
+
182
+ .container {
183
+ display: flex;
184
+ height: 100vh;
185
+ }
186
+
187
+ .sidebar {
188
+ width: 280px;
189
+ background: #161b22;
190
+ border-right: 1px solid #30363d;
191
+ display: flex;
192
+ flex-direction: column;
193
+ }
194
+
195
+ .sidebar-header {
196
+ padding: 20px;
197
+ border-bottom: 1px solid #30363d;
198
+ }
199
+
200
+ .logo {
201
+ font-size: 20px;
202
+ font-weight: bold;
203
+ color: #00E676;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ }
208
+
209
+ .sidebar-content {
210
+ flex: 1;
211
+ overflow-y: auto;
212
+ padding: 16px;
213
+ }
214
+
215
+ .section {
216
+ margin-bottom: 24px;
217
+ }
218
+
219
+ .section-title {
220
+ font-size: 12px;
221
+ font-weight: 600;
222
+ color: #8b949e;
223
+ text-transform: uppercase;
224
+ margin-bottom: 8px;
225
+ letter-spacing: 0.5px;
226
+ }
227
+
228
+ .bot-item, .item {
229
+ padding: 10px 12px;
230
+ background: #0d1117;
231
+ border: 1px solid #30363d;
232
+ border-radius: 6px;
233
+ margin-bottom: 8px;
234
+ cursor: pointer;
235
+ transition: all 0.2s;
236
+ }
237
+
238
+ .bot-item:hover, .item:hover {
239
+ background: #161b22;
240
+ border-color: #00E676;
241
+ }
242
+
243
+ .bot-item.active {
244
+ background: #00E676;
245
+ color: #0d1117;
246
+ border-color: #00E676;
247
+ }
248
+
249
+ .bot-name {
250
+ font-weight: 600;
251
+ font-size: 14px;
252
+ }
253
+
254
+ .bot-id {
255
+ font-size: 11px;
256
+ color: #8b949e;
257
+ margin-top: 4px;
258
+ }
259
+
260
+ .bot-item.active .bot-id {
261
+ color: #0d1117;
262
+ opacity: 0.7;
263
+ }
264
+
265
+ .main {
266
+ flex: 1;
267
+ display: flex;
268
+ flex-direction: column;
269
+ }
270
+
271
+ .header {
272
+ padding: 20px 24px;
273
+ border-bottom: 1px solid #30363d;
274
+ background: #161b22;
275
+ }
276
+
277
+ .header-title {
278
+ font-size: 18px;
279
+ font-weight: 600;
280
+ color: #00E676;
281
+ }
282
+
283
+ .chat-container {
284
+ flex: 1;
285
+ display: flex;
286
+ flex-direction: column;
287
+ overflow: hidden;
288
+ }
289
+
290
+ .messages {
291
+ flex: 1;
292
+ overflow-y: auto;
293
+ padding: 24px;
294
+ }
295
+
296
+ .message {
297
+ margin-bottom: 16px;
298
+ display: flex;
299
+ gap: 12px;
300
+ }
301
+
302
+ .message-avatar {
303
+ width: 32px;
304
+ height: 32px;
305
+ border-radius: 50%;
306
+ background: #00E676;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ font-weight: bold;
311
+ color: #0d1117;
312
+ flex-shrink: 0;
313
+ }
314
+
315
+ .message-content {
316
+ flex: 1;
317
+ }
318
+
319
+ .message-author {
320
+ font-weight: 600;
321
+ font-size: 14px;
322
+ margin-bottom: 4px;
323
+ }
324
+
325
+ .message-text {
326
+ font-size: 14px;
327
+ line-height: 1.6;
328
+ white-space: pre-wrap;
329
+ }
330
+
331
+ .message.user .message-avatar {
332
+ background: #58a6ff;
333
+ }
334
+
335
+ .input-container {
336
+ padding: 16px 24px;
337
+ border-top: 1px solid #30363d;
338
+ background: #161b22;
339
+ }
340
+
341
+ .input-wrapper {
342
+ display: flex;
343
+ gap: 12px;
344
+ }
345
+
346
+ .input {
347
+ flex: 1;
348
+ padding: 12px 16px;
349
+ background: #0d1117;
350
+ border: 1px solid #30363d;
351
+ border-radius: 6px;
352
+ color: #c9d1d9;
353
+ font-size: 14px;
354
+ font-family: inherit;
355
+ outline: none;
356
+ transition: border-color 0.2s;
357
+ }
358
+
359
+ .input:focus {
360
+ border-color: #00E676;
361
+ }
362
+
363
+ .send-btn {
364
+ padding: 12px 24px;
365
+ background: #00E676;
366
+ color: #0d1117;
367
+ border: none;
368
+ border-radius: 6px;
369
+ font-weight: 600;
370
+ cursor: pointer;
371
+ transition: all 0.2s;
372
+ font-size: 14px;
373
+ }
374
+
375
+ .send-btn:hover {
376
+ background: #00c853;
377
+ }
378
+
379
+ .send-btn:disabled {
380
+ opacity: 0.5;
381
+ cursor: not-allowed;
382
+ }
383
+
384
+ .loading {
385
+ display: inline-block;
386
+ width: 16px;
387
+ height: 16px;
388
+ border: 2px solid #30363d;
389
+ border-top-color: #00E676;
390
+ border-radius: 50%;
391
+ animation: spin 0.8s linear infinite;
392
+ }
393
+
394
+ @keyframes spin {
395
+ to { transform: rotate(360deg); }
396
+ }
397
+
398
+ .empty-state {
399
+ display: flex;
400
+ flex-direction: column;
401
+ align-items: center;
402
+ justify-content: center;
403
+ height: 100%;
404
+ color: #8b949e;
405
+ text-align: center;
406
+ padding: 24px;
407
+ }
408
+
409
+ .empty-icon {
410
+ font-size: 48px;
411
+ margin-bottom: 16px;
412
+ }
413
+
414
+ .empty-title {
415
+ font-size: 18px;
416
+ font-weight: 600;
417
+ margin-bottom: 8px;
418
+ color: #c9d1d9;
419
+ }
420
+
421
+ .empty-text {
422
+ font-size: 14px;
423
+ }
424
+
425
+ .stat {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 8px;
429
+ padding: 8px 12px;
430
+ background: #0d1117;
431
+ border: 1px solid #30363d;
432
+ border-radius: 6px;
433
+ font-size: 13px;
434
+ margin-bottom: 8px;
435
+ }
436
+
437
+ .stat-icon {
438
+ color: #00E676;
439
+ }
440
+
441
+ ::-webkit-scrollbar {
442
+ width: 8px;
443
+ }
444
+
445
+ ::-webkit-scrollbar-track {
446
+ background: #0d1117;
447
+ }
448
+
449
+ ::-webkit-scrollbar-thumb {
450
+ background: #30363d;
451
+ border-radius: 4px;
452
+ }
453
+
454
+ ::-webkit-scrollbar-thumb:hover {
455
+ background: #484f58;
456
+ }
457
+ </style>
458
+ </head>
459
+ <body>
460
+ <div class="container">
461
+ <div class="sidebar">
462
+ <div class="sidebar-header">
463
+ <div class="logo">
464
+ 🌿 NatureCo
465
+ </div>
466
+ </div>
467
+ <div class="sidebar-content">
468
+ <div class="section">
469
+ <div class="section-title">Bots</div>
470
+ <div id="bots-list">
471
+ <div class="loading"></div>
472
+ </div>
473
+ </div>
474
+ <div class="section">
475
+ <div class="section-title">Stats</div>
476
+ <div class="stat">
477
+ <span class="stat-icon">✨</span>
478
+ <span id="skills-count">Skills: 0</span>
479
+ </div>
480
+ <div class="stat">
481
+ <span class="stat-icon">🔌</span>
482
+ <span id="mcp-count">MCP: 0</span>
483
+ </div>
484
+ <div class="stat">
485
+ <span class="stat-icon">💬</span>
486
+ <span id="sessions-count">Sessions: 0</span>
487
+ </div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
492
+ <div class="main">
493
+ <div class="header">
494
+ <div class="header-title" id="current-bot">Select a bot</div>
495
+ </div>
496
+
497
+ <div class="chat-container">
498
+ <div class="messages" id="messages">
499
+ <div class="empty-state">
500
+ <div class="empty-icon">💬</div>
501
+ <div class="empty-title">Start a conversation</div>
502
+ <div class="empty-text">Select a bot and send a message</div>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="input-container">
507
+ <div class="input-wrapper">
508
+ <input
509
+ type="text"
510
+ class="input"
511
+ id="message-input"
512
+ placeholder="Type your message..."
513
+ disabled
514
+ />
515
+ <button class="send-btn" id="send-btn" disabled>Send</button>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ <script>
523
+ const API_BASE = 'https://api.natureco.me';
524
+ let apiKey = '';
525
+ let currentBotId = null;
526
+ let currentBotName = '';
527
+ let sessionId = null;
528
+
529
+ // Load config
530
+ fetch('/config')
531
+ .then(r => r.json())
532
+ .then(config => {
533
+ apiKey = config.apiKey;
534
+ loadBots();
535
+ loadStats();
536
+ });
537
+
538
+ async function loadBots() {
539
+ try {
540
+ const response = await fetch(API_BASE + '/api/v1/bots', {
541
+ headers: {
542
+ 'Authorization': 'Bearer ' + apiKey,
543
+ },
544
+ });
545
+
546
+ const data = await response.json();
547
+ const botsList = document.getElementById('bots-list');
548
+
549
+ if (data.bots && data.bots.length > 0) {
550
+ botsList.innerHTML = data.bots.map(bot => \`
551
+ <div class="bot-item" onclick="selectBot('\${bot.id}', '\${bot.name}')">
552
+ <div class="bot-name">\${bot.name}</div>
553
+ <div class="bot-id">\${bot.id.slice(0, 8)}...</div>
554
+ </div>
555
+ \`).join('');
556
+ } else {
557
+ botsList.innerHTML = '<div class="empty-text">No bots found</div>';
558
+ }
559
+ } catch (err) {
560
+ console.error('Failed to load bots:', err);
561
+ }
562
+ }
563
+
564
+ function selectBot(botId, botName) {
565
+ currentBotId = botId;
566
+ currentBotName = botName;
567
+ sessionId = null;
568
+
569
+ // Update UI
570
+ document.getElementById('current-bot').textContent = botName;
571
+ document.getElementById('message-input').disabled = false;
572
+ document.getElementById('send-btn').disabled = false;
573
+
574
+ // Clear messages
575
+ document.getElementById('messages').innerHTML = \`
576
+ <div class="empty-state">
577
+ <div class="empty-icon">💬</div>
578
+ <div class="empty-title">Start a conversation with \${botName}</div>
579
+ <div class="empty-text">Type a message below</div>
580
+ </div>
581
+ \`;
582
+
583
+ // Update active state
584
+ document.querySelectorAll('.bot-item').forEach(item => {
585
+ item.classList.remove('active');
586
+ });
587
+ event.target.closest('.bot-item').classList.add('active');
588
+ }
589
+
590
+ async function sendMessage() {
591
+ const input = document.getElementById('message-input');
592
+ const message = input.value.trim();
593
+
594
+ if (!message || !currentBotId) return;
595
+
596
+ // Add user message
597
+ addMessage('user', 'You', message);
598
+ input.value = '';
599
+
600
+ // Disable input
601
+ input.disabled = true;
602
+ document.getElementById('send-btn').disabled = true;
603
+
604
+ try {
605
+ const response = await fetch(API_BASE + '/api/v1/chat', {
606
+ method: 'POST',
607
+ headers: {
608
+ 'Authorization': 'Bearer ' + apiKey,
609
+ 'Content-Type': 'application/json',
610
+ },
611
+ body: JSON.stringify({
612
+ bot_id: currentBotId,
613
+ message: message,
614
+ session_id: sessionId,
615
+ }),
616
+ });
617
+
618
+ const data = await response.json();
619
+
620
+ if (data.reply) {
621
+ addMessage('bot', currentBotName, data.reply);
622
+ sessionId = data.session_id;
623
+ }
624
+ } catch (err) {
625
+ addMessage('system', 'System', 'Error: ' + err.message);
626
+ } finally {
627
+ input.disabled = false;
628
+ document.getElementById('send-btn').disabled = false;
629
+ input.focus();
630
+ }
631
+ }
632
+
633
+ function addMessage(type, author, text) {
634
+ const messagesDiv = document.getElementById('messages');
635
+
636
+ // Remove empty state
637
+ const emptyState = messagesDiv.querySelector('.empty-state');
638
+ if (emptyState) {
639
+ emptyState.remove();
640
+ }
641
+
642
+ const messageDiv = document.createElement('div');
643
+ messageDiv.className = 'message ' + type;
644
+
645
+ const avatar = type === 'user' ? 'U' : type === 'bot' ? '🤖' : 'ℹ️';
646
+
647
+ messageDiv.innerHTML = \`
648
+ <div class="message-avatar">\${avatar}</div>
649
+ <div class="message-content">
650
+ <div class="message-author">\${author}</div>
651
+ <div class="message-text">\${text}</div>
652
+ </div>
653
+ \`;
654
+
655
+ messagesDiv.appendChild(messageDiv);
656
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
657
+ }
658
+
659
+ async function loadStats() {
660
+ // Load skills count
661
+ try {
662
+ const config = await fetch('/config').then(r => r.json());
663
+ // This would need to be implemented properly
664
+ document.getElementById('skills-count').textContent = 'Skills: 0';
665
+ document.getElementById('mcp-count').textContent = 'MCP: 0';
666
+ document.getElementById('sessions-count').textContent = 'Sessions: 0';
667
+ } catch (err) {
668
+ console.error('Failed to load stats:', err);
669
+ }
670
+ }
671
+
672
+ // Event listeners
673
+ document.getElementById('send-btn').addEventListener('click', sendMessage);
674
+ document.getElementById('message-input').addEventListener('keypress', (e) => {
675
+ if (e.key === 'Enter') {
676
+ sendMessage();
677
+ }
678
+ });
679
+
680
+ // Make selectBot global
681
+ window.selectBot = selectBot;
682
+ </script>
683
+ </body>
684
+ </html>`;
685
+ }
686
+
687
+ module.exports = dashboard;