navada-edge-cli 2.0.0 → 2.1.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.
package/lib/agent.js ADDED
@@ -0,0 +1,306 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const navada = require('navada-edge-sdk');
8
+ const ui = require('./ui');
9
+ const config = require('./config');
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // NAVADA Edge Agent — personality + tools + routing
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const IDENTITY = {
16
+ name: 'NAVADA Edge',
17
+ role: 'AI Infrastructure Agent',
18
+ personality: `You are NAVADA Edge — an AI agent that operates inside the user's terminal.
19
+ You are professional, technical, concise, and helpful. You speak with authority about distributed systems, Docker, AI, and cloud infrastructure.
20
+ You have full access to the user's computer: you can read/write files, run shell commands, manage processes, and connect to the NAVADA Edge Network.
21
+ You can invoke two sub-agents:
22
+ - Lucas CTO: runs bash, SSH, Docker commands on remote NAVADA Edge nodes
23
+ - Claude CoS: sends emails, generates images, manages the network
24
+ When users ask you to do something, DO it — don't just describe how. Use your tools.
25
+ When you don't have a tool for something, say so clearly and suggest an alternative.
26
+ Keep responses short. Code blocks when needed. No fluff.`,
27
+ };
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Local tools — run on the USER's machine
31
+ // ---------------------------------------------------------------------------
32
+ const localTools = {
33
+ shell: {
34
+ description: 'Execute a shell command on this machine',
35
+ execute: (cmd) => {
36
+ try {
37
+ const output = execSync(cmd, { timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
38
+ return output.trim();
39
+ } catch (e) {
40
+ return `Error: ${e.stderr?.trim() || e.message}`;
41
+ }
42
+ },
43
+ },
44
+
45
+ readFile: {
46
+ description: 'Read a file from this machine',
47
+ execute: (filePath) => {
48
+ try {
49
+ return fs.readFileSync(path.resolve(filePath), 'utf-8');
50
+ } catch (e) { return `Error: ${e.message}`; }
51
+ },
52
+ },
53
+
54
+ writeFile: {
55
+ description: 'Write content to a file on this machine',
56
+ execute: (filePath, content) => {
57
+ try {
58
+ const resolved = path.resolve(filePath);
59
+ const dir = path.dirname(resolved);
60
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
61
+ fs.writeFileSync(resolved, content);
62
+ return `Written: ${resolved}`;
63
+ } catch (e) { return `Error: ${e.message}`; }
64
+ },
65
+ },
66
+
67
+ listFiles: {
68
+ description: 'List files in a directory on this machine',
69
+ execute: (dir) => {
70
+ try {
71
+ const items = fs.readdirSync(path.resolve(dir || '.'), { withFileTypes: true });
72
+ return items.map(i => `${i.isDirectory() ? 'd' : 'f'} ${i.name}`).join('\n');
73
+ } catch (e) { return `Error: ${e.message}`; }
74
+ },
75
+ },
76
+
77
+ systemInfo: {
78
+ description: 'Get system information',
79
+ execute: () => {
80
+ return JSON.stringify({
81
+ hostname: os.hostname(),
82
+ platform: os.platform(),
83
+ arch: os.arch(),
84
+ cpus: os.cpus().length,
85
+ totalMem: `${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)} GB`,
86
+ freeMem: `${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)} GB`,
87
+ uptime: `${(os.uptime() / 3600).toFixed(1)} hours`,
88
+ user: os.userInfo().username,
89
+ home: os.homedir(),
90
+ cwd: process.cwd(),
91
+ nodeVersion: process.version,
92
+ }, null, 2);
93
+ },
94
+ },
95
+ };
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Anthropic Claude API — conversational agent
99
+ // ---------------------------------------------------------------------------
100
+ async function chat(userMessage, conversationHistory = []) {
101
+ const anthropicKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY || '';
102
+
103
+ if (!anthropicKey) {
104
+ // Fall back to other providers
105
+ return fallbackChat(userMessage);
106
+ }
107
+
108
+ const tools = [
109
+ {
110
+ name: 'shell',
111
+ description: 'Execute a shell command on the user\'s local machine. Use for: file operations, git, npm, docker, system commands.',
112
+ input_schema: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to run' } }, required: ['command'] },
113
+ },
114
+ {
115
+ name: 'read_file',
116
+ description: 'Read the contents of a file on the user\'s local machine.',
117
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File path to read' } }, required: ['path'] },
118
+ },
119
+ {
120
+ name: 'write_file',
121
+ description: 'Write content to a file on the user\'s local machine.',
122
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'Content to write' } }, required: ['path', 'content'] },
123
+ },
124
+ {
125
+ name: 'list_files',
126
+ description: 'List files and directories.',
127
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } },
128
+ },
129
+ {
130
+ name: 'system_info',
131
+ description: 'Get local system information (CPU, RAM, disk, OS, hostname).',
132
+ input_schema: { type: 'object', properties: {} },
133
+ },
134
+ {
135
+ name: 'network_status',
136
+ description: 'Ping all NAVADA Edge Network nodes and cloud services.',
137
+ input_schema: { type: 'object', properties: {} },
138
+ },
139
+ {
140
+ name: 'lucas_exec',
141
+ description: 'Run a bash command on EC2 via Lucas CTO agent.',
142
+ input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
143
+ },
144
+ {
145
+ name: 'lucas_ssh',
146
+ description: 'SSH to a NAVADA Edge node (hp, ec2, oracle) and run a command via Lucas CTO.',
147
+ input_schema: { type: 'object', properties: { node: { type: 'string' }, command: { type: 'string' } }, required: ['node', 'command'] },
148
+ },
149
+ {
150
+ name: 'lucas_docker',
151
+ description: 'Run a command inside a Docker container on EC2 via Lucas CTO.',
152
+ input_schema: { type: 'object', properties: { container: { type: 'string' }, command: { type: 'string' } }, required: ['container', 'command'] },
153
+ },
154
+ {
155
+ name: 'mcp_call',
156
+ description: 'Call a tool on the NAVADA Edge MCP server (18 tools: docker, ssh, files, database, monitoring).',
157
+ input_schema: { type: 'object', properties: { tool: { type: 'string' }, args: { type: 'object' } }, required: ['tool'] },
158
+ },
159
+ {
160
+ name: 'docker_registry',
161
+ description: 'List images or tags in the NAVADA private Docker registry.',
162
+ input_schema: { type: 'object', properties: { image: { type: 'string', description: 'Image name for tags (optional — omit to list all)' } } },
163
+ },
164
+ {
165
+ name: 'send_email',
166
+ description: 'Send an email via the NAVADA Edge MCP email tool.',
167
+ input_schema: { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' } }, required: ['to', 'subject', 'body'] },
168
+ },
169
+ {
170
+ name: 'generate_image',
171
+ description: 'Generate an image using Cloudflare Flux (FREE) or DALL-E.',
172
+ input_schema: { type: 'object', properties: { prompt: { type: 'string' }, provider: { type: 'string', description: 'flux (default, free) or dalle' } }, required: ['prompt'] },
173
+ },
174
+ ];
175
+
176
+ const messages = [
177
+ ...conversationHistory,
178
+ { role: 'user', content: userMessage },
179
+ ];
180
+
181
+ // Call Anthropic API
182
+ let response = await callAnthropic(anthropicKey, messages, tools);
183
+
184
+ // Handle tool use loop
185
+ let iterations = 0;
186
+ while (response.stop_reason === 'tool_use' && iterations < 10) {
187
+ iterations++;
188
+ const toolBlocks = response.content.filter(b => b.type === 'tool_use');
189
+ const results = [];
190
+
191
+ for (const block of toolBlocks) {
192
+ // Print what the agent is doing
193
+ console.log(ui.dim(` [${block.name}] ${JSON.stringify(block.input).slice(0, 80)}`));
194
+ const result = await executeTool(block.name, block.input);
195
+ results.push({ type: 'tool_result', tool_use_id: block.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
196
+ }
197
+
198
+ // Print any text blocks
199
+ for (const block of response.content) {
200
+ if (block.type === 'text' && block.text) console.log(` ${block.text}`);
201
+ }
202
+
203
+ // Continue conversation with tool results
204
+ messages.push({ role: 'assistant', content: response.content });
205
+ messages.push({ role: 'user', content: results });
206
+ response = await callAnthropic(anthropicKey, messages, tools);
207
+ }
208
+
209
+ // Extract final text
210
+ const textBlocks = response.content?.filter(b => b.type === 'text') || [];
211
+ return textBlocks.map(b => b.text).join('\n');
212
+ }
213
+
214
+ async function callAnthropic(key, messages, tools) {
215
+ const r = await navada.request('https://api.anthropic.com/v1/messages', {
216
+ method: 'POST',
217
+ body: {
218
+ model: 'claude-sonnet-4-20250514',
219
+ max_tokens: 4096,
220
+ system: IDENTITY.personality,
221
+ messages,
222
+ tools,
223
+ },
224
+ headers: {
225
+ 'x-api-key': key,
226
+ 'anthropic-version': '2023-06-01',
227
+ 'Content-Type': 'application/json',
228
+ },
229
+ timeout: 60000,
230
+ });
231
+
232
+ if (r.status !== 200) {
233
+ throw new Error(`Anthropic API error ${r.status}: ${JSON.stringify(r.data).slice(0, 200)}`);
234
+ }
235
+
236
+ return r.data;
237
+ }
238
+
239
+ async function executeTool(name, input) {
240
+ try {
241
+ switch (name) {
242
+ case 'shell': return localTools.shell.execute(input.command);
243
+ case 'read_file': return localTools.readFile.execute(input.path);
244
+ case 'write_file': return localTools.writeFile.execute(input.path, input.content);
245
+ case 'list_files': return localTools.listFiles.execute(input.path);
246
+ case 'system_info': return localTools.systemInfo.execute();
247
+ case 'network_status': return JSON.stringify(await navada.network.ping());
248
+ case 'lucas_exec': return JSON.stringify(await navada.lucas.exec(input.command));
249
+ case 'lucas_ssh': return JSON.stringify(await navada.lucas.ssh(input.node, input.command));
250
+ case 'lucas_docker': return JSON.stringify(await navada.lucas.docker(input.container, input.command));
251
+ case 'mcp_call': return JSON.stringify(await navada.mcp.call(input.tool, input.args || {}));
252
+ case 'docker_registry':
253
+ if (input.image) return JSON.stringify(await navada.registry.tags(input.image));
254
+ return JSON.stringify(await navada.registry.catalog());
255
+ case 'send_email': return JSON.stringify(await navada.mcp.call('send-email', input));
256
+ case 'generate_image':
257
+ if (input.provider === 'dalle') return JSON.stringify(await navada.ai.openai.image(input.prompt));
258
+ const { size } = await navada.cloudflare.flux.generate(input.prompt, { savePath: `navada-${Date.now()}.png` });
259
+ return `Image generated: ${size} bytes`;
260
+ default: return `Unknown tool: ${name}`;
261
+ }
262
+ } catch (e) {
263
+ return `Tool error: ${e.message}`;
264
+ }
265
+ }
266
+
267
+ async function fallbackChat(msg) {
268
+ // Try MCP → Qwen → OpenAI → error
269
+ if (navada.config.mcp) {
270
+ try { return JSON.stringify(await navada.mcp.call('chat', { message: msg })); } catch {}
271
+ }
272
+ if (navada.config.hfToken) {
273
+ try { return await navada.ai.huggingface.qwen(msg); } catch {}
274
+ }
275
+ if (navada.config.openaiKey) {
276
+ try { return await navada.ai.openai.chat(msg); } catch {}
277
+ }
278
+ return 'No AI provider configured. Run /setup or set ANTHROPIC_API_KEY.';
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Telemetry — track installs + usage
283
+ // ---------------------------------------------------------------------------
284
+ async function reportTelemetry(event, data = {}) {
285
+ if (!navada.config.dashboard) return;
286
+ try {
287
+ await navada.request(navada.config.dashboard + '/api/agent-heartbeat', {
288
+ method: 'POST',
289
+ body: {
290
+ agent: 'navada-edge-cli',
291
+ event,
292
+ version: require('../package.json').version,
293
+ hostname: os.hostname(),
294
+ platform: os.platform(),
295
+ arch: os.arch(),
296
+ nodeVersion: process.version,
297
+ apiKey: config.getApiKey()?.slice(0, 8) || 'none',
298
+ ts: new Date().toISOString(),
299
+ ...data,
300
+ },
301
+ timeout: 5000,
302
+ });
303
+ } catch {}
304
+ }
305
+
306
+ module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat };
package/lib/cli.js CHANGED
@@ -8,6 +8,7 @@ const history = require('./history');
8
8
  const { completer } = require('./completer');
9
9
  const { execute, getCompletions } = require('./registry');
10
10
  const { loadAll } = require('./commands/index');
11
+ const { reportTelemetry } = require('./agent');
11
12
 
12
13
  function applyConfig() {
13
14
  const cfg = config.getAll();
@@ -86,10 +87,13 @@ async function run(argv) {
86
87
  if (argv.length === 0) {
87
88
  // Check first run
88
89
  if (config.isFirstRun()) {
90
+ reportTelemetry('install');
89
91
  const { runSetup } = require('./commands/setup');
90
92
  await runSetup();
91
93
  return;
92
94
  }
95
+ // Report session start
96
+ reportTelemetry('session_start');
93
97
  // Interactive mode
94
98
  showWelcome();
95
99
  startRepl();
@@ -3,48 +3,39 @@
3
3
  const navada = require('navada-edge-sdk');
4
4
  const ui = require('../ui');
5
5
  const config = require('../config');
6
+ const { chat: agentChat, reportTelemetry } = require('../agent');
6
7
 
7
8
  module.exports = function(reg) {
8
9
 
9
- reg('chat', 'Chat with AI (uses configured model)', async (args) => {
10
+ // Conversation history for multi-turn
11
+ const conversationHistory = [];
12
+
13
+ reg('chat', 'Chat with NAVADA Edge AI agent', async (args) => {
10
14
  const msg = args.join(' ');
11
- if (!msg) { console.log(ui.dim('Usage: /chat What is the ISA allowance for 2026?')); return; }
15
+ if (!msg) { console.log(ui.dim('Just type naturally no /command needed.')); return; }
12
16
  const ora = require('ora');
13
- const model = config.getModel();
17
+ const spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
14
18
 
15
- if (model === 'qwen' || (!navada.config.openaiKey && navada.config.hfToken)) {
16
- // Use Qwen (FREE)
17
- const spinner = ora({ text: ' Qwen thinking...', color: 'white' }).start();
18
- const result = await navada.ai.huggingface.qwen(msg);
19
- spinner.stop();
20
- console.log(ui.header('QWEN CODER'));
21
- console.log(typeof result === 'object' ? ui.jsonColorize(result) : ` ${result}`);
22
- } else if (navada.config.openaiKey) {
23
- // Use OpenAI
24
- const spinner = ora({ text: ' Thinking...', color: 'white' }).start();
25
- const response = await navada.ai.openai.chat(msg);
19
+ try {
20
+ const response = await agentChat(msg, conversationHistory);
26
21
  spinner.stop();
27
- console.log(ui.header('AI RESPONSE'));
22
+
23
+ // Update conversation history
24
+ conversationHistory.push({ role: 'user', content: msg });
25
+ conversationHistory.push({ role: 'assistant', content: response });
26
+
27
+ // Keep history manageable (last 20 turns)
28
+ while (conversationHistory.length > 40) conversationHistory.splice(0, 2);
29
+
30
+ console.log(ui.header('NAVADA'));
28
31
  console.log(` ${response}`);
29
- } else {
30
- // Route through MCP if available
31
- if (navada.config.mcp) {
32
- const spinner = ora({ text: ' Asking NAVADA...', color: 'white' }).start();
33
- try {
34
- const result = await navada.mcp.call('chat', { message: msg });
35
- spinner.stop();
36
- console.log(ui.header('NAVADA'));
37
- console.log(typeof result === 'object' ? ui.jsonColorize(result) : ` ${result}`);
38
- } catch {
39
- spinner.stop();
40
- console.log(ui.warn('No AI provider configured. Set OPENAI_API_KEY, HF_TOKEN, or connect to MCP.'));
41
- console.log(ui.dim('Run /setup to configure, or /model to choose a provider.'));
42
- }
43
- } else {
44
- console.log(ui.warn('No AI provider configured.'));
45
- console.log(ui.dim('Options: set OPENAI_API_KEY, HF_TOKEN, or connect to MCP server.'));
46
- console.log(ui.dim('Run /setup to configure, or /model to choose a provider.'));
47
- }
32
+
33
+ // Track usage
34
+ reportTelemetry('chat', { messageLength: msg.length });
35
+ } catch (e) {
36
+ spinner.stop();
37
+ console.log(ui.error(e.message));
38
+ console.log(ui.dim('Check: /config to see which providers are set, or /setup to configure.'));
48
39
  }
49
40
  }, { category: 'AI', aliases: ['ask'] });
50
41
 
@@ -56,7 +47,7 @@ module.exports = function(reg) {
56
47
  const result = await navada.ai.huggingface.qwen(prompt);
57
48
  spinner.stop();
58
49
  console.log(ui.header('QWEN CODER'));
59
- console.log(typeof result === 'object' ? ui.jsonColorize(result) : ` ${result}`);
50
+ console.log(` ${typeof result === 'object' ? ui.jsonColorize(result) : result}`);
60
51
  }, { category: 'AI' });
61
52
 
62
53
  reg('yolo', 'YOLO object detection', async (args) => {
@@ -88,12 +79,12 @@ module.exports = function(reg) {
88
79
  const ora = require('ora');
89
80
 
90
81
  if (useDalle && navada.config.openaiKey) {
91
- const spinner = ora({ text: ` DALL-E generating...`, color: 'white' }).start();
82
+ const spinner = ora({ text: ' DALL-E generating...', color: 'white' }).start();
92
83
  const result = await navada.ai.openai.image(prompt);
93
84
  spinner.stop();
94
85
  console.log(ui.success(`Generated: ${result.url || result.revised_prompt || 'done'}`));
95
86
  } else {
96
- const spinner = ora({ text: ` Flux generating (FREE)...`, color: 'white' }).start();
87
+ const spinner = ora({ text: ' Flux generating (FREE)...', color: 'white' }).start();
97
88
  const savePath = `navada-image-${Date.now()}.png`;
98
89
  const { size } = await navada.cloudflare.flux.generate(prompt, { savePath });
99
90
  spinner.stop();
@@ -103,7 +94,7 @@ module.exports = function(reg) {
103
94
 
104
95
  reg('model', 'Show/set default AI model', (args) => {
105
96
  if (args[0]) {
106
- const valid = ['auto', 'gpt-4o', 'gpt-4o-mini', 'qwen', 'flux'];
97
+ const valid = ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen'];
107
98
  if (!valid.includes(args[0])) { console.log(ui.error(`Invalid model. Options: ${valid.join(', ')}`)); return; }
108
99
  config.setModel(args[0]);
109
100
  console.log(ui.success(`Model set to: ${args[0]}`));
@@ -113,15 +104,14 @@ module.exports = function(reg) {
113
104
  console.log(ui.label('Current', current));
114
105
  console.log('');
115
106
  console.log(ui.dim('Available:'));
116
- console.log(ui.label('auto', 'Route to best available provider'));
107
+ console.log(ui.label('auto', 'Claude (Anthropic) with tool use — default'));
108
+ console.log(ui.label('claude', 'Claude Sonnet 4 via Anthropic API'));
117
109
  console.log(ui.label('gpt-4o', 'OpenAI GPT-4o (requires OPENAI_API_KEY)'));
118
- console.log(ui.label('gpt-4o-mini', 'OpenAI GPT-4o-mini (requires OPENAI_API_KEY)'));
119
110
  console.log(ui.label('qwen', 'Qwen Coder 32B (FREE via HuggingFace)'));
120
- console.log(ui.label('flux', 'Cloudflare Flux (FREE image gen)'));
121
111
  console.log('');
122
- console.log(ui.dim('Set with: /model qwen'));
112
+ console.log(ui.dim('Set with: /model claude'));
123
113
  }
124
- }, { category: 'AI', subs: ['auto', 'gpt-4o', 'gpt-4o-mini', 'qwen', 'flux'] });
114
+ }, { category: 'AI', subs: ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen'] });
125
115
 
126
116
  reg('research', 'RAG search via MCP', async (args) => {
127
117
  const query = args.join(' ');
@@ -217,13 +217,67 @@ module.exports = function(reg) {
217
217
  }, { category: 'SYSTEM' });
218
218
 
219
219
  // --- /email ---
220
- reg('email', 'Send email via MCP', async (args) => {
221
- if (args.length < 3) { console.log(ui.dim('Usage: /email <to> <subject> <body>')); return; }
220
+ reg('email', 'Send email (SMTP or MCP)', async (args) => {
221
+ if (args[0] === 'setup') {
222
+ // Configure SMTP
223
+ const readline = require('readline');
224
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
225
+ const ask = (q) => new Promise(r => rl.question(q, r));
226
+
227
+ console.log(ui.header('EMAIL SETUP'));
228
+ console.log(ui.dim(' Providers: Hotmail (smtp-mail.outlook.com), Gmail (smtp.gmail.com), Zoho (smtp.zoho.eu)'));
229
+ console.log('');
230
+ const host = await ask(' SMTP host: ');
231
+ const port = await ask(' SMTP port [587]: ');
232
+ const user = await ask(' Email/username: ');
233
+ const pass = await ask(' Password/app password: ');
234
+ const from = await ask(' From address: ');
235
+ rl.close();
236
+
237
+ config.setSmtp({ host: host.trim(), port: parseInt(port.trim() || '587'), user: user.trim(), pass: pass.trim(), from: from.trim() || user.trim() });
238
+ console.log(ui.success('SMTP configured'));
239
+ return;
240
+ }
241
+
242
+ if (args.length < 3) {
243
+ console.log(ui.dim('Usage: /email <to> <subject> <body>'));
244
+ console.log(ui.dim('Setup: /email setup'));
245
+ return;
246
+ }
247
+
222
248
  const [to, subject, ...bodyParts] = args;
223
249
  const body = bodyParts.join(' ');
224
- const result = await navada.mcp.call('send-email', { to, subject, body });
225
- console.log(ui.success(`Email sent to ${to}`));
226
- }, { category: 'SYSTEM' });
250
+ const smtp = config.getSmtp();
251
+
252
+ // Try local SMTP first
253
+ if (smtp.host && smtp.user && smtp.pass) {
254
+ try {
255
+ const nodemailer = require('nodemailer');
256
+ const transport = nodemailer.createTransport({
257
+ host: smtp.host, port: smtp.port, secure: smtp.port === 465,
258
+ auth: { user: smtp.user, pass: smtp.pass },
259
+ });
260
+ await transport.sendMail({ from: smtp.from || smtp.user, to, subject, text: body });
261
+ console.log(ui.success(`Email sent to ${to} via ${smtp.host}`));
262
+ return;
263
+ } catch (e) {
264
+ console.log(ui.warn(`SMTP failed: ${e.message}. Trying MCP...`));
265
+ }
266
+ }
267
+
268
+ // Fall back to MCP
269
+ if (navada.config.mcp) {
270
+ try {
271
+ await navada.mcp.call('send-email', { to, subject, body });
272
+ console.log(ui.success(`Email sent to ${to} via MCP`));
273
+ return;
274
+ } catch (e) {
275
+ console.log(ui.error(`MCP email failed: ${e.message}`));
276
+ }
277
+ }
278
+
279
+ console.log(ui.error('No email provider configured. Run /email setup to configure SMTP.'));
280
+ }, { category: 'SYSTEM', subs: ['setup'] });
227
281
 
228
282
  // --- /activity ---
229
283
  reg('activity', 'Recent activity log', async () => {
package/lib/config.js CHANGED
@@ -44,7 +44,19 @@ function getServePort() { return parseInt(load().servePort || process.env.NAVADA
44
44
  function getTier() { return load().tier || 'FREE'; }
45
45
  function setTier(tier) { const c = load(); c.tier = tier; save(c); }
46
46
 
47
- function get(key) { return process.env[`NAVADA_${key.toUpperCase()}`] || load()[key] || ''; }
47
+ // SMTP email config
48
+ function getSmtp() {
49
+ return load().smtp || {
50
+ host: process.env.SMTP_HOST || '',
51
+ port: parseInt(process.env.SMTP_PORT || '587'),
52
+ user: process.env.SMTP_USER || '',
53
+ pass: process.env.SMTP_PASS || '',
54
+ from: process.env.SMTP_FROM || '',
55
+ };
56
+ }
57
+ function setSmtp(smtp) { const c = load(); c.smtp = smtp; save(c); }
58
+
59
+ function get(key) { return process.env[`NAVADA_${key.toUpperCase()}`] || process.env[key.toUpperCase()] || process.env[key] || load()[key] || ''; }
48
60
  function set(key, value) { const c = load(); c[key] = value; save(c); }
49
61
 
50
62
  function getAll() { return { ...load(), apiKey: getApiKey() }; }
@@ -59,4 +71,5 @@ module.exports = {
59
71
  getServePort,
60
72
  getTier, setTier,
61
73
  get, set, getAll,
74
+ getSmtp, setSmtp,
62
75
  };
package/lib/registry.js CHANGED
@@ -23,30 +23,45 @@ function register(name, description, handler, opts = {}) {
23
23
  }
24
24
 
25
25
  async function execute(input) {
26
- const parts = input.trim().replace(/^\//, '').split(/\s+/);
27
- let name = parts[0]?.toLowerCase();
28
- const args = parts.slice(1);
26
+ const trimmed = input.trim();
27
+ if (!trimmed) return;
29
28
 
30
- if (!name) return;
29
+ // If input starts with /, treat as a command
30
+ if (trimmed.startsWith('/')) {
31
+ const parts = trimmed.slice(1).split(/\s+/);
32
+ let name = parts[0]?.toLowerCase();
33
+ const args = parts.slice(1);
31
34
 
32
- // Check user-defined aliases
33
- const aliases = config.getAliases();
34
- if (aliases[name]) {
35
- const expanded = aliases[name].split(/\s+/);
36
- name = expanded[0];
37
- args.unshift(...expanded.slice(1));
38
- }
35
+ if (!name) return;
39
36
 
40
- const cmd = commands[name];
41
- if (!cmd) {
42
- const ui = require('./ui');
43
- console.log(ui.error(`Unknown command: /${parts[0]}`));
44
- console.log(ui.dim('Type /help for available commands'));
37
+ // Check user-defined aliases
38
+ const aliases = config.getAliases();
39
+ if (aliases[name]) {
40
+ const expanded = aliases[name].split(/\s+/);
41
+ name = expanded[0];
42
+ args.unshift(...expanded.slice(1));
43
+ }
44
+
45
+ const cmd = commands[name];
46
+ if (!cmd) {
47
+ const ui = require('./ui');
48
+ console.log(ui.error(`Unknown command: /${parts[0]}`));
49
+ console.log(ui.dim('Type /help for available commands'));
50
+ return;
51
+ }
52
+
53
+ try {
54
+ await cmd.handler(args);
55
+ } catch (e) {
56
+ const ui = require('./ui');
57
+ console.log(ui.error(e.message));
58
+ }
45
59
  return;
46
60
  }
47
61
 
62
+ // No slash — treat as natural language → route to AI agent
48
63
  try {
49
- await cmd.handler(args);
64
+ await commands['chat'].handler(trimmed.split(/\s+/));
50
65
  } catch (e) {
51
66
  const ui = require('./ui');
52
67
  console.log(ui.error(e.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navada-edge-cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Interactive CLI for the NAVADA Edge Network — explore nodes, agents, Cloudflare, AI, Docker, and MCP from your terminal",
5
5
  "main": "lib/cli.js",
6
6
  "bin": {
@@ -36,7 +36,8 @@
36
36
  "ora": "^5.4.1"
37
37
  },
38
38
  "optionalDependencies": {
39
- "qrcode-terminal": "^0.12.0"
39
+ "qrcode-terminal": "^0.12.0",
40
+ "nodemailer": "^6.9.0"
40
41
  },
41
42
  "publishConfig": {
42
43
  "access": "public"