navada-edge-cli 3.5.0 → 3.5.2

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
@@ -21,7 +21,7 @@ const IDENTITY = {
21
21
  You are professional, technical, concise, and helpful. You speak with authority about distributed systems, Docker, AI, and cloud infrastructure.
22
22
  You have FULL ACCESS to the user's computer — you CAN and SHOULD use your tools to execute tasks:
23
23
  - shell: run ANY bash, PowerShell, or system command on the user's machine
24
- - read_file / write_file / list_files: full filesystem access — create, read, modify any file
24
+ - read_file / write_file / edit_file / delete_file / list_files: full filesystem CRUD — create, read, edit, delete any file
25
25
  - python_exec / python_pip / python_script: run Python code directly
26
26
  - sandbox_run: run code with syntax-highlighted output
27
27
  - system_info: check CPU, RAM, disk, OS
@@ -34,6 +34,7 @@ You also connect to the NAVADA Edge Network (4 nodes via Tailscale VPN):
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
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
36
  When asked to create, edit, or delete files — use the file tools directly. You are a terminal agent with FULL access.
37
+ PLATFORM: This machine runs ` + (process.platform === 'win32' ? `Windows. Use Windows paths (C:\\Users\\...) not Unix paths (~/...). Desktop = ${path.join(os.homedir(), 'Desktop')}. Home = ${os.homedir()}.` : `${process.platform}. Home = ${os.homedir()}.`) + `
37
38
  Keep responses short. Code blocks when needed. No fluff.`,
38
39
  founder: {
39
40
  name: 'Leslie (Lee) Akpareva',
@@ -224,6 +225,36 @@ const localTools = {
224
225
  },
225
226
  },
226
227
 
228
+ editFile: {
229
+ description: 'Edit a file by replacing a search string with new content',
230
+ execute: (filePath, search, replace) => {
231
+ try {
232
+ const resolved = path.resolve(filePath);
233
+ const content = fs.readFileSync(resolved, 'utf-8');
234
+ if (!content.includes(search)) return `Error: search string not found in ${resolved}`;
235
+ const updated = content.replace(search, replace);
236
+ fs.writeFileSync(resolved, updated);
237
+ return `Edited: ${resolved} (replaced ${search.length} chars)`;
238
+ } catch (e) { return `Error: ${e.message}`; }
239
+ },
240
+ },
241
+
242
+ deleteFile: {
243
+ description: 'Delete a file or empty directory from this machine',
244
+ execute: (filePath) => {
245
+ try {
246
+ const resolved = path.resolve(filePath);
247
+ const stat = fs.statSync(resolved);
248
+ if (stat.isDirectory()) {
249
+ fs.rmdirSync(resolved);
250
+ } else {
251
+ fs.unlinkSync(resolved);
252
+ }
253
+ return `Deleted: ${resolved}`;
254
+ } catch (e) { return `Error: ${e.message}`; }
255
+ },
256
+ },
257
+
227
258
  systemInfo: {
228
259
  description: 'Get system information',
229
260
  execute: () => {
@@ -690,6 +721,8 @@ function openAITools() {
690
721
  { 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
722
  { 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
723
  { name: 'list_files', description: 'List files and directories.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } } },
724
+ { name: 'edit_file', description: 'Edit a file by finding and replacing text. Use for targeted edits.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, search: { type: 'string', description: 'Exact text to find' }, replace: { type: 'string', description: 'Replacement text' } }, required: ['path', 'search', 'replace'] } },
725
+ { name: 'delete_file', description: 'Delete a file or empty directory.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } },
693
726
  { name: 'system_info', description: 'Get local system information (CPU, RAM, disk, OS, hostname).', parameters: { type: 'object', properties: {} } },
694
727
  { 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
728
  { name: 'python_pip', description: 'Install a Python package via pip.', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } },
@@ -871,6 +904,16 @@ async function chat(userMessage, conversationHistory = []) {
871
904
  description: 'List files and directories.',
872
905
  input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } },
873
906
  },
907
+ {
908
+ name: 'edit_file',
909
+ description: 'Edit a file by finding and replacing text. Use for targeted edits without rewriting the whole file.',
910
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, search: { type: 'string', description: 'Exact text to find' }, replace: { type: 'string', description: 'Text to replace it with' } }, required: ['path', 'search', 'replace'] },
911
+ },
912
+ {
913
+ name: 'delete_file',
914
+ description: 'Delete a file or empty directory from the user\'s machine.',
915
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File or directory path to delete' } }, required: ['path'] },
916
+ },
874
917
  {
875
918
  name: 'system_info',
876
919
  description: 'Get local system information (CPU, RAM, disk, OS, hostname).',
@@ -995,6 +1038,8 @@ async function executeTool(name, input) {
995
1038
  case 'read_file': return localTools.readFile.execute(input.path);
996
1039
  case 'write_file': return localTools.writeFile.execute(input.path, input.content);
997
1040
  case 'list_files': return localTools.listFiles.execute(input.path);
1041
+ case 'edit_file': return localTools.editFile.execute(input.path, input.search, input.replace);
1042
+ case 'delete_file': return localTools.deleteFile.execute(input.path);
998
1043
  case 'system_info': return localTools.systemInfo.execute();
999
1044
  case 'network_status': return JSON.stringify(await navada.network.ping());
1000
1045
  case 'lucas_exec': return JSON.stringify(await navada.lucas.exec(input.command));
@@ -1027,7 +1072,97 @@ async function executeTool(name, input) {
1027
1072
  }
1028
1073
  }
1029
1074
 
1075
+ // ---------------------------------------------------------------------------
1076
+ // Local action interceptor — executes file/shell actions WITHOUT needing LLM tool use
1077
+ // This ensures free tier users can still create, read, edit, delete files
1078
+ // ---------------------------------------------------------------------------
1079
+ function tryLocalAction(userMessage) {
1080
+ const msg = userMessage.trim();
1081
+ const lower = msg.toLowerCase();
1082
+ const home = os.homedir();
1083
+ const desktop = path.join(home, 'Desktop');
1084
+
1085
+ // Resolve common path references
1086
+ function resolvePath(p) {
1087
+ return p
1088
+ .replace(/^~\//, home + '/')
1089
+ .replace(/^~\\/, home + '\\')
1090
+ .replace(/\bmy desktop\b/i, desktop)
1091
+ .replace(/\bthe desktop\b/i, desktop)
1092
+ .replace(/\bdesktop\b/i, desktop);
1093
+ }
1094
+
1095
+ // ── Create folder/directory ──
1096
+ const mkdirMatch = lower.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named|on|at|in)?\s*[""']?(.+?)[""']?\s*(?:on|at|in)\s+(.+)/i)
1097
+ || lower.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:on|at|in)\s+(.+?)\s+(?:called|named)\s+[""']?(.+?)[""']?$/i)
1098
+ || lower.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?(.+?)[""']?\s*(?:on|at|in)\s+(.+)/i)
1099
+ || lower.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?(.+?)[""']?\s*$/i);
1100
+
1101
+ if (mkdirMatch) {
1102
+ let folderName, location;
1103
+ if (mkdirMatch.length === 3) {
1104
+ folderName = mkdirMatch[1].trim().replace(/[""']/g, '');
1105
+ location = mkdirMatch[2].trim().replace(/[""']/g, '');
1106
+ } else {
1107
+ folderName = mkdirMatch[1].trim().replace(/[""']/g, '');
1108
+ location = lower.includes('desktop') ? desktop : process.cwd();
1109
+ }
1110
+ const resolved = path.resolve(resolvePath(location), folderName);
1111
+ const result = localTools.shell.execute(process.platform === 'win32' ? `mkdir "${resolved}"` : `mkdir -p "${resolved}"`);
1112
+ if (result.includes('Error')) return null; // let LLM handle
1113
+ return `Created folder: ${resolved}`;
1114
+ }
1115
+
1116
+ // ── Create file ──
1117
+ const touchMatch = lower.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file|empty file)\s+(?:called|named)\s+[""']?(.+?)[""']?\s*(?:on|at|in)\s+(.+)/i)
1118
+ || lower.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file)\s+(?:called|named)\s+[""']?(.+?)[""']?\s*$/i);
1119
+
1120
+ if (touchMatch) {
1121
+ let fileName = touchMatch[1].trim().replace(/[""']/g, '');
1122
+ let location = touchMatch[2] ? touchMatch[2].trim().replace(/[""']/g, '') : process.cwd();
1123
+ const resolved = path.resolve(resolvePath(location), fileName);
1124
+ return localTools.writeFile.execute(resolved, '');
1125
+ }
1126
+
1127
+ // ── Read file ──
1128
+ const readMatch = lower.match(/(?:read|show|display|cat|open)\s+(?:the\s+)?(?:file\s+)?[""']?(.+\.\w+)[""']?/i);
1129
+ if (readMatch) {
1130
+ const filePath = resolvePath(readMatch[1].trim().replace(/[""']/g, ''));
1131
+ return localTools.readFile.execute(filePath);
1132
+ }
1133
+
1134
+ // ── Delete file/folder ──
1135
+ const deleteMatch = lower.match(/(?:delete|remove|rm)\s+(?:the\s+)?(?:file|folder|directory)?\s*[""']?(.+?)[""']?\s*$/i);
1136
+ if (deleteMatch && (lower.includes('file') || lower.includes('folder') || lower.includes('directory'))) {
1137
+ const filePath = path.resolve(resolvePath(deleteMatch[1].trim().replace(/[""']/g, '')));
1138
+ return localTools.deleteFile.execute(filePath);
1139
+ }
1140
+
1141
+ // ── List files ──
1142
+ const lsMatch = lower.match(/(?:list|show|ls|dir)\s+(?:the\s+)?(?:files|contents|items)\s+(?:in|on|at|of)\s+(.+)/i);
1143
+ if (lsMatch) {
1144
+ const dir = resolvePath(lsMatch[1].trim().replace(/[""']/g, ''));
1145
+ return localTools.listFiles.execute(dir);
1146
+ }
1147
+
1148
+ return null; // No match — let LLM handle
1149
+ }
1150
+
1030
1151
  async function grokChat(userMessage, conversationHistory = []) {
1152
+ // Try local action first — enables file ops on free tier
1153
+ const localResult = tryLocalAction(userMessage);
1154
+ if (localResult) {
1155
+ console.log(` ${localResult}`);
1156
+ // Send result to LLM for a natural confirmation
1157
+ const confirmMsg = `The user asked: "${userMessage}"\nI executed the action locally. Result: ${localResult}\nGive a brief, friendly confirmation and ask if they need anything else.`;
1158
+ try {
1159
+ const result = await callFreeTier([{ role: 'user', content: confirmMsg }], true);
1160
+ return result.content || localResult;
1161
+ } catch {
1162
+ return localResult;
1163
+ }
1164
+ }
1165
+
1031
1166
  const messages = [
1032
1167
  ...conversationHistory.slice(-20).map(m => ({
1033
1168
  role: m.role,
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('../ui');
6
+
7
+ module.exports = function(reg) {
8
+
9
+ // ── /read <path> ── Read a file
10
+ reg('read', 'Read a file from your machine', (args) => {
11
+ if (!args[0]) { console.log(ui.dim('Usage: /read <file-path>')); return; }
12
+ const filePath = path.resolve(args.join(' '));
13
+ try {
14
+ const content = fs.readFileSync(filePath, 'utf-8');
15
+ const lines = content.split('\n');
16
+ console.log(ui.header(`FILE: ${filePath}`));
17
+ console.log(ui.dim(`${lines.length} lines, ${Buffer.byteLength(content)} bytes`));
18
+ console.log('');
19
+ // Show with line numbers, cap at 200 lines
20
+ const show = lines.slice(0, 200);
21
+ show.forEach((line, i) => console.log(` ${String(i + 1).padStart(4)} ${line}`));
22
+ if (lines.length > 200) console.log(ui.dim(` ... ${lines.length - 200} more lines`));
23
+ } catch (e) {
24
+ console.log(ui.error(e.code === 'ENOENT' ? `File not found: ${filePath}` : e.message));
25
+ }
26
+ }, { category: 'FILES', aliases: ['cat'] });
27
+
28
+ // ── /write <path> <content> ── Write/create a file
29
+ reg('write', 'Write content to a file (creates dirs if needed)', (args) => {
30
+ if (args.length < 2) {
31
+ console.log(ui.dim('Usage: /write <file-path> <content>'));
32
+ console.log(ui.dim(' /write hello.txt Hello World'));
33
+ console.log(ui.dim(' /write src/config.json {"key":"value"}'));
34
+ return;
35
+ }
36
+ const filePath = path.resolve(args[0]);
37
+ const content = args.slice(1).join(' ');
38
+ try {
39
+ const dir = path.dirname(filePath);
40
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
41
+ fs.writeFileSync(filePath, content);
42
+ console.log(ui.success(`Written: ${filePath} (${Buffer.byteLength(content)} bytes)`));
43
+ } catch (e) {
44
+ console.log(ui.error(e.message));
45
+ }
46
+ }, { category: 'FILES' });
47
+
48
+ // ── /edit <path> <search> -> <replace> ── Find and replace in a file
49
+ reg('edit', 'Edit a file (find and replace)', (args) => {
50
+ const raw = args.join(' ');
51
+ const sepIdx = raw.indexOf(' -> ');
52
+ if (!args[0] || sepIdx === -1) {
53
+ console.log(ui.dim('Usage: /edit <file-path> <search-text> -> <replace-text>'));
54
+ console.log(ui.dim(' /edit config.json "port": 3000 -> "port": 8080'));
55
+ return;
56
+ }
57
+ // First arg is the file path, rest is search -> replace
58
+ const filePath = path.resolve(args[0]);
59
+ const afterPath = raw.slice(raw.indexOf(args[0]) + args[0].length + 1);
60
+ const arrowIdx = afterPath.indexOf(' -> ');
61
+ if (arrowIdx === -1) {
62
+ console.log(ui.dim('Use " -> " to separate search and replace text'));
63
+ return;
64
+ }
65
+ const search = afterPath.slice(0, arrowIdx);
66
+ const replace = afterPath.slice(arrowIdx + 4);
67
+
68
+ try {
69
+ const content = fs.readFileSync(filePath, 'utf-8');
70
+ if (!content.includes(search)) {
71
+ console.log(ui.error('Search text not found in file'));
72
+ return;
73
+ }
74
+ const updated = content.replace(search, replace);
75
+ fs.writeFileSync(filePath, updated);
76
+ console.log(ui.success(`Edited: ${filePath}`));
77
+ console.log(ui.dim(` "${search.slice(0, 40)}${search.length > 40 ? '...' : ''}" → "${replace.slice(0, 40)}${replace.length > 40 ? '...' : ''}"`));
78
+ } catch (e) {
79
+ console.log(ui.error(e.code === 'ENOENT' ? `File not found: ${filePath}` : e.message));
80
+ }
81
+ }, { category: 'FILES' });
82
+
83
+ // ── /delete <path> ── Delete a file
84
+ reg('delete', 'Delete a file or empty directory', (args) => {
85
+ if (!args[0]) { console.log(ui.dim('Usage: /delete <file-path>')); return; }
86
+ const filePath = path.resolve(args.join(' '));
87
+ try {
88
+ const stat = fs.statSync(filePath);
89
+ if (stat.isDirectory()) {
90
+ fs.rmdirSync(filePath);
91
+ console.log(ui.success(`Deleted directory: ${filePath}`));
92
+ } else {
93
+ fs.unlinkSync(filePath);
94
+ console.log(ui.success(`Deleted: ${filePath} (${stat.size} bytes)`));
95
+ }
96
+ } catch (e) {
97
+ if (e.code === 'ENOENT') console.log(ui.error(`Not found: ${filePath}`));
98
+ else if (e.code === 'ENOTEMPTY') console.log(ui.error('Directory not empty. Use /run rm -rf <path> for recursive delete.'));
99
+ else console.log(ui.error(e.message));
100
+ }
101
+ }, { category: 'FILES', aliases: ['rm'] });
102
+
103
+ // ── /ls [path] ── List files
104
+ reg('ls', 'List files and directories', (args) => {
105
+ const dir = path.resolve(args.join(' ') || '.');
106
+ try {
107
+ const items = fs.readdirSync(dir, { withFileTypes: true });
108
+ console.log(ui.header(`DIR: ${dir}`));
109
+ if (items.length === 0) { console.log(ui.dim(' (empty)')); return; }
110
+
111
+ const dirs = items.filter(i => i.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
112
+ const files = items.filter(i => !i.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
113
+
114
+ for (const d of dirs) console.log(` \x1b[34md\x1b[0m ${d.name}/`);
115
+ for (const f of files) {
116
+ try {
117
+ const stat = fs.statSync(path.join(dir, f.name));
118
+ const size = stat.size < 1024 ? `${stat.size}B` : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(1)}K` : `${(stat.size / 1048576).toFixed(1)}M`;
119
+ console.log(` f ${f.name.padEnd(40)} ${size}`);
120
+ } catch {
121
+ console.log(` f ${f.name}`);
122
+ }
123
+ }
124
+ console.log('');
125
+ console.log(ui.dim(` ${dirs.length} dirs, ${files.length} files`));
126
+ } catch (e) {
127
+ console.log(ui.error(e.code === 'ENOENT' ? `Not found: ${dir}` : e.message));
128
+ }
129
+ }, { category: 'FILES', aliases: ['dir'] });
130
+
131
+ // ── /mkdir <path> ── Create directory
132
+ reg('mkdir', 'Create a directory (including parents)', (args) => {
133
+ if (!args[0]) { console.log(ui.dim('Usage: /mkdir <dir-path>')); return; }
134
+ const dirPath = path.resolve(args.join(' '));
135
+ try {
136
+ fs.mkdirSync(dirPath, { recursive: true });
137
+ console.log(ui.success(`Created: ${dirPath}`));
138
+ } catch (e) {
139
+ console.log(ui.error(e.message));
140
+ }
141
+ }, { category: 'FILES' });
142
+
143
+ // ── /touch <path> ── Create empty file
144
+ reg('touch', 'Create an empty file', (args) => {
145
+ if (!args[0]) { console.log(ui.dim('Usage: /touch <file-path>')); return; }
146
+ const filePath = path.resolve(args.join(' '));
147
+ try {
148
+ const dir = path.dirname(filePath);
149
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
150
+ if (fs.existsSync(filePath)) {
151
+ // Update mtime like Unix touch
152
+ const now = new Date();
153
+ fs.utimesSync(filePath, now, now);
154
+ console.log(ui.success(`Touched: ${filePath}`));
155
+ } else {
156
+ fs.writeFileSync(filePath, '');
157
+ console.log(ui.success(`Created: ${filePath}`));
158
+ }
159
+ } catch (e) {
160
+ console.log(ui.error(e.message));
161
+ }
162
+ }, { category: 'FILES' });
163
+
164
+ };
@@ -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', 'audit', 'compute',
9
+ 'learn', 'sandbox', 'nvidia', 'edge', 'conversations', 'audit', 'compute', 'files',
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.5.0",
3
+ "version": "3.5.2",
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": {