@banaxi/banana-code 1.3.0 → 1.5.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/README.md +32 -3
- package/package.json +2 -1
- package/src/config.js +1 -1
- package/src/index.js +169 -75
- package/src/providers/gemini.js +2 -2
- package/src/providers/ollama.js +7 -5
- package/src/providers/ollamaCloud.js +67 -16
- package/src/sessions.js +1 -0
- package/src/tools/registry.js +48 -1
- package/src/utils/mcp.js +96 -0
package/README.md
CHANGED
|
@@ -49,13 +49,15 @@ While tools like Cursor provide great GUI experiences, Banana Code is built for
|
|
|
49
49
|
|
|
50
50
|
## ✨ Key Features
|
|
51
51
|
|
|
52
|
-
- **Multi-Provider Support**: Switch between **Google Gemini**, **Anthropic Claude**, **OpenAI**, **Ollama Cloud**, and **Ollama (Local)** effortlessly.
|
|
52
|
+
- **Multi-Provider Support**: Switch between **Google Gemini**, **Anthropic Claude**, **OpenAI**, **Mistral AI**, **Ollama Cloud**, and **Ollama (Local)** effortlessly.
|
|
53
|
+
- **Model Context Protocol (MCP)**: Connect Banana Code to any community-built MCP server (like SQLite, GitHub, Google Maps) to give your AI infinite new superpowers via `/beta`.
|
|
53
54
|
- **Plan & Agent Modes**: Use `/agent` for instant execution, or `/plan` to make the AI draft a step-by-step implementation plan for your approval before it touches any code.
|
|
55
|
+
- **Hierarchical Sub-Agents**: The main AI can spawn specialized "sub-agents" (Researchers, Coders, Reviewers) to handle complex tasks without polluting your main chat history.
|
|
54
56
|
- **Self-Healing Loop**: If the AI runs a command (like running tests) and it fails, Banana Code automatically feeds the error trace back to the AI so it can fix its own code.
|
|
55
57
|
- **Agent Skills**: Teach your AI specialized workflows. Drop a `SKILL.md` file in your config folder, and the AI will automatically activate it when relevant.
|
|
56
|
-
- **Smart Context**: Use `@file/path.js` to instantly inject file contents
|
|
58
|
+
- **Smart Context & Pruning**: Use `@file/path.js` to instantly inject file contents, auto-feed your workspace, and use `/clean` to instantly compress long chat histories to save tokens.
|
|
57
59
|
- **Web Research**: Deep integration with DuckDuckGo APIs and Scrapers to give the AI real-time access to the internet.
|
|
58
|
-
- **Persistent Sessions**: All chats are saved
|
|
60
|
+
- **Persistent Sessions**: All chats are auto-titled and saved. Use `/chats` for a fully interactive menu to resume any past session.
|
|
59
61
|
- **Syntax Highlighting**: Beautiful, readable markdown output with syntax coloring directly in your terminal.
|
|
60
62
|
|
|
61
63
|
## 🚀 Installation
|
|
@@ -91,7 +93,14 @@ banana --resume <uuid>
|
|
|
91
93
|
|
|
92
94
|
### In-App Commands
|
|
93
95
|
While in a chat, use these special commands:
|
|
96
|
+
- `/provider`: Switch AI provider (gemini, claude, openai, mistral, ollama_cloud, ollama).
|
|
94
97
|
- `/model`: Switch the active AI model on the fly.
|
|
98
|
+
- `/chats`: Open an interactive menu to view and resume past auto-titled chat sessions.
|
|
99
|
+
- `/clean`: Compress your current chat history into a dense summary to save tokens.
|
|
100
|
+
- `/context`: View your current message count and estimated token usage.
|
|
101
|
+
- `/settings`: Toggle UI features like syntax highlighting and auto-workspace feeding.
|
|
102
|
+
- `/plan` & `/agent`: Toggle between Plan & Execute mode and standard Agent mode.
|
|
103
|
+
- `/beta`: Enable experimental features like MCP Support and Sub-Agents.
|
|
95
104
|
- `/clear`: Clear the current chat history.
|
|
96
105
|
- `/exit` or `CTRL+D`: Save and exit the session.
|
|
97
106
|
|
|
@@ -126,6 +135,26 @@ description: Use this skill whenever you are asked to build or edit a React comp
|
|
|
126
135
|
```
|
|
127
136
|
3. Type `/skills` in Banana Code to verify it loaded. The AI will now follow these rules automatically!
|
|
128
137
|
|
|
138
|
+
### 🔌 Model Context Protocol (MCP) Support
|
|
139
|
+
Banana Code supports the open standard [Model Context Protocol](https://modelcontextprotocol.io/), allowing you to plug in community-built servers to give your AI access to your databases, GitHub, Slack, Google Maps, and more.
|
|
140
|
+
|
|
141
|
+
1. Enable **MCP Support** in the `/beta` menu.
|
|
142
|
+
2. Create a configuration file at `~/.config/banana-code/mcp.json`.
|
|
143
|
+
3. Add your servers. For example, to add the "fetch" and "math" tools using the test server:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mcpServers": {
|
|
148
|
+
"everything": {
|
|
149
|
+
"command": "npx",
|
|
150
|
+
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Restart Banana Code, and the AI will instantly know how to use these new tools natively!
|
|
157
|
+
|
|
129
158
|
## 🔐 Privacy & Security
|
|
130
159
|
|
|
131
160
|
Banana Code is built with transparency in mind:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banaxi/banana-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "🍌 BananaCode",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"banana",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@anthropic-ai/sdk": "^0.36.1",
|
|
30
30
|
"@google/generative-ai": "^0.21.0",
|
|
31
31
|
"@inquirer/prompts": "^7.2.3",
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
32
33
|
"@openai/codex": "^0.117.0",
|
|
33
34
|
"chalk": "^5.4.1",
|
|
34
35
|
"diff": "^8.0.4",
|
package/src/config.js
CHANGED
package/src/index.js
CHANGED
|
@@ -15,10 +15,12 @@ import { MistralProvider } from './providers/mistral.js';
|
|
|
15
15
|
import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSessions } from './sessions.js';
|
|
16
16
|
import { printMarkdown } from './utils/markdown.js';
|
|
17
17
|
import { estimateConversationTokens } from './utils/tokens.js';
|
|
18
|
+
import { mcpManager } from './utils/mcp.js';
|
|
18
19
|
|
|
19
20
|
let config;
|
|
20
21
|
let providerInstance;
|
|
21
22
|
let currentSessionId;
|
|
23
|
+
let currentSessionTitle = null;
|
|
22
24
|
const commandHistory = [];
|
|
23
25
|
let historyIndex = -1;
|
|
24
26
|
let currentInputSaved = '';
|
|
@@ -67,7 +69,7 @@ async function handleSlashCommand(command) {
|
|
|
67
69
|
providerInstance = createProvider();
|
|
68
70
|
console.log(chalk.green(`Switched provider to ${newProv} (${config.model}).`));
|
|
69
71
|
} else {
|
|
70
|
-
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|ollama_cloud|ollama>`));
|
|
72
|
+
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|mistral|ollama_cloud|ollama>`));
|
|
71
73
|
}
|
|
72
74
|
break;
|
|
73
75
|
case '/model':
|
|
@@ -107,7 +109,7 @@ async function handleSlashCommand(command) {
|
|
|
107
109
|
message: 'Select a model:',
|
|
108
110
|
choices: finalChoices,
|
|
109
111
|
loop: false,
|
|
110
|
-
pageSize:
|
|
112
|
+
pageSize: 10
|
|
111
113
|
});
|
|
112
114
|
|
|
113
115
|
if (newModel === 'CUSTOM_ID') {
|
|
@@ -244,6 +246,12 @@ async function handleSlashCommand(command) {
|
|
|
244
246
|
checked: (config.betaTools || []).includes('clean_command')
|
|
245
247
|
});
|
|
246
248
|
|
|
249
|
+
choices.push({
|
|
250
|
+
name: 'MCP Support (Model Context Protocol)',
|
|
251
|
+
value: 'mcp_support',
|
|
252
|
+
checked: (config.betaTools || []).includes('mcp_support')
|
|
253
|
+
});
|
|
254
|
+
|
|
247
255
|
if (choices.length === 0) {
|
|
248
256
|
console.log(chalk.yellow("No beta features available."));
|
|
249
257
|
break;
|
|
@@ -271,6 +279,12 @@ async function handleSlashCommand(command) {
|
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
if (enabledBetaTools.includes('mcp_support') && !(config.betaTools || []).includes('mcp_support')) {
|
|
283
|
+
await mcpManager.init();
|
|
284
|
+
} else if (!enabledBetaTools.includes('mcp_support') && (config.betaTools || []).includes('mcp_support')) {
|
|
285
|
+
await mcpManager.cleanup();
|
|
286
|
+
}
|
|
287
|
+
|
|
274
288
|
config.betaTools = enabledBetaTools;
|
|
275
289
|
await saveConfig(config);
|
|
276
290
|
if (providerInstance) {
|
|
@@ -375,18 +389,45 @@ async function handleSlashCommand(command) {
|
|
|
375
389
|
if (sessions.length === 0) {
|
|
376
390
|
console.log(chalk.yellow("No saved chat sessions found."));
|
|
377
391
|
} else {
|
|
378
|
-
|
|
379
|
-
sessions.
|
|
380
|
-
const active = s.uuid === currentSessionId ?
|
|
381
|
-
|
|
392
|
+
const { select: chatSelect } = await import('@inquirer/prompts');
|
|
393
|
+
const choices = sessions.map(s => {
|
|
394
|
+
const active = s.uuid === currentSessionId ? ' (active)' : '';
|
|
395
|
+
const date = new Date(s.updatedAt).toLocaleString();
|
|
396
|
+
const titleText = s.title ? `"${s.title}"` : s.uuid.slice(0, 8) + '...';
|
|
397
|
+
return {
|
|
398
|
+
name: `${titleText} - ${date} (${s.provider}/${s.model})${active}`,
|
|
399
|
+
value: s.uuid
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const selectedSessionId = await chatSelect({
|
|
404
|
+
message: 'Select a chat session to resume:',
|
|
405
|
+
choices: choices,
|
|
406
|
+
pageSize: 10,
|
|
407
|
+
loop: false
|
|
382
408
|
});
|
|
383
|
-
|
|
409
|
+
|
|
410
|
+
if (selectedSessionId && selectedSessionId !== currentSessionId) {
|
|
411
|
+
const session = await loadSession(selectedSessionId);
|
|
412
|
+
if (session) {
|
|
413
|
+
currentSessionId = session.uuid;
|
|
414
|
+
currentSessionTitle = session.title || null;
|
|
415
|
+
config.provider = session.provider;
|
|
416
|
+
config.model = session.model;
|
|
417
|
+
providerInstance = createProvider();
|
|
418
|
+
if (providerInstance.messages !== undefined) {
|
|
419
|
+
providerInstance.messages = session.messages;
|
|
420
|
+
}
|
|
421
|
+
playHistory(session);
|
|
422
|
+
console.log(chalk.green(`Resumed session: ${currentSessionTitle || currentSessionId} (${session.provider}/${session.model})\n`));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
384
425
|
}
|
|
385
426
|
break;
|
|
386
427
|
case '/help':
|
|
387
428
|
console.log(chalk.yellow(`
|
|
388
429
|
Available commands:
|
|
389
|
-
/provider <name> - Switch AI provider (gemini, claude, openai, ollama_cloud, ollama)
|
|
430
|
+
/provider <name> - Switch AI provider (gemini, claude, openai, mistral, ollama_cloud, ollama)
|
|
390
431
|
/model [name] - Switch model within current provider (opens menu if name omitted)
|
|
391
432
|
/chats - List persistent chat sessions
|
|
392
433
|
/clear - Clear chat history
|
|
@@ -430,6 +471,75 @@ function padLine(text, width) {
|
|
|
430
471
|
return text + ' '.repeat(pad);
|
|
431
472
|
}
|
|
432
473
|
|
|
474
|
+
function playHistory(session) {
|
|
475
|
+
process.stdout.write('\x1bc'); // Clear screen
|
|
476
|
+
console.log(chalk.yellow.bold("🍌 Banana Code — Peeling the project..."));
|
|
477
|
+
console.log(chalk.gray("-------------------------------------------"));
|
|
478
|
+
for (const msg of session.messages) {
|
|
479
|
+
if (msg.role === 'system') continue;
|
|
480
|
+
|
|
481
|
+
if (session.provider === 'gemini') {
|
|
482
|
+
if (msg.role === 'user') {
|
|
483
|
+
if (msg.parts[0]?.text) console.log(`${chalk.yellow('🍌 >')} ${msg.parts[0].text}`);
|
|
484
|
+
else if (msg.parts[0]?.functionResponse) {
|
|
485
|
+
console.log(chalk.yellow(`[Tool Result Received]`));
|
|
486
|
+
}
|
|
487
|
+
} else if (msg.role === 'model') {
|
|
488
|
+
msg.parts.forEach(p => {
|
|
489
|
+
if (p.text) {
|
|
490
|
+
if (config.useMarkedTerminal) printMarkdown(p.text);
|
|
491
|
+
else process.stdout.write(chalk.cyan(p.text));
|
|
492
|
+
}
|
|
493
|
+
if (p.functionCall) console.log(chalk.yellow(`\n[Banana Calling Tool: ${p.functionCall.name}]`));
|
|
494
|
+
});
|
|
495
|
+
console.log();
|
|
496
|
+
}
|
|
497
|
+
} else if (session.provider === 'claude') {
|
|
498
|
+
if (msg.role === 'user') {
|
|
499
|
+
if (typeof msg.content === 'string') console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
|
|
500
|
+
else {
|
|
501
|
+
msg.content.forEach(c => {
|
|
502
|
+
if (c.type === 'tool_result') console.log(chalk.yellow(`[Tool Result Received]`));
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
} else if (msg.role === 'assistant') {
|
|
506
|
+
if (typeof msg.content === 'string') {
|
|
507
|
+
if (config.useMarkedTerminal) printMarkdown(msg.content);
|
|
508
|
+
else process.stdout.write(chalk.cyan(msg.content));
|
|
509
|
+
} else {
|
|
510
|
+
msg.content.forEach(c => {
|
|
511
|
+
if (c.type === 'text') {
|
|
512
|
+
if (config.useMarkedTerminal) printMarkdown(c.text);
|
|
513
|
+
else process.stdout.write(chalk.cyan(c.text));
|
|
514
|
+
}
|
|
515
|
+
if (c.type === 'tool_use') console.log(chalk.yellow(`\n[Banana Calling Tool: ${c.name}]`));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
console.log();
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
// OpenAI, Ollama, Mistral
|
|
522
|
+
if (msg.role === 'user') {
|
|
523
|
+
console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
|
|
524
|
+
} else if (msg.role === 'assistant' || msg.role === 'output_text') {
|
|
525
|
+
if (msg.content) {
|
|
526
|
+
if (config.useMarkedTerminal) printMarkdown(msg.content);
|
|
527
|
+
else process.stdout.write(chalk.cyan(msg.content));
|
|
528
|
+
}
|
|
529
|
+
if (msg.tool_calls) {
|
|
530
|
+
msg.tool_calls.forEach(tc => {
|
|
531
|
+
const name = tc.function ? tc.function.name : tc.name;
|
|
532
|
+
console.log(chalk.yellow(`\n[Banana Calling Tool: ${name}]`));
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
console.log();
|
|
536
|
+
} else if (msg.role === 'tool') {
|
|
537
|
+
console.log(chalk.yellow(`[Tool Result Received]`));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
433
543
|
let lastPromptRows = 1;
|
|
434
544
|
|
|
435
545
|
function drawPromptBox(inputText, cursorPos) {
|
|
@@ -598,6 +708,7 @@ function promptUser() {
|
|
|
598
708
|
process.stdout.write(`\x1b[${moveDown}B\x1b[2K\x1b[1B\x1b[2K\x1b[1G\n`);
|
|
599
709
|
console.log(chalk.yellow(`\nTo resume this session: node bin/banana.js --resume ${currentSessionId}`));
|
|
600
710
|
console.log(chalk.yellow("🍌 Bye BananaCode. See ya!"));
|
|
711
|
+
mcpManager.cleanup();
|
|
601
712
|
process.exit(0);
|
|
602
713
|
}
|
|
603
714
|
};
|
|
@@ -714,6 +825,10 @@ async function main() {
|
|
|
714
825
|
config = await loadConfig();
|
|
715
826
|
await runStartup();
|
|
716
827
|
|
|
828
|
+
if (config.betaTools && config.betaTools.includes('mcp_support')) {
|
|
829
|
+
await mcpManager.init();
|
|
830
|
+
}
|
|
831
|
+
|
|
717
832
|
const resumeIdx = process.argv.indexOf('--resume');
|
|
718
833
|
if (resumeIdx !== -1) {
|
|
719
834
|
let resumeId = process.argv[resumeIdx + 1];
|
|
@@ -725,78 +840,15 @@ async function main() {
|
|
|
725
840
|
const session = await loadSession(resumeId);
|
|
726
841
|
if (session) {
|
|
727
842
|
currentSessionId = session.uuid;
|
|
843
|
+
currentSessionTitle = session.title || null;
|
|
728
844
|
config.provider = session.provider;
|
|
729
845
|
config.model = session.model;
|
|
730
846
|
providerInstance = createProvider();
|
|
731
847
|
if (providerInstance.messages !== undefined) {
|
|
732
848
|
providerInstance.messages = session.messages;
|
|
733
849
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
// Playback history
|
|
737
|
-
for (const msg of session.messages) {
|
|
738
|
-
if (msg.role === 'system') continue;
|
|
739
|
-
|
|
740
|
-
if (config.provider === 'gemini') {
|
|
741
|
-
if (msg.role === 'user') {
|
|
742
|
-
if (msg.parts[0]?.text) console.log(`${chalk.yellow('🍌 >')} ${msg.parts[0].text}`);
|
|
743
|
-
else if (msg.parts[0]?.functionResponse) {
|
|
744
|
-
console.log(chalk.yellow(`[Tool Result Received]`));
|
|
745
|
-
}
|
|
746
|
-
} else if (msg.role === 'model') {
|
|
747
|
-
msg.parts.forEach(p => {
|
|
748
|
-
if (p.text) {
|
|
749
|
-
if (config.useMarkedTerminal) printMarkdown(p.text);
|
|
750
|
-
else process.stdout.write(chalk.cyan(p.text));
|
|
751
|
-
}
|
|
752
|
-
if (p.functionCall) console.log(chalk.yellow(`\n[Banana Calling Tool: ${p.functionCall.name}]`));
|
|
753
|
-
});
|
|
754
|
-
console.log();
|
|
755
|
-
}
|
|
756
|
-
} else if (config.provider === 'claude') {
|
|
757
|
-
if (msg.role === 'user') {
|
|
758
|
-
if (typeof msg.content === 'string') console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
|
|
759
|
-
else {
|
|
760
|
-
msg.content.forEach(c => {
|
|
761
|
-
if (c.type === 'tool_result') console.log(chalk.yellow(`[Tool Result Received]`));
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
} else if (msg.role === 'assistant') {
|
|
765
|
-
if (typeof msg.content === 'string') {
|
|
766
|
-
if (config.useMarkedTerminal) printMarkdown(msg.content);
|
|
767
|
-
else process.stdout.write(chalk.cyan(msg.content));
|
|
768
|
-
} else {
|
|
769
|
-
msg.content.forEach(c => {
|
|
770
|
-
if (c.type === 'text') {
|
|
771
|
-
if (config.useMarkedTerminal) printMarkdown(c.text);
|
|
772
|
-
else process.stdout.write(chalk.cyan(c.text));
|
|
773
|
-
}
|
|
774
|
-
if (c.type === 'tool_use') console.log(chalk.yellow(`\n[Banana Calling Tool: ${c.name}]`));
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
console.log();
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
// OpenAI, Ollama
|
|
781
|
-
if (msg.role === 'user') {
|
|
782
|
-
console.log(`${chalk.yellow('🍌 >')} ${msg.content}`);
|
|
783
|
-
} else if (msg.role === 'assistant' || msg.role === 'output_text') {
|
|
784
|
-
if (msg.content) {
|
|
785
|
-
if (config.useMarkedTerminal) printMarkdown(msg.content);
|
|
786
|
-
else process.stdout.write(chalk.cyan(msg.content));
|
|
787
|
-
}
|
|
788
|
-
if (msg.tool_calls) {
|
|
789
|
-
msg.tool_calls.forEach(tc => {
|
|
790
|
-
const name = tc.function ? tc.function.name : tc.name;
|
|
791
|
-
console.log(chalk.yellow(`\n[Banana Calling Tool: ${name}]`));
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
console.log();
|
|
795
|
-
} else if (msg.role === 'tool') {
|
|
796
|
-
console.log(chalk.yellow(`[Tool Result Received]`));
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
850
|
+
playHistory(session);
|
|
851
|
+
console.log(chalk.green(`Resumed session: ${currentSessionTitle || currentSessionId} (${session.provider}/${session.model})\n`));
|
|
800
852
|
} else {
|
|
801
853
|
console.log(chalk.red(`Could not find session ${resumeId}. Starting fresh.`));
|
|
802
854
|
}
|
|
@@ -867,11 +919,53 @@ async function main() {
|
|
|
867
919
|
process.stdout.write(chalk.cyan('✦ '));
|
|
868
920
|
await providerInstance.sendMessage(finalInput);
|
|
869
921
|
console.log(); // Extra newline after AI response
|
|
922
|
+
|
|
923
|
+
// Auto-generate title every 10 messages (or on the 3rd message)
|
|
924
|
+
const msgLen = providerInstance.messages ? providerInstance.messages.length : 0;
|
|
925
|
+
if (!currentSessionTitle && msgLen >= 3 || msgLen > 0 && msgLen % 10 === 0) {
|
|
926
|
+
const titleSpinner = ora({ text: 'Generating chat title...', color: 'gray', stream: process.stdout }).start();
|
|
927
|
+
try {
|
|
928
|
+
// Temporarily disable terminal formatting and debug for the title request
|
|
929
|
+
const originalUseMarked = config.useMarkedTerminal;
|
|
930
|
+
const originalDebug = config.debug;
|
|
931
|
+
config.useMarkedTerminal = false;
|
|
932
|
+
config.debug = false;
|
|
933
|
+
|
|
934
|
+
// Temporarily silence stdout so the model's streaming text doesn't bleed into the terminal
|
|
935
|
+
const originalStdoutWrite = process.stdout.write;
|
|
936
|
+
process.stdout.write = () => {};
|
|
937
|
+
|
|
938
|
+
const titlePrompt = "SYSTEM: You are a title generator. Based on this conversation, provide a VERY SHORT (2-5 words) title. Reply ONLY with the title string, no quotes or formatting.";
|
|
939
|
+
|
|
940
|
+
// Use a temporary provider instance so we never pollute the main history array
|
|
941
|
+
const titleProvider = createProvider();
|
|
942
|
+
// Deep copy messages so the AI knows the context, but modifications don't leak back
|
|
943
|
+
titleProvider.messages = JSON.parse(JSON.stringify(providerInstance.messages));
|
|
944
|
+
|
|
945
|
+
let title = '';
|
|
946
|
+
try {
|
|
947
|
+
title = await titleProvider.sendMessage(titlePrompt);
|
|
948
|
+
} finally {
|
|
949
|
+
// Always restore stdout even if it crashes
|
|
950
|
+
process.stdout.write = originalStdoutWrite;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
currentSessionTitle = title.replace(/['"]/g, '').trim();
|
|
954
|
+
|
|
955
|
+
config.useMarkedTerminal = originalUseMarked;
|
|
956
|
+
config.debug = originalDebug;
|
|
957
|
+
} catch (e) {
|
|
958
|
+
// ignore title gen errors
|
|
959
|
+
}
|
|
960
|
+
titleSpinner.stop();
|
|
961
|
+
}
|
|
962
|
+
|
|
870
963
|
// Save session after AI message
|
|
871
964
|
await saveSession(currentSessionId, {
|
|
872
965
|
provider: config.provider,
|
|
873
966
|
model: config.model || providerInstance.modelName,
|
|
874
|
-
messages: providerInstance.messages
|
|
967
|
+
messages: providerInstance.messages,
|
|
968
|
+
title: currentSessionTitle
|
|
875
969
|
});
|
|
876
970
|
}
|
|
877
971
|
}
|
package/src/providers/gemini.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
1
|
+
import { getAvailableTools, executeTool, sanitizeSchemaForStrictAPIs } from '../tools/registry.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getSystemPrompt } from '../prompt.js';
|
|
@@ -13,7 +13,7 @@ export class GeminiProvider {
|
|
|
13
13
|
this.tools = getAvailableTools(config).map(t => ({
|
|
14
14
|
name: t.name,
|
|
15
15
|
description: t.description,
|
|
16
|
-
parameters: t.parameters
|
|
16
|
+
parameters: sanitizeSchemaForStrictAPIs(t.parameters)
|
|
17
17
|
}));
|
|
18
18
|
this.systemPrompt = getSystemPrompt(config);
|
|
19
19
|
}
|
package/src/providers/ollama.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
1
|
+
import { getAvailableTools, executeTool, sanitizeSchemaForStrictAPIs } from '../tools/registry.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getSystemPrompt } from '../prompt.js';
|
|
@@ -15,7 +15,7 @@ export class OllamaProvider {
|
|
|
15
15
|
function: {
|
|
16
16
|
name: t.name,
|
|
17
17
|
description: t.description,
|
|
18
|
-
parameters: t.parameters
|
|
18
|
+
parameters: sanitizeSchemaForStrictAPIs(t.parameters)
|
|
19
19
|
}
|
|
20
20
|
}));
|
|
21
21
|
this.URL = 'http://localhost:11434/api/chat';
|
|
@@ -42,13 +42,14 @@ export class OllamaProvider {
|
|
|
42
42
|
body: JSON.stringify({
|
|
43
43
|
model: this.modelName,
|
|
44
44
|
messages: this.messages,
|
|
45
|
-
tools: this.tools,
|
|
45
|
+
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
46
46
|
stream: false
|
|
47
47
|
})
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
if (!response.ok) {
|
|
51
|
-
|
|
51
|
+
const errorText = await response.text();
|
|
52
|
+
throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const data = await response.json();
|
|
@@ -76,7 +77,7 @@ export class OllamaProvider {
|
|
|
76
77
|
const fn = call.function;
|
|
77
78
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
78
79
|
|
|
79
|
-
let res = await executeTool(fn.name, fn.arguments);
|
|
80
|
+
let res = await executeTool(fn.name, fn.arguments, this.config);
|
|
80
81
|
if (this.config.debug) {
|
|
81
82
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
82
83
|
}
|
|
@@ -84,6 +85,7 @@ export class OllamaProvider {
|
|
|
84
85
|
|
|
85
86
|
this.messages.push({
|
|
86
87
|
role: 'tool',
|
|
88
|
+
tool_call_id: call.id || 'mcp_call',
|
|
87
89
|
content: typeof res === 'string' ? res : JSON.stringify(res)
|
|
88
90
|
});
|
|
89
91
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
1
|
+
import { getAvailableTools, executeTool, sanitizeSchemaForStrictAPIs } from '../tools/registry.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getSystemPrompt } from '../prompt.js';
|
|
@@ -16,7 +16,7 @@ export class OllamaCloudProvider {
|
|
|
16
16
|
function: {
|
|
17
17
|
name: t.name,
|
|
18
18
|
description: t.description,
|
|
19
|
-
parameters: t.parameters
|
|
19
|
+
parameters: sanitizeSchemaForStrictAPIs(t.parameters)
|
|
20
20
|
}
|
|
21
21
|
}));
|
|
22
22
|
this.URL = 'https://ollama.com/api/chat';
|
|
@@ -47,7 +47,7 @@ export class OllamaCloudProvider {
|
|
|
47
47
|
model: this.modelName,
|
|
48
48
|
messages: this.messages,
|
|
49
49
|
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
50
|
-
stream:
|
|
50
|
+
stream: true
|
|
51
51
|
})
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -57,28 +57,78 @@ export class OllamaCloudProvider {
|
|
|
57
57
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
let currentChunkResponse = '';
|
|
63
|
+
let lastMessageObj = { role: 'assistant', content: '' };
|
|
64
|
+
let lineBuffer = '';
|
|
65
|
+
|
|
66
|
+
while (true) {
|
|
67
|
+
const { done, value } = await reader.read();
|
|
68
|
+
if (done) break;
|
|
69
|
+
|
|
70
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
71
|
+
lineBuffer += chunk;
|
|
72
|
+
const lines = lineBuffer.split('\n');
|
|
73
|
+
lineBuffer = lines.pop(); // Keep partial line
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!line.trim()) continue;
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(line);
|
|
79
|
+
if (data.message) {
|
|
80
|
+
if (data.message.tool_calls) {
|
|
81
|
+
lastMessageObj.tool_calls = data.message.tool_calls;
|
|
82
|
+
}
|
|
83
|
+
if (data.message.content) {
|
|
84
|
+
const content = data.message.content;
|
|
85
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
86
|
+
if (!this.config.useMarkedTerminal) {
|
|
87
|
+
process.stdout.write(chalk.cyan(content));
|
|
88
|
+
}
|
|
89
|
+
currentChunkResponse += content;
|
|
90
|
+
finalResponse += content;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) { }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
62
96
|
|
|
63
|
-
|
|
97
|
+
if (lineBuffer.trim()) {
|
|
98
|
+
try {
|
|
99
|
+
const data = JSON.parse(lineBuffer);
|
|
100
|
+
if (data.message) {
|
|
101
|
+
if (data.message.tool_calls) {
|
|
102
|
+
lastMessageObj.tool_calls = data.message.tool_calls;
|
|
103
|
+
}
|
|
104
|
+
if (data.message.content) {
|
|
105
|
+
const content = data.message.content;
|
|
106
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
107
|
+
if (!this.config.useMarkedTerminal) {
|
|
108
|
+
process.stdout.write(chalk.cyan(content));
|
|
109
|
+
}
|
|
110
|
+
currentChunkResponse += content;
|
|
111
|
+
finalResponse += content;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { }
|
|
115
|
+
}
|
|
64
116
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
process.stdout.write(chalk.cyan(messageObj.content));
|
|
70
|
-
}
|
|
71
|
-
finalResponse += messageObj.content;
|
|
117
|
+
if (spinner.isSpinning) spinner.stop();
|
|
118
|
+
|
|
119
|
+
if (currentChunkResponse && this.config.useMarkedTerminal) {
|
|
120
|
+
printMarkdown(currentChunkResponse);
|
|
72
121
|
}
|
|
73
122
|
|
|
74
|
-
|
|
123
|
+
lastMessageObj.content = currentChunkResponse || '';
|
|
124
|
+
this.messages.push(lastMessageObj);
|
|
75
125
|
|
|
76
|
-
if (!
|
|
126
|
+
if (!lastMessageObj.tool_calls || lastMessageObj.tool_calls.length === 0) {
|
|
77
127
|
console.log();
|
|
78
128
|
break;
|
|
79
129
|
}
|
|
80
130
|
|
|
81
|
-
for (const call of
|
|
131
|
+
for (const call of lastMessageObj.tool_calls) {
|
|
82
132
|
const fn = call.function;
|
|
83
133
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
84
134
|
|
|
@@ -90,6 +140,7 @@ export class OllamaCloudProvider {
|
|
|
90
140
|
|
|
91
141
|
this.messages.push({
|
|
92
142
|
role: 'tool',
|
|
143
|
+
tool_call_id: call.id || 'mcp_call', // Use ID from call if available
|
|
93
144
|
content: typeof res === 'string' ? res : JSON.stringify(res)
|
|
94
145
|
});
|
|
95
146
|
}
|
package/src/sessions.js
CHANGED
package/src/tools/registry.js
CHANGED
|
@@ -9,6 +9,7 @@ import { duckDuckGoScrape } from './duckDuckGoScrape.js';
|
|
|
9
9
|
import { patchFile } from './patchFile.js';
|
|
10
10
|
import { activateSkill } from './activateSkill.js';
|
|
11
11
|
import { delegateTask } from './delegateTask.js';
|
|
12
|
+
import { mcpManager } from '../utils/mcp.js';
|
|
12
13
|
|
|
13
14
|
export const TOOLS = [
|
|
14
15
|
{
|
|
@@ -173,15 +174,61 @@ export const TOOLS = [
|
|
|
173
174
|
];
|
|
174
175
|
|
|
175
176
|
export function getAvailableTools(config = {}) {
|
|
176
|
-
|
|
177
|
+
let available = TOOLS.filter(tool => {
|
|
177
178
|
if (tool.beta) {
|
|
178
179
|
return config.betaTools && config.betaTools.includes(tool.name);
|
|
179
180
|
}
|
|
180
181
|
return true;
|
|
181
182
|
});
|
|
183
|
+
|
|
184
|
+
// Add MCP tools if enabled in beta
|
|
185
|
+
if (config.betaTools && config.betaTools.includes('mcp_support')) {
|
|
186
|
+
const mcpTools = mcpManager.getTools().map(t => ({
|
|
187
|
+
name: t.name,
|
|
188
|
+
description: t.description,
|
|
189
|
+
parameters: t.inputSchema,
|
|
190
|
+
isMcp: true
|
|
191
|
+
}));
|
|
192
|
+
available = available.concat(mcpTools);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return available;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Some providers (Gemini, Ollama) are extremely strict about JSON Schema.
|
|
200
|
+
* They will throw a 400 if they see "additionalProperties", "$schema", or other unknown fields.
|
|
201
|
+
*/
|
|
202
|
+
export function sanitizeSchemaForStrictAPIs(schema) {
|
|
203
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
204
|
+
|
|
205
|
+
const sanitized = Array.isArray(schema) ? [] : {};
|
|
206
|
+
|
|
207
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
208
|
+
// Skip keys that strict APIs don't support
|
|
209
|
+
if (key === 'additionalProperties' || key === '$schema' || key === 'const') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof value === 'object' && value !== null) {
|
|
214
|
+
sanitized[key] = sanitizeSchemaForStrictAPIs(value);
|
|
215
|
+
} else {
|
|
216
|
+
sanitized[key] = value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return sanitized;
|
|
182
221
|
}
|
|
183
222
|
|
|
184
223
|
export async function executeTool(name, args, config) {
|
|
224
|
+
// Check if it's an MCP tool first
|
|
225
|
+
if (config.betaTools && config.betaTools.includes('mcp_support')) {
|
|
226
|
+
const mcpTools = mcpManager.getTools();
|
|
227
|
+
if (mcpTools.some(t => t.name === name)) {
|
|
228
|
+
return await mcpManager.callTool(name, args);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
185
232
|
switch (name) {
|
|
186
233
|
case 'execute_command': return await execCommand(args);
|
|
187
234
|
case 'read_file': return await readFile(args);
|
package/src/utils/mcp.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const MCP_CONFIG_FILE = path.join(os.homedir(), '.config', 'banana-code', 'mcp.json');
|
|
9
|
+
|
|
10
|
+
class MCPManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.clients = new Map();
|
|
13
|
+
this.tools = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async init() {
|
|
17
|
+
try {
|
|
18
|
+
const configData = await fs.readFile(MCP_CONFIG_FILE, 'utf-8');
|
|
19
|
+
const config = JSON.parse(configData);
|
|
20
|
+
|
|
21
|
+
if (!config.mcpServers) return;
|
|
22
|
+
|
|
23
|
+
console.log(chalk.cyan("\nInitializing MCP Servers..."));
|
|
24
|
+
|
|
25
|
+
for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
|
|
26
|
+
try {
|
|
27
|
+
const transport = new StdioClientTransport({
|
|
28
|
+
command: serverConfig.command,
|
|
29
|
+
args: serverConfig.args || [],
|
|
30
|
+
env: { ...process.env, ...serverConfig.env }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const client = new Client(
|
|
34
|
+
{ name: "banana-code-client", version: "1.0.0" },
|
|
35
|
+
{ capabilities: { tools: {} } }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await client.connect(transport);
|
|
39
|
+
this.clients.set(serverName, client);
|
|
40
|
+
|
|
41
|
+
const response = await client.listTools();
|
|
42
|
+
|
|
43
|
+
// Map MCP tools to our internal format
|
|
44
|
+
const mappedTools = response.tools.map(tool => ({
|
|
45
|
+
...tool,
|
|
46
|
+
serverName, // Track which server this belongs to
|
|
47
|
+
isMcp: true
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
this.tools.push(...mappedTools);
|
|
51
|
+
console.log(chalk.green(` ✔ Connected to ${serverName} (${response.tools.length} tools discovered)`));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.log(chalk.red(` ✘ Failed to connect to ${serverName}: ${err.message}`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err.code !== 'ENOENT') {
|
|
58
|
+
console.log(chalk.red(`Error loading MCP config: ${err.message}`));
|
|
59
|
+
} else {
|
|
60
|
+
// Create default empty config if not found
|
|
61
|
+
await fs.writeFile(MCP_CONFIG_FILE, JSON.stringify({ mcpServers: {} }, null, 2));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getTools() {
|
|
67
|
+
return this.tools;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async callTool(name, args) {
|
|
71
|
+
// Find which server has this tool
|
|
72
|
+
const tool = this.tools.find(t => t.name === name);
|
|
73
|
+
if (!tool) throw new Error(`MCP Tool ${name} not found`);
|
|
74
|
+
|
|
75
|
+
const client = this.clients.get(tool.serverName);
|
|
76
|
+
if (!client) throw new Error(`MCP Client for ${tool.serverName} not found`);
|
|
77
|
+
|
|
78
|
+
const result = await client.callTool({
|
|
79
|
+
name: name,
|
|
80
|
+
arguments: args
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// MCP returns content array, we usually want the text from the first item
|
|
84
|
+
return result.content.map(c => c.text).join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async cleanup() {
|
|
88
|
+
for (const [name, client] of this.clients) {
|
|
89
|
+
try {
|
|
90
|
+
await client.close();
|
|
91
|
+
} catch (e) {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const mcpManager = new MCPManager();
|