navada-edge-cli 2.4.1 → 2.5.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/bin/navada.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const [major] = process.versions.node.split('.').map(Number);
5
+ if (major < 18) {
6
+ console.error(`\nNAVADA Edge CLI requires Node.js >= 18 (you have ${process.version}).\nUpdate: https://nodejs.org/\n`);
7
+ process.exit(1);
8
+ }
9
+
4
10
  const { run } = require('../lib/cli');
5
11
  run(process.argv.slice(2));
package/lib/agent.js CHANGED
@@ -4,6 +4,8 @@ const { execSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const https = require('https');
8
+ const http = require('http');
7
9
  const navada = require('navada-edge-sdk');
8
10
  const ui = require('./ui');
9
11
  const config = require('./config');
@@ -26,6 +28,36 @@ When you don't have a tool for something, say so clearly and suggest an alternat
26
28
  Keep responses short. Code blocks when needed. No fluff.`,
27
29
  };
28
30
 
31
+ // ---------------------------------------------------------------------------
32
+ // Rate limit tracking (in-memory, per session)
33
+ // ---------------------------------------------------------------------------
34
+ const rateTracker = {
35
+ requests: [], // timestamps of recent requests
36
+ windowMs: 60000, // 1 minute window
37
+ limit: 30,
38
+
39
+ record() {
40
+ const now = Date.now();
41
+ this.requests.push(now);
42
+ this.cleanup();
43
+ },
44
+
45
+ cleanup() {
46
+ const cutoff = Date.now() - this.windowMs;
47
+ this.requests = this.requests.filter(t => t > cutoff);
48
+ },
49
+
50
+ remaining() {
51
+ this.cleanup();
52
+ return Math.max(0, this.limit - this.requests.length);
53
+ },
54
+
55
+ used() {
56
+ this.cleanup();
57
+ return this.requests.length;
58
+ },
59
+ };
60
+
29
61
  // ---------------------------------------------------------------------------
30
62
  // Local tools — run on the USER's machine
31
63
  // ---------------------------------------------------------------------------
@@ -97,17 +129,18 @@ const localTools = {
97
129
  // ---------------------------------------------------------------------------
98
130
  // Free tier — proxied through NAVADA Edge Dashboard (key stays on ASUS)
99
131
  // ---------------------------------------------------------------------------
100
- // Dashboard proxy endpoints — tries Cloudflare tunnel first (works for everyone),
101
- // then falls back to direct Tailscale IP (faster but only works on VPN)
102
132
  const FREE_TIER_ENDPOINTS = [
103
133
  'https://api.navada-edge-server.uk/api/v1/chat', // Cloudflare tunnel (public, works for all)
104
134
  'http://100.88.118.128:7900/api/v1/chat', // Direct Tailscale (VPN users only)
105
135
  ];
106
136
 
107
- async function callFreeTier(messages) {
108
- // Try each endpoint (public first, then Tailscale)
137
+ async function callFreeTier(messages, stream = false) {
109
138
  for (const endpoint of FREE_TIER_ENDPOINTS) {
110
139
  try {
140
+ if (stream) {
141
+ return await streamFreeTier(endpoint, messages);
142
+ }
143
+
111
144
  const r = await navada.request(endpoint, {
112
145
  method: 'POST',
113
146
  body: { messages },
@@ -116,7 +149,7 @@ async function callFreeTier(messages) {
116
149
 
117
150
  if (r.status === 429) {
118
151
  return {
119
- content: `Free tier limit reached (30 requests/min).
152
+ content: `Free tier limit reached (${rateTracker.used()}/${rateTracker.limit} RPM used).
120
153
 
121
154
  To continue, add your own API key:
122
155
  /login sk-ant-your-anthropic-key (full agent with tool use)
@@ -129,37 +162,256 @@ Or wait a minute and try again.`,
129
162
  }
130
163
 
131
164
  if (r.status === 200) {
165
+ rateTracker.record();
132
166
  return { content: r.data?.choices?.[0]?.message?.content || '', isRateLimit: false };
133
167
  }
134
168
  } catch {
135
- continue; // Try next endpoint
169
+ continue;
136
170
  }
137
171
  }
