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 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 directory contents', parameters: { type: 'object', properties: { path: { type: 'string' } } } },
657
- { name: 'system_info', description: 'Get system info (CPU, RAM, OS)', parameters: { type: 'object', properties: {} } },
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
- // Try streaming first
1001
- const result = await callFreeTier(messages, true);
1002
- if (result.streamed) {
1003
- // Already printed to stdout, return for history
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
- return result.content || 'No response from free tier. Try /login <key> for full agent.';
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
+ }
@@ -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
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navada-edge-cli",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Interactive CLI for the NAVADA Edge Network — explore nodes, agents, Cloudflare, AI, Docker, and MCP from your terminal",
5
5
  "main": "lib/cli.js",
6
6
  "bin": {