navada-edge-cli 2.0.0 → 2.1.1

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,328 @@
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
+ // Smart key detection — check all possible locations
102
+ const anthropicKey = config.get('anthropicKey')
103
+ || config.getApiKey() // /setup saves here
104
+ || process.env.ANTHROPIC_API_KEY
105
+ || '';
106
+
107
+ // Auto-detect: if the key starts with sk-ant, it's Anthropic
108
+ const apiKey = config.getApiKey();
109
+ const effectiveKey = anthropicKey
110
+ || (apiKey && apiKey.startsWith('sk-ant') ? apiKey : '')
111
+ || '';
112
+
113
+ if (!effectiveKey) {
114
+ return fallbackChat(userMessage);
115
+ }
116
+
117
+ const tools = [
118
+ {
119
+ name: 'shell',
120
+ description: 'Execute a shell command on the user\'s local machine. Use for: file operations, git, npm, docker, system commands.',
121
+ input_schema: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to run' } }, required: ['command'] },
122
+ },
123
+ {
124
+ name: 'read_file',
125
+ description: 'Read the contents of a file on the user\'s local machine.',
126
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File path to read' } }, required: ['path'] },
127
+ },
128
+ {
129
+ name: 'write_file',
130
+ description: 'Write content to a file on the user\'s local machine.',
131
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'Content to write' } }, required: ['path', 'content'] },
132
+ },
133
+ {
134
+ name: 'list_files',
135
+ description: 'List files and directories.',
136
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } },
137
+ },
138
+ {
139
+ name: 'system_info',
140
+ description: 'Get local system information (CPU, RAM, disk, OS, hostname).',
141
+ input_schema: { type: 'object', properties: {} },
142
+ },
143
+ {
144
+ name: 'network_status',
145
+ description: 'Ping all NAVADA Edge Network nodes and cloud services.',
146
+ input_schema: { type: 'object', properties: {} },
147
+ },
148
+ {
149
+ name: 'lucas_exec',
150
+ description: 'Run a bash command on EC2 via Lucas CTO agent.',
151
+ input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
152
+ },
153
+ {
154
+ name: 'lucas_ssh',
155
+ description: 'SSH to a NAVADA Edge node (hp, ec2, oracle) and run a command via Lucas CTO.',
156
+ input_schema: { type: 'object', properties: { node: { type: 'string' }, command: { type: 'string' } }, required: ['node', 'command'] },
157
+ },
158
+ {
159
+ name: 'lucas_docker',
160
+ description: 'Run a command inside a Docker container on EC2 via Lucas CTO.',
161
+ input_schema: { type: 'object', properties: { container: { type: 'string' }, command: { type: 'string' } }, required: ['container', 'command'] },
162
+ },
163
+ {
164
+ name: 'mcp_call',
165
+ description: 'Call a tool on the NAVADA Edge MCP server (18 tools: docker, ssh, files, database, monitoring).',
166
+ input_schema: { type: 'object', properties: { tool: { type: 'string' }, args: { type: 'object' } }, required: ['tool'] },
167
+ },
168
+ {
169
+ name: 'docker_registry',
170
+ description: 'List images or tags in the NAVADA private Docker registry.',
171
+ input_schema: { type: 'object', properties: { image: { type: 'string', description: 'Image name for tags (optional — omit to list all)' } } },
172
+ },
173
+ {
174
+ name: 'send_email',
175
+ description: 'Send an email via the NAVADA Edge MCP email tool.',
176
+ input_schema: { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' } }, required: ['to', 'subject', 'body'] },
177
+ },
178
+ {
179
+ name: 'generate_image',
180
+ description: 'Generate an image using Cloudflare Flux (FREE) or DALL-E.',
181
+ input_schema: { type: 'object', properties: { prompt: { type: 'string' }, provider: { type: 'string', description: 'flux (default, free) or dalle' } }, required: ['prompt'] },
182
+ },
183
+ ];
184
+
185
+ const messages = [
186
+ ...conversationHistory,
187
+ { role: 'user', content: userMessage },
188
+ ];
189
+
190
+ // Call Anthropic API
191
+ let response = await callAnthropic(effectiveKey, messages, tools);
192
+
193
+ // Handle tool use loop
194
+ let iterations = 0;
195
+ while (response.stop_reason === 'tool_use' && iterations < 10) {
196
+ iterations++;
197
+ const toolBlocks = response.content.filter(b => b.type === 'tool_use');
198
+ const results = [];
199
+
200
+ for (const block of toolBlocks) {
201
+ // Print what the agent is doing
202
+ console.log(ui.dim(` [${block.name}] ${JSON.stringify(block.input).slice(0, 80)}`));
203
+ const result = await executeTool(block.name, block.input);
204
+ results.push({ type: 'tool_result', tool_use_id: block.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
205
+ }
206
+
207
+ // Print any text blocks
208
+ for (const block of response.content) {
209
+ if (block.type === 'text' && block.text) console.log(` ${block.text}`);
210
+ }
211
+
212
+ // Continue conversation with tool results
213
+ messages.push({ role: 'assistant', content: response.content });
214
+ messages.push({ role: 'user', content: results });
215
+ response = await callAnthropic(anthropicKey, messages, tools);
216
+ }
217
+
218
+ // Extract final text
219
+ const textBlocks = response.content?.filter(b => b.type === 'text') || [];
220
+ return textBlocks.map(b => b.text).join('\n');
221
+ }
222
+
223
+ async function callAnthropic(key, messages, tools) {
224
+ const r = await navada.request('https://api.anthropic.com/v1/messages', {
225
+ method: 'POST',
226
+ body: {
227
+ model: 'claude-sonnet-4-20250514',
228
+ max_tokens: 4096,
229
+ system: IDENTITY.personality,
230
+ messages,
231
+ tools,
232
+ },
233
+ headers: {
234
+ 'x-api-key': key,
235
+ 'anthropic-version': '2023-06-01',
236
+ 'Content-Type': 'application/json',
237
+ },
238
+ timeout: 60000,
239
+ });
240
+
241
+ if (r.status !== 200) {
242
+ throw new Error(`Anthropic API error ${r.status}: ${JSON.stringify(r.data).slice(0, 200)}`);
243
+ }
244
+
245
+ return r.data;
246
+ }
247
+
248
+ async function executeTool(name, input) {
249
+ try {
250
+ switch (name) {
251
+ case 'shell': return localTools.shell.execute(input.command);
252
+ case 'read_file': return localTools.readFile.execute(input.path);
253
+ case 'write_file': return localTools.writeFile.execute(input.path, input.content);
254
+ case 'list_files': return localTools.listFiles.execute(input.path);
255
+ case 'system_info': return localTools.systemInfo.execute();
256
+ case 'network_status': return JSON.stringify(await navada.network.ping());
257
+ case 'lucas_exec': return JSON.stringify(await navada.lucas.exec(input.command));
258
+ case 'lucas_ssh': return JSON.stringify(await navada.lucas.ssh(input.node, input.command));
259
+ case 'lucas_docker': return JSON.stringify(await navada.lucas.docker(input.container, input.command));
260
+ case 'mcp_call': return JSON.stringify(await navada.mcp.call(input.tool, input.args || {}));
261
+ case 'docker_registry':
262
+ if (input.image) return JSON.stringify(await navada.registry.tags(input.image));
263
+ return JSON.stringify(await navada.registry.catalog());
264
+ case 'send_email': return JSON.stringify(await navada.mcp.call('send-email', input));
265
+ case 'generate_image':
266
+ if (input.provider === 'dalle') return JSON.stringify(await navada.ai.openai.image(input.prompt));
267
+ const { size } = await navada.cloudflare.flux.generate(input.prompt, { savePath: `navada-${Date.now()}.png` });
268
+ return `Image generated: ${size} bytes`;
269
+ default: return `Unknown tool: ${name}`;
270
+ }
271
+ } catch (e) {
272
+ return `Tool error: ${e.message}`;
273
+ }
274
+ }
275
+
276
+ async function fallbackChat(msg) {
277
+ // Try MCP → Qwen → OpenAI → helpful error
278
+ if (navada.config.mcp) {
279
+ try {
280
+ const r = await navada.mcp.call('chat', { message: msg });
281
+ return typeof r === 'object' ? JSON.stringify(r) : r;
282
+ } catch {}
283
+ }
284
+ if (navada.config.hfToken) {
285
+ try {
286
+ const r = await navada.ai.huggingface.qwen(msg);
287
+ return typeof r === 'object' ? JSON.stringify(r) : r;
288
+ } catch {}
289
+ }
290
+ if (navada.config.openaiKey) {
291
+ try { return await navada.ai.openai.chat(msg); } catch {}
292
+ }
293
+ return `I need an API key to chat. Quick fix:
294
+
295
+ /login sk-ant-your-anthropic-key (recommended — enables full agent with tool use)
296
+ /login sk-your-openai-key (GPT-4o)
297
+ /init hfToken hf_your_token (Qwen Coder — FREE)
298
+ /setup (guided wizard)
299
+
300
+ /commands still work without a key — try /help`;
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Telemetry — track installs + usage
305
+ // ---------------------------------------------------------------------------
306
+ async function reportTelemetry(event, data = {}) {
307
+ if (!navada.config.dashboard) return;
308
+ try {
309
+ await navada.request(navada.config.dashboard + '/api/agent-heartbeat', {
310
+ method: 'POST',
311
+ body: {
312
+ agent: 'navada-edge-cli',
313
+ event,
314
+ version: require('../package.json').version,
315
+ hostname: os.hostname(),
316
+ platform: os.platform(),
317
+ arch: os.arch(),
318
+ nodeVersion: process.version,
319
+ apiKey: config.getApiKey()?.slice(0, 8) || 'none',
320
+ ts: new Date().toISOString(),
321
+ ...data,
322
+ },
323
+ timeout: 5000,
324
+ });
325
+ } catch {}
326
+ }
327
+
328
+ 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();
@@ -84,14 +85,21 @@ async function run(argv) {
84
85
  applyConfig();
85
86
 
86
87
  if (argv.length === 0) {
87
- // Check first run
88
- if (config.isFirstRun()) {
89
- const { runSetup } = require('./commands/setup');
90
- await runSetup();
91
- return;
92
- }
93
- // Interactive mode
88
+ // Report telemetry
89
+ if (config.isFirstRun()) reportTelemetry('install');
90
+ else reportTelemetry('session_start');
91
+
92
+ // Interactive mode — always start. User can /setup if needed.
94
93
  showWelcome();
94
+
95
+ // Hint if no key configured
96
+ const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
97
+ if (!hasKey) {
98
+ console.log(ui.warn('No API key set. Type /login <key> or /setup to configure.'));
99
+ console.log(ui.dim('You can still use /commands. Natural chat requires an API key.'));
100
+ console.log('');
101
+ }
102
+
95
103
  startRepl();
96
104
  } else if (argv[0] === '--version' || argv[0] === '-v') {
97
105
  const pkg = require('../package.json');
@@ -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(' ');
@@ -9,128 +9,97 @@ function ask(rl, question) {
9
9
  return new Promise(resolve => rl.question(question, resolve));
10
10
  }
11
11
 
12
- async function runSetup() {
13
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ async function runSetup(fromRepl = false) {
13
+ // Create a fresh readline don't conflict with the REPL
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ terminal: true,
18
+ });
14
19
 
15
- console.log(ui.banner());
16
- console.log(ui.box('WELCOME', ' First-time setup. Let\'s connect you to the NAVADA Edge Network.'));
17
20
  console.log('');
18
-
19
- // 1. API Key
20
- const apiKey = await ask(rl, ' Enter your NAVADA Edge API key (or press Enter for FREE tier): ');
21
- if (apiKey.trim()) {
22
- config.setApiKey(apiKey.trim());
23
- navada.init({ mcpApiKey: apiKey.trim(), dashboardApiKey: apiKey.trim() });
24
- console.log(ui.success('API key saved'));
25
- } else {
26
- console.log(ui.dim('Continuing with FREE tier'));
27
- }
21
+ console.log(ui.box('SETUP', ' Configure your NAVADA Edge CLI.'));
28
22
  console.log('');
29
23
 
30
- // 2. Auto-detect network
31
- console.log(ui.dim('Scanning for NAVADA Edge nodes...'));
32
- // Ask for primary node IP to scan from
33
- const scanIp = await ask(rl, ' Enter your primary node IP to scan (or press Enter to skip): ');
34
- const defaultIps = {};
35
- if (scanIp.trim()) {
36
- defaultIps.asus = scanIp.trim();
37
- }
24
+ // 1. API Key — the only required thing
25
+ console.log(ui.dim(' Your API key powers the AI agent. Accepts:'));
26
+ console.log(ui.dim(' - Anthropic key (sk-ant-...)'));
27
+ console.log(ui.dim(' - NAVADA Edge API key (nv_edge_...)'));
28
+ console.log(ui.dim(' - OpenAI key (sk-...)'));
29
+ console.log(ui.dim(' - Or press Enter to skip (limited mode)'));
30
+ console.log('');
38
31
 
39
- let detected = {};
40
- for (const [node, ip] of Object.entries(defaultIps)) {
41
- try {
42
- const port = node === 'oracle' ? 9000 : 3200;
43
- await navada.request(`http://${ip}:${port}/`, { timeout: 3000 });
44
- detected[node] = ip;
45
- console.log(ui.online(node.toUpperCase(), true, ip));
46
- } catch {
47
- console.log(ui.online(node.toUpperCase(), false, 'not found'));
32
+ const apiKey = await ask(rl, ' API key: ');
33
+ if (apiKey.trim()) {
34
+ const key = apiKey.trim();
35
+ config.setApiKey(key);
36
+ // Auto-detect key type and save to right field
37
+ if (key.startsWith('sk-ant')) {
38
+ config.set('anthropicKey', key);
39
+ console.log(ui.success('Anthropic key saved — full agent mode enabled'));
40
+ } else if (key.startsWith('nv_edge')) {
41
+ console.log(ui.success('NAVADA Edge key saved — MCP access enabled'));
42
+ } else if (key.startsWith('sk-')) {
43
+ config.set('openaiKey', key);
44
+ navada.init({ openaiKey: key });
45
+ console.log(ui.success('OpenAI key saved'));
46
+ } else {
47
+ console.log(ui.success('Key saved'));
48
48
  }
49
+ } else {
50
+ console.log(ui.dim('Skipped — you can set this later with /login <key>'));
49
51
  }
50
52
  console.log('');
51
53
 
52
- // 3. Confirm or enter manually
53
- if (Object.keys(detected).length > 0) {
54
- const useDetected = await ask(rl, ' Use detected nodes? (Y/n): ');
55
- if (useDetected.toLowerCase() !== 'n') {
56
- for (const [node, ip] of Object.entries(detected)) {
57
- config.set(node, ip);
58
- }
59
- // Auto-derive service URLs from ASUS
60
- if (detected.asus) {
61
- config.set('mcp', `http://${detected.asus}:8811`);
62
- config.set('dashboard', `http://${detected.asus}:7900`);
63
- config.set('registry', `http://${detected.asus}:5000`);
64
- navada.init({
65
- asus: detected.asus, hp: detected.hp || '', ec2: detected.ec2 || '', oracle: detected.oracle || '',
66
- mcp: `http://${detected.asus}:8811`, dashboard: `http://${detected.asus}:7900`, registry: `http://${detected.asus}:5000`,
67
- });
68
- }
69
- if (detected.ec2) {
70
- config.set('lucas', `http://${detected.ec2}:8820`);
71
- navada.init({ lucas: `http://${detected.ec2}:8820` });
72
- }
73
- console.log(ui.success('Nodes configured'));
74
- }
75
- } else {
76
- console.log(ui.dim('No nodes auto-detected. Enter manually:'));
54
+ // 2. Node IPs optional
55
+ const configureNodes = await ask(rl, ' Configure network nodes? (y/N): ');
56
+ if (configureNodes.trim().toLowerCase() === 'y') {
57
+ console.log('');
58
+ console.log(ui.dim(' Enter Tailscale IPs for your NAVADA Edge nodes.'));
59
+ console.log(ui.dim(' Press Enter to skip any node.'));
60
+ console.log('');
61
+
77
62
  const asusIp = await ask(rl, ' ASUS IP (Production Engine): ');
78
63
  if (asusIp.trim()) {
79
64
  config.set('asus', asusIp.trim());
80
65
  config.set('mcp', `http://${asusIp.trim()}:8811`);
81
66
  config.set('dashboard', `http://${asusIp.trim()}:7900`);
82
67
  config.set('registry', `http://${asusIp.trim()}:5000`);
83
- navada.init({ asus: asusIp.trim(), mcp: `http://${asusIp.trim()}:8811`, dashboard: `http://${asusIp.trim()}:7900`, registry: `http://${asusIp.trim()}:5000` });
68
+ navada.init({
69
+ asus: asusIp.trim(),
70
+ mcp: `http://${asusIp.trim()}:8811`,
71
+ dashboard: `http://${asusIp.trim()}:7900`,
72
+ registry: `http://${asusIp.trim()}:5000`,
73
+ });
84
74
  }
85
- const hpIp = await ask(rl, ' HP IP (Database, press Enter to skip): ');
75
+ const hpIp = await ask(rl, ' HP IP (Database): ');
86
76
  if (hpIp.trim()) config.set('hp', hpIp.trim());
87
- const ec2Ip = await ask(rl, ' EC2 IP (Monitoring, press Enter to skip): ');
77
+ const ec2Ip = await ask(rl, ' EC2 IP (Monitoring): ');
88
78
  if (ec2Ip.trim()) {
89
79
  config.set('ec2', ec2Ip.trim());
90
80
  config.set('lucas', `http://${ec2Ip.trim()}:8820`);
91
81
  }
92
- const oracleIp = await ask(rl, ' Oracle IP (Docker Infra, press Enter to skip): ');
82
+ const oracleIp = await ask(rl, ' Oracle IP (Infrastructure): ');
93
83
  if (oracleIp.trim()) config.set('oracle', oracleIp.trim());
84
+ console.log(ui.success('Nodes configured'));
94
85
  }
95
86
  console.log('');
96
87
 
97
- // 4. Choose theme
98
- const theme = await ask(rl, ' Choose theme (dark/crow/matrix/light) [dark]: ');
88
+ // 3. Theme
89
+ const theme = await ask(rl, ' Theme (dark/crow/matrix/light) [dark]: ');
99
90
  config.setTheme(theme.trim() || 'dark');
100
- console.log(ui.success(`Theme: ${config.getTheme()}`));
101
- console.log('');
102
-
103
- // 5. Test connections
104
- console.log(ui.dim('Testing connections...'));
105
- const tests = [
106
- { name: 'MCP', url: navada.config.mcp ? navada.config.mcp + '/health' : '' },
107
- { name: 'Dashboard', url: navada.config.dashboard || '' },
108
- { name: 'Registry', url: navada.config.registry ? navada.config.registry + '/v2/_catalog' : '' },
109
- ];
110
- let pass = 0;
111
- for (const t of tests) {
112
- if (!t.url) { console.log(ui.online(t.name, false, 'not configured')); continue; }
113
- try {
114
- const r = await navada.request(t.url, { timeout: 5000 });
115
- console.log(ui.online(t.name, r.status < 500));
116
- pass++;
117
- } catch {
118
- console.log(ui.online(t.name, false, 'unreachable'));
119
- }
120
- }
121
91
  console.log('');
122
92
 
123
- // 6. Summary
124
- console.log(ui.box('SETUP COMPLETE', ` ${pass}/${tests.length} services connected\n Theme: ${config.getTheme()}\n Tier: ${config.getTier()}\n Config: ${config.CONFIG_FILE}`));
93
+ // 4. Done
94
+ console.log(ui.box('READY', ` Config saved: ${config.CONFIG_FILE}\n Type naturally to chat, or /help for commands.`));
125
95
  console.log('');
126
- console.log(ui.dim('Run `navada` to start, or `navada doctor` to test all connections.'));
127
96
 
128
97
  rl.close();
129
98
  }
130
99
 
131
100
  module.exports = function(reg) {
132
- reg('setup', 'Run onboarding wizard', async () => {
133
- await runSetup();
101
+ reg('setup', 'Run setup wizard', async () => {
102
+ await runSetup(true);
134
103
  }, { category: 'SYSTEM' });
135
104
  };
136
105
 
@@ -65,11 +65,32 @@ module.exports = function(reg) {
65
65
  }, { category: 'SYSTEM' });
66
66
 
67
67
  // --- /login ---
68
- reg('login', 'Set API key', (args) => {
69
- if (!args[0]) { console.log(ui.dim('Usage: /login <api-key>')); return; }
70
- config.setApiKey(args[0]);
71
- navada.init({ mcpApiKey: args[0], dashboardApiKey: args[0] });
72
- console.log(ui.success(`API key saved to ${config.CONFIG_FILE}`));
68
+ reg('login', 'Set API key (Anthropic, OpenAI, NAVADA Edge, or HuggingFace)', (args) => {
69
+ if (!args[0]) {
70
+ console.log(ui.dim('Usage: /login <api-key>'));
71
+ console.log(ui.dim('Accepts: sk-ant-... (Anthropic) | sk-... (OpenAI) | nv_edge_... (NAVADA) | hf_... (HuggingFace)'));
72
+ return;
73
+ }
74
+ const key = args[0].trim();
75
+ config.setApiKey(key);
76
+
77
+ if (key.startsWith('sk-ant')) {
78
+ config.set('anthropicKey', key);
79
+ console.log(ui.success('Anthropic key saved — full agent mode with tool use enabled'));
80
+ } else if (key.startsWith('hf_')) {
81
+ config.set('hfToken', key);
82
+ navada.init({ hfToken: key });
83
+ console.log(ui.success('HuggingFace key saved — Qwen Coder (FREE) enabled'));
84
+ } else if (key.startsWith('nv_edge')) {
85
+ navada.init({ mcpApiKey: key, dashboardApiKey: key });
86
+ console.log(ui.success('NAVADA Edge key saved — MCP + Dashboard access enabled'));
87
+ } else if (key.startsWith('sk-')) {
88
+ config.set('openaiKey', key);
89
+ navada.init({ openaiKey: key });
90
+ console.log(ui.success('OpenAI key saved — GPT-4o enabled'));
91
+ } else {
92
+ console.log(ui.success('Key saved'));
93
+ }
73
94
  }, { category: 'SYSTEM' });
74
95
 
75
96
  // --- /init ---
@@ -217,13 +238,67 @@ module.exports = function(reg) {
217
238
  }, { category: 'SYSTEM' });
218
239
 
219
240
  // --- /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; }
241
+ reg('email', 'Send email (SMTP or MCP)', async (args) => {
242
+ if (args[0] === 'setup') {
243
+ // Configure SMTP
244
+ const readline = require('readline');
245
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
246
+ const ask = (q) => new Promise(r => rl.question(q, r));
247
+
248
+ console.log(ui.header('EMAIL SETUP'));
249
+ console.log(ui.dim(' Providers: Hotmail (smtp-mail.outlook.com), Gmail (smtp.gmail.com), Zoho (smtp.zoho.eu)'));
250
+ console.log('');
251
+ const host = await ask(' SMTP host: ');
252
+ const port = await ask(' SMTP port [587]: ');
253
+ const user = await ask(' Email/username: ');
254
+ const pass = await ask(' Password/app password: ');
255
+ const from = await ask(' From address: ');
256
+ rl.close();
257
+
258
+ config.setSmtp({ host: host.trim(), port: parseInt(port.trim() || '587'), user: user.trim(), pass: pass.trim(), from: from.trim() || user.trim() });
259
+ console.log(ui.success('SMTP configured'));
260
+ return;
261
+ }
262
+
263
+ if (args.length < 3) {
264
+ console.log(ui.dim('Usage: /email <to> <subject> <body>'));
265
+ console.log(ui.dim('Setup: /email setup'));
266
+ return;
267
+ }
268
+
222
269
  const [to, subject, ...bodyParts] = args;
223
270
  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' });
271
+ const smtp = config.getSmtp();
272
+
273
+ // Try local SMTP first
274
+ if (smtp.host && smtp.user && smtp.pass) {
275
+ try {
276
+ const nodemailer = require('nodemailer');
277
+ const transport = nodemailer.createTransport({
278
+ host: smtp.host, port: smtp.port, secure: smtp.port === 465,
279
+ auth: { user: smtp.user, pass: smtp.pass },
280
+ });
281
+ await transport.sendMail({ from: smtp.from || smtp.user, to, subject, text: body });
282
+ console.log(ui.success(`Email sent to ${to} via ${smtp.host}`));
283
+ return;
284
+ } catch (e) {
285
+ console.log(ui.warn(`SMTP failed: ${e.message}. Trying MCP...`));
286
+ }
287
+ }
288
+
289
+ // Fall back to MCP
290
+ if (navada.config.mcp) {
291
+ try {
292
+ await navada.mcp.call('send-email', { to, subject, body });
293
+ console.log(ui.success(`Email sent to ${to} via MCP`));
294
+ return;
295
+ } catch (e) {
296
+ console.log(ui.error(`MCP email failed: ${e.message}`));
297
+ }
298
+ }
299
+
300
+ console.log(ui.error('No email provider configured. Run /email setup to configure SMTP.'));
301
+ }, { category: 'SYSTEM', subs: ['setup'] });
227
302
 
228
303
  // --- /activity ---
229
304
  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.1",
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"