navada-edge-cli 3.1.0 → 3.3.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/edge.js +186 -0
- 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; }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ui = require('../ui');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const navada = require('navada-edge-sdk');
|
|
6
|
+
|
|
7
|
+
const PORTAL_URL = 'https://portal.navada-edge-server.uk';
|
|
8
|
+
const VALIDATE_ENDPOINTS = [
|
|
9
|
+
'https://api.navada-edge-server.uk/api/v1/public/validate-key',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
module.exports = function(reg) {
|
|
13
|
+
|
|
14
|
+
// /onboard — open portal in browser
|
|
15
|
+
reg('onboard', 'Open NAVADA Edge Portal to create account and get API key', async () => {
|
|
16
|
+
console.log(ui.header('NAVADA EDGE NETWORK — ONBOARDING'));
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(ui.label('Step 1', 'Create account at the Edge Portal'));
|
|
19
|
+
console.log(ui.label('Step 2', 'Generate an API key (Dashboard > API Keys)'));
|
|
20
|
+
console.log(ui.label('Step 3', 'Connect: /edge login nv_edge_your_key'));
|
|
21
|
+
console.log('');
|
|
22
|
+
|
|
23
|
+
// Try to open browser
|
|
24
|
+
const url = PORTAL_URL + '/sign-up';
|
|
25
|
+
try {
|
|
26
|
+
const { exec } = require('child_process');
|
|
27
|
+
const cmd = process.platform === 'win32' ? `start ${url}`
|
|
28
|
+
: process.platform === 'darwin' ? `open ${url}`
|
|
29
|
+
: `xdg-open ${url}`;
|
|
30
|
+
exec(cmd);
|
|
31
|
+
console.log(ui.success(`Opening ${url} in your browser...`));
|
|
32
|
+
} catch {
|
|
33
|
+
console.log(ui.dim(`Open this URL in your browser:`));
|
|
34
|
+
console.log(ui.label('Portal', url));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(ui.dim('After signing up and generating a key, run:'));
|
|
39
|
+
console.log(ui.dim(' /edge login nv_edge_your_key_here'));
|
|
40
|
+
}, { category: 'EDGE', aliases: ['signup', 'register'] });
|
|
41
|
+
|
|
42
|
+
// /edge — edge network commands
|
|
43
|
+
reg('edge', 'NAVADA Edge Network commands', async (args) => {
|
|
44
|
+
const sub = args[0];
|
|
45
|
+
|
|
46
|
+
if (!sub || sub === 'help') {
|
|
47
|
+
console.log(ui.header('NAVADA EDGE NETWORK'));
|
|
48
|
+
console.log(ui.dim('Connect your CLI to the 24/7 Edge Network'));
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(ui.cmd('edge login <key>', 'Connect with your NAVADA Edge API key'));
|
|
51
|
+
console.log(ui.cmd('edge status', 'Check your Edge Network connection'));
|
|
52
|
+
console.log(ui.cmd('edge logout', 'Disconnect from Edge Network'));
|
|
53
|
+
console.log(ui.cmd('edge tier', 'Show your current tier and limits'));
|
|
54
|
+
console.log(ui.cmd('onboard', 'Create account and get API key'));
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(ui.dim('Get started: /onboard'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// /edge login <key>
|
|
61
|
+
if (sub === 'login') {
|
|
62
|
+
const key = args[1];
|
|
63
|
+
if (!key) {
|
|
64
|
+
console.log(ui.error('Usage: /edge login nv_edge_your_key_here'));
|
|
65
|
+
console.log(ui.dim('Get a key: /onboard'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!key.startsWith('nv_edge_') || key.length !== 56) {
|
|
70
|
+
console.log(ui.error('Invalid key format. NAVADA Edge keys start with nv_edge_ and are 56 characters.'));
|
|
71
|
+
console.log(ui.dim('Get a key: /onboard'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate against the Edge Network
|
|
76
|
+
const ora = require('ora');
|
|
77
|
+
const spinner = ora({ text: ' Validating key with NAVADA Edge Network...', color: 'white' }).start();
|
|
78
|
+
|
|
79
|
+
let validated = false;
|
|
80
|
+
for (const endpoint of VALIDATE_ENDPOINTS) {
|
|
81
|
+
try {
|
|
82
|
+
const r = await navada.request(endpoint, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: { key },
|
|
85
|
+
timeout: 10000,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (r.status === 200 && r.data?.valid) {
|
|
89
|
+
spinner.stop();
|
|
90
|
+
config.set('edgeKey', key);
|
|
91
|
+
config.set('edgeTier', r.data.tier || 'free');
|
|
92
|
+
config.set('edgeUserId', r.data.userId || '');
|
|
93
|
+
config.set('edgeConnected', true);
|
|
94
|
+
|
|
95
|
+
console.log(ui.success('Connected to NAVADA Edge Network'));
|
|
96
|
+
console.log(ui.label('Tier', (r.data.tier || 'FREE').toUpperCase()));
|
|
97
|
+
console.log(ui.label('User', r.data.name || r.data.userId || 'connected'));
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(ui.dim('You now have access to:'));
|
|
100
|
+
console.log(ui.dim(' /status — network health'));
|
|
101
|
+
console.log(ui.dim(' /doctor — test connections'));
|
|
102
|
+
console.log(ui.dim(' /offload — run tasks 24/7 (coming soon)'));
|
|
103
|
+
console.log(ui.dim(' /sessions — view running tasks (coming soon)'));
|
|
104
|
+
validated = true;
|
|
105
|
+
break;
|
|
106
|
+
} else if (r.status === 401) {
|
|
107
|
+
spinner.stop();
|
|
108
|
+
console.log(ui.error('Key not recognised. Check your key or generate a new one: /onboard'));
|
|
109
|
+
validated = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!validated) {
|
|
118
|
+
spinner.stop();
|
|
119
|
+
// Can't reach server — store key locally anyway (offline-first)
|
|
120
|
+
config.set('edgeKey', key);
|
|
121
|
+
config.set('edgeConnected', false);
|
|
122
|
+
console.log(ui.warn('Could not reach NAVADA Edge Network to validate key.'));
|
|
123
|
+
console.log(ui.dim('Key saved locally. It will be validated when the network is reachable.'));
|
|
124
|
+
console.log(ui.dim('Check: /edge status'));
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// /edge status
|
|
130
|
+
if (sub === 'status') {
|
|
131
|
+
const key = config.get('edgeKey');
|
|
132
|
+
if (!key) {
|
|
133
|
+
console.log(ui.warn('Not connected to NAVADA Edge Network'));
|
|
134
|
+
console.log(ui.dim('Get started: /onboard'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(ui.header('EDGE NETWORK STATUS'));
|
|
139
|
+
console.log(ui.label('Key', key.substring(0, 12) + '...' + key.slice(-4)));
|
|
140
|
+
console.log(ui.label('Tier', (config.get('edgeTier') || 'FREE').toUpperCase()));
|
|
141
|
+
console.log(ui.label('Connected', config.get('edgeConnected') ? 'yes' : 'pending validation'));
|
|
142
|
+
|
|
143
|
+
// Ping the network
|
|
144
|
+
try {
|
|
145
|
+
const r = await navada.request('https://api.navada-edge-server.uk/api/health', { timeout: 5000 });
|
|
146
|
+
console.log(ui.online('Edge Network', r.status === 200, 'api.navada-edge-server.uk'));
|
|
147
|
+
} catch {
|
|
148
|
+
console.log(ui.online('Edge Network', false, 'unreachable'));
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// /edge logout
|
|
154
|
+
if (sub === 'logout') {
|
|
155
|
+
config.set('edgeKey', '');
|
|
156
|
+
config.set('edgeTier', '');
|
|
157
|
+
config.set('edgeUserId', '');
|
|
158
|
+
config.set('edgeConnected', false);
|
|
159
|
+
console.log(ui.success('Disconnected from NAVADA Edge Network'));
|
|
160
|
+
console.log(ui.dim('Your local CLI still works. Reconnect anytime: /edge login <key>'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// /edge tier
|
|
165
|
+
if (sub === 'tier') {
|
|
166
|
+
const tier = (config.get('edgeTier') || 'free').toUpperCase();
|
|
167
|
+
console.log(ui.header('EDGE NETWORK TIER'));
|
|
168
|
+
console.log(ui.label('Current', tier));
|
|
169
|
+
console.log('');
|
|
170
|
+
if (tier === 'FREE') {
|
|
171
|
+
console.log(ui.dim('Free tier includes:'));
|
|
172
|
+
console.log(ui.dim(' 100 requests/day'));
|
|
173
|
+
console.log(ui.dim(' 50K tokens/day'));
|
|
174
|
+
console.log(ui.dim(' 10 edge tasks'));
|
|
175
|
+
console.log(ui.dim(' 5min max runtime per task'));
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(ui.dim('Upgrade: coming soon'));
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(ui.dim('Unknown subcommand. Try /edge help'));
|
|
183
|
+
|
|
184
|
+
}, { category: 'EDGE', subs: ['login', 'status', 'logout', 'tier', 'help'] });
|
|
185
|
+
|
|
186
|
+
};
|
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', 'edge',
|
|
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.3.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"
|