navada-edge-cli 3.2.0 → 3.4.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.
@@ -0,0 +1,171 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" width="800" height="500">
2
+ <style>
3
+ text { font-family: 'SF Mono', 'Fira Code', monospace; fill: #e0e0e0; }
4
+ .label { font-size: 9px; fill: #666; }
5
+ .title { font-size: 11px; font-weight: bold; }
6
+ .small { font-size: 8px; fill: #888; }
7
+ .accent { fill: #a855f7; }
8
+ .box { fill: #0a0a0a; stroke: #1a1a1a; stroke-width: 1; }
9
+ .box-accent { fill: #0a0a0a; stroke: #a855f7; stroke-width: 1; }
10
+ .line { stroke: #333; stroke-width: 1; fill: none; }
11
+ .line-accent { stroke: #a855f7; stroke-width: 1; fill: none; }
12
+ .arrow { fill: #a855f7; }
13
+ </style>
14
+
15
+ <rect width="800" height="500" fill="#0a0a0a"/>
16
+
17
+ <!-- Title -->
18
+ <text x="400" y="24" text-anchor="middle" font-size="13" font-weight="bold" fill="#e0e0e0">NAVADA Edge CLI — Architecture</text>
19
+ <text x="400" y="38" text-anchor="middle" class="label">v3.3.0</text>
20
+
21
+ <!-- USER -->
22
+ <rect x="20" y="60" width="110" height="40" rx="2" class="box-accent"/>
23
+ <text x="75" y="78" text-anchor="middle" class="title">USER</text>
24
+ <text x="75" y="90" text-anchor="middle" class="label">Terminal</text>
25
+
26
+ <!-- Arrow: User -> CLI -->
27
+ <line x1="130" y1="80" x2="178" y2="80" class="line-accent"/>
28
+ <polygon points="178,76 186,80 178,84" class="arrow"/>
29
+
30
+ <!-- CLI Core -->
31
+ <rect x="188" y="55" width="140" height="50" rx="2" class="box-accent"/>
32
+ <text x="258" y="76" text-anchor="middle" class="title" fill="#a855f7">NAVADA Edge CLI</text>
33
+ <text x="258" y="90" text-anchor="middle" class="label">agent.md + config</text>
34
+
35
+ <!-- Three output arrows from CLI -->
36
+ <!-- Arrow: CLI -> AI Router -->
37
+ <line x1="328" y1="70" x2="410" y2="70" class="line-accent"/>
38
+ <line x1="410" y1="70" x2="410" y2="140" class="line-accent"/>
39
+ <polygon points="406,140 410,148 414,140" class="arrow"/>
40
+
41
+ <!-- Arrow: CLI -> Tool Engine -->
42
+ <line x1="328" y1="80" x2="540" y2="80" class="line-accent"/>
43
+ <line x1="540" y1="80" x2="540" y2="140" class="line-accent"/>
44
+ <polygon points="536,140 540,148 544,140" class="arrow"/>
45
+
46
+ <!-- Arrow: CLI -> Edge Network -->
47
+ <line x1="328" y1="90" x2="680" y2="90" class="line-accent"/>
48
+ <line x1="680" y1="90" x2="680" y2="140" class="line-accent"/>
49
+ <polygon points="676,140 680,148 684,140" class="arrow"/>
50
+
51
+ <!-- === COLUMN 1: AI Router === -->
52
+ <rect x="340" y="150" width="140" height="36" rx="2" class="box-accent"/>
53
+ <text x="410" y="170" text-anchor="middle" class="title">AI Router</text>
54
+ <text x="410" y="180" text-anchor="middle" class="label">model selection</text>
55
+
56
+ <!-- Provider grid: 3x2 -->
57
+ <rect x="310" y="200" width="90" height="28" rx="2" class="box"/>
58
+ <text x="355" y="215" text-anchor="middle" font-size="9" fill="#a855f7">NAVADA Free</text>
59
+
60
+ <rect x="410" y="200" width="90" height="28" rx="2" class="box"/>
61
+ <text x="455" y="215" text-anchor="middle" font-size="9">Anthropic</text>
62
+
63
+ <rect x="310" y="236" width="90" height="28" rx="2" class="box"/>
64
+ <text x="355" y="251" text-anchor="middle" font-size="9">OpenAI</text>
65
+
66
+ <rect x="410" y="236" width="90" height="28" rx="2" class="box"/>
67
+ <text x="455" y="251" text-anchor="middle" font-size="9">Gemini</text>
68
+
69
+ <rect x="310" y="272" width="90" height="28" rx="2" class="box"/>
70
+ <text x="355" y="287" text-anchor="middle" font-size="9">NVIDIA</text>
71
+
72
+ <rect x="410" y="272" width="90" height="28" rx="2" class="box"/>
73
+ <text x="455" y="287" text-anchor="middle" font-size="9">HuggingFace</text>
74
+
75
+ <!-- Connector lines from router to providers -->
76
+ <line x1="410" y1="186" x2="355" y2="200" class="line"/>
77
+ <line x1="410" y1="186" x2="455" y2="200" class="line"/>
78
+ <line x1="355" y1="228" x2="355" y2="236" class="line"/>
79
+ <line x1="455" y1="228" x2="455" y2="236" class="line"/>
80
+ <line x1="355" y1="264" x2="355" y2="272" class="line"/>
81
+ <line x1="455" y1="264" x2="455" y2="272" class="line"/>
82
+
83
+ <!-- === COLUMN 2: Tool Engine === -->
84
+ <rect x="510" y="150" width="120" height="36" rx="2" class="box-accent"/>
85
+ <text x="570" y="170" text-anchor="middle" class="title">Tool Engine</text>
86
+ <text x="570" y="180" text-anchor="middle" class="label">execution layer</text>
87
+
88
+ <!-- Tools: Local (left col) + Network (right col) -->
89
+ <text x="540" y="207" text-anchor="middle" class="label">LOCAL</text>
90
+ <text x="620" y="207" text-anchor="middle" class="label">NETWORK</text>
91
+
92
+ <rect x="510" y="214" width="56" height="24" rx="2" class="box"/>
93
+ <text x="538" y="229" text-anchor="middle" font-size="8">Shell</text>
94
+
95
+ <rect x="510" y="244" width="56" height="24" rx="2" class="box"/>
96
+ <text x="538" y="259" text-anchor="middle" font-size="8">Files</text>
97
+
98
+ <rect x="510" y="274" width="56" height="24" rx="2" class="box"/>
99
+ <text x="538" y="289" text-anchor="middle" font-size="8">Python</text>
100
+
101
+ <rect x="510" y="304" width="56" height="24" rx="2" class="box"/>
102
+ <text x="538" y="319" text-anchor="middle" font-size="8">Docker</text>
103
+
104
+ <rect x="576" y="214" width="56" height="24" rx="2" class="box"/>
105
+ <text x="604" y="229" text-anchor="middle" font-size="8">MCP</text>
106
+
107
+ <rect x="576" y="244" width="56" height="24" rx="2" class="box"/>
108
+ <text x="604" y="259" text-anchor="middle" font-size="8">SSH</text>
109
+
110
+ <rect x="576" y="274" width="56" height="24" rx="2" class="box"/>
111
+ <text x="604" y="289" text-anchor="middle" font-size="8">Email</text>
112
+
113
+ <rect x="576" y="304" width="56" height="24" rx="2" class="box"/>
114
+ <text x="604" y="319" text-anchor="middle" font-size="8">Registry</text>
115
+
116
+ <!-- Connector lines from engine to tools -->
117
+ <line x1="550" y1="186" x2="538" y2="214" class="line"/>
118
+ <line x1="590" y1="186" x2="604" y2="214" class="line"/>
119
+
120
+ <!-- === COLUMN 3: Edge Network === -->
121
+ <rect x="645" y="150" width="130" height="36" rx="2" class="box-accent"/>
122
+ <text x="710" y="170" text-anchor="middle" class="title">Edge Network</text>
123
+ <text x="710" y="180" text-anchor="middle" class="label">Tailscale mesh</text>
124
+
125
+ <!-- 4 Node boxes -->
126
+ <rect x="650" y="200" width="120" height="34" rx="2" class="box"/>
127
+ <text x="710" y="215" text-anchor="middle" font-size="9" fill="#a855f7">ASUS</text>
128
+ <text x="710" y="226" text-anchor="middle" class="small">Production Engine</text>
129
+
130
+ <rect x="650" y="242" width="120" height="34" rx="2" class="box"/>
131
+ <text x="710" y="257" text-anchor="middle" font-size="9">HP</text>
132
+ <text x="710" y="268" text-anchor="middle" class="small">Database (PG17)</text>
133
+
134
+ <rect x="650" y="284" width="120" height="34" rx="2" class="box"/>
135
+ <text x="710" y="299" text-anchor="middle" font-size="9">EC2</text>
136
+ <text x="710" y="310" text-anchor="middle" class="small">24/7 Monitoring</text>
137
+
138
+ <rect x="650" y="326" width="120" height="34" rx="2" class="box"/>
139
+ <text x="710" y="341" text-anchor="middle" font-size="9">Oracle</text>
140
+ <text x="710" y="352" text-anchor="middle" class="small">Infra + Tunnel</text>
141
+
142
+ <!-- Connector lines from network to nodes -->
143
+ <line x1="710" y1="186" x2="710" y2="200" class="line"/>
144
+ <line x1="710" y1="234" x2="710" y2="242" class="line"/>
145
+ <line x1="710" y1="276" x2="710" y2="284" class="line"/>
146
+ <line x1="710" y1="318" x2="710" y2="326" class="line"/>
147
+
148
+ <!-- Mesh lines between nodes (subtle) -->
149
+ <line x1="650" y1="217" x2="645" y2="259" stroke="#1a1a1a" stroke-width="1" stroke-dasharray="2,3"/>
150
+ <line x1="650" y1="259" x2="645" y2="301" stroke="#1a1a1a" stroke-width="1" stroke-dasharray="2,3"/>
151
+ <line x1="650" y1="301" x2="645" y2="343" stroke="#1a1a1a" stroke-width="1" stroke-dasharray="2,3"/>
152
+
153
+ <!-- Bottom bar -->
154
+ <line x1="40" y1="400" x2="760" y2="400" stroke="#1a1a1a" stroke-width="1"/>
155
+
156
+ <!-- Legend -->
157
+ <text x="40" y="425" class="label">FLOW</text>
158
+ <line x1="70" y1="422" x2="100" y2="422" class="line-accent"/>
159
+ <polygon points="100,419 106,422 100,425" class="arrow"/>
160
+ <text x="112" y="425" class="small">data path</text>
161
+
162
+ <rect x="170" y="414" width="14" height="14" rx="2" class="box-accent"/>
163
+ <text x="190" y="425" class="small">core module</text>
164
+
165
+ <rect x="260" y="414" width="14" height="14" rx="2" class="box"/>
166
+ <text x="280" y="425" class="small">component</text>
167
+
168
+ <!-- Footer -->
169
+ <text x="400" y="470" text-anchor="middle" class="label">navada-edge-cli@3.3.0 | npm i -g navada-edge-cli | github.com/navada25/edge-cli</text>
170
+ <text x="400" y="484" text-anchor="middle" font-size="8" fill="#333">NAVADA Edge Network 2026</text>
171
+ </svg>
@@ -1,7 +1,7 @@
1
1
  services:
2
2
  cli:
3
3
  build: .
4
- image: navada-edge-cli:3.2.0
4
+ image: navada-edge-cli:3.3.0
5
5
  container_name: navada-edge-cli
6
6
  restart: always
7
7
  stdin_open: true
package/lib/agent.js CHANGED
@@ -79,8 +79,8 @@ function getSystemPrompt() {
79
79
  // Session state — exposed for UI panels
80
80
  // ---------------------------------------------------------------------------
81
81
  const sessionState = {
82
- provider: 'Grok (free)',
83
- model: 'grok-3-mini',
82
+ provider: 'NAVADA (free)',
83
+ model: 'gpt-4o-mini',
84
84
  tokens: { input: 0, output: 0, total: 0 },
85
85
  cost: 0,
86
86
  messages: 0,
@@ -335,7 +335,7 @@ function streamFreeTier(endpoint, messages) {
335
335
  const req = transport.request(url, {
336
336
  method: 'POST',
337
337
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
338
- timeout: endpoint.includes('navada-edge-server.uk') ? 30000 : 5000,
338
+ timeout: endpoint.includes('navada-edge-server.uk') ? 120000 : 10000,
339
339
  }, (res) => {
340
340
  // If server doesn't support streaming, collect full response
341
341
  if (!res.headers['content-type']?.includes('text/event-stream')) {
@@ -373,10 +373,13 @@ function streamFreeTier(endpoint, messages) {
373
373
  if (data === '[DONE]') continue;
374
374
  try {
375
375
  const parsed = JSON.parse(data);
376
- const delta = parsed.choices?.[0]?.delta?.content || '';
377
- if (delta) {
378
- process.stdout.write(delta);
379
- fullContent += delta;
376
+ const delta = parsed.choices?.[0]?.delta;
377
+ // Grok-3-mini streams reasoning_content first, then content — skip reasoning
378
+ if (delta?.reasoning_content && !delta?.content) continue;
379
+ const text = delta?.content || '';
380
+ if (text) {
381
+ process.stdout.write(text);
382
+ fullContent += text;
380
383
  }
381
384
  } catch {}
382
385
  }
@@ -579,6 +582,72 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
579
582
  });
580
583
  }
581
584
 
585
+ // ---------------------------------------------------------------------------
586
+ // Streaming — Google Gemini API (gemini-2.0-flash)
587
+ // ---------------------------------------------------------------------------
588
+ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
589
+ return new Promise((resolve, reject) => {
590
+ const contents = messages.map(m => ({
591
+ role: m.role === 'assistant' ? 'model' : 'user',
592
+ parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
593
+ }));
594
+
595
+ const body = JSON.stringify({
596
+ contents,
597
+ generationConfig: { maxOutputTokens: 4096 },
598
+ systemInstruction: { parts: [{ text: getSystemPrompt() }] },
599
+ });
600
+
601
+ const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${key}`);
602
+
603
+ const req = https.request(url, {
604
+ method: 'POST',
605
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
606
+ timeout: 120000,
607
+ }, (res) => {
608
+ if (res.statusCode !== 200) {
609
+ let data = '';
610
+ res.on('data', c => data += c);
611
+ res.on('end', () => reject(new Error(`Gemini API error ${res.statusCode}: ${data.slice(0, 200)}`)));
612
+ return;
613
+ }
614
+
615
+ let buffer = '';
616
+ let fullContent = '';
617
+
618
+ res.on('data', (chunk) => {
619
+ buffer += chunk.toString();
620
+ const lines = buffer.split('\n');
621
+ buffer = lines.pop();
622
+
623
+ for (const line of lines) {
624
+ if (!line.startsWith('data: ')) continue;
625
+ const data = line.slice(6).trim();
626
+ if (!data) continue;
627
+ try {
628
+ const parsed = JSON.parse(data);
629
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text || '';
630
+ if (text) {
631
+ process.stdout.write(text);
632
+ fullContent += text;
633
+ }
634
+ } catch {}
635
+ }
636
+ });
637
+
638
+ res.on('end', () => {
639
+ if (fullContent) process.stdout.write('\n');
640
+ resolve({ content: fullContent });
641
+ });
642
+ });
643
+
644
+ req.on('error', reject);
645
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
646
+ req.write(body);
647
+ req.end();
648
+ });
649
+ }
650
+
582
651
  function openAITools() {
583
652
  const defs = [
584
653
  { name: 'shell', description: 'Execute a shell command on the user\'s machine', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
@@ -662,12 +731,14 @@ async function chat(userMessage, conversationHistory = []) {
662
731
  const anthropicKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY || '';
663
732
  const openaiKey = config.get('openaiKey') || process.env.OPENAI_API_KEY || '';
664
733
  const nvidiaKey = config.get('nvidiaKey') || process.env.NVIDIA_API_KEY || '';
734
+ const geminiKey = config.get('geminiKey') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
665
735
  const apiKey = config.getApiKey() || '';
666
736
 
667
737
  // Determine which provider to use
668
738
  const effectiveAnthropicKey = anthropicKey || (apiKey.startsWith('sk-ant') ? apiKey : '');
669
739
  const effectiveOpenAIKey = openaiKey || (apiKey.startsWith('sk-') && !apiKey.startsWith('sk-ant') ? apiKey : '');
670
740
  const effectiveNvidiaKey = nvidiaKey || (apiKey.startsWith('nvapi-') ? apiKey : '');
741
+ const effectiveGeminiKey = geminiKey || (apiKey.startsWith('AIza') ? apiKey : '');
671
742
 
672
743
  const modelPref = config.getModel();
673
744
  const intent = detectIntent(userMessage);
@@ -675,11 +746,12 @@ async function chat(userMessage, conversationHistory = []) {
675
746
  // Track active provider for UI
676
747
  if (effectiveAnthropicKey) sessionState.provider = 'Anthropic';
677
748
  else if (effectiveOpenAIKey) sessionState.provider = 'OpenAI';
749
+ else if (effectiveGeminiKey) sessionState.provider = 'Gemini';
678
750
  else if (effectiveNvidiaKey) sessionState.provider = 'NVIDIA';
679
751
  else sessionState.provider = 'Grok (free)';
680
752
 
681
753
  // No personal key — use free tier
682
- if (!effectiveAnthropicKey && !effectiveOpenAIKey && !effectiveNvidiaKey) {
754
+ if (!effectiveAnthropicKey && !effectiveOpenAIKey && !effectiveNvidiaKey && !effectiveGeminiKey) {
683
755
  if (intent === 'code' && navada.config.hfToken) {
684
756
  try {
685
757
  const r = await navada.ai.huggingface.qwen(userMessage);
@@ -689,6 +761,28 @@ async function chat(userMessage, conversationHistory = []) {
689
761
  return grokChat(userMessage, conversationHistory);
690
762
  }
691
763
 
764
+ // Gemini key — route to Gemini
765
+ if (effectiveGeminiKey && (!effectiveAnthropicKey || modelPref === 'gemini' || modelPref?.startsWith('gemini-'))) {
766
+ const geminiModel = config.get('geminiModel') || 'gemini-2.0-flash';
767
+ sessionState.provider = 'Gemini';
768
+ sessionState.model = geminiModel;
769
+ const messages = [
770
+ ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
771
+ { role: 'user', content: userMessage },
772
+ ];
773
+ process.stdout.write(ui.dim(' NAVADA > '));
774
+ try {
775
+ const result = await streamGemini(effectiveGeminiKey, messages, geminiModel);
776
+ return result.content;
777
+ } catch (e) {
778
+ if (!sessionState._geminiWarned) {
779
+ console.log(ui.warn('Gemini API unavailable, using Grok free tier.'));
780
+ sessionState._geminiWarned = true;
781
+ }
782
+ return grokChat(userMessage, conversationHistory);
783
+ }
784
+ }
785
+
692
786
  // NVIDIA key — route to NVIDIA
693
787
  if (effectiveNvidiaKey && (!effectiveAnthropicKey || modelPref?.startsWith('nvidia') || modelPref?.startsWith('llama') || modelPref?.startsWith('deepseek') || modelPref?.startsWith('mistral') || modelPref?.startsWith('gemma') || modelPref?.startsWith('nemotron'))) {
694
788
  const { streamNvidia } = require('./commands/nvidia');
@@ -3,7 +3,7 @@
3
3
  const navada = require('navada-edge-sdk');
4
4
  const ui = require('../ui');
5
5
  const config = require('../config');
6
- const { chat: agentChat, reportTelemetry, rateTracker, addToHistory, getConversationHistory, clearHistory } = require('../agent');
6
+ const { chat: agentChat, reportTelemetry, rateTracker, addToHistory, getConversationHistory, clearHistory, sessionState } = require('../agent');
7
7
 
8
8
  module.exports = function(reg) {
9
9
 
@@ -109,12 +109,14 @@ module.exports = function(reg) {
109
109
 
110
110
  reg('model', 'Show/set default AI model', (args) => {
111
111
  if (args[0]) {
112
- const valid = ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen', 'nvidia', 'llama-3.3-70b', 'llama-3.1-8b', 'mistral-large', 'gemma-2-27b', 'codellama-70b', 'deepseek-r1', 'phi-3-medium', 'nemotron-70b'];
112
+ const valid = ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'gemini', 'gemini-2.0-flash', 'gemini-2.5-pro', 'qwen', 'nvidia', 'llama-3.3-70b', 'llama-3.1-8b', 'mistral-large', 'gemma-2-27b', 'codellama-70b', 'deepseek-r1', 'phi-3-medium', 'nemotron-70b'];
113
113
  if (!valid.includes(args[0])) { console.log(ui.error(`Invalid model. Options: ${valid.join(', ')}`)); return; }
114
114
  config.setModel(args[0]);
115
115
  // If it's an NVIDIA model name, also set it as the nvidia model
116
116
  const nvidiaModels = ['llama-3.3-70b', 'llama-3.1-8b', 'mistral-large', 'gemma-2-27b', 'codellama-70b', 'deepseek-r1', 'phi-3-medium', 'nemotron-70b'];
117
117
  if (nvidiaModels.includes(args[0])) config.set('nvidiaModel', args[0]);
118
+ // If it's a Gemini model name, set it
119
+ if (args[0].startsWith('gemini')) config.set('geminiModel', args[0]);
118
120
  console.log(ui.success(`Model set to: ${args[0]}`));
119
121
  } else {
120
122
  console.log(ui.header('AI MODELS'));
@@ -125,6 +127,8 @@ module.exports = function(reg) {
125
127
  console.log(ui.label('auto', 'Smart routing — picks best provider per query'));
126
128
  console.log(ui.label('claude', 'Claude Sonnet 4 (Anthropic) — full agent + tools'));
127
129
  console.log(ui.label('gpt-4o', 'GPT-4o (OpenAI) — tool use + streaming'));
130
+ console.log(ui.label('gemini', 'Gemini 2.0 Flash (Google — FREE)'));
131
+ console.log(ui.label('gemini-2.5-pro', 'Gemini 2.5 Pro (Google)'));
128
132
  console.log(ui.label('qwen', 'Qwen Coder 32B (HuggingFace — FREE)'));
129
133
  console.log('');
130
134
  console.log(ui.dim('NVIDIA models (FREE via build.nvidia.com):'));
@@ -138,7 +142,61 @@ module.exports = function(reg) {
138
142
  console.log(ui.dim('Set: /model deepseek-r1'));
139
143
  console.log(ui.dim('NVIDIA key: /login nvapi-your-key (free at build.nvidia.com)'));
140
144
  }
141
- }, { category: 'AI', subs: ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'qwen', 'nvidia', 'llama-3.3-70b', 'deepseek-r1', 'mistral-large', 'codellama-70b', 'gemma-2-27b', 'nemotron-70b'] });
145
+ }, { category: 'AI', subs: ['auto', 'claude', 'gpt-4o', 'gpt-4o-mini', 'gemini', 'gemini-2.0-flash', 'gemini-2.5-pro', 'qwen', 'nvidia', 'llama-3.3-70b', 'deepseek-r1', 'mistral-large', 'codellama-70b', 'gemma-2-27b', 'nemotron-70b'] });
146
+
147
+ // --- /retry ---
148
+ reg('retry', 'Resend the last message to the AI', async () => {
149
+ const history = getConversationHistory();
150
+ const lastUserMsg = [...history].reverse().find(m => m.role === 'user');
151
+ if (!lastUserMsg) {
152
+ console.log(ui.warn('No previous message to retry.'));
153
+ return;
154
+ }
155
+
156
+ const msg = typeof lastUserMsg.content === 'string' ? lastUserMsg.content : JSON.stringify(lastUserMsg.content);
157
+ console.log(ui.dim(` Retrying: ${msg.slice(0, 80)}${msg.length > 80 ? '...' : ''}`));
158
+
159
+ const hasKey = config.getApiKey() || config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY;
160
+ let spinner;
161
+ if (!hasKey) {
162
+ process.stdout.write(ui.dim(' NAVADA > '));
163
+ } else {
164
+ const ora = require('ora');
165
+ spinner = ora({ text: ' NAVADA thinking...', color: 'white' }).start();
166
+ }
167
+
168
+ try {
169
+ // Remove the last assistant response + user message, then resend
170
+ const trimmedHistory = history.slice(0, -2);
171
+ const response = await agentChat(msg, trimmedHistory);
172
+ if (spinner) spinner.stop();
173
+ // Replace the last exchange in history
174
+ addToHistory('user', msg);
175
+ addToHistory('assistant', response);
176
+ } catch (e) {
177
+ if (spinner) spinner.stop();
178
+ console.log(ui.error(e.message));
179
+ }
180
+ }, { category: 'AI', aliases: ['r'] });
181
+
182
+ // --- /tokens ---
183
+ reg('tokens', 'Show session token usage and cost', () => {
184
+ console.log(ui.header('SESSION USAGE'));
185
+ const s = sessionState;
186
+ const uptime = ((Date.now() - s.startTime) / 60000).toFixed(1);
187
+ console.log(ui.label('Provider', s.provider));
188
+ console.log(ui.label('Model', s.model || config.getModel()));
189
+ console.log(ui.label('Messages', String(s.messages)));
190
+ console.log(ui.label('Input tokens', String(s.tokens.input)));
191
+ console.log(ui.label('Output tokens', String(s.tokens.output)));
192
+ console.log(ui.label('Total tokens', String(s.tokens.total)));
193
+ console.log(ui.label('Est. cost', `$${s.cost.toFixed(4)}`));
194
+ console.log(ui.label('Session time', `${uptime} min`));
195
+ console.log(ui.label('Rate limit', `${rateTracker.used()}/${rateTracker.limit} RPM`));
196
+ console.log('');
197
+ console.log(ui.dim('Free tier: Grok 3 via NAVADA Edge server'));
198
+ console.log(ui.dim('Upgrade: /login <key> for full agent + tools'));
199
+ }, { category: 'AI', aliases: ['usage'] });
142
200
 
143
201
  reg('research', 'RAG search via MCP', async (args) => {
144
202
  const query = args.join(' ');
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('../ui');
6
+ const config = require('../config');
7
+ const { getConversationHistory, clearHistory, addToHistory, sessionState } = require('../agent');
8
+
9
+ const CONV_DIR = path.join(config.CONFIG_DIR, 'conversations');
10
+
11
+ function ensureDir() {
12
+ if (!fs.existsSync(CONV_DIR)) fs.mkdirSync(CONV_DIR, { recursive: true });
13
+ }
14
+
15
+ function slugify(name) {
16
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
17
+ }
18
+
19
+ function listSaved() {
20
+ ensureDir();
21
+ const files = fs.readdirSync(CONV_DIR).filter(f => f.endsWith('.json'));
22
+ return files.map(f => {
23
+ try {
24
+ const data = JSON.parse(fs.readFileSync(path.join(CONV_DIR, f), 'utf-8'));
25
+ return {
26
+ file: f,
27
+ name: data.name || f.replace('.json', ''),
28
+ messages: data.messages?.length || 0,
29
+ provider: data.provider || 'unknown',
30
+ saved: data.savedAt || '',
31
+ };
32
+ } catch {
33
+ return { file: f, name: f.replace('.json', ''), messages: 0, provider: '?', saved: '' };
34
+ }
35
+ }).sort((a, b) => b.saved.localeCompare(a.saved));
36
+ }
37
+
38
+ module.exports = function(reg) {
39
+
40
+ reg('save', 'Save current conversation', (args) => {
41
+ const history = getConversationHistory();
42
+ if (history.length === 0) {
43
+ console.log(ui.warn('No conversation to save.'));
44
+ return;
45
+ }
46
+
47
+ const name = args.join(' ') || `session-${new Date().toISOString().slice(0, 16).replace(/[T:]/g, '-')}`;
48
+ const slug = slugify(name);
49
+ ensureDir();
50
+
51
+ const data = {
52
+ name,
53
+ provider: sessionState.provider,
54
+ model: sessionState.model,
55
+ messages: history,
56
+ messageCount: history.length,
57
+ tokens: sessionState.tokens,
58
+ savedAt: new Date().toISOString(),
59
+ };
60
+
61
+ const filePath = path.join(CONV_DIR, `${slug}.json`);
62
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
63
+ console.log(ui.success(`Conversation saved: ${name}`));
64
+ console.log(ui.label('Messages', String(history.length)));
65
+ console.log(ui.label('File', filePath));
66
+ console.log(ui.dim('Load it later: /load ' + slug));
67
+ }, { category: 'AI' });
68
+
69
+ reg('load', 'Load a saved conversation', (args) => {
70
+ const name = args.join(' ');
71
+ if (!name) {
72
+ console.log(ui.dim('Usage: /load <name>'));
73
+ console.log(ui.dim('List saved: /conversations'));
74
+ return;
75
+ }
76
+
77
+ ensureDir();
78
+ const slug = slugify(name);
79
+ const filePath = path.join(CONV_DIR, `${slug}.json`);
80
+
81
+ if (!fs.existsSync(filePath)) {
82
+ // Try partial match
83
+ const files = fs.readdirSync(CONV_DIR).filter(f => f.includes(slug) && f.endsWith('.json'));
84
+ if (files.length === 0) {
85
+ console.log(ui.error(`No saved conversation matching: ${name}`));
86
+ console.log(ui.dim('List saved: /conversations'));
87
+ return;
88
+ }
89
+ // Use first match
90
+ const matchPath = path.join(CONV_DIR, files[0]);
91
+ return loadConversation(matchPath);
92
+ }
93
+
94
+ loadConversation(filePath);
95
+ }, { category: 'AI' });
96
+
97
+ reg('conversations', 'List saved conversations', () => {
98
+ const saved = listSaved();
99
+ console.log(ui.header('SAVED CONVERSATIONS'));
100
+
101
+ if (saved.length === 0) {
102
+ console.log(ui.dim('No saved conversations yet.'));
103
+ console.log(ui.dim('Save one: /save my-project-chat'));
104
+ return;
105
+ }
106
+
107
+ for (const s of saved) {
108
+ const date = s.saved ? s.saved.slice(0, 10) : '?';
109
+ console.log(ui.label(
110
+ s.name.slice(0, 28).padEnd(28),
111
+ `${s.messages} msgs ${s.provider} ${date}`
112
+ ));
113
+ }
114
+
115
+ console.log('');
116
+ console.log(ui.dim(`${saved.length} conversations saved`));
117
+ console.log(ui.dim('Load: /load <name> | Delete: /conversations rm <name>'));
118
+ }, { category: 'AI', aliases: ['convos'], subs: ['rm'] });
119
+
120
+ function loadConversation(filePath) {
121
+ try {
122
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
123
+ clearHistory();
124
+
125
+ for (const msg of data.messages || []) {
126
+ addToHistory(msg.role, msg.content);
127
+ }
128
+
129
+ console.log(ui.success(`Loaded: ${data.name}`));
130
+ console.log(ui.label('Messages', String(data.messages?.length || 0)));
131
+ console.log(ui.label('Provider', data.provider || 'unknown'));
132
+ console.log(ui.label('Saved', data.savedAt?.slice(0, 16) || '?'));
133
+ console.log('');
134
+ console.log(ui.dim('Conversation context restored. Continue chatting.'));
135
+ } catch (e) {
136
+ console.log(ui.error(`Failed to load: ${e.message}`));
137
+ }
138
+ }
139
+ };