@banaxi/banana-code 1.6.0 ā 1.8.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/package.json +1 -1
- package/src/config.js +59 -0
- package/src/constants.js +6 -5
- package/src/index.js +114 -11
- package/src/permissions.js +8 -0
- package/src/prompt.js +21 -1
- package/src/providers/openrouter.js +169 -0
- package/src/server.js +3 -3
- package/src/tools/readManyFiles.js +32 -0
- package/src/tools/registry.js +22 -1
- package/src.zip +0 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -146,6 +146,63 @@ export async function setupProvider(provider, config = {}) {
|
|
|
146
146
|
choices: OPENAI_MODELS
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
|
+
} else if (provider === 'openrouter') {
|
|
150
|
+
config.apiKey = await input({
|
|
151
|
+
message: 'Enter your OPENROUTER_API_KEY (from openrouter.ai/keys):',
|
|
152
|
+
default: config.apiKey
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let modelAccepted = false;
|
|
156
|
+
while (!modelAccepted) {
|
|
157
|
+
const modelId = await input({
|
|
158
|
+
message: 'Enter the OpenRouter model ID (e.g., nvidia/nemotron-3-super-120b-a12b:free):',
|
|
159
|
+
default: config.model || '',
|
|
160
|
+
validate: (v) => v.trim().length > 0 || 'Model ID cannot be empty'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log(chalk.cyan(`\nValidating model "${modelId}" on OpenRouter...`));
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetch('https://openrouter.ai/api/v1/models');
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
const found = data.data?.find(m => m.id === modelId.trim());
|
|
168
|
+
|
|
169
|
+
if (!found) {
|
|
170
|
+
console.log(chalk.red(`Model "${modelId}" was not found on OpenRouter.`));
|
|
171
|
+
console.log(chalk.yellow('Browse available models at: https://openrouter.ai/models'));
|
|
172
|
+
const retry = await input({ message: 'Try a different model ID? (y/n):', default: 'y' });
|
|
173
|
+
if (retry.toLowerCase() !== 'y') {
|
|
174
|
+
config.model = modelId.trim();
|
|
175
|
+
modelAccepted = true;
|
|
176
|
+
console.log(chalk.yellow('Proceeding anyway ā tool calling may not work.'));
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const supported = found.supported_parameters || [];
|
|
182
|
+
const hasToolCalling = supported.includes('tools') || supported.includes('tool_choice');
|
|
183
|
+
|
|
184
|
+
if (hasToolCalling) {
|
|
185
|
+
console.log(chalk.green(`ā "${modelId}" supports tool calling. Good to go!`));
|
|
186
|
+
config.model = modelId.trim();
|
|
187
|
+
modelAccepted = true;
|
|
188
|
+
} else {
|
|
189
|
+
console.log(chalk.red(`ā "${modelId}" does NOT support tool calling.`));
|
|
190
|
+
console.log(chalk.gray(` Supported parameters: ${supported.join(', ') || 'none listed'}`));
|
|
191
|
+
console.log(chalk.yellow('Banana Code requires tool calling to function correctly.'));
|
|
192
|
+
const retry = await input({ message: 'Choose a different model? (y/n):', default: 'y' });
|
|
193
|
+
if (retry.toLowerCase() !== 'y') {
|
|
194
|
+
config.model = modelId.trim();
|
|
195
|
+
modelAccepted = true;
|
|
196
|
+
console.log(chalk.yellow('Proceeding anyway ā tool calling will likely fail.'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.log(chalk.red(`Could not reach OpenRouter API: ${err.message}`));
|
|
201
|
+
console.log(chalk.yellow('Skipping validation and using the model ID as-is.'));
|
|
202
|
+
config.model = modelId.trim();
|
|
203
|
+
modelAccepted = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
149
206
|
} else if (provider === 'ollama') {
|
|
150
207
|
console.log(chalk.cyan("Detecting running Ollama models..."));
|
|
151
208
|
try {
|
|
@@ -180,12 +237,14 @@ async function runSetupWizard() {
|
|
|
180
237
|
{ name: 'Anthropic Claude', value: 'claude' },
|
|
181
238
|
{ name: 'OpenAI', value: 'openai' },
|
|
182
239
|
{ name: 'Mistral AI', value: 'mistral' },
|
|
240
|
+
{ name: 'OpenRouter (Any Model)', value: 'openrouter' },
|
|
183
241
|
{ name: 'Ollama Cloud', value: 'ollama_cloud' },
|
|
184
242
|
{ name: 'Ollama (Local)', value: 'ollama' }
|
|
185
243
|
]
|
|
186
244
|
});
|
|
187
245
|
|
|
188
246
|
const config = await setupProvider(provider);
|
|
247
|
+
config.useMemory = true;
|
|
189
248
|
|
|
190
249
|
await saveConfig(config);
|
|
191
250
|
console.log(chalk.yellow.bold("\nYou're all peeled and ready. Type your first message!\n"));
|
package/src/constants.js
CHANGED
|
@@ -7,9 +7,9 @@ export const GEMINI_MODELS = [
|
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
export const CLAUDE_MODELS = [
|
|
10
|
-
{ name: 'Claude Opus 4.6 (Flagship)', value: 'claude-4-6
|
|
11
|
-
{ name: 'Claude Sonnet 4.6 (Fast & Smart)', value: 'claude-4-6
|
|
12
|
-
{ name: 'Claude Haiku 4.5', value: 'claude-4-5
|
|
10
|
+
{ name: 'Claude Opus 4.6 (Flagship)', value: 'claude-opus-4-6' },
|
|
11
|
+
{ name: 'Claude Sonnet 4.6 (Fast & Smart)', value: 'claude-sonnet-4-6' },
|
|
12
|
+
{ name: 'Claude Haiku 4.5', value: 'claude-haiku-4-5' }
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
export const OPENAI_MODELS = [
|
|
@@ -24,10 +24,11 @@ export const OLLAMA_CLOUD_MODELS = [
|
|
|
24
24
|
{ name: 'Kimi K2.5 (Cloud)', value: 'kimi-k2.5:cloud' },
|
|
25
25
|
{ name: 'Qwen 3.5 397B (Cloud)', value: 'qwen3.5:397b-cloud' },
|
|
26
26
|
{ name: 'DeepSeek V3.2 (Cloud)', value: 'deepseek-v3.2:cloud' },
|
|
27
|
-
{ name: 'GLM-5 (Cloud)', value: 'glm-5:cloud' },
|
|
27
|
+
{ name: 'GLM-5.1 (Cloud)', value: 'glm-5.1:cloud' },
|
|
28
28
|
{ name: 'MiniMax M2.7 (Cloud)', value: 'minimax-m2.7:cloud' },
|
|
29
29
|
{ name: 'Llama 3.3 70B (Cloud)', value: 'llama3.3:cloud' },
|
|
30
|
-
{ name: 'Llama 3.1 405B (Cloud)', value: 'llama3.1:405b-cloud' }
|
|
30
|
+
{ name: 'Llama 3.1 405B (Cloud)', value: 'llama3.1:405b-cloud' },
|
|
31
|
+
{ name: 'Gemma 4 31B (Cheapest, Very Good Code)', value: 'gemma4:31b-cloud' }
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
export const MISTRAL_MODELS = [
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { loadConfig, saveConfig, setupProvider } from './config.js';
|
|
5
5
|
import { runStartup } from './startup.js';
|
|
6
|
-
import { getSessionPermissions } from './permissions.js';
|
|
6
|
+
import { getSessionPermissions, setYoloMode } from './permissions.js';
|
|
7
7
|
|
|
8
8
|
import { GeminiProvider } from './providers/gemini.js';
|
|
9
9
|
import { ClaudeProvider } from './providers/claude.js';
|
|
@@ -11,6 +11,7 @@ import { OpenAIProvider } from './providers/openai.js';
|
|
|
11
11
|
import { OllamaProvider } from './providers/ollama.js';
|
|
12
12
|
import { OllamaCloudProvider } from './providers/ollamaCloud.js';
|
|
13
13
|
import { MistralProvider } from './providers/mistral.js';
|
|
14
|
+
import { OpenRouterProvider } from './providers/openrouter.js';
|
|
14
15
|
|
|
15
16
|
import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSessions } from './sessions.js';
|
|
16
17
|
import { printMarkdown } from './utils/markdown.js';
|
|
@@ -33,6 +34,7 @@ function createProvider(overrideConfig = null) {
|
|
|
33
34
|
case 'claude': return new ClaudeProvider(activeConfig);
|
|
34
35
|
case 'openai': return new OpenAIProvider(activeConfig);
|
|
35
36
|
case 'mistral': return new MistralProvider(activeConfig);
|
|
37
|
+
case 'openrouter': return new OpenRouterProvider(activeConfig);
|
|
36
38
|
case 'ollama_cloud': return new OllamaCloudProvider(activeConfig);
|
|
37
39
|
case 'ollama': return new OllamaProvider(activeConfig);
|
|
38
40
|
default:
|
|
@@ -57,20 +59,21 @@ async function handleSlashCommand(command) {
|
|
|
57
59
|
{ name: 'Anthropic Claude', value: 'claude' },
|
|
58
60
|
{ name: 'OpenAI', value: 'openai' },
|
|
59
61
|
{ name: 'Mistral AI', value: 'mistral' },
|
|
62
|
+
{ name: 'OpenRouter (Any Model)', value: 'openrouter' },
|
|
60
63
|
{ name: 'Ollama Cloud', value: 'ollama_cloud' },
|
|
61
64
|
{ name: 'Ollama (Local)', value: 'ollama' }
|
|
62
65
|
]
|
|
63
66
|
});
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
if (['gemini', 'claude', 'openai', 'mistral', 'ollama_cloud', 'ollama'].includes(newProv)) {
|
|
69
|
+
if (['gemini', 'claude', 'openai', 'mistral', 'openrouter', 'ollama_cloud', 'ollama'].includes(newProv)) {
|
|
67
70
|
// Use the shared setup logic to get keys/models
|
|
68
71
|
config = await setupProvider(newProv, config);
|
|
69
72
|
await saveConfig(config);
|
|
70
73
|
providerInstance = createProvider();
|
|
71
74
|
console.log(chalk.green(`Switched provider to ${newProv} (${config.model}).`));
|
|
72
75
|
} else {
|
|
73
|
-
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|mistral|ollama_cloud|ollama>`));
|
|
76
|
+
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|mistral|openrouter|ollama_cloud|ollama>`));
|
|
74
77
|
}
|
|
75
78
|
break;
|
|
76
79
|
case '/model':
|
|
@@ -87,6 +90,13 @@ async function handleSlashCommand(command) {
|
|
|
87
90
|
choices = config.authType === 'oauth' ? CODEX_MODELS : OPENAI_MODELS;
|
|
88
91
|
} else if (config.provider === 'mistral') {
|
|
89
92
|
choices = MISTRAL_MODELS;
|
|
93
|
+
} else if (config.provider === 'openrouter') {
|
|
94
|
+
// Re-run setup flow so the user gets full validation
|
|
95
|
+
config = await setupProvider('openrouter', config);
|
|
96
|
+
await saveConfig(config);
|
|
97
|
+
providerInstance = createProvider();
|
|
98
|
+
console.log(chalk.green(`Switched OpenRouter model to ${config.model}.`));
|
|
99
|
+
break;
|
|
90
100
|
} else if (config.provider === 'ollama_cloud') {
|
|
91
101
|
choices = OLLAMA_CLOUD_MODELS;
|
|
92
102
|
} else if (config.provider === 'ollama') {
|
|
@@ -124,6 +134,29 @@ async function handleSlashCommand(command) {
|
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
if (newModel) {
|
|
137
|
+
if (config.provider === 'openrouter') {
|
|
138
|
+
// Validate tool calling support before switching
|
|
139
|
+
console.log(chalk.cyan(`Validating "${newModel}" on OpenRouter...`));
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch('https://openrouter.ai/api/v1/models');
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
const found = data.data?.find(m => m.id === newModel);
|
|
144
|
+
if (!found) {
|
|
145
|
+
console.log(chalk.yellow(`Model "${newModel}" not found on OpenRouter ā proceeding anyway.`));
|
|
146
|
+
} else {
|
|
147
|
+
const supported = found.supported_parameters || [];
|
|
148
|
+
const hasToolCalling = supported.includes('tools') || supported.includes('tool_choice');
|
|
149
|
+
if (hasToolCalling) {
|
|
150
|
+
console.log(chalk.green(`ā "${newModel}" supports tool calling.`));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(chalk.red(`ā "${newModel}" does NOT support tool calling. Banana Code may not work correctly.`));
|
|
153
|
+
console.log(chalk.gray(` Supported parameters: ${supported.join(', ') || 'none listed'}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.log(chalk.yellow(`Could not validate on OpenRouter: ${err.message}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
config.model = newModel;
|
|
128
161
|
await saveConfig(config);
|
|
129
162
|
if (providerInstance) {
|
|
@@ -325,7 +358,7 @@ async function handleSlashCommand(command) {
|
|
|
325
358
|
{
|
|
326
359
|
name: 'Enable Global AI Memory (Allows AI to save facts persistently)',
|
|
327
360
|
value: 'useMemory',
|
|
328
|
-
checked: config.useMemory
|
|
361
|
+
checked: config.useMemory !== false
|
|
329
362
|
}
|
|
330
363
|
]
|
|
331
364
|
});
|
|
@@ -375,6 +408,8 @@ async function handleSlashCommand(command) {
|
|
|
375
408
|
break;
|
|
376
409
|
case '/plan':
|
|
377
410
|
config.planMode = true;
|
|
411
|
+
config.askMode = false;
|
|
412
|
+
config.securityMode = false;
|
|
378
413
|
await saveConfig(config);
|
|
379
414
|
if (providerInstance) {
|
|
380
415
|
const savedMessages = providerInstance.messages;
|
|
@@ -385,8 +420,52 @@ async function handleSlashCommand(command) {
|
|
|
385
420
|
}
|
|
386
421
|
console.log(chalk.magenta(`Plan mode enabled. For significant changes, the AI will now propose an implementation plan before writing code.`));
|
|
387
422
|
break;
|
|
423
|
+
case '/ask':
|
|
424
|
+
config.askMode = true;
|
|
425
|
+
config.planMode = false;
|
|
426
|
+
config.securityMode = false;
|
|
427
|
+
await saveConfig(config);
|
|
428
|
+
if (providerInstance) {
|
|
429
|
+
const savedMessages = providerInstance.messages;
|
|
430
|
+
providerInstance = createProvider();
|
|
431
|
+
providerInstance.messages = savedMessages;
|
|
432
|
+
} else {
|
|
433
|
+
providerInstance = createProvider();
|
|
434
|
+
}
|
|
435
|
+
console.log(chalk.blue(`Ask mode enabled. The AI will only answer questions and cannot edit files.`));
|
|
436
|
+
break;
|
|
437
|
+
case '/security':
|
|
438
|
+
config.securityMode = true;
|
|
439
|
+
config.askMode = false;
|
|
440
|
+
config.planMode = false;
|
|
441
|
+
await saveConfig(config);
|
|
442
|
+
if (providerInstance) {
|
|
443
|
+
const savedMessages = providerInstance.messages;
|
|
444
|
+
providerInstance = createProvider();
|
|
445
|
+
providerInstance.messages = savedMessages;
|
|
446
|
+
} else {
|
|
447
|
+
providerInstance = createProvider();
|
|
448
|
+
}
|
|
449
|
+
console.log(chalk.red(`Security mode enabled. The AI will look for and help fix vulnerabilities.`));
|
|
450
|
+
console.log(chalk.yellow(`Disclaimer: Please only use this mode for defensive purposes to secure your own code, and do not use the identified vulnerabilities maliciously.`));
|
|
451
|
+
break;
|
|
452
|
+
case '/yolo':
|
|
453
|
+
config.yolo = !config.yolo;
|
|
454
|
+
setYoloMode(config.yolo);
|
|
455
|
+
await saveConfig(config);
|
|
456
|
+
if (providerInstance) {
|
|
457
|
+
const savedMessages = providerInstance.messages;
|
|
458
|
+
providerInstance = createProvider();
|
|
459
|
+
providerInstance.messages = savedMessages;
|
|
460
|
+
} else {
|
|
461
|
+
providerInstance = createProvider();
|
|
462
|
+
}
|
|
463
|
+
console.log(config.yolo ? chalk.bgRed.white.bold('\n ā ļø YOLO MODE ENABLED - All permission requests will be auto-accepted! \n') : chalk.green('\nYOLO mode disabled.\n'));
|
|
464
|
+
break;
|
|
388
465
|
case '/agent':
|
|
389
466
|
config.planMode = false;
|
|
467
|
+
config.askMode = false;
|
|
468
|
+
config.securityMode = false;
|
|
390
469
|
await saveConfig(config);
|
|
391
470
|
if (providerInstance) {
|
|
392
471
|
const savedMessages = providerInstance.messages;
|
|
@@ -438,7 +517,7 @@ async function handleSlashCommand(command) {
|
|
|
438
517
|
}
|
|
439
518
|
break;
|
|
440
519
|
case '/memory':
|
|
441
|
-
if (
|
|
520
|
+
if (config.useMemory === false) {
|
|
442
521
|
console.log(chalk.yellow("Global AI Memory is disabled. Enable it in /settings first."));
|
|
443
522
|
break;
|
|
444
523
|
}
|
|
@@ -525,7 +604,7 @@ async function handleSlashCommand(command) {
|
|
|
525
604
|
case '/help':
|
|
526
605
|
console.log(chalk.yellow(`
|
|
527
606
|
Available commands:
|
|
528
|
-
/provider <name> - Switch AI provider (gemini, claude, openai, mistral, ollama_cloud, ollama)
|
|
607
|
+
/provider <name> - Switch AI provider (gemini, claude, openai, mistral, openrouter, ollama_cloud, ollama)
|
|
529
608
|
/model [name] - Switch model within current provider (opens menu if name omitted)
|
|
530
609
|
/chats - List persistent chat sessions
|
|
531
610
|
/clear - Clear chat history
|
|
@@ -539,6 +618,7 @@ Available commands:
|
|
|
539
618
|
/init - Generate a BANANA.md project summary file
|
|
540
619
|
/plan - Enable Plan Mode (AI proposes a plan for big changes)
|
|
541
620
|
/agent - Enable Agent Mode (default, AI edits directly)
|
|
621
|
+
/yolo - Toggle YOLO mode (skip all permission requests)
|
|
542
622
|
/debug - Toggle debug mode (show tool results)
|
|
543
623
|
/help - Show all commands
|
|
544
624
|
/exit - Quit Banana Code
|
|
@@ -667,7 +747,10 @@ function drawPromptBox(inputText, cursorPos) {
|
|
|
667
747
|
// Redraw status bar and separator (they are always below the prompt)
|
|
668
748
|
const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
|
|
669
749
|
const providerDisplay = config.provider.toUpperCase();
|
|
670
|
-
|
|
750
|
+
let modeDisplay = chalk.green('AGENT MODE');
|
|
751
|
+
if (config.askMode) modeDisplay = chalk.blue('ASK MODE');
|
|
752
|
+
else if (config.securityMode) modeDisplay = chalk.red('SECURITY MODE');
|
|
753
|
+
else if (config.planMode) modeDisplay = chalk.magenta('PLAN MODE');
|
|
671
754
|
|
|
672
755
|
let tokenDisplay = '';
|
|
673
756
|
if (config.showTokenCount && providerInstance) {
|
|
@@ -685,7 +768,8 @@ function drawPromptBox(inputText, cursorPos) {
|
|
|
685
768
|
tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
|
|
686
769
|
}
|
|
687
770
|
|
|
688
|
-
const
|
|
771
|
+
const yoloDisplay = config.yolo ? chalk.bgRed.white.bold(' ā ļø YOLO ') : '';
|
|
772
|
+
const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}${yoloDisplay ? ' / ' + yoloDisplay : ''}`;
|
|
689
773
|
const rightText = '/help for shortcuts ';
|
|
690
774
|
const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
|
|
691
775
|
const midPad = Math.max(0, width - leftStripped.length - rightText.length);
|
|
@@ -728,7 +812,10 @@ function drawPromptBoxInitial(inputText) {
|
|
|
728
812
|
// Status bar: Current Provider / Model + right-aligned "/help for shortcuts"
|
|
729
813
|
const modelDisplay = providerInstance ? providerInstance.modelName : (config.model || 'unknown');
|
|
730
814
|
const providerDisplay = config.provider.toUpperCase();
|
|
731
|
-
|
|
815
|
+
let modeDisplay = chalk.green('AGENT MODE');
|
|
816
|
+
if (config.askMode) modeDisplay = chalk.blue('ASK MODE');
|
|
817
|
+
else if (config.securityMode) modeDisplay = chalk.red('SECURITY MODE');
|
|
818
|
+
else if (config.planMode) modeDisplay = chalk.magenta('PLAN MODE');
|
|
732
819
|
|
|
733
820
|
let tokenDisplay = '';
|
|
734
821
|
if (config.showTokenCount && providerInstance) {
|
|
@@ -746,7 +833,8 @@ function drawPromptBoxInitial(inputText) {
|
|
|
746
833
|
tokenDisplay = ` / Tokens: ${color(tokens.toLocaleString())}`;
|
|
747
834
|
}
|
|
748
835
|
|
|
749
|
-
const
|
|
836
|
+
const yoloDisplay = config.yolo ? chalk.bgRed.white.bold(' ā ļø YOLO ') : '';
|
|
837
|
+
const leftText = ` Provider: ${chalk.cyan(providerDisplay)} / Model: ${chalk.yellow(modelDisplay)} / ${modeDisplay}${tokenDisplay}${yoloDisplay ? ' / ' + yoloDisplay : ''}`;
|
|
750
838
|
const rightText = '/help for shortcuts ';
|
|
751
839
|
|
|
752
840
|
const leftStripped = leftText.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -923,6 +1011,12 @@ function promptUser() {
|
|
|
923
1011
|
async function main() {
|
|
924
1012
|
try {
|
|
925
1013
|
config = await loadConfig();
|
|
1014
|
+
|
|
1015
|
+
if (process.argv.includes('--yolo')) {
|
|
1016
|
+
config.yolo = true;
|
|
1017
|
+
}
|
|
1018
|
+
setYoloMode(config.yolo);
|
|
1019
|
+
|
|
926
1020
|
await runStartup();
|
|
927
1021
|
|
|
928
1022
|
if (config.betaTools && config.betaTools.includes('mcp_support')) {
|
|
@@ -933,7 +1027,16 @@ async function main() {
|
|
|
933
1027
|
if (apiIdx !== -1) {
|
|
934
1028
|
const portStr = process.argv[apiIdx + 1];
|
|
935
1029
|
const port = portStr && !portStr.startsWith('-') ? parseInt(portStr) : 3000;
|
|
936
|
-
|
|
1030
|
+
|
|
1031
|
+
let host = '127.0.0.1';
|
|
1032
|
+
const hostIdx = process.argv.indexOf('--host');
|
|
1033
|
+
if (hostIdx !== -1 && process.argv[hostIdx + 1] && !process.argv[hostIdx + 1].startsWith('-')) {
|
|
1034
|
+
host = process.argv[hostIdx + 1];
|
|
1035
|
+
} else if (process.argv.includes('--expose')) {
|
|
1036
|
+
host = '0.0.0.0';
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
await startApiServer(port, createProvider, host);
|
|
937
1040
|
return;
|
|
938
1041
|
}
|
|
939
1042
|
|
package/src/permissions.js
CHANGED
|
@@ -4,6 +4,10 @@ import crypto from 'crypto';
|
|
|
4
4
|
|
|
5
5
|
const sessionPermissions = new Set();
|
|
6
6
|
|
|
7
|
+
export function setYoloMode(enabled) {
|
|
8
|
+
global.bananaYoloMode = !!enabled;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
function wrapText(text, width) {
|
|
8
12
|
const lines = [];
|
|
9
13
|
for (let i = 0; i < text.length; i += width) {
|
|
@@ -13,6 +17,10 @@ function wrapText(text, width) {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export async function requestPermission(actionType, details) {
|
|
20
|
+
if (global.bananaYoloMode || process.argv.includes('--yolo')) {
|
|
21
|
+
return { allowed: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
const permKey = `allow_session_${actionType}`;
|
|
17
25
|
|
|
18
26
|
if (sessionPermissions.has(permKey)) {
|
package/src/prompt.js
CHANGED
|
@@ -41,7 +41,7 @@ Always use tools when they would help. Be concise but thorough. `;
|
|
|
41
41
|
} catch (e) {}
|
|
42
42
|
|
|
43
43
|
// Load Global Memory
|
|
44
|
-
if (config.useMemory) {
|
|
44
|
+
if (config.useMemory !== false) {
|
|
45
45
|
prompt += `\n\n# Global AI Memory\nYou have the ability to remember facts across ALL sessions and projects using the \`save_memory\` tool. If the user tells you their name, personal preferences, coding rules, or other information they might want to persist, feel free to use the \`save_memory\` tool so you can remember it in the future.\n`;
|
|
46
46
|
|
|
47
47
|
try {
|
|
@@ -92,6 +92,26 @@ The user is operating in "Plan Mode".
|
|
|
92
92
|
`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
if (config.askMode) {
|
|
96
|
+
prompt += `
|
|
97
|
+
[ASK MODE ENABLED]
|
|
98
|
+
The user is operating in "Ask Mode".
|
|
99
|
+
- You are strictly restricted to answering questions, explaining code, and providing information.
|
|
100
|
+
- You MUST NOT make any changes to the codebase. Do NOT use tools that modify files or execute shell commands that change state (e.g. creating/deleting files, installing packages).
|
|
101
|
+
- Use read-only tools like search_files, list_directory, read_file, and read-only execute_command (like running a test or git status) to gather information to answer the user's questions.
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (config.securityMode) {
|
|
106
|
+
prompt += `
|
|
107
|
+
[SECURITY MODE ENABLED]
|
|
108
|
+
The user is operating in "Security Mode".
|
|
109
|
+
- Your primary objective is to find security vulnerabilities, misconfigurations, and bad practices in the codebase.
|
|
110
|
+
- Act as a red-team auditor. Search for OWASP Top 10 vulnerabilities, leaked API keys, unsafe inputs, injection flaws, etc.
|
|
111
|
+
- Provide detailed reports of any vulnerabilities found, including the file path, the affected lines, and suggestions for remediation.
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
if (hasPatchTool) {
|
|
96
116
|
prompt += `
|
|
97
117
|
When editing existing files, PREFER using the 'patch_file' tool for surgical, targeted changes instead of 'write_file', especially for large files. This prevents accidental truncation and is much more efficient. Only use 'write_file' when creating new files or when making very extensive changes to a small file.`;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { getSystemPrompt } from '../prompt.js';
|
|
6
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
7
|
+
|
|
8
|
+
export class OpenRouterProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.openai = new OpenAI({
|
|
12
|
+
apiKey: config.apiKey,
|
|
13
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
14
|
+
defaultHeaders: {
|
|
15
|
+
'HTTP-Referer': 'https://github.com/Banaxi-Tech/Banana-Code',
|
|
16
|
+
'X-Title': 'Banana Code'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
this.modelName = config.model;
|
|
20
|
+
this.systemPrompt = getSystemPrompt(config);
|
|
21
|
+
this.messages = [{ role: 'system', content: this.systemPrompt }];
|
|
22
|
+
this.tools = getAvailableTools(config).map(t => ({
|
|
23
|
+
type: 'function',
|
|
24
|
+
function: {
|
|
25
|
+
name: t.name,
|
|
26
|
+
description: t.description,
|
|
27
|
+
parameters: t.parameters
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateSystemPrompt(newPrompt) {
|
|
33
|
+
this.systemPrompt = newPrompt;
|
|
34
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system') {
|
|
35
|
+
this.messages[0].content = newPrompt;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async sendMessage(message) {
|
|
40
|
+
this.messages.push({ role: 'user', content: message });
|
|
41
|
+
|
|
42
|
+
let spinner = ora({ text: 'Thinking...', color: 'yellow', stream: process.stdout }).start();
|
|
43
|
+
let finalResponse = '';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
let stream = null;
|
|
48
|
+
try {
|
|
49
|
+
stream = await this.openai.chat.completions.create({
|
|
50
|
+
model: this.modelName,
|
|
51
|
+
messages: this.messages,
|
|
52
|
+
tools: this.tools,
|
|
53
|
+
stream: true
|
|
54
|
+
});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
spinner.stop();
|
|
57
|
+
let errMsg = e.message;
|
|
58
|
+
if (e.error && e.error.message) {
|
|
59
|
+
errMsg += ` - ${e.error.message}`;
|
|
60
|
+
} else if (e.response && e.response.data) {
|
|
61
|
+
try {
|
|
62
|
+
errMsg += ` - ${JSON.stringify(e.response.data)}`;
|
|
63
|
+
} catch(err){}
|
|
64
|
+
} else if (e.error && typeof e.error === 'object') {
|
|
65
|
+
try {
|
|
66
|
+
errMsg += ` - ${JSON.stringify(e.error)}`;
|
|
67
|
+
} catch(err){}
|
|
68
|
+
}
|
|
69
|
+
console.error(chalk.red(`OpenRouter Request Error: ${errMsg}`));
|
|
70
|
+
return `Error: ${errMsg}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let chunkResponse = '';
|
|
74
|
+
let toolCalls = [];
|
|
75
|
+
|
|
76
|
+
for await (const chunk of stream) {
|
|
77
|
+
const delta = chunk.choices[0]?.delta;
|
|
78
|
+
|
|
79
|
+
if (delta?.content) {
|
|
80
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
81
|
+
if (!this.config.useMarkedTerminal) {
|
|
82
|
+
if (this.config.isApiMode && this.onChunk) {
|
|
83
|
+
this.onChunk(delta.content);
|
|
84
|
+
} else {
|
|
85
|
+
process.stdout.write(chalk.cyan(delta.content));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
chunkResponse += delta.content;
|
|
89
|
+
finalResponse += delta.content;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (delta?.tool_calls) {
|
|
93
|
+
if (spinner.isSpinning) spinner.stop();
|
|
94
|
+
for (const tc of delta.tool_calls) {
|
|
95
|
+
if (tc.index === undefined) continue;
|
|
96
|
+
if (!toolCalls[tc.index]) {
|
|
97
|
+
toolCalls[tc.index] = { id: tc.id, type: 'function', function: { name: tc.function?.name || '', arguments: '' } };
|
|
98
|
+
}
|
|
99
|
+
if (tc.function?.name && !toolCalls[tc.index].function.name) {
|
|
100
|
+
toolCalls[tc.index].function.name = tc.function.name;
|
|
101
|
+
}
|
|
102
|
+
if (tc.function?.arguments) {
|
|
103
|
+
toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
104
|
+
if (!spinner.isSpinning) {
|
|
105
|
+
spinner = ora({ text: `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`, color: 'yellow', stream: process.stdout }).start();
|
|
106
|
+
} else {
|
|
107
|
+
spinner.text = `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (spinner.isSpinning) spinner.stop();
|
|
114
|
+
|
|
115
|
+
if (chunkResponse) {
|
|
116
|
+
if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
|
|
117
|
+
this.messages.push({ role: 'assistant', content: chunkResponse });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
toolCalls = toolCalls.filter(Boolean);
|
|
121
|
+
|
|
122
|
+
if (toolCalls.length === 0) {
|
|
123
|
+
console.log();
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.messages.push({
|
|
128
|
+
role: 'assistant',
|
|
129
|
+
tool_calls: toolCalls,
|
|
130
|
+
content: chunkResponse || null
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
for (const call of toolCalls) {
|
|
134
|
+
if (this.config.isApiMode && this.onToolStart) {
|
|
135
|
+
this.onToolStart(call.function.name);
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
|
|
138
|
+
let args = {};
|
|
139
|
+
try {
|
|
140
|
+
args = JSON.parse(call.function.arguments);
|
|
141
|
+
} catch (e) { }
|
|
142
|
+
|
|
143
|
+
const res = await executeTool(call.function.name, args, this.config);
|
|
144
|
+
if (this.config.isApiMode && this.onToolEnd) {
|
|
145
|
+
this.onToolEnd(res);
|
|
146
|
+
}
|
|
147
|
+
if (this.config.debug) {
|
|
148
|
+
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
149
|
+
}
|
|
150
|
+
console.log(chalk.yellow(`[Tool Result Received]\n`));
|
|
151
|
+
|
|
152
|
+
this.messages.push({
|
|
153
|
+
role: 'tool',
|
|
154
|
+
tool_call_id: call.id,
|
|
155
|
+
content: typeof res === 'string' ? res : JSON.stringify(res)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
spinner = ora({ text: 'Processing tool results...', color: 'yellow', stream: process.stdout }).start();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return finalResponse;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (spinner && spinner.isSpinning) spinner.stop();
|
|
165
|
+
console.error(chalk.red(`OpenRouter Runtime Error: ${err.message}`));
|
|
166
|
+
return `Error: ${err.message}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import chalk from 'chalk';
|
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
7
|
import { listSessions, loadSession } from './sessions.js';
|
|
8
8
|
|
|
9
|
-
export async function startApiServer(port = 3000, createProvider) {
|
|
9
|
+
export async function startApiServer(port = 3000, createProvider, host = '127.0.0.1') {
|
|
10
10
|
const app = express();
|
|
11
11
|
const server = http.createServer(app);
|
|
12
12
|
const wss = new WebSocketServer({ server });
|
|
@@ -140,8 +140,8 @@ export async function startApiServer(port = 3000, createProvider) {
|
|
|
140
140
|
res.json({ status: 'running', provider: config.provider, model: config.model });
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
server.listen(port, () => {
|
|
144
|
-
console.log(chalk.green.bold(`\nš Banana Code API Server running at http
|
|
143
|
+
server.listen(port, host, () => {
|
|
144
|
+
console.log(chalk.green.bold(`\nš Banana Code API Server running at http://${host}:${port}`));
|
|
145
145
|
console.log(chalk.gray(`WebSocket streaming enabled on the same port.\n`));
|
|
146
146
|
});
|
|
147
147
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { requestPermission } from '../permissions.js';
|
|
4
|
+
|
|
5
|
+
export async function readManyFiles({ filepaths }) {
|
|
6
|
+
if (!Array.isArray(filepaths)) {
|
|
7
|
+
return 'Error: filepaths must be an array of strings.';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let results = [];
|
|
11
|
+
|
|
12
|
+
// Request permission for all files. You could ask for them individually or batch them.
|
|
13
|
+
// Here we'll request them individually but sequentially.
|
|
14
|
+
for (const filepath of filepaths) {
|
|
15
|
+
const absPath = path.resolve(process.cwd(), filepath);
|
|
16
|
+
const perm = await requestPermission('Read File', filepath);
|
|
17
|
+
|
|
18
|
+
if (!perm.allowed) {
|
|
19
|
+
results.push(`--- File: ${filepath} ---\nUser denied permission to read: ${filepath}\n`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(absPath, 'utf8');
|
|
25
|
+
results.push(`--- File: ${filepath} ---\n${content}\n`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
results.push(`--- File: ${filepath} ---\nError reading file: ${err.message}\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return results.join('\n');
|
|
32
|
+
}
|
package/src/tools/registry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execCommand } from './execCommand.js';
|
|
2
2
|
import { readFile } from './readFile.js';
|
|
3
|
+
import { readManyFiles } from './readManyFiles.js';
|
|
3
4
|
import { writeFile } from './writeFile.js';
|
|
4
5
|
import { fetchUrl } from './fetchUrl.js';
|
|
5
6
|
import { searchFiles } from './searchFiles.js';
|
|
@@ -36,6 +37,21 @@ export const TOOLS = [
|
|
|
36
37
|
required: ['filepath']
|
|
37
38
|
}
|
|
38
39
|
},
|
|
40
|
+
{
|
|
41
|
+
name: 'read_many_files',
|
|
42
|
+
description: 'Read the contents of multiple files at once.',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
filepaths: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: 'List of file paths to read',
|
|
49
|
+
items: { type: 'string' }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: ['filepaths']
|
|
53
|
+
}
|
|
54
|
+
},
|
|
39
55
|
{
|
|
40
56
|
name: 'write_file',
|
|
41
57
|
description: 'Write content to a file. Overwrites existing content.',
|
|
@@ -210,11 +226,15 @@ export const TOOLS = [
|
|
|
210
226
|
|
|
211
227
|
export function getAvailableTools(config = {}) {
|
|
212
228
|
let available = TOOLS.filter(tool => {
|
|
229
|
+
if (config.askMode) {
|
|
230
|
+
const forbiddenInAskMode = ['write_file', 'patch_file'];
|
|
231
|
+
if (forbiddenInAskMode.includes(tool.name)) return false;
|
|
232
|
+
}
|
|
213
233
|
if (tool.beta) {
|
|
214
234
|
return config.betaTools && config.betaTools.includes(tool.name);
|
|
215
235
|
}
|
|
216
236
|
if (tool.memoryFeature) {
|
|
217
|
-
return config.useMemory
|
|
237
|
+
return config.useMemory !== false;
|
|
218
238
|
}
|
|
219
239
|
if (tool.settingsFeature) {
|
|
220
240
|
// Default to true if not explicitly set to false
|
|
@@ -274,6 +294,7 @@ export async function executeTool(name, args, config) {
|
|
|
274
294
|
switch (name) {
|
|
275
295
|
case 'execute_command': return await execCommand(args);
|
|
276
296
|
case 'read_file': return await readFile(args);
|
|
297
|
+
case 'read_many_files': return await readManyFiles(args);
|
|
277
298
|
case 'write_file': return await writeFile(args);
|
|
278
299
|
case 'fetch_url': return await fetchUrl(args);
|
|
279
300
|
case 'search_files': return await searchFiles(args);
|
package/src.zip
ADDED
|
Binary file
|