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/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.0.0",
4
- "description": "Interactive CLI for the NAVADA Edge Network AI-driven hybrid cloud platform with AWS, Oracle, Azure, NVIDIA GPU compute, and 15 MCP tools",
3
+ "version": "4.2.0",
4
+ "description": "AI agent in your terminal3-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
- "mcp",
27
- "sdk",
28
- "agents",
29
- "yolo",
30
- "terminal"
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": "^1.3.0",
48
+ "navada-edge-sdk": "^2.0.0",
47
49
  "ora": "^5.4.1"
48
50
  },
49
51
  "optionalDependencies": {