navada-edge-cli 3.1.0 → 3.2.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/docker-compose.yml +1 -1
- package/lib/agent.js +35 -10
- package/lib/cli.js +9 -0
- package/lib/commands/ai.js +14 -11
- package/lib/commands/index.js +13 -19
- package/lib/config.js +18 -3
- package/lib/serve.js +20 -7
- package/lib/ui.js +11 -5
- package/package.json +4 -4
package/docker-compose.yml
CHANGED
package/lib/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execSync } = require('child_process');
|
|
3
|
+
const { execSync, execFileSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -58,10 +58,8 @@ Keep responses short. Code blocks when needed. No fluff.`,
|
|
|
58
58
|
'Raven Terminal — security terminal',
|
|
59
59
|
],
|
|
60
60
|
contact: {
|
|
61
|
-
email: 'leeakpareva@hotmail.com',
|
|
62
61
|
github: 'github.com/leeakpareva',
|
|
63
62
|
website: 'navada-lab.space',
|
|
64
|
-
phone: '+447935237704',
|
|
65
63
|
},
|
|
66
64
|
},
|
|
67
65
|
};
|
|
@@ -88,8 +86,30 @@ const sessionState = {
|
|
|
88
86
|
messages: 0,
|
|
89
87
|
startTime: Date.now(),
|
|
90
88
|
learningMode: null, // 'python' | 'csharp' | 'node' | null
|
|
89
|
+
history: [], // conversation history for context continuity
|
|
91
90
|
};
|
|
92
91
|
|
|
92
|
+
// Conversation history management
|
|
93
|
+
function addToHistory(role, content) {
|
|
94
|
+
sessionState.history.push({ role, content });
|
|
95
|
+
// Keep last 40 turns to avoid token overflow
|
|
96
|
+
if (sessionState.history.length > 40) {
|
|
97
|
+
sessionState.history = sessionState.history.slice(-40);
|
|
98
|
+
}
|
|
99
|
+
sessionState.messages++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getConversationHistory() {
|
|
103
|
+
return sessionState.history;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearHistory() {
|
|
107
|
+
sessionState.history = [];
|
|
108
|
+
sessionState.messages = 0;
|
|
109
|
+
sessionState.tokens = { input: 0, output: 0, total: 0 };
|
|
110
|
+
sessionState.cost = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
// ---------------------------------------------------------------------------
|
|
94
114
|
// Rate limit tracking (in-memory, per session)
|
|
95
115
|
// ---------------------------------------------------------------------------
|
|
@@ -192,7 +212,8 @@ const localTools = {
|
|
|
192
212
|
execute: (code) => {
|
|
193
213
|
try {
|
|
194
214
|
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
195
|
-
|
|
215
|
+
// Safe: use execFileSync with -c flag — no shell interpolation
|
|
216
|
+
const output = execFileSync(py, ['-c', code], {
|
|
196
217
|
timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
197
218
|
});
|
|
198
219
|
return output.trim();
|
|
@@ -206,8 +227,12 @@ const localTools = {
|
|
|
206
227
|
description: 'Install a Python package',
|
|
207
228
|
execute: (pkg) => {
|
|
208
229
|
try {
|
|
230
|
+
// Validate package name — alphanumeric, hyphens, underscores, dots, brackets, version specifiers
|
|
231
|
+
if (!/^[a-zA-Z0-9._\-\[\],>=<! ]+$/.test(pkg)) {
|
|
232
|
+
return 'Error: Invalid package name';
|
|
233
|
+
}
|
|
209
234
|
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
210
|
-
const output =
|
|
235
|
+
const output = execFileSync(py, ['-m', 'pip', 'install', ...pkg.split(/\s+/)], {
|
|
211
236
|
timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
212
237
|
});
|
|
213
238
|
return output.trim().split('\n').slice(-3).join('\n');
|
|
@@ -221,10 +246,12 @@ const localTools = {
|
|
|
221
246
|
description: 'Run a Python script file',
|
|
222
247
|
execute: (scriptPath) => {
|
|
223
248
|
try {
|
|
249
|
+
const resolved = path.resolve(scriptPath);
|
|
250
|
+
if (!fs.existsSync(resolved)) return `Error: File not found: ${resolved}`;
|
|
224
251
|
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
225
|
-
const output =
|
|
252
|
+
const output = execFileSync(py, [resolved], {
|
|
226
253
|
timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
227
|
-
cwd: path.dirname(
|
|
254
|
+
cwd: path.dirname(resolved),
|
|
228
255
|
});
|
|
229
256
|
return output.trim();
|
|
230
257
|
} catch (e) {
|
|
@@ -244,7 +271,6 @@ const localTools = {
|
|
|
244
271
|
// ---------------------------------------------------------------------------
|
|
245
272
|
const FREE_TIER_ENDPOINTS = [
|
|
246
273
|
'https://api.navada-edge-server.uk/api/v1/chat', // Cloudflare tunnel (public, works for all)
|
|
247
|
-
'http://100.88.118.128:7900/api/v1/chat', // Direct Tailscale (VPN users only)
|
|
248
274
|
];
|
|
249
275
|
|
|
250
276
|
async function callFreeTier(messages, stream = false) {
|
|
@@ -932,7 +958,6 @@ function getUpdateInfo() { return _updateInfo; }
|
|
|
932
958
|
async function reportTelemetry(event, data = {}) {
|
|
933
959
|
// Try dashboard first, then public tunnel
|
|
934
960
|
const endpoints = [
|
|
935
|
-
'http://100.88.118.128:7900',
|
|
936
961
|
'https://api.navada-edge-server.uk',
|
|
937
962
|
];
|
|
938
963
|
|
|
@@ -959,4 +984,4 @@ async function reportTelemetry(event, data = {}) {
|
|
|
959
984
|
}
|
|
960
985
|
}
|
|
961
986
|
|
|
962
|
-
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState };
|
|
987
|
+
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState, addToHistory, getConversationHistory, clearHistory };
|
package/lib/cli.js
CHANGED
|
@@ -10,6 +10,15 @@ const { execute, getCompletions } = require('./registry');
|
|
|
10
10
|
const { loadAll } = require('./commands/index');
|
|
11
11
|
const { reportTelemetry, checkForUpdate, getUpdateInfo, rateTracker } = require('./agent');
|
|
12
12
|
|
|
13
|
+
// Global error safety — prevent unhandled crashes
|
|
14
|
+
process.on('unhandledRejection', (err) => {
|
|
15
|
+
console.log(ui.error(`Unhandled error: ${err?.message || err}`));
|
|
16
|
+
});
|
|
17
|
+
process.on('uncaughtException', (err) => {
|
|
18
|
+
console.log(ui.error(`Fatal: ${err?.message || err}`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
13
22
|
function applyConfig() {
|
|
14
23
|
const cfg = config.getAll();
|
|
15
24
|
const overrides = {};
|
package/lib/commands/ai.js
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
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, rateTracker } = require('../agent');
|
|
6
|
+
const { chat: agentChat, reportTelemetry, rateTracker, addToHistory, getConversationHistory, clearHistory } = require('../agent');
|
|
7
7
|
|
|
8
8
|
module.exports = function(reg) {
|
|
9
9
|
|
|
10
|
-
// Conversation history for multi-turn
|
|
11
|
-
const conversationHistory = [];
|
|
12
|
-
|
|
13
10
|
reg('chat', 'Chat with NAVADA Edge AI agent', async (args) => {
|
|
14
11
|
const msg = args.join(' ');
|
|
15
12
|
if (!msg) { console.log(ui.dim('Just type naturally — no /command needed.')); return; }
|
|
@@ -17,24 +14,22 @@ module.exports = function(reg) {
|
|
|
17
14
|
const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
|
|
18
15
|
|
|
19
16
|
// Show a brief "thinking" indicator, then clear it when streaming starts
|
|
17
|
+
let spinner;
|
|
20
18
|
if (!hasKey) {
|
|
21
19
|
process.stdout.write(ui.dim(' NAVADA > '));
|
|
22
20
|
} else {
|
|
23
21
|
const ora = require('ora');
|
|
24
|
-
|
|
22
|
+
spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
try {
|
|
28
|
-
const response = await agentChat(msg,
|
|
26
|
+
const response = await agentChat(msg, getConversationHistory());
|
|
29
27
|
|
|
30
28
|
if (spinner) spinner.stop();
|
|
31
29
|
|
|
32
30
|
// Update conversation history
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Keep history manageable (last 20 turns)
|
|
37
|
-
while (conversationHistory.length > 40) conversationHistory.splice(0, 2);
|
|
31
|
+
addToHistory('user', msg);
|
|
32
|
+
addToHistory('assistant', response);
|
|
38
33
|
|
|
39
34
|
// Only print if not already streamed
|
|
40
35
|
if (!response._streamed) {
|
|
@@ -51,6 +46,14 @@ module.exports = function(reg) {
|
|
|
51
46
|
}
|
|
52
47
|
}, { category: 'AI', aliases: ['ask'] });
|
|
53
48
|
|
|
49
|
+
reg('clear', 'Clear conversation history and reset session', () => {
|
|
50
|
+
clearHistory();
|
|
51
|
+
console.clear();
|
|
52
|
+
const banner = require('../ui').banner;
|
|
53
|
+
console.log(banner());
|
|
54
|
+
console.log(ui.success('Conversation cleared — fresh session'));
|
|
55
|
+
}, { category: 'AI' });
|
|
56
|
+
|
|
54
57
|
reg('qwen', 'Qwen Coder 32B (FREE via HuggingFace)', async (args) => {
|
|
55
58
|
const prompt = args.join(' ');
|
|
56
59
|
if (!prompt) { console.log(ui.dim('Usage: /qwen Write a function to validate UK postcodes')); return; }
|
package/lib/commands/index.js
CHANGED
|
@@ -3,28 +3,22 @@
|
|
|
3
3
|
const { register } = require('../registry');
|
|
4
4
|
|
|
5
5
|
// Load all command modules — each exports a function(register)
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
require('./docker'),
|
|
11
|
-
require('./database'),
|
|
12
|
-
require('./cloudflare'),
|
|
13
|
-
require('./ai'),
|
|
14
|
-
require('./azure'),
|
|
15
|
-
require('./agents'),
|
|
16
|
-
require('./tasks'),
|
|
17
|
-
require('./keys'),
|
|
18
|
-
require('./setup'),
|
|
19
|
-
require('./system'),
|
|
20
|
-
require('./learn'),
|
|
21
|
-
require('./sandbox'),
|
|
22
|
-
require('./nvidia'),
|
|
6
|
+
const moduleNames = [
|
|
7
|
+
'network', 'mcp', 'lucas', 'docker', 'database', 'cloudflare',
|
|
8
|
+
'ai', 'azure', 'agents', 'tasks', 'keys', 'setup', 'system',
|
|
9
|
+
'learn', 'sandbox', 'nvidia',
|
|
23
10
|
];
|
|
24
11
|
|
|
25
12
|
function loadAll() {
|
|
26
|
-
for (const
|
|
27
|
-
|
|
13
|
+
for (const name of moduleNames) {
|
|
14
|
+
try {
|
|
15
|
+
const mod = require(`./${name}`);
|
|
16
|
+
mod(register);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Don't crash the entire CLI if one module fails to load
|
|
19
|
+
const ui = require('../ui');
|
|
20
|
+
console.log(ui.warn(`Failed to load module: ${name} — ${e.message}`));
|
|
21
|
+
}
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|
package/lib/config.js
CHANGED
|
@@ -15,13 +15,28 @@ function ensureDir() {
|
|
|
15
15
|
function load() {
|
|
16
16
|
try {
|
|
17
17
|
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
19
|
+
if (!raw.trim()) return {};
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// Back up corrupted config
|
|
23
|
+
try {
|
|
24
|
+
const backupPath = CONFIG_FILE + '.bak';
|
|
25
|
+
if (fs.existsSync(CONFIG_FILE)) fs.copyFileSync(CONFIG_FILE, backupPath);
|
|
26
|
+
} catch {}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
function save(config) {
|
|
23
32
|
ensureDir();
|
|
24
|
-
|
|
33
|
+
const data = JSON.stringify(config, null, 2);
|
|
34
|
+
// Write atomically via temp file
|
|
35
|
+
const tmpFile = CONFIG_FILE + '.tmp';
|
|
36
|
+
fs.writeFileSync(tmpFile, data);
|
|
37
|
+
fs.renameSync(tmpFile, CONFIG_FILE);
|
|
38
|
+
// Restrict permissions on config file (contains API keys)
|
|
39
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
function isFirstRun() { return !fs.existsSync(CONFIG_FILE); }
|
package/lib/serve.js
CHANGED
|
@@ -9,8 +9,10 @@ function start(port = 7800) {
|
|
|
9
9
|
const server = http.createServer(async (req, res) => {
|
|
10
10
|
const url = req.url.split('?')[0];
|
|
11
11
|
|
|
12
|
-
// CORS
|
|
13
|
-
|
|
12
|
+
// CORS — restrict to local/Tailscale origins
|
|
13
|
+
const origin = req.headers.origin || '';
|
|
14
|
+
const allowedOrigin = (origin.includes('localhost') || origin.includes('127.0.0.1') || origin.includes('100.')) ? origin : '';
|
|
15
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin || `http://localhost:${port}`);
|
|
14
16
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
15
17
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
16
18
|
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
|
|
@@ -24,25 +26,36 @@ function start(port = 7800) {
|
|
|
24
26
|
// Execute command
|
|
25
27
|
if (url === '/api/cmd' && req.method === 'POST') {
|
|
26
28
|
let body = '';
|
|
27
|
-
|
|
29
|
+
let bodySize = 0;
|
|
30
|
+
const MAX_BODY = 8192;
|
|
31
|
+
req.on('data', c => {
|
|
32
|
+
bodySize += c.length;
|
|
33
|
+
if (bodySize > MAX_BODY) { req.destroy(); return; }
|
|
34
|
+
body += c;
|
|
35
|
+
});
|
|
28
36
|
req.on('end', async () => {
|
|
29
37
|
try {
|
|
38
|
+
if (bodySize > MAX_BODY) { res.writeHead(413); return res.end(JSON.stringify({ error: 'Request too large' })); }
|
|
30
39
|
const { command } = JSON.parse(body);
|
|
31
|
-
if (!command) { res.writeHead(400); return res.end(JSON.stringify({ error: 'command required' })); }
|
|
40
|
+
if (!command || typeof command !== 'string') { res.writeHead(400); return res.end(JSON.stringify({ error: 'command required (string)' })); }
|
|
41
|
+
if (command.length > 2000) { res.writeHead(400); return res.end(JSON.stringify({ error: 'command too long' })); }
|
|
32
42
|
|
|
33
43
|
// Capture output
|
|
34
44
|
const orig = console.log;
|
|
35
45
|
const buf = [];
|
|
36
46
|
console.log = (...args) => buf.push(args.join(' '));
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
try {
|
|
48
|
+
await execute(command);
|
|
49
|
+
} finally {
|
|
50
|
+
console.log = orig;
|
|
51
|
+
}
|
|
39
52
|
|
|
40
53
|
const output = buf.join('\n').replace(/\x1b\[[0-9;]*m/g, ''); // strip ANSI
|
|
41
54
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
55
|
res.end(JSON.stringify({ command, output }));
|
|
43
56
|
} catch (e) {
|
|
44
57
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
45
|
-
res.end(JSON.stringify({ error:
|
|
58
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
46
59
|
}
|
|
47
60
|
});
|
|
48
61
|
return;
|
package/lib/ui.js
CHANGED
|
@@ -90,8 +90,9 @@ function sessionPanel(width) {
|
|
|
90
90
|
kvLine(' Model: ', model),
|
|
91
91
|
kvLine(' Tier: ', tier),
|
|
92
92
|
'',
|
|
93
|
-
sectionLine('
|
|
94
|
-
kvLine('
|
|
93
|
+
sectionLine('Usage'),
|
|
94
|
+
kvLine(' Messages:', String(state.messages || 0)),
|
|
95
|
+
kvLine(' Tokens: ', String(state.tokens?.total || 0)),
|
|
95
96
|
kvLine(' Cost: ', `$${(state.cost || 0).toFixed(4)}`),
|
|
96
97
|
'',
|
|
97
98
|
sectionLine('Configuration'),
|
|
@@ -114,15 +115,18 @@ function sessionPanel(width) {
|
|
|
114
115
|
return lines;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
let _pythonCache = null;
|
|
117
119
|
function detectPython() {
|
|
120
|
+
if (_pythonCache !== null) return _pythonCache;
|
|
118
121
|
try {
|
|
119
122
|
const { execSync } = require('child_process');
|
|
120
123
|
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
121
124
|
const ver = execSync(`${py} --version`, { timeout: 3000, encoding: 'utf-8' }).trim();
|
|
122
|
-
|
|
125
|
+
_pythonCache = style('success', ver.replace('Python ', ''));
|
|
123
126
|
} catch {
|
|
124
|
-
|
|
127
|
+
_pythonCache = style('offline', 'not found');
|
|
125
128
|
}
|
|
129
|
+
return _pythonCache;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
function footerBar(width) {
|
|
@@ -178,7 +182,9 @@ function prompt() {
|
|
|
178
182
|
try { state = require('./agent').sessionState; } catch { state = {}; }
|
|
179
183
|
const mode = state?.learningMode;
|
|
180
184
|
const modeTag = mode ? style('accent', `[${mode}]`) + ' ' : '';
|
|
181
|
-
|
|
185
|
+
const msgCount = state?.messages || 0;
|
|
186
|
+
const countTag = msgCount > 0 ? style('dim', `(${msgCount}) `) : '';
|
|
187
|
+
return modeTag + countTag + style('dim', 'navada') + style('accent', '> ');
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
function box(title, content) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navada-edge-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"url": "git+https://github.com/Navada25/edge-sdk.git"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"navada-edge-sdk": "^1.1.0",
|
|
45
44
|
"chalk": "^4.1.2",
|
|
46
45
|
"cli-table3": "^0.6.5",
|
|
46
|
+
"navada-edge-sdk": "^1.1.0",
|
|
47
47
|
"ora": "^5.4.1"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"nodemailer": "^8.0.4",
|
|
51
|
+
"qrcode-terminal": "^0.12.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|