navada-edge-cli 1.1.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/Dockerfile ADDED
@@ -0,0 +1,3 @@
1
+ FROM node:22-slim
2
+ RUN npm install -g navada-edge-cli@2.0.0
3
+ ENTRYPOINT ["navada"]
@@ -0,0 +1,10 @@
1
+ services:
2
+ cli:
3
+ build: .
4
+ image: navada-edge-cli:2.0.0
5
+ container_name: navada-edge-cli
6
+ env_file: .env
7
+ stdin_open: true
8
+ tty: true
9
+ ports:
10
+ - "${SERVE_PORT:-7800}:7800"
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
@@ -3,41 +3,56 @@
3
3
  const readline = require('readline');
4
4
  const navada = require('navada-edge-sdk');
5
5
  const ui = require('./ui');
6
- const auth = require('./auth');
7
- const { execute, commands } = require('./commands');
6
+ const config = require('./config');
7
+ const history = require('./history');
8
+ const { completer } = require('./completer');
9
+ const { execute, getCompletions } = require('./registry');
10
+ const { loadAll } = require('./commands/index');
11
+ const { reportTelemetry } = require('./agent');
8
12
 
9
13
  function applyConfig() {
10
- const config = auth.getConfig();
14
+ const cfg = config.getAll();
11
15
  const overrides = {};
12
- if (config.apiKey) { overrides.mcpApiKey = config.apiKey; overrides.dashboardApiKey = config.apiKey; }
13
- if (config.asus) overrides.asus = config.asus;
14
- if (config.hp) overrides.hp = config.hp;
15
- if (config.ec2) overrides.ec2 = config.ec2;
16
- if (config.oracle) overrides.oracle = config.oracle;
17
- if (config.mcp) overrides.mcp = config.mcp;
18
- if (config.dashboard) overrides.dashboard = config.dashboard;
19
- if (config.registry) overrides.registry = config.registry;
20
- if (config.lucas) overrides.lucas = config.lucas;
16
+ const map = {
17
+ apiKey: ['mcpApiKey', 'dashboardApiKey'],
18
+ asus: ['asus'], hp: ['hp'], ec2: ['ec2'], oracle: ['oracle'],
19
+ mcp: ['mcp'], dashboard: ['dashboard'], registry: ['registry'],
20
+ lucas: ['lucas'], portainer: ['portainer'],
21
+ cfAccountId: ['cfAccountId'], cfApiToken: ['cfApiToken'],
22
+ cfR2Bucket: ['cfR2Bucket'], cfDomain: ['cfDomain'],
23
+ cfZoneId: ['cfZoneId'], cfStreamSubdomain: ['cfStreamSubdomain'],
24
+ azureN8nUrl: ['azureN8nUrl'], azureN8nRg: ['azureN8nRg'], azureN8nApp: ['azureN8nApp'],
25
+ yoloUrl: ['yoloUrl'], hfToken: ['hfToken'], openaiKey: ['openaiKey'],
26
+ pgHost: ['pgHost'], pgPort: ['pgPort'], pgDb: ['pgDb'], pgUser: ['pgUser'], pgPass: ['pgPass'],
27
+ opencodePassword: ['opencodePassword'],
28
+ };
29
+
30
+ for (const [cfgKey, sdkKeys] of Object.entries(map)) {
31
+ if (cfg[cfgKey]) {
32
+ for (const sk of sdkKeys) overrides[sk] = cfg[cfgKey];
33
+ }
34
+ }
35
+
21
36
  if (Object.keys(overrides).length > 0) navada.init(overrides);
22
37
  }
23
38
 
24
39
  function showWelcome() {
25
40
  console.clear();
26
- console.log(ui.LOGO);
41
+ console.log(ui.banner());
27
42
  console.log(ui.dim('Type /help for commands. Tab to autocomplete. Ctrl+C to exit.'));
28
43
  console.log('');
29
44
  }
30
45
 
