navada-edge-cli 2.5.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent.js +249 -16
- package/lib/commands/index.js +1 -0
- package/lib/commands/learn.js +141 -0
- package/lib/ui.js +134 -39
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -28,6 +28,30 @@ When you don't have a tool for something, say so clearly and suggest an alternat
|
|
|
28
28
|
Keep responses short. Code blocks when needed. No fluff.`,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
function getSystemPrompt() {
|
|
32
|
+
if (sessionState.learningMode) {
|
|
33
|
+
try {
|
|
34
|
+
const { MODES } = require('./commands/learn');
|
|
35
|
+
const mode = MODES[sessionState.learningMode];
|
|
36
|
+
if (mode) return mode.system;
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
return IDENTITY.personality;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Session state — exposed for UI panels
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
const sessionState = {
|
|
46
|
+
provider: 'Grok (free)',
|
|
47
|
+
model: 'grok-3-mini',
|
|
48
|
+
tokens: { input: 0, output: 0, total: 0 },
|
|
49
|
+
cost: 0,
|
|
50
|
+
messages: 0,
|
|
51
|
+
startTime: Date.now(),
|
|
52
|
+
learningMode: null, // 'python' | 'csharp' | 'node' | null
|
|
53
|
+
};
|
|
54
|
+
|
|
31
55
|
// ---------------------------------------------------------------------------
|
|
32
56
|
// Rate limit tracking (in-memory, per session)
|
|
33
57
|
// ---------------------------------------------------------------------------
|
|
@@ -124,6 +148,52 @@ const localTools = {
|
|
|
124
148
|
}, null, 2);
|
|
125
149
|
},
|
|
126
150
|
},
|
|
151
|
+
|
|
152
|
+
pythonExec: {
|
|
153
|
+
description: 'Execute Python code',
|
|
154
|
+
execute: (code) => {
|
|
155
|
+
try {
|
|
156
|
+
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
157
|
+
const output = execSync(`${py} -c "${code.replace(/"/g, '\\"')}"`, {
|
|
158
|
+
timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
159
|
+
});
|
|
160
|
+
return output.trim();
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return `Error: ${e.stderr?.trim() || e.message}`;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
pythonPip: {
|
|
168
|
+
description: 'Install a Python package',
|
|
169
|
+
execute: (pkg) => {
|
|
170
|
+
try {
|
|
171
|
+
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
172
|
+
const output = execSync(`${py} -m pip install ${pkg}`, {
|
|
173
|
+
timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
+
});
|
|
175
|
+
return output.trim().split('\n').slice(-3).join('\n');
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return `Error: ${e.stderr?.trim() || e.message}`;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
pythonScript: {
|
|
183
|
+
description: 'Run a Python script file',
|
|
184
|
+
execute: (scriptPath) => {
|
|
185
|
+
try {
|
|
186
|
+
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
187
|
+
const output = execSync(`${py} "${path.resolve(scriptPath)}"`, {
|
|
188
|
+
timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
189
|
+
cwd: path.dirname(path.resolve(scriptPath)),
|
|
190
|
+
});
|
|
191
|
+
return output.trim();
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return `Error: ${e.stderr?.trim() || e.message}`;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
},
|
|
127
197
|
};
|
|
128
198
|
|
|
129
199
|
// ---------------------------------------------------------------------------
|
|
@@ -264,7 +334,7 @@ function streamAnthropic(key, messages, tools, system) {
|
|
|
264
334
|
const body = JSON.stringify({
|
|
265
335
|
model: 'claude-sonnet-4-20250514',
|
|
266
336
|
max_tokens: 4096,
|
|
267
|
-
system: system ||
|
|
337
|
+
system: system || getSystemPrompt(),
|
|
268
338
|
messages,
|
|
269
339
|
tools,
|
|
270
340
|
stream: true,
|
|
@@ -354,6 +424,147 @@ function streamAnthropic(key, messages, tools, system) {
|
|
|
354
424
|
});
|
|
355
425
|
}
|
|
356
426
|
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Streaming — OpenAI API (GPT-4o, GPT-4o-mini)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
function streamOpenAI(key, messages, model = 'gpt-4o') {
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
const body = JSON.stringify({
|
|
433
|
+
model,
|
|
434
|
+
messages: [{ role: 'system', content: getSystemPrompt() }, ...messages],
|
|
435
|
+
max_tokens: 4096,
|
|
436
|
+
stream: true,
|
|
437
|
+
tools: openAITools(),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const req = https.request('https://api.openai.com/v1/chat/completions', {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: {
|
|
443
|
+
'Authorization': `Bearer ${key}`,
|
|
444
|
+
'Content-Type': 'application/json',
|
|
445
|
+
'Content-Length': Buffer.byteLength(body),
|
|
446
|
+
},
|
|
447
|
+
timeout: 120000,
|
|
448
|
+
}, (res) => {
|
|
449
|
+
if (res.statusCode !== 200) {
|
|
450
|
+
let data = '';
|
|
451
|
+
res.on('data', c => data += c);
|
|
452
|
+
res.on('end', () => reject(new Error(`OpenAI API error ${res.statusCode}: ${data.slice(0, 200)}`)));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let buffer = '';
|
|
457
|
+
let fullContent = '';
|
|
458
|
+
let toolCalls = [];
|
|
459
|
+
let finishReason = null;
|
|
460
|
+
|
|
461
|
+
res.on('data', (chunk) => {
|
|
462
|
+
buffer += chunk.toString();
|
|
463
|
+
const lines = buffer.split('\n');
|
|
464
|
+
buffer = lines.pop();
|
|
465
|
+
|
|
466
|
+
for (const line of lines) {
|
|
467
|
+
if (!line.startsWith('data: ')) continue;
|
|
468
|
+
const data = line.slice(6).trim();
|
|
469
|
+
if (data === '[DONE]') continue;
|
|
470
|
+
try {
|
|
471
|
+
const event = JSON.parse(data);
|
|
472
|
+
const delta = event.choices?.[0]?.delta;
|
|
473
|
+
const finish = event.choices?.[0]?.finish_reason;
|
|
474
|
+
|
|
475
|
+
if (finish) finishReason = finish;
|
|
476
|
+
|
|
477
|
+
if (delta?.content) {
|
|
478
|
+
process.stdout.write(delta.content);
|
|
479
|
+
fullContent += delta.content;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Accumulate tool calls
|
|
483
|
+
if (delta?.tool_calls) {
|
|
484
|
+
for (const tc of delta.tool_calls) {
|
|
485
|
+
if (tc.index !== undefined) {
|
|
486
|
+
if (!toolCalls[tc.index]) {
|
|
487
|
+
toolCalls[tc.index] = { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } };
|
|
488
|
+
}
|
|
489
|
+
if (tc.id) toolCalls[tc.index].id = tc.id;
|
|
490
|
+
if (tc.function?.name) toolCalls[tc.index].function.name += tc.function.name;
|
|
491
|
+
if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch {}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
res.on('end', () => {
|
|
500
|
+
if (fullContent) process.stdout.write('\n');
|
|
501
|
+
toolCalls = toolCalls.filter(Boolean);
|
|
502
|
+
resolve({ content: fullContent, tool_calls: toolCalls, finish_reason: finishReason });
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
req.on('error', reject);
|
|
507
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
508
|
+
req.write(body);
|
|
509
|
+
req.end();
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function openAITools() {
|
|
514
|
+
const defs = [
|
|
515
|
+
{ name: 'shell', description: 'Execute a shell command on the user\'s machine', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
|
|
516
|
+
{ name: 'read_file', description: 'Read a file', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
517
|
+
{ name: 'write_file', description: 'Write to a file', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
|
|
518
|
+
{ name: 'list_files', description: 'List directory contents', parameters: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
519
|
+
{ name: 'system_info', description: 'Get system info (CPU, RAM, OS)', parameters: { type: 'object', properties: {} } },
|
|
520
|
+
{ name: 'python_exec', description: 'Execute Python code', parameters: { type: 'object', properties: { code: { type: 'string' } }, required: ['code'] } },
|
|
521
|
+
{ name: 'python_pip', description: 'Install a Python package', parameters: { type: 'object', properties: { package: { type: 'string' } }, required: ['package'] } },
|
|
522
|
+
];
|
|
523
|
+
return defs.map(d => ({ type: 'function', function: d }));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// OpenAI chat with tool use loop
|
|
527
|
+
async function openAIChat(key, userMessage, conversationHistory = []) {
|
|
528
|
+
const model = config.getModel() === 'gpt-4o-mini' ? 'gpt-4o-mini' : 'gpt-4o';
|
|
529
|
+
const messages = [
|
|
530
|
+
...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
|
|
531
|
+
{ role: 'user', content: userMessage },
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
let response;
|
|
535
|
+
try {
|
|
536
|
+
response = await streamOpenAI(key, messages, model);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
if (e.message.includes('401') || e.message.includes('429') || e.message.includes('billing')) {
|
|
539
|
+
console.log(ui.warn('OpenAI API unavailable, falling back to Grok free tier...'));
|
|
540
|
+
return grokChat(userMessage, conversationHistory);
|
|
541
|
+
}
|
|
542
|
+
throw e;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Handle tool calls
|
|
546
|
+
let iterations = 0;
|
|
547
|
+
while (response.finish_reason === 'tool_calls' && response.tool_calls.length > 0 && iterations < 10) {
|
|
548
|
+
iterations++;
|
|
549
|
+
const toolResults = [];
|
|
550
|
+
|
|
551
|
+
for (const tc of response.tool_calls) {
|
|
552
|
+
let input;
|
|
553
|
+
try { input = JSON.parse(tc.function.arguments); } catch { input = {}; }
|
|
554
|
+
console.log(ui.dim(` [${tc.function.name}] ${JSON.stringify(input).slice(0, 80)}`));
|
|
555
|
+
const result = await executeTool(tc.function.name, input);
|
|
556
|
+
toolResults.push({ role: 'tool', tool_call_id: tc.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Add assistant message with tool_calls + results
|
|
560
|
+
messages.push({ role: 'assistant', content: response.content || null, tool_calls: response.tool_calls });
|
|
561
|
+
messages.push(...toolResults);
|
|
562
|
+
response = await streamOpenAI(key, messages, model);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return response.content || '';
|
|
566
|
+
}
|
|
567
|
+
|
|
357
568
|
// ---------------------------------------------------------------------------
|
|
358
569
|
// Smart model routing — detect intent and pick best provider
|
|
359
570
|
// ---------------------------------------------------------------------------
|
|
@@ -376,23 +587,24 @@ function detectIntent(message) {
|
|
|
376
587
|
// Anthropic Claude API — conversational agent with tool use
|
|
377
588
|
// ---------------------------------------------------------------------------
|
|
378
589
|
async function chat(userMessage, conversationHistory = []) {
|
|
379
|
-
const anthropicKey = config.get('anthropicKey')
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|| '';
|
|
590
|
+
const anthropicKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY || '';
|
|
591
|
+
const openaiKey = config.get('openaiKey') || process.env.OPENAI_API_KEY || '';
|
|
592
|
+
const apiKey = config.getApiKey() || '';
|
|
383
593
|
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|| '';
|
|
594
|
+
// Determine which provider to use
|
|
595
|
+
const effectiveAnthropicKey = anthropicKey || (apiKey.startsWith('sk-ant') ? apiKey : '');
|
|
596
|
+
const effectiveOpenAIKey = openaiKey || (apiKey.startsWith('sk-') && !apiKey.startsWith('sk-ant') ? apiKey : '');
|
|
388
597
|
|
|
389
|
-
// Smart routing when model is set to 'auto'
|
|
390
598
|
const modelPref = config.getModel();
|
|
391
599
|
const intent = detectIntent(userMessage);
|
|
392
600
|
|
|
601
|
+
// Track active provider for UI
|
|
602
|
+
if (effectiveAnthropicKey) sessionState.provider = 'Anthropic';
|
|
603
|
+
else if (effectiveOpenAIKey) sessionState.provider = 'OpenAI';
|
|
604
|
+
else sessionState.provider = 'Grok (free)';
|
|
605
|
+
|
|
393
606
|
// No personal key — use free tier
|
|
394
|
-
if (!
|
|
395
|
-
// Try Qwen for code if HuggingFace token available
|
|
607
|
+
if (!effectiveAnthropicKey && !effectiveOpenAIKey) {
|
|
396
608
|
if (intent === 'code' && navada.config.hfToken) {
|
|
397
609
|
try {
|
|
398
610
|
const r = await navada.ai.huggingface.qwen(userMessage);
|
|
@@ -402,14 +614,17 @@ async function chat(userMessage, conversationHistory = []) {
|
|
|
402
614
|
return grokChat(userMessage, conversationHistory);
|
|
403
615
|
}
|
|
404
616
|
|
|
405
|
-
//
|
|
617
|
+
// OpenAI key — route to OpenAI
|
|
618
|
+
if (effectiveOpenAIKey && (!effectiveAnthropicKey || modelPref === 'gpt-4o' || modelPref === 'gpt-4o-mini')) {
|
|
619
|
+
return openAIChat(effectiveOpenAIKey, userMessage, conversationHistory);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Has Anthropic key but route simple queries to free tier to save cost
|
|
406
623
|
if (modelPref === 'auto' && intent === 'general' && !conversationHistory.length) {
|
|
407
|
-
// General questions don't need tool use — use Grok free tier to save API cost
|
|
408
624
|
try {
|
|
409
625
|
const result = await callFreeTier([{ role: 'user', content: userMessage }], true);
|
|
410
626
|
if (result.content) return result.content;
|
|
411
627
|
} catch {}
|
|
412
|
-
// Fall through to Anthropic if free tier fails
|
|
413
628
|
}
|
|
414
629
|
|
|
415
630
|
const tools = [
|
|
@@ -478,6 +693,21 @@ async function chat(userMessage, conversationHistory = []) {
|
|
|
478
693
|
description: 'Generate an image using Cloudflare Flux (FREE) or DALL-E.',
|
|
479
694
|
input_schema: { type: 'object', properties: { prompt: { type: 'string' }, provider: { type: 'string', description: 'flux (default, free) or dalle' } }, required: ['prompt'] },
|
|
480
695
|
},
|
|
696
|
+
{
|
|
697
|
+
name: 'python_exec',
|
|
698
|
+
description: 'Execute Python code inline. Use for data analysis, scripts, calculations, ML tasks.',
|
|
699
|
+
input_schema: { type: 'object', properties: { code: { type: 'string', description: 'Python code to execute' } }, required: ['code'] },
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: 'python_pip',
|
|
703
|
+
description: 'Install a Python package via pip.',
|
|
704
|
+
input_schema: { type: 'object', properties: { package: { type: 'string', description: 'Package name (e.g. pandas, numpy)' } }, required: ['package'] },
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: 'python_script',
|
|
708
|
+
description: 'Run a Python script file.',
|
|
709
|
+
input_schema: { type: 'object', properties: { path: { type: 'string', description: 'Path to .py file' } }, required: ['path'] },
|
|
710
|
+
},
|
|
481
711
|
];
|
|
482
712
|
|
|
483
713
|
const messages = [
|
|
@@ -543,6 +773,9 @@ async function executeTool(name, input) {
|
|
|
543
773
|
if (input.provider === 'dalle') return JSON.stringify(await navada.ai.openai.image(input.prompt));
|
|
544
774
|
const { size } = await navada.cloudflare.flux.generate(input.prompt, { savePath: `navada-${Date.now()}.png` });
|
|
545
775
|
return `Image generated: ${size} bytes`;
|
|
776
|
+
case 'python_exec': return localTools.pythonExec.execute(input.code);
|
|
777
|
+
case 'python_pip': return localTools.pythonPip.execute(input.package);
|
|
778
|
+
case 'python_script': return localTools.pythonScript.execute(input.path);
|
|
546
779
|
default: return `Unknown tool: ${name}`;
|
|
547
780
|
}
|
|
548
781
|
} catch (e) {
|
|
@@ -641,4 +874,4 @@ async function reportTelemetry(event, data = {}) {
|
|
|
641
874
|
}
|
|
642
875
|
}
|
|
643
876
|
|
|
644
|
-
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker };
|
|
877
|
+
module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState };
|
package/lib/commands/index.js
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ui = require('../ui');
|
|
4
|
+
const { sessionState } = require('../agent');
|
|
5
|
+
|
|
6
|
+
const MODES = {
|
|
7
|
+
python: {
|
|
8
|
+
name: 'Python',
|
|
9
|
+
description: 'Python full-stack development — data science, ML, APIs, scripting',
|
|
10
|
+
system: `You are a Python expert tutor inside the NAVADA Edge terminal. Teach Python concepts with practical examples.
|
|
11
|
+
Always provide runnable code. Use the python_exec tool to demonstrate.
|
|
12
|
+
Cover: fundamentals, data structures, OOP, async, Flask/FastAPI, pandas, numpy, ML basics.
|
|
13
|
+
When the user asks a question, explain concisely then show working code.
|
|
14
|
+
Use the NAVADA Edge network for practical exercises (Docker, APIs, infrastructure).
|
|
15
|
+
Keep explanations beginner-friendly but technically accurate.`,
|
|
16
|
+
topics: [
|
|
17
|
+
'Variables, types, f-strings',
|
|
18
|
+
'Lists, dicts, comprehensions',
|
|
19
|
+
'Functions, decorators, generators',
|
|
20
|
+
'Classes, inheritance, dataclasses',
|
|
21
|
+
'File I/O, JSON, CSV',
|
|
22
|
+
'async/await, aiohttp',
|
|
23
|
+
'Flask & FastAPI APIs',
|
|
24
|
+
'pandas, numpy, matplotlib',
|
|
25
|
+
'ML with scikit-learn',
|
|
26
|
+
'Docker + Python apps',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
csharp: {
|
|
31
|
+
name: 'C#',
|
|
32
|
+
description: 'C# and .NET development — APIs, Azure, enterprise patterns',
|
|
33
|
+
system: `You are a C# and .NET expert tutor inside the NAVADA Edge terminal. Teach C# with practical examples.
|
|
34
|
+
Provide code snippets that illustrate concepts. Since we can't run C# directly, show complete examples and explain output.
|
|
35
|
+
Cover: fundamentals, OOP, LINQ, async/await, ASP.NET Core, Entity Framework, Azure integration.
|
|
36
|
+
When the user asks a question, explain concisely then show working code.
|
|
37
|
+
Focus on modern C# (12+), .NET 8+, and enterprise patterns.
|
|
38
|
+
Keep explanations beginner-friendly but technically accurate.`,
|
|
39
|
+
topics: [
|
|
40
|
+
'Variables, types, string interpolation',
|
|
41
|
+
'Collections, LINQ queries',
|
|
42
|
+
'Classes, interfaces, records',
|
|
43
|
+
'async/await, Task patterns',
|
|
44
|
+
'ASP.NET Core Web APIs',
|
|
45
|
+
'Entity Framework Core',
|
|
46
|
+
'Dependency injection',
|
|
47
|
+
'Azure Functions & Services',
|
|
48
|
+
'Design patterns (SOLID, Repository)',
|
|
49
|
+
'Docker + .NET apps',
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
node: {
|
|
54
|
+
name: 'Node.js',
|
|
55
|
+
description: 'Node.js network programming — APIs, Docker, real-time systems',
|
|
56
|
+
system: `You are a Node.js expert tutor inside the NAVADA Edge terminal. Teach Node.js with practical examples.
|
|
57
|
+
Always provide runnable code. Use the shell tool to demonstrate with node -e.
|
|
58
|
+
Cover: fundamentals, async patterns, Express/Fastify, WebSockets, Docker, PM2, databases.
|
|
59
|
+
When the user asks a question, explain concisely then show working code.
|
|
60
|
+
Use the NAVADA Edge network for practical exercises (MCP, Docker, real-time infrastructure).
|
|
61
|
+
The user has full access to the NAVADA Edge Network — 4 nodes, Docker, PostgreSQL, Redis.
|
|
62
|
+
Keep explanations beginner-friendly but technically accurate.`,
|
|
63
|
+
topics: [
|
|
64
|
+
'Modules, CommonJS vs ESM',
|
|
65
|
+
'Callbacks, Promises, async/await',
|
|
66
|
+
'Event loop, streams, buffers',
|
|
67
|
+
'Express & Fastify APIs',
|
|
68
|
+
'WebSocket & SSE real-time',
|
|
69
|
+
'PostgreSQL, Redis, SQLite',
|
|
70
|
+
'Docker containerization',
|
|
71
|
+
'Testing (Jest, Vitest)',
|
|
72
|
+
'CLI tools with Node.js',
|
|
73
|
+
'MCP server development',
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
module.exports = function(reg) {
|
|
79
|
+
|
|
80
|
+
reg('learn', 'Enter learning mode (python, csharp, node)', (args) => {
|
|
81
|
+
const mode = args[0]?.toLowerCase();
|
|
82
|
+
|
|
83
|
+
// No args — show available modes
|
|
84
|
+
if (!mode) {
|
|
85
|
+
console.log(ui.header('LEARNING MODES'));
|
|
86
|
+
console.log('');
|
|
87
|
+
for (const [key, m] of Object.entries(MODES)) {
|
|
88
|
+
const active = sessionState.learningMode === key ? ' ' + ui.success('ACTIVE') : '';
|
|
89
|
+
console.log(' ' + ui.label(m.name, m.description) + active);
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(ui.dim('Start: /learn python'));
|
|
93
|
+
console.log(ui.dim('Stop: /learn off'));
|
|
94
|
+
console.log(ui.dim('Topics: /learn python topics'));
|
|
95
|
+
console.log('');
|
|
96
|
+
if (sessionState.learningMode) {
|
|
97
|
+
console.log(ui.success(`Currently in ${MODES[sessionState.learningMode].name} mode`));
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Turn off learning mode
|
|
103
|
+
if (mode === 'off' || mode === 'stop') {
|
|
104
|
+
sessionState.learningMode = null;
|
|
105
|
+
console.log(ui.success('Learning mode disabled. Back to normal agent.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Show topics for a mode
|
|
110
|
+
if (args[1] === 'topics' && MODES[mode]) {
|
|
111
|
+
const m = MODES[mode];
|
|
112
|
+
console.log(ui.header(`${m.name} TOPICS`));
|
|
113
|
+
m.topics.forEach((t, i) => console.log(` ${String(i + 1).padStart(2)}. ${t}`));
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(ui.dim(`Start with: /learn ${mode}`));
|
|
116
|
+
console.log(ui.dim(`Then ask: "teach me about ${m.topics[0].toLowerCase()}"`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Activate mode
|
|
121
|
+
if (!MODES[mode]) {
|
|
122
|
+
console.log(ui.error(`Unknown mode: ${mode}. Options: python, csharp, node`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sessionState.learningMode = mode;
|
|
127
|
+
const m = MODES[mode];
|
|
128
|
+
console.log(ui.header(`${m.name} LEARNING MODE`));
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(ui.success(`${m.name} mode activated. Your AI tutor is ready.`));
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(ui.dim('Topics covered:'));
|
|
133
|
+
m.topics.forEach((t, i) => console.log(` ${String(i + 1).padStart(2)}. ${t}`));
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(ui.dim(`Just type naturally: "teach me about ${m.topics[0].toLowerCase()}"`));
|
|
136
|
+
console.log(ui.dim('Exit: /learn off'));
|
|
137
|
+
}, { category: 'LEARNING', subs: ['python', 'csharp', 'node', 'off', 'stop'] });
|
|
138
|
+
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports.MODES = MODES;
|
package/lib/ui.js
CHANGED
|
@@ -3,54 +3,145 @@
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { style } = require('./theme');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const FOOTER_TEXT = 'Designed by Lee Akpareva MBA, MA';
|
|
7
|
+
|
|
8
|
+
// Centered ASCII banner with session panel
|
|
7
9
|
function banner() {
|
|
8
|
-
const cols = process.stdout.columns ||
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const w = 57;
|
|
16
|
-
const lpad = Math.max(0, Math.floor((cols - w) / 2));
|
|
17
|
-
const sp = ' '.repeat(lpad);
|
|
18
|
-
const topBorder = sp + style('border', '╭' + '─'.repeat(w) + '╮');
|
|
19
|
-
const botBorder = sp + style('border', '╰' + '─'.repeat(w) + '╯');
|
|
20
|
-
const side = style('border', '│');
|
|
10
|
+
const cols = process.stdout.columns || 100;
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
const ver = pkg.version;
|
|
13
|
+
|
|
14
|
+
// Calculate layout
|
|
15
|
+
const leftWidth = Math.max(40, Math.floor(cols * 0.6));
|
|
16
|
+
const rightWidth = Math.max(30, cols - leftWidth - 3);
|
|
21
17
|
|
|
22
18
|
const lines = [
|
|
23
|
-
style('banner1', '███╗ ██╗ █████╗ ██╗ ██╗ █████╗ ██████╗ █████╗ '),
|
|
24
|
-
style('banner1', '████╗ ██║██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔══██╗'),
|
|
25
|
-
style('banner1', '██╔██╗ ██║███████║██║ ██║███████║██║ ██║███████║'),
|
|
26
|
-
style('banner2', '██║╚██╗██║██╔══██║╚██╗ ██╔╝██╔══██║██║ ██║██╔══██║'),
|
|
27
|
-
style('banner2', '██║ ╚████║██║ ██║ ╚████╔╝ ██║ ██║██████╔╝██║ ██║'),
|
|
28
|
-
style('banner3', '╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝'),
|
|
19
|
+
style('banner1', ' ███╗ ██╗ █████╗ ██╗ ██╗ █████╗ ██████╗ █████╗ '),
|
|
20
|
+
style('banner1', ' ████╗ ██║██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔══██╗'),
|
|
21
|
+
style('banner1', ' ██╔██╗ ██║███████║██║ ██║███████║██║ ██║███████║'),
|
|
22
|
+
style('banner2', ' ██║╚██╗██║██╔══██║╚██╗ ██╔╝██╔══██║██║ ██║██╔══██║'),
|
|
23
|
+
style('banner2', ' ██║ ╚████║██║ ██║ ╚████╔╝ ██║ ██║██████╔╝██║ ██║'),
|
|
24
|
+
style('banner3', ' ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝'),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
let out = '\n';
|
|
28
|
+
|
|
29
|
+
// Top border
|
|
30
|
+
out += style('border', '╭' + '─'.repeat(leftWidth) + '┬' + '─'.repeat(rightWidth) + '╮') + '\n';
|
|
31
|
+
|
|
32
|
+
// Banner lines (left) + Session panel (right)
|
|
33
|
+
const rightLines = sessionPanel(rightWidth);
|
|
34
|
+
|
|
35
|
+
const totalLines = Math.max(lines.length + 3, rightLines.length);
|
|
36
|
+
const leftContent = [
|
|
37
|
+
'',
|
|
38
|
+
...lines,
|
|
39
|
+
'',
|
|
40
|
+
' ' + style('header', 'E D G E N E T W O R K') + ' ' + style('dim', `v${ver}`),
|
|
41
|
+
'',
|
|
42
|
+
' ' + style('dim', 'AI Infrastructure Agent — Full Stack Terminal'),
|
|
43
|
+
'',
|
|
29
44
|
];
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
for (let i = 0; i < totalLines; i++) {
|
|
47
|
+
const left = leftContent[i] || '';
|
|
48
|
+
const right = rightLines[i] || '';
|
|
49
|
+
|
|
50
|
+
const leftStripped = left.replace(/\x1b\[[0-9;]*m/g, '');
|
|
51
|
+
const rightStripped = right.replace(/\x1b\[[0-9;]*m/g, '');
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
out += sp + side + ' '.repeat(innerPad) + l + ' '.repeat(Math.max(0, rightPad)) + side + '\n';
|
|
53
|
+
const leftPad = Math.max(0, leftWidth - leftStripped.length);
|
|
54
|
+
const rightPad = Math.max(0, rightWidth - rightStripped.length);
|
|
55
|
+
|
|
56
|
+
out += style('border', '│') + left + ' '.repeat(leftPad);
|
|
57
|
+
out += style('border', '│') + right + ' '.repeat(rightPad);
|
|
58
|
+
out += style('border', '│') + '\n';
|
|
41
59
|
}
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
out +=
|
|
48
|
-
out += sp + side + ' '.repeat(w) + side + '\n';
|
|
49
|
-
out += botBorder + '\n';
|
|
60
|
+
|
|
61
|
+
// Bottom border
|
|
62
|
+
out += style('border', '╰' + '─'.repeat(leftWidth) + '┴' + '─'.repeat(rightWidth) + '╯') + '\n';
|
|
63
|
+
|
|
64
|
+
// Footer
|
|
65
|
+
out += footerBar(cols);
|
|
50
66
|
|
|
51
67
|
return out;
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
function sessionPanel(width) {
|
|
71
|
+
let state;
|
|
72
|
+
try { state = require('./agent').sessionState; } catch { state = {}; }
|
|
73
|
+
const config = require('./config');
|
|
74
|
+
|
|
75
|
+
const provider = state.provider || 'Grok (free)';
|
|
76
|
+
const model = config.getModel() || 'auto';
|
|
77
|
+
const tier = config.getTier() || 'FREE';
|
|
78
|
+
const theme = config.getTheme() || 'dark';
|
|
79
|
+
const hasKey = config.getApiKey() || config.get('anthropicKey') || config.get('openaiKey');
|
|
80
|
+
const mode = state.learningMode;
|
|
81
|
+
|
|
82
|
+
const w = width - 2; // inner padding
|
|
83
|
+
const sectionLine = (title) => ' ' + style('dim', title + ' ' + '─'.repeat(Math.max(0, w - title.length - 2)));
|
|
84
|
+
const kvLine = (k, v) => ' ' + style('label', k) + ' ' + style('value', v);
|
|
85
|
+
|
|
86
|
+
const lines = [
|
|
87
|
+
'',
|
|
88
|
+
sectionLine('Session'),
|
|
89
|
+
kvLine(' Provider:', provider),
|
|
90
|
+
kvLine(' Model: ', model),
|
|
91
|
+
kvLine(' Tier: ', tier),
|
|
92
|
+
'',
|
|
93
|
+
sectionLine('Token Usage'),
|
|
94
|
+
kvLine(' Total: ', String(state.tokens?.total || 0)),
|
|
95
|
+
kvLine(' Cost: ', `$${(state.cost || 0).toFixed(4)}`),
|
|
96
|
+
'',
|
|
97
|
+
sectionLine('Configuration'),
|
|
98
|
+
kvLine(' Theme: ', theme),
|
|
99
|
+
kvLine(' API Key: ', hasKey ? style('success', 'set') : style('warn', 'free tier')),
|
|
100
|
+
kvLine(' Python: ', detectPython()),
|
|
101
|
+
'',
|
|
102
|
+
sectionLine('Learning Mode'),
|
|
103
|
+
kvLine(' Active: ', mode ? style('accent', mode) : style('dim', 'none')),
|
|
104
|
+
' ' + style('dim', '/learn python | csharp | node'),
|
|
105
|
+
'',
|
|
106
|
+
sectionLine('Quick Start'),
|
|
107
|
+
' ' + style('dim', '/help — all commands'),
|
|
108
|
+
' ' + style('dim', '/doctor — test connections'),
|
|
109
|
+
' ' + style('dim', '/status — network health'),
|
|
110
|
+
' ' + style('dim', '/login — set API key'),
|
|
111
|
+
'',
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function detectPython() {
|
|
118
|
+
try {
|
|
119
|
+
const { execSync } = require('child_process');
|
|
120
|
+
const py = process.platform === 'win32' ? 'python' : 'python3';
|
|
121
|
+
const ver = execSync(`${py} --version`, { timeout: 3000, encoding: 'utf-8' }).trim();
|
|
122
|
+
return style('success', ver.replace('Python ', ''));
|
|
123
|
+
} catch {
|
|
124
|
+
return style('offline', 'not found');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function footerBar(width) {
|
|
129
|
+
const w = width || process.stdout.columns || 100;
|
|
130
|
+
const keys = [
|
|
131
|
+
['Ctrl+C', 'quit'],
|
|
132
|
+
['Tab', 'autocomplete'],
|
|
133
|
+
['/help', 'commands'],
|
|
134
|
+
['/learn', 'tutorials'],
|
|
135
|
+
];
|
|
136
|
+
const keyStr = keys.map(([k, v]) => style('accent', k) + ' ' + style('dim', v)).join(' ');
|
|
137
|
+
const credit = style('dim', FOOTER_TEXT);
|
|
138
|
+
const creditStripped = FOOTER_TEXT.length;
|
|
139
|
+
const keysStripped = keys.map(([k, v]) => k + ' ' + v).join(' ').length;
|
|
140
|
+
const gap = Math.max(2, w - keysStripped - creditStripped - 4);
|
|
141
|
+
|
|
142
|
+
return style('border', '─'.repeat(w)) + '\n' + ' ' + keyStr + ' '.repeat(gap) + credit + '\n';
|
|
143
|
+
}
|
|
144
|
+
|
|
54
145
|
const DIVIDER_CHAR = '─';
|
|
55
146
|
|
|
56
147
|
function divider(width) {
|
|
@@ -83,7 +174,11 @@ function warn(msg) { return style('warn', ` ! ${msg}`); }
|
|
|
83
174
|
function dim(msg) { return style('dim', ` ${msg}`); }
|
|
84
175
|
|
|
85
176
|
function prompt() {
|
|
86
|
-
|
|
177
|
+
let state;
|
|
178
|
+
try { state = require('./agent').sessionState; } catch { state = {}; }
|
|
179
|
+
const mode = state?.learningMode;
|
|
180
|
+
const modeTag = mode ? style('accent', `[${mode}]`) + ' ' : '';
|
|
181
|
+
return modeTag + style('dim', 'navada') + style('accent', '> ');
|
|
87
182
|
}
|
|
88
183
|
|
|
89
184
|
function box(title, content) {
|
|
@@ -123,4 +218,4 @@ function progressBar(current, total, width = 30) {
|
|
|
123
218
|
return ` ${bar} ${style('value', Math.round(pct * 100) + '%')}`;
|
|
124
219
|
}
|
|
125
220
|
|
|
126
|
-
module.exports = { banner, divider, header, label, online, cmd, error, success, warn, dim, prompt, box, jsonColorize, progressBar };
|
|
221
|
+
module.exports = { banner, divider, header, label, online, cmd, error, success, warn, dim, prompt, box, jsonColorize, progressBar, footerBar, sessionPanel, FOOTER_TEXT };
|
package/package.json
CHANGED