navada-edge-cli 3.3.0 → 3.4.1

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>
package/lib/agent.js CHANGED
@@ -32,6 +32,8 @@ You also connect to the NAVADA Edge Network (4 nodes via Tailscale VPN):
32
32
  - send_email / generate_image: communications and AI image generation
33
33
  - founder_info: information about Lee Akpareva, the creator of NAVADA
34
34
  When users ask you to DO something — DO IT. Use write_file to create files. Use shell to run commands. Never say "I can't" when you have a tool for it.
35
+ When asked to generate diagrams — use write_file to create Mermaid (.mmd), SVG, or HTML files. You can also use python_exec with matplotlib/graphviz for complex diagrams.
36
+ When asked to create, edit, or delete files — use the file tools directly. You are a terminal agent with FULL access.
35
37
  Keep responses short. Code blocks when needed. No fluff.`,
36
38
  founder: {
37
39
  name: 'Leslie (Lee) Akpareva',
@@ -79,8 +81,8 @@ function getSystemPrompt() {
79
81
  // Session state — exposed for UI panels
80
82
  // ---------------------------------------------------------------------------
81
83
  const sessionState = {
82
- provider: 'Grok (free)',
83
- model: 'grok-3-mini',
84
+ provider: 'NAVADA (free)',
85
+ model: 'gpt-4o-mini',
84
86
  tokens: { input: 0, output: 0, total: 0 },
85
87
  cost: 0,
86
88
  messages: 0,
@@ -335,7 +337,7 @@ function streamFreeTier(endpoint, messages) {
335
337
  const req = transport.request(url, {
336
338
  method: 'POST',
337
339
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
338
- timeout: endpoint.includes('navada-edge-server.uk') ? 30000 : 5000,
340
+ timeout: endpoint.includes('navada-edge-server.uk') ? 120000 : 10000,
339
341
  }, (res) => {
340
342
  // If server doesn't support streaming, collect full response
341
343
  if (!res.headers['content-type']?.includes('text/event-stream')) {
@@ -373,10 +375,13 @@ function streamFreeTier(endpoint, messages) {
373
375
  if (data === '[DONE]') continue;
374
376
  try {
375
377
  const parsed = JSON.parse(data);
376
- const delta = parsed.choices?.[0]?.delta?.content || '';
377
- if (delta) {
378
- process.stdout.write(delta);
379
- fullContent += delta;
378
+ const delta = parsed.choices?.[0]?.delta;
379
+ // Grok-3-mini streams reasoning_content first, then content — skip reasoning
380
+ if (delta?.reasoning_content && !delta?.content) continue;
381
+ const text = delta?.content || '';
382
+ if (text) {
383
+ process.stdout.write(text);
384
+ fullContent += text;
380
385
  }
381
386
  } catch {}
382
387
  }
@@ -579,15 +584,84 @@ function streamOpenAI(key, messages, model = 'gpt-4o') {
579
584
  });
580
585
  }
581
586
 
587
+ // ---------------------------------------------------------------------------
588
+ // Streaming — Google Gemini API (gemini-2.0-flash)
589
+ // ---------------------------------------------------------------------------
590
+ function streamGemini(key, messages, model = 'gemini-2.0-flash') {
591
+ return new Promise((resolve, reject) => {
592
+ const contents = messages.map(m => ({
593
+ role: m.role === 'assistant' ? 'model' : 'user',
594
+ parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
595
+ }));
596
+
597
+ const body = JSON.stringify({
598
+ contents,
599
+ generationConfig: { maxOutputTokens: 4096 },
600
+ systemInstruction: { parts: [{ text: getSystemPrompt() }] },
601
+ });
602
+
603
+ const url = new URL(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${key}`);
604
+
605
+ const req = https.request(url, {
606
+ method: 'POST',
607
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
608
+ timeout: 120000,
609
+ }, (res) => {
610
+ if (res.statusCode !== 200) {
611
+ let data = '';
612
+ res.on('data', c => data += c);
613
+ res.on('end', () => reject(new Error(`Gemini API error ${res.statusCode}: ${data.slice(0, 200)}`)));
614
+ return;
615
+ }
616
+
617
+ let buffer = '';
618
+ let fullContent = '';
619
+
620
+ res.on('data', (chunk) => {
621
+ buffer += chunk.toString();
622
+ const lines = buffer.split('\n');
623
+ buffer = lines.pop();
624
+
625
+ for (const line of lines) {
626
+ if (!line.startsWith('data: ')) continue;
627
+ const data = line.slice(6).trim();
628
+ if (!data) continue;
629
+ try {
630
+ const parsed = JSON.parse(data);
631
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text || '';
632
+ if (text) {
633
+ process.stdout.write(text);
634
+ fullContent += text;
635
+ }
636
+ } catch {}
637
+ }
638
+ });
639
+
640
+ res.on('end', () => {
641
+ if (fullContent) process.stdout.write('\n');
642
+ resolve({ content: fullContent });
643
+ });
644
+ });
645
+
646
+ req.on('error', reject);
647
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
648
+ req.write(body);
649
+ req.end();
650
+ });
651
+ }
652
+
582
653
  function openAITools() {
583
654
  const defs = [
584
- { name: 'shell', description: 'Execute a shell command on the user\'s machine', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } },
585
- { name: 'read_file', description: 'Read a file', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
586
- { name: 'write_file', description: 'Write to a file', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
587
- { name: 'list_files', description: 'List directory contents', parameters: { type: 'object', properties: { path: { type: 'string' } } } },
588
- { name: 'system_info', description: 'Get system info (CPU, RAM, OS)', parameters: { type: 'object', properties: {} } },
589
- { name: 'python_exec', description: 'Execute Python code', parameters: { type: 'object', properties: { code: { type: 'string' } }, required: ['code'] } },
590
- { name: 'python_pip', description: 'Install a Python package', parameters: { type: 'object', properties: { package: { type: 'string' } }, required: ['package'] } },
655
+ { name: 'shell', description: 'Execute a shell command on the user\'s machine. Use for: file operations, git, npm, docker, system commands, creating directories, running scripts.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to run' } }, required: ['command'] } },
656
+ { name: 'read_file', description: 'Read the contents of a file on the user\'s machine.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute or relative file path' } }, required: ['path'] } },
657
+ { name: 'write_file', description: 'Write content to a file. Creates parent directories if needed. Use for creating new files, scripts, configs, diagrams (Mermaid, SVG, HTML), code files.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to write' }, content: { type: 'string', description: 'Full content to write to the file' } }, required: ['path', 'content'] } },
658
+ { name: 'list_files', description: 'List files and directories.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path (default: current dir)' } } } },
659
+ { name: 'system_info', description: 'Get local system information (CPU, RAM, disk, OS, hostname).', parameters: { type: 'object', properties: {} } },
660
+ { name: 'python_exec', description: 'Execute Python code inline. Use for data analysis, calculations, generating content, processing files, ML tasks.', parameters: { type: 'object', properties: { code: { type: 'string', description: 'Python code to execute' } }, required: ['code'] } },
661
+ { name: 'python_pip', description: 'Install a Python package via pip.', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } },
662
+ { name: 'python_script', description: 'Run a Python script file.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to .py file' } }, required: ['path'] } },
663
+ { name: 'sandbox_run', description: 'Run code in an isolated sandbox with syntax highlighting. Supports javascript, python, typescript.', parameters: { type: 'object', properties: { code: { type: 'string' }, language: { type: 'string', description: 'javascript, python, or typescript' } }, required: ['code'] } },
664
+ { name: 'founder_info', description: 'Get information about Lee Akpareva, founder of NAVADA Edge.', parameters: { type: 'object', properties: {} } },
591
665
  ];
