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/memory.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(require('os').homedir(), '.navada');
|
|
8
|
+
const MEMORY_DIR = path.join(CONFIG_DIR, 'memory');
|
|
9
|
+
const EPISODES_DIR = path.join(MEMORY_DIR, 'episodes');
|
|
10
|
+
const KNOWLEDGE_FILE = path.join(MEMORY_DIR, 'knowledge.json');
|
|
11
|
+
|
|
12
|
+
function ensureDirs() {
|
|
13
|
+
for (const d of [CONFIG_DIR, MEMORY_DIR, EPISODES_DIR]) {
|
|
14
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
19
|
+
// TIER 1: WORKING MEMORY — in-session buffer with rolling summary
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
21
|
+
const working = {
|
|
22
|
+
recentMessages: [], // Last 20 full messages
|
|
23
|
+
summary: '', // Compressed summary of older messages
|
|
24
|
+
turnsSinceSummary: 0, // Counter for auto-summarisation trigger
|
|
25
|
+
MAX_RECENT: 20,
|
|
26
|
+
SUMMARISE_EVERY: 15,
|
|
27
|
+
|
|
28
|
+
add(role, content) {
|
|
29
|
+
this.recentMessages.push({ role, content, ts: Date.now() });
|
|
30
|
+
this.turnsSinceSummary++;
|
|
31
|
+
|
|
32
|
+
// When buffer exceeds limit, compress oldest messages into summary
|
|
33
|
+
if (this.recentMessages.length > this.MAX_RECENT) {
|
|
34
|
+
const overflow = this.recentMessages.splice(0, this.recentMessages.length - this.MAX_RECENT);
|
|
35
|
+
const overflowText = overflow.map(m => `${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 200) : JSON.stringify(m.content).slice(0, 200)}`).join('\n');
|
|
36
|
+
|
|
37
|
+
if (this.summary) {
|
|
38
|
+
this.summary += '\n' + overflowText;
|
|
39
|
+
} else {
|
|
40
|
+
this.summary = overflowText;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Keep summary from growing unbounded — trim to ~2000 chars
|
|
44
|
+
if (this.summary.length > 2000) {
|
|
45
|
+
this.summary = this.summary.slice(-2000);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Get messages formatted for AI provider (summary + recent)
|
|
51
|
+
getContextMessages() {
|
|
52
|
+
const messages = [];
|
|
53
|
+
if (this.summary) {
|
|
54
|
+
messages.push({
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: `[Earlier conversation summary]\n${this.summary}\n[End of summary — recent messages follow]`,
|
|
57
|
+
});
|
|
58
|
+
messages.push({
|
|
59
|
+
role: 'assistant',
|
|
60
|
+
content: 'Understood, I have context from our earlier conversation.',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Add recent messages
|
|
64
|
+
for (const m of this.recentMessages) {
|
|
65
|
+
messages.push({ role: m.role, content: m.content });
|
|
66
|
+
}
|
|
67
|
+
return messages;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getMessageCount() {
|
|
71
|
+
return this.recentMessages.length;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
clear() {
|
|
75
|
+
this.recentMessages = [];
|
|
76
|
+
this.summary = '';
|
|
77
|
+
this.turnsSinceSummary = 0;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Check if it's time to auto-summarise (called by agent after each turn)
|
|
81
|
+
needsSummary() {
|
|
82
|
+
return this.turnsSinceSummary >= this.SUMMARISE_EVERY && this.recentMessages.length > 10;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Inject a compressed summary (called after AI generates one)
|
|
86
|
+
injectSummary(summaryText) {
|
|
87
|
+
// Move current recent to summary, keep last 5 for immediate context
|
|
88
|
+
const keep = this.recentMessages.slice(-5);
|
|
89
|
+
const oldText = this.recentMessages.slice(0, -5).map(m => `${m.role}: ${typeof m.content === 'string' ? m.content.slice(0, 150) : '...'}`).join('\n');
|
|
90
|
+
|
|
91
|
+
this.summary = summaryText || oldText;
|
|
92
|
+
this.recentMessages = keep;
|
|
93
|
+
this.turnsSinceSummary = 0;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
98
|
+
// TIER 2: EPISODIC MEMORY — auto-saved session summaries
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
100
|
+
const episodic = {
|
|
101
|
+
// Save a session episode when CLI exits or /save is called
|
|
102
|
+
saveEpisode(summary, tags = []) {
|
|
103
|
+
ensureDirs();
|
|
104
|
+
const id = `ep_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
|
|
105
|
+
const episode = {
|
|
106
|
+
id,
|
|
107
|
+
summary,
|
|
108
|
+
tags,
|
|
109
|
+
messageCount: working.getMessageCount(),
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
date: new Date().toISOString().slice(0, 10),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const filePath = path.join(EPISODES_DIR, `${id}.json`);
|
|
115
|
+
fs.writeFileSync(filePath, JSON.stringify(episode, null, 2));
|
|
116
|
+
return id;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Load recent episodes (for context injection)
|
|
120
|
+
loadRecent(count = 5) {
|
|
121
|
+
ensureDirs();
|
|
122
|
+
try {
|
|
123
|
+
const files = fs.readdirSync(EPISODES_DIR)
|
|
124
|
+
.filter(f => f.endsWith('.json'))
|
|
125
|
+
.sort()
|
|
126
|
+
.reverse()
|
|
127
|
+
.slice(0, count);
|
|
128
|
+
|
|
129
|
+
return files.map(f => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(fs.readFileSync(path.join(EPISODES_DIR, f), 'utf-8'));
|
|
132
|
+
} catch { return null; }
|
|
133
|
+
}).filter(Boolean);
|
|
134
|
+
} catch { return []; }
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Search episodes by keyword (simple text match)
|
|
138
|
+
search(query) {
|
|
139
|
+
ensureDirs();
|
|
140
|
+
const q = query.toLowerCase();
|
|
141
|
+
try {
|
|
142
|
+
const files = fs.readdirSync(EPISODES_DIR).filter(f => f.endsWith('.json'));
|
|
143
|
+
const results = [];
|
|
144
|
+
|
|
145
|
+
for (const f of files) {
|
|
146
|
+
try {
|
|
147
|
+
const ep = JSON.parse(fs.readFileSync(path.join(EPISODES_DIR, f), 'utf-8'));
|
|
148
|
+
const text = `${ep.summary} ${(ep.tags || []).join(' ')}`.toLowerCase();
|
|
149
|
+
if (text.includes(q)) {
|
|
150
|
+
results.push(ep);
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 10);
|
|
156
|
+
} catch { return []; }
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Get episode count
|
|
160
|
+
count() {
|
|
161
|
+
ensureDirs();
|
|
162
|
+
try {
|
|
163
|
+
return fs.readdirSync(EPISODES_DIR).filter(f => f.endsWith('.json')).length;
|
|
164
|
+
} catch { return 0; }
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
169
|
+
// TIER 3: SEMANTIC KNOWLEDGE — persistent facts, prefs, patterns
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
// TF-IDF search (pure JS, zero deps)
|
|
173
|
+
function tokenize(text) {
|
|
174
|
+
return text.toLowerCase()
|
|
175
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
176
|
+
.split(/\s+/)
|
|
177
|
+
.filter(w => w.length > 2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function tfidfScore(query, document) {
|
|
181
|
+
const queryTokens = tokenize(query);
|
|
182
|
+
const docTokens = tokenize(document);
|
|
183
|
+
if (docTokens.length === 0) return 0;
|
|
184
|
+
|
|
185
|
+
const docFreq = {};
|
|
186
|
+
for (const t of docTokens) {
|
|
187
|
+
docFreq[t] = (docFreq[t] || 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let score = 0;
|
|
191
|
+
for (const qt of queryTokens) {
|
|
192
|
+
if (docFreq[qt]) {
|
|
193
|
+
// TF * simple IDF approximation
|
|
194
|
+
const tf = docFreq[qt] / docTokens.length;
|
|
195
|
+
score += tf * (1 + Math.log(1 + docFreq[qt]));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return score;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const knowledge = {
|
|
202
|
+
_cache: null,
|
|
203
|
+
|
|
204
|
+
_load() {
|
|
205
|
+
if (this._cache) return this._cache;
|
|
206
|
+
ensureDirs();
|
|
207
|
+
try {
|
|
208
|
+
this._cache = JSON.parse(fs.readFileSync(KNOWLEDGE_FILE, 'utf-8'));
|
|
209
|
+
} catch {
|
|
210
|
+
this._cache = { facts: [], preferences: [], skills: [], people: [], decisions: [] };
|
|
211
|
+
}
|
|
212
|
+
return this._cache;
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
_save() {
|
|
216
|
+
ensureDirs();
|
|
217
|
+
fs.writeFileSync(KNOWLEDGE_FILE, JSON.stringify(this._cache, null, 2));
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Add a knowledge entry
|
|
221
|
+
add(category, content, source = 'conversation') {
|
|
222
|
+
const db = this._load();
|
|
223
|
+
if (!db[category]) db[category] = [];
|
|
224
|
+
|
|
225
|
+
// Deduplicate — don't add if very similar entry exists
|
|
226
|
+
const isDuplicate = db[category].some(entry => {
|
|
227
|
+
const similarity = tfidfScore(content, entry.content);
|
|
228
|
+
return similarity > 0.8;
|
|
229
|
+
});
|
|
230
|
+
if (isDuplicate) return false;
|
|
231
|
+
|
|
232
|
+
db[category].push({
|
|
233
|
+
id: `k_${Date.now()}_${crypto.randomBytes(2).toString('hex')}`,
|
|
234
|
+
content,
|
|
235
|
+
source,
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Keep each category under 100 entries (FIFO)
|
|
240
|
+
if (db[category].length > 100) {
|
|
241
|
+
db[category] = db[category].slice(-100);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this._save();
|
|
245
|
+
return true;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Search knowledge by query (TF-IDF ranked)
|
|
249
|
+
search(query, limit = 5) {
|
|
250
|
+
const db = this._load();
|
|
251
|
+
const allEntries = [];
|
|
252
|
+
|
|
253
|
+
for (const [category, entries] of Object.entries(db)) {
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const score = tfidfScore(query, entry.content);
|
|
256
|
+
if (score > 0) {
|
|
257
|
+
allEntries.push({ ...entry, category, score });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return allEntries
|
|
263
|
+
.sort((a, b) => b.score - a.score)
|
|
264
|
+
.slice(0, limit);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// Get all entries in a category
|
|
268
|
+
getCategory(category) {
|
|
269
|
+
const db = this._load();
|
|
270
|
+
return db[category] || [];
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// Get a summary of all knowledge for system prompt injection
|
|
274
|
+
getSummary() {
|
|
275
|
+
const db = this._load();
|
|
276
|
+
const parts = [];
|
|
277
|
+
|
|
278
|
+
for (const [category, entries] of Object.entries(db)) {
|
|
279
|
+
if (entries.length > 0) {
|
|
280
|
+
const recent = entries.slice(-5);
|
|
281
|
+
parts.push(`${category}: ${recent.map(e => e.content).join('; ')}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return parts.length > 0 ? parts.join('\n') : '';
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Get stats
|
|
289
|
+
stats() {
|
|
290
|
+
const db = this._load();
|
|
291
|
+
const counts = {};
|
|
292
|
+
for (const [cat, entries] of Object.entries(db)) {
|
|
293
|
+
counts[cat] = entries.length;
|
|
294
|
+
}
|
|
295
|
+
return counts;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
// Clear cache (force reload from disk)
|
|
299
|
+
clearCache() {
|
|
300
|
+
this._cache = null;
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
305
|
+
// MEMORY MANAGER — orchestrates all 3 tiers
|
|
306
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
307
|
+
const manager = {
|
|
308
|
+
// Called at session start — loads relevant context
|
|
309
|
+
loadSessionContext() {
|
|
310
|
+
const parts = [];
|
|
311
|
+
|
|
312
|
+
// Load recent episodes (Tier 2)
|
|
313
|
+
const recentEpisodes = episodic.loadRecent(3);
|
|
314
|
+
if (recentEpisodes.length > 0) {
|
|
315
|
+
parts.push('Recent sessions:');
|
|
316
|
+
for (const ep of recentEpisodes) {
|
|
317
|
+
parts.push(` [${ep.date}] ${ep.summary}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Load knowledge summary (Tier 3)
|
|
322
|
+
const knowledgeSummary = knowledge.getSummary();
|
|
323
|
+
if (knowledgeSummary) {
|
|
324
|
+
parts.push('\nKnown about this user:');
|
|
325
|
+
parts.push(knowledgeSummary);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return parts.length > 0 ? parts.join('\n') : '';
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// Called after each AI response — extract and save knowledge automatically
|
|
332
|
+
autoExtract(userMessage, aiResponse) {
|
|
333
|
+
const text = typeof aiResponse === 'string' ? aiResponse : '';
|
|
334
|
+
const userText = typeof userMessage === 'string' ? userMessage : '';
|
|
335
|
+
|
|
336
|
+
// Extract user preferences (heuristics)
|
|
337
|
+
const prefPatterns = [
|
|
338
|
+
/(?:i (?:prefer|like|want|use|always|usually))\s+(.{10,80})/gi,
|
|
339
|
+
/(?:my (?:name|role|job|stack|language|framework) is)\s+(.{3,50})/gi,
|
|
340
|
+
/(?:i(?:'m| am) (?:a|an|the))\s+(.{3,50})/gi,
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
for (const pattern of prefPatterns) {
|
|
344
|
+
let match;
|
|
345
|
+
while ((match = pattern.exec(userText)) !== null) {
|
|
346
|
+
knowledge.add('preferences', match[0].trim());
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Extract facts about projects/tech mentioned
|
|
351
|
+
const factPatterns = [
|
|
352
|
+
/(?:(?:we|i) (?:built|created|deployed|use|have|run|maintain))\s+(.{10,100})/gi,
|
|
353
|
+
/(?:the (?:project|app|service|api|database|server) (?:is|runs|uses))\s+(.{10,80})/gi,
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
for (const pattern of factPatterns) {
|
|
357
|
+
let match;
|
|
358
|
+
while ((match = pattern.exec(userText)) !== null) {
|
|
359
|
+
knowledge.add('facts', match[0].trim());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Extract people mentioned
|
|
364
|
+
const peoplePattern = /(?:(?:my|our) (?:team|boss|colleague|manager|client|partner|co-founder))\s+(.{3,50})/gi;
|
|
365
|
+
let match;
|
|
366
|
+
while ((match = peoplePattern.exec(userText)) !== null) {
|
|
367
|
+
knowledge.add('people', match[0].trim());
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// Called when session ends — save episode
|
|
372
|
+
saveSessionEpisode() {
|
|
373
|
+
if (working.getMessageCount() < 3) return null; // Don't save trivial sessions
|
|
374
|
+
|
|
375
|
+
// Build summary from working memory
|
|
376
|
+
const messages = working.recentMessages;
|
|
377
|
+
const topics = [];
|
|
378
|
+
|
|
379
|
+
for (const m of messages) {
|
|
380
|
+
if (m.role === 'user' && typeof m.content === 'string') {
|
|
381
|
+
// Extract key phrases (first 60 chars of each user message)
|
|
382
|
+
topics.push(m.content.slice(0, 60).trim());
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const summary = topics.length > 0
|
|
387
|
+
? `Discussed: ${topics.slice(0, 5).join(', ')}`
|
|
388
|
+
: `Session with ${messages.length} messages`;
|
|
389
|
+
|
|
390
|
+
// Extract tags from conversation
|
|
391
|
+
const allText = messages.map(m => typeof m.content === 'string' ? m.content : '').join(' ').toLowerCase();
|
|
392
|
+
const tagKeywords = ['python', 'javascript', 'docker', 'deploy', 'api', 'database', 'email', 'marketing', 'automation', 'debug', 'test', 'build', 'react', 'node', 'css', 'html', 'git', 'aws', 'azure'];
|
|
393
|
+
const tags = tagKeywords.filter(t => allText.includes(t));
|
|
394
|
+
|
|
395
|
+
return episodic.saveEpisode(summary, tags);
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
// Get relevant memories for a specific query (for system prompt injection)
|
|
399
|
+
getRelevantContext(query) {
|
|
400
|
+
const parts = [];
|
|
401
|
+
|
|
402
|
+
// Search knowledge base (Tier 3)
|
|
403
|
+
const relevant = knowledge.search(query, 3);
|
|
404
|
+
if (relevant.length > 0) {
|
|
405
|
+
parts.push('Relevant memories:');
|
|
406
|
+
for (const r of relevant) {
|
|
407
|
+
parts.push(` [${r.category}] ${r.content}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Search episodes (Tier 2)
|
|
412
|
+
const episodes = episodic.search(query);
|
|
413
|
+
if (episodes.length > 0) {
|
|
414
|
+
parts.push('Related past sessions:');
|
|
415
|
+
for (const ep of episodes.slice(0, 2)) {
|
|
416
|
+
parts.push(` [${ep.date}] ${ep.summary}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return parts.length > 0 ? parts.join('\n') : '';
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
module.exports = {
|
|
425
|
+
working,
|
|
426
|
+
episodic,
|
|
427
|
+
knowledge,
|
|
428
|
+
manager,
|
|
429
|
+
MEMORY_DIR,
|
|
430
|
+
EPISODES_DIR,
|
|
431
|
+
KNOWLEDGE_FILE,
|
|
432
|
+
};
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
|
|
7
|
+
const SKILLS_DIR = path.join(config.CONFIG_DIR, 'skills');
|
|
8
|
+
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parse a skill markdown file into structured data
|
|
14
|
+
function parseSkill(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
17
|
+
const lines = raw.split('\n');
|
|
18
|
+
const skill = {
|
|
19
|
+
name: path.basename(filePath, '.md'),
|
|
20
|
+
file: filePath,
|
|
21
|
+
title: '',
|
|
22
|
+
trigger: [],
|
|
23
|
+
description: '',
|
|
24
|
+
steps: '',
|
|
25
|
+
output: '',
|
|
26
|
+
raw,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let section = 'header';
|
|
30
|
+
const sectionContent = { steps: [], output: [] };
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
|
|
35
|
+
// Title
|
|
36
|
+
if (trimmed.startsWith('# ') && !skill.title) {
|
|
37
|
+
skill.title = trimmed.slice(2).trim();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Metadata lines
|
|
42
|
+
if (trimmed.startsWith('trigger:')) {
|
|
43
|
+
skill.trigger = trimmed.slice(8).split(',').map(t => t.trim().replace(/"/g, '').toLowerCase()).filter(Boolean);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (trimmed.startsWith('description:')) {
|
|
47
|
+
skill.description = trimmed.slice(12).trim();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Section headers
|
|
52
|
+
if (trimmed.startsWith('## Steps')) { section = 'steps'; continue; }
|
|
53
|
+
if (trimmed.startsWith('## Output')) { section = 'output'; continue; }
|
|
54
|
+
if (trimmed.startsWith('## ')) { section = 'other'; continue; }
|
|
55
|
+
|
|
56
|
+
if (section === 'steps') sectionContent.steps.push(line);
|
|
57
|
+
if (section === 'output') sectionContent.output.push(line);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
skill.steps = sectionContent.steps.join('\n').trim();
|
|
61
|
+
skill.output = sectionContent.output.join('\n').trim();
|
|
62
|
+
if (!skill.title) skill.title = skill.name;
|
|
63
|
+
if (!skill.description) skill.description = skill.title;
|
|
64
|
+
|
|
65
|
+
return skill;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load all skills
|
|
72
|
+
function loadAll() {
|
|
73
|
+
ensureDir();
|
|
74
|
+
try {
|
|
75
|
+
const files = fs.readdirSync(SKILLS_DIR).filter(f => f.endsWith('.md'));
|
|
76
|
+
return files.map(f => parseSkill(path.join(SKILLS_DIR, f))).filter(Boolean);
|
|
77
|
+
} catch { return []; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find a skill by trigger phrase
|
|
81
|
+
function matchSkill(input) {
|
|
82
|
+
const lower = input.toLowerCase();
|
|
83
|
+
const skills = loadAll();
|
|
84
|
+
|
|
85
|
+
for (const skill of skills) {
|
|
86
|
+
for (const trigger of skill.trigger) {
|
|
87
|
+
if (lower.includes(trigger)) return skill;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get skill by name
|
|
94
|
+
function getSkill(name) {
|
|
95
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
96
|
+
const filePath = path.join(SKILLS_DIR, `${slug}.md`);
|
|
97
|
+
if (fs.existsSync(filePath)) return parseSkill(filePath);
|
|
98
|
+
|
|
99
|
+
// Try partial match
|
|
100
|
+
ensureDir();
|
|
101
|
+
const files = fs.readdirSync(SKILLS_DIR).filter(f => f.endsWith('.md'));
|
|
102
|
+
const match = files.find(f => f.toLowerCase().includes(slug));
|
|
103
|
+
if (match) return parseSkill(path.join(SKILLS_DIR, match));
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create a new skill from structured data
|
|
108
|
+
function createSkill(name, data) {
|
|
109
|
+
ensureDir();
|
|
110
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
111
|
+
const filePath = path.join(SKILLS_DIR, `${slug}.md`);
|
|
112
|
+
|
|
113
|
+
const content = `# ${data.title || name}
|
|
114
|
+
trigger: ${(data.triggers || []).map(t => `"${t}"`).join(', ')}
|
|
115
|
+
description: ${data.description || ''}
|
|
116
|
+
|
|
117
|
+
## Steps
|
|
118
|
+
${data.steps || '1. Analyse the request\n2. Execute the task\n3. Return the result'}
|
|
119
|
+
|
|
120
|
+
## Output
|
|
121
|
+
${data.output || 'Formatted result based on the task'}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(filePath, content);
|
|
125
|
+
return filePath;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Delete a skill
|
|
129
|
+
function deleteSkill(name) {
|
|
130
|
+
const skill = getSkill(name);
|
|
131
|
+
if (!skill) return false;
|
|
132
|
+
try {
|
|
133
|
+
fs.unlinkSync(skill.file);
|
|
134
|
+
return true;
|
|
135
|
+
} catch { return false; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get skills summary for system prompt injection
|
|
139
|
+
function getSkillsPrompt() {
|
|
140
|
+
const skills = loadAll();
|
|
141
|
+
if (skills.length === 0) return '';
|
|
142
|
+
|
|
143
|
+
const lines = ['Available user skills (invoke when request matches):'];
|
|
144
|
+
for (const s of skills) {
|
|
145
|
+
lines.push(`- "${s.title}": ${s.description}. Triggers: ${s.trigger.join(', ') || 'manual'}. Steps: ${s.steps.slice(0, 150)}`);
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Built-in skill templates
|
|
151
|
+
const TEMPLATES = {
|
|
152
|
+
'seo-audit': {
|
|
153
|
+
title: 'SEO Audit',
|
|
154
|
+
triggers: ['seo audit', 'check seo', 'analyse website seo'],
|
|
155
|
+
description: 'Run a comprehensive SEO audit on any URL',
|
|
156
|
+
steps: `1. Use shell to curl the target URL and capture HTML
|
|
157
|
+
2. Use python_exec to parse HTML — extract title, meta description, h1-h6 tags, img alt attributes, internal/external links
|
|
158
|
+
3. Check: title length (50-60 chars), meta description (150-160 chars), heading hierarchy, missing alt text, broken links
|
|
159
|
+
4. Score each category out of 10
|
|
160
|
+
5. Generate a markdown report with scores and actionable recommendations`,
|
|
161
|
+
output: 'Markdown report: overall score, category breakdown, top 5 fixes',
|
|
162
|
+
},
|
|
163
|
+
'email-template': {
|
|
164
|
+
title: 'Marketing Email',
|
|
165
|
+
triggers: ['marketing email', 'email template', 'write email campaign'],
|
|
166
|
+
description: 'Generate a professional marketing email template',
|
|
167
|
+
steps: `1. Ask for: target audience, product/service, tone, call-to-action
|
|
168
|
+
2. Write subject line (under 50 chars, no spam words)
|
|
169
|
+
3. Write preview text (90 chars)
|
|
170
|
+
4. Write email body: hook, value proposition, social proof, CTA
|
|
171
|
+
5. Save as HTML file with inline CSS for email client compatibility`,
|
|
172
|
+
output: 'HTML email template file + plain text version',
|
|
173
|
+
},
|
|
174
|
+
'api-scaffold': {
|
|
175
|
+
title: 'REST API Scaffold',
|
|
176
|
+
triggers: ['scaffold api', 'create api', 'generate api project'],
|
|
177
|
+
description: 'Generate a complete REST API project with routes, models, and tests',
|
|
178
|
+
steps: `1. Ask for: language (Node/Python/Go), database, entity names
|
|
179
|
+
2. Create project directory with standard structure
|
|
180
|
+
3. Generate: package.json/requirements.txt, entry point, routes, models, middleware
|
|
181
|
+
4. Add: error handling, validation, health endpoint, CORS
|
|
182
|
+
5. Generate: Dockerfile, .env.example, README with API docs
|
|
183
|
+
6. Run initial install and verify it starts`,
|
|
184
|
+
output: 'Complete project directory, ready to run',
|
|
185
|
+
},
|
|
186
|
+
'git-pr': {
|
|
187
|
+
title: 'Git PR Creator',
|
|
188
|
+
triggers: ['create pr', 'pull request', 'git pr'],
|
|
189
|
+
description: 'Analyse changes and create a well-documented pull request',
|
|
190
|
+
steps: `1. Run git diff to see all changes
|
|
191
|
+
2. Run git log to see commit history since branch point
|
|
192
|
+
3. Categorise changes: features, fixes, refactors, tests
|
|
193
|
+
4. Write PR title (under 72 chars, conventional commit style)
|
|
194
|
+
5. Write PR body: summary, changes list, testing notes, screenshots if UI
|
|
195
|
+
6. Use shell to create the PR via gh cli`,
|
|
196
|
+
output: 'PR created with full description and labels',
|
|
197
|
+
},
|
|
198
|
+
'data-report': {
|
|
199
|
+
title: 'Data Report Generator',
|
|
200
|
+
triggers: ['analyse data', 'data report', 'generate report from'],
|
|
201
|
+
description: 'Analyse a data file and produce a visual report',
|
|
202
|
+
steps: `1. Read the data file (CSV, JSON, Excel)
|
|
203
|
+
2. Use python_exec with pandas: shape, dtypes, null counts, describe()
|
|
204
|
+
3. Identify key patterns: trends, outliers, correlations
|
|
205
|
+
4. Generate visualisations with matplotlib (save as PNG)
|
|
206
|
+
5. Write a markdown summary report with embedded charts
|
|
207
|
+
6. Save report as HTML for easy sharing`,
|
|
208
|
+
output: 'HTML report with charts, summary statistics, and insights',
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
SKILLS_DIR,
|
|
214
|
+
loadAll,
|
|
215
|
+
matchSkill,
|
|
216
|
+
getSkill,
|
|
217
|
+
createSkill,
|
|
218
|
+
deleteSkill,
|
|
219
|
+
parseSkill,
|
|
220
|
+
getSkillsPrompt,
|
|
221
|
+
TEMPLATES,
|
|
222
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navada-edge-cli",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "AI agent in your terminal — 3-tier memory, 16 tools, automation pipeline, bring your own model. Learns who you are, remembers across sessions.",
|
|
5
5
|
"main": "lib/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"navada": "bin/navada.js"
|
|
@@ -18,16 +18,18 @@
|
|
|
18
18
|
"navada",
|
|
19
19
|
"edge",
|
|
20
20
|
"cli",
|
|
21
|
-
"tui",
|
|
22
|
-
"distributed",
|
|
23
|
-
"cloudflare",
|
|
24
|
-
"docker",
|
|
25
21
|
"ai",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
22
|
+
"agent",
|
|
23
|
+
"terminal",
|
|
24
|
+
"memory",
|
|
25
|
+
"automation",
|
|
26
|
+
"tools",
|
|
27
|
+
"nvidia",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"openai",
|
|
30
|
+
"gemini",
|
|
31
|
+
"byom",
|
|
32
|
+
"sdk"
|
|
31
33
|
],
|
|
32
34
|
"author": {
|
|
33
35
|
"name": "Leslie Akpareva",
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
"dependencies": {
|
|
44
46
|
"chalk": "^4.1.2",
|
|
45
47
|
"cli-table3": "^0.6.5",
|
|
46
|
-
"navada-edge-sdk": "^
|
|
48
|
+
"navada-edge-sdk": "^2.0.0",
|
|
47
49
|
"ora": "^5.4.1"
|
|
48
50
|
},
|
|
49
51
|
"optionalDependencies": {
|