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 +6 -0
- package/lib/agent.js +318 -70
- package/lib/cli.js +38 -12
- package/lib/commands/ai.js +19 -7
- package/lib/ui.js +2 -1
- package/package.json +16 -5
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 (
|
|
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;
|
|
169
|
+
continue;
|
|
136
170
|
}
|
|
137
171
|
}
|
|
138
172
|
|
|
139
|
-
|
|
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()
|
|
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
|
-
//
|
|
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
|
-
//
|
|
237
|
-
let response = await
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
584
|
+
// Update checker (non-blocking, runs once per session)
|
|
362
585
|
// ---------------------------------------------------------------------------
|
|
363
|
-
|
|
364
|
-
|
|
586
|
+
let _updateInfo = null;
|
|
587
|
+
|
|
588
|
+
async function checkForUpdate() {
|
|
365
589
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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.');
|
package/lib/commands/ai.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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",
|
|
19
|
-
"
|
|
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",
|