592
666
  return defs.map(d => ({ type: 'function', function: d }));
593
667
  }
@@ -662,12 +736,14 @@ async function chat(userMessage, conversationHistory = []) {
662
736
  const anthropicKey = config.get('anthropicKey') || process.env.ANTHROPIC_API_KEY || '';
663
737
  const openaiKey = config.get('openaiKey') || process.env.OPENAI_API_KEY || '';
664
738
  const nvidiaKey = config.get('nvidiaKey') || process.env.NVIDIA_API_KEY || '';
739
+ const geminiKey = config.get('geminiKey') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
665
740
  const apiKey = config.getApiKey() || '';
666
741
 
667
742
  // Determine which provider to use
668
743
  const effectiveAnthropicKey = anthropicKey || (apiKey.startsWith('sk-ant') ? apiKey : '');
669
744
  const effectiveOpenAIKey = openaiKey || (apiKey.startsWith('sk-') && !apiKey.startsWith('sk-ant') ? apiKey : '');
670
745
  const effectiveNvidiaKey = nvidiaKey || (apiKey.startsWith('nvapi-') ? apiKey : '');
746
+ const effectiveGeminiKey = geminiKey || (apiKey.startsWith('AIza') ? apiKey : '');
671
747
 
672
748
  const modelPref = config.getModel();
673
749
  const intent = detectIntent(userMessage);
@@ -675,11 +751,12 @@ async function chat(userMessage, conversationHistory = []) {
675
751
  // Track active provider for UI
676
752
  if (effectiveAnthropicKey) sessionState.provider = 'Anthropic';
677
753
  else if (effectiveOpenAIKey) sessionState.provider = 'OpenAI';
754
+ else if (effectiveGeminiKey) sessionState.provider = 'Gemini';
678
755
  else if (effectiveNvidiaKey) sessionState.provider = 'NVIDIA';
679
756
  else sessionState.provider = 'Grok (free)';
680
757
 
681
758
  // No personal key — use free tier
682
- if (!effectiveAnthropicKey && !effectiveOpenAIKey && !effectiveNvidiaKey) {
759
+ if (!effectiveAnthropicKey && !effectiveOpenAIKey && !effectiveNvidiaKey && !effectiveGeminiKey) {
683
760
  if (intent === 'code' && navada.config.hfToken) {
684
761
  try {
685
762
  const r = await navada.ai.huggingface.qwen(userMessage);
@@ -689,6 +766,28 @@ async function chat(userMessage, conversationHistory = []) {
689
766
  return grokChat(userMessage, conversationHistory);
690
767
  }
691
768
 
769
+ // Gemini key — route to Gemini
770
+ if (effectiveGeminiKey && (!effectiveAnthropicKey || modelPref === 'gemini' || modelPref?.startsWith('gemini-'))) {
771
+ const geminiModel = config.get('geminiModel') || 'gemini-2.0-flash';
772
+ sessionState.provider = 'Gemini';
773
+ sessionState.model = geminiModel;
774
+ const messages = [
775
+ ...conversationHistory.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
776
+ { role: 'user', content: userMessage },
777
+ ];
778
+ process.stdout.write(ui.dim(' NAVADA > '));
779
+ try {
780
+ const result = await streamGemini(effectiveGeminiKey, messages, geminiModel);
781
+ return result.content;
782
+ } catch (e) {
783
+ if (!sessionState._geminiWarned) {
784
+ console.log(ui.warn('Gemini API unavailable, using Grok free tier.'));
785
+ sessionState._geminiWarned = true;
786
+ }
787
+ return grokChat(userMessage, conversationHistory);
788
+ }
789
+ }
790
+
692
791
  // NVIDIA key — route to NVIDIA
693
792
  if (effectiveNvidiaKey && (!effectiveAnthropicKey || modelPref?.startsWith('nvidia') || modelPref?.startsWith('llama') || modelPref?.startsWith('deepseek') || modelPref?.startsWith('mistral') || modelPref?.startsWith('gemma') || modelPref?.startsWith('nemotron'))) {
694
793
  const { streamNvidia } = require('./commands/nvidia');
@@ -903,13 +1002,72 @@ async function grokChat(userMessage, conversationHistory = []) {
903
1002
  { role: 'user', content: userMessage },
904
1003
  ];
905
1004
 
906
- // Try streaming first
907
- const result = await callFreeTier(messages, true);
908
- if (result.streamed) {
909
- // Already printed to stdout, return for history
1005
+ // Send tools with the request — free tier now supports tool use
1006
+ const tools = openAITools();
1007
+ const endpoint = FREE_TIER_ENDPOINTS[0];
1008
+
1009
+ // Non-streaming request with tools (streaming + tools is complex, use non-streaming for tool calls)
1010
+ let response;
1011
+ try {
1012
+ const r = await navada.request(endpoint, {
1013
+ method: 'POST',
1014
+ body: { messages, tools },
1015
+ timeout: 120000,
1016
+ });
1017
+
1018
+ if (r.status === 429) {
1019
+ return `Free tier limit reached. /login <key> for unlimited access.`;
1020
+ }
1021
+ if (r.status !== 200) {
1022
+ // Fall back to streaming without tools
1023
+ const result = await callFreeTier(messages, true);
1024
+ return result.content || 'No response from free tier.';
1025
+ }
1026
+
1027
+ rateTracker.record();
1028
+ response = r.data;
1029
+ } catch {
1030
+ // Network error — try streaming fallback
1031
+ const result = await callFreeTier(messages, true);
910
1032
  return result.content || 'No response from free tier. Try /login <key> for full agent.';
911
1033
  }
912
- return result.content || 'No response from free tier. Try /login <key> for full agent.';
1034
+
1035
+ // Handle tool use loop (same as OpenAI path)
1036
+ let iterations = 0;
1037
+ while (response?.choices?.[0]?.finish_reason === 'tool_calls' && iterations < 10) {
1038
+ iterations++;
1039
+ const toolCalls = response.choices[0].message.tool_calls || [];
1040
+ if (toolCalls.length === 0) break;
1041
+
1042
+ const toolResults = [];
1043
+ for (const tc of toolCalls) {
1044
+ let input;
1045
+ try { input = JSON.parse(tc.function.arguments); } catch { input = {}; }
1046
+ console.log(ui.dim(` [${tc.function.name}] ${JSON.stringify(input).slice(0, 80)}`));
1047
+ const result = await executeTool(tc.function.name, input);
1048
+ toolResults.push({ role: 'tool', tool_call_id: tc.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
1049
+ }
1050
+
1051
+ // Add assistant message with tool_calls + results, then call again
1052
+ messages.push({ role: 'assistant', content: response.choices[0].message.content || null, tool_calls: toolCalls });
1053
+ messages.push(...toolResults);
1054
+
1055
+ try {
1056
+ const r = await navada.request(endpoint, {
1057
+ method: 'POST',
1058
+ body: { messages, tools },
1059
+ timeout: 120000,
1060
+ });
1061
+ if (r.status !== 200) break;
1062
+ rateTracker.record();
1063
+ response = r.data;
1064
+ } catch { break; }
1065
+ }
1066
+
1067
+ // Extract final text
1068
+ const content = response?.choices?.[0]?.message?.content || '';
1069
+ if (content) console.log(` ${content}`);
1070
+ return content || 'No response.';
913
1071
  }
914
1072
 
915
1073
  async function fallbackChat(msg) {
@@ -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(' ');