navada-edge-cli 2.5.0 → 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 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 || IDENTITY.personality,
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
- || config.getApiKey()
381
- || process.env.ANTHROPIC_API_KEY
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
- const apiKey = config.getApiKey();
385
- const effectiveKey = anthropicKey
386
- || (apiKey && apiKey.startsWith('sk-ant') ? apiKey : '')
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 (!effectiveKey) {
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
- // Has Anthropic key but could route to cheaper options for simple queries
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 = [
@@ -485,8 +715,19 @@ async function chat(userMessage, conversationHistory = []) {
485
715
  { role: 'user', content: userMessage },
486
716
  ];
487
717
 
488
- // Stream the response
489
- let response = await streamAnthropic(effectiveKey, messages, tools);
718
+ // Stream the response — fall back to Grok if Anthropic fails (billing, rate limit, etc.)
719
+ let response;
720
+ try {
721
+ response = await streamAnthropic(effectiveKey, messages, tools);
722
+ } catch (e) {
723
+ const errMsg = e.message || '';
724
+ // If billing/rate limit/auth error, fall back to free tier
725
+ if (errMsg.includes('400') || errMsg.includes('401') || errMsg.includes('429') || errMsg.includes('usage limits')) {
726
+ console.log(ui.warn('Anthropic API unavailable, falling back to Grok free tier...'));
727
+ return grokChat(userMessage, conversationHistory);
728
+ }
729
+ throw e;
730
+ }
490
731
 
491
732
  // Handle tool use loop
492
733
  let iterations = 0;
@@ -532,6 +773,9 @@ async function executeTool(name, input) {
532
773
  if (input.provider === 'dalle') return JSON.stringify(await navada.ai.openai.image(input.prompt));
533
774
  const { size } = await navada.cloudflare.flux.generate(input.prompt, { savePath: `navada-${Date.now()}.png` });
534
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);
535
779
  default: return `Unknown tool: ${name}`;
536
780
  }
537
781
  } catch (e) {
@@ -630,4 +874,4 @@ async function reportTelemetry(event, data = {}) {
630
874
  }
631
875
  }
632
876
 
633
- module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker };
877
+ module.exports = { IDENTITY, chat, localTools, reportTelemetry, fallbackChat, checkForUpdate, getUpdateInfo, rateTracker, sessionState };
@@ -17,6 +17,7 @@ const modules = [
17
17
  require('./keys'),
18
18
  require('./setup'),
19
19
  require('./system'),
20
+ require('./learn'),
20
21
  ];
21
22
 
22
23
  function loadAll() {
@@ -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
- // Centered ASCII banner with rounded box
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 || 80;
9
- const pad = (text) => {
10
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
11
- const left = Math.max(0, Math.floor((cols - stripped.length) / 2));
12
- return ' '.repeat(left) + text;
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
- const ver = require('../package.json').version;
32
- const titleLine = style('header', 'E D G E N E T W O R K') + ' ' + style('dim', `v${ver}`);
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
- let out = '\n' + topBorder + '\n';
35
- out += sp + side + ' '.repeat(w) + side + '\n';
36
- for (const l of lines) {
37
- const stripped = l.replace(/\x1b\[[0-9;]*m/g, '');
38
- const innerPad = Math.max(0, Math.floor((w - stripped.length) / 2));
39
- const rightPad = w - stripped.length - innerPad;
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
- out += sp + side + ' '.repeat(w) + side + '\n';
43
- // Title line centered
44
- const tStripped = titleLine.replace(/\x1b\[[0-9;]*m/g, '');
45
- const tPad = Math.max(0, Math.floor((w - tStripped.length) / 2));
46
- const tRight = w - tStripped.length - tPad;
47
- out += sp + side + ' '.repeat(tPad) + titleLine + ' '.repeat(Math.max(0, tRight)) + side + '\n';
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
- return style('dim', ' navada') + style('accent', '> ');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navada-edge-cli",
3
- "version": "2.5.0",
3
+ "version": "3.0.0",
4
4
  "description": "Interactive CLI for the NAVADA Edge Network — explore nodes, agents, Cloudflare, AI, Docker, and MCP from your terminal",
5
5
  "main": "lib/cli.js",
6
6
  "bin": {