navada-edge-cli 3.4.0 → 3.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/lib/agent.js +111 -13
- package/lib/commands/audit.js +355 -0
- package/lib/commands/compute.js +225 -0
- package/lib/commands/edge.js +130 -2
- package/lib/commands/index.js +1 -1
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -32,6 +32,8 @@ You also connect to the NAVADA Edge Network (4 nodes via Tailscale VPN):
|
|
|
32
32
|
- send_email / generate_image: communications and AI image generation
|
|
33
33
|
- founder_info: information about Lee Akpareva, the creator of NAVADA
|
|
34
34
|
When users ask you to DO something — DO IT. Use write_file to create files. Use shell to run commands. Never say "I can't" when you have a tool for it.
|
|
35
|
+
When asked to generate diagrams — use write_file to create Mermaid (.mmd), SVG, or HTML files. You can also use python_exec with matplotlib/graphviz for complex diagrams.
|
|
36
|
+
When asked to create, edit, or delete files — use the file tools directly. You are a terminal agent with FULL access.
|
|
35
37
|
Keep responses short. Code blocks when needed. No fluff.`,
|
|
36
38
|
founder: {
|
|
37
39
|
name: 'Leslie (Lee) Akpareva',
|
|
@@ -65,6 +67,7 @@ Keep responses short. Code blocks when needed. No fluff.`,
|
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
function getSystemPrompt() {
|
|
70
|
+
// Learning mode overrides everything
|
|
68
71
|
if (sessionState.learningMode) {
|
|
69
72
|
try {
|
|
70
73
|
const { MODES } = require('./commands/learn');
|
|
@@ -72,9 +75,41 @@ function getSystemPrompt() {
|
|
|
72
75
|
if (mode) return mode.system;
|
|
73
76
|
} catch {}
|
|
74
77
|
}
|
|
78
|
+
|
|
79
|
+
// Load user's agent.md customisation if it exists
|
|
80
|
+
const agentMdPath = path.join(config.CONFIG_DIR, 'agent.md');
|
|
81
|
+
let userPrompt = '';
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(agentMdPath)) {
|
|
84
|
+
userPrompt = fs.readFileSync(agentMdPath, 'utf-8').trim();
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
// Load active sub-agent if selected
|
|
89
|
+
if (sessionState.subAgent) {
|
|
90
|
+
const subPath = path.join(config.CONFIG_DIR, 'agents', `${sessionState.subAgent}.md`);
|
|
91
|
+
try {
|
|
92
|
+
if (fs.existsSync(subPath)) {
|
|
93
|
+
return fs.readFileSync(subPath, 'utf-8').trim();
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Combine: base personality + user customisation
|
|
99
|
+
if (userPrompt) {
|
|
100
|
+
return `${IDENTITY.personality}\n\n--- USER CUSTOMISATION (from agent.md) ---\n${userPrompt}`;
|
|
101
|
+
}
|
|
75
102
|
return IDENTITY.personality;
|
|
76
103
|
}
|
|
77
104
|
|
|
105
|
+
function listSubAgents() {
|
|
106
|
+
const dir = path.join(config.CONFIG_DIR, 'agents');
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(dir)) return [];
|
|
109
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''));
|
|
110
|
+
} catch { return []; }
|
|
111
|
+
}
|
|
112
|
+
|
|
78
113
|
// ---------------------------------------------------------------------------
|
|
79
114
|
// Session state — exposed for UI panels
|
|
80
115
|
// ---------------------------------------------------------------------------
|
|
@@ -86,6 +121,7 @@ const sessionState = {
|
|
|
86
121
|
messages: 0,
|
|
87
122
|
startTime: Date.now(),
|
|
88
123
|
learningMode: null, // 'python' | 'csharp' | 'node' | null
|
|
124
|
+
subAgent: null, // active sub-agent name (loads from ~/.navada/agents/<name>.md)
|
|
89
125
|
history: [], // conversation history for context continuity
|
|
90
126
|
};
|
|
91
127
|
|
|
@@ -650,13 +686,16 @@ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
|
|
|
650
686
|
|
|
651
687
|
function openAITools() {
|
|
652
688
|
const defs = [
|
|
653
|
-
{ name: 'shell', description: 'Execute a shell command on the user\'s machine', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
|
|
654
|
-
{ name: 'read_file', description: 'Read a file', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
655
|
-
{ name: 'write_file', description: 'Write to a file', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
|
|
656
|
-
{ name: 'list_files', description: 'List
|
|
657
|
-
{ name: 'system_info', description: 'Get system
|
|
658
|
-
{ name: 'python_exec', description: 'Execute Python code', parameters: { type: 'object', properties: { code: { type: 'string' } }, required: ['code'] } },
|
|
659
|
-
{ name: 'python_pip', description: 'Install a Python package', parameters: { type: 'object', properties: { package: { type: 'string' } }, required: ['package'] } },
|
|
689
|
+
{ name: 'shell', description: 'Execute a shell command on the user\'s machine. Use for: file operations, git, npm, docker, system commands, creating directories, running scripts.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to run' } }, required: ['command'] } },
|
|
690
|
+
{ name: 'read_file', description: 'Read the contents of a file on the user\'s machine.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute or relative file path' } }, required: ['path'] } },
|
|
691
|
+
{ name: 'write_file', description: 'Write content to a file. Creates parent directories if needed. Use for creating new files, scripts, configs, diagrams (Mermaid, SVG, HTML), code files.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to write' }, content: { type: 'string', description: 'Full content to write to the file' } }, required: ['path', 'content'] } },
|
|
692
|
+
{ name: 'list_files', description: 'List files and directories.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } } },
|
|
693
|
+
{ name: 'system_info', description: 'Get local system information (CPU, RAM, disk, OS, hostname).', parameters: { type: 'object', properties: {} } },
|
|
694
|
+
{ name: 'python_exec', description: 'Execute Python code inline. Use for data analysis, calculations, generating content, processing files, ML tasks.', parameters: { type: 'object', properties: { code: { type: 'string', description: 'Python code to execute' } }, required: ['code'] } },
|
|
695
|
+
{ name: 'python_pip', description: 'Install a Python package via pip.', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } },
|
|
696
|
+
{ name: 'python_script', description: 'Run a Python script file.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to .py file' } }, required: ['path'] } },
|
|
697
|
+
{ name: 'sandbox_run', description: 'Run code in an isolated sandbox with syntax highlighting. Supports javascript, python, typescript.', parameters: { type: 'object', properties: { code: { type: 'string' }, language: { type: 'string', description: 'javascript, python, or typescript' } }, required: ['code'] } },
|
|
698
|
+
{ name: 'founder_info', description: 'Get information about Lee Akpareva, founder of NAVADA Edge.', parameters: { type: 'object', properties: {} } },
|
|
660
699
|
];
|
|
661
700
|
return defs.map(d => ({ type: 'function', function: d }));
|
|
662
701
|
}
|
|
@@ -997,13 +1036,72 @@ async function grokChat(userMessage, conversationHistory = []) {
|
|
|
997
1036
|
{ role: 'user', content: userMessage },
|
|
998
1037
|
];
|
|
999
1038
|
|
|
1000
|
-
//
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1039
|
+
// Send tools with the request — free tier now supports tool use
|
|
1040
|
+
const tools = openAITools();
|
|
1041
|
+
const endpoint = FREE_TIER_ENDPOINTS[0];
|
|
1042
|
+
|
|
1043
|
+
// Non-streaming request with tools (streaming + tools is complex, use non-streaming for tool calls)
|
|
1044
|
+
let response;
|
|
1045
|
+
try {
|
|
1046
|
+
const r = await navada.request(endpoint, {
|
|
1047
|
+
method: 'POST',
|
|
1048
|
+
body: { messages, tools },
|
|
1049
|
+
timeout: 120000,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
if (r.status === 429) {
|
|
1053
|
+
return `Free tier limit reached. /login <key> for unlimited access.`;
|
|
1054
|
+
}
|
|
1055
|
+
if (r.status !== 200) {
|
|
1056
|
+
// Fall back to streaming without tools
|
|
1057
|
+
const result = await callFreeTier(messages, true);
|
|
1058
|
+
return result.content || 'No response from free tier.';
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
rateTracker.record();
|
|
1062
|
+
response = r.data;
|
|
1063
|
+
} catch {
|
|
1064
|
+
// Network error — try streaming fallback
|
|
1065
|
+
const result = await callFreeTier(messages, true);
|
|
1004
1066
|
return result.content || 'No response from free tier. Try /login <key> for full agent.';
|
|
1005
1067
|
}
|
|
1006
|
-
|
|
1068
|
+
|
|
1069
|
+
// Handle tool use loop (same as OpenAI path)
|
|
1070
|
+
let iterations = 0;
|
|
1071
|
+
while (response?.choices?.[0]?.finish_reason === 'tool_calls' && iterations < 10) {
|
|
1072
|
+
iterations++;
|
|
1073
|
+
const toolCalls = response.choices[0].message.tool_calls || [];
|
|
1074
|
+
if (toolCalls.length === 0) break;
|
|
1075
|
+
|
|
1076
|
+
const toolResults = [];
|
|
1077
|
+
for (const tc of toolCalls) {
|
|
1078
|
+
let input;
|
|
1079
|
+
try { input = JSON.parse(tc.function.arguments); } catch { input = {}; }
|
|
1080
|
+
console.log(ui.dim(` [${tc.function.name}] ${JSON.stringify(input).slice(0, 80)}`));
|
|
1081
|
+
const result = await executeTool(tc.function.name, input);
|
|
1082
|
+
toolResults.push({ role: 'tool', tool_call_id: tc.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Add assistant message with tool_calls + results, then call again
|
|
1086
|
+
messages.push({ role: 'assistant', content: response.choices[0].message.content || null, tool_calls: toolCalls });
|
|
1087
|
+
messages.push(...toolResults);
|
|
1088
|
+
|
|
1089
|
+
try {
|
|
1090
|
+
const r = await navada.request(endpoint, {
|
|
1091
|
+
method: 'POST',
|
|
1092
|
+
body: { messages, tools },
|
|
1093
|
+
timeout: 120000,
|
|
1094
|
+
});
|
|
1095
|
+
if (r.status !== 200) break;
|
|
1096
|
+
rateTracker.record();
|
|
1097
|
+
response = r.data;
|
|
1098
|
+
} catch { break; }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Extract final text
|
|
1102
|
+
const content = response?.choices?.[0]?.message?.content || '';
|
|
1103
|
+
if (content) console.log(` ${content}`);
|
|
1104
|
+
return content || 'No response.';
|
|
1007
1105
|
}
|
|
1008
1106
|
|
|
1009
1107
|
async function fallbackChat(msg) {
|
|
@@ -1078,4 +1176,4 @@ async function reportTelemetry(event, data = {}) {
|
|
|
1078
1176
|
}
|
|
1079
1177
|
}
|
|
1080
1178
|
|
|
1081
|
-
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState, addToHistory, getConversationHistory, clearHistory };
|
|
1179
|
+
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState, addToHistory, getConversationHistory, clearHistory, listSubAgents };
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const config = require('../config');
|
|
8
|
+
|
|
9
|
+
// ─────────────────────────────────────────────
|
|
10
|
+
// CONFIG
|
|
11
|
+
// ─────────────────────────────────────────────
|
|
12
|
+
const NAVADA_DIR = path.join(os.homedir(), '.navada');
|
|
13
|
+
const REPORTS_DIR = path.join(NAVADA_DIR, 'audit-reports');
|
|
14
|
+
const CONFIG_FILE = path.join(NAVADA_DIR, 'config.json');
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────
|
|
17
|
+
// SOURCE SCANNER
|
|
18
|
+
// ─────────────────────────────────────────────
|
|
19
|
+
function findInSource(pattern, dir) {
|
|
20
|
+
const searchDirs = dir ? [dir] : ['src', 'lib', 'bin', 'server', 'api', 'routes', '.'];
|
|
21
|
+
const extensions = ['js', 'ts', 'mjs', 'cjs'];
|
|
22
|
+
|
|
23
|
+
for (const d of searchDirs) {
|
|
24
|
+
if (!fs.existsSync(d)) continue;
|
|
25
|
+
const files = walkDir(d, extensions);
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
29
|
+
if (new RegExp(pattern, 'i').test(content)) return { found: true, file };
|
|
30
|
+
} catch { continue; }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { found: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function walkDir(dir, extensions, files = []) {
|
|
37
|
+
try {
|
|
38
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
39
|
+
if (entry === 'node_modules' || entry.startsWith('.')) continue;
|
|
40
|
+
const full = path.join(dir, entry);
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.statSync(full);
|
|
43
|
+
if (stat.isDirectory()) walkDir(full, extensions, files);
|
|
44
|
+
else if (extensions.some(ext => full.endsWith(`.${ext}`))) files.push(full);
|
|
45
|
+
} catch { continue; }
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────
|
|
52
|
+
// STATIC CHECKS — current real-world state
|
|
53
|
+
// ─────────────────────────────────────────────
|
|
54
|
+
const CHECKS = {
|
|
55
|
+
|
|
56
|
+
cli: [
|
|
57
|
+
{ id: 'CLI-01', name: 'Entry file has shebang', severity: 'CRITICAL', check: () => {
|
|
58
|
+
for (const f of ['bin/navada.js', 'bin/index.js', 'index.js']) {
|
|
59
|
+
if (fs.existsSync(f)) return fs.readFileSync(f, 'utf8').split('\n')[0].startsWith('#!/usr/bin/env node');
|
|
60
|
+
}
|
|
61
|
+
return { skipped: true, reason: 'No entry file found' };
|
|
62
|
+
}},
|
|
63
|
+
{ id: 'CLI-02', name: 'package.json has bin field', severity: 'CRITICAL', check: () => {
|
|
64
|
+
if (!fs.existsSync('package.json')) return { skipped: true, reason: 'No package.json' };
|
|
65
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
66
|
+
return !!(pkg.bin && Object.keys(pkg.bin).length > 0);
|
|
67
|
+
}},
|
|
68
|
+
{ id: 'CLI-03', name: 'NO_COLOR respected', severity: 'MEDIUM', check: () => findInSource('NO_COLOR').found },
|
|
69
|
+
{ id: 'CLI-04', name: 'Semantic versioning', severity: 'HIGH', check: () => {
|
|
70
|
+
if (!fs.existsSync('package.json')) return false;
|
|
71
|
+
return /^\d+\.\d+\.\d+/.test(JSON.parse(fs.readFileSync('package.json', 'utf8')).version || '');
|
|
72
|
+
}},
|
|
73
|
+
{ id: 'CLI-05', name: 'Config in ~/.navada/', severity: 'HIGH', check: () => findInSource('.navada').found },
|
|
74
|
+
{ id: 'CLI-06', name: 'Non-zero exit on failure', severity: 'HIGH', check: () => findInSource('process.exit\\(1\\)').found },
|
|
75
|
+
{ id: 'CLI-07', name: 'README exists and >500 chars', severity: 'MEDIUM', check: () => {
|
|
76
|
+
if (!fs.existsSync('README.md')) return false;
|
|
77
|
+
return fs.readFileSync('README.md', 'utf8').length > 500;
|
|
78
|
+
}},
|
|
79
|
+
{ id: 'CLI-08', name: 'LICENSE file exists', severity: 'LOW', check: () => fs.existsSync('LICENSE') || fs.existsSync('LICENSE.md') },
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
security: [
|
|
83
|
+
{ id: 'SEC-01', name: 'No hardcoded API keys in source', severity: 'CRITICAL', check: () => {
|
|
84
|
+
for (const p of ['sk-ant-api', 'sk-proj-', 'xai-[a-zA-Z]']) {
|
|
85
|
+
const r = findInSource(p);
|
|
86
|
+
if (r.found) return { pass: false, detail: `Pattern ${p} in ${r.file}` };
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}},
|
|
90
|
+
{ id: 'SEC-02', name: 'No plaintext passwords in source', severity: 'CRITICAL', check: () => !findInSource('password.*=.*["\'][^"\']{8,}["\']').found },
|
|
91
|
+
{ id: 'SEC-03', name: '.gitignore covers .env', severity: 'CRITICAL', check: () => {
|
|
92
|
+
if (!fs.existsSync('.gitignore')) return { pass: false, detail: 'No .gitignore' };
|
|
93
|
+
return fs.readFileSync('.gitignore', 'utf8').includes('.env');
|
|
94
|
+
}},
|
|
95
|
+
{ id: 'SEC-04', name: 'Config file permissions 600', severity: 'HIGH', check: () => {
|
|
96
|
+
if (!fs.existsSync(CONFIG_FILE)) return { skipped: true, reason: 'No config file' };
|
|
97
|
+
try { return (fs.statSync(CONFIG_FILE).mode & 0o777).toString(8) === '600'; }
|
|
98
|
+
catch { return { skipped: true, reason: 'Cannot read permissions' }; }
|
|
99
|
+
}},
|
|
100
|
+
{ id: 'SEC-05', name: 'crypto.randomBytes for key gen', severity: 'CRITICAL', check: () => findInSource('randomBytes').found },
|
|
101
|
+
{ id: 'SEC-06', name: 'IP sanitiser strips internal IPs', severity: 'CRITICAL', check: () => findInSource('100\\\\.\\d|192\\\\.168|sanitise').found },
|
|
102
|
+
{ id: 'SEC-07', name: 'No Tailscale IPs in published npm files', severity: 'CRITICAL', check: () => {
|
|
103
|
+
const r = findInSource('100\\.88\\.118\\.128|100\\.98\\.118\\.33|100\\.77\\.206\\.9|100\\.121\\.187\\.67');
|
|
104
|
+
return !r.found;
|
|
105
|
+
}},
|
|
106
|
+
],
|
|
107
|
+
|
|
108
|
+
api: [
|
|
109
|
+
{ id: 'API-01', name: 'Versioned routes (/v1/)', severity: 'HIGH', check: () => findInSource('/v1/').found },
|
|
110
|
+
{ id: 'API-02', name: 'Rate limiting implemented', severity: 'HIGH', check: () => findInSource('rateLimit|rate.*limit|rateTracker').found },
|
|
111
|
+
{ id: 'API-03', name: 'Health endpoint exists', severity: 'HIGH', check: () => findInSource('/health').found },
|
|
112
|
+
{ id: 'API-04', name: 'CORS configured', severity: 'HIGH', check: () => findInSource('Access-Control-Allow-Origin|cors').found },
|
|
113
|
+
{ id: 'API-05', name: 'Clerk auth on user routes', severity: 'HIGH', check: () => findInSource('requireUser|clerkUser|verifySession').found },
|
|
114
|
+
{ id: 'API-06', name: 'WebSocket server implemented', severity: 'MEDIUM', check: () => findInSource('WebSocketServer|wss|/ws').found },
|
|
115
|
+
],
|
|
116
|
+
|
|
117
|
+
data: [
|
|
118
|
+
{ id: 'DATA-01', name: 'User keys file-based (PG migration pending)', severity: 'MEDIUM', check: () => findInSource('user-keys.json').found },
|
|
119
|
+
{ id: 'DATA-02', name: 'Key validation endpoint exists', severity: 'HIGH', check: () => findInSource('validate-key|validateKey').found },
|
|
120
|
+
{ id: 'DATA-03', name: 'Keys masked in API responses', severity: 'CRITICAL', check: () => findInSource('substring.*12|slice\\(-4\\)').found },
|
|
121
|
+
{ id: 'DATA-04', name: 'User-scoped key queries (userId filter)', severity: 'CRITICAL', check: () => findInSource('userId.*filter|filter.*userId').found },
|
|
122
|
+
{ id: 'DATA-05', name: 'Azure Key Vault sync on key ops', severity: 'HIGH', check: () => findInSource('keyvault|syncKeyToVault|navada-edge-vault').found },
|
|
123
|
+
{ id: 'DATA-06', name: 'Telemetry hashes hostname (no PII)', severity: 'HIGH', check: () => findInSource('createHash.*hostname|sha256.*hostname').found },
|
|
124
|
+
],
|
|
125
|
+
|
|
126
|
+
docker: [
|
|
127
|
+
{ id: 'DOCK-01', name: 'Docker Compose exists', severity: 'HIGH', check: () => fs.existsSync('docker-compose.yml') || fs.existsSync('Dockerfile') },
|
|
128
|
+
{ id: 'DOCK-02', name: 'Restart policy: always', severity: 'HIGH', check: () => {
|
|
129
|
+
if (!fs.existsSync('docker-compose.yml')) return { skipped: true, reason: 'No compose file' };
|
|
130
|
+
return fs.readFileSync('docker-compose.yml', 'utf8').includes('restart: always');
|
|
131
|
+
}},
|
|
132
|
+
{ id: 'DOCK-03', name: 'Healthchecks defined', severity: 'MEDIUM', check: () => {
|
|
133
|
+
if (!fs.existsSync('docker-compose.yml') && !fs.existsSync('Dockerfile')) return { skipped: true, reason: 'No Docker files' };
|
|
134
|
+
const content = fs.existsSync('docker-compose.yml') ? fs.readFileSync('docker-compose.yml', 'utf8') : fs.readFileSync('Dockerfile', 'utf8');
|
|
135
|
+
return content.toLowerCase().includes('healthcheck');
|
|
136
|
+
}},
|
|
137
|
+
{ id: 'DOCK-04', name: 'Non-root user in Dockerfile', severity: 'MEDIUM', check: () => {
|
|
138
|
+
if (!fs.existsSync('Dockerfile')) return { skipped: true, reason: 'No Dockerfile' };
|
|
139
|
+
const content = fs.readFileSync('Dockerfile', 'utf8');
|
|
140
|
+
return content.includes('USER ') && !content.includes('USER root');
|
|
141
|
+
}},
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
compliance: [
|
|
145
|
+
{ id: 'COMP-01', name: 'Privacy Policy exists', severity: 'HIGH', check: () => fs.existsSync('privacy.md') || fs.existsSync('PRIVACY.md') || findInSource('/privacy').found },
|
|
146
|
+
{ id: 'COMP-02', name: 'Terms of Service exists', severity: 'HIGH', check: () => fs.existsSync('terms.md') || fs.existsSync('TERMS.md') || findInSource('/terms').found },
|
|
147
|
+
{ id: 'COMP-03', name: 'CHANGELOG exists', severity: 'MEDIUM', check: () => fs.existsSync('CHANGELOG.md') },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────
|
|
152
|
+
// AI AUDIT PROMPT — matches current deployed state
|
|
153
|
+
// ─────────────────────────────────────────────
|
|
154
|
+
const AI_PROMPT = `You are auditing the NAVADA Edge Platform as deployed TODAY. Be precise — only reference what actually exists.
|
|
155
|
+
|
|
156
|
+
CURRENT STATE (March 2026):
|
|
157
|
+
- CLI: navada-edge-cli v3.4.1 on npm, 75 commands, 10 local tools (shell, files, python, sandbox)
|
|
158
|
+
- SDK: navada-edge-sdk v1.2.0 on npm, zero deps
|
|
159
|
+
- AI Providers: 6 (NAVADA free via OpenAI proxy, Anthropic, OpenAI, Gemini, NVIDIA, HuggingFace)
|
|
160
|
+
- Free tier: Full tool use via GPT-4o-mini proxied through NAVADA dashboard, no API key needed
|
|
161
|
+
- Auth: Clerk on portal (portal.navada-edge-server.uk), API keys (nv_edge_ format, 56 chars)
|
|
162
|
+
- Key storage: File-based JSON (data/user-keys.json) — NOT in a database yet
|
|
163
|
+
- Keys: Generated with crypto.randomBytes(24), stored as plaintext JSON — NOT hashed with bcrypt yet
|
|
164
|
+
- IP sanitiser: Strips 100.x.x.x, 192.168.x.x, 10.x.x.x from user-facing responses
|
|
165
|
+
- Dashboard: Express on :7900, WebSocket on /ws, SSE for metrics
|
|
166
|
+
- Portal: Next.js + Clerk on :3500, via Cloudflare tunnel
|
|
167
|
+
- Infrastructure: 4 Docker nodes (ASUS 9, EC2 7, Oracle 8, HP 2 = 26 containers)
|
|
168
|
+
- Networking: Tailscale VPN mesh, Cloudflare tunnel (18 subdomains), navada-edge Docker network
|
|
169
|
+
- Database: PostgreSQL 17 on HP :5433 (host, not Docker) — used for projects, NOT for user data yet
|
|
170
|
+
- Registry: Private Docker registry at ASUS:5000
|
|
171
|
+
- Portainer: Oracle :9000 managing all 4 nodes via agents
|
|
172
|
+
- Azure Key Vault: navada-edge-vault stores all secrets, user keys synced on create/revoke
|
|
173
|
+
|
|
174
|
+
NOT YET BUILT:
|
|
175
|
+
- Edge Compute MCP (/offload, /sessions, /attach)
|
|
176
|
+
- Agent customisation (agent.md, sub-agents)
|
|
177
|
+
- Azure compute node
|
|
178
|
+
- Billing/metering
|
|
179
|
+
- User data in PostgreSQL (still file-based)
|
|
180
|
+
- bcrypt key hashing
|
|
181
|
+
- GDPR endpoints (deletion, export)
|
|
182
|
+
- OpenAPI spec
|
|
183
|
+
- Privacy Policy / Terms of Service
|
|
184
|
+
|
|
185
|
+
Based on the static check results, audit:
|
|
186
|
+
1. SECURITY: Key handling, auth, IP exposure, file permissions, secret management
|
|
187
|
+
2. API DESIGN: Versioning, rate limiting, error handling, CORS, WebSocket auth
|
|
188
|
+
3. DATA: User isolation, key masking, storage security, vault sync
|
|
189
|
+
4. DOCKER: Compose management, healthchecks, restart policies, network isolation
|
|
190
|
+
5. GAPS: What must be fixed before first paying customer (prioritised)
|
|
191
|
+
|
|
192
|
+
For each finding: [PASS/FAIL/PARTIAL] — ID — Item — Severity — One-line fix
|
|
193
|
+
End with: Top 5 priorities before first customer (numbered, actionable).`;
|
|
194
|
+
|
|
195
|
+
// ─────────────────────────────────────────────
|
|
196
|
+
// RUN CHECKS
|
|
197
|
+
// ─────────────────────────────────────────────
|
|
198
|
+
function runCheck(check) {
|
|
199
|
+
try {
|
|
200
|
+
const r = check.check();
|
|
201
|
+
if (r === true) return 'PASS';
|
|
202
|
+
if (r === false) return 'FAIL';
|
|
203
|
+
if (r && r.skipped) return 'SKIP';
|
|
204
|
+
if (r && r.pass === false) return 'FAIL';
|
|
205
|
+
return 'PASS';
|
|
206
|
+
} catch { return 'ERROR'; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runAIAudit(staticSummary) {
|
|
210
|
+
const apiKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
|
|
211
|
+
if (!apiKey) return null;
|
|
212
|
+
|
|
213
|
+
const https = require('https');
|
|
214
|
+
const body = JSON.stringify({
|
|
215
|
+
model: 'claude-sonnet-4-20250514',
|
|
216
|
+
max_tokens: 4000,
|
|
217
|
+
messages: [{ role: 'user', content: `${AI_PROMPT}\n\nSTATIC RESULTS:\n${staticSummary}` }],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const req = https.request('https://api.anthropic.com/v1/messages', {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
224
|
+
timeout: 60000,
|
|
225
|
+
}, (res) => {
|
|
226
|
+
let data = '';
|
|
227
|
+
res.on('data', c => data += c);
|
|
228
|
+
res.on('end', () => {
|
|
229
|
+
try {
|
|
230
|
+
const d = JSON.parse(data);
|
|
231
|
+
resolve(d.content?.[0]?.text || d.error?.message || 'No response');
|
|
232
|
+
} catch { resolve('Failed to parse AI response'); }
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
req.on('error', (e) => resolve(`AI audit error: ${e.message}`));
|
|
236
|
+
req.on('timeout', () => { req.destroy(); resolve('AI audit timed out'); });
|
|
237
|
+
req.write(body);
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─────────────────────────────────────────────
|
|
243
|
+
// CLI COMMAND REGISTRATION
|
|
244
|
+
// ─────────────────────────────────────────────
|
|
245
|
+
module.exports = function(reg) {
|
|
246
|
+
|
|
247
|
+
reg('audit', 'Enterprise security and compliance audit', async (args) => {
|
|
248
|
+
const section = args[0];
|
|
249
|
+
const saveFlag = args.includes('--save');
|
|
250
|
+
const jsonFlag = args.includes('--json');
|
|
251
|
+
|
|
252
|
+
if (section === 'help') {
|
|
253
|
+
console.log(ui.header('NAVADA ENTERPRISE AUDIT'));
|
|
254
|
+
console.log(ui.cmd('audit', 'Full audit (all sections)'));
|
|
255
|
+
console.log(ui.cmd('audit cli', 'CLI standards checks'));
|
|
256
|
+
console.log(ui.cmd('audit security', 'Security checks'));
|
|
257
|
+
console.log(ui.cmd('audit api', 'API design checks'));
|
|
258
|
+
console.log(ui.cmd('audit data', 'Data management checks'));
|
|
259
|
+
console.log(ui.cmd('audit docker', 'Docker/infrastructure checks'));
|
|
260
|
+
console.log(ui.cmd('audit compliance', 'Compliance checks'));
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(ui.dim('Flags: --save (save report) --json (machine output)'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const startTime = Date.now();
|
|
267
|
+
if (!jsonFlag) console.log(ui.header('NAVADA ENTERPRISE AUDIT'));
|
|
268
|
+
|
|
269
|
+
// Determine sections to run
|
|
270
|
+
const validSections = Object.keys(CHECKS);
|
|
271
|
+
const sections = section && validSections.includes(section) ? { [section]: CHECKS[section] } : CHECKS;
|
|
272
|
+
|
|
273
|
+
if (section && !validSections.includes(section) && section !== '--save' && section !== '--json') {
|
|
274
|
+
console.log(ui.error(`Unknown section: ${section}`));
|
|
275
|
+
console.log(ui.dim(`Available: ${validSections.join(', ')}`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Run static checks
|
|
280
|
+
const results = {};
|
|
281
|
+
let pass = 0, fail = 0, skip = 0;
|
|
282
|
+
const criticals = [];
|
|
283
|
+
|
|
284
|
+
for (const [name, checks] of Object.entries(sections)) {
|
|
285
|
+
results[name] = [];
|
|
286
|
+
if (!jsonFlag) console.log(`\n ${ui.dim(name.toUpperCase())}`);
|
|
287
|
+
|
|
288
|
+
for (const check of checks) {
|
|
289
|
+
const status = runCheck(check);
|
|
290
|
+
results[name].push({ id: check.id, name: check.name, severity: check.severity, status });
|
|
291
|
+
|
|
292
|
+
if (status === 'PASS') pass++;
|
|
293
|
+
else if (status === 'FAIL') { fail++; if (check.severity === 'CRITICAL') criticals.push(check); }
|
|
294
|
+
else skip++;
|
|
295
|
+
|
|
296
|
+
if (!jsonFlag) {
|
|
297
|
+
const icon = status === 'PASS' ? '\x1b[32mPASS\x1b[0m' : status === 'FAIL' ? '\x1b[31mFAIL\x1b[0m' : '\x1b[2mSKIP\x1b[0m';
|
|
298
|
+
const sev = check.severity === 'CRITICAL' ? '\x1b[31m' : check.severity === 'HIGH' ? '\x1b[33m' : '\x1b[2m';
|
|
299
|
+
console.log(` ${icon} ${sev}${check.severity.padEnd(8)}\x1b[0m ${check.id} ${check.name}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Summary
|
|
305
|
+
const total = pass + fail + skip;
|
|
306
|
+
const rate = Math.round((pass / (total - skip)) * 100);
|
|
307
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
308
|
+
|
|
309
|
+
if (!jsonFlag) {
|
|
310
|
+
console.log(ui.header('SUMMARY'));
|
|
311
|
+
console.log(ui.label('PASS', String(pass)));
|
|
312
|
+
console.log(ui.label('FAIL', String(fail)));
|
|
313
|
+
console.log(ui.label('SKIP', String(skip)));
|
|
314
|
+
console.log(ui.label('Pass rate', `${rate}%`));
|
|
315
|
+
console.log(ui.label('Time', `${elapsed}s`));
|
|
316
|
+
|
|
317
|
+
if (criticals.length > 0) {
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log(ui.error('CRITICAL FAILURES:'));
|
|
320
|
+
for (const c of criticals) console.log(ui.error(` ${c.id} — ${c.name}`));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// AI audit (if no specific section and API key available)
|
|
325
|
+
let aiResult = null;
|
|
326
|
+
if (!section || section === '--save') {
|
|
327
|
+
const summary = Object.entries(results).map(([s, checks]) =>
|
|
328
|
+
`[${s.toUpperCase()}]\n` + checks.map(c => ` ${c.status.padEnd(5)} ${c.id} — ${c.name}`).join('\n')
|
|
329
|
+
).join('\n');
|
|
330
|
+
|
|
331
|
+
if (!jsonFlag) console.log(ui.dim('\n Running AI audit...'));
|
|
332
|
+
aiResult = await runAIAudit(summary);
|
|
333
|
+
if (aiResult && !jsonFlag) {
|
|
334
|
+
console.log(ui.header('AI AUDIT FINDINGS'));
|
|
335
|
+
console.log(` ${aiResult}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Save report
|
|
340
|
+
if (saveFlag) {
|
|
341
|
+
const report = { meta: { date: new Date().toISOString(), elapsed, cwd: process.cwd() }, summary: { total, pass, fail, skip, rate }, results, aiAudit: aiResult };
|
|
342
|
+
try {
|
|
343
|
+
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
|
344
|
+
const file = path.join(REPORTS_DIR, `audit-${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
|
|
345
|
+
fs.writeFileSync(file, JSON.stringify(report, null, 2));
|
|
346
|
+
if (!jsonFlag) console.log(ui.success(`Report saved: ${file}`));
|
|
347
|
+
} catch (e) { if (!jsonFlag) console.log(ui.warn(`Could not save: ${e.message}`)); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (jsonFlag) {
|
|
351
|
+
console.log(JSON.stringify({ summary: { total, pass, fail, skip, rate }, results, aiAudit: aiResult }, null, 2));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
}, { category: 'SYSTEM', subs: ['cli', 'security', 'api', 'data', 'docker', 'compliance', 'help'] });
|
|
355
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const navada = require('navada-edge-sdk');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const config = require('../config');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
|
|
9
|
+
const COMPUTE_ENDPOINT = 'https://edge-compute.navada-edge-server.uk';
|
|
10
|
+
|
|
11
|
+
function getEdgeKey() {
|
|
12
|
+
return config.get('edgeKey') || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function request(method, path, body) {
|
|
16
|
+
const key = getEdgeKey();
|
|
17
|
+
if (!key) return Promise.reject(new Error('Not connected. Run /edge login <key> first.'));
|
|
18
|
+
|
|
19
|
+
const url = new URL(COMPUTE_ENDPOINT + path);
|
|
20
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
21
|
+
const payload = body ? JSON.stringify(body) : '';
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const req = transport.request(url, {
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'x-api-key': key,
|
|
29
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
30
|
+
},
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
}, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', c => data += c);
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
|
37
|
+
catch { resolve({ status: res.statusCode, data }); }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
42
|
+
if (payload) req.write(payload);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = function(reg) {
|
|
48
|
+
|
|
49
|
+
// ── /offload ── Send a task to the cloud
|
|
50
|
+
reg('offload', 'Run a task on the Edge Network (24/7 cloud)', async (args) => {
|
|
51
|
+
if (!args.length) {
|
|
52
|
+
console.log(ui.header('EDGE COMPUTE — OFFLOAD'));
|
|
53
|
+
console.log(ui.cmd('offload <command>', 'Run a shell command on EC2'));
|
|
54
|
+
console.log(ui.cmd('offload --python <code>', 'Run Python code on EC2'));
|
|
55
|
+
console.log(ui.cmd('offload --js <code>', 'Run JavaScript on EC2'));
|
|
56
|
+
console.log(ui.cmd('offload --name <name> <cmd>', 'Name the task'));
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(ui.dim('Examples:'));
|
|
59
|
+
console.log(ui.dim(' /offload npm test'));
|
|
60
|
+
console.log(ui.dim(' /offload --python "import time; time.sleep(60); print(\'done\')"'));
|
|
61
|
+
console.log(ui.dim(' /offload --name "nightly-backup" tar czf backup.tar.gz /data'));
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(ui.dim('Requires Edge Network connection: /edge login <key>'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isPython = args.includes('--python');
|
|
68
|
+
const isJs = args.includes('--js');
|
|
69
|
+
const nameIdx = args.indexOf('--name');
|
|
70
|
+
let name = 'task';
|
|
71
|
+
let filteredArgs = [...args];
|
|
72
|
+
|
|
73
|
+
if (nameIdx >= 0) {
|
|
74
|
+
name = args[nameIdx + 1] || 'task';
|
|
75
|
+
filteredArgs.splice(nameIdx, 2);
|
|
76
|
+
}
|
|
77
|
+
filteredArgs = filteredArgs.filter(a => a !== '--python' && a !== '--js');
|
|
78
|
+
|
|
79
|
+
const body = {};
|
|
80
|
+
if (isPython) {
|
|
81
|
+
body.code = filteredArgs.join(' ');
|
|
82
|
+
body.language = 'python';
|
|
83
|
+
} else if (isJs) {
|
|
84
|
+
body.code = filteredArgs.join(' ');
|
|
85
|
+
body.language = 'javascript';
|
|
86
|
+
} else {
|
|
87
|
+
body.command = filteredArgs.join(' ');
|
|
88
|
+
}
|
|
89
|
+
body.name = name;
|
|
90
|
+
|
|
91
|
+
const ora = require('ora');
|
|
92
|
+
const spinner = ora({ text: ' Offloading to Edge Network...', color: 'white' }).start();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const r = await request('POST', '/offload', body);
|
|
96
|
+
spinner.stop();
|
|
97
|
+
|
|
98
|
+
if (r.status === 201) {
|
|
99
|
+
console.log(ui.success(`Task offloaded: ${r.data.session.id}`));
|
|
100
|
+
console.log(ui.label('Session', r.data.session.id));
|
|
101
|
+
console.log(ui.label('Task', r.data.session.task));
|
|
102
|
+
console.log(ui.label('Status', r.data.session.status));
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(ui.dim(`Track: /sessions`));
|
|
105
|
+
console.log(ui.dim(`Output: /attach ${r.data.session.id}`));
|
|
106
|
+
} else if (r.status === 401) {
|
|
107
|
+
console.log(ui.error('Invalid API key. Run /edge login <key>'));
|
|
108
|
+
} else if (r.status === 429) {
|
|
109
|
+
console.log(ui.warn(r.data.error || 'Concurrent task limit reached'));
|
|
110
|
+
} else {
|
|
111
|
+
console.log(ui.error(r.data.error || `HTTP ${r.status}`));
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
spinner.stop();
|
|
115
|
+
console.log(ui.error(`Edge Compute unreachable: ${e.message}`));
|
|
116
|
+
console.log(ui.dim('The Edge Network may be offline. Try /doctor'));
|
|
117
|
+
}
|
|
118
|
+
}, { category: 'EDGE' });
|
|
119
|
+
|
|
120
|
+
// ── /sessions ── List cloud sessions
|
|
121
|
+
reg('sessions', 'View Edge Network task sessions', async () => {
|
|
122
|
+
const ora = require('ora');
|
|
123
|
+
const spinner = ora({ text: ' Loading sessions...', color: 'white' }).start();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const r = await request('GET', '/sessions');
|
|
127
|
+
spinner.stop();
|
|
128
|
+
|
|
129
|
+
if (r.status === 401) { console.log(ui.error('Not connected. /edge login <key>')); return; }
|
|
130
|
+
if (r.status !== 200) { console.log(ui.error(r.data.error || `HTTP ${r.status}`)); return; }
|
|
131
|
+
|
|
132
|
+
console.log(ui.header('EDGE SESSIONS'));
|
|
133
|
+
const sessions = r.data.sessions || [];
|
|
134
|
+
|
|
135
|
+
if (sessions.length === 0) {
|
|
136
|
+
console.log(ui.dim('No sessions yet. /offload <command> to start one.'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const s of sessions) {
|
|
141
|
+
const statusColor = s.status === 'completed' ? '\x1b[32m' : s.status === 'running' ? '\x1b[33m' : '\x1b[31m';
|
|
142
|
+
const statusLabel = `${statusColor}${s.status.toUpperCase()}\x1b[0m`;
|
|
143
|
+
const age = s.completedAt ? timeSince(s.completedAt) : timeSince(s.createdAt);
|
|
144
|
+
console.log(` ${statusLabel} ${s.id.slice(0, 16)} ${(s.task || '').padEnd(20)} ${age}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(ui.label('Total', String(r.data.total)));
|
|
149
|
+
console.log(ui.label('Limits', `${r.data.limits.concurrent} concurrent, ${Math.round(r.data.limits.maxRuntime / 60000)}min max`));
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(ui.dim('Details: /attach <session-id>'));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
spinner.stop();
|
|
154
|
+
console.log(ui.error(`Edge Compute unreachable: ${e.message}`));
|
|
155
|
+
}
|
|
156
|
+
}, { category: 'EDGE' });
|
|
157
|
+
|
|
158
|
+
// ── /attach ── Stream session output
|
|
159
|
+
reg('attach', 'Attach to a running Edge session (stream output)', async (args) => {
|
|
160
|
+
const id = args[0];
|
|
161
|
+
if (!id) {
|
|
162
|
+
console.log(ui.dim('Usage: /attach <session-id>'));
|
|
163
|
+
console.log(ui.dim('List sessions: /sessions'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If short ID, try to match
|
|
168
|
+
const sessionId = id.startsWith('ses_') ? id : `ses_${id}`;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const r = await request('GET', `/session/${sessionId}`);
|
|
172
|
+
if (r.status === 404) { console.log(ui.error('Session not found')); return; }
|
|
173
|
+
if (r.status === 401) { console.log(ui.error('Not connected. /edge login <key>')); return; }
|
|
174
|
+
|
|
175
|
+
const s = r.data;
|
|
176
|
+
console.log(ui.header(`SESSION ${s.id}`));
|
|
177
|
+
console.log(ui.label('Task', s.task?.name || s.task?.command?.slice(0, 60) || '?'));
|
|
178
|
+
console.log(ui.label('Status', s.status));
|
|
179
|
+
console.log(ui.label('Created', s.createdAt));
|
|
180
|
+
if (s.completedAt) console.log(ui.label('Completed', s.completedAt));
|
|
181
|
+
if (s.exitCode !== null) console.log(ui.label('Exit code', String(s.exitCode)));
|
|
182
|
+
if (s.error) console.log(ui.label('Error', s.error));
|
|
183
|
+
|
|
184
|
+
if (s.output) {
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(ui.dim('── OUTPUT ──'));
|
|
187
|
+
console.log(s.output);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (s.status === 'running') {
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(ui.dim('Session is running. Output will update on refresh.'));
|
|
193
|
+
console.log(ui.dim('Kill: /kill ' + s.id));
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.log(ui.error(`Edge Compute unreachable: ${e.message}`));
|
|
197
|
+
}
|
|
198
|
+
}, { category: 'EDGE' });
|
|
199
|
+
|
|
200
|
+
// ── /kill ── Kill a running session
|
|
201
|
+
reg('kill', 'Kill a running Edge session', async (args) => {
|
|
202
|
+
const id = args[0];
|
|
203
|
+
if (!id) { console.log(ui.dim('Usage: /kill <session-id>')); return; }
|
|
204
|
+
const sessionId = id.startsWith('ses_') ? id : `ses_${id}`;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const r = await request('DELETE', `/session/${sessionId}`);
|
|
208
|
+
if (r.status === 200) {
|
|
209
|
+
console.log(ui.success(`Session ${sessionId} killed`));
|
|
210
|
+
} else {
|
|
211
|
+
console.log(ui.error(r.data.error || `HTTP ${r.status}`));
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.log(ui.error(e.message));
|
|
215
|
+
}
|
|
216
|
+
}, { category: 'EDGE' });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function timeSince(iso) {
|
|
220
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
221
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s ago`;
|
|
222
|
+
if (ms < 3600000) return `${Math.round(ms / 60000)}m ago`;
|
|
223
|
+
if (ms < 86400000) return `${Math.round(ms / 3600000)}h ago`;
|
|
224
|
+
return `${Math.round(ms / 86400000)}d ago`;
|
|
225
|
+
}
|
package/lib/commands/edge.js
CHANGED
|
@@ -9,6 +9,8 @@ const VALIDATE_ENDPOINTS = [
|
|
|
9
9
|
'https://api.navada-edge-server.uk/api/v1/public/validate-key',
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
const { sessionState, listSubAgents } = require('../agent');
|
|
13
|
+
|
|
12
14
|
module.exports = function(reg) {
|
|
13
15
|
|
|
14
16
|
// /onboard — open portal in browser
|
|
@@ -51,9 +53,10 @@ module.exports = function(reg) {
|
|
|
51
53
|
console.log(ui.cmd('edge status', 'Check your Edge Network connection'));
|
|
52
54
|
console.log(ui.cmd('edge logout', 'Disconnect from Edge Network'));
|
|
53
55
|
console.log(ui.cmd('edge tier', 'Show your current tier and limits'));
|
|
56
|
+
console.log(ui.cmd('edge setup', 'Create agent.md and sub-agents directory'));
|
|
54
57
|
console.log(ui.cmd('onboard', 'Create account and get API key'));
|
|
55
58
|
console.log('');
|
|
56
|
-
console.log(ui.dim('Get started: /onboard'));
|
|
59
|
+
console.log(ui.dim('Get started: /onboard | Customise: /edge setup'));
|
|
57
60
|
return;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -179,8 +182,133 @@ module.exports = function(reg) {
|
|
|
179
182
|
return;
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
// /edge setup — create agent.md and agents/ directory
|
|
186
|
+
if (sub === 'setup') {
|
|
187
|
+
const fs = require('fs');
|
|
188
|
+
const path = require('path');
|
|
189
|
+
const agentDir = path.join(config.CONFIG_DIR, 'agents');
|
|
190
|
+
const agentMd = path.join(config.CONFIG_DIR, 'agent.md');
|
|
191
|
+
|
|
192
|
+
console.log(ui.header('NAVADA EDGE — AGENT SETUP'));
|
|
193
|
+
|
|
194
|
+
// Create directories
|
|
195
|
+
if (!fs.existsSync(config.CONFIG_DIR)) fs.mkdirSync(config.CONFIG_DIR, { recursive: true });
|
|
196
|
+
if (!fs.existsSync(agentDir)) fs.mkdirSync(agentDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
// Create agent.md if it doesn't exist
|
|
199
|
+
if (!fs.existsSync(agentMd)) {
|
|
200
|
+
fs.writeFileSync(agentMd, `# My NAVADA Agent
|
|
201
|
+
|
|
202
|
+
## About Me
|
|
203
|
+
<!-- Tell the agent who you are, what you do, what stack you use -->
|
|
204
|
+
I am a developer working on...
|
|
205
|
+
|
|
206
|
+
## My Infrastructure
|
|
207
|
+
<!-- Describe your servers, services, databases -->
|
|
208
|
+
- ...
|
|
209
|
+
|
|
210
|
+
## Preferences
|
|
211
|
+
<!-- How should the agent behave? What tone? What conventions? -->
|
|
212
|
+
- Be concise and technical
|
|
213
|
+
- Use TypeScript for new code
|
|
214
|
+
- Always explain changes before making them
|
|
215
|
+
|
|
216
|
+
## Automation Rules
|
|
217
|
+
<!-- What should the agent do automatically? -->
|
|
218
|
+
- ...
|
|
219
|
+
`);
|
|
220
|
+
console.log(ui.success('Created ~/.navada/agent.md'));
|
|
221
|
+
console.log(ui.dim(' Edit this file to customise your agent\'s personality and context.'));
|
|
222
|
+
} else {
|
|
223
|
+
console.log(ui.dim(' agent.md already exists'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Create example sub-agent
|
|
227
|
+
const exampleAgent = path.join(agentDir, 'deploy-bot.md');
|
|
228
|
+
if (!fs.existsSync(exampleAgent)) {
|
|
229
|
+
fs.writeFileSync(exampleAgent, `You are a deployment specialist agent. When the user asks you to deploy, you:
|
|
230
|
+
1. Check the current git status
|
|
231
|
+
2. Run tests if a test script exists
|
|
232
|
+
3. Build the Docker image
|
|
233
|
+
4. Push to the registry at 100.88.118.128:5000
|
|
234
|
+
5. Deploy to the target node via SSH
|
|
235
|
+
|
|
236
|
+
Be methodical. Confirm each step before proceeding. Report any failures immediately.
|
|
237
|
+
`);
|
|
238
|
+
console.log(ui.success('Created ~/.navada/agents/deploy-bot.md (example)'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(ui.label('Agent config', agentMd));
|
|
243
|
+
console.log(ui.label('Sub-agents', agentDir));
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(ui.dim('Your agent.md is loaded every time you chat.'));
|
|
246
|
+
console.log(ui.dim('Sub-agents: /agent list | /agent use deploy-bot'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
182
250
|
console.log(ui.dim('Unknown subcommand. Try /edge help'));
|
|
183
251
|
|
|
184
|
-
}, { category: 'EDGE', subs: ['login', 'status', 'logout', 'tier', 'help'] });
|
|
252
|
+
}, { category: 'EDGE', subs: ['login', 'status', 'logout', 'tier', 'setup', 'help'] });
|
|
253
|
+
|
|
254
|
+
// ── /agent ── Manage sub-agents
|
|
255
|
+
reg('agent', 'Manage NAVADA sub-agents', (args) => {
|
|
256
|
+
const sub = args[0];
|
|
257
|
+
const fs = require('fs');
|
|
258
|
+
const path = require('path');
|
|
259
|
+
|
|
260
|
+
if (!sub || sub === 'list') {
|
|
261
|
+
const agents = listSubAgents();
|
|
262
|
+
console.log(ui.header('SUB-AGENTS'));
|
|
263
|
+
if (agents.length === 0) {
|
|
264
|
+
console.log(ui.dim('No sub-agents. Create one: /edge setup'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
for (const a of agents) {
|
|
268
|
+
const active = sessionState.subAgent === a ? ' \x1b[32m(active)\x1b[0m' : '';
|
|
269
|
+
console.log(ui.label(a, active));
|
|
270
|
+
}
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(ui.dim('Use: /agent use <name> | Off: /agent off'));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (sub === 'use' && args[1]) {
|
|
277
|
+
const name = args[1];
|
|
278
|
+
const agentFile = path.join(config.CONFIG_DIR, 'agents', `${name}.md`);
|
|
279
|
+
if (!fs.existsSync(agentFile)) {
|
|
280
|
+
console.log(ui.error(`Sub-agent not found: ${name}`));
|
|
281
|
+
console.log(ui.dim('Available: ' + (listSubAgents().join(', ') || 'none')));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
sessionState.subAgent = name;
|
|
285
|
+
console.log(ui.success(`Sub-agent activated: ${name}`));
|
|
286
|
+
console.log(ui.dim('The agent will now use this persona. /agent off to reset.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (sub === 'off' || sub === 'reset') {
|
|
291
|
+
sessionState.subAgent = null;
|
|
292
|
+
console.log(ui.success('Sub-agent deactivated. Using default NAVADA agent.'));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (sub === 'show' && args[1]) {
|
|
297
|
+
const agentFile = path.join(config.CONFIG_DIR, 'agents', `${args[1]}.md`);
|
|
298
|
+
if (!fs.existsSync(agentFile)) { console.log(ui.error('Not found')); return; }
|
|
299
|
+
console.log(ui.header(`AGENT: ${args[1]}`));
|
|
300
|
+
console.log(fs.readFileSync(agentFile, 'utf-8'));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(ui.header('AGENT COMMANDS'));
|
|
305
|
+
console.log(ui.cmd('agent list', 'List all sub-agents'));
|
|
306
|
+
console.log(ui.cmd('agent use <name>', 'Activate a sub-agent'));
|
|
307
|
+
console.log(ui.cmd('agent off', 'Deactivate sub-agent'));
|
|
308
|
+
console.log(ui.cmd('agent show <name>', 'View sub-agent prompt'));
|
|
309
|
+
console.log('');
|
|
310
|
+
console.log(ui.dim('Create agents: /edge setup'));
|
|
311
|
+
console.log(ui.dim('Agents live in: ~/.navada/agents/<name>.md'));
|
|
312
|
+
}, { category: 'EDGE', subs: ['list', 'use', 'off', 'show', 'reset'] });
|
|
185
313
|
|
|
186
314
|
};
|
package/lib/commands/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const { register } = require('../registry');
|
|
|
6
6
|
const moduleNames = [
|
|
7
7
|
'network', 'mcp', 'lucas', 'docker', 'database', 'cloudflare',
|
|
8
8
|
'ai', 'azure', 'agents', 'tasks', 'keys', 'setup', 'system',
|
|
9
|
-
'learn', 'sandbox', 'nvidia', 'edge', 'conversations',
|
|
9
|
+
'learn', 'sandbox', 'nvidia', 'edge', 'conversations', 'audit', 'compute',
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
function loadAll() {
|
package/package.json
CHANGED