navada-edge-cli 3.5.2 → 3.5.4

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
@@ -454,6 +454,7 @@ function streamFreeTier(endpoint, messages) {
454
454
 
455
455
  res.on('end', () => {
456
456
  if (fullContent) process.stdout.write('\n');
457
+ sessionState._lastStreamed = true;
457
458
  resolve({ content: fullContent, isRateLimit: false, streamed: true });
458
459
  });
459
460
  });
@@ -552,6 +553,7 @@ function streamAnthropic(key, messages, tools, system) {
552
553
 
553
554
  res.on('end', () => {
554
555
  if (contentBlocks.some(b => b.type === 'text')) process.stdout.write('\n');
556
+ sessionState._lastStreamed = true;
555
557
  resolve({ content: contentBlocks, stop_reason: stopReason });
556
558
  });
557
559
  });
@@ -637,6 +639,7 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
637
639
 
638
640
  res.on('end', () => {
639
641
  if (fullContent) process.stdout.write('\n');
642
+ sessionState._lastStreamed = true;
640
643
  toolCalls = toolCalls.filter(Boolean);
641
644
  resolve({ content: fullContent, tool_calls: toolCalls, finish_reason: finishReason });
642
645
  });
@@ -704,6 +707,7 @@ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
704
707
 
705
708
  res.on('end', () => {
706
709
  if (fullContent) process.stdout.write('\n');
710
+ sessionState._lastStreamed = true;
707
711
  resolve({ content: fullContent });
708
712
  });
709
713
  });
@@ -746,10 +750,7 @@ async function openAIChat(key, userMessage, conversationHistory = []) {
746
750
  response = await streamOpenAI(key, messages, model);
747
751
  } catch (e) {
748
752
  if (e.message.includes('401') || e.message.includes('429') || e.message.includes('billing')) {
749
- if (!sessionState._openaiWarned) {
750
- console.log(ui.warn('OpenAI API unavailable, using Grok free tier. /login with a valid key to switch.'));
751
- sessionState._openaiWarned = true;
752
- }
753
+ sessionState._openaiWarned = true;
753
754
  return grokChat(userMessage, conversationHistory);
754
755
  }
755
756
  throw e;
@@ -842,15 +843,12 @@ async function chat(userMessage, conversationHistory = []) {
842
843
  ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
843
844
  { role: 'user', content: userMessage },
844
845
  ];
845
- process.stdout.write(ui.dim(' NAVADA > '));
846
846
  try {
847
+ process.stdout.write(ui.dim(' '));
847
848
  const result = await streamGemini(effectiveGeminiKey, messages, geminiModel);
848
849
  return result.content;
849
850
  } catch (e) {
850
- if (!sessionState._geminiWarned) {
851
- console.log(ui.warn('Gemini API unavailable, using Grok free tier.'));
852
- sessionState._geminiWarned = true;
853
- }
851
+ sessionState._geminiWarned = true;
854
852
  return grokChat(userMessage, conversationHistory);
855
853
  }
856
854
  }
@@ -865,7 +863,7 @@ async function chat(userMessage, conversationHistory = []) {
865
863
  ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
866
864
  { role: 'user', content: userMessage },
867
865
  ];
868
- process.stdout.write(ui.dim(' NAVADA > '));
866
+ process.stdout.write(ui.dim(' '));
869
867
  const result = await streamNvidia(effectiveNvidiaKey, messages, nvidiaModel);
870
868
  return result.content;
871
869
  }