138
172
 
139
- throw new Error('Free tier unavailable. Add your own key: /login <key>');
173
+ return {
174
+ content: `NAVADA Edge server is currently offline.
175
+
176
+ Options:
177
+ /login sk-ant-your-key Use your own Anthropic key (works offline from our server)
178
+ /login sk-your-key Use OpenAI key
179
+ /doctor Check which services are reachable
180
+
181
+ The free tier requires the NAVADA Edge server to be online.
182
+ It will come back automatically — try again in a few minutes.`,
183
+ isRateLimit: false,
184
+ };
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Streaming — free tier (Grok via dashboard)
189
+ // ---------------------------------------------------------------------------
190
+ function streamFreeTier(endpoint, messages) {
191
+ return new Promise((resolve, reject) => {
192
+ const url = new URL(endpoint);
193
+ const transport = url.protocol === 'https:' ? https : http;
194
+ const body = JSON.stringify({ messages, stream: true });
195
+
196
+ const req = transport.request(url, {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
199
+ timeout: endpoint.includes('navada-edge-server.uk') ? 30000 : 5000,
200
+ }, (res) => {
201
+ // If server doesn't support streaming, collect full response
202
+ if (!res.headers['content-type']?.includes('text/event-stream')) {
203
+ let data = '';
204
+ res.on('data', c => data += c);
205
+ res.on('end', () => {
206
+ try {
207
+ const parsed = JSON.parse(data);
208
+ if (res.statusCode === 429) {
209
+ resolve({ content: `Free tier limit reached (${rateTracker.used()}/${rateTracker.limit} RPM).`, isRateLimit: true });
210
+ } else if (res.statusCode === 200) {
211
+ rateTracker.record();
212
+ resolve({ content: parsed?.choices?.[0]?.message?.content || '', isRateLimit: false });
213
+ } else {
214
+ reject(new Error(`HTTP ${res.statusCode}`));
215
+ }
216
+ } catch { reject(new Error(`HTTP ${res.statusCode}`)); }
217
+ });
218
+ return;
219
+ }
220
+
221
+ // SSE streaming
222
+ rateTracker.record();
223
+ let fullContent = '';
224
+ let buffer = '';
225
+
226
+ res.on('data', (chunk) => {
227
+ buffer += chunk.toString();
228
+ const lines = buffer.split('\n');
229
+ buffer = lines.pop(); // keep incomplete line
230
+
231
+ for (const line of lines) {
232
+ if (!line.startsWith('data: ')) continue;
233
+ const data = line.slice(6).trim();
234
+ if (data === '[DONE]') continue;
235
+ try {
236
+ const parsed = JSON.parse(data);
237
+ const delta = parsed.choices?.[0]?.delta?.content || '';
238
+ if (delta) {
239
+ process.stdout.write(delta);
240
+ fullContent += delta;
241
+ }
242
+ } catch {}
243
+ }
244
+ });
245
+
246
+ res.on('end', () => {
247
+ if (fullContent) process.stdout.write('\n');
248
+ resolve({ content: fullContent, isRateLimit: false, streamed: true });
249
+ });
250
+ });
251
+
252
+ req.on('error', reject);
253
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
254
+ req.write(body);
255
+ req.end();
256
+ });
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Streaming — Anthropic Claude API
261
+ // ---------------------------------------------------------------------------
262
+ function streamAnthropic(key, messages, tools, system) {
263
+ return new Promise((resolve, reject) => {
264
+ const body = JSON.stringify({
265
+ model: 'claude-sonnet-4-20250514',
266
+ max_tokens: 4096,
267
+ system: system || IDENTITY.personality,
268
+ messages,
269
+ tools,
270
+ stream: true,
271
+ });
272
+
273
+ const req = https.request('https://api.anthropic.com/v1/messages', {
274
+ method: 'POST',
275
+ headers: {
276
+ 'x-api-key': key,
277
+ 'anthropic-version': '2023-06-01',
278
+ 'Content-Type': 'application/json',
279
+ 'Content-Length': Buffer.byteLength(body),
280
+ },
281
+ timeout: 120000,
282
+ }, (res) => {
283
+ if (res.statusCode !== 200) {
284
+ let data = '';
285
+ res.on('data', c => data += c);
286
+ res.on('end', () => reject(new Error(`Anthropic API error ${res.statusCode}: ${data.slice(0, 200)}`)));
287
+ return;
288
+ }
289
+
290
+ let buffer = '';
291
+ const contentBlocks = [];
292
+ let currentText = '';
293
+ let stopReason = null;
294
+
295
+ res.on('data', (chunk) => {
296
+ buffer += chunk.toString();
297
+ const lines = buffer.split('\n');
298
+ buffer = lines.pop();
299
+
300
+ for (const line of lines) {
301
+ if (!line.startsWith('data: ')) continue;
302
+ try {
303
+ const event = JSON.parse(line.slice(6));
304
+
305
+ switch (event.type) {
306
+ case 'content_block_start':
307
+ if (event.content_block?.type === 'text') {
308
+ currentText = '';
309
+ } else if (event.content_block?.type === 'tool_use') {
310
+ contentBlocks.push({ type: 'tool_use', id: event.content_block.id, name: event.content_block.name, input: '' });
311
+ }
312
+ break;
313
+
314
+ case 'content_block_delta':
315
+ if (event.delta?.type === 'text_delta') {
316
+ process.stdout.write(event.delta.text);
317
+ currentText += event.delta.text;
318
+ } else if (event.delta?.type === 'input_json_delta') {
319
+ const last = contentBlocks[contentBlocks.length - 1];
320
+ if (last?.type === 'tool_use') last.input += event.delta.partial_json;
321
+ }
322
+ break;
323
+
324
+ case 'content_block_stop':
325
+ if (currentText) {
326
+ contentBlocks.push({ type: 'text', text: currentText });
327
+ currentText = '';
328
+ }
329
+ // Parse tool input JSON
330
+ const lastBlock = contentBlocks[contentBlocks.length - 1];
331
+ if (lastBlock?.type === 'tool_use' && typeof lastBlock.input === 'string') {
332
+ try { lastBlock.input = JSON.parse(lastBlock.input); } catch { lastBlock.input = {}; }
333
+ }
334
+ break;
335
+
336
+ case 'message_delta':
337
+ stopReason = event.delta?.stop_reason;
338
+ break;
339
+ }
340
+ } catch {}
341
+ }
342
+ });
343
+
344
+ res.on('end', () => {
345
+ if (contentBlocks.some(b => b.type === 'text')) process.stdout.write('\n');
346
+ resolve({ content: contentBlocks, stop_reason: stopReason });
347
+ });
348
+ });
349
+
350
+ req.on('error', reject);
351
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
352
+ req.write(body);
353
+ req.end();
354
+ });
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Smart model routing — detect intent and pick best provider
359
+ // ---------------------------------------------------------------------------
360
+ const CODE_PATTERNS = /\b(function|class|const |let |var |import |export |def |async |await |return |console\.|print\(|=>|\.map\(|\.filter\(|\.reduce\(|npm |pip |git |docker |curl |ssh |sudo )\b|```|\.js\b|\.py\b|\.ts\b/i;
361
+ const COMPLEX_PATTERNS = /\b(explain|analyze|compare|design|architect|plan|review|debug|refactor|deploy|migrate|optimize)\b/i;
362
+
363
+ function detectIntent(message) {
364
+ const lower = message.toLowerCase();
365
+ const isCode = CODE_PATTERNS.test(message);
366
+ const isComplex = COMPLEX_PATTERNS.test(lower) || message.length > 500;
367
+ const isAction = /\b(run|execute|create|delete|install|start|stop|restart|build|push|pull)\b/i.test(lower);
368
+
369
+ if (isAction) return 'action'; // needs tool use → Anthropic
370
+ if (isCode && !isComplex) return 'code'; // code help → Qwen (free) or Grok
371
+ if (isComplex) return 'complex'; // deep analysis → Anthropic
372
+ return 'general'; // general chat → Grok (free)
140
373
  }
141
374
 
142
375
  // ---------------------------------------------------------------------------
143
376
  // Anthropic Claude API — conversational agent with tool use
144
377
  // ---------------------------------------------------------------------------
145
378
  async function chat(userMessage, conversationHistory = []) {
146
- // Smart key detection — check all possible locations
147
379
  const anthropicKey = config.get('anthropicKey')
148
- || config.getApiKey() // /setup saves here
380
+ || config.getApiKey()
149
381
  || process.env.ANTHROPIC_API_KEY
150
382
  || '';
151
383
 
152
- // Auto-detect: if the key starts with sk-ant, it's Anthropic
153
384
  const apiKey = config.getApiKey();
154
385
  const effectiveKey = anthropicKey
155
386
  || (apiKey && apiKey.startsWith('sk-ant') ? apiKey : '')
156
387
  || '';
157
388
 
158
- // No personal key use Grok free tier
389
+ // Smart routing when model is set to 'auto'
390
+ const modelPref = config.getModel();
391
+ const intent = detectIntent(userMessage);
392
+
393
+ // No personal key — use free tier
159
394
  if (!effectiveKey) {
395
+ // Try Qwen for code if HuggingFace token available
396
+ if (intent === 'code' && navada.config.hfToken) {
397
+ try {
398
+ const r = await navada.ai.huggingface.qwen(userMessage);
399
+ return typeof r === 'object' ? JSON.stringify(r) : r;
400
+ } catch {}
401
+ }
160
402
  return grokChat(userMessage, conversationHistory);
161
403
  }
162
404
 
405
+ // Has Anthropic key but could route to cheaper options for simple queries
406
+ if (modelPref === 'auto' && intent === 'general' && !conversationHistory.length) {
407
+ // General questions don't need tool use — use Grok free tier to save API cost
408
+ try {
409
+ const result = await callFreeTier([{ role: 'user', content: userMessage }], true);
410
+ if (result.content) return result.content;
411
+ } catch {}
412
+ // Fall through to Anthropic if free tier fails
413
+ }
414
+
163
415
  const tools = [
164
416
  {
165
417
  name: 'shell',
@@ -233,8 +485,8 @@ async function chat(userMessage, conversationHistory = []) {
233
485
  { role: 'user', content: userMessage },
234
486
  ];
235
487
 
236
- // Call Anthropic API
237
- let response = await callAnthropic(effectiveKey, messages, tools);
488
+ // Stream the response
489
+ let response = await streamAnthropic(effectiveKey, messages, tools);
238
490
 
239
491
  // Handle tool use loop
240
492
  let iterations = 0;
@@ -244,53 +496,21 @@ async function chat(userMessage, conversationHistory = []) {
244
496
  const results = [];
245
497
 
246
498
  for (const block of toolBlocks) {
247
- // Print what the agent is doing
248
499
  console.log(ui.dim(` [${block.name}] ${JSON.stringify(block.input).slice(0, 80)}`));
249
500
  const result = await executeTool(block.name, block.input);
250
501
  results.push({ type: 'tool_result', tool_use_id: block.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
251
502
  }
252
503
 
253
- // Print any text blocks
254
- for (const block of response.content) {
255
- if (block.type === 'text' && block.text) console.log(` ${block.text}`);
256
- }
257
-
258
- // Continue conversation with tool results
259
504
  messages.push({ role: 'assistant', content: response.content });
260
505
  messages.push({ role: 'user', content: results });
261
- response = await callAnthropic(anthropicKey, messages, tools);
506
+ response = await streamAnthropic(effectiveKey, messages, tools);
262
507
  }
263
508
 
264
- // Extract final text
509
+ // Extract final text (already streamed to stdout)
265
510
  const textBlocks = response.content?.filter(b => b.type === 'text') || [];
266
511
  return textBlocks.map(b => b.text).join('\n');
267
512
  }
268
513
 
269
- async function callAnthropic(key, messages, tools) {
270
- const r = await navada.request('https://api.anthropic.com/v1/messages', {
271
- method: 'POST',
272
- body: {
273
- model: 'claude-sonnet-4-20250514',
274
- max_tokens: 4096,
275
- system: IDENTITY.personality,
276
- messages,
277
- tools,
278
- },
279
- headers: {
280
- 'x-api-key': key,
281
- 'anthropic-version': '2023-06-01',
282
- 'Content-Type': 'application/json',
283
- },
284
- timeout: 60000,
285
- });
286
-
287
- if (r.status !== 200) {
288
- throw new Error(`Anthropic API error ${r.status}: ${JSON.stringify(r.data).slice(0, 200)}`);
289
- }
290
-
291
- return r.data;
292
- }
293
-
294
514
  async function executeTool(name, input) {
295
515
  try {
296
516
  switch (name) {
@@ -328,12 +548,16 @@ async function grokChat(userMessage, conversationHistory = []) {
328
548
  { role: 'user', content: userMessage },
329
549
  ];
330
550
 
331
- const result = await callFreeTier(messages);
551
+ // Try streaming first
552
+ const result = await callFreeTier(messages, true);
553
+ if (result.streamed) {
554
+ // Already printed to stdout, return for history
555
+ return result.content || 'No response from free tier. Try /login <key> for full agent.';
556
+ }
332
557
  return result.content || 'No response from free tier. Try /login <key> for full agent.';
333
558
  }
334
559
 
335
560
  async function fallbackChat(msg) {
336
- // Try MCP → Qwen → OpenAI → Grok free tier
337
561
  if (navada.config.mcp) {
338
562
  try {
339
563
  const r = await navada.mcp.call('chat', { message: msg });
@@ -349,7 +573,6 @@ async function fallbackChat(msg) {
349
573
  if (navada.config.openaiKey) {
350
574
  try { return await navada.ai.openai.chat(msg); } catch {}
351
575
  }
352
- // Grok is always available as the free fallback
353
576
  try {
354
577
  const result = await callFreeTier([{ role: 'user', content: msg }]);
355
578
  return result.content;
@@ -358,28 +581,53 @@ async function fallbackChat(msg) {
358
581
  }
359
582
 
360
583
  // ---------------------------------------------------------------------------
361
- // Telemetry track installs + usage
584
+ // Update checker (non-blocking, runs once per session)
362
585
  // ---------------------------------------------------------------------------
363
- async function reportTelemetry(event, data = {}) {
364
- if (!navada.config.dashboard) return;
586
+ let _updateInfo = null;
587
+
588
+ async function checkForUpdate() {
365
589
  try {
366
- await navada.request(navada.config.dashboard + '/api/agent-heartbeat', {
367
- method: 'POST',
368
- body: {
369
- agent: 'navada-edge-cli',
370
- event,
371
- version: require('../package.json').version,
372
- hostname: os.hostname(),
373
- platform: os.platform(),
374
- arch: os.arch(),
375
- nodeVersion: process.version,
376
- apiKey: config.getApiKey()?.slice(0, 8) || 'none',
377
- ts: new Date().toISOString(),
378
- ...data,
379
- },
380
- timeout: 5000,
381
- });
590
+ const pkg = require('../package.json');
591
+ const r = await navada.request('https://registry.npmjs.org/navada-edge-cli/latest', { timeout: 5000 });
592
+ if (r.status === 200 && r.data?.version && r.data.version !== pkg.version) {
593
+ _updateInfo = { current: pkg.version, latest: r.data.version };
594
+ }
382
595
  } catch {}
383
596
  }
384
597
 
385
- module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat };
598
+ function getUpdateInfo() { return _updateInfo; }
599
+
600
+ // ---------------------------------------------------------------------------
601
+ // Telemetry — track installs + usage
602
+ // ---------------------------------------------------------------------------
603
+ async function reportTelemetry(event, data = {}) {
604
+ // Try dashboard first, then public tunnel
605
+ const endpoints = [
606
+ 'http://100.88.118.128:7900',
607
+ 'https://api.navada-edge-server.uk',
608
+ ];
609
+
610
+ for (const base of endpoints) {
611
+ try {
612
+ await navada.request(base + '/api/agent-heartbeat', {
613
+ method: 'POST',
614
+ body: {
615
+ agent: 'navada-edge-cli',
616
+ event,
617
+ version: require('../package.json').version,
618
+ hostname: require('crypto').createHash('sha256').update(os.hostname()).digest('hex').slice(0, 12),
619
+ platform: os.platform(),
620
+ arch: os.arch(),
621
+ nodeVersion: process.version,
622
+ tier: config.getTier(),
623
+ ts: new Date().toISOString(),
624
+ ...data,
625
+ },
626
+ timeout: 3000,
627
+ });
628
+ return; // success
629
+ } catch { continue; }
630
+ }
631
+ }
632
+
633
+ module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker };
package/lib/cli.js CHANGED
@@ -8,7 +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
+ const { reportTelemetry, checkForUpdate, getUpdateInfo, rateTracker } = require('./agent');
12
12
 
13
13
  function applyConfig() {
14
14
  const cfg = config.getAll();
@@ -43,6 +43,23 @@ function showWelcome() {
43
43
  console.log('');
44
44
  }
45
45
 
46
+ function showTierInfo() {
47
+ const hasPersonalKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
48
+ if (!hasPersonalKey) {
49
+ console.log(ui.dim(`Free tier active (Grok, ${rateTracker.remaining()}/${rateTracker.limit} RPM). /login <key> for full agent.`));
50
+ console.log('');
51
+ }
52
+ }
53
+
54
+ function showUpdateBanner() {
55
+ const info = getUpdateInfo();
56
+ if (info) {
57
+ console.log(ui.warn(`Update available: v${info.current} → v${info.latest}`));
58
+ console.log(ui.dim('Run: npm install -g navada-edge-cli'));
59
+ console.log('');
60
+ }
61
+ }
62
+
46
63
  function startRepl() {
47
64
  const historyItems = history.load();
48
65
 
@@ -62,6 +79,15 @@ function startRepl() {
62
79
  if (input) {
63
80
  history.append(input);
64
81
  await execute(input);
82
+
83
+ // Show rate limit after free tier usage
84
+ const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
85
+ if (!hasKey && rateTracker.used() > 0) {
86
+ const remaining = rateTracker.remaining();
87
+ if (remaining <= 5 && remaining > 0) {
88
+ console.log(ui.warn(`${remaining} free requests remaining this minute`));
89
+ }
90
+ }
65
91
  }
66
92
  console.log('');
67
93
  rl.prompt();
@@ -85,20 +111,19 @@ async function run(argv) {
85
111
  applyConfig();
86
112
 
87
113
  if (argv.length === 0) {
88
- // Report telemetry
114
+ // Check for updates (non-blocking)
115
+ checkForUpdate().then(() => {
116
+ // Show update banner after a short delay (if in REPL)
117
+ setTimeout(showUpdateBanner, 2000);
118
+ });
119
+
120
+ // Report telemetry (non-blocking)
89
121
  if (config.isFirstRun()) reportTelemetry('install');
90
122
  else reportTelemetry('session_start');
91
123
 
92
- // Interactive mode — always start. User can /setup if needed.
124
+ // Interactive mode
93
125
  showWelcome();
94
-
95
- // Show tier info
96
- const hasPersonalKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
97
- if (!hasPersonalKey) {
98
- console.log(ui.dim('Free tier active (Grok). /login <your-key> for full agent with tool use.'));
99
- console.log('');
100
- }
101
-
126
+ showTierInfo();
102
127
  startRepl();
103
128
  } else if (argv[0] === '--version' || argv[0] === '-v') {
104
129
  const pkg = require('../package.json');
@@ -106,7 +131,7 @@ async function run(argv) {
106
131
  } else if (argv[0] === '--help' || argv[0] === '-h') {
107
132
  console.log(ui.banner());
108
133
  console.log(' Usage:');
109
- console.log(' navada Interactive mode (first run triggers setup)');
134
+ console.log(' navada Interactive mode');
110
135
  console.log(' navada setup Run onboarding wizard');
111
136
  console.log(' navada status Ping all nodes');
112
137
  console.log(' navada doctor Test all connections');
@@ -116,6 +141,7 @@ async function run(argv) {
116
141
  console.log(' navada chat "question" Chat with AI');
117
142
  console.log(' navada login <key> Set API key');
118
143
  console.log(' navada theme crow Switch theme');
144
+ console.log(' navada upgrade Check for updates');
119
145
  console.log(' navada --version Show version');
120
146
  console.log('');
121
147
  console.log(' Run `navada` with no args for interactive mode.');
@@ -3,7 +3,7 @@
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
+ const { chat: agentChat, reportTelemetry, rateTracker } = require('../agent');
7
7
 
8
8
  module.exports = function(reg) {
9
9
 
@@ -13,12 +13,21 @@ module.exports = function(reg) {
13
13
  reg('chat', 'Chat with NAVADA Edge AI agent', async (args) => {
14
14
  const msg = args.join(' ');
15
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();
16
+
17
+ const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
18
+
19
+ // Show a brief "thinking" indicator, then clear it when streaming starts
20
+ if (!hasKey) {
21
+ process.stdout.write(ui.dim(' NAVADA > '));
22
+ } else {
23
+ const ora = require('ora');
24
+ var spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
25
+ }
18
26
 
19
27
  try {
20
28
  const response = await agentChat(msg, conversationHistory);
21
- spinner.stop();
29
+
30
+ if (spinner) spinner.stop();
22
31
 
23
32
  // Update conversation history
24
33
  conversationHistory.push({ role: 'user', content: msg });
@@ -27,13 +36,16 @@ module.exports = function(reg) {
27
36
  // Keep history manageable (last 20 turns)
28
37
  while (conversationHistory.length > 40) conversationHistory.splice(0, 2);
29
38
 
30
- console.log(ui.header('NAVADA'));
31
- console.log(` ${response}`);
39
+ // Only print if not already streamed
40
+ if (!response._streamed) {
41
+ console.log(ui.header('NAVADA'));
42
+ console.log(` ${response}`);
43
+ }
32
44
 
33
45
  // Track usage
34
46
  reportTelemetry('chat', { messageLength: msg.length });
35
47
  } catch (e) {
36
- spinner.stop();
48
+ if (spinner) spinner.stop();
37
49
  console.log(ui.error(e.message));
38
50
  console.log(ui.dim('Check: /config to see which providers are set, or /setup to configure.'));
39
51
  }
package/lib/ui.js CHANGED
@@ -28,7 +28,8 @@ function banner() {
28
28
  style('banner3', '╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝'),
29
29
  ];
30
30
 
31
- const titleLine = style('header', 'E D G E N E T W O R K') + ' ' + style('dim', 'v2.0.0');
31
+ const ver = require('../package.json').version;
32
+ const titleLine = style('header', 'E D G E N E T W O R K') + ' ' + style('dim', `v${ver}`);
32
33
 
33
34
  let out = '\n' + topBorder + '\n';
34
35
  out += sp + side + ' '.repeat(w) + side + '\n';
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "navada-edge-cli",
3
- "version": "2.4.1",
3
+ "version": "2.5.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": {
7
- "navada": "./bin/navada.js"
7
+ "navada": "bin/navada.js"
8
8
  },
9
9
  "type": "commonjs",
10
10
  "engines": {
@@ -15,8 +15,19 @@
15
15
  "test": "node -e \"require('./lib/registry'); require('./lib/commands/index').loadAll(); console.log('OK — ' + Object.keys(require('./lib/registry').commands).length + ' commands loaded');\""
16
16
  },
17
17
  "keywords": [
18
- "navada", "edge", "cli", "tui", "distributed", "cloudflare",
19
- "docker", "ai", "mcp", "sdk", "agents", "yolo", "terminal"
18
+ "navada",
19
+ "edge",
20
+ "cli",
21
+ "tui",
22
+ "distributed",
23
+ "cloudflare",
24
+ "docker",
25
+ "ai",
26
+ "mcp",
27
+ "sdk",
28
+ "agents",
29
+ "yolo",
30
+ "terminal"
20
31
  ],
21
32
  "author": {
22
33
  "name": "Leslie Akpareva",
@@ -27,7 +38,7 @@
27
38
  "homepage": "https://github.com/Navada25/edge-sdk",
28
39
  "repository": {
29
40
  "type": "git",
30
- "url": "https://github.com/Navada25/edge-sdk.git"
41
+ "url": "git+https://github.com/Navada25/edge-sdk.git"
31
42
  },
32
43
  "dependencies": {
33
44
  "navada-edge-sdk": "^1.1.0",