navada-edge-cli 4.0.0 → 4.2.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
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { execSync, execFileSync } = require('child_process');
4
+ const crypto = require('crypto');
4
5
  const fs = require('fs');
5
6
  const path = require('path');
6
7
  const os = require('os');
@@ -9,24 +10,64 @@ const http = require('http');
9
10
  const navada = require('navada-edge-sdk');
10
11
  const ui = require('./ui');
11
12
  const config = require('./config');
13
+ const memory = require('./memory');
12
14
 
13
15
  // ---------------------------------------------------------------------------
14
- // NAVADA Edge Agent personality + tools + routing
16
+ // Request helpersauth, tracing, rate-limit headers
15
17
  // ---------------------------------------------------------------------------
18
+ function generateRequestId() { return `nv_${crypto.randomUUID()}`; }
19
+
20
+ function navadaAuthHeaders() {
21
+ const edgeKey = config.get('edgeKey') || '';
22
+ const headers = {
23
+ 'Content-Type': 'application/json',
24
+ 'X-Request-ID': generateRequestId(),
25
+ 'X-Client-Version': require('../package.json').version,
26
+ };
27
+ if (edgeKey) headers['Authorization'] = `Bearer ${edgeKey}`;
28
+ return headers;
29
+ }
16
30
 
17
- // Strip markdown formatting for clean terminal output
18
- function cleanOutput(text) {
19
- if (typeof text !== 'string') return text;
20
- return text
21
- .replace(/\*\*(.+?)\*\*/g, '$1') // **bold** → plain
22
- .replace(/\*(.+?)\*/g, '$1') // *italic* → plain
23
- .replace(/__(.+?)__/g, '$1') // __underline__ → plain
24
- .replace(/^#{1,6}\s+/gm, '') // ### headers plain
25
- .replace(/---+/g, '') // horizontal rules → remove
26
- .replace(/\s--\s/g, ' ') // spaced -- space
27
- .replace(/--/g, ', '); // remaining -- comma
31
+ // ---------------------------------------------------------------------------
32
+ // Edge Gateway — authenticated requests to NAVADA Azure compute
33
+ // ---------------------------------------------------------------------------
34
+ const EDGE_GATEWAY = 'https://edge-compute.navada-edge-server.uk';
35
+
36
+ async function navadaEdgeRequest(method, reqPath, body) {
37
+ const edgeKey = config.get('edgeKey');
38
+ if (!edgeKey) throw new Error('Not connected. /edge login <key>');
39
+
40
+ const url = new URL(EDGE_GATEWAY + reqPath);
41
+ const transport = url.protocol === 'https:' ? https : http;
42
+ const payload = body ? JSON.stringify(body) : '';
43
+
44
+ return new Promise((resolve, reject) => {
45
+ const headers = {
46
+ ...navadaAuthHeaders(),
47
+ 'Authorization': `Bearer ${edgeKey}`,
48
+ };
49
+ if (payload) headers['Content-Length'] = Buffer.byteLength(payload);
50
+
51
+ const req = transport.request(url, { method, headers, timeout: 60000 }, (res) => {
52
+ let data = '';
53
+ res.on('data', c => data += c);
54
+ res.on('end', () => {
55
+ rateTracker.updateFromServer(res.headers);
56
+ try { resolve({ status: res.statusCode, data: JSON.parse(data), headers: res.headers }); }
57
+ catch { resolve({ status: res.statusCode, data, headers: res.headers }); }
58
+ });
59
+ });
60
+ req.on('error', reject);
61
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
62
+ if (payload) req.write(payload);
63
+ req.end();
64
+ });
28
65
  }
29
66
 
67
+ // ---------------------------------------------------------------------------
68
+ // NAVADA Edge Agent — personality + tools + routing
69
+ // ---------------------------------------------------------------------------
70
+
30
71
  const IDENTITY = {
31
72
  name: 'NAVADA Edge',
32
73
  role: 'AI Infrastructure Agent',
@@ -34,22 +75,20 @@ const IDENTITY = {
34
75
  You are professional, technical, concise, and helpful. You speak with authority about distributed systems, Docker, AI, and cloud infrastructure.
35
76
  You have FULL ACCESS to the user's computer — you CAN and SHOULD use your tools to execute tasks:
36
77
  - shell: run ANY bash, PowerShell, or system command on the user's machine
37
- - read_file / write_file / edit_file / delete_file / list_files: full filesystem CRUD — create, read, edit, delete any file
78
+ - read_file / write_file / list_files: full filesystem access — create, read, modify any file
38
79
  - python_exec / python_pip / python_script: run Python code directly
39
80
  - sandbox_run: run code with syntax-highlighted output
40
81
  - system_info: check CPU, RAM, disk, OS
41
- You also connect to the NAVADA Edge Network (4 nodes via Tailscale VPN):
42
- - lucas_exec / lucas_ssh / lucas_docker: execute commands on remote nodes (EC2, HP, Oracle)
43
- - mcp_call: access 18 MCP tools on the ASUS server
44
- - docker_registry: manage the private Docker registry
45
- - send_email / generate_image: communications and AI image generation
46
- - founder_info: ALWAYS use this tool when asked about Lee Akpareva, his career, education, projects, or NAVADA's founder. It has his full CV. Never guess about Lee, always call this tool.
82
+ You also connect to the NAVADA Edge Network cloud:
83
+ - automation_request: submit automation requests (emails, marketing, builds, schedules)
84
+ - web_search: search the web for information
85
+ - save_memory / recall_memory: persistent memory across sessions
86
+ - screenshot / describe_image: visual perception tools
87
+ - founder_info: information about Lee Akpareva, the creator of NAVADA
47
88
  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.
48
89
  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.
49
90
  When asked to create, edit, or delete files — use the file tools directly. You are a terminal agent with FULL access.
50
- PLATFORM: This machine runs ` + (process.platform === 'win32' ? `Windows. Use Windows paths. Desktop = ${fs.existsSync(path.join(os.homedir(), 'OneDrive', 'Desktop')) ? path.join(os.homedir(), 'OneDrive', 'Desktop') : path.join(os.homedir(), 'Desktop')}. Home = ${os.homedir()}.` : `${process.platform}. Home = ${os.homedir()}.`) + `
51
- Keep responses short. Code blocks when needed. No fluff.
52
- FORMATTING: Never use markdown formatting like **bold**, *italic*, ### headers, or -- dashes. Write plain text only. This is a terminal, not a web page.`,
91
+ Keep responses short. Code blocks when needed. No fluff.`,
53
92
  founder: {
54
93
  name: 'Leslie (Lee) Akpareva',
55
94
  title: 'Principal AI Consultant & Founder, NAVADA Edge Network',
@@ -91,15 +130,6 @@ function getSystemPrompt() {
91
130
  } catch {}
92
131
  }
93
132
 
94
- // Load user's agent.md customisation if it exists
95
- const agentMdPath = path.join(config.CONFIG_DIR, 'agent.md');
96
- let userPrompt = '';
97
- try {
98
- if (fs.existsSync(agentMdPath)) {
99
- userPrompt = fs.readFileSync(agentMdPath, 'utf-8').trim();
100
- }
101
- } catch {}
102
-
103
133
  // Load active sub-agent if selected
104
134
  if (sessionState.subAgent) {
105
135
  const subPath = path.join(config.CONFIG_DIR, 'agents', `${sessionState.subAgent}.md`);
@@ -110,11 +140,51 @@ function getSystemPrompt() {
110
140
  } catch {}
111
141
  }
112
142
 
113
- // Combine: base personality + user customisation
114
- if (userPrompt) {
115
- return `${IDENTITY.personality}\n\n--- USER CUSTOMISATION (from agent.md) ---\n${userPrompt}`;
143
+ let prompt = IDENTITY.personality;
144
+
145
+ // Load soul.md user identity and preferences
146
+ const soulPath = path.join(config.CONFIG_DIR, 'soul.md');
147
+ try {
148
+ if (fs.existsSync(soulPath)) {
149
+ const soul = fs.readFileSync(soulPath, 'utf-8').trim();
150
+ if (soul) prompt += `\n\n--- USER IDENTITY (from soul.md) ---\n${soul}`;
151
+ }
152
+ } catch {}
153
+
154
+ // Load guardrail.md — safety boundaries
155
+ const guardrailPath = path.join(config.CONFIG_DIR, 'guardrail.md');
156
+ try {
157
+ if (fs.existsSync(guardrailPath)) {
158
+ const guardrail = fs.readFileSync(guardrailPath, 'utf-8').trim();
159
+ if (guardrail) prompt += `\n\n--- GUARDRAILS (from guardrail.md) ---\n${guardrail}`;
160
+ }
161
+ } catch {}
162
+
163
+ // Load agent.md — legacy customisation (backwards compat)
164
+ const agentMdPath = path.join(config.CONFIG_DIR, 'agent.md');
165
+ try {
166
+ if (fs.existsSync(agentMdPath)) {
167
+ const userPrompt = fs.readFileSync(agentMdPath, 'utf-8').trim();
168
+ if (userPrompt) prompt += `\n\n--- AGENT CUSTOMISATION (from agent.md) ---\n${userPrompt}`;
169
+ }
170
+ } catch {}
171
+
172
+ // Inject memory context (Tier 2 episodes + Tier 3 knowledge)
173
+ const memoryContext = memory.manager.loadSessionContext();
174
+ if (memoryContext) {
175
+ prompt += `\n\n--- MEMORY (auto-loaded) ---\n${memoryContext}`;
116
176
  }
117
- return IDENTITY.personality;
177
+
178
+ // Inject user skills (so agent knows what skills are available)
179
+ try {
180
+ const skills = require('./skills');
181
+ const skillsPrompt = skills.getSkillsPrompt();
182
+ if (skillsPrompt) {
183
+ prompt += `\n\n--- USER SKILLS ---\n${skillsPrompt}`;
184
+ }
185
+ } catch {}
186
+
187
+ return prompt;
118
188
  }
119
189
 
120
190
  function listSubAgents() {
@@ -135,27 +205,33 @@ const sessionState = {
135
205
  cost: 0,
136
206
  messages: 0,
137
207
  startTime: Date.now(),
138
- learningMode: null, // 'python' | 'csharp' | 'node' | null
139
- subAgent: null, // active sub-agent name (loads from ~/.navada/agents/<name>.md)
140
- history: [], // conversation history for context continuity
208
+ learningMode: null,
209
+ subAgent: null,
210
+ get history() { return memory.working.recentMessages; },
141
211
  };
142
212
 
143
- // Conversation history management
213
+ // Conversation history — powered by 3-tier memory system
144
214
  function addToHistory(role, content) {
145
- sessionState.history.push({ role, content });
146
- // Keep last 40 turns to avoid token overflow
147
- if (sessionState.history.length > 40) {
148
- sessionState.history = sessionState.history.slice(-40);
149
- }
215
+ memory.working.add(role, content);
150
216
  sessionState.messages++;
217
+
218
+ // Auto-extract knowledge from user messages (Tier 3)
219
+ if (role === 'user') {
220
+ const lastAssistant = memory.working.recentMessages
221
+ .filter(m => m.role === 'assistant')
222
+ .pop();
223
+ memory.manager.autoExtract(content, lastAssistant?.content || '');
224
+ }
151
225
  }
152
226
 
153
227
  function getConversationHistory() {
154
- return sessionState.history;
228
+ return memory.working.getContextMessages();
155
229
  }
156
230
 
157
231
  function clearHistory() {
158
- sessionState.history = [];
232
+ // Save episode before clearing (Tier 2)
233
+ memory.manager.saveSessionEpisode();
234
+ memory.working.clear();
159
235
  sessionState.messages = 0;
160
236
  sessionState.tokens = { input: 0, output: 0, total: 0 };
161
237
  sessionState.cost = 0;
@@ -182,6 +258,7 @@ const rateTracker = {
182
258
 
183
259
  remaining() {
184
260
  this.cleanup();
261
+ if (this.serverRemaining !== null && this.serverRemaining !== undefined) return this.serverRemaining;
185
262
  return Math.max(0, this.limit - this.requests.length);
186
263
  },
187
264
 
@@ -189,6 +266,18 @@ const rateTracker = {
189
266
  this.cleanup();
190
267
  return this.requests.length;
191
268
  },
269
+
270
+ updateFromServer(headers) {
271
+ const limit = parseInt(headers['x-ratelimit-limit']);
272
+ const remaining = parseInt(headers['x-ratelimit-remaining']);
273
+ const reset = headers['x-ratelimit-reset'];
274
+ if (!isNaN(limit)) this.limit = limit;
275
+ this.serverRemaining = isNaN(remaining) ? null : remaining;
276
+ this.serverReset = reset || null;
277
+ },
278
+
279
+ serverRemaining: null,
280
+ serverReset: null,
192
281
  };
193
282
 
194
283
  // ---------------------------------------------------------------------------
@@ -239,36 +328,6 @@ const localTools = {
239
328
  },
240
329
  },
241
330
 
242
- editFile: {
243
- description: 'Edit a file by replacing a search string with new content',
244
- execute: (filePath, search, replace) => {
245
- try {
246
- const resolved = path.resolve(filePath);
247
- const content = fs.readFileSync(resolved, 'utf-8');
248
- if (!content.includes(search)) return `Error: search string not found in ${resolved}`;
249
- const updated = content.replace(search, replace);
250
- fs.writeFileSync(resolved, updated);
251
- return `Edited: ${resolved} (replaced ${search.length} chars)`;
252
- } catch (e) { return `Error: ${e.message}`; }
253
- },
254
- },
255
-
256
- deleteFile: {
257
- description: 'Delete a file or empty directory from this machine',
258
- execute: (filePath) => {
259
- try {
260
- const resolved = path.resolve(filePath);
261
- const stat = fs.statSync(resolved);
262
- if (stat.isDirectory()) {
263
- fs.rmdirSync(resolved);
264
- } else {
265
- fs.unlinkSync(resolved);
266
- }
267
- return `Deleted: ${resolved}`;
268
- } catch (e) { return `Error: ${e.message}`; }
269
- },
270
- },
271
-
272
331
  systemInfo: {
273
332
  description: 'Get system information',
274
333
  execute: () => {
@@ -342,21 +401,8 @@ const localTools = {
342
401
  },
343
402
 
344
403
  founderInfo: {
345
- description: 'Answer questions about Lee Akpareva, founder of NAVADA Edge, using his full CV and career history',
346
- execute: (question) => {
347
- try {
348
- const knowledgePath = path.join(__dirname, 'knowledge.py');
349
- const py = process.platform === 'win32' ? 'python' : 'python3';
350
- const openaiKey = config.get('openaiKey') || process.env.OPENAI_API_KEY || '';
351
- const output = execFileSync(py, [knowledgePath, question || 'Who is Lee Akpareva?'], {
352
- timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
353
- env: { ...process.env, OPENAI_API_KEY: openaiKey },
354
- });
355
- return output.trim();
356
- } catch (e) {
357
- return `Error: ${e.stderr?.trim() || e.message}`;
358
- }
359
- },
404
+ description: 'Get information about the NAVADA Edge founder',
405
+ execute: () => JSON.stringify(IDENTITY.founder, null, 2),
360
406
  },
361
407
  };
362
408
 
@@ -377,6 +423,7 @@ async function callFreeTier(messages, stream = false) {
377
423
  const r = await navada.request(endpoint, {
378
424
  method: 'POST',
379
425
  body: { messages },
426
+ headers: navadaAuthHeaders(),
380
427
  timeout: endpoint.includes('navada-edge-server.uk') ? 30000 : 5000,
381
428
  });
382
429
 
@@ -426,11 +473,13 @@ function streamFreeTier(endpoint, messages) {
426
473
  const transport = url.protocol === 'https:' ? https : http;
427
474
  const body = JSON.stringify({ messages, stream: true });
428
475
 
476
+ const authHeaders = navadaAuthHeaders();
429
477
  const req = transport.request(url, {
430
478
  method: 'POST',
431
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
479
+ headers: { ...authHeaders, 'Content-Length': Buffer.byteLength(body) },
432
480
  timeout: endpoint.includes('navada-edge-server.uk') ? 120000 : 10000,
433
481
  }, (res) => {
482
+ rateTracker.updateFromServer(res.headers);
434
483
  // If server doesn't support streaming, collect full response
435
484
  if (!res.headers['content-type']?.includes('text/event-stream')) {
436
485
  let data = '';
@@ -470,7 +519,7 @@ function streamFreeTier(endpoint, messages) {
470
519
  const delta = parsed.choices?.[0]?.delta;
471
520
  // Grok-3-mini streams reasoning_content first, then content — skip reasoning
472
521
  if (delta?.reasoning_content && !delta?.content) continue;
473
- const text = cleanOutput(delta?.content || '');
522
+ const text = delta?.content || '';
474
523
  if (text) {
475
524
  process.stdout.write(text);
476
525
  fullContent += text;
@@ -481,7 +530,6 @@ function streamFreeTier(endpoint, messages) {
481
530
 
482
531
  res.on('end', () => {
483
532
  if (fullContent) process.stdout.write('\n');
484
- sessionState._lastStreamed = true;
485
533
  resolve({ content: fullContent, isRateLimit: false, streamed: true });
486
534
  });
487
535
  });
@@ -550,9 +598,8 @@ function streamAnthropic(key, messages, tools, system) {
550
598
 
551
599
  case 'content_block_delta':
552
600
  if (event.delta?.type === 'text_delta') {
553
- const clean = cleanOutput(event.delta.text);
554
- process.stdout.write(clean);
555
- currentText += clean;
601
+ process.stdout.write(event.delta.text);
602
+ currentText += event.delta.text;
556
603
  } else if (event.delta?.type === 'input_json_delta') {
557
604
  const last = contentBlocks[contentBlocks.length - 1];
558
605
  if (last?.type === 'tool_use') last.input += event.delta.partial_json;
@@ -581,7 +628,6 @@ function streamAnthropic(key, messages, tools, system) {
581
628
 
582
629
  res.on('end', () => {
583
630
  if (contentBlocks.some(b => b.type === 'text')) process.stdout.write('\n');
584
- sessionState._lastStreamed = true;
585
631
  resolve({ content: contentBlocks, stop_reason: stopReason });
586
632
  });
587
633
  });
@@ -644,9 +690,8 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
644
690
  if (finish) finishReason = finish;
645
691
 
646
692
  if (delta?.content) {
647
- const clean = cleanOutput(delta.content);
648
- process.stdout.write(clean);
649
- fullContent += clean;
693
+ process.stdout.write(delta.content);
694
+ fullContent += delta.content;
650
695
  }
651
696
 
652
697
  // Accumulate tool calls
@@ -668,7 +713,6 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
668
713
 
669
714
  res.on('end', () => {
670
715
  if (fullContent) process.stdout.write('\n');
671
- sessionState._lastStreamed = true;
672
716
  toolCalls = toolCalls.filter(Boolean);
673
717
  resolve({ content: fullContent, tool_calls: toolCalls, finish_reason: finishReason });
674
718
  });
@@ -684,7 +728,7 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
684
728
  // ---------------------------------------------------------------------------
685
729
  // Streaming — Google Gemini API (gemini-2.0-flash)
686
730
  // ---------------------------------------------------------------------------
687
- function streamGemini(key, messages, model = 'gemini-2.0-flash') {
731
+ function streamGemini(key, messages, model = 'gemini-2.0-flash', systemPrompt = null) {
688
732
  return new Promise((resolve, reject) => {
689
733
  const contents = messages.map(m => ({
690
734
  role: m.role === 'assistant' ? 'model' : 'user',
@@ -694,7 +738,7 @@ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
694
738
  const body = JSON.stringify({
695
739
  contents,
696
740
  generationConfig: { maxOutputTokens: 4096 },
697
- systemInstruction: { parts: [{ text: getSystemPrompt() }] },
741
+ systemInstruction: { parts: [{ text: systemPrompt || getSystemPrompt() }] },
698
742
  });
699
743
 
700
744
  const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${key}`);
@@ -725,7 +769,7 @@ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
725
769
  if (!data) continue;
726
770
  try {
727
771
  const parsed = JSON.parse(data);
728
- const text = cleanOutput(parsed.candidates?.[0]?.content?.parts?.[0]?.text || '');
772
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text || '';
729
773
  if (text) {
730
774
  process.stdout.write(text);
731
775
  fullContent += text;
@@ -736,7 +780,6 @@ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
736
780
 
737
781
  res.on('end', () => {
738
782
  if (fullContent) process.stdout.write('\n');
739
- sessionState._lastStreamed = true;
740
783
  resolve({ content: fullContent });
741
784
  });
742
785
  });
@@ -754,14 +797,18 @@ function openAITools() {
754
797
  { 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'] } },
755
798
  { 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'] } },
756
799
  { name: 'list_files', description: 'List files and directories.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } } },
757
- { 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'] } },
758
- { name: 'delete_file', description: 'Delete a file or empty directory.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } },
759
800
  { name: 'system_info', description: 'Get local system information (CPU, RAM, disk, OS, hostname).', parameters: { type: 'object', properties: {} } },
760
801
  { 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'] } },
761
802
  { name: 'python_pip', description: 'Install a Python package via pip.', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } },
762
803
  { name: 'python_script', description: 'Run a Python script file.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to .py file' } }, required: ['path'] } },
763
804
  { 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'] } },
764
- { name: 'founder_info', description: 'Answer questions about Lee Akpareva (founder of NAVADA Edge) using his full CV and career history. Always use this tool when asked about Lee, his career, experience, education, or projects.', parameters: { type: 'object', properties: { question: { type: 'string', description: 'The question about Lee' } }, required: ['question'] } },
805
+ { name: 'automation_request', description: 'Submit automation request for review. Types: email, marketing, build, data, schedule.', parameters: { type: 'object', properties: { title: { type: 'string' }, description: { type: 'string' }, type: { type: 'string' }, schedule: { type: 'string' } }, required: ['title', 'description'] } },
806
+ { name: 'web_search', description: 'Search the web.', parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
807
+ { name: 'save_memory', description: 'Save to persistent memory.', parameters: { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' } }, required: ['key', 'value'] } },
808
+ { name: 'recall_memory', description: 'Recall saved memories.', parameters: { type: 'object', properties: { key: { type: 'string' } } } },
809
+ { name: 'screenshot', description: 'Take a screenshot.', parameters: { type: 'object', properties: { output: { type: 'string' } } } },
810
+ { name: 'describe_image', description: 'Analyze an image with AI vision.', parameters: { type: 'object', properties: { path: { type: 'string' }, question: { type: 'string' } }, required: ['path'] } },
811
+ { name: 'founder_info', description: 'Get information about Lee Akpareva, founder of NAVADA Edge.', parameters: { type: 'object', properties: {} } },
765
812
  ];
766
813
  return defs.map(d => ({ type: 'function', function: d }));
767
814
  }
@@ -779,7 +826,10 @@ async function openAIChat(key, userMessage, conversationHistory = []) {
779
826
  response = await streamOpenAI(key, messages, model);
780
827
  } catch (e) {
781
828
  if (e.message.includes('401') || e.message.includes('429') || e.message.includes('billing')) {
782
- sessionState._openaiWarned = true;
829
+ if (!sessionState._openaiWarned) {
830
+ console.log(ui.warn('OpenAI API unavailable, using Grok free tier. /login with a valid key to switch.'));
831
+ sessionState._openaiWarned = true;
832
+ }
783
833
  return grokChat(userMessage, conversationHistory);
784
834
  }
785
835
  throw e;
@@ -827,15 +877,65 @@ function detectIntent(message) {
827
877
  }
828
878
 
829
879
  // ---------------------------------------------------------------------------
830
- // Anthropic Claude API conversational agent with tool use
880
+ // Prompt-based tool calling for providers without native function calling
831
881
  // ---------------------------------------------------------------------------
832
- async function chat(userMessage, conversationHistory = []) {
833
- // Local action interceptor — file/folder ops work on ALL tiers without LLM
834
- const localResult = tryLocalAction(userMessage);
835
- if (localResult) {
836
- return `${localResult}\n\nWhat would you like to do next?`;
882
+ const TOOL_PROMPT_SUFFIX = `
883
+
884
+ You have access to these tools. To use a tool, respond with a JSON block:
885
+ \`\`\`tool
886
+ {"name": "tool_name", "input": {"param": "value"}}
887
+ \`\`\`
888
+
889
+ Available tools:
890
+ - shell: Execute shell command. Input: {"command": "string"}
891
+ - read_file: Read file. Input: {"path": "string"}
892
+ - write_file: Write file. Input: {"path": "string", "content": "string"}
893
+ - list_files: List directory. Input: {"path": "string"}
894
+ - system_info: System info. Input: {}
895
+ - python_exec: Run Python. Input: {"code": "string"}
896
+ - python_pip: Install pip package. Input: {"package": "string"}
897
+ - python_script: Run .py file. Input: {"path": "string"}
898
+ - sandbox_run: Run code in sandbox. Input: {"code": "string", "language": "javascript|python|typescript"}
899
+ - automation_request: Submit automation request. Input: {"title": "string", "description": "string", "type": "email|marketing|build|data|schedule|custom", "schedule": "daily|weekly|cron|on-demand"}
900
+ - web_search: Web search. Input: {"query": "string"}
901
+ - save_memory: Save memory. Input: {"key": "string", "value": "string"}
902
+ - recall_memory: Recall memory. Input: {"key": "string"} (key optional)
903
+ - screenshot: Screenshot. Input: {"output": "filepath"}
904
+ - describe_image: Analyze image. Input: {"path": "string", "question": "string"}
905
+ - founder_info: NAVADA founder info. Input: {}
906
+
907
+ After receiving a tool result, continue your response. Use multiple tools in sequence if needed.
908
+ If no tool needed, respond normally without the tool block.`;
909
+
910
+ function getToolEnhancedSystemPrompt() {
911
+ return getSystemPrompt() + TOOL_PROMPT_SUFFIX;
912
+ }
913
+
914
+ async function parseAndExecuteTools(content) {
915
+ const toolPattern = /```tool\s*\n?([\s\S]*?)\n?```/g;
916
+ let match;
917
+ let hasTools = false;
918
+ const toolResults = [];
919
+
920
+ while ((match = toolPattern.exec(content)) !== null) {
921
+ hasTools = true;
922
+ try {
923
+ const toolCall = JSON.parse(match[1].trim());
924
+ console.log(ui.dim(` [${toolCall.name}] ${JSON.stringify(toolCall.input || {}).slice(0, 80)}`));
925
+ const result = await executeTool(toolCall.name, toolCall.input || {});
926
+ toolResults.push({ name: toolCall.name, result: typeof result === 'string' ? result : JSON.stringify(result) });
927
+ } catch (e) {
928
+ toolResults.push({ name: 'error', result: `Tool parse error: ${e.message}` });
929
+ }
837
930
  }
838
931
 
932
+ return { hasTools, toolResults };
933
+ }
934
+
935
+ // ---------------------------------------------------------------------------
936
+ // Anthropic Claude API — conversational agent with tool use
937
+ // ---------------------------------------------------------------------------
938
+ async function chat(userMessage, conversationHistory = []) {
839
939
  const anthropicKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY || '';
840
940
  const openaiKey = config.get('openaiKey') || process.env.OPENAI_API_KEY || '';
841
941
  const nvidiaKey = config.get('nvidiaKey') || process.env.NVIDIA_API_KEY || '';
@@ -878,12 +978,29 @@ async function chat(userMessage, conversationHistory = []) {
878
978
  ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
879
979
  { role: 'user', content: userMessage },
880
980
  ];
981
+ process.stdout.write(ui.dim(' NAVADA > '));
881
982
  try {
882
- process.stdout.write(ui.dim(' '));
883
- const result = await streamGemini(effectiveGeminiKey, messages, geminiModel);
983
+ let result = await streamGemini(effectiveGeminiKey, messages, geminiModel, getToolEnhancedSystemPrompt());
984
+
985
+ // Prompt-based tool calling loop
986
+ let iterations = 0;
987
+ while (iterations < 5) {
988
+ const { hasTools, toolResults } = await parseAndExecuteTools(result.content);
989
+ if (!hasTools) break;
990
+ iterations++;
991
+ const toolResultText = toolResults.map(t => `Tool "${t.name}" returned:\n${t.result}`).join('\n\n');
992
+ messages.push({ role: 'assistant', content: result.content });
993
+ messages.push({ role: 'user', content: `Tool results:\n${toolResultText}\n\nContinue your response.` });
994
+ process.stdout.write(ui.dim(' NAVADA > '));
995
+ result = await streamGemini(effectiveGeminiKey, messages, geminiModel, getToolEnhancedSystemPrompt());
996
+ }
997
+
884
998
  return result.content;
885
999
  } catch (e) {
886
- sessionState._geminiWarned = true;
1000
+ if (!sessionState._geminiWarned) {
1001
+ console.log(ui.warn('Gemini API unavailable, using Grok free tier.'));
1002
+ sessionState._geminiWarned = true;
1003
+ }
887
1004
  return grokChat(userMessage, conversationHistory);
888
1005
  }
889
1006
  }
@@ -898,8 +1015,22 @@ async function chat(userMessage, conversationHistory = []) {
898
1015
  ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
899
1016
  { role: 'user', content: userMessage },
900
1017
  ];
901
- process.stdout.write(ui.dim(' '));
902
- const result = await streamNvidia(effectiveNvidiaKey, messages, nvidiaModel);
1018
+ process.stdout.write(ui.dim(' NAVADA > '));
1019
+ let result = await streamNvidia(effectiveNvidiaKey, messages, nvidiaModel, getToolEnhancedSystemPrompt());
1020
+
1021
+ // Prompt-based tool calling loop
1022
+ let iterations = 0;
1023
+ while (iterations < 5) {
1024
+ const { hasTools, toolResults } = await parseAndExecuteTools(result.content);
1025
+ if (!hasTools) break;
1026
+ iterations++;
1027
+ const toolResultText = toolResults.map(t => `Tool "${t.name}" returned:\n${t.result}`).join('\n\n');
1028
+ messages.push({ role: 'assistant', content: result.content });
1029
+ messages.push({ role: 'user', content: `Tool results:\n${toolResultText}\n\nContinue your response based on these results.` });
1030
+ process.stdout.write(ui.dim(' NAVADA > '));
1031
+ result = await streamNvidia(effectiveNvidiaKey, messages, nvidiaModel, getToolEnhancedSystemPrompt());
1032
+ }
1033
+
903
1034
  return result.content;
904
1035
  }
905
1036
 
@@ -937,60 +1068,48 @@ async function chat(userMessage, conversationHistory = []) {
937
1068
  description: 'List files and directories.',
938
1069
  input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } },
939
1070
  },
940
- {
941
- name: 'edit_file',
942
- description: 'Edit a file by finding and replacing text. Use for targeted edits without rewriting the whole file.',
943
- 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'] },
944
- },
945
- {
946
- name: 'delete_file',
947
- description: 'Delete a file or empty directory from the user\'s machine.',
948
- input_schema: { type: 'object', properties: { path: { type: 'string', description: 'File or directory path to delete' } }, required: ['path'] },
949
- },
950
1071
  {
951
1072
  name: 'system_info',
952
1073
  description: 'Get local system information (CPU, RAM, disk, OS, hostname).',
953
1074
  input_schema: { type: 'object', properties: {} },
954
1075
  },
1076
+ // ── Automation Pipeline ──
955
1077
  {
956
- name: 'network_status',
957
- description: 'Ping all NAVADA Edge Network nodes and cloud services.',
958
- input_schema: { type: 'object', properties: {} },
959
- },
960
- {
961
- name: 'lucas_exec',
962
- description: 'Run a bash command on EC2 via Lucas CTO agent.',
963
- input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
964
- },
965
- {
966
- name: 'lucas_ssh',
967
- description: 'SSH to a NAVADA Edge node (hp, ec2, oracle) and run a command via Lucas CTO.',
968
- input_schema: { type: 'object', properties: { node: { type: 'string' }, command: { type: 'string' } }, required: ['node', 'command'] },
1078
+ name: 'automation_request',
1079
+ description: 'Submit an automation request to the NAVADA Edge team. Requests are queued for review and setup. Use for: scheduled emails, marketing campaigns, recurring tasks, app builds, data pipelines.',
1080
+ input_schema: { type: 'object', properties: {
1081
+ title: { type: 'string', description: 'Short title for the automation' },
1082
+ description: { type: 'string', description: 'Detailed description of what to automate' },
1083
+ type: { type: 'string', description: 'Type: email, marketing, build, data, schedule, custom' },
1084
+ schedule: { type: 'string', description: 'When/how often: daily, weekly, cron expression, one-time' },
1085
+ }, required: ['title', 'description'] },
969
1086
  },
970
1087
  {
971
- name: 'lucas_docker',
972
- description: 'Run a command inside a Docker container on EC2 via Lucas CTO.',
973
- input_schema: { type: 'object', properties: { container: { type: 'string' }, command: { type: 'string' } }, required: ['container', 'command'] },
1088
+ name: 'web_search',
1089
+ description: 'Search the web for information.',
1090
+ input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] },
974
1091
  },
1092
+ // ── Memory Tools ──
975
1093
  {
976
- name: 'mcp_call',
977
- description: 'Call a tool on the NAVADA Edge MCP server (18 tools: docker, ssh, files, database, monitoring).',
978
- input_schema: { type: 'object', properties: { tool: { type: 'string' }, args: { type: 'object' } }, required: ['tool'] },
1094
+ name: 'save_memory',
1095
+ description: 'Save information to persistent memory for future sessions. Use for important context, preferences, or facts the user wants remembered.',
1096
+ input_schema: { type: 'object', properties: { key: { type: 'string', description: 'Memory key (e.g. "preferred_language", "project_name")' }, value: { type: 'string', description: 'The information to remember' } }, required: ['key', 'value'] },
979
1097
  },
980
1098
  {
981
- name: 'docker_registry',
982
- description: 'List images or tags in the NAVADA private Docker registry.',
983
- input_schema: { type: 'object', properties: { image: { type: 'string', description: 'Image name for tags (optional — omit to list all)' } } },
1099
+ name: 'recall_memory',
1100
+ description: 'Recall previously saved memories. Use when user references past conversations or saved context.',
1101
+ input_schema: { type: 'object', properties: { key: { type: 'string', description: 'Specific key to recall (optional — omit to list all)' } } },
984
1102
  },
1103
+ // ── Perception Tools ──
985
1104
  {
986
- name: 'send_email',
987
- description: 'Send an email via the NAVADA Edge MCP email tool.',
988
- input_schema: { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' } }, required: ['to', 'subject', 'body'] },
1105
+ name: 'screenshot',
1106
+ description: 'Take a screenshot of the current screen and save it.',
1107
+ input_schema: { type: 'object', properties: { output: { type: 'string', description: 'Output file path (default: screenshot.png)' } } },
989
1108
  },
990
1109
  {
991
- name: 'generate_image',
992
- description: 'Generate an image using Cloudflare Flux (FREE) or DALL-E.',
993
- input_schema: { type: 'object', properties: { prompt: { type: 'string' }, provider: { type: 'string', description: 'flux (default, free) or dalle' } }, required: ['prompt'] },
1110
+ name: 'describe_image',
1111
+ description: 'Describe or analyze an image file using AI vision.',
1112
+ input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Path to image file' }, question: { type: 'string', description: 'What to analyze about the image' } }, required: ['path'] },
994
1113
  },
995
1114
  {
996
1115
  name: 'python_exec',
@@ -1014,8 +1133,8 @@ async function chat(userMessage, conversationHistory = []) {
1014
1133
  },
1015
1134
  {
1016
1135
  name: 'founder_info',
1017
- description: 'Answer questions about Lee Akpareva (founder of NAVADA Edge) using his full CV and career history. Always use this tool when asked about Lee, his career, experience, education, certifications, or projects.',
1018
- input_schema: { type: 'object', properties: { question: { type: 'string', description: 'The question about Lee' } }, required: ['question'] },
1136
+ description: 'Get information about Lee Akpareva, founder of NAVADA Edge Network. Use when asked about the creator, founder, Lee, or who made NAVADA.',
1137
+ input_schema: { type: 'object', properties: {} },
1019
1138
  },
1020
1139
  ];
1021
1140
 
@@ -1032,7 +1151,10 @@ async function chat(userMessage, conversationHistory = []) {
1032
1151
  const errMsg = e.message || '';
1033
1152
  // If billing/rate limit/auth error, fall back to free tier
1034
1153
  if (errMsg.includes('400') || errMsg.includes('401') || errMsg.includes('429') || errMsg.includes('usage limits')) {
1035
- sessionState._anthropicWarned = true;
1154
+ if (!sessionState._anthropicWarned) {
1155
+ console.log(ui.warn('Anthropic API unavailable, using Grok free tier. /login with a valid key to switch.'));
1156
+ sessionState._anthropicWarned = true;
1157
+ }
1036
1158
  return grokChat(userMessage, conversationHistory);
1037
1159
  }
1038
1160
  throw e;
@@ -1068,22 +1190,100 @@ async function executeTool(name, input) {
1068
1190
  case 'read_file': return localTools.readFile.execute(input.path);
1069
1191
  case 'write_file': return localTools.writeFile.execute(input.path, input.content);
1070
1192
  case 'list_files': return localTools.listFiles.execute(input.path);
1071
- case 'edit_file': return localTools.editFile.execute(input.path, input.search, input.replace);
1072
- case 'delete_file': return localTools.deleteFile.execute(input.path);
1073
1193
  case 'system_info': return localTools.systemInfo.execute();
1074
- case 'network_status': return JSON.stringify(await navada.network.ping());
1075
- case 'lucas_exec': return JSON.stringify(await navada.lucas.exec(input.command));
1076
- case 'lucas_ssh': return JSON.stringify(await navada.lucas.ssh(input.node, input.command));
1077
- case 'lucas_docker': return JSON.stringify(await navada.lucas.docker(input.container, input.command));
1078
- case 'mcp_call': return JSON.stringify(await navada.mcp.call(input.tool, input.args || {}));
1079
- case 'docker_registry':
1080
- if (input.image) return JSON.stringify(await navada.registry.tags(input.image));
1081
- return JSON.stringify(await navada.registry.catalog());
1082
- case 'send_email': return JSON.stringify(await navada.mcp.call('send-email', input));
1083
- case 'generate_image':
1084
- if (input.provider === 'dalle') return JSON.stringify(await navada.ai.openai.image(input.prompt));
1085
- const { size } = await navada.cloudflare.flux.generate(input.prompt, { savePath: `navada-${Date.now()}.png` });
1086
- return `Image generated: ${size} bytes`;
1194
+ case 'automation_request': {
1195
+ try {
1196
+ const edgeKey = config.get('edgeKey');
1197
+ const userId = config.get('edgeUserId') || 'anonymous';
1198
+ const email = config.get('edgeEmail') || '';
1199
+ const requestId = `req_${crypto.randomUUID().slice(0, 8)}`;
1200
+ const request = {
1201
+ id: requestId,
1202
+ title: input.title,
1203
+ description: input.description,
1204
+ type: input.type || 'custom',
1205
+ schedule: input.schedule || 'on-demand',
1206
+ userId,
1207
+ email,
1208
+ status: 'pending',
1209
+ submittedAt: new Date().toISOString(),
1210
+ };
1211
+ // Submit to NAVADA queue API
1212
+ const r = await navadaEdgeRequest('POST', '/api/v1/queue/automation', request);
1213
+ if (r.status === 201 || r.status === 200) {
1214
+ return `Automation request submitted!\n ID: ${requestId}\n Title: ${input.title}\n Status: Pending review\n\nYou'll receive an email once your automation is set up.`;
1215
+ }
1216
+ return `Request submitted locally (ID: ${requestId}). Server confirmation pending.`;
1217
+ } catch (e) {
1218
+ // Save locally if API unavailable
1219
+ const reqDir = path.join(config.CONFIG_DIR, 'requests');
1220
+ if (!fs.existsSync(reqDir)) fs.mkdirSync(reqDir, { recursive: true });
1221
+ const requestId = `req_${Date.now()}`;
1222
+ const request = { id: requestId, title: input.title, description: input.description, type: input.type || 'custom', schedule: input.schedule || 'on-demand', status: 'queued_locally', submittedAt: new Date().toISOString() };
1223
+ fs.writeFileSync(path.join(reqDir, `${requestId}.json`), JSON.stringify(request, null, 2));
1224
+ return `Request saved locally (ID: ${requestId}). Will sync when connected.\nCheck status: /requests`;
1225
+ }
1226
+ }
1227
+ case 'web_search': {
1228
+ try {
1229
+ const r = await navadaEdgeRequest('POST', '/search', { query: input.query });
1230
+ return r.data?.results ? JSON.stringify(r.data.results) : JSON.stringify(r.data);
1231
+ } catch (e) { return `Search error: ${e.message}`; }
1232
+ }
1233
+ // ── Memory tools ──
1234
+ case 'save_memory': {
1235
+ // Tier 3 — save to semantic knowledge base
1236
+ const category = input.key?.includes('pref') ? 'preferences'
1237
+ : input.key?.includes('person') || input.key?.includes('name') ? 'people'
1238
+ : input.key?.includes('decision') ? 'decisions'
1239
+ : 'facts';
1240
+ memory.knowledge.add(category, `${input.key}: ${input.value}`);
1241
+ return `Remembered: "${input.key}" → "${input.value}"`;
1242
+ }
1243
+ case 'recall_memory': {
1244
+ if (input.key) {
1245
+ // Search across all knowledge
1246
+ const results = memory.knowledge.search(input.key, 5);
1247
+ if (results.length === 0) {
1248
+ // Also check episodes
1249
+ const episodes = memory.episodic.search(input.key);
1250
+ if (episodes.length > 0) {
1251
+ return episodes.map(e => `[${e.date}] ${e.summary}`).join('\n');
1252
+ }
1253
+ return `No memories found for: "${input.key}"`;
1254
+ }
1255
+ return results.map(r => `[${r.category}] ${r.content}`).join('\n');
1256
+ }
1257
+ // No key — return knowledge summary + episode count
1258
+ const stats = memory.knowledge.stats();
1259
+ const epCount = memory.episodic.count();
1260
+ const summary = memory.knowledge.getSummary();
1261
+ const statsLine = Object.entries(stats).map(([k, v]) => `${k}: ${v}`).join(', ');
1262
+ return `Memory: ${statsLine}, episodes: ${epCount}\n${summary || 'No knowledge stored yet.'}`;
1263
+ }
1264
+ // ── Perception tools ──
1265
+ case 'screenshot': {
1266
+ try {
1267
+ const outPath = path.resolve(input.output || 'screenshot.png');
1268
+ const py = process.platform === 'win32' ? 'python' : 'python3';
1269
+ execFileSync(py, ['-c', `from PIL import ImageGrab; img = ImageGrab.grab(); img.save(r'${outPath}')`], { timeout: 15000, encoding: 'utf-8' });
1270
+ return `Screenshot saved: ${outPath}`;
1271
+ } catch (e) { return `Screenshot failed: ${e.message}. Install Pillow: pip install Pillow`; }
1272
+ }
1273
+ case 'describe_image': {
1274
+ try {
1275
+ const imgPath = path.resolve(input.path);
1276
+ if (!fs.existsSync(imgPath)) return `Image not found: ${imgPath}`;
1277
+ const imgData = fs.readFileSync(imgPath).toString('base64');
1278
+ const mimeType = imgPath.endsWith('.png') ? 'image/png' : 'image/jpeg';
1279
+ const edgeKey = config.get('edgeKey');
1280
+ if (edgeKey) {
1281
+ const r = await navadaEdgeRequest('POST', '/vision', { image: imgData, mimeType, question: input.question || 'Describe this image.' });
1282
+ if (r.status === 200) return r.data?.description || JSON.stringify(r.data);
1283
+ }
1284
+ return `Image loaded (${(imgData.length / 1024).toFixed(0)}KB). Vision API requires Edge connection (/edge login).`;
1285
+ } catch (e) { return `Vision error: ${e.message}`; }
1286
+ }
1087
1287
  case 'python_exec': return localTools.pythonExec.execute(input.code);
1088
1288
  case 'python_pip': return localTools.pythonPip.execute(input.package);
1089
1289
  case 'python_script': return localTools.pythonScript.execute(input.path);
@@ -1094,7 +1294,7 @@ async function executeTool(name, input) {
1094
1294
  displayOutput(result);
1095
1295
  return result.error ? `Error (exit ${result.exitCode}): ${result.error}` : result.output;
1096
1296
  }
1097
- case 'founder_info': return localTools.founderInfo.execute(input.question);
1297
+ case 'founder_info': return localTools.founderInfo.execute();
1098
1298
  default: return `Unknown tool: ${name}`;
1099
1299
  }
1100
1300
  } catch (e) {
@@ -1102,110 +1302,6 @@ async function executeTool(name, input) {
1102
1302
  }
1103
1303
  }
1104
1304
 
1105
- // ---------------------------------------------------------------------------
1106
- // Local action interceptor — executes file/shell actions WITHOUT needing LLM tool use
1107
- // This ensures free tier users can still create, read, edit, delete files
1108
- // ---------------------------------------------------------------------------
1109
- function tryLocalAction(userMessage) {
1110
- const msg = userMessage.trim();
1111
- const home = os.homedir();
1112
- // Windows OneDrive redirects Desktop — check OneDrive first
1113
- const oneDriveDesktop = path.join(home, 'OneDrive', 'Desktop');
1114
- const desktop = (process.platform === 'win32' && fs.existsSync(oneDriveDesktop)) ? oneDriveDesktop : path.join(home, 'Desktop');
1115
-
1116
- // Resolve a location phrase to an absolute path (use ORIGINAL case, not lowered)
1117
- function resolveLocation(phrase) {
1118
- const p = phrase.trim().replace(/[""'.,!]/g, '');
1119
- const low = p.toLowerCase();
1120
- if (low === 'my desktop' || low === 'the desktop' || low === 'desktop') return desktop;
1121
- if (low === 'home' || low === 'my home' || low === 'home directory') return home;
1122
- if (p.startsWith('~')) return p.replace(/^~[/\\]?/, home + path.sep);
1123
- if (path.isAbsolute(p)) return p;
1124
- return path.join(process.cwd(), p);
1125
- }
1126
-
1127
- // Extract the ORIGINAL-CASE name from the original message using a case-insensitive match
1128
- // We match on the original message to preserve casing
1129
- let m;
1130
-
1131
- // ── Create folder/directory ──
1132
- // Pattern: "create a folder called NAME on my desktop"
1133
- m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s+(?:on|at|in)\s+(.+?)$/i);
1134
- if (m) {
1135
- const name = m[1].trim();
1136
- const loc = resolveLocation(m[2]);
1137
- const resolved = path.join(loc, name);
1138
- try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1139
- catch (e) { return null; }
1140
- }
1141
-
1142
- // Pattern: "create a folder on my desktop called NAME"
1143
- 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);
1144
- if (m) {
1145
- const loc = resolveLocation(m[1]);
1146
- const name = m[2].trim();
1147
- const resolved = path.join(loc, name);
1148
- try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1149
- catch (e) { return null; }
1150
- }
1151
-
1152
- // Pattern: "create a folder called NAME" (no location — use cwd, or desktop if mentioned earlier)
1153
- m = msg.match(/(?:create|make|new)\s+(?:a\s+)?(?:new\s+)?(?:folder|directory|dir)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s*$/i);
1154
- if (m) {
1155
- const name = m[1].trim();
1156
- const loc = msg.toLowerCase().includes('desktop') ? desktop : process.cwd();
1157
- const resolved = path.join(loc, name);
1158
- try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1159
- catch (e) { return null; }
1160
- }
1161
-
1162
- // Pattern: "create a new folder NAME on my desktop" (no "called/named")
1163
- 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);
1164
- if (m) {
1165
- const name = m[1].trim();
1166
- const loc = resolveLocation(m[2]);
1167
- const resolved = path.join(loc, name);
1168
- try { fs.mkdirSync(resolved, { recursive: true }); return `Created folder: ${resolved}`; }
1169
- catch (e) { return null; }
1170
- }
1171
-
1172
- // ── Create file ──
1173
- m = msg.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s+(?:on|at|in)\s+(.+?)$/i);
1174
- if (m) {
1175
- const resolved = path.join(resolveLocation(m[2]), m[1].trim());
1176
- return localTools.writeFile.execute(resolved, '');
1177
- }
1178
- m = msg.match(/(?:create|make|new|touch)\s+(?:a\s+)?(?:new\s+)?(?:file)\s+(?:called|named)\s+[""']?([^""']+?)[""']?\s*$/i);
1179
- if (m) {
1180
- const loc = msg.toLowerCase().includes('desktop') ? desktop : process.cwd();
1181
- return localTools.writeFile.execute(path.join(loc, m[1].trim()), '');
1182
- }
1183
-
1184
- // ── Read file ──
1185
- m = msg.match(/(?:read|show|display|cat|open)\s+(?:the\s+)?(?:file\s+)?[""']?([^""']+\.\w{1,5})[""']?/i);
1186
- if (m) {
1187
- const p = m[1].trim();
1188
- const filePath = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
1189
- return localTools.readFile.execute(filePath);
1190
- }
1191
-
1192
- // ── Delete file/folder ──
1193
- m = msg.match(/(?:delete|remove|rm)\s+(?:the\s+)?(?:file|folder|directory)\s+[""']?([^""']+?)[""']?\s*$/i);
1194
- if (m) {
1195
- const p = m[1].trim();
1196
- const filePath = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
1197
- return localTools.deleteFile.execute(filePath);
1198
- }
1199
-
1200
- // ── List files ──
1201
- m = msg.match(/(?:list|show|ls|dir)\s+(?:the\s+)?(?:files|contents|items)\s+(?:in|on|at|of)\s+(.+)/i);
1202
- if (m) {
1203
- return localTools.listFiles.execute(resolveLocation(m[1]));
1204
- }
1205
-
1206
- return null;
1207
- }
1208
-
1209
1305
  async function grokChat(userMessage, conversationHistory = []) {
1210
1306
  const messages = [
1211
1307
  ...conversationHistory.slice(-20).map(m => ({
@@ -1225,6 +1321,7 @@ async function grokChat(userMessage, conversationHistory = []) {
1225
1321
  const r = await navada.request(endpoint, {
1226
1322
  method: 'POST',
1227
1323
  body: { messages, tools },
1324
+ headers: navadaAuthHeaders(),
1228
1325
  timeout: 120000,
1229
1326
  });
1230
1327
 
@@ -1269,6 +1366,7 @@ async function grokChat(userMessage, conversationHistory = []) {
1269
1366
  const r = await navada.request(endpoint, {
1270
1367
  method: 'POST',
1271
1368
  body: { messages, tools },
1369
+ headers: navadaAuthHeaders(),
1272
1370
  timeout: 120000,
1273
1371
  });
1274
1372
  if (r.status !== 200) break;
@@ -1279,7 +1377,8 @@ async function grokChat(userMessage, conversationHistory = []) {
1279
1377
 
1280
1378
  // Extract final text
1281
1379
  const content = response?.choices?.[0]?.message?.content || '';
1282
- return cleanOutput(content) || 'No response.';
1380
+ if (content) console.log(` ${content}`);
1381
+ return content || 'No response.';
1283
1382
  }
1284
1383
 
1285
1384
  async function fallbackChat(msg) {
@@ -1310,6 +1409,14 @@ async function fallbackChat(msg) {
1310
1409
  // ---------------------------------------------------------------------------
1311
1410
  let _updateInfo = null;
1312
1411
 
1412
+ // Auto-save session episode on exit
1413
+ process.on('beforeExit', () => {
1414
+ try { memory.manager.saveSessionEpisode(); } catch {}
1415
+ });
1416
+ process.on('SIGINT', () => {
1417
+ try { memory.manager.saveSessionEpisode(); } catch {}
1418
+ });
1419
+
1313
1420
  async function checkForUpdate() {
1314
1421
  try {
1315
1422
  const pkg = require('../package.json');
@@ -1335,6 +1442,7 @@ async function reportTelemetry(event, data = {}) {
1335
1442
  try {
1336
1443
  await navada.request(base + '/api/agent-heartbeat', {
1337
1444
  method: 'POST',
1445
+ headers: navadaAuthHeaders(),
1338
1446
  body: {
1339
1447
  agent: 'navada-edge-cli',
1340
1448
  event,
@@ -1354,4 +1462,4 @@ async function reportTelemetry(event, data = {}) {
1354
1462
  }
1355
1463
  }
1356
1464
 
1357
- module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState, addToHistory, getConversationHistory, clearHistory, listSubAgents };
1465
+ module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState, addToHistory, getConversationHistory, clearHistory, listSubAgents, memory };