@@ -999,10 +997,7 @@ async function chat(userMessage, conversationHistory = []) {
999
997
  const errMsg = e.message || '';
1000
998
  // If billing/rate limit/auth error, fall back to free tier
1001
999
  if (errMsg.includes('400') || errMsg.includes('401') || errMsg.includes('429') || errMsg.includes('usage limits')) {
1002
- if (!sessionState._anthropicWarned) {
1003
- console.log(ui.warn('Anthropic API unavailable, using Grok free tier. /login with a valid key to switch.'));
1004
- sessionState._anthropicWarned = true;
1005
- }
1000
+ sessionState._anthropicWarned = true;
1006
1001
  return grokChat(userMessage, conversationHistory);
1007
1002
  }
1008
1003
  throw e;
@@ -1078,89 +1073,107 @@ async function executeTool(name, input) {
1078
1073
  // ---------------------------------------------------------------------------
1079
1074
  function tryLocalAction(userMessage) {
1080
1075
  const msg = userMessage.trim();
1081
- const lower = msg.toLowerCase();
1082
1076
  const home = os.homedir();
1083
1077
  const desktop = path.join(home, 'Desktop');
1084
1078
 
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);
1079
+ // Resolve a location phrase to an absolute path (use ORIGINAL case, not lowered)
1080
+ function resolveLocation(phrase) {
1081
+ const p = phrase.trim().replace(/[""'.,!]/g, '');
1082
+ const low = p.toLowerCase();
1083
+ if (low === 'my desktop' || low === 'the desktop' || low === 'desktop') return desktop;
1084
+ if (low === 'home' || low === 'my home' || low === 'home directory') return home;
1085
+ if (p.startsWith('~')) return p.replace(/^~[/\\]?/, home + path.sep);
1086
+ if (path.isAbsolute(p)) return p;
1087
+ return path.join(process.cwd(), p);
1093
1088
  }
1094
1089
 
1090
+ // Extract the ORIGINAL-CASE name from the original message using a case-insensitive match
1091
+ // We match on the original message to preserve casing
1092
+ let m;
1093
+
1095
1094
  // ── 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}`;
1095
+ // Pattern: "create a folder called NAME on my desktop"
1096
+ m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s+(?:on|at|in)\s+(.+?)$/i);
1097
+ if (m) {
1098
+ const name = m[1].trim();
1099
+ const loc = resolveLocation(m[2]);
1100
+ const resolved = path.join(loc, name);
1101
+ try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1102
+ catch (e) { return null; }
1114
1103
  }
1115
1104
 
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);
1105
+ // Pattern: "create a folder on my desktop called NAME"
1106
+ m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:on|at|in)\s+(.+?)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s*$/i);
1107
+ if (m) {
1108
+ const loc = resolveLocation(m[1]);
1109
+ const name = m[2].trim();
1110
+ const resolved = path.join(loc, name);
1111
+ try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1112
+ catch (e) { return null; }
1113
+ }
1114
+
1115
+ // Pattern: "create a folder called NAME" (no location — use cwd, or desktop if mentioned earlier)
1116
+ m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s*$/i);
1117
+ if (m) {
1118
+ const name = m[1].trim();
1119
+ const loc = msg.toLowerCase().includes('desktop') ? desktop : process.cwd();
1120
+ const resolved = path.join(loc, name);
1121
+ try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1122
+ catch (e) { return null; }
1123
+ }
1124
+
1125
+ // Pattern: "create a new folder NAME on my desktop" (no "called/named")
1126
+ m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+([A-Za-z0-9_\-. ]+?)\s+(?:on|at|in)\s+(.+?)$/i);
1127
+ if (m) {
1128
+ const name = m[1].trim();
1129
+ const loc = resolveLocation(m[2]);
1130
+ const resolved = path.join(loc, name);
1131
+ try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1132
+ catch (e) { return null; }
1133
+ }
1119
1134
 
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);
1135
+ // ── Create file ──
1136
+ m = msg.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s+(?:on|at|in)\s+(.+?)$/i);
1137
+ if (m) {
1138
+ const resolved = path.join(resolveLocation(m[2]), m[1].trim());
1124
1139
  return localTools.writeFile.execute(resolved, '');
1125
1140
  }
