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/README.md +297 -523
- package/lib/agent.js +392 -284
- package/lib/commands/ai.js +8 -9
- package/lib/commands/audit.js +1 -1
- package/lib/commands/compute.js +144 -165
- package/lib/commands/edge.js +139 -14
- package/lib/commands/index.js +1 -1
- package/lib/commands/lucas.js +6 -34
- package/lib/commands/mcp.js +6 -29
- package/lib/commands/nvidia.js +4 -4
- package/lib/commands/setup.js +271 -59
- package/lib/commands/skills.js +209 -0
- package/lib/commands/system.js +173 -0
- package/lib/memory.js +432 -0
- package/lib/skills.js +222 -0
- package/package.json +14 -12
- package/lib/commands/files.js +0 -164
- package/lib/knowledge.py +0 -197
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
|
-
//
|
|
16
|
+
// Request helpers — auth, 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
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 /
|
|
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
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- founder_info:
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
139
|
-
subAgent: null,
|
|
140
|
-
history
|
|
208
|
+
learningMode: null,
|
|
209
|
+
subAgent: null,
|
|
210
|
+
get history() { return memory.working.recentMessages; },
|
|
141
211
|
};
|
|
142
212
|
|
|
143
|
-
// Conversation history
|
|
213
|
+
// Conversation history — powered by 3-tier memory system
|
|
144
214
|
function addToHistory(role, content) {
|
|
145
|
-
|
|
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
|
|
228
|
+
return memory.working.getContextMessages();
|
|
155
229
|
}
|
|
156
230
|
|
|
157
231
|
function clearHistory() {
|
|
158
|
-
|
|
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: '
|
|
346
|
-
execute: (
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
648
|
-
|
|
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 =
|
|
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: '
|
|
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
|
|
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
|
-
//
|
|
880
|
+
// Prompt-based tool calling for providers without native function calling
|
|
831
881
|
// ---------------------------------------------------------------------------
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
883
|
-
|
|
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
|
|
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
|
-
|
|
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: '
|
|
957
|
-
description: '
|
|
958
|
-
input_schema: { type: 'object', properties: {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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: '
|
|
972
|
-
description: '
|
|
973
|
-
input_schema: { type: 'object', properties: {
|
|
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: '
|
|
977
|
-
description: '
|
|
978
|
-
input_schema: { type: 'object', properties: {
|
|
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: '
|
|
982
|
-
description: '
|
|
983
|
-
input_schema: { type: 'object', properties: {
|
|
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: '
|
|
987
|
-
description: '
|
|
988
|
-
input_schema: { type: 'object', properties: {
|
|
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: '
|
|
992
|
-
description: '
|
|
993
|
-
input_schema: { type: 'object', properties: {
|
|
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: '
|
|
1018
|
-
input_schema: { type: 'object', properties: {
|
|
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
|
|
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 '
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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(
|
|
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
|
-
|
|
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 };
|