31
46
  function startRepl() {
47
+ const historyItems = history.load();
48
+
32
49
  const rl = readline.createInterface({
33
50
  input: process.stdin,
34
51
  output: process.stdout,
35
52
  prompt: ui.prompt(),
36
- completer: (line) => {
37
- const cmds = Object.keys(commands).map(c => '/' + c);
38
- const hits = cmds.filter(c => c.startsWith(line));
39
- return [hits.length ? hits : cmds, line];
40
- },
53
+ completer,
54
+ history: historyItems,
55
+ historySize: 1000,
41
56
  });
42
57
 
43
58
  rl.prompt();
@@ -45,6 +60,7 @@ function startRepl() {
45
60
  rl.on('line', async (line) => {
46
61
  const input = line.trim();
47
62
  if (input) {
63
+ history.append(input);
48
64
  await execute(input);
49
65
  }
50
66
  console.log('');
@@ -58,14 +74,26 @@ function startRepl() {
58
74
  }
59
75
 
60
76
  async function runDirect(args) {
61
- const input = args.join(' ');
62
- await execute(input);
77
+ await execute(args.join(' '));
63
78
  }
64
79
 
65
- function run(argv) {
80
+ async function run(argv) {
81
+ // Load all command modules
82
+ loadAll();
83
+
84
+ // Apply saved config
66
85
  applyConfig();
67
86
 
68
87
  if (argv.length === 0) {
88
+ // Check first run
89
+ if (config.isFirstRun()) {
90
+ reportTelemetry('install');
91
+ const { runSetup } = require('./commands/setup');
92
+ await runSetup();
93
+ return;
94
+ }
95
+ // Report session start
96
+ reportTelemetry('session_start');
69
97
  // Interactive mode
70
98
  showWelcome();
71
99
  startRepl();
@@ -73,24 +101,34 @@ function run(argv) {
73
101
  const pkg = require('../package.json');
74
102
  console.log(`navada-edge-cli v${pkg.version}`);
75
103
  } else if (argv[0] === '--help' || argv[0] === '-h') {
76
- console.log(ui.LOGO);
104
+ console.log(ui.banner());
77
105
  console.log(' Usage:');
78
- console.log(' navada Interactive mode');
106
+ console.log(' navada Interactive mode (first run triggers setup)');
107
+ console.log(' navada setup Run onboarding wizard');
79
108
  console.log(' navada status Ping all nodes');
109
+ console.log(' navada doctor Test all connections');
80
110
  console.log(' navada mcp tools List MCP tools');
81
111
  console.log(' navada registry List Docker images');
82
- console.log(' navada chat "question" Chat with GPT-4o');
112
+ console.log(' navada agents Show Lucas + Claude status');
113
+ console.log(' navada chat "question" Chat with AI');
83
114
  console.log(' navada login <key> Set API key');
115
+ console.log(' navada theme crow Switch theme');
84
116
  console.log(' navada --version Show version');
85
117
  console.log('');
86
118
  console.log(' Run `navada` with no args for interactive mode.');
87
119
  console.log('');
120
+ } else if (argv[0] === '--serve') {
121
+ const serve = require('./serve');
122
+ const port = parseInt(argv[1]) || config.getServePort();
123
+ serve.start(port);
88
124
  } else {
89
125
  // Direct command mode
90
- runDirect(argv).then(() => process.exit(0)).catch(e => {
126
+ try {
127
+ await runDirect(argv);
128
+ } catch (e) {
91
129
  console.log(ui.error(e.message));
92
130
  process.exit(1);
93
- });
131
+ }
94
132
  }
95
133
  }
96
134
 
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const navada = require('navada-edge-sdk');
4
+ const ui = require('../ui');
5
+
6
+ module.exports = function(reg) {
7
+
8
+ reg('agents', 'Show NAVADA Edge agents status', async () => {
9
+ console.log(ui.header('NAVADA EDGE AGENTS'));
10
+
11
+ // Lucas CTO
12
+ if (navada.config.lucas) {
13
+ try {
14
+ const r = await navada.request(navada.config.lucas + '/health', { timeout: 5000 });
15
+ console.log(ui.online('Lucas CTO', r.status < 500, `MCP ${navada.config.lucas}`));
16
+ } catch {
17
+ console.log(ui.online('Lucas CTO', false, 'unreachable'));
18
+ }
19
+ } else {
20
+ console.log(ui.online('Lucas CTO', false, 'not configured'));
21
+ }
22
+ console.log(ui.dim(' Tools: bash, ssh, docker_exec, deploy, read_file, write_file, list_files, network_status'));
23
+
24
+ console.log('');
25
+
26
+ // Claude CoS
27
+ if (navada.config.dashboard) {
28
+ try {
29
+ const r = await navada.request(navada.config.dashboard + '/api/agent-heartbeat', { timeout: 5000 });
30
+ console.log(ui.online('Claude CoS', r.status < 500, `Dashboard ${navada.config.dashboard}`));
31
+ } catch {
32
+ console.log(ui.online('Claude CoS', false, 'unreachable'));
33
+ }
34
+ } else {
35
+ console.log(ui.online('Claude CoS', false, 'not configured'));
36
+ }
37
+ console.log(ui.dim(' Controls: Telegram, email, SMS, image gen, R2, cost tracking'));
38
+
39
+ console.log('');
40
+ // MCP Server
41
+ if (navada.config.mcp) {
42
+ try {
43
+ const r = await navada.request(navada.config.mcp + '/health', { timeout: 5000 });
44
+ console.log(ui.online('MCP Server', r.status < 500, `${navada.config.mcp}`));
45
+ if (r.data?.version) console.log(ui.dim(` Version: ${r.data.version}`));
46
+ } catch {
47
+ console.log(ui.online('MCP Server', false, 'unreachable'));
48
+ }
49
+ } else {
50
+ console.log(ui.online('MCP Server', false, 'not configured'));
51
+ }
52
+ }, { category: 'AGENTS' });
53
+
54
+ reg('claude', 'Send message to Claude CoS', async (args) => {
55
+ const msg = args.join(' ');
56
+ if (!msg) { console.log(ui.dim('Usage: /claude Check HP disk space')); return; }
57
+ const ora = require('ora');
58
+ const spinner = ora({ text: ' Asking Claude...', color: 'white' }).start();
59
+ try {
60
+ // Try via MCP first
61
+ const result = await navada.mcp.call('chat', { message: msg, agent: 'claude' });
62
+ spinner.stop();
63
+ console.log(ui.header('CLAUDE'));
64
+ console.log(typeof result === 'object' ? ui.jsonColorize(result) : ` ${result}`);
65
+ } catch {
66
+ spinner.stop();
67
+ console.log(ui.warn('Claude agent not available via MCP. Try /lucas exec for direct commands.'));
68
+ }
69
+ }, { category: 'AGENTS' });
70
+ };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const navada = require('navada-edge-sdk');
4
+ const ui = require('../ui');
5
+ const config = require('../config');
6
+ const { chat: agentChat, reportTelemetry } = require('../agent');
7
+
8
+ module.exports = function(reg) {
9
+
10
+ // Conversation history for multi-turn
11
+ const conversationHistory = [];
12
+
13
+ reg('chat', 'Chat with NAVADA Edge AI agent', async (args) => {
14
+ const msg = args.join(' ');
15
+ if (!msg) { console.log(ui.dim('Just type naturally — no /command needed.')); return; }
16
+ const ora = require('ora');
17
+ const spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
18
+
19
+ try {
20
+ const response = await agentChat(msg, conversationHistory);
21
+ spinner.stop();
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'));
31
+ console.log(` ${response}`);
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.'));
39
+ }
40
+ }, { category: 'AI', aliases: ['ask'] });
41
+
42
+ reg('qwen', 'Qwen Coder 32B (FREE via HuggingFace)', async (args) => {
43
+ const prompt = args.join(' ');
44
+ if (!prompt) { console.log(ui.dim('Usage: /qwen Write a function to validate UK postcodes')); return; }
45
+ const ora = require('ora');
46
+ const spinner = ora({ text: ' Qwen thinking...', color: 'white' }).start();
47
+ const result = await navada.ai.huggingface.qwen(prompt);
48
+ spinner.stop();
49
+ console.log(ui.header('QWEN CODER'));
50
+ console.log(` ${typeof result === 'object' ? ui.jsonColorize(result) : result}`);
51
+ }, { category: 'AI' });
52
+
53
+ reg('yolo', 'YOLO object detection', async (args) => {
54
+ if (args[0] === 'detect' && args[1]) {
55
+ const ora = require('ora');
56
+ const spinner = ora({ text: ' Detecting...', color: 'white' }).start();
57
+ const result = await navada.ai.yolo.detect(args[1]);
58
+ spinner.stop();
59
+ console.log(ui.header(`DETECTIONS (${result.count || 0})`));
60
+ if (result.detections) {
61
+ result.detections.forEach(d => console.log(ui.label(d.class, `${(d.confidence * 100).toFixed(1)}%`)));
62
+ }
63
+ console.log(ui.dim(`Inference: ${result.inference_ms}ms`));
64
+ } else if (args[0] === 'model') {
65
+ const model = await navada.ai.yolo.model();
66
+ console.log(ui.header('YOLO MODEL'));
67
+ console.log(ui.jsonColorize(model));
68
+ } else {
69
+ const health = await navada.ai.yolo.health();
70
+ console.log(ui.header('YOLO'));
71
+ console.log(ui.online('YOLO Service', health.status === 'ok'));
72
+ }
73
+ }, { category: 'AI', subs: ['detect', 'model'] });
74
+
75
+ reg('image', 'Generate an image', async (args) => {
76
+ const prompt = args.filter(a => !a.startsWith('--')).join(' ');
77
+ if (!prompt) { console.log(ui.dim('Usage: /image a futuristic city | /image --dalle NAVADA logo')); return; }
78
+ const useDalle = args.includes('--dalle');
79
+ const ora = require('ora');
80
+
81
+ if (useDalle && navada.config.openaiKey) {
82
+ const spinner = ora({ text: ' DALL-E generating...', color: 'white' }).start();
83
+ const result = await navada.ai.openai.image(prompt);
84
+ spinner.stop();
85
+ console.log(ui.success(`Generated: ${result.url || result.revised_prompt || 'done'}`));
86
+ } else {
87
+ const spinner = ora({ text: ' Flux generating (FREE)...', color: 'white' }).start();
88
+ const savePath = `navada-image-${Date.now()}.png`;
89
+ const { size } = await navada.cloudflare.flux.generate(prompt, { savePath });
90
+ spinner.stop();
91
+ console.log(ui.success(`Generated: ${savePath} (${(size / 1024).toFixed(1)} KB)`));
92
+ }
93
+ }, { category: 'AI' });
94
+
95
+ reg('model', 'Show/set default AI model', (args) => {
96
+ if (args[0]) {
97
+ const valid = ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen'];
98
+ if (!valid.includes(args[0])) { console.log(ui.error(`Invalid model. Options: ${valid.join(', ')}`)); return; }
99
+ config.setModel(args[0]);
100
+ console.log(ui.success(`Model set to: ${args[0]}`));
101
+ } else {
102
+ console.log(ui.header('AI MODELS'));
103
+ const current = config.getModel();
104
+ console.log(ui.label('Current', current));
105
+ console.log('');
106
+ console.log(ui.dim('Available:'));
107
+ console.log(ui.label('auto', 'Claude (Anthropic) with tool use — default'));
108
+ console.log(ui.label('claude', 'Claude Sonnet 4 via Anthropic API'));
109
+ console.log(ui.label('gpt-4o', 'OpenAI GPT-4o (requires OPENAI_API_KEY)'));
110
+ console.log(ui.label('qwen', 'Qwen Coder 32B (FREE via HuggingFace)'));
111
+ console.log('');
112
+ console.log(ui.dim('Set with: /model claude'));
113
+ }
114
+ }, { category: 'AI', subs: ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen'] });
115
+
116
+ reg('research', 'RAG search via MCP', async (args) => {
117
+ const query = args.join(' ');
118
+ if (!query) { console.log(ui.dim('Usage: /research Docker networking best practices')); return; }
119
+ const ora = require('ora');
120
+ const spinner = ora({ text: ' Searching...', color: 'white' }).start();
121
+ const result = await navada.mcp.call('rag-search', { query });
122
+ spinner.stop();
123
+ console.log(ui.header('RESEARCH'));
124
+ console.log(typeof result === 'object' ? ui.jsonColorize(result) : ` ${result}`);
125
+ }, { category: 'AI' });
126
+ };