1141
+ m = msg.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s*$/i);
1142
+ if (m) {
1143
+ const loc = msg.toLowerCase().includes('desktop') ? desktop : process.cwd();
1144
+ return localTools.writeFile.execute(path.join(loc, m[1].trim()), '');
1145
+ }
1126
1146
 
1127
1147
  // ── 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, ''));
1148
+ m = msg.match(/(?:read|show|display|cat|open)\s+(?:the\s+)?(?:file\s+)?[""']?([^""']+\.\w{1,5})[""']?/i);
1149
+ if (m) {
1150
+ const p = m[1].trim();
1151
+ const filePath = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
1131
1152
  return localTools.readFile.execute(filePath);
1132
1153
  }
1133
1154
 
1134
1155
  // ── 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, '')));
1156
+ m = msg.match(/(?:delete|remove|rm)\s+(?:the\s+)?(?:file|folder|directory)\s+[""']?([^""']+?)[""']?\s*$/i);
1157
+ if (m) {
1158
+ const p = m[1].trim();
1159
+ const filePath = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
1138
1160
  return localTools.deleteFile.execute(filePath);
1139
1161
  }
1140
1162
 
1141
1163
  // ── 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);
1164
+ m = msg.match(/(?:list|show|ls|dir)\s+(?:the\s+)?(?:files|contents|items)\s+(?:in|on|at|of)\s+(.+)/i);
1165
+ if (m) {
1166
+ return localTools.listFiles.execute(resolveLocation(m[1]));
1146
1167
  }
1147
1168
 
1148
- return null; // No match — let LLM handle
1169
+ return null;
1149
1170
  }
1150
1171
 
1151
1172
  async function grokChat(userMessage, conversationHistory = []) {
1152
1173
  // Try local action first — enables file ops on free tier
1153
1174
  const localResult = tryLocalAction(userMessage);
1154
1175
  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
- }
1176
+ return `${localResult}\n\nWhat would you like to do next?`;
1164
1177
  }
1165
1178
 
1166
1179
  const messages = [
@@ -1235,7 +1248,6 @@ async function grokChat(userMessage, conversationHistory = []) {
1235
1248
 
1236
1249
  // Extract final text
1237
1250
  const content = response?.choices?.[0]?.message?.content || '';
1238
- if (content) console.log(` ${content}`);
1239
1251
  return content || 'No response.';
1240
1252
  }
1241
1253
 
@@ -13,11 +13,12 @@ module.exports = function(reg) {
13
13
 
14
14
  const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
15
15
 
16
- // Show a brief "thinking" indicator, then clear it when streaming starts
16
+ // Reset streaming flag before each chat
17
+ sessionState._lastStreamed = false;
18
+
19
+ // Show thinking indicator
17
20
  let spinner;
18
- if (!hasKey) {
19
- process.stdout.write(ui.dim(' NAVADA > '));
20
- } else {
21
+ if (hasKey) {
21
22
  const ora = require('ora');
22
23
  spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
23
24
  }
@@ -31,9 +32,9 @@ module.exports = function(reg) {
31
32
  addToHistory('user', msg);
32
33
  addToHistory('assistant', response);
33
34
 
34
- // Only print if not already streamed
35
- if (!response._streamed) {
36
- console.log(ui.header('NAVADA'));
35
+ // Only print if not already streamed to stdout
36
+ if (response && !sessionState._lastStreamed) {
37
+ console.log('');
37
38
  console.log(` ${response}`);
38
39
  }
39
40
 
@@ -42,7 +43,7 @@ module.exports = function(reg) {
42
43
  } catch (e) {
43
44
  if (spinner) spinner.stop();
44
45
  console.log(ui.error(e.message));
45
- console.log(ui.dim('Check: /config to see which providers are set, or /setup to configure.'));
46
+ console.log(ui.dim('/config to check providers, or /setup to configure.'));
46
47
  }
47
48
  }, { category: 'AI', aliases: ['ask'] });
48
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navada-edge-cli",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
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": {