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.
@@ -1,7 +1,7 @@
1
1
  services:
2
2
  cli:
3
3
  build: .
4
- image: navada-edge-cli:3.1.0
4
+ image: navada-edge-cli:3.2.0
5
5
  container_name: navada-edge-cli
6
6
  restart: always
7
7
  stdin_open: true
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
- const output = execSync(`${py} -c "${code.replace(/"/g, '\\"')}"`, {
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 = execSync(`${py} -m pip install ${pkg}`, {
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 = execSync(`${py} "${path.resolve(scriptPath)}"`, {
252
+ const output = execFileSync(py, [resolved], {
226
253
  timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
227
- cwd: path.dirname(path.resolve(scriptPath)),
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 = {};
@@ -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
- var spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
22
+ spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
25
23
  }
26
24
 
27
25
  try {
28
- const response = await agentChat(msg, conversationHistory);
26
+ const response = await agentChat(msg, getConversationHistory());
29
27
 
30
28
  if (spinner) spinner.stop();
31
29
 
32
30
  // Update conversation history
33
- conversationHistory.push({ role: 'user', content: msg });
34
- conversationHistory.push({ role: 'assistant', content: response });
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; }
@@ -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 modules = [
7
- require('./network'),
8
- require('./mcp'),
9
- require('./lucas'),
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 mod of modules) {
27
- mod(register);
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
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
19
- } catch { return {}; }
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
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
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
- res.setHeader('Access-Control-Allow-Origin', '*');
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
- req.on('data', c => body += c);
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
- await execute(command);
38
- console.log = orig;
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: e.message }));
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('Token Usage'),
94
- kvLine(' Total: ', String(state.tokens?.total || 0)),
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
- return style('success', ver.replace('Python ', ''));
125
+ _pythonCache = style('success', ver.replace('Python ', ''));
123
126
  } catch {
124
- return style('offline', 'not found');
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
- return modeTag + style('dim', 'navada') + style('accent', '> ');
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.1.0",
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
- "qrcode-terminal": "^0.12.0",
51
- "nodemailer": "^6.9.0"
50
+ "nodemailer": "^8.0.4",
51
+ "qrcode-terminal": "^0.12.0"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"