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 +306 -0
- package/lib/cli.js +4 -0
- package/lib/commands/ai.js +33 -43
- package/lib/commands/system.js +59 -5
- package/lib/config.js +14 -1
- package/lib/registry.js +32 -17
- package/package.json +3 -2
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();
|
package/lib/commands/ai.js
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
15
|
+
if (!msg) { console.log(ui.dim('Just type naturally — no /command needed.')); return; }
|
|
12
16
|
const ora = require('ora');
|
|
13
|
-
const
|
|
17
|
+
const spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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) :
|
|
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:
|
|
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:
|
|
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'
|
|
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', '
|
|
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
|
|
112
|
+
console.log(ui.dim('Set with: /model claude'));
|
|
123
113
|
}
|
|
124
|
-
}, { category: 'AI', subs: ['auto', 'gpt-4o', 'gpt-4o-mini', 'qwen'
|
|
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(' ');
|
package/lib/commands/system.js
CHANGED
|
@@ -217,13 +217,67 @@ module.exports = function(reg) {
|
|
|
217
217
|
}, { category: 'SYSTEM' });
|
|
218
218
|
|
|
219
219
|
// --- /email ---
|
|
220
|
-
reg('email', 'Send email
|
|
221
|
-
if (args
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
const args = parts.slice(1);
|
|
26
|
+
const trimmed = input.trim();
|
|
27
|
+
if (!trimmed) return;
|
|
29
28
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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.
|
|
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"
|